ManimCat / frontend /src /hooks /useProblemFraming.ts
Bin29's picture
Sync from main: c1ef036 chore: document docker persistence volumes
94e1b2f
import { useCallback, useRef, useState } from 'react';
import { generateProblemFraming } from '../lib/api';
import { loadSettings } from '../lib/settings';
import { getActiveProvider, providerToCustomApiConfig } from '../lib/ai-providers';
import type { OutputMode, ProblemFramingPlan, Quality, ReferenceImage } from '../types/api';
import { useI18n } from '../i18n';
import { loadPrompts } from './usePrompts';
interface GenerationDraft {
concept: string;
quality: Quality;
outputMode: OutputMode;
referenceImages?: ReferenceImage[];
}
interface StartPlanOptions {
request: GenerationDraft;
feedback?: string;
}
interface RefinePlanOptions {
feedback: string;
}
interface UseProblemFramingResult {
status: 'idle' | 'loading' | 'ready' | 'error';
plan: ProblemFramingPlan | null;
error: string | null;
draft: GenerationDraft | null;
startPlan: (options: StartPlanOptions) => Promise<void>;
refinePlan: (options: RefinePlanOptions) => Promise<void>;
reset: () => void;
}
function hasIncompleteCustomProvider(provider: { apiUrl: string; apiKey: string; model: string } | null): boolean {
if (!provider) {
return false;
}
const hasAny = Boolean(provider.apiUrl.trim() || provider.apiKey.trim() || provider.model.trim());
const hasRequired = Boolean(provider.apiUrl.trim() && provider.apiKey.trim() && provider.model.trim());
return hasAny && !hasRequired;
}
export function useProblemFraming(): UseProblemFramingResult {
const { locale, t } = useI18n();
const [status, setStatus] = useState<'idle' | 'loading' | 'ready' | 'error'>('idle');
const [plan, setPlan] = useState<ProblemFramingPlan | null>(null);
const [error, setError] = useState<string | null>(null);
const [draft, setDraft] = useState<GenerationDraft | null>(null);
const [feedbackHistory, setFeedbackHistory] = useState<string[]>([]);
const abortControllerRef = useRef<AbortController | null>(null);
const callPlanner = useCallback(async (
request: GenerationDraft,
feedback?: string,
currentPlan?: ProblemFramingPlan | null,
history?: string[]
) => {
abortControllerRef.current?.abort();
abortControllerRef.current = new AbortController();
const settings = loadSettings();
const activeProvider = getActiveProvider(settings.api);
const customApiConfig = providerToCustomApiConfig(activeProvider);
if (hasIncompleteCustomProvider(activeProvider) && !customApiConfig) {
throw new Error(t('settings.test.needUrlAndKey'));
}
return generateProblemFraming(
{
concept: request.concept,
feedback,
feedbackHistory: history && history.length ? history : undefined,
currentPlan: currentPlan || undefined,
locale,
referenceImages: request.referenceImages,
promptOverrides: loadPrompts(locale),
customApiConfig: customApiConfig || undefined,
},
abortControllerRef.current.signal
);
}, [locale, t]);
const startPlan = useCallback(async ({ request, feedback }: StartPlanOptions) => {
setStatus('loading');
setError(null);
setDraft(request);
const trimmedFeedback = feedback?.trim();
const nextHistory = trimmedFeedback ? [trimmedFeedback] : [];
setFeedbackHistory(nextHistory);
try {
const response = await callPlanner(request, trimmedFeedback, undefined, nextHistory);
setPlan(response.plan);
setStatus('ready');
} catch (err) {
if (err instanceof Error && err.name === 'AbortError') {
return;
}
setStatus('error');
setError(err instanceof Error ? err.message : t('generation.problemFramingFailed'));
}
}, [callPlanner, t]);
const refinePlan = useCallback(async ({ feedback }: RefinePlanOptions) => {
if (!draft) {
return;
}
setStatus('loading');
setError(null);
const trimmedFeedback = feedback.trim();
const nextHistory = trimmedFeedback ? [...feedbackHistory, trimmedFeedback] : feedbackHistory;
try {
const response = await callPlanner(draft, trimmedFeedback, plan, nextHistory);
setPlan(response.plan);
setFeedbackHistory(nextHistory);
setStatus('ready');
} catch (err) {
if (err instanceof Error && err.name === 'AbortError') {
return;
}
setStatus('error');
setError(err instanceof Error ? err.message : t('generation.problemFramingFailed'));
}
}, [callPlanner, draft, feedbackHistory, plan, t]);
const reset = useCallback(() => {
abortControllerRef.current?.abort();
setStatus('idle');
setPlan(null);
setError(null);
setDraft(null);
setFeedbackHistory([]);
}, []);
return {
status,
plan,
error,
draft,
startPlan,
refinePlan,
reset,
};
}