first commit

This commit is contained in:
Ichitux
2026-04-05 03:08:53 +02:00
commit 1082d36c12
28015 changed files with 3767672 additions and 0 deletions
@@ -0,0 +1,219 @@
import { useState } from 'react';
import { Wand2, ChevronLeft, ChevronRight, Sparkles } from 'lucide-react';
import { useBookStore } from '../../stores/bookStore';
import { booksApi } from '../../services/api';
export default function BookEditor() {
const { activeBook, updateChapterContent, setCurrentChapter } = useBookStore();
const [isGenerating, setIsGenerating] = useState(false);
const [showAIHelp, setShowAIHelp] = useState(false);
if (!activeBook || !activeBook.outline) {
return (
<div className="card text-center py-12">
<p className="text-gray-500">
Generate an outline first to start writing your book.
</p>
</div>
);
}
const currentChapter = activeBook.outline.chapters.find(
(c) => c.number === activeBook.currentChapter
);
const handlePreviousChapter = () => {
if (activeBook.currentChapter > 1) {
setCurrentChapter(activeBook.currentChapter - 1);
}
};
const handleNextChapter = () => {
const maxChapter = activeBook.outline!.chapters.length;
if (activeBook.currentChapter < maxChapter) {
setCurrentChapter(activeBook.currentChapter + 1);
}
};
const handleGenerateChapter = async () => {
if (!currentChapter || !activeBook) return;
setIsGenerating(true);
try {
const previousChapter = activeBook.outline?.chapters.find(
(c) => c.number === activeBook.currentChapter - 1
);
const response = await booksApi.generateChapter(
activeBook.genre,
currentChapter.title,
currentChapter.summary,
previousChapter?.content
);
updateChapterContent(activeBook.currentChapter, response.data.content);
} catch (error) {
console.error('Failed to generate chapter:', error);
} finally {
setIsGenerating(false);
}
};
const handleExpandText = async () => {
// TODO: Implement expand functionality
alert('Expand feature coming soon!');
};
return (
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* Chapter Navigation */}
<div className="space-y-4">
<div className="card">
<h3 className="font-semibold text-gray-900 mb-4">Chapters</h3>
<div className="space-y-2">
{activeBook.outline.chapters.map((chapter) => (
<button
key={chapter.number}
onClick={() => setCurrentChapter(chapter.number)}
className={`w-full text-left p-3 rounded-lg transition-colors ${
chapter.number === activeBook.currentChapter
? 'bg-primary-100 text-primary-700'
: 'hover:bg-gray-50'
}`}
>
<div className="flex items-center gap-2">
<span className="flex-shrink-0 w-6 h-6 bg-white rounded-full flex items-center justify-center text-xs font-medium border">
{chapter.number}
</span>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">{chapter.title}</p>
{chapter.content && (
<span className="text-xs text-green-600">✓ Written</span>
)}
</div>
</div>
</button>
))}
</div>
</div>
{/* Chapter Info */}
<div className="card">
<h4 className="font-medium text-gray-900 mb-2">Chapter Summary</h4>
<p className="text-sm text-gray-600">{currentChapter?.summary}</p>
</div>
</div>
{/* Writing Area */}
<div className="lg:col-span-3">
<div className="card">
{/* Chapter Header */}
<div className="flex items-center justify-between mb-6 pb-4 border-b border-gray-200">
<div className="flex items-center gap-4">
<button
onClick={handlePreviousChapter}
disabled={activeBook.currentChapter <= 1}
className="p-2 rounded-lg hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
>
<ChevronLeft className="w-5 h-5" />
</button>
<div>
<h2 className="text-xl font-bold text-gray-900">
{currentChapter?.title}
</h2>
<p className="text-sm text-gray-500">
Chapter {activeBook.currentChapter} of {activeBook.outline.chapters.length}
</p>
</div>
<button
onClick={handleNextChapter}
disabled={activeBook.currentChapter >= activeBook.outline.chapters.length}
className="p-2 rounded-lg hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
>
<ChevronRight className="w-5 h-5" />
</button>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setShowAIHelp(!showAIHelp)}
className="btn-secondary flex items-center gap-2"
>
<Sparkles className="w-4 h-4" />
AI Assist
</button>
<button
onClick={handleGenerateChapter}
disabled={isGenerating || !currentChapter}
className="btn-primary flex items-center gap-2"
>
<Wand2 className="w-4 h-4" />
{isGenerating ? 'Generating...' : 'Generate Chapter'}
</button>
</div>
</div>
{/* AI Help Panel */}
{showAIHelp && (
<div className="mb-6 bg-gradient-to-r from-accent-50 to-primary-50 rounded-lg p-4 border border-accent-200">
<h4 className="font-medium text-gray-900 mb-3">AI Writing Assistant</h4>
<div className="flex flex-wrap gap-2">
<button
onClick={handleExpandText}
className="text-sm px-3 py-1.5 bg-white rounded-lg border border-gray-200 hover:border-accent-300 transition-colors"
>
✨ Expand this section
</button>
<button className="text-sm px-3 py-1.5 bg-white rounded-lg border border-gray-200 hover:border-accent-300 transition-colors">
📝 Improve prose
</button>
<button className="text-sm px-3 py-1.5 bg-white rounded-lg border border-gray-200 hover:border-accent-300 transition-colors">
💡 Add description
</button>
<button className="text-sm px-3 py-1.5 bg-white rounded-lg border border-gray-200 hover:border-accent-300 transition-colors">
🔄 Rewrite paragraph
</button>
</div>
</div>
)}
{/* Editor */}
<div className="min-h-[500px]">
{currentChapter?.content ? (
<textarea
value={currentChapter.content}
onChange={(e) =>
updateChapterContent(activeBook.currentChapter, e.target.value)
}
className="w-full h-[600px] p-6 font-serif text-lg leading-relaxed border-0 focus:ring-0 resize-y outline-none"
placeholder="Start writing or use AI to generate this chapter..."
/>
) : (
<div className="h-[500px] flex items-center justify-center text-gray-400">
<div className="text-center">
<Wand2 className="w-12 h-12 mx-auto mb-3 opacity-50" />
<p>Click "Generate Chapter" to create content with AI</p>
<p className="text-sm mt-1">Or start writing manually</p>
</div>
</div>
)}
</div>
{/* Word Count */}
<div className="mt-4 pt-4 border-t border-gray-200 flex items-center justify-between text-sm text-gray-500">
<span>
{currentChapter?.content
? currentChapter.content.split(/\s+/).length
: 0}{' '}
words
</span>
<span>
Last updated:{' '}
{new Date(activeBook.updatedAt).toLocaleDateString()}
</span>
</div>
</div>
</div>
</div>
);
}
@@ -0,0 +1,223 @@
import { useState } from 'react';
import { BookOpen, Wand2, Edit3, Users, FileText } from 'lucide-react';
import GenreSelector from './GenreSelector';
import BookOutlineGenerator from './BookOutlineGenerator';
import BookEditor from './BookEditor';
import { useBookStore, Book, BookOutline } from '../../stores/bookStore';
import { booksApi } from '../../services/api';
import { useQuery } from '@tanstack/react-query';
export default function BookGenerator() {
const [activeTab, setActiveTab] = useState<'genre' | 'outline' | 'editor' | 'characters'>('genre');
const { activeBook, setActiveBook, setOutline } = useBookStore();
const { data: genresData } = useQuery({
queryKey: ['genres'],
queryFn: async () => {
const response = await booksApi.getGenres();
return response.data.genres;
},
});
const handleGenreSelect = (genreId: string, idea: string, title: string) => {
const newBook: Book = {
id: `book-${Date.now()}`,
title,
genre: genreId,
idea,
outline: null,
characters: [],
currentChapter: 1,
createdAt: new Date(),
updatedAt: new Date(),
};
setActiveBook(newBook);
setActiveTab('outline');
};
const handleOutlineGenerated = (outline: BookOutline) => {
if (activeBook && outline) {
setOutline(outline);
setActiveTab('editor');
}
};
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-gray-900">Book Generator</h2>
<p className="text-gray-500 mt-1">
Generate book ideas and write with AI assistance
</p>
</div>
{/* Tab Navigation */}
<div className="flex bg-white rounded-lg p-1 border border-gray-200">
<button
onClick={() => setActiveTab('genre')}
className={`flex items-center gap-2 px-4 py-2 rounded-md transition-colors ${
activeTab === 'genre' || !activeBook
? 'bg-primary-100 text-primary-700'
: 'text-gray-600 hover:bg-gray-100'
}`}
disabled={!activeBook && activeTab !== 'genre'}
>
<BookOpen className="w-4 h-4" />
<span className="hidden sm:inline">Genre</span>
</button>
<button
onClick={() => setActiveTab('outline')}
disabled={!activeBook}
className={`flex items-center gap-2 px-4 py-2 rounded-md transition-colors ${
activeTab === 'outline'
? 'bg-primary-100 text-primary-700'
: 'text-gray-600 hover:bg-gray-100 disabled:opacity-50'
}`}
>
<FileText className="w-4 h-4" />
<span className="hidden sm:inline">Outline</span>
</button>
<button
onClick={() => setActiveTab('editor')}
disabled={!activeBook}
className={`flex items-center gap-2 px-4 py-2 rounded-md transition-colors ${
activeTab === 'editor'
? 'bg-primary-100 text-primary-700'
: 'text-gray-600 hover:bg-gray-100'
}`}
>
<Edit3 className="w-4 h-4" />
<span className="hidden sm:inline">Write</span>
</button>
<button
onClick={() => setActiveTab('characters')}
disabled={!activeBook}
className={`flex items-center gap-2 px-4 py-2 rounded-md transition-colors ${
activeTab === 'characters'
? 'bg-primary-100 text-primary-700'
: 'text-gray-600 hover:bg-gray-100'
}`}
>
<Users className="w-4 h-4" />
<span className="hidden sm:inline">Characters</span>
</button>
</div>
</div>
{activeTab === 'genre' && (
<GenreSelector
genres={genresData || []}
onSelect={handleGenreSelect}
/>
)}
{activeTab === 'outline' && activeBook && (
<BookOutlineGenerator
book={activeBook}
onComplete={handleOutlineGenerated}
/>
)}
{activeTab === 'editor' && activeBook && (
<BookEditor />
)}
{activeTab === 'characters' && activeBook && (
<CharacterGenerator />
)}
</div>
);
}
function CharacterGenerator() {
const { activeBook, addCharacter } = useBookStore();
const [isGenerating, setIsGenerating] = useState(false);
const handleGenerateCharacters = async () => {
if (!activeBook) return;
setIsGenerating(true);
try {
const response = await booksApi.generateCharacters(activeBook.genre, activeBook.idea);
const characters = response.data.characters;
if (Array.isArray(characters)) {
characters.forEach((char: any) => {
addCharacter({
name: char.name || 'Unknown',
role: char.role || 'Supporting',
traits: char.traits || [],
motivation: char.motivation || '',
backstory: char.backstory || '',
});
});
}
} catch (error) {
console.error('Failed to generate characters:', error);
} finally {
setIsGenerating(false);
}
};
if (!activeBook) return null;
return (
<div className="card">
<div className="flex items-center justify-between mb-6">
<h3 className="text-lg font-semibold text-gray-900">
Character Development
</h3>
<button
onClick={handleGenerateCharacters}
disabled={isGenerating}
className="btn-primary flex items-center gap-2"
>
<Wand2 className="w-4 h-4" />
{isGenerating ? 'Generating...' : 'Generate Characters'}
</button>
</div>
{activeBook.characters.length === 0 ? (
<div className="text-center py-8">
<Users className="w-12 h-12 text-gray-300 mx-auto mb-3" />
<p className="text-gray-500">
No characters yet. Click "Generate Characters" to create AI-suggested characters.
</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{activeBook.characters.map((character, index) => (
<div
key={index}
className="border border-gray-200 rounded-lg p-4 hover:border-primary-300 transition-colors"
>
<div className="flex items-start justify-between mb-2">
<h4 className="font-semibold text-gray-900">{character.name}</h4>
<span className={`text-xs px-2 py-1 rounded-full ${
character.role === 'protagonist'
? 'bg-green-100 text-green-700'
: character.role === 'antagonist'
? 'bg-red-100 text-red-700'
: 'bg-gray-100 text-gray-600'
}`}>
{character.role}
</span>
</div>
<div className="space-y-2 text-sm">
<div>
<span className="text-gray-500">Traits:</span>
<p className="text-gray-700">{character.traits?.join(', ') || 'N/A'}</p>
</div>
<div>
<span className="text-gray-500">Motivation:</span>
<p className="text-gray-700">{character.motivation || 'N/A'}</p>
</div>
</div>
</div>
))}
</div>
)}
</div>
);
}
@@ -0,0 +1,136 @@
import { useState } from 'react';
import { Wand2, CheckCircle, Loader2 } from 'lucide-react';
import { Book, useBookStore } from '../../stores/bookStore';
import { booksApi } from '../../services/api';
interface BookOutlineGeneratorProps {
book: Book;
onComplete: (outline: any) => void;
}
export default function BookOutlineGenerator({ book, onComplete }: BookOutlineGeneratorProps) {
const { activeBook } = useBookStore();
const [isGenerating, setIsGenerating] = useState(false);
const [error, setError] = useState<string>('');
const outline = activeBook?.outline;
const handleGenerate = async () => {
setIsGenerating(true);
setError('');
try {
const response = await booksApi.generateOutline(book.genre, book.idea, book.title);
const generatedOutline = response.data.outline;
onComplete(generatedOutline);
} catch (err: any) {
setError(err.response?.data?.error || 'Failed to generate outline');
} finally {
setIsGenerating(false);
}
};
return (
<div className="space-y-6">
{/* Book Info Summary */}
<div className="card bg-gradient-to-r from-primary-50 to-accent-50">
<div className="flex items-start justify-between">
<div>
<h3 className="font-semibold text-gray-900">{book.title || 'Untitled Book'}</h3>
<p className="text-sm text-gray-600 mt-1 capitalize">Genre: {book.genre}</p>
<p className="text-sm text-gray-500 mt-2 line-clamp-2">{book.idea}</p>
</div>
{!outline && (
<button
onClick={handleGenerate}
disabled={isGenerating}
className="btn-primary flex items-center gap-2"
>
{isGenerating ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
Generating...
</>
) : (
<>
<Wand2 className="w-4 h-4" />
Generate Outline
</>
)}
</button>
)}
</div>
</div>
{/* Error Message */}
{error && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<p className="text-red-700">{error}</p>
</div>
)}
{/* Outline Display */}
{outline && (
<div className="card">
<div className="flex items-center gap-2 mb-6">
<CheckCircle className="w-5 h-5 text-green-500" />
<h3 className="text-lg font-semibold text-gray-900">
Generated Outline: {outline.title || book.title}
</h3>
</div>
{outline.logline && (
<div className="mb-6 pb-6 border-b border-gray-200">
<h4 className="text-sm font-medium text-gray-500 mb-2">Logline</h4>
<p className="text-gray-900 italic">{outline.logline}</p>
</div>
)}
<div>
<h4 className="text-sm font-medium text-gray-500 mb-4">Chapter Outline</h4>
<div className="space-y-3">
{outline.chapters?.map((chapter: any, index: number) => (
<div
key={index}
className="border border-gray-200 rounded-lg p-4 hover:border-primary-300 transition-colors"
>
<div className="flex items-start gap-3">
<span className="flex-shrink-0 w-8 h-8 bg-primary-100 text-primary-700 rounded-full flex items-center justify-center text-sm font-medium">
{chapter.number || index + 1}
</span>
<div className="flex-1">
<h5 className="font-medium text-gray-900">
{chapter.title || `Chapter ${chapter.number || index + 1}`}
</h5>
<p className="text-sm text-gray-600 mt-1">
{chapter.summary || 'No summary available'}
</p>
</div>
</div>
</div>
))}
</div>
</div>
<div className="mt-6 flex items-center justify-end gap-3">
<p className="text-sm text-gray-500">
Ready to start writing! Navigate to the Write tab to begin.
</p>
</div>
</div>
)}
{/* Initial State */}
{!outline && !isGenerating && (
<div className="card text-center py-12">
<Wand2 className="w-12 h-12 text-gray-300 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900">Ready to Generate</h3>
<p className="text-gray-500 mt-2 max-w-md mx-auto">
Click "Generate Outline" to create a detailed chapter outline based on your genre and book idea.
The AI will create a structure following genre-specific patterns.
</p>
</div>
)}
</div>
);
}
@@ -0,0 +1,111 @@
import { useState } from 'react';
import { BookOpen, Sparkles } from 'lucide-react';
interface Genre {
id: string;
name: string;
description: string;
icon: string;
}
interface GenreSelectorProps {
genres: Genre[];
onSelect: (genreId: string, idea: string, title: string) => void;
}
export default function GenreSelector({ genres, onSelect }: GenreSelectorProps) {
const [selectedGenre, setSelectedGenre] = useState<string>('');
const [title, setTitle] = useState('');
const [idea, setIdea] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (selectedGenre && idea.trim()) {
onSelect(selectedGenre, idea.trim(), title.trim());
}
};
return (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Genre Selection */}
<div className="lg:col-span-2">
<div className="card">
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2">
<BookOpen className="w-5 h-5" />
Select Your Genre
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
{genres.map((genre) => (
<button
key={genre.id}
onClick={() => setSelectedGenre(genre.id)}
className={`p-4 rounded-xl border-2 text-left transition-all ${
selectedGenre === genre.id
? 'border-primary-500 bg-primary-50 shadow-md'
: 'border-gray-200 hover:border-gray-300 hover:bg-gray-50'
}`}
>
<span className="text-2xl mb-2 block">{genre.icon}</span>
<h4 className="font-medium text-gray-900">{genre.name}</h4>
<p className="text-xs text-gray-500 mt-1 line-clamp-2">
{genre.description}
</p>
</button>
))}
</div>
</div>
</div>
{/* Book Details Form */}
<div>
<div className="card sticky top-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2">
<Sparkles className="w-5 h-5" />
Book Details
</h3>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="label">Book Title (optional)</label>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="My Amazing Book"
className="input"
/>
</div>
<div>
<label className="label">
Core Idea <span className="text-red-500">*</span>
</label>
<textarea
value={idea}
onChange={(e) => setIdea(e.target.value)}
placeholder="Describe your book idea... What's the story about? Who are the main characters? What conflict drives the narrative?"
className="input min-h-[150px] resize-y"
required
/>
</div>
<div className="bg-amber-50 border border-amber-200 rounded-lg p-3">
<p className="text-xs text-amber-700">
<strong>Tip:</strong> The more details you provide, the better the AI can generate your outline and content.
</p>
</div>
<button
type="submit"
disabled={!selectedGenre || !idea.trim()}
className="btn-primary w-full py-3"
>
Generate Outline
</button>
</form>
</div>
</div>
</div>
);
}