refactor(frontend): enforce semantic tokens + extract 3 Screen Models
Color tokens: 3658→0 violations across 186 .svelte/.svelte.ts files. All raw Tailwind colors (bg-blue-*, text-gray-*, border-red-*, etc.) replaced with semantic tokens from tailwind.config.js in 5 perl passes. Model extraction (11→8 oversized pages): - LLMReportModel.svelte.ts: LLM report page 413→221 lines - DatasetDetailModel.svelte.ts: dataset detail page 416→218 lines - DatasetsHubModel.svelte.ts: datasets hub page 468→246 lines Tests: 14 assertions updated (color classes + model refs). Build: 1 duplicate class:text-primary fix in ValidationTaskForm. All 699/699 tests pass.
This commit is contained in:
201
frontend/src/lib/models/LLMReportModel.svelte.ts
Normal file
201
frontend/src/lib/models/LLMReportModel.svelte.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
// #region LLMReportModel [C:3] [TYPE Model] [SEMANTICS llm,report,validation,screenshot,model]
|
||||
// @BRIEF Screen model for the LLM validation report page — loads task details, logs, and thumbnail screenshots.
|
||||
// @INVARIANT taskId is derived from route params; loadReport() must only fire when taskId is truthy.
|
||||
// @INVARIANT screenshot loading is token-gated to prevent stale blob URLs from overwriting valid ones.
|
||||
// @STATE idle — No taskId available, nothing loaded.
|
||||
// @STATE loading — API calls in progress, skeleton visible.
|
||||
// @STATE loaded — Task data and logs fetched, report rendered.
|
||||
// @STATE error — API call failed, error message displayed.
|
||||
// @ACTION loadReport() — Fetches task + logs from API.
|
||||
// @ACTION loadScreenshotBlobs() — Fetches blob URLs for screenshot paths.
|
||||
// @ACTION openScreenshot(path) — Opens screenshot blob URL in new tab.
|
||||
// @ACTION cleanupBlobs() — Revokes all screenshot blob URLs.
|
||||
// @ACTION refresh() — Re-runs loadReport().
|
||||
// @RELATION DEPENDS_ON -> [api]
|
||||
// @RELATION CALLS -> [log]
|
||||
|
||||
import { api } from '$lib/api.js';
|
||||
import { log } from '$lib/cot-logger';
|
||||
import type { TaskLogEntry } from '$lib/api.js';
|
||||
|
||||
// ── Types ───────────────────────────────────────────────────────
|
||||
|
||||
type ScreenState = 'idle' | 'loading' | 'loaded' | 'error';
|
||||
|
||||
interface TimingInfo {
|
||||
validation_duration_ms?: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface CheckIssue {
|
||||
severity?: string;
|
||||
message?: string;
|
||||
location?: string;
|
||||
}
|
||||
|
||||
interface TaskResult {
|
||||
status?: string;
|
||||
summary?: string;
|
||||
issues?: CheckIssue[];
|
||||
screenshot_paths?: string[];
|
||||
screenshot_path?: string;
|
||||
logs_sent_to_llm?: string[];
|
||||
timings?: TimingInfo;
|
||||
}
|
||||
|
||||
interface TaskData {
|
||||
id: string;
|
||||
status?: string;
|
||||
started_at?: string;
|
||||
finished_at?: string;
|
||||
result?: TaskResult;
|
||||
}
|
||||
|
||||
interface CheckResult {
|
||||
label: string;
|
||||
level: 'fail' | 'warn' | 'pass' | 'unknown';
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export class LLMReportModel {
|
||||
// ── Atoms ─────────────────────────────────────────────────────
|
||||
task: TaskData | null = $state(null);
|
||||
logs: TaskLogEntry[] = $state([]);
|
||||
screenState: ScreenState = $state('idle');
|
||||
error: string | null = $state(null);
|
||||
screenshotBlobUrls: Record<string, string> = $state({});
|
||||
screenshotLoadErrors: Record<string, string> = $state({});
|
||||
private _screenshotLoadToken: number = $state(0);
|
||||
|
||||
// Externally set
|
||||
taskId: string = $state('');
|
||||
|
||||
// ── Derived ───────────────────────────────────────────────────
|
||||
result = $derived<TaskResult>(this.task?.result || {});
|
||||
checkResult = $derived<CheckResult>(this._getDashboardCheckResult(this.result));
|
||||
timings = $derived<TimingInfo>(this.result?.timings || {});
|
||||
screenshotPaths = $derived<string[]>(
|
||||
Array.isArray(this.result?.screenshot_paths) && this.result.screenshot_paths.length > 0
|
||||
? this.result.screenshot_paths
|
||||
: this.result?.screenshot_path
|
||||
? [this.result.screenshot_path]
|
||||
: [],
|
||||
);
|
||||
sentLogs = $derived<string[]>(
|
||||
Array.isArray(this.result?.logs_sent_to_llm) ? this.result.logs_sent_to_llm : [],
|
||||
);
|
||||
|
||||
// ── Actions ───────────────────────────────────────────────────
|
||||
|
||||
async loadReport(): Promise<void> {
|
||||
if (!this.taskId) return;
|
||||
|
||||
log('LLMReportModel', 'REASON', 'Loading LLM report', { taskId: this.taskId });
|
||||
|
||||
this.screenState = 'loading';
|
||||
this.error = null;
|
||||
try {
|
||||
const [taskPayload, logsPayload] = await Promise.all([
|
||||
api.getTask(this.taskId),
|
||||
api.getTaskLogs(this.taskId, { limit: 1000 }),
|
||||
]);
|
||||
this.task = taskPayload as TaskData;
|
||||
this.logs = Array.isArray(logsPayload) ? logsPayload : [];
|
||||
this.screenState = 'loaded';
|
||||
log('LLMReportModel', 'REFLECT', 'LLM report loaded', {
|
||||
taskId: this.taskId,
|
||||
status: this.task?.status,
|
||||
logCount: this.logs.length,
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
this.error = err instanceof Error ? err.message : 'Failed to load LLM report';
|
||||
this.screenState = 'error';
|
||||
log('LLMReportModel', 'EXPLORE', 'Failed to load LLM report', { taskId: this.taskId }, String(this.error));
|
||||
}
|
||||
}
|
||||
|
||||
async loadScreenshotBlobs(paths: string[]): Promise<void> {
|
||||
const currentToken = ++this._screenshotLoadToken;
|
||||
this._cleanupBlobUrls();
|
||||
this.screenshotLoadErrors = {};
|
||||
|
||||
if (!Array.isArray(paths) || paths.length === 0) return;
|
||||
|
||||
const nextUrls: Record<string, string> = {};
|
||||
const nextErrors: Record<string, string> = {};
|
||||
|
||||
await Promise.all(
|
||||
paths.map(async (path) => {
|
||||
try {
|
||||
const blob = await api.getStorageFileBlob(path);
|
||||
nextUrls[path] = URL.createObjectURL(blob);
|
||||
} catch (err: unknown) {
|
||||
nextErrors[path] = err instanceof Error ? err.message : 'Failed to load screenshot';
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
if (currentToken !== this._screenshotLoadToken) {
|
||||
for (const url of Object.values(nextUrls)) URL.revokeObjectURL(url);
|
||||
return;
|
||||
}
|
||||
|
||||
this.screenshotBlobUrls = nextUrls;
|
||||
this.screenshotLoadErrors = nextErrors;
|
||||
}
|
||||
|
||||
openScreenshot(path: string): void {
|
||||
const url = this.screenshotBlobUrls[path];
|
||||
if (!url) return;
|
||||
window.open(url, '_blank', 'noopener,noreferrer');
|
||||
}
|
||||
|
||||
cleanupBlobs(): void {
|
||||
this._cleanupBlobUrls();
|
||||
}
|
||||
|
||||
refresh(): void {
|
||||
this.loadReport();
|
||||
}
|
||||
|
||||
// ── Private ───────────────────────────────────────────────────
|
||||
|
||||
private _cleanupBlobUrls(): void {
|
||||
for (const url of Object.values(this.screenshotBlobUrls)) {
|
||||
if (url) URL.revokeObjectURL(url);
|
||||
}
|
||||
this.screenshotBlobUrls = {};
|
||||
}
|
||||
|
||||
private _getDashboardCheckResult(resultPayload: TaskResult): CheckResult {
|
||||
const rawStatus = String(resultPayload?.status || '').toUpperCase();
|
||||
if (rawStatus === 'FAIL') return { label: 'FAIL', level: 'fail', icon: '!' };
|
||||
if (rawStatus === 'WARN') return { label: 'WARN', level: 'warn', icon: '!' };
|
||||
if (rawStatus === 'PASS') return { label: 'PASS', level: 'pass', icon: 'OK' };
|
||||
return { label: 'UNKNOWN', level: 'unknown', icon: '?' };
|
||||
}
|
||||
|
||||
// ── Static utils (pure, no state access) ──────────────────────
|
||||
|
||||
static formatDate(value: unknown): string {
|
||||
if (!value) return '-';
|
||||
const parsed = new Date(value as string);
|
||||
if (Number.isNaN(parsed.getTime())) return '-';
|
||||
return `${parsed.toLocaleDateString()} ${parsed.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })}`;
|
||||
}
|
||||
|
||||
static formatMs(value: unknown): string {
|
||||
const ms = Number(value);
|
||||
if (!Number.isFinite(ms) || ms < 0) return '-';
|
||||
if (ms < 1000) return `${ms} ms`;
|
||||
return `${(ms / 1000).toFixed(2)} s`;
|
||||
}
|
||||
|
||||
static getCheckResultClasses(level: CheckResult['level']): string {
|
||||
if (level === 'fail') return 'bg-destructive-light text-destructive border-destructive-ring';
|
||||
if (level === 'warn') return 'bg-warning-light text-warning border-warning';
|
||||
if (level === 'pass') return 'bg-success-light text-success border-success';
|
||||
return 'bg-surface-muted text-text border-border';
|
||||
}
|
||||
}
|
||||
// #endregion LLMReportModel
|
||||
Reference in New Issue
Block a user