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}`),
/** POST /syntheses/generate -- kick off an async generation job, returns a job ID. */
generate: (): Promise<GenerateResponse> =>
api.post<GenerateResponse>('/syntheses/generate'),
generate: (themeId: string): Promise<GenerateResponse> =>
api.post<GenerateResponse>('/syntheses/generate', { theme_id: themeId }),
/** Build the SSE endpoint URL for streaming generation progress. */
progressUrl: (jobId: string): string =>

@ -83,6 +83,9 @@ const fr = {
'generate.model': 'Modele',
'generate.noWebSearchWarning':
'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.title': 'Synthese de la Semaine {week}',

@ -7,12 +7,13 @@ import {
Show,
For,
} from 'solid-js';
import { useNavigate } from '@solidjs/router';
import { useNavigate, A } from '@solidjs/router';
import { AlertCircle, AlertTriangle, CheckCircle, Circle, Loader2 } from 'lucide-solid';
import { useI18n } from '~/i18n';
import { synthesesApi } from '~/api/syntheses';
import { settingsApi } from '~/api/settings';
import { configApi } from '~/api/config';
import { themesApi, type ThemeResponse } from '~/api/themes';
import { isApiError, DEFAULT_SETTINGS } from '~/types';
import type { UserSettings, ProviderConfig, ProgressEvent } from '~/types';
import { createSSEConnection, type SSEConnection, type SSEStatus } from '~/utils/sse';
@ -52,6 +53,8 @@ const GenerateSynthesis: Component = () => {
const [settings, setSettings] = createSignal<UserSettings>({ ...DEFAULT_SETTINGS });
const [providers, setProviders] = createSignal<ProviderConfig[]>([]);
const [themes, setThemes] = createSignal<ThemeResponse[]>([]);
const [selectedThemeId, setSelectedThemeId] = createSignal<string>('');
const [loadingSettings, setLoadingSettings] = createSignal(true);
const [generating, setGenerating] = createSignal(false);
const [error, setError] = createSignal<string | null>(null);
@ -60,15 +63,20 @@ const GenerateSynthesis: Component = () => {
onMount(async () => {
try {
const [data, providerList] = await Promise.all([
const [data, providerList, themeList] = await Promise.all([
settingsApi.get().catch((err) => {
if (isApiError(err) && err.status === 404) return null;
throw err;
}),
configApi.listProviders().catch(() => [] as ProviderConfig[]),
themesApi.list().catch(() => [] as ThemeResponse[]),
]);
if (data) setSettings(data);
setProviders(providerList);
setThemes(themeList);
if (themeList.length > 0) {
setSelectedThemeId(themeList[0].id);
}
} catch {
// Non-404 settings error — use defaults silently
} finally {
@ -210,7 +218,7 @@ const GenerateSynthesis: Component = () => {
setSuccess(false);
try {
const response = await synthesesApi.generate();
const response = await synthesesApi.generate(selectedThemeId());
const url = synthesesApi.progressUrl(response.job_id);
const conn = createSSEConnection(url);
setSSEConnection(conn);
@ -264,6 +272,39 @@ const GenerateSynthesis: Component = () => {
</p>
</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 */}
<Show when={settings().ai_provider && !hasWebSearch()}>
<div class="mt-4 bg-yellow-50 border border-yellow-200 rounded-md p-4">
@ -382,7 +423,7 @@ const GenerateSynthesis: Component = () => {
<button
type="button"
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"
>
<Show when={generating() && !success()}>

Loading…
Cancel
Save