feat: LLM logs viewer page + log button on Home synthesis list
- Add LlmLogs page with collapsible prompts/response sections, call-type colored badges, and duration display - Wire /llm-logs/:jobId route in App.tsx (lazy-loaded) - Expose job_id in backend SynthesisListItem and frontend SynthesisListItem type; update test fixture accordingly - Add log-icon link next to delete button on each Home synthesis card Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>master
parent
cbe1cd6507
commit
f9023cff7e
@ -0,0 +1,144 @@
|
||||
import { type Component, createSignal, onMount, For, Show } from 'solid-js';
|
||||
import { useParams, A } from '@solidjs/router';
|
||||
import { useI18n } from '~/i18n';
|
||||
import { llmLogsApi } from '~/api/llmLogs';
|
||||
import type { LlmCallLogEntry } from '~/types';
|
||||
import LoadingSpinner from '~/components/ui/LoadingSpinner';
|
||||
|
||||
const CALL_TYPE_BADGE: Record<string, string> = {
|
||||
search: 'bg-blue-100 text-blue-800',
|
||||
classification_phase1: 'bg-purple-100 text-purple-800',
|
||||
classification_phase2: 'bg-purple-100 text-purple-800',
|
||||
rewrite: 'bg-green-100 text-green-800',
|
||||
link_extraction: 'bg-orange-100 text-orange-800',
|
||||
article_extraction: 'bg-orange-100 text-orange-800',
|
||||
};
|
||||
|
||||
function badgeClass(callType: string): string {
|
||||
return CALL_TYPE_BADGE[callType] ?? 'bg-gray-100 text-gray-800';
|
||||
}
|
||||
|
||||
function formatDuration(ms: number): string {
|
||||
return (ms / 1000).toFixed(1) + 's';
|
||||
}
|
||||
|
||||
function prettyResponse(raw: string): string {
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
return JSON.stringify(parsed, null, 2);
|
||||
} catch {
|
||||
return raw;
|
||||
}
|
||||
}
|
||||
|
||||
const LlmLogs: Component = () => {
|
||||
const { t } = useI18n();
|
||||
const params = useParams<{ jobId: string }>();
|
||||
|
||||
const [logs, setLogs] = createSignal<LlmCallLogEntry[]>([]);
|
||||
const [loading, setLoading] = createSignal(true);
|
||||
const [error, setError] = createSignal<string | null>(null);
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const data = await llmLogsApi.getByJobId(params.jobId);
|
||||
setLogs(data);
|
||||
} catch {
|
||||
setError(t('common.error'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="mb-6">
|
||||
<A
|
||||
href="/"
|
||||
class="inline-flex items-center text-sm text-indigo-600 hover:text-indigo-800"
|
||||
>
|
||||
← {t('llmLogs.back')}
|
||||
</A>
|
||||
</div>
|
||||
|
||||
<h1 class="text-2xl font-bold text-gray-900 mb-6">{t('llmLogs.title')}</h1>
|
||||
|
||||
<Show when={!loading()} fallback={<LoadingSpinner />}>
|
||||
<Show when={error()}>
|
||||
<div class="bg-red-50 border border-red-200 rounded-md p-4 text-sm text-red-800">
|
||||
{error()}
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show
|
||||
when={logs().length > 0}
|
||||
fallback={
|
||||
<Show when={!error()}>
|
||||
<p class="text-gray-500 text-sm">{t('llmLogs.empty')}</p>
|
||||
</Show>
|
||||
}
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<For each={logs()}>
|
||||
{(entry) => (
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
|
||||
<div class="flex items-center gap-3 px-5 py-3 border-b border-gray-100 bg-gray-50">
|
||||
<span
|
||||
class={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${badgeClass(entry.call_type)}`}
|
||||
>
|
||||
{entry.call_type}
|
||||
</span>
|
||||
<span class="text-sm font-medium text-gray-700">{entry.model}</span>
|
||||
<span class="ml-auto text-sm text-gray-500">
|
||||
{formatDuration(entry.duration_ms)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="divide-y divide-gray-100">
|
||||
<details class="group">
|
||||
<summary class="cursor-pointer px-5 py-2 text-sm font-medium text-gray-600 hover:text-gray-900 select-none list-none flex items-center gap-2">
|
||||
<span class="transition-transform group-open:rotate-90">▶</span>
|
||||
{t('llmLogs.systemPrompt')}
|
||||
</summary>
|
||||
<div class="px-5 pb-4">
|
||||
<pre class="whitespace-pre-wrap font-mono text-xs text-gray-700 max-h-64 overflow-y-auto bg-gray-50 rounded p-3">
|
||||
{entry.system_prompt}
|
||||
</pre>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="group">
|
||||
<summary class="cursor-pointer px-5 py-2 text-sm font-medium text-gray-600 hover:text-gray-900 select-none list-none flex items-center gap-2">
|
||||
<span class="transition-transform group-open:rotate-90">▶</span>
|
||||
{t('llmLogs.userPrompt')}
|
||||
</summary>
|
||||
<div class="px-5 pb-4">
|
||||
<pre class="whitespace-pre-wrap font-mono text-xs text-gray-700 max-h-64 overflow-y-auto bg-gray-50 rounded p-3">
|
||||
{entry.user_prompt}
|
||||
</pre>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="group">
|
||||
<summary class="cursor-pointer px-5 py-2 text-sm font-medium text-gray-600 hover:text-gray-900 select-none list-none flex items-center gap-2">
|
||||
<span class="transition-transform group-open:rotate-90">▶</span>
|
||||
{t('llmLogs.response')}
|
||||
</summary>
|
||||
<div class="px-5 pb-4">
|
||||
<pre class="whitespace-pre-wrap font-mono text-xs text-gray-700 max-h-64 overflow-y-auto bg-gray-50 rounded p-3">
|
||||
{prettyResponse(entry.response_body)}
|
||||
</pre>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LlmLogs;
|
||||
Loading…
Reference in New Issue