feat: Phase 3 — add theme dropdown to Generate page

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
master
oabrivard 3 months ago
parent d9bfed7ff5
commit ed29e74b1b

@ -74,8 +74,8 @@ export const synthesesApi = {
api.delete<void>(`/syntheses/${id}`), api.delete<void>(`/syntheses/${id}`),
/** POST /syntheses/generate -- kick off an async generation job, returns a job ID. */ /** POST /syntheses/generate -- kick off an async generation job, returns a job ID. */
generate: (): Promise<GenerateResponse> => generate: (themeId: string): Promise<GenerateResponse> =>
api.post<GenerateResponse>('/syntheses/generate'), api.post<GenerateResponse>('/syntheses/generate', { theme_id: themeId }),
/** Build the SSE endpoint URL for streaming generation progress. */ /** Build the SSE endpoint URL for streaming generation progress. */
progressUrl: (jobId: string): string => progressUrl: (jobId: string): string =>

@ -83,6 +83,9 @@ const fr = {
'generate.model': 'Modele', 'generate.model': 'Modele',
'generate.noWebSearchWarning': 'generate.noWebSearchWarning':
'Le fournisseur selectionne ne supporte pas la recherche web. Les resultats seront bases uniquement sur les connaissances du modele.', 'Le fournisseur selectionne ne supporte pas la recherche web. Les resultats seront bases uniquement sur les connaissances du modele.',
'generate.selectTheme': 'Theme a generer',
'generate.noThemes': 'Aucun theme configure. Creez un theme pour pouvoir generer une synthese.',
'generate.createThemeLink': 'Creer un theme',
// Synthesis Detail // Synthesis Detail
'synthesis.title': 'Synthese de la Semaine {week}', 'synthesis.title': 'Synthese de la Semaine {week}',

@ -7,12 +7,13 @@ import {
Show, Show,
For, For,
} from 'solid-js'; } from 'solid-js';
import { useNavigate } from '@solidjs/router'; import { useNavigate, A } from '@solidjs/router';
import { AlertCircle, AlertTriangle, CheckCircle, Circle, Loader2 } from 'lucide-solid'; import { AlertCircle, AlertTriangle, CheckCircle, Circle, Loader2 } from 'lucide-solid';
import { useI18n } from '~/i18n'; import { useI18n } from '~/i18n';
import { synthesesApi } from '~/api/syntheses'; import { synthesesApi } from '~/api/syntheses';
import { settingsApi } from '~/api/settings'; import { settingsApi } from '~/api/settings';
import { configApi } from '~/api/config'; import { configApi } from '~/api/config';
import { themesApi, type ThemeResponse } from '~/api/themes';
import { isApiError, DEFAULT_SETTINGS } from '~/types'; import { isApiError, DEFAULT_SETTINGS } from '~/types';
import type { UserSettings, ProviderConfig, ProgressEvent } from '~/types'; import type { UserSettings, ProviderConfig, ProgressEvent } from '~/types';
import { createSSEConnection, type SSEConnection, type SSEStatus } from '~/utils/sse'; import { createSSEConnection, type SSEConnection, type SSEStatus } from '~/utils/sse';
@ -52,6 +53,8 @@ const GenerateSynthesis: Component = () => {
const [settings, setSettings] = createSignal<UserSettings>({ ...DEFAULT_SETTINGS }); const [settings, setSettings] = createSignal<UserSettings>({ ...DEFAULT_SETTINGS });
const [providers, setProviders] = createSignal<ProviderConfig[]>([]); const [providers, setProviders] = createSignal<ProviderConfig[]>([]);
const [themes, setThemes] = createSignal<ThemeResponse[]>([]);
const [selectedThemeId, setSelectedThemeId] = createSignal<string>('');
const [loadingSettings, setLoadingSettings] = createSignal(true); const [loadingSettings, setLoadingSettings] = createSignal(true);
const [generating, setGenerating] = createSignal(false); const [generating, setGenerating] = createSignal(false);
const [error, setError] = createSignal<string | null>(null); const [error, setError] = createSignal<string | null>(null);
@ -60,15 +63,20 @@ const GenerateSynthesis: Component = () => {
onMount(async () => { onMount(async () => {
try { try {
const [data, providerList] = await Promise.all([ const [data, providerList, themeList] = await Promise.all([
settingsApi.get().catch((err) => { settingsApi.get().catch((err) => {
if (isApiError(err) && err.status === 404) return null; if (isApiError(err) && err.status === 404) return null;
throw err; throw err;
}), }),
configApi.listProviders().catch(() => [] as ProviderConfig[]), configApi.listProviders().catch(() => [] as ProviderConfig[]),
themesApi.list().catch(() => [] as ThemeResponse[]),
]); ]);
if (data) setSettings(data); if (data) setSettings(data);
setProviders(providerList); setProviders(providerList);
setThemes(themeList);
if (themeList.length > 0) {
setSelectedThemeId(themeList[0].id);
}
} catch { } catch {
// Non-404 settings error — use defaults silently // Non-404 settings error — use defaults silently
} finally { } finally {
@ -210,7 +218,7 @@ const GenerateSynthesis: Component = () => {
setSuccess(false); setSuccess(false);
try { try {
const response = await synthesesApi.generate(); const response = await synthesesApi.generate(selectedThemeId());
const url = synthesesApi.progressUrl(response.job_id); const url = synthesesApi.progressUrl(response.job_id);
const conn = createSSEConnection(url); const conn = createSSEConnection(url);
setSSEConnection(conn); setSSEConnection(conn);
@ -264,6 +272,39 @@ const GenerateSynthesis: Component = () => {
</p> </p>
</div> </div>
{/* Theme selection */}
<Show
when={themes().length > 0}
fallback={
<div class="mt-4 bg-yellow-50 border border-yellow-200 rounded-md p-4">
<p class="text-sm text-yellow-800">
{t('generate.noThemes')}{' '}
<A href="/themes" class="font-medium underline hover:text-yellow-900">
{t('generate.createThemeLink')}
</A>
</p>
</div>
}
>
<div class="mt-4">
<label for="theme-select" class="block text-sm font-medium text-gray-700">
{t('generate.selectTheme')}
</label>
<select
id="theme-select"
value={selectedThemeId()}
onInput={(e) => setSelectedThemeId(e.currentTarget.value)}
class="mt-1 block w-full sm:w-64 rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
>
<For each={themes()}>
{(theme) => (
<option value={theme.id}>{theme.name}</option>
)}
</For>
</select>
</div>
</Show>
{/* No web search warning */} {/* No web search warning */}
<Show when={settings().ai_provider && !hasWebSearch()}> <Show when={settings().ai_provider && !hasWebSearch()}>
<div class="mt-4 bg-yellow-50 border border-yellow-200 rounded-md p-4"> <div class="mt-4 bg-yellow-50 border border-yellow-200 rounded-md p-4">
@ -382,7 +423,7 @@ const GenerateSynthesis: Component = () => {
<button <button
type="button" type="button"
onClick={handleGenerate} onClick={handleGenerate}
disabled={generating() || success()} disabled={generating() || success() || !selectedThemeId()}
class="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:text-sm disabled:opacity-50 disabled:cursor-not-allowed w-full sm:w-auto" class="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:text-sm disabled:opacity-50 disabled:cursor-not-allowed w-full sm:w-auto"
> >
<Show when={generating() && !success()}> <Show when={generating() && !success()}>

Loading…
Cancel
Save