import React, { useState, useEffect, useMemo } from 'react';
import {
Crown, LayoutDashboard, Library, PenTool, Settings, Brain, BookOpen, Map,
GitBranch, GraduationCap, Armchair, Lock, Plus, Trash2, ChevronRight,
X, Loader2, Key, AlertCircle, Save, Sparkles, ArrowLeft, Menu, Feather,
Check, Eye, EyeOff, Pause, Play, Users, MapPin, Shield, Package, Clock,
ShieldCheck, Zap, Skull, Heart, Layers, Flag, KeyRound, Pencil,
Milestone, Wand2, MessageCircle, Send, StickyNote, RotateCcw, Star
} from 'lucide-react';
/* =========================================================
CONSTANTES
========================================================= */
const SIDEBAR_ITEMS = [
{ id: 'painel', label: 'Painel Imperial', icon: LayoutDashboard, active: true },
{ id: 'gaveta', label: 'Gaveta Literária', icon: Library, active: true },
{ id: 'forjar', label: 'Forjar História', icon: PenTool, active: true },
{ id: 'forjar-imperio', label: 'Forjar Império', icon: Crown, active: true },
{ id: 'biblioteca', label: 'Biblioteca Imperial', icon: BookOpen, active: true },
{ id: 'atlas', label: 'Atlas de Universos', icon: Map, active: true },
{ id: 'planejador', label: 'Planejador de Saga', icon: GitBranch, active: true },
{ id: 'mentor', label: 'Mentor Literário', icon: GraduationCap, active: true },
{ id: 'sala-autor', label: 'Sala do Autor', icon: Armchair, active: true },
{ id: 'config', label: 'Configurações', icon: Settings, active: true },
{ id: 'mente-suprema', label: 'Mente Suprema', icon: Brain, active: true },
];
const GENEROS = [
'Fantasia', 'Ficção Científica', 'Romance', 'Suspense/Thriller', 'Terror',
'Drama', 'Aventura', 'Mistério', 'Histórico', 'Distopia', 'Young Adult', 'Literário'
];
const PONTOS_DE_VISTA = [
'Primeira Pessoa',
'Terceira Pessoa Limitada',
'Terceira Pessoa Onisciente',
'Múltiplos Narradores (POV Alternado)'
];
const TONS = [
'Épico e Grandioso', 'Sombrio e Intenso', 'Leve e Esperançoso',
'Melancólico e Reflexivo', 'Tenso e Urgente', 'Satírico e Irônico',
'Romântico', 'Filosófico'
];
const CAPITULOS_SUGERIDOS = [5, 10, 20, 50, 100, 300, 500, 1000];
const CAPA_GRADIENTES = {
'Fantasia': 'linear-gradient(135deg,#3a2a0f,#7a5a1a,#1a1208)',
'Ficção Científica': 'linear-gradient(135deg,#0a1a2a,#1a3a5a,#0a0a14)',
'Romance': 'linear-gradient(135deg,#3a1020,#6a2030,#1a0a10)',
'Suspense/Thriller': 'linear-gradient(135deg,#1a1a1a,#3a3a2a,#0a0a0a)',
'Terror': 'linear-gradient(135deg,#1a0505,#3a0a0a,#050505)',
'Drama': 'linear-gradient(135deg,#2a2010,#4a3a1a,#100a05)',
'Aventura': 'linear-gradient(135deg,#1a2a10,#3a5a1a,#0a140a)',
'Mistério': 'linear-gradient(135deg,#15101f,#2f2040,#0a0510)',
'Histórico': 'linear-gradient(135deg,#2a1f10,#4a3520,#140d05)',
'Distopia': 'linear-gradient(135deg,#1a1a1a,#3a2a1a,#0a0a0a)',
'Young Adult': 'linear-gradient(135deg,#2a1530,#4a2550,#150a1a)',
'Literário': 'linear-gradient(135deg,#1f1810,#3a2f1a,#0d0a05)',
};
const CAPA_DEFAULT = 'linear-gradient(135deg,#1a1610,#2a2410,#0a0805)';
const STATUS_INFO = {
'em-andamento': { label: 'Em Andamento', color: '#d4af37' },
'concluida': { label: 'Concluída', color: '#7fd49a' },
'pausada': { label: 'Pausada', color: '#9a958a' },
};
const MEMORIA_VAZIA = { personagens: [], locais: [], faccoes: [], objetos: [], eventos: [] };
const MEMORIA_CATEGORIAS = [
{ chave: 'personagens', label: 'Personagens', icon: Users },
{ chave: 'locais', label: 'Locais', icon: MapPin },
{ chave: 'faccoes', label: 'Facções', icon: Shield },
{ chave: 'objetos', label: 'Objetos', icon: Package },
{ chave: 'eventos', label: 'Eventos', icon: Clock },
];
const CAMPOS_IMPERIO = [
{ chave: 'descricaoMundo', label: 'Mundo', icon: Map, tipo: 'texto', placeholder: 'Geografia, cosmologia e regras fundamentais deste universo...' },
{ chave: 'sistemaDePoder', label: 'Sistema de Poder', icon: Zap, tipo: 'texto', placeholder: 'Magia, tecnologia, hierarquias de poder...' },
{ chave: 'reinos', label: 'Reinos', icon: Crown, tipo: 'lista', placeholder: 'Um reino por linha' },
{ chave: 'faccoes', label: 'Facções', icon: Shield, tipo: 'lista', placeholder: 'Uma facção por linha' },
{ chave: 'racas', label: 'Raças', icon: Users, tipo: 'lista', placeholder: 'Uma raça por linha' },
{ chave: 'religioes', label: 'Religiões', icon: BookOpen, tipo: 'lista', placeholder: 'Uma religião por linha' },
{ chave: 'misterios', label: 'Mistérios', icon: Eye, tipo: 'texto', placeholder: 'Mistérios não resolvidos do universo...' },
{ chave: 'segredos', label: 'Segredos', icon: KeyRound, tipo: 'texto', placeholder: 'Segredos guardados por personagens ou facções...' },
{ chave: 'conflitos', label: 'Conflitos', icon: Flag, tipo: 'lista', placeholder: 'Um conflito por linha' },
{ chave: 'antagonistas', label: 'Antagonistas', icon: Skull, tipo: 'lista', placeholder: 'Um antagonista por linha' },
{ chave: 'romances', label: 'Romances', icon: Heart, tipo: 'texto', placeholder: 'Relações românticas planejadas...' },
{ chave: 'subtramas', label: 'Subtramas', icon: Layers, tipo: 'texto', placeholder: 'Subtramas planejadas...' },
{ chave: 'plotTwists', label: 'Plot Twists', icon: Sparkles, tipo: 'texto', placeholder: 'Reviravoltas planejadas (a IA usará com o timing correto)...' },
{ chave: 'finalPlanejado', label: 'Final Planejado', icon: Flag, tipo: 'texto', placeholder: 'Como esta saga deve terminar...' },
];
function camposImperioVazios() {
const obj = {};
CAMPOS_IMPERIO.forEach((c) => { obj[c.chave] = ''; });
return obj;
}
const ANALISE_CATEGORIAS = [
{ chave: 'ritmo', label: 'Ritmo' },
{ chave: 'emocao', label: 'Emoção' },
{ chave: 'dialogos', label: 'Diálogos' },
{ chave: 'tensao', label: 'Tensão' },
{ chave: 'construcaoMundo', label: 'Construção de Mundo' },
{ chave: 'consistencia', label: 'Consistência' },
{ chave: 'qualidadeGeral', label: 'Qualidade Geral' },
];
const ACOES_MENTE_SUPREMA = [
{ id: 'planejar', label: 'Planejar Próximos Capítulos', icon: Milestone },
{ id: 'personagem', label: 'Gerar Novo Personagem', icon: Users },
{ id: 'plottwist', label: 'Criar Plot Twist', icon: Sparkles },
{ id: 'dialogos', label: 'Melhorar Diálogos do Último Capítulo', icon: Wand2 },
{ id: 'consistencia', label: 'Analisar Consistência Geral', icon: ShieldCheck },
];
function paraTexto(v) {
if (v == null) return '';
if (typeof v === 'string') return v;
if (Array.isArray(v)) return v.map((x) => (typeof x === 'string' ? x : JSON.stringify(x))).join(', ');
if (typeof v === 'object') return JSON.stringify(v);
return String(v);
}
const NUCLEO_ESCRITORA_SUPREMA = [
'Você é uma escritora literária de elite, com décadas de experiência publicando romances aclamados. Sua escrita NUNCA parece gerada por uma IA.',
'REGRAS ABSOLUTAS DE ESTILO:',
'- Mostre, não explique. Emoções surgem através de ações, expressões, diálogos e decisões - nunca por exposição direta.',
'- Cada personagem tem voz própria: maneira de falar, pensar e agir reconhecível mesmo sem identificação de nome.',
'- Evite frases repetitivas, descrições genéricas, diálogos artificiais, emoções superficiais e finais vazios.',
'- Mantenha ritmo natural, tensão crescente e profundidade psicológica.',
'- Cada capítulo termina com propósito narrativo - nunca enrolação ou preenchimento artificial.',
'- Lembre-se constantemente de medos, desejos, traumas, objetivos e relacionamentos estabelecidos anteriormente na história.',
'- A qualidade deve permanecer idêntica do primeiro ao último capítulo, não importa o tamanho da saga.'
].join('\n');
const STORAGE_KEYS = { stories: 'isf_stories_v1', apiKey: 'isf_api_key_v1', universos: 'isf_universos_v1', notas: 'isf_notas_v1' };
/* =========================================================
STORAGE HELPERS
========================================================= */
function carregarHistorias() {
try {
const raw = localStorage.getItem(STORAGE_KEYS.stories);
const lista = raw ? JSON.parse(raw) : [];
return lista.map((s) => ({
...s,
memoria: s.memoria || { personagens: [], locais: [], faccoes: [], objetos: [], eventos: [] },
planejamento: s.planejamento || { arcos: [] },
ultimaAnalise: s.ultimaAnalise || null,
}));
} catch (e) { return []; }
}
function salvarHistorias(stories) {
try { localStorage.setItem(STORAGE_KEYS.stories, JSON.stringify(stories)); } catch (e) {}
}
function carregarChaveApi() {
try { return localStorage.getItem(STORAGE_KEYS.apiKey) || ''; } catch (e) { return ''; }
}
function salvarChaveApi(key) {
try { localStorage.setItem(STORAGE_KEYS.apiKey, key); } catch (e) {}
}
function carregarUniversos() {
try {
const raw = localStorage.getItem(STORAGE_KEYS.universos);
return raw ? JSON.parse(raw) : [];
} catch (e) { return []; }
}
function salvarUniversos(universos) {
try { localStorage.setItem(STORAGE_KEYS.universos, JSON.stringify(universos)); } catch (e) {}
}
function carregarNotas() {
try {
const raw = localStorage.getItem(STORAGE_KEYS.notas);
return raw ? JSON.parse(raw) : [];
} catch (e) { return []; }
}
function salvarNotas(notas) {
try { localStorage.setItem(STORAGE_KEYS.notas, JSON.stringify(notas)); } catch (e) {}
}
/* =========================================================
UTILS
========================================================= */
function novoId() {
return 'story_' + Date.now() + '_' + Math.random().toString(36).slice(2, 8);
}
function contarPalavras(texto) {
return (texto || '').trim().split(/\s+/).filter(Boolean).length;
}
function formatarData(iso) {
if (!iso) return '-';
const d = new Date(iso);
return d.toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit', year: 'numeric' });
}
/* =========================================================
CHAMADA À API ANTHROPIC
A chave NUNCA fica fixa no código. Ela é lida do localStorage,
preenchida pelo usuário/cliente em Configurações.
========================================================= */
async function chamarIA({ apiKey, systemPrompt, userPrompt, maxTokens }) {
const headers = {
'content-type': 'application/json',
'anthropic-version': '2023-06-01',
};
if (apiKey) {
headers['x-api-key'] = apiKey;
headers['anthropic-dangerous-direct-browser-access'] = 'true';
}
const maxTokensFinal = apiKey ? Math.min(8192, Math.max(512, maxTokens)) : 1000;
const res = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers,
body: JSON.stringify({
model: 'claude-sonnet-4-6',
max_tokens: maxTokensFinal,
system: systemPrompt,
messages: [{ role: 'user', content: userPrompt }],
}),
});
if (!res.ok) {
const txt = await res.text();
throw new Error('Erro ' + res.status + ': ' + txt.slice(0, 220));
}
const data = await res.json();
const bloco = (data.content || []).find((b) => b.type === 'text');
return bloco ? bloco.text : '';
}
async function gerarCapitulo({ apiKey, systemPrompt, userPrompt, maxPalavras }) {
const maxTokens = Math.ceil(maxPalavras * 2.2);
return chamarIA({ apiKey, systemPrompt, userPrompt, maxTokens });
}
function montarSecaoMundoExpandido(mundoAvancado) {
if (!mundoAvancado) return '';
const partes = [];
CAMPOS_IMPERIO.forEach((campo) => {
const valor = (mundoAvancado[campo.chave] || '').trim();
if (!valor) return;
if (campo.tipo === 'lista') {
const itens = valor.split('\n').map((l) => l.trim()).filter(Boolean);
if (itens.length) partes.push(campo.label.toUpperCase() + ': ' + itens.join('; '));
} else {
partes.push(campo.label.toUpperCase() + ': ' + valor);
}
});
if (!partes.length) return '';
return 'MUNDO EXPANDIDO (use estes elementos para enriquecer a história, com timing e sutileza - não despeje tudo de uma vez):\n' + partes.join('\n');
}
function montarPromptCapitulo(story, numero) {
const anterior = story.capitulos[story.capitulos.length - 1];
const contexto = anterior
? 'CONTEXTO DO CAPÍTULO ANTERIOR (mantenha continuidade total - personagens, tom, eventos):\n' + anterior.conteudo.slice(-2500)
: 'Este é o capítulo de abertura da história. Estabeleça o mundo, o tom e os protagonistas de forma envolvente.';
const linhas = [
'HISTÓRIA: ' + story.titulo,
'GÊNERO: ' + story.genero,
'IDEIA PRINCIPAL / SINOPSE: ' + story.sinopse,
'PROTAGONISTAS: ' + story.protagonistas,
'PONTO DE VISTA: ' + story.pov,
'TOM DA NARRATIVA: ' + story.tom,
'OBJETIVO FINAL DA HISTÓRIA: ' + story.objetivoFinal,
];
const secaoMundo = montarSecaoMundoExpandido(story.mundoAvancado);
if (secaoMundo) linhas.push(secaoMundo);
if (story.vozDoAutor && story.vozDoAutor.trim()) {
linhas.push('INSTRUÇÕES ABSOLUTAS DO AUTOR - VOZ DO AUTOR (seguir rigorosamente, têm prioridade sobre tudo): ' + story.vozDoAutor.trim());
}
linhas.push(contexto);
linhas.push('Escreva o CAPÍTULO ' + numero + ' de ' + story.capitulosTotais + ' desta história, com aproximadamente ' + story.palavrasPorCapitulo + ' palavras.');
if (numero === story.capitulosTotais) {
linhas.push('Este é o capítulo FINAL da saga. Conduza a história ao objetivo final de forma satisfatória e memorável.');
}
linhas.push('Responda EXATAMENTE neste formato, sem nenhum texto, comentário ou markdown adicional antes ou depois:');
linhas.push('TÍTULO: [título do capítulo, sem a palavra "Capítulo"]');
linhas.push('---');
linhas.push('[conteúdo integral do capítulo]');
return linhas.join('\n');
}
function interpretarRespostaCapitulo(texto, numero) {
const partes = texto.split('---');
if (partes.length >= 2) {
const titulo = partes[0].replace(/TÍTULO:/i, '').trim();
const conteudo = partes.slice(1).join('---').trim();
return { titulo: titulo || ('Capítulo ' + numero), conteudo };
}
return { titulo: 'Capítulo ' + numero, conteudo: texto.trim() };
}
/* =========================================================
MEMÓRIA VIVA - extração automática de entidades por capítulo
========================================================= */
function montarPromptExtracaoMemoria(story, capitulo) {
const memoriaResumo = JSON.stringify(story.memoria || MEMORIA_VAZIA);
const linhas = [
'MEMÓRIA ATUAL DA HISTÓRIA (JSON):',
memoriaResumo,
'',
'NOVO CAPÍTULO (Nº ' + capitulo.numero + ' - "' + capitulo.titulo + '"):',
capitulo.conteudo,
'',
'Analise o novo capítulo e retorne APENAS um objeto JSON válido, sem markdown, sem texto antes ou depois, com as chaves: personagens, locais, faccoes, objetos, eventos.',
'Cada chave deve conter um array com APENAS as entidades NOVAS ou ALTERADAS neste capítulo (não repita entidades sem mudanças).',
'Formato de cada item:',
'- personagens: {"nome","descricao","papel","status","tracos","relacionamentos"}',
'- locais: {"nome","descricao","tipo"}',
'- faccoes: {"nome","descricao"}',
'- objetos: {"nome","descricao","portador"}',
'- eventos: {"titulo","descricao"}',
'Todos os campos de texto devem ser strings simples (nunca objetos ou arrays aninhados).',
'Se nada novo ou alterado em uma categoria, retorne array vazio para ela.',
];
return linhas.join('\n');
}
function extrairJson(texto) {
try {
const limpo = texto.replace(/```json/gi, '').replace(/```/g, '').trim();
return JSON.parse(limpo);
} catch (e) {
return null;
}
}
function mesclarMemoria(memoriaAtual, delta, numeroCapitulo) {
const base = memoriaAtual || MEMORIA_VAZIA;
const resultado = {
personagens: [...(base.personagens || [])],
locais: [...(base.locais || [])],
faccoes: [...(base.faccoes || [])],
objetos: [...(base.objetos || [])],
eventos: [...(base.eventos || [])],
};
MEMORIA_CATEGORIAS.forEach((cat) => {
const novos = (delta && Array.isArray(delta[cat.chave])) ? delta[cat.chave] : [];
novos.forEach((item) => {
if (!item) return;
const chaveNome = paraTexto(item.nome || item.titulo).trim().toLowerCase();
if (!chaveNome) return;
const idx = resultado[cat.chave].findIndex(
(e) => paraTexto(e.nome || e.titulo).trim().toLowerCase() === chaveNome
);
const entrada = { ...item, atualizadoNoCapitulo: numeroCapitulo };
if (idx >= 0) resultado[cat.chave][idx] = { ...resultado[cat.chave][idx], ...entrada };
else resultado[cat.chave].push(entrada);
});
});
return resultado;
}
async function extrairMemoria({ apiKey, story, capitulo }) {
const systemPrompt = 'Você é um sistema de extração de memória narrativa. Responda SOMENTE com JSON válido, sem markdown, sem comentários, sem texto adicional antes ou depois.';
const userPrompt = montarPromptExtracaoMemoria(story, capitulo);
const resposta = await chamarIA({ apiKey, systemPrompt, userPrompt, maxTokens: 2048 });
const json = extrairJson(resposta);
if (!json) return story.memoria || MEMORIA_VAZIA;
return mesclarMemoria(story.memoria, json, capitulo.numero);
}
/* =========================================================
CONSISTÊNCIA NARRATIVA
========================================================= */
function montarPromptConsistencia(story) {
const memoriaResumo = JSON.stringify(story.memoria || MEMORIA_VAZIA);
const ultimos = story.capitulos
.slice(-2)
.map((c) => 'CAPÍTULO ' + c.numero + ' - ' + c.titulo + '\n' + c.conteudo)
.join('\n\n');
const linhas = [
'MEMÓRIA REGISTRADA DA HISTÓRIA (JSON):',
memoriaResumo,
'',
'CAPÍTULOS RECENTES:',
ultimos,
'',
'Revise os capítulos recentes em busca de: contradições, furos de roteiro, mudanças não explicadas de aparência/idade/personalidade, erros cronológicos ou de continuidade em relação à memória registrada.',
'Responda em português, em formato de lista curta com marcadores "-". Se nada for encontrado, responda apenas: "Nenhuma inconsistência detectada."',
];
return linhas.join('\n');
}
async function verificarConsistencia({ apiKey, story }) {
const systemPrompt = NUCLEO_ESCRITORA_SUPREMA + '\n\nAlém de escrever, você atua agora como revisora de continuidade meticulosa, comparando os capítulos recentes com a memória registrada da história.';
const userPrompt = montarPromptConsistencia(story);
const resposta = await chamarIA({ apiKey, systemPrompt, userPrompt, maxTokens: 1024 });
return resposta.trim();
}
/* =========================================================
MENTOR LITERÁRIO
========================================================= */
async function analisarComMentor({ apiKey, story, capitulo }) {
const systemPrompt = 'Você é uma mentora literária renomada, especialista em estrutura narrativa, ritmo, prosa e construção de mundo. Responda SOMENTE com JSON válido, sem markdown, sem texto adicional.';
const userPrompt = [
'HISTÓRIA: ' + story.titulo,
'GÊNERO: ' + story.genero,
'CAPÍTULO ANALISADO (Nº ' + capitulo.numero + ' - ' + capitulo.titulo + '):',
capitulo.conteudo,
'',
'Avalie este capítulo e retorne APENAS um JSON com esta estrutura exata:',
'{"ritmo":0,"emocao":0,"dialogos":0,"tensao":0,"construcaoMundo":0,"consistencia":0,"qualidadeGeral":0,"pontosFortes":"","sugestoes":""}',
'Todas as notas de 0 a 10 (números). Os textos "pontosFortes" e "sugestoes" em português, concisos (2-4 frases cada).',
].join('\n');
const resposta = await chamarIA({ apiKey, systemPrompt, userPrompt, maxTokens: 1024 });
return extrairJson(resposta);
}
/* =========================================================
PLANEJADOR DE SAGA
========================================================= */
async function gerarPlanoSaga({ apiKey, story }) {
const systemPrompt = 'Você é uma planejadora narrativa especialista em estrutura de sagas longas. Responda SOMENTE com um array JSON válido, sem markdown, sem texto adicional.';
const userPrompt = [
'HISTÓRIA: ' + story.titulo,
'GÊNERO: ' + story.genero,
'SINOPSE: ' + story.sinopse,
'OBJETIVO FINAL: ' + story.objetivoFinal,
'TOTAL DE CAPÍTULOS DA SAGA: ' + story.capitulosTotais,
'',
'Divida esta saga em arcos narrativos cobrindo do capítulo 1 ao ' + story.capitulosTotais + ', sem sobreposição e em ordem.',
'Use entre 2 e 8 arcos, proporcional ao tamanho da saga.',
'Retorne APENAS um array JSON, cada item com a estrutura exata:',
'{"titulo":"","descricao":"","capituloInicio":1,"capituloFim":1}',
].join('\n');
const resposta = await chamarIA({ apiKey, systemPrompt, userPrompt, maxTokens: 1536 });
const json = extrairJson(resposta);
return Array.isArray(json) ? json : [];
}
/* =========================================================
MENTE SUPREMA
========================================================= */
function montarPromptMenteSuprema(acaoId, story, extra) {
const memoriaResumo = JSON.stringify(story.memoria || MEMORIA_VAZIA);
const ultimoCap = story.capitulos[story.capitulos.length - 1];
const base = [
'HISTÓRIA: ' + story.titulo,
'GÊNERO: ' + story.genero,
'SINOPSE: ' + story.sinopse,
'MEMÓRIA REGISTRADA (JSON): ' + memoriaResumo,
];
if (acaoId === 'planejar') {
base.push('PROGRESSO ATUAL: capítulo ' + story.capitulos.length + ' de ' + story.capitulosTotais);
base.push('OBJETIVO FINAL: ' + story.objetivoFinal);
base.push('Sugira, em português, um plano resumido para os próximos 3 a 5 capítulos. Responda em lista com marcadores "-", uma linha por capítulo sugerido.');
} else if (acaoId === 'personagem') {
base.push('Crie um novo personagem secundário coerente com este universo e memória registrada, que poderia ser introduzido em capítulos futuros.');
base.push('Descreva, em um parágrafo conciso: nome, papel na história, aparência, personalidade e motivação.');
} else if (acaoId === 'plottwist') {
if (ultimoCap) base.push('ÚLTIMO CAPÍTULO REGISTRADO (Nº ' + ultimoCap.numero + '): ' + ultimoCap.conteudo.slice(-1500));
base.push('Sugira UMA reviravolta (plot twist) coerente com a história e a memória registrada, que poderia ser usada em capítulos futuros. Explique a ideia e o impacto narrativo em até 4 frases.');
} else if (acaoId === 'dialogos') {
if (ultimoCap) {
base.push('ÚLTIMO CAPÍTULO (Nº ' + ultimoCap.numero + ' - ' + ultimoCap.titulo + '):');
base.push(ultimoCap.conteudo);
} else {
base.push('Esta história ainda não possui capítulos gerados.');
}
base.push('Aponte 2-3 trechos de diálogo deste capítulo que poderiam ficar mais afiados, naturais ou reveladores de personalidade, e sugira versões melhoradas. Responda em lista com marcadores "-".');
} else if (acaoId === 'livre') {
base.push('PERGUNTA / PEDIDO DO AUTOR: ' + extra);
}
return base.join('\n');
}
async function consultarMenteSuprema({ apiKey, story, acaoId, extra }) {
const systemPrompt = NUCLEO_ESCRITORA_SUPREMA + '\n\nVocê também atua como diretora criativa estratégica desta saga, oferecendo conselhos objetivos e acionáveis ao autor.';
const userPrompt = montarPromptMenteSuprema(acaoId, story, extra);
const resposta = await chamarIA({ apiKey, systemPrompt, userPrompt, maxTokens: 1280 });
return resposta.trim();
}
function montarPromptCorrecao(story, capitulo, instrucao) {
return [
'HISTÓRIA: ' + story.titulo,
'MEMÓRIA REGISTRADA (JSON): ' + JSON.stringify(story.memoria || MEMORIA_VAZIA),
'',
'CAPÍTULO ORIGINAL (Nº ' + capitulo.numero + ' - ' + capitulo.titulo + '):',
capitulo.conteudo,
'',
'INSTRUÇÃO DE CORREÇÃO: ' + instrucao,
'',
'Reescreva o capítulo aplicando a instrução, preservando o máximo possível do que já funciona e mantendo total consistência com a memória registrada.',
'Responda EXATAMENTE neste formato, sem texto adicional:',
'TÍTULO: [título do capítulo]',
'---',
'[conteúdo integral revisado]',
].join('\n');
}
async function corrigirCapitulo({ apiKey, story, capitulo, instrucao }) {
const systemPrompt = NUCLEO_ESCRITORA_SUPREMA + '\n\nVocê também atua como editora de texto, revisando e corrigindo capítulos sem perder a essência original.';
const userPrompt = montarPromptCorrecao(story, capitulo, instrucao);
const maxTokens = Math.ceil(contarPalavras(capitulo.conteudo) * 2.5);
const resposta = await chamarIA({ apiKey, systemPrompt, userPrompt, maxTokens });
return interpretarRespostaCapitulo(resposta, capitulo.numero);
}
/* =========================================================
COMPONENTES VISUAIS PEQUENOS
========================================================= */
function SeloImperial({ size = 40 }) {
return (
);
}
function Barra({ progresso }) {
const p = Math.max(0, Math.min(100, progresso));
return (
);
}
function Etiqueta({ children, color }) {
return (
{children}
);
}
/* =========================================================
SIDEBAR
========================================================= */
function Sidebar({ view, setView, sidebarOpen, setSidebarOpen, apiConfigurada }) {
return (
<>
{sidebarOpen && (
setSidebarOpen(false)}
/>
)}
{SIDEBAR_ITEMS.map((item) => {
const Icon = item.icon;
const isActiveView = view === item.id;
return (
{ setView(item.id); setSidebarOpen(false); }}
className={
'w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm transition-colors text-left ' +
(isActiveView ? 'isf-glow' : 'hover:bg-white/[0.03]')
}
style={{
color: isActiveView ? '#f4e4a6' : item.active ? '#d8d3c6' : '#7a766c',
background: isActiveView ? 'rgba(212,175,55,0.08)' : 'transparent',
border: isActiveView ? '1px solid rgba(212,175,55,0.25)' : '1px solid transparent',
}}
>
{item.label}
{!item.active && }
{item.id === 'config' && (
)}
);
})}
PLATAFORMA COMPLETA
>
);
}
/* =========================================================
TOPBAR
========================================================= */
function Topbar({ setSidebarOpen, view, activeStory, apiConfigurada }) {
const titulos = {
painel: 'Painel Imperial',
gaveta: activeStory ? activeStory.titulo : 'Gaveta Literária',
forjar: 'Forjar História',
config: 'Configurações',
};
const titulo = titulos[view] || (SIDEBAR_ITEMS.find((i) => i.id === view) || {}).label || '';
return (
);
}
/* =========================================================
PAINEL IMPERIAL (DASHBOARD)
========================================================= */
function PainelImperial({ stats, stories, setView, setActiveStoryId }) {
const recentes = useMemo(
() => [...stories].sort((a, b) => new Date(b.atualizadoEm) - new Date(a.atualizadoEm)).slice(0, 3),
[stories]
);
const cards = [
{ label: 'Histórias Ativas', valor: stats.historiasAtivas },
{ label: 'Capítulos Gerados', valor: stats.capitulosGerados },
{ label: 'Palavras Escritas', valor: stats.palavrasEscritas.toLocaleString('pt-BR') },
{ label: 'Progresso Médio', valor: stats.progressoMedio + '%' },
];
return (
Bem-vindo ao seu Império Literário
Cada história forjada aqui é registrada, versionada e mantida com consistência absoluta.
HISTÓRIAS RECENTES
{recentes.length === 0 ? (
Sua gaveta está vazia. Forje sua primeira história para começar.
) : (
{recentes.map((s) => (
{ setActiveStoryId(s.id); setView('gaveta'); }}
className="isf-glass rounded-xl p-4 text-left hover:isf-glow transition-shadow"
>
{s.titulo}
{s.capitulos.length} / {s.capitulosTotais} capítulos
))}
)}
MÓDULOS DA PLATAFORMA
{SIDEBAR_ITEMS.filter((i) => i.id !== 'config').map((item) => {
const Icon = item.icon;
return (
{item.label}
{item.active ? 'Ativo' : 'Em Breve'}
);
})}
);
}
/* =========================================================
GAVETA LITERÁRIA
========================================================= */
function GavetaLiteraria({ stories, setActiveStoryId, setView, excluirHistoria }) {
const [confirmarExclusao, setConfirmarExclusao] = useState(null);
if (stories.length === 0) {
return (
Sua gaveta está vazia
Toda grande saga começa com uma faísca. Forje sua primeira história e ela viverá aqui para sempre.
setView('forjar')}
className="isf-display text-xs tracking-widest uppercase px-5 py-2.5 rounded-lg isf-glow"
style={{ background: 'rgba(212,175,55,0.12)', border: '1px solid rgba(212,175,55,0.4)', color: '#f4e4a6' }}
>
Forjar Primeira História
);
}
return (
{stories.map((s) => {
const progresso = (s.capitulos.length / s.capitulosTotais) * 100;
const status = STATUS_INFO[s.status] || STATUS_INFO['em-andamento'];
const palavras = s.capitulos.reduce((acc, c) => acc + contarPalavras(c.conteudo), 0);
return (
{status.label}
{s.titulo}
{s.genero}
{formatarData(s.atualizadoEm)}
{s.sinopse}
{s.capitulos.length} / {s.capitulosTotais} capítulos
{palavras.toLocaleString('pt-BR')} palavras
{ setActiveStoryId(s.id); setView('gaveta'); }}
className="flex-1 isf-display text-[11px] tracking-widest uppercase py-2 rounded-lg"
style={{ background: 'rgba(212,175,55,0.1)', border: '1px solid rgba(212,175,55,0.3)', color: '#f4e4a6' }}
>
Abrir
{confirmarExclusao === s.id ? (
<>
excluirHistoria(s.id)} className="px-3 rounded-lg text-xs" style={{ background: 'rgba(192,88,74,0.18)', border: '1px solid rgba(192,88,74,0.5)', color: '#e0a094' }}>
Confirmar
setConfirmarExclusao(null)} className="px-3 rounded-lg text-xs text-[#9a958a]" style={{ border: '1px solid rgba(255,255,255,0.1)' }}>
>
) : (
setConfirmarExclusao(s.id)} className="px-3 rounded-lg" style={{ border: '1px solid rgba(255,255,255,0.08)', color: '#9a958a' }}>
)}
);
})}
);
}
/* =========================================================
LEITOR DE HISTÓRIA
========================================================= */
function LeitorHistoria({ story, setActiveStoryId, atualizarHistoria, apiKey }) {
const [gerando, setGerando] = useState(false);
const [erro, setErro] = useState('');
const [vozDoAutor, setVozDoAutor] = useState(story.vozDoAutor || '');
const [capituloAberto, setCapituloAberto] = useState(story.capitulos.length ? story.capitulos.length - 1 : null);
const [verificando, setVerificando] = useState(false);
const [consistencia, setConsistencia] = useState(null);
const [mundoAvancado, setMundoAvancado] = useState(story.mundoAvancado || camposImperioVazios());
const [mundoAberto, setMundoAberto] = useState(false);
const proximo = story.capitulos.length + 1;
const concluida = story.capitulos.length >= story.capitulosTotais;
const palavras = story.capitulos.reduce((acc, c) => acc + contarPalavras(c.conteudo), 0);
const memoria = story.memoria || MEMORIA_VAZIA;
const totalMemoria = MEMORIA_CATEGORIAS.reduce((acc, cat) => acc + (memoria[cat.chave] || []).length, 0);
async function handleGerar() {
if (!apiKey) { setErro('Configure sua chave API em Configurações antes de gerar capítulos.'); return; }
setErro('');
setConsistencia(null);
setGerando(true);
try {
const userPrompt = montarPromptCapitulo({ ...story, vozDoAutor, mundoAvancado }, proximo);
const resposta = await gerarCapitulo({
apiKey,
systemPrompt: NUCLEO_ESCRITORA_SUPREMA,
userPrompt,
maxPalavras: story.palavrasPorCapitulo,
});
const { titulo, conteudo } = interpretarRespostaCapitulo(resposta, proximo);
const novoCapitulo = { numero: proximo, titulo, conteudo, geradoEm: new Date().toISOString() };
const novosCapitulos = [...story.capitulos, novoCapitulo];
// Memória Viva: extração best-effort - não bloqueia o capítulo caso falhe
let novaMemoria = story.memoria || MEMORIA_VAZIA;
try {
novaMemoria = await extrairMemoria({ apiKey, story: { ...story, capitulos: novosCapitulos }, capitulo: novoCapitulo });
} catch (memErr) { /* memória é best-effort */ }
atualizarHistoria(story.id, {
capitulos: novosCapitulos,
memoria: novaMemoria,
vozDoAutor,
mundoAvancado,
status: novosCapitulos.length >= story.capitulosTotais ? 'concluida' : story.status,
atualizadoEm: new Date().toISOString(),
});
setCapituloAberto(novosCapitulos.length - 1);
} catch (e) {
setErro(e.message || 'Falha ao gerar capítulo.');
} finally {
setGerando(false);
}
}
async function handleVerificarConsistencia() {
if (!apiKey) { setErro('Configure sua chave API em Configurações para usar a verificação de consistência.'); return; }
setErro('');
setVerificando(true);
try {
const resultado = await verificarConsistencia({ apiKey, story });
setConsistencia(resultado);
} catch (e) {
setErro(e.message || 'Falha ao verificar consistência.');
} finally {
setVerificando(false);
}
}
function alternarPausa() {
atualizarHistoria(story.id, {
status: story.status === 'pausada' ? 'em-andamento' : 'pausada',
atualizadoEm: new Date().toISOString(),
});
}
return (
setActiveStoryId(null)} className="flex items-center gap-2 text-sm text-[#9a958a] hover:text-[#d4af37]">
Voltar à Gaveta
{story.genero}
{story.pov}
{story.tom}
{(STATUS_INFO[story.status] || STATUS_INFO['em-andamento']).label}
{story.titulo}
{story.sinopse}
Capítulos: {story.capitulos.length} / {story.capitulosTotais}
Palavras: {palavras.toLocaleString('pt-BR')}
Criada em: {formatarData(story.criadoEm)}
VOZ DO AUTOR (instruções absolutas para a IA)
setMundoAberto((v) => !v)} className="w-full flex items-center justify-between text-left">
MUNDO EXPANDIDO (Modo Avançado)
{mundoAberto && (
{CAMPOS_IMPERIO.map((campo) => {
const Icon = campo.icon;
return (
{campo.label.toUpperCase()}
);
})}
)}
MEMÓRIA VIVA
{totalMemoria > 0 &&
{totalMemoria} registros }
{totalMemoria === 0 ? (
A memória desta história ainda está vazia. Ela é preenchida automaticamente após cada capítulo forjado.
) : (
{MEMORIA_CATEGORIAS.map((cat) => {
const Icon = cat.icon;
return (
{(memoria[cat.chave] || []).length}
{cat.label}
);
})}
)}
{erro && (
)}
{consistencia && (
RELATÓRIO DE CONSISTÊNCIA
{consistencia}
)}
{!concluida && (
{gerando ? : }
{gerando ? 'Forjando capítulo...' : 'Forjar Capítulo ' + proximo}
)}
{story.capitulos.length > 0 && (
{verificando ? : }
Verificar Consistência
)}
{story.status === 'pausada' ? : }
{story.status === 'pausada' ? 'Retomar' : 'Pausar'}
CAPÍTULOS
{story.capitulos.length === 0 && (
Nenhum capítulo forjado ainda.
)}
{story.capitulos.map((cap, idx) => (
setCapituloAberto(capituloAberto === idx ? null : idx)}
className="w-full flex items-center justify-between gap-3 px-4 py-3 text-left"
>
{String(cap.numero).padStart(2, '0')}
{cap.titulo}
{contarPalavras(cap.conteudo).toLocaleString('pt-BR')} palavras
{capituloAberto === idx && (
)}
))}
);
}
/* =========================================================
FORJAR HISTÓRIA - MODO SIMPLES
========================================================= */
function ForjarHistoria({ criarHistoria, setView, setActiveStoryId, apiConfigurada, universos }) {
const [modoAvancado, setModoAvancado] = useState(false);
const [form, setForm] = useState({
titulo: '', genero: GENEROS[0], sinopse: '', protagonistas: '',
pov: PONTOS_DE_VISTA[0], tom: TONS[0], objetivoFinal: '',
capitulosTotais: 5, palavrasPorCapitulo: 1500,
mundoAvancado: camposImperioVazios(),
});
const [avisoSemChave, setAvisoSemChave] = useState(false);
function set(campo, valor) { setForm((f) => ({ ...f, [campo]: valor })); }
function setMundo(chave, valor) { setForm((f) => ({ ...f, mundoAvancado: { ...f.mundoAvancado, [chave]: valor } })); }
function importarImperio(id) {
const imperio = universos.find((u) => u.id === id);
if (!imperio) return;
const novoMundo = camposImperioVazios();
CAMPOS_IMPERIO.forEach((c) => { novoMundo[c.chave] = imperio[c.chave] || ''; });
setForm((f) => ({ ...f, mundoAvancado: novoMundo }));
}
function handleSubmit(e) {
e.preventDefault();
if (!form.titulo.trim() || !form.sinopse.trim() || !form.objetivoFinal.trim()) return;
const id = criarHistoria({ ...form, mundoAvancado: modoAvancado ? form.mundoAvancado : null });
if (!apiConfigurada) setAvisoSemChave(true);
setActiveStoryId(id);
setView('gaveta');
}
return (
Forjar Nova História
Modo Simples - defina os pilares da sua obra. A IA escreve, você comanda.
{!apiConfigurada && (
Nenhuma chave API configurada. Você pode criar a história agora, mas precisará configurar a chave em Configurações antes de forjar o primeiro capítulo.
)}
);
}
function Campo({ label, children }) {
return (
{label.toUpperCase()}
{children}
);
}
/* =========================================================
CONFIGURAÇÕES
========================================================= */
function Configuracoes({ apiKey, setApiKey }) {
const [valor, setValor] = useState(apiKey);
const [visivel, setVisivel] = useState(false);
const [salvo, setSalvo] = useState(false);
function handleSalvar() {
setApiKey(valor.trim());
salvarChaveApi(valor.trim());
setSalvo(true);
setTimeout(() => setSalvo(false), 2000);
}
return (
Configurações
Conecte sua chave da API Anthropic para ativar a geração de histórias.
SOBRE ESTA CHAVE
A chave é gravada apenas no localStorage do navegador - nunca fica fixa no código-fonte
e nunca é enviada para nenhum servidor além da própria Anthropic. Cada pessoa que usar este site
(incluindo seus clientes finais, se você revender o código) configura a própria chave aqui,
de forma independente.
Em produção (fora do ambiente de preview), o navegador faz a chamada diretamente à API da Anthropic
usando esta chave. Isso funciona, mas expõe a chave no tráfego do navegador do usuário final.
Se preferir uma camada extra de segurança - especialmente ao vender o código para clientes -
o caminho recomendado é um proxy simples (ex: Cloudflare Worker, como no Zenith Imperial) que
recebe a chave por variável de ambiente no servidor. Isso pode ser adicionado na Fase 2 sem
alterar a interface.
);
}
/* =========================================================
FORJAR IMPÉRIO (construção de universos reutilizáveis)
========================================================= */
function ForjarImperio({ universos, criarUniverso, atualizarUniverso, excluirUniverso }) {
const [editandoId, setEditandoId] = useState(null);
const [criando, setCriando] = useState(false);
const [confirmarExclusao, setConfirmarExclusao] = useState(null);
const vazio = () => ({ nome: '', ...camposImperioVazios() });
const [form, setForm] = useState(vazio());
function set(chave, valor) { setForm((f) => ({ ...f, [chave]: valor })); }
function abrirNovo() {
setForm(vazio());
setCriando(true);
setEditandoId(null);
}
function abrirEdicao(u) {
setForm({ ...vazio(), ...u });
setEditandoId(u.id);
setCriando(true);
}
function fechar() {
setCriando(false);
setEditandoId(null);
setForm(vazio());
}
function salvar(e) {
e.preventDefault();
if (!form.nome.trim()) return;
if (editandoId) atualizarUniverso(editandoId, form);
else criarUniverso(form);
fechar();
}
if (!criando) {
return (
Forjar Império
Construa universos completos e reutilizáveis: mundo, facções, reinos, sistema de poder e mais.
Novo Império
{universos.length === 0 ? (
Nenhum império forjado ainda
Crie um universo aqui para reutilizá-lo no Modo Avançado de qualquer história, e visualizá-lo no Atlas de Universos.
) : (
{universos.map((u) => {
const preenchidos = CAMPOS_IMPERIO.filter((c) => (u[c.chave] || '').trim()).length;
return (
{u.nome}
{u.descricaoMundo || 'Sem descrição de mundo ainda.'}
{preenchidos} / {CAMPOS_IMPERIO.length} campos preenchidos
abrirEdicao(u)}
className="flex-1 isf-display text-[11px] tracking-widest uppercase py-2 rounded-lg flex items-center justify-center gap-1.5"
style={{ background: 'rgba(212,175,55,0.1)', border: '1px solid rgba(212,175,55,0.3)', color: '#f4e4a6' }}>
Editar
{confirmarExclusao === u.id ? (
<>
excluirUniverso(u.id)} className="px-3 rounded-lg text-xs" style={{ background: 'rgba(192,88,74,0.18)', border: '1px solid rgba(192,88,74,0.5)', color: '#e0a094' }}>
Confirmar
setConfirmarExclusao(null)} className="px-3 rounded-lg text-xs text-[#9a958a]" style={{ border: '1px solid rgba(255,255,255,0.1)' }}>
>
) : (
setConfirmarExclusao(u.id)} className="px-3 rounded-lg" style={{ border: '1px solid rgba(255,255,255,0.08)', color: '#9a958a' }}>
)}
);
})}
)}
);
}
return (
Voltar
{editandoId ? 'Editar Império' : 'Novo Império'}
Cada campo é opcional, mas quanto mais completo, mais rico o universo disponível para suas histórias.
set('nome', e.target.value)} required
className="isf-input" placeholder="Ex: O Império de Aethros" />
{CAMPOS_IMPERIO.map((campo) => {
const Icon = campo.icon;
return (
set(campo.chave, e.target.value)}
className="isf-input resize-none" style={{ minHeight: '60px' }}
placeholder={campo.placeholder}
/>
);
})}
{editandoId ? 'Salvar Alterações' : 'Forjar Império'}
);
}
/* =========================================================
ATLAS DE UNIVERSOS
========================================================= */
function AtlasUniversos({ universos }) {
const [universoId, setUniversoId] = useState(universos[0] ? universos[0].id : null);
if (universos.length === 0) {
return (
O Atlas está vazio
Forje um Império primeiro - o Atlas exibe visualmente reinos, facções, raças, religiões, conflitos e mais de cada universo.
);
}
const universo = universos.find((u) => u.id === universoId) || universos[0];
return (
IMPÉRIO
setUniversoId(e.target.value)} className="isf-input md:max-w-sm">
{universos.map((u) => {u.nome} )}
{universo.nome}
{universo.descricaoMundo &&
{universo.descricaoMundo}
}
{CAMPOS_IMPERIO.filter((c) => c.chave !== 'descricaoMundo').map((campo) => {
const valor = (universo[campo.chave] || '').trim();
if (!valor) return null;
const Icon = campo.icon;
if (campo.tipo === 'lista') {
const itens = valor.split('\n').map((l) => l.trim()).filter(Boolean);
return (
{campo.label.toUpperCase()}
{itens.map((item, idx) => {item} )}
);
}
return (
{campo.label.toUpperCase()}
{valor}
);
})}
);
}
/* =========================================================
BIBLIOTECA IMPERIAL
========================================================= */
function BibliotecaImperial({ stories }) {
const [storyId, setStoryId] = useState(stories[0] ? stories[0].id : null);
const [categoria, setCategoria] = useState('personagens');
if (stories.length === 0) {
return (
A Biblioteca está vazia
Forje histórias e gere capítulos para que a memória viva - personagens, locais, facções, objetos e eventos - seja registrada automaticamente aqui.
);
}
const story = stories.find((s) => s.id === storyId) || stories[0];
const memoria = story.memoria || MEMORIA_VAZIA;
const itens = memoria[categoria] || [];
return (
HISTÓRIA
setStoryId(e.target.value)} className="isf-input md:max-w-sm">
{stories.map((s) => {s.titulo} )}
{MEMORIA_CATEGORIAS.map((cat) => {
const Icon = cat.icon;
const count = (memoria[cat.chave] || []).length;
const ativo = categoria === cat.chave;
return (
setCategoria(cat.chave)}
className="flex items-center gap-2 px-3 py-2 rounded-lg text-sm"
style={{
background: ativo ? 'rgba(212,175,55,0.12)' : 'transparent',
border: '1px solid ' + (ativo ? 'rgba(212,175,55,0.4)' : 'rgba(255,255,255,0.08)'),
color: ativo ? '#f4e4a6' : '#9a958a',
}}
>
{cat.label}
{count}
);
})}
{itens.length === 0 ? (
Nenhum registro nesta categoria ainda. A memória é preenchida automaticamente conforme novos capítulos são forjados.
) : (
{itens.map((item, idx) => (
{paraTexto(item.nome || item.titulo)}
{item.papel && {paraTexto(item.papel)} }
{item.tipo && {paraTexto(item.tipo)} }
{item.status && {paraTexto(item.status)} }
{item.descricao &&
{paraTexto(item.descricao)}
}
{item.tracos &&
Traços: {paraTexto(item.tracos)}
}
{item.relacionamentos &&
Relações: {paraTexto(item.relacionamentos)}
}
{item.portador &&
Com: {paraTexto(item.portador)}
}
{item.atualizadoNoCapitulo && (
Atualizado no capítulo {item.atualizadoNoCapitulo}
)}
))}
)}
);
}
/* =========================================================
MENTOR LITERÁRIO
========================================================= */
function MentorLiterario({ stories, apiKey, atualizarHistoria }) {
const [storyId, setStoryId] = useState(stories[0] ? stories[0].id : null);
const [capituloIdx, setCapituloIdx] = useState(null);
const [analisando, setAnalisando] = useState(false);
const [erro, setErro] = useState('');
if (stories.length === 0) {
return (
Nenhuma história para analisar
Forje uma história e gere capítulos para que o Mentor Literário possa avaliá-los.
);
}
const story = stories.find((s) => s.id === storyId) || stories[0];
const idx = capituloIdx == null ? story.capitulos.length - 1 : capituloIdx;
const capitulo = story.capitulos[idx];
const analise = story.ultimaAnalise;
async function handleAnalisar() {
if (!apiKey) { setErro('Configure sua chave API em Configurações.'); return; }
if (!capitulo) { setErro('Esta história ainda não possui capítulos.'); return; }
setErro('');
setAnalisando(true);
try {
const resultado = await analisarComMentor({ apiKey, story, capitulo });
if (!resultado) throw new Error('Não foi possível interpretar a análise retornada pela IA.');
atualizarHistoria(story.id, { ultimaAnalise: { ...resultado, capituloNumero: capitulo.numero, analisadoEm: new Date().toISOString() } });
} catch (e) {
setErro(e.message || 'Falha ao analisar capítulo.');
} finally {
setAnalisando(false);
}
}
return (
HISTÓRIA
{ setStoryId(e.target.value); setCapituloIdx(null); }} className="isf-input md:max-w-sm">
{stories.map((s) => {s.titulo} )}
{story.capitulos.length > 0 && (
setCapituloIdx(Number(e.target.value))} className="isf-input md:max-w-xs">
{story.capitulos.map((c, i) => Capítulo {c.numero} - {c.titulo} )}
)}
{story.capitulos.length === 0 ? (
Esta história ainda não possui capítulos forjados.
) : (
<>
{erro && (
)}
{analisando ? : }
{analisando ? 'Analisando...' : 'Analisar Capítulo ' + capitulo.numero}
{analise && (
AVALIAÇÃO - CAPÍTULO {analise.capituloNumero}
{formatarData(analise.analisadoEm)}
{ANALISE_CATEGORIAS.map((cat) => {
const valor = Number(analise[cat.chave]) || 0;
return (
);
})}
{analise.pontosFortes && (
PONTOS FORTES
{analise.pontosFortes}
)}
{analise.sugestoes && (
SUGESTÕES DE MELHORIA
{analise.sugestoes}
)}
)}
>
)}
);
}
/* =========================================================
PLANEJADOR DE SAGA
========================================================= */
function PlanejadorSaga({ stories, apiKey, atualizarHistoria }) {
const [storyId, setStoryId] = useState(stories[0] ? stories[0].id : null);
const [gerando, setGerando] = useState(false);
const [erro, setErro] = useState('');
const [novoArco, setNovoArco] = useState({ titulo: '', descricao: '', capituloInicio: 1, capituloFim: 1 });
if (stories.length === 0) {
return (
Nenhuma saga para planejar
Forje uma história para começar a estruturar arcos narrativos aqui.
);
}
const story = stories.find((s) => s.id === storyId) || stories[0];
const arcos = (story.planejamento && story.planejamento.arcos) || [];
async function handleGerarPlano() {
if (!apiKey) { setErro('Configure sua chave API em Configurações.'); return; }
setErro('');
setGerando(true);
try {
const novosArcos = await gerarPlanoSaga({ apiKey, story });
if (!novosArcos.length) throw new Error('A IA não retornou um plano válido. Tente novamente.');
atualizarHistoria(story.id, { planejamento: { arcos: novosArcos } });
} catch (e) {
setErro(e.message || 'Falha ao gerar plano.');
} finally {
setGerando(false);
}
}
function adicionarArco(e) {
e.preventDefault();
if (!novoArco.titulo.trim()) return;
const arco = {
titulo: novoArco.titulo.trim(),
descricao: novoArco.descricao.trim(),
capituloInicio: Number(novoArco.capituloInicio) || 1,
capituloFim: Number(novoArco.capituloFim) || 1,
};
atualizarHistoria(story.id, { planejamento: { arcos: [...arcos, arco] } });
setNovoArco({ titulo: '', descricao: '', capituloInicio: 1, capituloFim: 1 });
}
function removerArco(idx) {
atualizarHistoria(story.id, { planejamento: { arcos: arcos.filter((_, i) => i !== idx) } });
}
return (
HISTÓRIA
setStoryId(e.target.value)} className="isf-input md:max-w-sm">
{stories.map((s) => {s.titulo} )}
{erro && (
)}
{gerando ? : }
{gerando ? 'Planejando saga...' : (arcos.length ? 'Gerar Novo Plano com IA' : 'Gerar Plano com IA')}
{arcos.length === 0 ? (
Nenhum arco definido ainda.
) : (
arcos.map((arco, idx) => {
const total = story.capitulos.length;
const inicio = Number(arco.capituloInicio) || 1;
const fim = Number(arco.capituloFim) || inicio;
const capsNoArco = Math.max(1, fim - inicio + 1);
const gerados = Math.max(0, Math.min(fim, total) - inicio + 1);
const progresso = (Math.max(0, gerados) / capsNoArco) * 100;
return (
{arco.titulo}
Capítulos {inicio} a {fim}
removerArco(idx)} className="px-2 rounded-lg shrink-0" style={{ border: '1px solid rgba(255,255,255,0.08)', color: '#9a958a' }}>
{arco.descricao &&
{arco.descricao}
}
);
})
)}
);
}
/* =========================================================
SALA DO AUTOR
========================================================= */
function SalaDoAutor({ notas, criarNota, atualizarNota, excluirNota }) {
const [editandoId, setEditandoId] = useState(null);
const [form, setForm] = useState({ titulo: '', conteudo: '' });
const [confirmarExclusao, setConfirmarExclusao] = useState(null);
function abrirNova() {
setForm({ titulo: '', conteudo: '' });
setEditandoId('nova');
}
function abrirEdicao(n) {
setForm({ titulo: n.titulo, conteudo: n.conteudo });
setEditandoId(n.id);
}
function fechar() {
setEditandoId(null);
setForm({ titulo: '', conteudo: '' });
}
function salvar(e) {
e.preventDefault();
if (!form.titulo.trim() && !form.conteudo.trim()) { fechar(); return; }
if (editandoId === 'nova') criarNota(form);
else atualizarNota(editandoId, form);
fechar();
}
if (editandoId) {
return (
);
}
return (
Sala do Autor
Seu espaço pessoal para anotações, referências e processo criativo.
Nova Nota
{notas.length === 0 ? (
Nenhuma anotação ainda
Use este espaço para guardar referências, ideias soltas e reflexões sobre seu processo criativo.
) : (
{notas.map((n) => (
{n.titulo || 'Sem título'}
{n.conteudo}
{formatarData(n.atualizadoEm)}
abrirEdicao(n)} className="flex-1 isf-display text-[11px] tracking-widest uppercase py-2 rounded-lg flex items-center justify-center gap-1.5"
style={{ background: 'rgba(212,175,55,0.1)', border: '1px solid rgba(212,175,55,0.3)', color: '#f4e4a6' }}>
Editar
{confirmarExclusao === n.id ? (
<>
excluirNota(n.id)} className="px-3 rounded-lg text-xs" style={{ background: 'rgba(192,88,74,0.18)', border: '1px solid rgba(192,88,74,0.5)', color: '#e0a094' }}>Confirmar
setConfirmarExclusao(null)} className="px-3 rounded-lg text-xs text-[#9a958a]" style={{ border: '1px solid rgba(255,255,255,0.1)' }}>
>
) : (
setConfirmarExclusao(n.id)} className="px-3 rounded-lg" style={{ border: '1px solid rgba(255,255,255,0.08)', color: '#9a958a' }}>
)}
))}
)}
);
}
/* =========================================================
MENTE SUPREMA
========================================================= */
function MenteSuprema({ stories, apiKey, atualizarHistoria }) {
const [storyId, setStoryId] = useState(stories[0] ? stories[0].id : null);
const [mensagens, setMensagens] = useState([]);
const [pergunta, setPergunta] = useState('');
const [carregando, setCarregando] = useState(false);
const [erro, setErro] = useState('');
const [capituloIdx, setCapituloIdx] = useState('');
const [instrucao, setInstrucao] = useState('');
const [corrigindo, setCorrigindo] = useState(false);
const [revisao, setRevisao] = useState(null);
if (stories.length === 0) {
return (
A Mente Suprema aguarda
Forje sua primeira história para ativar a central estratégica de IA.
);
}
const story = stories.find((s) => s.id === storyId) || stories[0];
async function executarAcao(acao) {
if (!apiKey) { setErro('Configure sua chave API em Configurações.'); return; }
setErro('');
setMensagens((m) => [...m, { tipo: 'usuario', texto: acao.label }]);
if (acao.id === 'consistencia' && story.capitulos.length === 0) {
setMensagens((m) => [...m, { tipo: 'ia', texto: 'Esta história ainda não possui capítulos para revisar. Forje o primeiro capítulo e tente novamente.' }]);
return;
}
setCarregando(true);
try {
const resposta = acao.id === 'consistencia'
? await verificarConsistencia({ apiKey, story })
: await consultarMenteSuprema({ apiKey, story, acaoId: acao.id });
setMensagens((m) => [...m, { tipo: 'ia', texto: resposta }]);
} catch (e) {
setErro(e.message || 'Falha ao consultar a Mente Suprema.');
} finally {
setCarregando(false);
}
}
async function enviarPergunta(e) {
e.preventDefault();
if (!pergunta.trim()) return;
if (!apiKey) { setErro('Configure sua chave API em Configurações.'); return; }
setErro('');
const texto = pergunta.trim();
setMensagens((m) => [...m, { tipo: 'usuario', texto }]);
setPergunta('');
setCarregando(true);
try {
const resposta = await consultarMenteSuprema({ apiKey, story, acaoId: 'livre', extra: texto });
setMensagens((m) => [...m, { tipo: 'ia', texto: resposta }]);
} catch (e) {
setErro(e.message || 'Falha ao consultar a Mente Suprema.');
} finally {
setCarregando(false);
}
}
async function handleCorrigir() {
if (!apiKey) { setErro('Configure sua chave API em Configurações.'); return; }
if (capituloIdx === '' || !story.capitulos[capituloIdx]) { setErro('Selecione um capítulo para corrigir.'); return; }
if (!instrucao.trim()) { setErro('Descreva o que deve ser corrigido.'); return; }
setErro('');
setCorrigindo(true);
setRevisao(null);
try {
const resultado = await corrigirCapitulo({ apiKey, story, capitulo: story.capitulos[capituloIdx], instrucao: instrucao.trim() });
setRevisao(resultado);
} catch (e) {
setErro(e.message || 'Falha ao corrigir capítulo.');
} finally {
setCorrigindo(false);
}
}
function aplicarRevisao() {
if (!revisao || capituloIdx === '') return;
const idx = Number(capituloIdx);
const novosCapitulos = story.capitulos.map((c, i) => (i === idx ? { ...c, titulo: revisao.titulo, conteudo: revisao.conteudo } : c));
atualizarHistoria(story.id, { capitulos: novosCapitulos, atualizadoEm: new Date().toISOString() });
setRevisao(null);
setInstrucao('');
}
return (
Mente Suprema
Central estratégica de IA para esta história.
HISTÓRIA
{ setStoryId(e.target.value); setMensagens([]); setRevisao(null); setCapituloIdx(''); }} className="isf-input md:max-w-sm">
{stories.map((s) => {s.titulo} )}
{erro && (
)}
{ACOES_MENTE_SUPREMA.map((acao) => {
const Icon = acao.icon;
return (
executarAcao(acao)} disabled={carregando}
className="flex items-center gap-2 px-3 py-2 rounded-lg text-xs disabled:opacity-50"
style={{ border: '1px solid rgba(212,175,55,0.25)', color: '#d4af37' }}>
{acao.label}
);
})}
{mensagens.length === 0 ? (
Use as ações rápidas acima ou faça uma pergunta livre abaixo.
) : (
mensagens.map((m, i) => (