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