diff --git a/backend/src/models/synthesis.rs b/backend/src/models/synthesis.rs index 2f09243..8c21261 100644 --- a/backend/src/models/synthesis.rs +++ b/backend/src/models/synthesis.rs @@ -82,6 +82,7 @@ pub struct SynthesisListItem { pub created_at: DateTime, pub first_section_title: Option, pub first_section_item_count: usize, + pub job_id: Option, } impl TryFrom for SynthesisListItem { @@ -102,6 +103,7 @@ impl TryFrom for SynthesisListItem { created_at: s.created_at, first_section_title, first_section_item_count, + job_id: s.job_id, }) } } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e061a5a..7e6592c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -17,6 +17,7 @@ const Sources = lazy(() => import('~/pages/Sources')); const GenerateSynthesis = lazy(() => import('~/pages/GenerateSynthesis')); const SynthesisDetail = lazy(() => import('~/pages/SynthesisDetail')); const ArticleHistory = lazy(() => import('~/pages/ArticleHistory')); +const LlmLogs = lazy(() => import('~/pages/LlmLogs')); const AdminProviders = lazy(() => import('~/pages/admin/Providers')); const AdminRateLimits = lazy(() => import('~/pages/admin/RateLimits')); const AdminUsers = lazy(() => import('~/pages/admin/Users')); @@ -65,6 +66,7 @@ const App: Component = () => { + {/* Admin routes with admin layout wrapper */} diff --git a/frontend/src/__tests__/fixtures.ts b/frontend/src/__tests__/fixtures.ts index 83c5b09..403ec9e 100644 --- a/frontend/src/__tests__/fixtures.ts +++ b/frontend/src/__tests__/fixtures.ts @@ -17,6 +17,7 @@ export const MOCK_SYNTHESIS_LIST_ITEM: SynthesisListItem = { created_at: '2026-03-21T10:00:00Z', first_section_title: 'Annonces majeures', first_section_item_count: 3, + job_id: 'job-test-1', }; export const MOCK_SYNTHESIS_LIST: SynthesisListItem[] = [ diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 2d8cb99..9f46a9d 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -192,26 +192,40 @@ const Home: Component = () => { {t('home.readLink')} → - +
+ {synth.job_id && ( + e.stopPropagation()} + > + + + + + )} + +
)} diff --git a/frontend/src/pages/LlmLogs.tsx b/frontend/src/pages/LlmLogs.tsx new file mode 100644 index 0000000..eb360e8 --- /dev/null +++ b/frontend/src/pages/LlmLogs.tsx @@ -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 = { + 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([]); + const [loading, setLoading] = createSignal(true); + const [error, setError] = createSignal(null); + + onMount(async () => { + try { + const data = await llmLogsApi.getByJobId(params.jobId); + setLogs(data); + } catch { + setError(t('common.error')); + } finally { + setLoading(false); + } + }); + + return ( +
+ + +

{t('llmLogs.title')}

+ + }> + +
+ {error()} +
+
+ + 0} + fallback={ + +

{t('llmLogs.empty')}

+
+ } + > +
+ + {(entry) => ( +
+
+ + {entry.call_type} + + {entry.model} + + {formatDuration(entry.duration_ms)} + +
+ +
+
+ + + {t('llmLogs.systemPrompt')} + +
+
+                          {entry.system_prompt}
+                        
+
+
+ +
+ + + {t('llmLogs.userPrompt')} + +
+
+                          {entry.user_prompt}
+                        
+
+
+ +
+ + + {t('llmLogs.response')} + +
+
+                          {prettyResponse(entry.response_body)}
+                        
+
+
+
+
+ )} +
+
+
+
+
+ ); +}; + +export default LlmLogs; diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 18abf33..481210d 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -136,6 +136,7 @@ export interface SynthesisListItem { created_at: string; first_section_title: string | null; first_section_item_count: number; + job_id: string | null; } export interface GenerateResponse {