mirror of
https://github.com/lucasrcsantana/story-generator.git
synced 2025-12-18 06:17:56 +00:00
303 lines
9.2 KiB
TypeScript
303 lines
9.2 KiB
TypeScript
import React from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { supabase } from '../../lib/supabase';
|
|
import { useSession } from '../../hooks/useSession';
|
|
import { useStoryCategories } from '../../hooks/useStoryCategories';
|
|
import { Wand2, ArrowLeft, ArrowRight } from 'lucide-react';
|
|
import { useStudentTracking } from '../../hooks/useStudentTracking';
|
|
|
|
interface Category {
|
|
id: string;
|
|
slug: string;
|
|
title: string;
|
|
description: string;
|
|
icon: string;
|
|
}
|
|
|
|
interface StoryStep {
|
|
title: string;
|
|
key?: keyof StoryChoices;
|
|
items?: Category[];
|
|
isContextStep?: boolean;
|
|
}
|
|
|
|
interface StoryChoices {
|
|
theme_id: string | null;
|
|
subject_id: string | null;
|
|
character_id: string | null;
|
|
setting_id: string | null;
|
|
context?: string;
|
|
}
|
|
|
|
export function StoryGenerator() {
|
|
const navigate = useNavigate();
|
|
const { session } = useSession();
|
|
const { themes, subjects, characters, settings, isLoading } = useStoryCategories();
|
|
const [step, setStep] = React.useState(1);
|
|
const [choices, setChoices] = React.useState<StoryChoices>({
|
|
theme_id: null,
|
|
subject_id: null,
|
|
character_id: null,
|
|
setting_id: null,
|
|
context: ''
|
|
});
|
|
const [isGenerating, setIsGenerating] = React.useState(false);
|
|
const [error, setError] = React.useState<string | null>(null);
|
|
const [generationStatus, setGenerationStatus] = React.useState<
|
|
'idle' | 'creating' | 'generating-images' | 'saving'
|
|
>('idle');
|
|
const { trackStoryGenerated } = useStudentTracking();
|
|
const startTime = React.useRef(Date.now());
|
|
|
|
const steps: StoryStep[] = [
|
|
{
|
|
title: 'Escolha o Tema',
|
|
items: themes || [],
|
|
key: 'theme_id'
|
|
},
|
|
{
|
|
title: 'Escolha a Disciplina',
|
|
items: subjects || [],
|
|
key: 'subject_id'
|
|
},
|
|
{
|
|
title: 'Escolha o Personagem',
|
|
items: characters || [],
|
|
key: 'character_id'
|
|
},
|
|
{
|
|
title: 'Escolha o Cenário',
|
|
items: settings || [],
|
|
key: 'setting_id'
|
|
},
|
|
{
|
|
title: 'Adicione um Contexto (Opcional)',
|
|
isContextStep: true
|
|
}
|
|
];
|
|
|
|
const currentStep = steps[step - 1];
|
|
const isLastStep = step === steps.length;
|
|
|
|
const handleSelect = (key: keyof StoryChoices, value: string) => {
|
|
setChoices(prev => ({ ...prev, [key]: value }));
|
|
if (step < steps.length) {
|
|
setStep(prev => prev + 1);
|
|
}
|
|
};
|
|
|
|
const handleContextChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
setChoices(prev => ({ ...prev, context: event.target.value }));
|
|
};
|
|
|
|
const handleBack = () => {
|
|
if (step > 1) {
|
|
setStep(prev => prev - 1);
|
|
}
|
|
};
|
|
|
|
const handleGenerate = async () => {
|
|
if (!session?.user?.id) return;
|
|
|
|
if (!choices.theme_id || !choices.subject_id || !choices.character_id || !choices.setting_id) {
|
|
setError('Por favor, preencha todas as escolhas antes de continuar.');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setIsGenerating(true);
|
|
setError(null);
|
|
setGenerationStatus('creating');
|
|
|
|
const { data: story, error: storyError } = await supabase
|
|
.from('stories')
|
|
.insert({
|
|
student_id: session.user.id,
|
|
title: 'Gerando...',
|
|
theme_id: choices.theme_id,
|
|
subject_id: choices.subject_id,
|
|
character_id: choices.character_id,
|
|
setting_id: choices.setting_id,
|
|
context: choices.context || null,
|
|
status: 'draft',
|
|
content: {
|
|
prompt: choices,
|
|
pages: []
|
|
}
|
|
})
|
|
.select()
|
|
.single();
|
|
|
|
if (storyError) throw storyError;
|
|
|
|
// Tracking da criação da história
|
|
const selectedTheme = themes?.find(t => t.id === choices.theme_id)?.title || '';
|
|
const selectedSubject = subjects?.find(s => s.id === choices.subject_id)?.title || '';
|
|
const selectedCharacter = characters?.find(c => c.id === choices.character_id)?.title || '';
|
|
const selectedSetting = settings?.find(s => s.id === choices.setting_id)?.title || '';
|
|
|
|
trackStoryGenerated({
|
|
story_id: story.id,
|
|
theme: selectedTheme,
|
|
subject: selectedSubject,
|
|
character: selectedCharacter,
|
|
setting: selectedSetting,
|
|
context: choices.context,
|
|
generation_time: Date.now() - startTime.current,
|
|
word_count: 0, // será atualizado após a geração
|
|
student_id: session.user.id,
|
|
school_id: session.user.user_metadata?.school_id,
|
|
class_id: session.user.user_metadata?.class_id
|
|
});
|
|
|
|
setGenerationStatus('generating-images');
|
|
console.log('Chamando Edge Function com:', story);
|
|
|
|
const { data: functionData, error: functionError } = await supabase.functions
|
|
.invoke('generate-story', {
|
|
body: { record: story }
|
|
});
|
|
|
|
console.log('Resposta da Edge Function:', functionData);
|
|
|
|
if (functionError) {
|
|
throw new Error(`Erro na Edge Function: ${functionError.message}`);
|
|
}
|
|
|
|
setGenerationStatus('saving');
|
|
const { data: updatedStory, error: updateError } = await supabase
|
|
.from('stories')
|
|
.select('*')
|
|
.eq('id', story.id)
|
|
.single();
|
|
|
|
if (updateError) throw updateError;
|
|
|
|
// Atualizar a contagem de palavras após a geração
|
|
const wordCount = updatedStory.content.pages.reduce((acc: number, page: { text: string }) =>
|
|
acc + page.text.split(/\s+/).length, 0);
|
|
|
|
await supabase.from('story_metrics').insert({
|
|
story_id: story.id,
|
|
word_count: wordCount,
|
|
generation_time: Date.now() - startTime.current
|
|
});
|
|
|
|
navigate(`/aluno/historias/${story.id}`);
|
|
} catch (err) {
|
|
console.error('Erro ao gerar história:', err);
|
|
setError('Não foi possível criar sua história. Tente novamente.');
|
|
} finally {
|
|
setIsGenerating(false);
|
|
setGenerationStatus('idle');
|
|
}
|
|
};
|
|
|
|
const getGenerationStatusText = () => {
|
|
switch (generationStatus) {
|
|
case 'creating':
|
|
return 'Iniciando criação...';
|
|
case 'generating-images':
|
|
return 'Gerando história e imagens...';
|
|
case 'saving':
|
|
return 'Finalizando...';
|
|
default:
|
|
return 'Criar História Mágica';
|
|
}
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="animate-pulse space-y-8">
|
|
<div className="h-2 bg-gray-200 rounded-full" />
|
|
<div className="grid grid-cols-2 gap-4">
|
|
{[...Array(4)].map((_, i) => (
|
|
<div key={i} className="h-32 bg-gray-200 rounded-xl" />
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-8">
|
|
{/* Progress Bar */}
|
|
<div className="flex gap-2 mb-8">
|
|
{steps.map((s, i) => (
|
|
<div
|
|
key={i}
|
|
className={`h-2 rounded-full flex-1 ${
|
|
i + 1 <= step ? 'bg-purple-600' : 'bg-gray-200'
|
|
}`}
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
<h2 className="text-xl font-medium text-gray-900 mb-6">
|
|
{currentStep.title}
|
|
</h2>
|
|
|
|
{currentStep.isContextStep ? (
|
|
<div className="space-y-4">
|
|
<textarea
|
|
value={choices.context}
|
|
onChange={handleContextChange}
|
|
placeholder="Adicione detalhes ou ideias específicas para sua história..."
|
|
className="w-full h-32 p-4 border border-gray-300 rounded-lg focus:ring-purple-500 focus:border-purple-500"
|
|
/>
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{currentStep.items?.map((item) => (
|
|
<button
|
|
key={item.id}
|
|
onClick={() => handleSelect(currentStep.key!, item.id)}
|
|
className={`p-6 rounded-xl border-2 transition-all text-left ${
|
|
choices[currentStep.key!] === item.id
|
|
? 'border-purple-500 bg-purple-50'
|
|
: 'border-gray-200 hover:border-purple-200 hover:bg-gray-50'
|
|
}`}
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-2xl">{item.icon}</span>
|
|
<div>
|
|
<h3 className="font-medium text-gray-900">{item.title}</h3>
|
|
<p className="text-sm text-gray-600">{item.description}</p>
|
|
</div>
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{error && (
|
|
<div className="p-4 bg-red-50 text-red-600 rounded-lg text-sm">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{/* Navigation Buttons */}
|
|
<div className="flex justify-between pt-6">
|
|
<button
|
|
onClick={handleBack}
|
|
disabled={step === 1}
|
|
className="flex items-center gap-2 px-4 py-2 text-gray-600 disabled:opacity-50"
|
|
>
|
|
<ArrowLeft className="h-5 w-5" />
|
|
Voltar
|
|
</button>
|
|
|
|
{isLastStep && (
|
|
<button
|
|
onClick={handleGenerate}
|
|
disabled={!choices.theme_id || !choices.subject_id || !choices.character_id || !choices.setting_id || isGenerating}
|
|
className="flex items-center gap-2 px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50"
|
|
>
|
|
<Wand2 className="h-5 w-5" />
|
|
{isGenerating ? getGenerationStatusText() : 'Criar História Mágica'}
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|