mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-08-19 03:12:16 +00:00
added an assistant to view logs
This commit is contained in:
parent
5dc6282908
commit
11dd357a05
@ -1630,6 +1630,84 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LEGALCHECK::ASSISTANTLEGALCHECK::T4254597
|
|||||||
-- Ask your questions
|
-- Ask your questions
|
||||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LEGALCHECK::ASSISTANTLEGALCHECK::T467099852"] = "Ask your questions"
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LEGALCHECK::ASSISTANTLEGALCHECK::T467099852"] = "Ask your questions"
|
||||||
|
|
||||||
|
-- Find
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T1042076026"] = "Find"
|
||||||
|
|
||||||
|
-- The log file could not be read: {0}
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T1147062477"] = "The log file could not be read: {0}"
|
||||||
|
|
||||||
|
-- Refresh
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T135637716"] = "Refresh"
|
||||||
|
|
||||||
|
-- Log type
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T1649755707"] = "Log type"
|
||||||
|
|
||||||
|
-- Showing {0} of {1} loaded lines. {2} older lines were skipped. Last refresh: {3}.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T1747827400"] = "Showing {0} of {1} loaded lines. {2} older lines were skipped. Last refresh: {3}."
|
||||||
|
|
||||||
|
-- The log file does not exist: {0}
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T1807514273"] = "The log file does not exist: {0}"
|
||||||
|
|
||||||
|
-- Other
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T1849229205"] = "Other"
|
||||||
|
|
||||||
|
-- Max lines
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T1904230854"] = "Max lines"
|
||||||
|
|
||||||
|
-- All
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T1974461284"] = "All"
|
||||||
|
|
||||||
|
-- Startup log
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T2288538420"] = "Startup log"
|
||||||
|
|
||||||
|
-- Showing {0} of {1} lines. Last refresh: {2}.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T2378353570"] = "Showing {0} of {1} lines. Last refresh: {2}."
|
||||||
|
|
||||||
|
-- No matching log lines.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T2511997530"] = "No matching log lines."
|
||||||
|
|
||||||
|
-- Source details
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T2686813966"] = "Source details"
|
||||||
|
|
||||||
|
-- Loaded {0} lines. Last refresh: {1}.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T2920304709"] = "Loaded {0} lines. Last refresh: {1}."
|
||||||
|
|
||||||
|
-- Filter only
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T3152625639"] = "Filter only"
|
||||||
|
|
||||||
|
-- Loading log file...
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T333036481"] = "Loading log file..."
|
||||||
|
|
||||||
|
-- The log file path is not available yet.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T3686775689"] = "The log file path is not available yet."
|
||||||
|
|
||||||
|
-- Logger
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T376222229"] = "Logger"
|
||||||
|
|
||||||
|
-- Auto-refresh
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T3837203600"] = "Auto-refresh"
|
||||||
|
|
||||||
|
-- not loaded yet
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T3863250749"] = "not loaded yet"
|
||||||
|
|
||||||
|
-- Loading...
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T397479987"] = "Loading..."
|
||||||
|
|
||||||
|
-- Usage log
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T4031747274"] = "Usage log"
|
||||||
|
|
||||||
|
-- Log file
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T4129341821"] = "Log file"
|
||||||
|
|
||||||
|
-- Log Viewer
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T4130241777"] = "Log Viewer"
|
||||||
|
|
||||||
|
-- Show timestamps
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T469116133"] = "Show timestamps"
|
||||||
|
|
||||||
|
-- Clear
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T77955010"] = "Clear"
|
||||||
|
|
||||||
-- You can enter text, attach one or more documents, or use both. At least one input is required.
|
-- You can enter text, attach one or more documents, or use both. At least one input is required.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T1442535450"] = "You can enter text, attach one or more documents, or use both. At least one input is required."
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T1442535450"] = "You can enter text, attach one or more documents, or use both. At least one input is required."
|
||||||
|
|
||||||
@ -6502,6 +6580,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T3930052338"] = "Job Posting"
|
|||||||
-- Ask a question about a legal document.
|
-- Ask a question about a legal document.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T3970214537"] = "Ask a question about a legal document."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T3970214537"] = "Ask a question about a legal document."
|
||||||
|
|
||||||
|
-- Log Viewer
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T4130241777"] = "Log Viewer"
|
||||||
|
|
||||||
-- ERI Server
|
-- ERI Server
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T4204533420"] = "ERI Server"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T4204533420"] = "ERI Server"
|
||||||
|
|
||||||
@ -6523,6 +6604,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T782102948"] = "Bias of the Day"
|
|||||||
-- Learn about one cognitive bias every day.
|
-- Learn about one cognitive bias every day.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T878695986"] = "Learn about one cognitive bias every day."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T878695986"] = "Learn about one cognitive bias every day."
|
||||||
|
|
||||||
|
-- View and filter AI Studio log files.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T892147062"] = "View and filter AI Studio log files."
|
||||||
|
|
||||||
-- Localization
|
-- Localization
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T897888480"] = "Localization"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T897888480"] = "Localization"
|
||||||
|
|
||||||
@ -7723,6 +7807,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T4262148639"] = "Rewrite
|
|||||||
-- Localization Assistant
|
-- Localization Assistant
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T446674624"] = "Localization Assistant"
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T446674624"] = "Localization Assistant"
|
||||||
|
|
||||||
|
-- Log Viewer Assistant
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T555062689"] = "Log Viewer Assistant"
|
||||||
|
|
||||||
-- New Chat
|
-- New Chat
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T826248509"] = "New Chat"
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T826248509"] = "New Chat"
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,109 @@
|
|||||||
|
@attribute [Route(Routes.ASSISTANT_LOG_VIEWER)]
|
||||||
|
@using AIStudio.Tools.Services
|
||||||
|
@inherits MSGComponentBase
|
||||||
|
|
||||||
|
<div class="inner-scrolling-context">
|
||||||
|
<MudText Typo="Typo.h3" Class="mb-2 mr-3">
|
||||||
|
@T("Log Viewer")
|
||||||
|
</MudText>
|
||||||
|
|
||||||
|
<InnerScrolling FillEntireHorizontalSpace="@true" Class="log-viewer-shell">
|
||||||
|
<HeaderContent>
|
||||||
|
<MudStack Row="@true" Wrap="@Wrap.Wrap" AlignItems="@AlignItems.Center" Spacing="2" Class="mb-2">
|
||||||
|
<MudSelect T="LogFileKind" Value="@this.selectedLogFile" ValueChanged="@this.SelectedLogFileChanged" Label="@T("Log file")" Variant="Variant.Outlined" Margin="Margin.Dense" Class="log-viewer-select">
|
||||||
|
<MudSelectItem T="LogFileKind" Value="@LogFileKind.APP">@T("Usage log")</MudSelectItem>
|
||||||
|
<MudSelectItem T="LogFileKind" Value="@LogFileKind.STARTUP">@T("Startup log")</MudSelectItem>
|
||||||
|
</MudSelect>
|
||||||
|
<MudCheckBox T="bool" Value="@this.autoRefresh" ValueChanged="@this.AutoRefreshChanged" Label="@T("Auto-refresh")" Color="Color.Primary" />
|
||||||
|
@if (!this.autoRefresh)
|
||||||
|
{
|
||||||
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Refresh" OnClick="@this.RefreshLogAsync" Disabled="@this.isLoading">
|
||||||
|
@T("Refresh")
|
||||||
|
</MudButton>
|
||||||
|
}
|
||||||
|
<MudNumericField T="int" Value="@this.maxLines" ValueChanged="@this.MaxLinesChanged" Label="@T("Max lines")" Variant="Variant.Outlined" Margin="Margin.Dense" Min="@MIN_MAX_LINES" Max="@MAX_MAX_LINES" Step="500" Class="log-viewer-number" />
|
||||||
|
</MudStack>
|
||||||
|
|
||||||
|
<MudStack Row="@true" Wrap="@Wrap.Wrap" AlignItems="@AlignItems.Center" Spacing="2" Class="mb-1">
|
||||||
|
<MudSelect T="string" Label="@T("Log type")" MultiSelection="@true" SelectedValues="@this.selectedLogLevels" SelectedValuesChanged="@this.SelectedLogLevelsChanged" MultiSelectionTextFunc="@this.GetMultiSelectionText" Variant="Variant.Outlined" Margin="Margin.Dense" Class="log-viewer-multiselect" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Label">
|
||||||
|
@foreach (var option in this.logLevelOptions)
|
||||||
|
{
|
||||||
|
<MudSelectItem T="string" Value="@option">
|
||||||
|
@this.GetFilterOptionDisplay(option)
|
||||||
|
</MudSelectItem>
|
||||||
|
}
|
||||||
|
</MudSelect>
|
||||||
|
<MudSelect T="string" Label="@T("Logger")" MultiSelection="@true" SelectedValues="@this.selectedLoggers" SelectedValuesChanged="@this.SelectedLoggersChanged" MultiSelectionTextFunc="@this.GetMultiSelectionText" Variant="Variant.Outlined" Margin="Margin.Dense" Class="log-viewer-multiselect" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Source">
|
||||||
|
@foreach (var option in this.loggerOptions)
|
||||||
|
{
|
||||||
|
<MudSelectItem T="string" Value="@option">
|
||||||
|
@this.GetFilterOptionDisplay(option)
|
||||||
|
</MudSelectItem>
|
||||||
|
}
|
||||||
|
</MudSelect>
|
||||||
|
<MudSelect T="string" Label="@T("Source details")" MultiSelection="@true" SelectedValues="@this.selectedSourceDetails" SelectedValuesChanged="@this.SelectedSourceDetailsChanged" MultiSelectionTextFunc="@this.GetMultiSelectionText" Variant="Variant.Outlined" Margin="Margin.Dense" Class="log-viewer-multiselect" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Settings">
|
||||||
|
@foreach (var option in this.sourceDetailOptions)
|
||||||
|
{
|
||||||
|
<MudSelectItem T="string" Value="@option">
|
||||||
|
@this.GetFilterOptionDisplay(option)
|
||||||
|
</MudSelectItem>
|
||||||
|
}
|
||||||
|
</MudSelect>
|
||||||
|
<MudCheckBox T="bool" @bind-Value="@this.ShowTimestamps" Label="@T("Show timestamps")" Color="Color.Primary" />
|
||||||
|
</MudStack>
|
||||||
|
|
||||||
|
<MudStack Row="@true" Wrap="@Wrap.Wrap" AlignItems="@AlignItems.Center" Spacing="2" Class="mb-1">
|
||||||
|
<MudTextField T="string" @bind-Text="@this.FilterText" Immediate="@true" Label="@T("Find")" Variant="Variant.Outlined" Margin="Margin.Dense" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" Class="log-viewer-filter" />
|
||||||
|
<MudButton Variant="Variant.Text" Color="Color.Default" StartIcon="@Icons.Material.Filled.Clear" Disabled="@(!this.HasActiveFilter)" OnClick="@this.ClearFilters">
|
||||||
|
@T("Clear")
|
||||||
|
</MudButton>
|
||||||
|
<MudCheckBox T="bool" @bind-Value="@this.FilterOnly" Label="@T("Filter only")" Color="Color.Primary" />
|
||||||
|
</MudStack>
|
||||||
|
|
||||||
|
<MudText Typo="Typo.body2" Class="log-viewer-path mb-1">
|
||||||
|
@this.CurrentLogPath
|
||||||
|
</MudText>
|
||||||
|
<MudText Typo="Typo.caption" Class="mb-2">
|
||||||
|
@this.StatusText
|
||||||
|
</MudText>
|
||||||
|
</HeaderContent>
|
||||||
|
|
||||||
|
<ChildContent>
|
||||||
|
@if (!string.IsNullOrWhiteSpace(this.loadError))
|
||||||
|
{
|
||||||
|
<MudAlert Severity="Severity.Error" Dense="@true" Variant="Variant.Outlined" Class="mb-2">
|
||||||
|
@this.loadError
|
||||||
|
</MudAlert>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="log-viewer-pane">
|
||||||
|
@if (this.isLoading && this.loadedLines.Count == 0)
|
||||||
|
{
|
||||||
|
<div class="log-viewer-empty">
|
||||||
|
<MudProgressCircular Size="Size.Small" Indeterminate="@true" />
|
||||||
|
<MudText Typo="Typo.body2">@T("Loading log file...")</MudText>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (this.displayLines.Count == 0)
|
||||||
|
{
|
||||||
|
<div class="log-viewer-empty">
|
||||||
|
<MudIcon Icon="@Icons.Material.Filled.SearchOff" />
|
||||||
|
<MudText Typo="Typo.body2">@T("No matching log lines.")</MudText>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="log-viewer-lines" role="textbox" aria-readonly="true">
|
||||||
|
@foreach (var line in this.displayLines)
|
||||||
|
{
|
||||||
|
<div class="@this.GetLineClass(line)">
|
||||||
|
<span class="log-viewer-line-number">@line.Number</span>
|
||||||
|
<span class="log-viewer-line-text">@((MarkupString)this.RenderLine(line))</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</ChildContent>
|
||||||
|
</InnerScrolling>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,721 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Net;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
using AIStudio.Components;
|
||||||
|
using AIStudio.Tools.Rust;
|
||||||
|
using AIStudio.Tools.Services;
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
|
namespace AIStudio.Assistants.LogViewer;
|
||||||
|
|
||||||
|
public partial class AssistantLogViewer : MSGComponentBase
|
||||||
|
{
|
||||||
|
private static readonly TimeSpan AUTO_REFRESH_INTERVAL = TimeSpan.FromSeconds(5);
|
||||||
|
private static readonly char[] WORD_SPLIT_CHARS = [' ', '\t', '\r', '\n'];
|
||||||
|
private static readonly Dictionary<string, int> LOG_LEVEL_ORDER = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["ERROR"] = 0,
|
||||||
|
["CRITICAL"] = 1,
|
||||||
|
["WARN"] = 2,
|
||||||
|
["WARNING"] = 3,
|
||||||
|
["INFO"] = 4,
|
||||||
|
["INFORMATION"] = 5,
|
||||||
|
["DEBUG"] = 6,
|
||||||
|
["TRACE"] = 7,
|
||||||
|
};
|
||||||
|
|
||||||
|
private const int DEFAULT_MAX_LINES = 5000;
|
||||||
|
protected const int MIN_MAX_LINES = 100;
|
||||||
|
protected const int MAX_MAX_LINES = 100000;
|
||||||
|
private const string OTHER_OPTION_VALUE = "__OTHER__";
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private RustService RustService { get; init; } = null!;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private NavigationManager NavigationManager { get; init; } = null!;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private ILogger<AssistantLogViewer> Logger { get; init; } = null!;
|
||||||
|
|
||||||
|
private GetLogPathsResponse logPaths;
|
||||||
|
private LogFileKind selectedLogFile = LogFileKind.APP;
|
||||||
|
private List<LogLine> loadedLines = [];
|
||||||
|
private List<LogLine> displayLines = [];
|
||||||
|
private List<string> logLevelOptions = [OTHER_OPTION_VALUE];
|
||||||
|
private List<string> loggerOptions = [OTHER_OPTION_VALUE];
|
||||||
|
private List<string> sourceDetailOptions = [OTHER_OPTION_VALUE];
|
||||||
|
private HashSet<string> selectedLogLevels = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private HashSet<string> selectedLoggers = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private HashSet<string> selectedSourceDetails = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private string[] activeSearchTerms = [];
|
||||||
|
private CancellationTokenSource? autoRefreshCancellationTokenSource;
|
||||||
|
private string filterText = string.Empty;
|
||||||
|
private string loadError = string.Empty;
|
||||||
|
private bool isLoading;
|
||||||
|
private bool autoRefresh;
|
||||||
|
private bool filterOnly = true;
|
||||||
|
private bool showTimestamps = true;
|
||||||
|
private int maxLines = DEFAULT_MAX_LINES;
|
||||||
|
private int totalLineCount;
|
||||||
|
private int skippedLineCount;
|
||||||
|
private DateTimeOffset? lastLoadedAt;
|
||||||
|
|
||||||
|
private string CurrentLogPath => this.selectedLogFile is LogFileKind.APP ? this.logPaths.LogAppPath : this.logPaths.LogStartupPath;
|
||||||
|
|
||||||
|
private bool HasDropdownFilter => this.selectedLogLevels.Count > 0 || this.selectedLoggers.Count > 0 || this.selectedSourceDetails.Count > 0;
|
||||||
|
|
||||||
|
private bool HasActiveFilter => !string.IsNullOrWhiteSpace(this.filterText) || this.HasDropdownFilter;
|
||||||
|
|
||||||
|
private string FilterText
|
||||||
|
{
|
||||||
|
get => this.filterText;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (this.filterText == value)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.filterText = value;
|
||||||
|
this.RefreshDisplayLines();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool FilterOnly
|
||||||
|
{
|
||||||
|
get => this.filterOnly;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (this.filterOnly == value)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.filterOnly = value;
|
||||||
|
this.RefreshDisplayLines();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool ShowTimestamps
|
||||||
|
{
|
||||||
|
get => this.showTimestamps;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (this.showTimestamps == value)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.showTimestamps = value;
|
||||||
|
this.RefreshDisplayLines();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string StatusText
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (this.isLoading)
|
||||||
|
return T("Loading...");
|
||||||
|
|
||||||
|
var visibleLineCount = this.displayLines.Count.ToString("N0", CultureInfo.CurrentCulture);
|
||||||
|
var loadedLineCount = this.loadedLines.Count.ToString("N0", CultureInfo.CurrentCulture);
|
||||||
|
var totalLineCountText = this.totalLineCount.ToString("N0", CultureInfo.CurrentCulture);
|
||||||
|
var lastLoadedText = this.lastLoadedAt?.LocalDateTime.ToString("g", CultureInfo.CurrentCulture) ?? T("not loaded yet");
|
||||||
|
|
||||||
|
if (this.loadedLines.Count == 0)
|
||||||
|
return string.Format(T("Loaded {0} lines. Last refresh: {1}."), loadedLineCount, lastLoadedText);
|
||||||
|
|
||||||
|
if (this.skippedLineCount > 0)
|
||||||
|
{
|
||||||
|
var skippedLineCountText = this.skippedLineCount.ToString("N0", CultureInfo.CurrentCulture);
|
||||||
|
return string.Format(T("Showing {0} of {1} loaded lines. {2} older lines were skipped. Last refresh: {3}."), visibleLineCount, loadedLineCount, skippedLineCountText, lastLoadedText);
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Format(T("Showing {0} of {1} lines. Last refresh: {2}."), visibleLineCount, totalLineCountText, lastLoadedText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
await base.OnInitializedAsync();
|
||||||
|
|
||||||
|
if (!this.SettingsManager.IsAssistantVisible(Tools.Components.LOG_VIEWER_ASSISTANT, assistantName: T("Log Viewer")))
|
||||||
|
{
|
||||||
|
this.NavigationManager.NavigateTo(Routes.ASSISTANTS);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.RefreshLogAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void DisposeResources()
|
||||||
|
{
|
||||||
|
this.StopAutoRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SelectedLogFileChanged(LogFileKind value)
|
||||||
|
{
|
||||||
|
if (this.selectedLogFile == value)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.selectedLogFile = value;
|
||||||
|
await this.RefreshLogAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task SelectedLogLevelsChanged(IEnumerable<string?>? selectedValues)
|
||||||
|
{
|
||||||
|
UpdateSelectedValues(this.selectedLogLevels, selectedValues);
|
||||||
|
this.RefreshDisplayLines();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task SelectedLoggersChanged(IEnumerable<string?>? selectedValues)
|
||||||
|
{
|
||||||
|
UpdateSelectedValues(this.selectedLoggers, selectedValues);
|
||||||
|
this.RefreshDisplayLines();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task SelectedSourceDetailsChanged(IEnumerable<string?>? selectedValues)
|
||||||
|
{
|
||||||
|
UpdateSelectedValues(this.selectedSourceDetails, selectedValues);
|
||||||
|
this.RefreshDisplayLines();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AutoRefreshChanged(bool value)
|
||||||
|
{
|
||||||
|
this.autoRefresh = value;
|
||||||
|
|
||||||
|
if (this.autoRefresh)
|
||||||
|
this.StartAutoRefresh();
|
||||||
|
else
|
||||||
|
this.StopAutoRefresh();
|
||||||
|
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task MaxLinesChanged(int value)
|
||||||
|
{
|
||||||
|
var normalizedValue = Math.Clamp(value, MIN_MAX_LINES, MAX_MAX_LINES);
|
||||||
|
if (this.maxLines == normalizedValue)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.maxLines = normalizedValue;
|
||||||
|
await this.RefreshLogAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearFilters()
|
||||||
|
{
|
||||||
|
this.filterText = string.Empty;
|
||||||
|
this.selectedLogLevels.Clear();
|
||||||
|
this.selectedLoggers.Clear();
|
||||||
|
this.selectedSourceDetails.Clear();
|
||||||
|
this.RefreshDisplayLines();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RefreshLogAsync()
|
||||||
|
{
|
||||||
|
if (this.isLoading)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.isLoading = true;
|
||||||
|
this.loadError = string.Empty;
|
||||||
|
await this.InvokeAsync(this.StateHasChanged);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
this.logPaths = await this.RustService.GetLogPaths();
|
||||||
|
var path = this.CurrentLogPath;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(path))
|
||||||
|
{
|
||||||
|
this.loadedLines = [];
|
||||||
|
this.totalLineCount = 0;
|
||||||
|
this.skippedLineCount = 0;
|
||||||
|
this.lastLoadedAt = null;
|
||||||
|
this.loadError = T("The log file path is not available yet.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!File.Exists(path))
|
||||||
|
{
|
||||||
|
this.loadedLines = [];
|
||||||
|
this.totalLineCount = 0;
|
||||||
|
this.skippedLineCount = 0;
|
||||||
|
this.lastLoadedAt = null;
|
||||||
|
this.loadError = string.Format(T("The log file does not exist: {0}"), path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var snapshot = await ReadLogSnapshotAsync(path, this.maxLines);
|
||||||
|
this.loadedLines = snapshot.Lines;
|
||||||
|
this.totalLineCount = snapshot.TotalLineCount;
|
||||||
|
this.skippedLineCount = snapshot.SkippedLineCount;
|
||||||
|
this.lastLoadedAt = DateTimeOffset.Now;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
this.Logger.LogWarning(e, "Could not read the log file for the log viewer assistant.");
|
||||||
|
this.loadedLines = [];
|
||||||
|
this.totalLineCount = 0;
|
||||||
|
this.skippedLineCount = 0;
|
||||||
|
this.lastLoadedAt = null;
|
||||||
|
this.loadError = string.Format(T("The log file could not be read: {0}"), e.Message);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
this.isLoading = false;
|
||||||
|
this.RebuildFilterOptions();
|
||||||
|
this.RefreshDisplayLines();
|
||||||
|
await this.InvokeAsync(this.StateHasChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<LogSnapshot> ReadLogSnapshotAsync(string path, int maxLines)
|
||||||
|
{
|
||||||
|
var queue = new Queue<string>(Math.Min(maxLines, 4096));
|
||||||
|
var totalLineCount = 0;
|
||||||
|
var skippedLineCount = 0;
|
||||||
|
|
||||||
|
await using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete, 65536, true);
|
||||||
|
using var reader = new StreamReader(stream, Encoding.UTF8, true);
|
||||||
|
|
||||||
|
while (await reader.ReadLineAsync() is { } line)
|
||||||
|
{
|
||||||
|
totalLineCount++;
|
||||||
|
queue.Enqueue(line);
|
||||||
|
|
||||||
|
if (queue.Count <= maxLines)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
queue.Dequeue();
|
||||||
|
skippedLineCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
var firstLineNumber = skippedLineCount + 1;
|
||||||
|
var lines = queue
|
||||||
|
.Select((line, index) => new LogLine(firstLineNumber + index, line, ParseLogSegments(line)))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return new(lines, totalLineCount, skippedLineCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RebuildFilterOptions()
|
||||||
|
{
|
||||||
|
this.logLevelOptions = BuildFilterOptions(this.loadedLines.Select(line => line.Segments.Level), CompareLogLevels);
|
||||||
|
this.loggerOptions = BuildFilterOptions(this.loadedLines.Select(line => line.Segments.Logger), (left, right) => StringComparer.OrdinalIgnoreCase.Compare(left, right));
|
||||||
|
this.sourceDetailOptions = BuildFilterOptions(this.loadedLines.Select(line => line.Segments.SourceDetails), (left, right) => StringComparer.OrdinalIgnoreCase.Compare(left, right));
|
||||||
|
|
||||||
|
NormalizeSelectedValues(this.selectedLogLevels, this.logLevelOptions);
|
||||||
|
NormalizeSelectedValues(this.selectedLoggers, this.loggerOptions);
|
||||||
|
NormalizeSelectedValues(this.selectedSourceDetails, this.sourceDetailOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshDisplayLines()
|
||||||
|
{
|
||||||
|
this.activeSearchTerms = BuildSearchTerms(this.filterText);
|
||||||
|
this.displayLines = this.loadedLines
|
||||||
|
.Where(this.LineMatchesFilters)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool LineMatchesFilters(LogLine line)
|
||||||
|
{
|
||||||
|
if (!MatchesSelection(line.Segments.Level, this.selectedLogLevels))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!MatchesSelection(line.Segments.Logger, this.selectedLoggers))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!MatchesSelection(line.Segments.SourceDetails, this.selectedSourceDetails))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!this.filterOnly || this.activeSearchTerms.Length == 0)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return MatchesSearchTerms(this.GetPlainRenderedLine(line), this.activeSearchTerms);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string RenderLine(LogLine line)
|
||||||
|
{
|
||||||
|
var text = this.GetPlainRenderedLine(line);
|
||||||
|
var ranges = new List<HighlightRange>();
|
||||||
|
AddSearchTermRanges(text, this.activeSearchTerms, ranges);
|
||||||
|
|
||||||
|
if (ranges.Count == 0)
|
||||||
|
return WebUtility.HtmlEncode(text);
|
||||||
|
|
||||||
|
ranges = MergeRanges(ranges);
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
var position = 0;
|
||||||
|
|
||||||
|
foreach (var range in ranges)
|
||||||
|
{
|
||||||
|
AppendEncoded(sb, text, position, range.Start - position);
|
||||||
|
sb.Append("""<mark class="log-viewer-highlight">""");
|
||||||
|
AppendEncoded(sb, text, range.Start, range.Length);
|
||||||
|
sb.Append("</mark>");
|
||||||
|
position = range.Start + range.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
AppendEncoded(sb, text, position, text.Length - position);
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetPlainRenderedLine(LogLine line)
|
||||||
|
{
|
||||||
|
var parts = new List<string>();
|
||||||
|
var segments = line.Segments;
|
||||||
|
|
||||||
|
if (this.showTimestamps && !string.IsNullOrWhiteSpace(segments.Timestamp))
|
||||||
|
parts.Add(segments.Timestamp);
|
||||||
|
|
||||||
|
if (!ShouldHideSelectedSegment(segments.Level, this.selectedLogLevels))
|
||||||
|
AddIfNotWhiteSpace(parts, segments.Level);
|
||||||
|
|
||||||
|
if (!ShouldHideSelectedSegment(segments.Logger, this.selectedLoggers))
|
||||||
|
AddIfNotWhiteSpace(parts, segments.Logger);
|
||||||
|
|
||||||
|
if (!ShouldHideSelectedSegment(segments.SourceDetails, this.selectedSourceDetails))
|
||||||
|
AddIfNotWhiteSpace(parts, segments.SourceDetails);
|
||||||
|
|
||||||
|
AddIfNotWhiteSpace(parts, segments.Message);
|
||||||
|
|
||||||
|
return parts.Count == 0 ? string.Empty : string.Join(" ", parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetLineClass(LogLine line)
|
||||||
|
{
|
||||||
|
var level = line.Segments.Level ?? string.Empty;
|
||||||
|
|
||||||
|
if (level.Contains("ERROR", StringComparison.OrdinalIgnoreCase) || level.Contains("CRITICAL", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return "log-viewer-line log-viewer-line-error";
|
||||||
|
|
||||||
|
if (level.Contains("WARN", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return "log-viewer-line log-viewer-line-warn";
|
||||||
|
|
||||||
|
if (level.Contains("DEBUG", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return "log-viewer-line log-viewer-line-debug";
|
||||||
|
|
||||||
|
if (level.Contains("TRACE", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return "log-viewer-line log-viewer-line-trace";
|
||||||
|
|
||||||
|
return "log-viewer-line";
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetFilterOptionDisplay(string value)
|
||||||
|
{
|
||||||
|
return value == OTHER_OPTION_VALUE ? T("Other") : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetMultiSelectionText(List<string?>? selectedValues)
|
||||||
|
{
|
||||||
|
if (selectedValues is null || selectedValues.Count == 0)
|
||||||
|
return T("All");
|
||||||
|
|
||||||
|
var selectedLabels = selectedValues
|
||||||
|
.Where(value => !string.IsNullOrWhiteSpace(value))
|
||||||
|
.Select(value => this.GetFilterOptionDisplay(value!))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return selectedLabels.Count == 0 ? T("All") : string.Join(", ", selectedLabels);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StartAutoRefresh()
|
||||||
|
{
|
||||||
|
this.StopAutoRefresh();
|
||||||
|
this.autoRefreshCancellationTokenSource = new CancellationTokenSource();
|
||||||
|
_ = this.AutoRefreshLoopAsync(this.autoRefreshCancellationTokenSource.Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StopAutoRefresh()
|
||||||
|
{
|
||||||
|
this.autoRefreshCancellationTokenSource?.Cancel();
|
||||||
|
this.autoRefreshCancellationTokenSource?.Dispose();
|
||||||
|
this.autoRefreshCancellationTokenSource = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AutoRefreshLoopAsync(CancellationToken token)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var timer = new PeriodicTimer(AUTO_REFRESH_INTERVAL);
|
||||||
|
while (await timer.WaitForNextTickAsync(token))
|
||||||
|
await this.InvokeAsync(this.RefreshLogAsync);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LogSegments ParseLogSegments(string line)
|
||||||
|
{
|
||||||
|
var index = 0;
|
||||||
|
var parsedAnySegment = false;
|
||||||
|
string? timestamp = null;
|
||||||
|
string? level = null;
|
||||||
|
string? logger = null;
|
||||||
|
string? sourceDetails = null;
|
||||||
|
|
||||||
|
if (TryReadBracket(line, index, out var bracket, out var content, out var nextIndex) && IsTimestamp(content))
|
||||||
|
{
|
||||||
|
timestamp = bracket;
|
||||||
|
index = nextIndex;
|
||||||
|
parsedAnySegment = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var candidateIndex = SkipWhiteSpace(line, index);
|
||||||
|
if (TryReadLogLevel(line, candidateIndex, out var detectedLevel, out nextIndex))
|
||||||
|
{
|
||||||
|
level = detectedLevel;
|
||||||
|
index = nextIndex;
|
||||||
|
parsedAnySegment = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
candidateIndex = SkipWhiteSpace(line, index);
|
||||||
|
if (TryReadBracket(line, candidateIndex, out bracket, out content, out nextIndex))
|
||||||
|
{
|
||||||
|
if (IsSourceDetails(content))
|
||||||
|
{
|
||||||
|
sourceDetails = bracket;
|
||||||
|
index = nextIndex;
|
||||||
|
parsedAnySegment = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger = bracket;
|
||||||
|
index = nextIndex;
|
||||||
|
parsedAnySegment = true;
|
||||||
|
|
||||||
|
candidateIndex = SkipWhiteSpace(line, index);
|
||||||
|
if (TryReadBracket(line, candidateIndex, out bracket, out content, out nextIndex) && IsSourceDetails(content))
|
||||||
|
{
|
||||||
|
sourceDetails = bracket;
|
||||||
|
index = nextIndex;
|
||||||
|
parsedAnySegment = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var message = parsedAnySegment ? ReadMessage(line, index) : line;
|
||||||
|
return new(timestamp, level, logger, sourceDetails, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadBracket(string text, int start, out string bracket, out string content, out int nextIndex)
|
||||||
|
{
|
||||||
|
bracket = string.Empty;
|
||||||
|
content = string.Empty;
|
||||||
|
nextIndex = start;
|
||||||
|
|
||||||
|
if (start >= text.Length || text[start] != '[')
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var end = text.IndexOf(']', start + 1);
|
||||||
|
if (end < 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
bracket = text[start..(end + 1)];
|
||||||
|
content = text[(start + 1)..end];
|
||||||
|
nextIndex = end + 1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadLogLevel(string text, int start, out string level, out int nextIndex)
|
||||||
|
{
|
||||||
|
level = string.Empty;
|
||||||
|
nextIndex = start;
|
||||||
|
|
||||||
|
if (start >= text.Length || text[start] == '[')
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var end = start;
|
||||||
|
while (end < text.Length && !char.IsWhiteSpace(text[end]))
|
||||||
|
end++;
|
||||||
|
|
||||||
|
if (end == start)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var candidate = text[start..end];
|
||||||
|
if (candidate.Length > 20 || candidate.Any(character => !char.IsLetter(character)))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var afterCandidate = SkipWhiteSpace(text, end);
|
||||||
|
if (afterCandidate >= text.Length || text[afterCandidate] != '[')
|
||||||
|
return false;
|
||||||
|
|
||||||
|
level = candidate;
|
||||||
|
nextIndex = end;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsTimestamp(string content)
|
||||||
|
{
|
||||||
|
return DateTimeOffset.TryParse(content, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsSourceDetails(string content)
|
||||||
|
{
|
||||||
|
return content.Contains("=", StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SkipWhiteSpace(string text, int start)
|
||||||
|
{
|
||||||
|
var index = start;
|
||||||
|
while (index < text.Length && char.IsWhiteSpace(text[index]))
|
||||||
|
index++;
|
||||||
|
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ReadMessage(string text, int start)
|
||||||
|
{
|
||||||
|
if (start >= text.Length)
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
if (char.IsWhiteSpace(text[start]))
|
||||||
|
start++;
|
||||||
|
|
||||||
|
return start >= text.Length ? string.Empty : text[start..];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<string> BuildFilterOptions(IEnumerable<string?> values, Comparison<string> comparison)
|
||||||
|
{
|
||||||
|
var options = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var value in values)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
options.TryAdd(value, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
var sortedOptions = options.Values.ToList();
|
||||||
|
sortedOptions.Sort(comparison);
|
||||||
|
sortedOptions.Add(OTHER_OPTION_VALUE);
|
||||||
|
return sortedOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int CompareLogLevels(string left, string right)
|
||||||
|
{
|
||||||
|
var leftRank = LOG_LEVEL_ORDER.GetValueOrDefault(left, int.MaxValue);
|
||||||
|
var rightRank = LOG_LEVEL_ORDER.GetValueOrDefault(right, int.MaxValue);
|
||||||
|
var rankComparison = leftRank.CompareTo(rightRank);
|
||||||
|
return rankComparison != 0 ? rankComparison : StringComparer.OrdinalIgnoreCase.Compare(left, right);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void NormalizeSelectedValues(HashSet<string> selectedValues, List<string> options)
|
||||||
|
{
|
||||||
|
var validOptions = options.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
|
selectedValues.RemoveWhere(value => !validOptions.Contains(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void UpdateSelectedValues(HashSet<string> target, IEnumerable<string?>? selectedValues)
|
||||||
|
{
|
||||||
|
target.Clear();
|
||||||
|
if (selectedValues is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (var value in selectedValues)
|
||||||
|
if (!string.IsNullOrWhiteSpace(value))
|
||||||
|
target.Add(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool MatchesSelection(string? value, HashSet<string> selectedValues)
|
||||||
|
{
|
||||||
|
if (selectedValues.Count == 0)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
var normalizedValue = string.IsNullOrWhiteSpace(value) ? OTHER_OPTION_VALUE : value;
|
||||||
|
return selectedValues.Contains(normalizedValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ShouldHideSelectedSegment(string? value, HashSet<string> selectedValues)
|
||||||
|
{
|
||||||
|
return selectedValues.Count == 1 && !string.IsNullOrWhiteSpace(value) && selectedValues.Contains(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string[] BuildSearchTerms(string text)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
|
return [];
|
||||||
|
|
||||||
|
return text
|
||||||
|
.Split(WORD_SPLIT_CHARS, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool MatchesSearchTerms(string text, string[] terms)
|
||||||
|
{
|
||||||
|
return terms.Length == 0 || terms.Any(term => text.Contains(term, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddSearchTermRanges(string text, string[] terms, List<HighlightRange> ranges)
|
||||||
|
{
|
||||||
|
foreach (var term in terms)
|
||||||
|
AddLiteralRanges(text, term, ranges);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddLiteralRanges(string line, string value, List<HighlightRange> ranges)
|
||||||
|
{
|
||||||
|
var index = 0;
|
||||||
|
while ((index = line.IndexOf(value, index, StringComparison.OrdinalIgnoreCase)) >= 0)
|
||||||
|
{
|
||||||
|
ranges.Add(new(index, value.Length));
|
||||||
|
index += value.Length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<HighlightRange> MergeRanges(List<HighlightRange> ranges)
|
||||||
|
{
|
||||||
|
var mergedRanges = new List<HighlightRange>();
|
||||||
|
foreach (var range in ranges.OrderBy(x => x.Start).ThenByDescending(x => x.Length))
|
||||||
|
{
|
||||||
|
if (mergedRanges.Count == 0)
|
||||||
|
{
|
||||||
|
mergedRanges.Add(range);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var previous = mergedRanges[^1];
|
||||||
|
var previousEnd = previous.Start + previous.Length;
|
||||||
|
var currentEnd = range.Start + range.Length;
|
||||||
|
if (range.Start <= previousEnd)
|
||||||
|
{
|
||||||
|
mergedRanges[^1] = previous with { Length = Math.Max(previousEnd, currentEnd) - previous.Start };
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
mergedRanges.Add(range);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mergedRanges;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AppendEncoded(StringBuilder sb, string value, int start, int length)
|
||||||
|
{
|
||||||
|
if (length <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
sb.Append(WebUtility.HtmlEncode(value.Substring(start, length)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddIfNotWhiteSpace(List<string> parts, string? value)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(value))
|
||||||
|
parts.Add(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly record struct LogLine(int Number, string Text, LogSegments Segments);
|
||||||
|
|
||||||
|
private readonly record struct LogSegments(string? Timestamp, string? Level, string? Logger, string? SourceDetails, string Message);
|
||||||
|
|
||||||
|
private readonly record struct LogSnapshot(List<LogLine> Lines, int TotalLineCount, int SkippedLineCount);
|
||||||
|
|
||||||
|
private readonly record struct HighlightRange(int Start, int Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum LogFileKind
|
||||||
|
{
|
||||||
|
APP,
|
||||||
|
STARTUP,
|
||||||
|
}
|
||||||
@ -101,7 +101,8 @@
|
|||||||
|
|
||||||
@if (this.SettingsManager.IsAnyCategoryAssistantVisible("Software Engineering",
|
@if (this.SettingsManager.IsAnyCategoryAssistantVisible("Software Engineering",
|
||||||
(Components.CODING_ASSISTANT, PreviewFeatures.NONE),
|
(Components.CODING_ASSISTANT, PreviewFeatures.NONE),
|
||||||
(Components.ERI_ASSISTANT, PreviewFeatures.PRE_RAG_2024)
|
(Components.ERI_ASSISTANT, PreviewFeatures.PRE_RAG_2024),
|
||||||
|
(Components.LOG_VIEWER_ASSISTANT, PreviewFeatures.NONE)
|
||||||
))
|
))
|
||||||
{
|
{
|
||||||
<MudText Typo="Typo.h4" Class="mb-2 mr-3 mt-6">
|
<MudText Typo="Typo.h4" Class="mb-2 mr-3 mt-6">
|
||||||
@ -110,6 +111,7 @@
|
|||||||
<MudStack Row="@true" Wrap="@Wrap.Wrap" Class="mb-3">
|
<MudStack Row="@true" Wrap="@Wrap.Wrap" Class="mb-3">
|
||||||
<AssistantBlock TSettings="SettingsDialogCoding" Component="Components.CODING_ASSISTANT" Name="@T("Coding")" Description="@T("Get coding and debugging support from an LLM.")" Icon="@Icons.Material.Filled.Code" Link="@Routes.ASSISTANT_CODING"/>
|
<AssistantBlock TSettings="SettingsDialogCoding" Component="Components.CODING_ASSISTANT" Name="@T("Coding")" Description="@T("Get coding and debugging support from an LLM.")" Icon="@Icons.Material.Filled.Code" Link="@Routes.ASSISTANT_CODING"/>
|
||||||
<AssistantBlock TSettings="SettingsDialogERIServer" Component="Components.ERI_ASSISTANT" RequiredPreviewFeature="PreviewFeatures.PRE_RAG_2024" Name="@T("ERI Server")" Description="@T("Generate an ERI server to integrate business systems.")" Icon="@Icons.Material.Filled.PrivateConnectivity" Link="@Routes.ASSISTANT_ERI"/>
|
<AssistantBlock TSettings="SettingsDialogERIServer" Component="Components.ERI_ASSISTANT" RequiredPreviewFeature="PreviewFeatures.PRE_RAG_2024" Name="@T("ERI Server")" Description="@T("Generate an ERI server to integrate business systems.")" Icon="@Icons.Material.Filled.PrivateConnectivity" Link="@Routes.ASSISTANT_ERI"/>
|
||||||
|
<AssistantBlock TSettings="NoSettingsPanel" Component="Components.LOG_VIEWER_ASSISTANT" Name="@T("Log Viewer")" Description="@T("View and filter AI Studio log files.")" Icon="@Icons.Material.Filled.Article" Link="@Routes.ASSISTANT_LOG_VIEWER"/>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -319,7 +319,8 @@ CONFIG["SETTINGS"] = {}
|
|||||||
-- CODING_ASSISTANT, TEXT_SUMMARIZER_ASSISTANT, EMAIL_ASSISTANT,
|
-- CODING_ASSISTANT, TEXT_SUMMARIZER_ASSISTANT, EMAIL_ASSISTANT,
|
||||||
-- LEGAL_CHECK_ASSISTANT, SYNONYMS_ASSISTANT, MY_TASKS_ASSISTANT,
|
-- LEGAL_CHECK_ASSISTANT, SYNONYMS_ASSISTANT, MY_TASKS_ASSISTANT,
|
||||||
-- JOB_POSTING_ASSISTANT, BIAS_DAY_ASSISTANT, ERI_ASSISTANT,
|
-- JOB_POSTING_ASSISTANT, BIAS_DAY_ASSISTANT, ERI_ASSISTANT,
|
||||||
-- DOCUMENT_ANALYSIS_ASSISTANT, SLIDE_BUILDER_ASSISTANT, I18N_ASSISTANT
|
-- DOCUMENT_ANALYSIS_ASSISTANT, SLIDE_BUILDER_ASSISTANT, I18N_ASSISTANT,
|
||||||
|
-- LOG_VIEWER_ASSISTANT
|
||||||
-- CONFIG["SETTINGS"]["DataApp.HiddenAssistants"] = { "ERI_ASSISTANT", "I18N_ASSISTANT" }
|
-- CONFIG["SETTINGS"]["DataApp.HiddenAssistants"] = { "ERI_ASSISTANT", "I18N_ASSISTANT" }
|
||||||
|
|
||||||
-- Configure enterprise approvals for assistant plugins.
|
-- Configure enterprise approvals for assistant plugins.
|
||||||
|
|||||||
@ -1632,6 +1632,84 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LEGALCHECK::ASSISTANTLEGALCHECK::T4254597
|
|||||||
-- Ask your questions
|
-- Ask your questions
|
||||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LEGALCHECK::ASSISTANTLEGALCHECK::T467099852"] = "Stellen Sie ihre Fragen"
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LEGALCHECK::ASSISTANTLEGALCHECK::T467099852"] = "Stellen Sie ihre Fragen"
|
||||||
|
|
||||||
|
-- Find
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T1042076026"] = "Suchen"
|
||||||
|
|
||||||
|
-- The log file could not be read: {0}
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T1147062477"] = "Die Protokolldatei konnte nicht gelesen werden: {0}"
|
||||||
|
|
||||||
|
-- Refresh
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T135637716"] = "Aktualisieren"
|
||||||
|
|
||||||
|
-- Log type
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T1649755707"] = "Protokolltyp"
|
||||||
|
|
||||||
|
-- Showing {0} of {1} loaded lines. {2} older lines were skipped. Last refresh: {3}.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T1747827400"] = "Anzeige von {0} von {1} geladenen Zeilen. {2} ältere Zeilen wurden übersprungen. Letzte Aktualisierung: {3}."
|
||||||
|
|
||||||
|
-- The log file does not exist: {0}
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T1807514273"] = "Die Protokolldatei existiert nicht: {0}"
|
||||||
|
|
||||||
|
-- Other
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T1849229205"] = "Andere"
|
||||||
|
|
||||||
|
-- Max lines
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T1904230854"] = "Max. Zeilen"
|
||||||
|
|
||||||
|
-- All
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T1974461284"] = "Alle"
|
||||||
|
|
||||||
|
-- Startup log
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T2288538420"] = "Startprotokoll"
|
||||||
|
|
||||||
|
-- Showing {0} of {1} lines. Last refresh: {2}.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T2378353570"] = "Anzeige von {0} von {1} Zeilen. Letzte Aktualisierung: {2}."
|
||||||
|
|
||||||
|
-- No matching log lines.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T2511997530"] = "Keine passenden Protokollzeilen."
|
||||||
|
|
||||||
|
-- Source details
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T2686813966"] = "Quellendetails"
|
||||||
|
|
||||||
|
-- Loaded {0} lines. Last refresh: {1}.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T2920304709"] = "{0} Zeilen geladen. Letzte Aktualisierung: {1}."
|
||||||
|
|
||||||
|
-- Filter only
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T3152625639"] = "Nur filtern"
|
||||||
|
|
||||||
|
-- Loading log file...
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T333036481"] = "Lade Protokolldatei..."
|
||||||
|
|
||||||
|
-- The log file path is not available yet.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T3686775689"] = "Der Pfad zur Protokolldatei ist noch nicht verfügbar."
|
||||||
|
|
||||||
|
-- Logger
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T376222229"] = "Protokoll"
|
||||||
|
|
||||||
|
-- Auto-refresh
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T3837203600"] = "Automatisch aktualisieren"
|
||||||
|
|
||||||
|
-- not loaded yet
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T3863250749"] = "noch nicht geladen"
|
||||||
|
|
||||||
|
-- Loading...
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T397479987"] = "Wird geladen..."
|
||||||
|
|
||||||
|
-- Usage log
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T4031747274"] = "Nutzungsprotokoll"
|
||||||
|
|
||||||
|
-- Log file
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T4129341821"] = "Protokolldatei"
|
||||||
|
|
||||||
|
-- Log Viewer
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T4130241777"] = "Protokollanzeige"
|
||||||
|
|
||||||
|
-- Show timestamps
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T469116133"] = "Zeitstempel anzeigen"
|
||||||
|
|
||||||
|
-- Clear
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T77955010"] = "Löschen"
|
||||||
|
|
||||||
-- You can enter text, attach one or more documents, or use both. At least one input is required.
|
-- You can enter text, attach one or more documents, or use both. At least one input is required.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T1442535450"] = "Sie können Text eingeben, ein oder mehrere Dokumente anhängen oder beides verwenden. Mindestens eine Eingabe ist erforderlich."
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T1442535450"] = "Sie können Text eingeben, ein oder mehrere Dokumente anhängen oder beides verwenden. Mindestens eine Eingabe ist erforderlich."
|
||||||
|
|
||||||
@ -6504,6 +6582,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T3930052338"] = "Stellenanzeige"
|
|||||||
-- Ask a question about a legal document.
|
-- Ask a question about a legal document.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T3970214537"] = "Stellen Sie Fragen zu einem juristischen Dokument."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T3970214537"] = "Stellen Sie Fragen zu einem juristischen Dokument."
|
||||||
|
|
||||||
|
-- Log Viewer
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T4130241777"] = "Protokollanzeige"
|
||||||
|
|
||||||
-- ERI Server
|
-- ERI Server
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T4204533420"] = "ERI-Server"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T4204533420"] = "ERI-Server"
|
||||||
|
|
||||||
@ -6525,6 +6606,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T782102948"] = "Vorurteil des Tage
|
|||||||
-- Learn about one cognitive bias every day.
|
-- Learn about one cognitive bias every day.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T878695986"] = "Lerne jeden Tag einen kognitiven Bias kennen."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T878695986"] = "Lerne jeden Tag einen kognitiven Bias kennen."
|
||||||
|
|
||||||
|
-- View and filter AI Studio log files.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T892147062"] = "AI Studio-Protokolldateien anzeigen und filtern."
|
||||||
|
|
||||||
-- Localization
|
-- Localization
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T897888480"] = "Lokalisierung"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T897888480"] = "Lokalisierung"
|
||||||
|
|
||||||
@ -7045,7 +7129,7 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T864851737"] = "Axum wird verwend
|
|||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T870640199"] = "Für einige Datenübertragungen müssen wir die Daten in Base64 kodieren. Diese Rust-Bibliothek eignet sich dafür hervorragend."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T870640199"] = "Für einige Datenübertragungen müssen wir die Daten in Base64 kodieren. Diese Rust-Bibliothek eignet sich dafür hervorragend."
|
||||||
|
|
||||||
-- How to update
|
-- How to update
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T906183311"] = "Update-Anleitung "
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T906183311"] = "Update-Anleitung"
|
||||||
|
|
||||||
-- Install Pandoc
|
-- Install Pandoc
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T986578435"] = "Pandoc installieren"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T986578435"] = "Pandoc installieren"
|
||||||
@ -7725,6 +7809,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T4262148639"] = "Umformu
|
|||||||
-- Localization Assistant
|
-- Localization Assistant
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T446674624"] = "Lokalisierungs-Assistent"
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T446674624"] = "Lokalisierungs-Assistent"
|
||||||
|
|
||||||
|
-- Log Viewer Assistant
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T555062689"] = "Assistent für die Protokollanzeige"
|
||||||
|
|
||||||
-- New Chat
|
-- New Chat
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T826248509"] = "Neuer Chat"
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T826248509"] = "Neuer Chat"
|
||||||
|
|
||||||
|
|||||||
@ -1632,6 +1632,84 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LEGALCHECK::ASSISTANTLEGALCHECK::T4254597
|
|||||||
-- Ask your questions
|
-- Ask your questions
|
||||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LEGALCHECK::ASSISTANTLEGALCHECK::T467099852"] = "Ask your questions"
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LEGALCHECK::ASSISTANTLEGALCHECK::T467099852"] = "Ask your questions"
|
||||||
|
|
||||||
|
-- Find
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T1042076026"] = "Find"
|
||||||
|
|
||||||
|
-- The log file could not be read: {0}
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T1147062477"] = "The log file could not be read: {0}"
|
||||||
|
|
||||||
|
-- Refresh
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T135637716"] = "Refresh"
|
||||||
|
|
||||||
|
-- Log type
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T1649755707"] = "Log type"
|
||||||
|
|
||||||
|
-- Showing {0} of {1} loaded lines. {2} older lines were skipped. Last refresh: {3}.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T1747827400"] = "Showing {0} of {1} loaded lines. {2} older lines were skipped. Last refresh: {3}."
|
||||||
|
|
||||||
|
-- The log file does not exist: {0}
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T1807514273"] = "The log file does not exist: {0}"
|
||||||
|
|
||||||
|
-- Other
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T1849229205"] = "Other"
|
||||||
|
|
||||||
|
-- Max lines
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T1904230854"] = "Max lines"
|
||||||
|
|
||||||
|
-- All
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T1974461284"] = "All"
|
||||||
|
|
||||||
|
-- Startup log
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T2288538420"] = "Startup log"
|
||||||
|
|
||||||
|
-- Showing {0} of {1} lines. Last refresh: {2}.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T2378353570"] = "Showing {0} of {1} lines. Last refresh: {2}."
|
||||||
|
|
||||||
|
-- No matching log lines.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T2511997530"] = "No matching log lines."
|
||||||
|
|
||||||
|
-- Source details
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T2686813966"] = "Source details"
|
||||||
|
|
||||||
|
-- Loaded {0} lines. Last refresh: {1}.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T2920304709"] = "Loaded {0} lines. Last refresh: {1}."
|
||||||
|
|
||||||
|
-- Filter only
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T3152625639"] = "Filter only"
|
||||||
|
|
||||||
|
-- Loading log file...
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T333036481"] = "Loading log file..."
|
||||||
|
|
||||||
|
-- The log file path is not available yet.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T3686775689"] = "The log file path is not available yet."
|
||||||
|
|
||||||
|
-- Logger
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T376222229"] = "Logger"
|
||||||
|
|
||||||
|
-- Auto-refresh
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T3837203600"] = "Auto-refresh"
|
||||||
|
|
||||||
|
-- not loaded yet
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T3863250749"] = "not loaded yet"
|
||||||
|
|
||||||
|
-- Loading...
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T397479987"] = "Loading..."
|
||||||
|
|
||||||
|
-- Usage log
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T4031747274"] = "Usage log"
|
||||||
|
|
||||||
|
-- Log file
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T4129341821"] = "Log file"
|
||||||
|
|
||||||
|
-- Log Viewer
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T4130241777"] = "Log Viewer"
|
||||||
|
|
||||||
|
-- Show timestamps
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T469116133"] = "Show timestamps"
|
||||||
|
|
||||||
|
-- Clear
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T77955010"] = "Clear"
|
||||||
|
|
||||||
-- You can enter text, attach one or more documents, or use both. At least one input is required.
|
-- You can enter text, attach one or more documents, or use both. At least one input is required.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T1442535450"] = "You can enter text, attach one or more documents, or use both. At least one input is required."
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T1442535450"] = "You can enter text, attach one or more documents, or use both. At least one input is required."
|
||||||
|
|
||||||
@ -6504,6 +6582,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T3930052338"] = "Job Posting"
|
|||||||
-- Ask a question about a legal document.
|
-- Ask a question about a legal document.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T3970214537"] = "Ask a question about a legal document."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T3970214537"] = "Ask a question about a legal document."
|
||||||
|
|
||||||
|
-- Log Viewer
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T4130241777"] = "Log Viewer"
|
||||||
|
|
||||||
-- ERI Server
|
-- ERI Server
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T4204533420"] = "ERI Server"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T4204533420"] = "ERI Server"
|
||||||
|
|
||||||
@ -6525,6 +6606,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T782102948"] = "Bias of the Day"
|
|||||||
-- Learn about one cognitive bias every day.
|
-- Learn about one cognitive bias every day.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T878695986"] = "Learn about one cognitive bias every day."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T878695986"] = "Learn about one cognitive bias every day."
|
||||||
|
|
||||||
|
-- View and filter AI Studio log files.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T892147062"] = "View and filter AI Studio log files."
|
||||||
|
|
||||||
-- Localization
|
-- Localization
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T897888480"] = "Localization"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T897888480"] = "Localization"
|
||||||
|
|
||||||
@ -7725,6 +7809,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T4262148639"] = "Rewrite
|
|||||||
-- Localization Assistant
|
-- Localization Assistant
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T446674624"] = "Localization Assistant"
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T446674624"] = "Localization Assistant"
|
||||||
|
|
||||||
|
-- Log Viewer Assistant
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T555062689"] = "Log Viewer Assistant"
|
||||||
|
|
||||||
-- New Chat
|
-- New Chat
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T826248509"] = "New Chat"
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T826248509"] = "New Chat"
|
||||||
|
|
||||||
|
|||||||
@ -32,5 +32,6 @@ public sealed partial class Routes
|
|||||||
public const string ASSISTANT_DOCUMENT_ANALYSIS = "/assistant/document-analysis";
|
public const string ASSISTANT_DOCUMENT_ANALYSIS = "/assistant/document-analysis";
|
||||||
public const string ASSISTANT_DYNAMIC = "/assistant/dynamic";
|
public const string ASSISTANT_DYNAMIC = "/assistant/dynamic";
|
||||||
public const string ASSISTANT_META_ASSISTANT = "/assistant/builder";
|
public const string ASSISTANT_META_ASSISTANT = "/assistant/builder";
|
||||||
|
public const string ASSISTANT_LOG_VIEWER = "/assistant/log-viewer";
|
||||||
// ReSharper restore InconsistentNaming
|
// ReSharper restore InconsistentNaming
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,4 +29,6 @@ public enum ConfigurableAssistant
|
|||||||
// ReSharper disable InconsistentNaming
|
// ReSharper disable InconsistentNaming
|
||||||
I18N_ASSISTANT,
|
I18N_ASSISTANT,
|
||||||
// ReSharper restore InconsistentNaming
|
// ReSharper restore InconsistentNaming
|
||||||
|
|
||||||
|
LOG_VIEWER_ASSISTANT,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -62,6 +62,7 @@ public static class AssistantVisibilityExtensions
|
|||||||
Components.DOCUMENT_ANALYSIS_ASSISTANT => ConfigurableAssistant.DOCUMENT_ANALYSIS_ASSISTANT,
|
Components.DOCUMENT_ANALYSIS_ASSISTANT => ConfigurableAssistant.DOCUMENT_ANALYSIS_ASSISTANT,
|
||||||
Components.SLIDE_BUILDER_ASSISTANT => ConfigurableAssistant.SLIDE_BUILDER_ASSISTANT,
|
Components.SLIDE_BUILDER_ASSISTANT => ConfigurableAssistant.SLIDE_BUILDER_ASSISTANT,
|
||||||
Components.I18N_ASSISTANT => ConfigurableAssistant.I18N_ASSISTANT,
|
Components.I18N_ASSISTANT => ConfigurableAssistant.I18N_ASSISTANT,
|
||||||
|
Components.LOG_VIEWER_ASSISTANT => ConfigurableAssistant.LOG_VIEWER_ASSISTANT,
|
||||||
|
|
||||||
_ => ConfigurableAssistant.UNKNOWN,
|
_ => ConfigurableAssistant.UNKNOWN,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -35,4 +35,5 @@ public enum Components
|
|||||||
AGENT_DATA_SOURCE_SELECTION,
|
AGENT_DATA_SOURCE_SELECTION,
|
||||||
AGENT_RETRIEVAL_CONTEXT_VALIDATION,
|
AGENT_RETRIEVAL_CONTEXT_VALIDATION,
|
||||||
AGENT_ASSISTANT_PLUGIN_AUDIT,
|
AGENT_ASSISTANT_PLUGIN_AUDIT,
|
||||||
|
LOG_VIEWER_ASSISTANT,
|
||||||
}
|
}
|
||||||
@ -17,6 +17,7 @@ public static class ComponentsExtensions
|
|||||||
Components.BIAS_DAY_ASSISTANT => false,
|
Components.BIAS_DAY_ASSISTANT => false,
|
||||||
Components.I18N_ASSISTANT => false,
|
Components.I18N_ASSISTANT => false,
|
||||||
Components.DOCUMENT_ANALYSIS_ASSISTANT => false,
|
Components.DOCUMENT_ANALYSIS_ASSISTANT => false,
|
||||||
|
Components.LOG_VIEWER_ASSISTANT => false,
|
||||||
|
|
||||||
Components.APP_SETTINGS => false,
|
Components.APP_SETTINGS => false,
|
||||||
Components.WRITER => false,
|
Components.WRITER => false,
|
||||||
@ -50,6 +51,7 @@ public static class ComponentsExtensions
|
|||||||
Components.DOCUMENT_ANALYSIS_ASSISTANT => TB("Document Analysis Assistant"),
|
Components.DOCUMENT_ANALYSIS_ASSISTANT => TB("Document Analysis Assistant"),
|
||||||
Components.SLIDE_BUILDER_ASSISTANT => TB("Slide Planner Assistant"),
|
Components.SLIDE_BUILDER_ASSISTANT => TB("Slide Planner Assistant"),
|
||||||
Components.META_ASSISTANT => TB("Assistant Builder"),
|
Components.META_ASSISTANT => TB("Assistant Builder"),
|
||||||
|
Components.LOG_VIEWER_ASSISTANT => TB("Log Viewer Assistant"),
|
||||||
|
|
||||||
Components.CHAT => TB("New Chat"),
|
Components.CHAT => TB("New Chat"),
|
||||||
|
|
||||||
|
|||||||
@ -192,3 +192,98 @@
|
|||||||
margin-left: 0 !important;
|
margin-left: 0 !important;
|
||||||
margin-right: 0 !important;
|
margin-right: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.log-viewer-shell {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-viewer-select {
|
||||||
|
min-width: 12rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-viewer-number {
|
||||||
|
max-width: 9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-viewer-filter {
|
||||||
|
min-width: 18rem;
|
||||||
|
max-width: 32rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-viewer-multiselect {
|
||||||
|
min-width: 16rem;
|
||||||
|
max-width: 34rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-viewer-path {
|
||||||
|
color: var(--mud-palette-text-secondary);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-viewer-pane {
|
||||||
|
min-height: 28rem;
|
||||||
|
border: 1px solid var(--mud-palette-lines-default);
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: var(--mud-palette-background-grey);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-viewer-lines {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
font-family: Consolas, "Courier New", monospace;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-viewer-line {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 5.5rem minmax(0, 1fr);
|
||||||
|
min-height: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-viewer-line:hover {
|
||||||
|
background-color: var(--mud-palette-action-default-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-viewer-line-number {
|
||||||
|
padding-right: 0.75rem;
|
||||||
|
color: var(--mud-palette-text-secondary);
|
||||||
|
text-align: right;
|
||||||
|
user-select: none;
|
||||||
|
border-right: 1px solid var(--mud-palette-lines-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-viewer-line-text {
|
||||||
|
padding-left: 0.75rem;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-viewer-line-error {
|
||||||
|
color: var(--mud-palette-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-viewer-line-warn {
|
||||||
|
color: var(--mud-palette-warning-darken);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-viewer-line-debug,
|
||||||
|
.log-viewer-line-trace {
|
||||||
|
color: var(--mud-palette-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-viewer-highlight {
|
||||||
|
padding: 0 2px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background-color: var(--mud-palette-warning);
|
||||||
|
color: var(--mud-palette-warning-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-viewer-empty {
|
||||||
|
display: flex;
|
||||||
|
min-height: 20rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
color: var(--mud-palette-text-secondary);
|
||||||
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
# v26.7.3, build 245 (2026-07-xx xx:xx UTC)
|
# v26.7.3, build 245 (2026-07-xx xx:xx UTC)
|
||||||
- Added support for OpenAI GPT-5.6 Sol, Terra, and Luna; Anthropic Claude Fable 5 and Mythos 5; and Google Gemini 3 Flash, Gemini 3.1 Flash-Lite, Gemini 3.1 Pro, and Gemini 3.5 Flash.
|
- Added support for OpenAI GPT-5.6 Sol, Terra, and Luna; Anthropic Claude Fable 5 and Mythos 5; and Google Gemini 3 Flash, Gemini 3.1 Flash-Lite, Gemini 3.1 Pro, and Gemini 3.5 Flash.
|
||||||
|
- Added a Log Viewer assistant that shows AI Studio log files in a read-only view with search, log filters, highlighting, and auto-refresh.
|
||||||
- Improved the "My Tasks Assistant": you can now provide one or more documents in addition to text or use documents alone when asking to identify tasks.
|
- Improved the "My Tasks Assistant": you can now provide one or more documents in addition to text or use documents alone when asking to identify tasks.
|
||||||
- Improved update guidance for Flatpak installations and added an enterprise option that lets organizations manage updates entirely through their IT department.
|
- Improved update guidance for Flatpak installations and added an enterprise option that lets organizations manage updates entirely through their IT department.
|
||||||
- Fixed an issue that could leave AI Studio unresponsive after waking the computer from sleep.
|
- Fixed an issue that could leave AI Studio unresponsive after waking the computer from sleep.
|
||||||
@ -7,4 +8,4 @@
|
|||||||
- Fixed voice recording not starting on Linux.
|
- Fixed voice recording not starting on Linux.
|
||||||
- Upgraded Rust to v1.97.0.
|
- Upgraded Rust to v1.97.0.
|
||||||
- Upgraded Tauri to v2.11.5.
|
- Upgraded Tauri to v2.11.5.
|
||||||
- Upgraded common dependencies.
|
- Upgraded common dependencies.
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user