Gliner/UI/GlinerWorkbench.razor
@using System.Linq
@using Sandbox
@using Sandbox.UI
@using GlinerPoc.Neural
@using GlinerPoc.Preprocessing
@inherits PanelComponent
@namespace GlinerPoc.UI
@{
// Workbench redesign (UI only). The inference contract is untouched: this
// panel still builds an immutable GlinerClassificationRequest snapshot from
// the entry texts, calls Service.ClassifyAsync, and renders the returned
// GlinerDecisionResult. No tokenizer/schema/score semantics live here.
}
<root>
<GlinerKeyCatcherPanel OnKey="@OnRootKey" @ref="KeyCatcher">
<ChildContent>
<!-- ================= HEADER ================= -->
<div class="header">
<div class="header-titles">
<div class="title">GLiNER 2.5 Small — Decision Workbench</div>
<div class="subtitle">Evaluate options and choose the best action using GLiNER 2.5</div>
</div>
<div class="header-controls">
<div class="backend-block">
<span class="backend-caption">Backend:</span>
<div class="seg">
<button class="seg-btn @( BackendIsGpu ? "active" : "" ) @( CanSwitchBackend ? "" : "disabled" )"
onclick="@(() => OnSelectBackend( GlinerBackendKind.NativeGpuFp32 ))">@( DefaultIsGpu ? "GPU · Default" : "GPU" )</button>
<button class="seg-btn @( BackendIsCpu ? "active" : "" ) @( CanSwitchBackend ? "" : "disabled" )"
onclick="@(() => OnSelectBackend( GlinerBackendKind.NativeScalarCpu ))">CPU</button>
</div>
</div>
<div class="status-block">
<div class="status-line">
<div class="dot @StatusClass"></div>
<span class="status-text @StatusClass">@StatusText</span>
</div>
<div class="status-sub">@StatusSub</div>
</div>
</div>
</div>
<!-- ================= BODY ================= -->
<div class="body">
<!-- ---------- LEFT: INPUT ---------- -->
<div class="card input-card">
<div class="card-scroll">
<div class="section">
<div class="section-title">1. Context</div>
<div class="helper">Describe the current situation, state or available information.</div>
<TextEntry @ref="ContextBox" class="context-input" Multiline="@true" />
<div class="field-foot">
<span>@ContextLength characters</span>
<span class="field-foot-note">encoded budget: @GlinerProcessor.MaxEncodedTokens tokens</span>
</div>
</div>
<div class="section">
<div class="section-title">2. Task / Question</div>
<div class="helper">What decision should be made?</div>
<TextEntry @ref="TaskBox" class="task-input" />
@if ( Service is { IsReady: true } && string.IsNullOrWhiteSpace( TaskBox?.Text ) && !RequestInFlight )
{
<div class="field-warn">A task is required to run the decision.</div>
}
</div>
<div class="section">
<div class="section-head">
<div class="section-title">3. Candidates</div>
<div class="count-chip @( PopulatedCount < 2 ? "chip-warn" : "" )">@PopulatedCount / 8</div>
</div>
<div class="helper">Add between 2 and 8 candidates (labels and optional descriptions).</div>
<div class="candidate-row">
<div class="candidate-index">1</div>
<TextEntry @ref="Label1" class="candidate-label" Placeholder="label 1" />
<TextEntry @ref="Desc1" class="candidate-desc" Placeholder="description (optional)" />
<div class="icon-button @( CanRemoveCandidate ? "" : "disabled" )" onclick="@(() => OnRemoveCandidate( 0 ))">✕</div>
</div>
<div class="candidate-row">
<div class="candidate-index">2</div>
<TextEntry @ref="Label2" class="candidate-label" Placeholder="label 2" />
<TextEntry @ref="Desc2" class="candidate-desc" Placeholder="description (optional)" />
<div class="icon-button @( CanRemoveCandidate ? "" : "disabled" )" onclick="@(() => OnRemoveCandidate( 1 ))">✕</div>
</div>
<div class="candidate-row">
<div class="candidate-index">3</div>
<TextEntry @ref="Label3" class="candidate-label" Placeholder="label 3" />
<TextEntry @ref="Desc3" class="candidate-desc" Placeholder="description (optional)" />
<div class="icon-button @( CanRemoveCandidate ? "" : "disabled" )" onclick="@(() => OnRemoveCandidate( 2 ))">✕</div>
</div>
<div class="candidate-row">
<div class="candidate-index">4</div>
<TextEntry @ref="Label4" class="candidate-label" Placeholder="label 4" />
<TextEntry @ref="Desc4" class="candidate-desc" Placeholder="description (optional)" />
<div class="icon-button @( CanRemoveCandidate ? "" : "disabled" )" onclick="@(() => OnRemoveCandidate( 3 ))">✕</div>
</div>
@if ( VisibleCandidateCount >= 5 )
{
<div class="candidate-row">
<div class="candidate-index">5</div>
<TextEntry @ref="Label5" class="candidate-label" Placeholder="label 5" />
<TextEntry @ref="Desc5" class="candidate-desc" Placeholder="description (optional)" />
<div class="icon-button @( CanRemoveCandidate ? "" : "disabled" )" onclick="@(() => OnRemoveCandidate( 4 ))">✕</div>
</div>
}
@if ( VisibleCandidateCount >= 6 )
{
<div class="candidate-row">
<div class="candidate-index">6</div>
<TextEntry @ref="Label6" class="candidate-label" Placeholder="label 6" />
<TextEntry @ref="Desc6" class="candidate-desc" Placeholder="description (optional)" />
<div class="icon-button @( CanRemoveCandidate ? "" : "disabled" )" onclick="@(() => OnRemoveCandidate( 5 ))">✕</div>
</div>
}
@if ( VisibleCandidateCount >= 7 )
{
<div class="candidate-row">
<div class="candidate-index">7</div>
<TextEntry @ref="Label7" class="candidate-label" Placeholder="label 7" />
<TextEntry @ref="Desc7" class="candidate-desc" Placeholder="description (optional)" />
<div class="icon-button @( CanRemoveCandidate ? "" : "disabled" )" onclick="@(() => OnRemoveCandidate( 6 ))">✕</div>
</div>
}
@if ( VisibleCandidateCount >= 8 )
{
<div class="candidate-row">
<div class="candidate-index">8</div>
<TextEntry @ref="Label8" class="candidate-label" Placeholder="label 8" />
<TextEntry @ref="Desc8" class="candidate-desc" Placeholder="description (optional)" />
<div class="icon-button @( CanRemoveCandidate ? "" : "disabled" )" onclick="@(() => OnRemoveCandidate( 7 ))">✕</div>
</div>
}
<div class="add-button @( CanAddCandidate ? "" : "disabled" )" onclick="@OnAddCandidate">+ Add Candidate</div>
@if ( HasDuplicateLabels )
{
<div class="field-warn">Duplicate labels — each entry is still scored, but ranking may be ambiguous.</div>
}
</div>
</div>
<div class="action-bar">
@if ( !RequestInFlight )
{
<button class="primary-button @( CanRun ? "" : "disabled" )" onclick="@OnRun">▶ Run Decision</button>
}
else
{
<button class="primary-button running" disabled>Running… @(ElapsedSeconds)s</button>
<button class="secondary-button @( CanCancel ? "" : "disabled" )" onclick="@OnCancel">Cancel</button>
}
<button class="secondary-button" onclick="@OnClear">↻ Clear All</button>
@if ( !RequestInFlight && Service is { IsReady: true } && !CanRun )
{
<span class="run-block-note">@RunBlockReason</span>
}
else if ( !RequestInFlight && CanRun )
{
<span class="action-hint-note">Ctrl+Enter</span>
}
</div>
</div>
<!-- ---------- RIGHT: RESULTS ---------- -->
<div class="card results-card">
<div class="results-head">
<div class="card-title">Results</div>
<div class="helper">Model ranking and selected decision.</div>
</div>
<div class="card-scroll results-body">
@if ( !string.IsNullOrEmpty( VisibleError ) )
{
<div class="banner @( ErrorIsCancel ? "banner-warn" : "banner-error" )">@VisibleError</div>
}
@if ( HasResult )
{
<!-- selected action -->
<div class="selected-card">
<div class="selected-main">
<div class="selected-caption">Selected Action</div>
<div class="selected-label">@Result.SelectedLabel</div>
@if ( !string.IsNullOrWhiteSpace( SelectedDescription ) )
{
<div class="selected-desc">@SelectedDescription</div>
}
</div>
<div class="selected-side">
<div class="selected-score-caption">Model Score</div>
<div class="selected-score">@Result.Scores[Result.SelectedIndex].ToString( "0.####" )</div>
<div class="bar-track selected-track">
<div class="bar-fill fill-green" style="width: 100%"></div>
</div>
</div>
</div>
<!-- ranking -->
<div class="ranking-head">
<div class="card-subtitle">Candidate Ranking</div>
<div class="seg seg-small">
<button class="seg-btn @( !ShowRawLogits ? "active" : "" )" onclick="@(() => ShowRawLogits = false)">Simple View</button>
<button class="seg-btn @( ShowRawLogits ? "active" : "" )" onclick="@(() => ShowRawLogits = true)">Show Raw Logits</button>
</div>
</div>
<div class="ranking-table">
<div class="ranking-row head">
<div class="rank-no">#</div>
<div class="rank-label">Candidate</div>
<div class="rank-desc">Description</div>
<div class="rank-score">Score</div>
@if ( ShowRawLogits )
{
<div class="rank-logit">Raw Logit</div>
}
<div class="rank-bar"></div>
</div>
@for ( int rank = 0; rank < RankedOrder.Length; rank++ )
{
var idx = RankedOrder[rank];
var pct = ScoreBarPercent( idx );
<div class="ranking-row @( idx == Result.SelectedIndex ? "winner" : "" )">
<div class="rank-no">@( rank + 1 )</div>
<div class="rank-label">@Result.Labels[idx]</div>
<div class="rank-desc">@RankDescription( idx )</div>
<div class="rank-score">@Result.Scores[idx].ToString( "0.####" )</div>
@if ( ShowRawLogits )
{
<div class="rank-logit">@Result.RawLogits[idx].ToString( "0.###" )</div>
}
<div class="rank-bar">
<div class="bar-track">
<div class="bar-fill @( idx == Result.SelectedIndex ? "fill-green" : "fill-blue" )" style="@($"width: {pct:0.#}%")"></div>
</div>
</div>
</div>
}
</div>
}
else if ( RequestInFlight )
{
<div class="state-note running-note">
<div class="state-title">Running…</div>
<div class="state-sub">Executing @Service?.BackendName on the native runtime — this may take a moment on CPU.</div>
</div>
}
else
{
<div class="state-note">
<div class="state-title">No decision yet</div>
<div class="state-sub">Enter a context, task/question and at least two candidates, then run the model.</div>
</div>
}
<!-- diagnostics -->
<div class="diag">
<div class="diag-head" onclick="@(() => DiagnosticsOpen = !DiagnosticsOpen)">
<span class="diag-arrow">@( DiagnosticsOpen ? "▼" : "▶" )</span>
<span>Diagnostics</span>
</div>
@if ( DiagnosticsOpen )
{
<div class="diag-grid">
<div class="diag-cell">
<div class="diag-label">Backend</div>
<div class="diag-value">@( HasResult ? Service?.LastResultBackend : Service?.BackendName )</div>
</div>
<div class="diag-cell">
<div class="diag-label">Encoded Tokens</div>
<div class="diag-value">@( HasResult ? Result.EncodedTokenCount.ToString() : "—" )</div>
</div>
<div class="diag-cell">
<div class="diag-label">Preprocess Time</div>
<div class="diag-value">@( HasResult ? $"{Result.PreprocessMs:0.#} ms" : "—" )</div>
</div>
<div class="diag-cell">
<div class="diag-label">Inference Time</div>
<div class="diag-value">@( HasResult ? FormatMs( Result.TotalNeuralMs - Result.PreprocessMs ) : "—" )</div>
</div>
<div class="diag-cell">
<div class="diag-label">Total Time</div>
<div class="diag-value">@( HasResult ? FormatMs( Result.TotalNeuralMs ) : "—" )</div>
</div>
<div class="diag-cell">
<div class="diag-label">GPU Weights</div>
<div class="diag-value">@GpuMemoryText</div>
</div>
</div>
@if ( Service is { State: GlinerServiceState.Ready } )
{
<div class="diag-foot">@($"init {Service.InitializationMs:0} ms · generation {Service.Generation}")</div>
}
}
</div>
</div>
<div class="results-foot">
<span>GLiNER 2.5 Small</span><span class="foot-sep">|</span><span>s&box</span>
</div>
</div>
</div>
</ChildContent>
</GlinerKeyCatcherPanel>
</root>
@code
{
private GlinerDecisionService Service;
private TextEntry ContextBox;
private TextEntry TaskBox;
private GlinerKeyCatcherPanel KeyCatcher;
private TextEntry Label1, Label2, Label3, Label4, Label5, Label6, Label7, Label8;
private TextEntry Desc1, Desc2, Desc3, Desc4, Desc5, Desc6, Desc7, Desc8;
private string UiError = "";
private GlinerDecisionResult Result;
private bool RequestInFlight;
private int ElapsedSeconds;
private double RunStart;
// Workbench UX state (UI only — no inference semantics).
private int VisibleCandidateCount = 4;
private bool ShowRawLogits;
private bool DiagnosticsOpen = true;
private GlinerClassificationRequest LastRequest;
private int[] RankedOrder = Array.Empty<int>();
// Poll-loop change detection (razor panels don't tick OnUpdate).
private int _pollContextLen = -1, _pollTaskLen = -1, _pollPopulated = -1,
_pollElapsed = -1, _pollVisible = -1;
private string _pollSignature = "";
private bool _pollFirst = true;
[Property]
public bool AutoRunOnReady { get; set; }
private const int MinCandidates = 2;
private const int MaxCandidates = 8;
// ---- run gating (same rules as before: ready, idle, >=2 labels) ----------
private bool CanRun => Service is { IsReady: true } && !RequestInFlight && PopulatedCount >= MinCandidates;
private bool CanCancel => RequestInFlight;
private bool CanAddCandidate => VisibleCandidateCount < MaxCandidates;
private bool CanRemoveCandidate => VisibleCandidateCount > MinCandidates;
private bool HasResult => Result is not null;
private bool BackendIsGpu => Service?.SelectedBackend == GlinerBackendKind.NativeGpuFp32;
private bool BackendIsCpu => Service?.SelectedBackend == GlinerBackendKind.NativeScalarCpu;
private bool DefaultIsGpu => Service?.DefaultBackend == GlinerBackendKind.NativeGpuFp32;
// 8E.10: switching only while idle — selector disabled while Running
private bool CanSwitchBackend => Service is { IsReady: true } && !RequestInFlight;
private int ContextLength => ContextBox?.Text?.Length ?? 0;
private string RunBlockReason
{
get
{
if ( string.IsNullOrWhiteSpace( TaskBox?.Text ) )
{
return "Enter a task to run the decision.";
}
if ( PopulatedCount < MinCandidates )
{
return $"At least {MinCandidates} candidates are required.";
}
return "";
}
}
private bool HasDuplicateLabels
{
get
{
var seen = new HashSet<string>( StringComparer.OrdinalIgnoreCase );
foreach ( var l in VisibleLabels() )
{
if ( !seen.Add( l ) )
{
return true;
}
}
return false;
}
}
private string SelectedDescription
{
get
{
if ( Result is null || LastRequest is null )
{
return "";
}
foreach ( var c in LastRequest.Candidates )
{
if ( string.Equals( c.Label?.Trim(), Result.SelectedLabel?.Trim(), StringComparison.OrdinalIgnoreCase ) )
{
return c.Description ?? "";
}
}
return "";
}
}
private string RankDescription( int idx )
{
if ( Result is null || LastRequest is null )
{
return "";
}
if ( idx < 0 || idx >= Result.Labels.Length )
{
return "";
}
var label = Result.Labels[idx];
foreach ( var c in LastRequest.Candidates )
{
if ( string.Equals( c.Label?.Trim(), label?.Trim(), StringComparison.OrdinalIgnoreCase ) )
{
return c.Description ?? "";
}
}
return "";
}
private string GpuMemoryText
{
get
{
var status = Service?.GpuStatus ?? "";
if ( Service is { GpuBackendAvailable: true } )
{
var m = System.Text.RegularExpressions.Regex.Match( status, @"([\d.]+) MiB" );
return m.Success ? $"{m.Groups[1].Value} MiB resident" : "resident";
}
return status.StartsWith( "failed" ) ? "unavailable" : "N/A";
}
}
// 8E.18: honest timing units — ms for fast (GPU) results, s for scalar
private static string FormatMs( double ms )
=> ms < 1000.0 ? $"{ms:0.#} ms" : $"{ms / 1000.0:0.##} s";
private double ScoreBarPercent( int idx )
{
if ( Result is null || Result.Scores.Length == 0 )
{
return 0;
}
float max = 0f;
foreach ( var s in Result.Scores )
{
if ( s > max )
{
max = s;
}
}
if ( max <= 0f )
{
return 0;
}
var pct = Result.Scores[idx] / max * 100.0;
return Result.Scores[idx] > 0f && pct < 2.0 ? 2.0 : pct;
}
private IEnumerable<string> VisibleLabels()
{
var boxes = new[] { Label1, Label2, Label3, Label4, Label5, Label6, Label7, Label8 };
for ( int i = 0; i < VisibleCandidateCount && i < boxes.Length; i++ )
{
var t = boxes[i]?.Text?.Trim();
if ( !string.IsNullOrEmpty( t ) )
{
yield return t;
}
}
}
private int PopulatedCount => VisibleLabels().Count();
// ---- header status -------------------------------------------------------
private string StatusText => Service switch
{
null => "Service missing",
_ => Service.State switch
{
GlinerServiceState.Uninitialized => "Offline",
GlinerServiceState.Initializing => "Loading",
GlinerServiceState.Ready => "Ready",
GlinerServiceState.Running => "Running",
GlinerServiceState.Cancelling => "Cancelling",
GlinerServiceState.Faulted => "Error",
_ => Service.State.ToString()
}
};
private string StatusClass =>
Service is { State: GlinerServiceState.Faulted } ? "error" :
Service is { IsReady: true } ? ( ( Service.GpuStatus ?? "" ).StartsWith( "failed" ) ? "warn" : "ready" ) :
Service is { State: GlinerServiceState.Running or GlinerServiceState.Cancelling or GlinerServiceState.Initializing } ? "busy" : "error";
private string StatusSub
{
get
{
if ( Service is null )
{
return "";
}
switch ( Service.State )
{
case GlinerServiceState.Ready:
var gpu = Service.GpuBackendAvailable
? ( BackendIsGpu ? $" · {GpuMemoryText}" : $" · GPU {GpuMemoryText}" )
: "";
return $"{Service.BackendName}{gpu}";
case GlinerServiceState.Initializing:
return "loading GLiNER 2.5 Small…";
case GlinerServiceState.Faulted:
return Truncate( Service.ErrorMessage, 52 );
default:
return "";
}
}
}
private static string Truncate( string s, int max )
=> string.IsNullOrEmpty( s ) ? "" : s.Length <= max ? s : s[..( max - 1 )] + "…";
private string VisibleError => !string.IsNullOrEmpty( UiError ) ? UiError : Service?.ErrorMessage ?? "";
private bool ErrorIsCancel => UiError == "Request cancelled.";
// ---- backend --------------------------------------------------------------
private void OnSelectBackend( GlinerBackendKind kind )
{
if ( !CanSwitchBackend || Service is null )
{
return;
}
_ = Service.SelectBackend( kind );
StateHasChanged();
}
// ---- candidate rows --------------------------------------------------------
private void OnAddCandidate()
{
if ( !CanAddCandidate )
{
return;
}
VisibleCandidateCount++;
StateHasChanged();
}
private void OnRemoveCandidate( int index )
{
if ( !CanRemoveCandidate )
{
return;
}
var labels = new[] { Label1, Label2, Label3, Label4, Label5, Label6, Label7, Label8 };
var descs = new[] { Desc1, Desc2, Desc3, Desc4, Desc5, Desc6, Desc7, Desc8 };
// shift rows above the removed one up; identity/order stays user-visible
for ( int i = index; i < VisibleCandidateCount - 1; i++ )
{
if ( labels[i] is not null )
{
labels[i].Text = labels[i + 1]?.Text ?? "";
}
if ( descs[i] is not null )
{
descs[i].Text = descs[i + 1]?.Text ?? "";
}
}
VisibleCandidateCount--;
// vacated tail rows unrender on rebuild — drop the refs so stale panels
// are never read
if ( VisibleCandidateCount < 8 ) { Label8 = null; Desc8 = null; }
if ( VisibleCandidateCount < 7 ) { Label7 = null; Desc7 = null; }
if ( VisibleCandidateCount < 6 ) { Label6 = null; Desc6 = null; }
if ( VisibleCandidateCount < 5 ) { Label5 = null; Desc5 = null; }
StateHasChanged();
}
// ---- run / cancel / clear --------------------------------------------------
private async void OnRun()
{
if ( !CanRun || Service is null )
{
return;
}
// Immutable request snapshot built from current UI text NOW.
var boxes = new[] { Label1, Label2, Label3, Label4, Label5, Label6, Label7, Label8 };
var descs = new[] { Desc1, Desc2, Desc3, Desc4, Desc5, Desc6, Desc7, Desc8 };
var candidates = new List<GlinerCandidate>();
for ( int i = 0; i < boxes.Length; i++ )
{
string label = boxes[i]?.Text?.Trim();
if ( string.IsNullOrEmpty( label ) )
{
continue;
}
string desc = descs[i]?.Text;
candidates.Add( new GlinerCandidate( label, string.IsNullOrEmpty( desc ) ? null : desc ) );
}
var request = new GlinerClassificationRequest(
ContextBox?.Text ?? "", TaskBox?.Text ?? "", candidates );
UiError = "";
Result = null;
RequestInFlight = true;
RunStart = Time.Now;
ElapsedSeconds = 0;
StateHasChanged();
try
{
var result = await Service.ClassifyAsync( request );
if ( result is null )
{
UiError = "Request cancelled.";
}
else
{
Result = result;
LastRequest = request;
RankedOrder = ComputeRanking( result );
}
}
catch ( Exception e )
{
UiError = Service?.ErrorMessage ?? e.Message;
}
finally
{
RequestInFlight = false;
StateHasChanged();
}
}
// Ranking derived from the model's own scores: descending score, with the
// selected candidate pinned to rank #1 on ties. Pure presentation.
private static int[] ComputeRanking( GlinerDecisionResult result )
{
var order = new int[result.Labels.Length];
for ( int i = 0; i < order.Length; i++ )
{
order[i] = i;
}
Array.Sort( order, ( a, b ) =>
{
int c = result.Scores[b].CompareTo( result.Scores[a] );
if ( c != 0 )
{
return c;
}
if ( a == result.SelectedIndex )
{
return -1;
}
if ( b == result.SelectedIndex )
{
return 1;
}
return a.CompareTo( b );
} );
return order;
}
private void OnCancel()
{
Service?.Cancel();
}
private void OnClear()
{
if ( RequestInFlight )
{
return; // never mutate an active request
}
foreach ( var box in new[] { ContextBox, TaskBox, Label1, Label2, Label3, Label4,
Label5, Label6, Label7, Label8, Desc1, Desc2, Desc3, Desc4,
Desc5, Desc6, Desc7, Desc8 } )
{
if ( box is not null )
{
box.Text = "";
}
}
Result = null;
LastRequest = null;
RankedOrder = Array.Empty<int>();
UiError = "";
VisibleCandidateCount = 4;
StateHasChanged();
}
// ---- lifecycle --------------------------------------------------------------
protected override void OnStart()
{
Service = GetComponent<GlinerDecisionService>();
if ( Service is null )
{
UiError = "GlinerDecisionService is missing from this GameObject.";
return;
}
// Phase 7 mouse policy: this is a dedicated UI scene, so the UI owns
// the cursor. Play mode defaults to a hidden/locked cursor, which made
// every panel unclickable. Save the previous mode and restore it when
// the workbench goes away.
_previousVisibility = Mouse.Visibility;
_mouseSaved = true;
Mouse.Visibility = MouseVisibility.Visible;
Log.Info( $"[GLI:UI] Mouse.Visibility {_previousVisibility} -> Visible" );
_ = Service.InitializeAsync();
_ = AutoRunWatcherAsync();
_ = RefreshLoopAsync();
}
private MouseVisibility _previousVisibility;
private bool _mouseSaved;
private void RestoreMouse()
{
if ( _mouseSaved )
{
Mouse.Visibility = _previousVisibility;
_mouseSaved = false;
Log.Info( $"[GLI:UI] Mouse.Visibility restored -> {_previousVisibility}" );
}
}
protected override void OnDisabled() => RestoreMouse();
protected override void OnDestroy() => RestoreMouse();
protected override void OnTreeFirstBuilt()
{
// Default example so testing is immediate (phase 7.28). Named refs are
// bound by this point; users can freely edit or clear everything.
if ( string.IsNullOrEmpty( ContextBox?.Text ) )
{
ContextBox.Text = "The character is badly injured. No enemy is currently visible. Healing is available. The weapon is loaded.";
}
if ( string.IsNullOrEmpty( TaskBox?.Text ) )
{
TaskBox.Text = "Choose the next action.";
}
var labels = new[] { Label1, Label2, Label3, Label4 };
var names = new[] { "heal", "attack", "reload", "retreat" };
var descs = new[] { Desc1, Desc2, Desc3, Desc4 };
var descTexts = new[] { "Restore health", "Engage an enemy", "Reload the current weapon", "Move away from danger" };
for ( int i = 0; i < labels.Length; i++ )
{
if ( string.IsNullOrEmpty( labels[i]?.Text ) )
{
labels[i].Text = names[i];
}
if ( string.IsNullOrEmpty( descs[i]?.Text ) )
{
descs[i].Text = descTexts[i];
}
}
}
protected override void OnTreeBuilt()
{
// Ctrl+Enter runs the decision. Key events bubble from the focused
// entry through the key-catcher wrapper panel; the handler is (re-)
// attached here because tree rebuilds recreate the catcher panel.
if ( KeyCatcher is not null )
{
KeyCatcher.OnKey = OnRootKey;
}
}
private void OnRootKey( ButtonEvent e )
{
if ( e.Pressed && e.HasCtrl && ( e.Button == "enter" || e.Button == "return" ) )
{
Log.Info( "[GLI:UI] Ctrl+Enter shortcut -> run" );
OnRun();
}
}
// OnUpdate does not tick on razor panel components in 26.09.22 (observed);
// the optional auto-run waits for readiness via this async watcher.
private async Task AutoRunWatcherAsync()
{
while ( Service is null || !Service.IsReady )
{
await Task.Delay( 100 );
}
Log.Info( "[GLI:UI] watcher: service ready" );
while ( ContextBox is null )
{
await Task.Delay( 100 );
}
Log.Info( "[GLI:UI] watcher: context ref bound" );
ContextBox.Focus();
await Task.Delay( 200 );
Log.Info( $"[GLI:UI] TextEntry focus validation: ContextBox.HasFocus={ContextBox.HasFocus}" );
await Task.Delay( 300 ); // let the first UI build settle
if ( AutoRunOnReady )
{
OnRun();
}
}
// Refresh loop: keeps counters, status and the running elapsed timer honest
// without an OnUpdate tick. StateHasChanged only fires when something the
// user can see actually changed (BuildHash gates the rebuild anyway).
private async Task RefreshLoopAsync()
{
while ( true )
{
await Task.Delay( 200 );
if ( RequestInFlight )
{
ElapsedSeconds = (int) ( Time.Now - RunStart );
}
var sig = $"{( Service?.State.ToString() ?? "null" )}|{Service?.GpuStatus}|{Service?.SelectedBackend}";
var contextLen = ContextLength;
var taskLen = TaskBox?.Text?.Length ?? 0;
var populated = PopulatedCount;
var elapsed = ElapsedSeconds;
var visible = VisibleCandidateCount;
if ( _pollFirst ||
sig != _pollSignature ||
contextLen != _pollContextLen || taskLen != _pollTaskLen ||
populated != _pollPopulated || elapsed != _pollElapsed ||
visible != _pollVisible )
{
_pollFirst = false;
_pollSignature = sig;
_pollContextLen = contextLen;
_pollTaskLen = taskLen;
_pollPopulated = populated;
_pollElapsed = elapsed;
_pollVisible = visible;
StateHasChanged();
}
}
}
protected override int BuildHash()
{
return HashCode.Combine(
HashCode.Combine(
Service?.State ?? GlinerServiceState.Uninitialized, RequestInFlight, UiError,
HasResult, ElapsedSeconds, Service?.SelectedBackend ?? GlinerBackendKind.NativeScalarCpu,
Service?.GpuStatus ?? "" ),
HashCode.Combine(
VisibleCandidateCount, ShowRawLogits, DiagnosticsOpen,
ContextLength, TaskBox?.Text?.Length ?? 0, PopulatedCount,
HasDuplicateLabels, LastRequest ) );
}
}