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:
2026-06-02 17:58:30 +03:00
parent 29acfb4e69
commit cf7b3556f7
156 changed files with 4581 additions and 4569 deletions

View 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