mono/packages/ui/src/hooks/usePageGenerator.ts

179 lines
7.2 KiB
TypeScript

import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { toast } from 'sonner';
import { T, translate } from '@/i18n';
import { useAuth } from '@/hooks/useAuth';
import { transcribeAudio, runTools } from '@/lib/openai';
import { useLog } from '@/contexts/LogContext';
import { createPageInDb } from '@/lib/pageTools';
// Define the states for the page generation process
type GenerationStatus = 'idle' | 'transcribing' | 'generating' | 'creating' | 'success' | 'error';
export const usePageGenerator = () => {
const [status, setStatus] = useState<GenerationStatus>('idle');
const [error, setError] = useState<string | null>(null);
const { user } = useAuth();
const navigate = useNavigate();
const { addLog } = useLog();
const cancelGeneration = () => {
addLog('info', '[PageGenerator] User cancelled page generation.');
setStatus('idle');
toast.warning(translate('Page generation cancelled.'));
};
const generatePageFromText = async (prompt: string, options: { useImageTools: boolean; model?: string; imageModel?: string; referenceImages?: string[]; parentId?: string }) => {
if (!user) {
toast.error(translate('You must be logged in to create a page.'));
return;
}
setStatus('generating');
setError(null);
addLog('info', `[PageGenerator] Starting page generation with prompt: ${prompt.substring(0, 50)}...`);
toast.info(translate('Generating page content...'));
try {
const preset = options.useImageTools ? 'page-generator' : 'page-generator-text-only';
addLog('debug', `[PageGenerator] Using AI preset: ${preset} with model: ${options.model || 'default'} (Image model: ${options.imageModel || 'default'})`);
// Stage 1: AI generates content and calls the appropriate tool
const effectivePrompt = options.useImageTools && options.imageModel
? `${prompt}\n\n[System Instruction: When generating images, YOU MUST set the 'model' argument to "${options.imageModel}" in the generate_text_with_images tool.]`
: prompt;
const result = await runTools({
prompt: effectivePrompt,
preset,
userId: user.id,
addLog,
model: options.model,
images: options.referenceImages
});
addLog('debug', `[PageGenerator] AI content generation completed. ${JSON.stringify(result)}`);
if (!result.success) {
throw new Error(result.error || 'The AI failed to generate the page content.');
}
// Stage 2: Handle the result
let finalPageResult;
// Check if the AI called the 'create_page' tool directly (backward compatibility)
const createPageCall = result.toolCalls?.find(tc => tc.function?.name === 'create_page');
if (createPageCall && createPageCall.function.output) {
addLog('debug', '[PageGenerator] Found direct create_page tool call.');
finalPageResult = createPageCall.function.output;
} else {
// Fallback: Check if we have content from 'generate_text_with_images' or raw content
addLog('debug', '[PageGenerator] No create_page tool call found. Attempting to create page from generated content.');
let content = '';
const genTextToolCall = result.toolCalls?.find(tc => tc.function?.name === 'generate_text_with_images');
if (genTextToolCall && genTextToolCall.function.output?.content) {
content = genTextToolCall.function.output.content;
addLog('debug', '[PageGenerator] Extracted content from generate_text_with_images tool.');
} else if (typeof result.content === 'string' && result.content.trim()) {
content = result.content; // Use raw content if available
addLog('debug', '[PageGenerator] Using raw message content.');
}
if (!content) {
throw new Error('AI failed to call the create_page tool and no valid content was found in the response.');
}
// Generate a title from the prompt or content
// Simple strategy: Use the first heading, or the first few words of the prompt
let title = 'Generated Page';
const titleMatch = content.match(/^#\s+(.+)$/m);
if (titleMatch) {
title = titleMatch[1].trim();
} else {
// Fallback to prompt-based title
title = prompt.split('\n')[0].substring(0, 50).trim();
if (prompt.length > 50) title += '...';
}
setStatus('creating');
addLog('info', `[PageGenerator] Creating page manually with title: "${title}"`);
// Call the createPageInDb function directly
finalPageResult = await createPageInDb(user.id, {
title,
content,
is_public: false,
visible: true,
parent: options.parentId
}, addLog);
}
if (!finalPageResult.success) {
throw new Error(`Failed to save page: ${finalPageResult.error}`);
}
const newPageUrl = finalPageResult.url || `/user/${user.id}/pages/${finalPageResult.slug}`;
setStatus('success');
toast.success(translate('Page created! Redirecting you now...'));
addLog('info', `[PageGenerator] Navigating to new page: ${newPageUrl}`);
setTimeout(() => navigate(newPageUrl), 1000);
return newPageUrl;
} catch (e: any) {
addLog('error', '[PageGenerator] Page generation from text failed.', e);
const errorMessage = e.message || translate('An unknown error occurred during page generation.');
setError(errorMessage);
setStatus('error');
toast.error(errorMessage);
}
};
const generatePageFromVoice = async (audioBlob: Blob, options: { useImageTools: boolean }) => {
if (!user) {
toast.error(translate('You must be logged in to create a page.'));
return;
}
setStatus('transcribing');
setError(null);
addLog('info', '[PageGenerator] Starting page generation from voice...');
toast.info(translate('Transcribing audio...'));
try {
// 1. Transcribe Audio
const audioFile = new File([audioBlob], "voice_prompt.webm", { type: "audio/webm" });
const transcribedText = await transcribeAudio(audioFile);
if (!transcribedText) {
throw new Error('Transcription failed or returned empty.');
}
addLog('info', `[PageGenerator] Transcription complete: ${transcribedText.substring(0, 100)}...`);
toast.success(translate('Transcription complete!'));
// 2. Defer to the text-based generation function
addLog('debug', '[PageGenerator] Deferring to text-based generation from voice input.');
await generatePageFromText(transcribedText, options);
} catch (e: any) {
addLog('error', '[PageGenerator] Page generation from voice failed.', e);
const errorMessage = e.message || translate('An unknown error occurred during page generation.');
setError(errorMessage);
setStatus('error');
toast.error(errorMessage);
}
};
return {
status,
error,
generatePageFromVoice,
generatePageFromText,
isGenerating: status !== 'idle' && status !== 'success' && status !== 'error',
cancelGeneration,
};
};