|
|
|
|
@ -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()}>
|
|
|
|
|
|