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

5
server/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
node_modules
# Keep environment variables out of version control
.env
/src/generated/prisma

BIN
server/dev.db Normal file

Binary file not shown.

2
server/dist/api/auth.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
export declare const authRoutes: import("express-serve-static-core").Router;
//# sourceMappingURL=auth.d.ts.map

1
server/dist/api/auth.d.ts.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/api/auth.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,UAAU,4CAAW,CAAC"}

111
server/dist/api/auth.js vendored Normal file
View File

@@ -0,0 +1,111 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.authRoutes = void 0;
const express_1 = require("express");
const bcryptjs_1 = __importDefault(require("bcryptjs"));
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
const db_js_1 = require("../db.js");
exports.authRoutes = (0, express_1.Router)();
const JWT_SECRET = process.env.JWT_SECRET || 'super-secret-key';
exports.authRoutes.post('/register', async (req, res) => {
try {
const { email, password, name } = req.body;
if (!email || !password) {
return res.status(400).json({ error: 'Email and password are required' });
}
const existingUser = await db_js_1.prisma.user.findUnique({ where: { email } });
if (existingUser) {
return res.status(400).json({ error: 'Email already in use' });
}
const hashedPassword = await bcryptjs_1.default.hash(password, 10);
const user = await db_js_1.prisma.user.create({
data: {
email,
password: hashedPassword,
name: name || email.split('@')[0],
role: email === 'admin@admin.com' || email === 'admin' ? 'ADMIN' : 'USER',
},
});
const token = jsonwebtoken_1.default.sign({ userId: user.id }, JWT_SECRET, { expiresIn: '7d' });
await db_js_1.prisma.session.create({
data: {
userId: user.id,
token,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days
}
});
res.json({ token, user: { id: user.id, email: user.email, name: user.name, role: user.role } });
}
catch (error) {
console.error('Register error:', error);
res.status(500).json({ error: 'Failed to register', details: String(error) });
}
});
exports.authRoutes.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ error: 'Email and password are required' });
}
const user = await db_js_1.prisma.user.findUnique({ where: { email } });
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const isValid = await bcryptjs_1.default.compare(password, user.password);
if (!isValid) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const token = jsonwebtoken_1.default.sign({ userId: user.id }, JWT_SECRET, { expiresIn: '7d' });
await db_js_1.prisma.session.create({
data: {
userId: user.id,
token,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
}
});
res.json({ token, user: { id: user.id, email: user.email, name: user.name, role: user.role } });
}
catch (error) {
console.error('Login error:', error);
res.status(500).json({ error: 'Failed to login', details: String(error) });
}
});
exports.authRoutes.get('/me', async (req, res) => {
try {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
const token = authHeader.substring(7);
const decoded = jsonwebtoken_1.default.verify(token, JWT_SECRET);
const session = await db_js_1.prisma.session.findUnique({ where: { token } });
if (!session || session.expiresAt < new Date()) {
return res.status(401).json({ error: 'Session expired' });
}
const user = await db_js_1.prisma.user.findUnique({ where: { id: decoded.userId } });
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json({ user: { id: user.id, email: user.email, name: user.name, role: user.role } });
}
catch (error) {
res.status(401).json({ error: 'Invalid token' });
}
});
exports.authRoutes.post('/logout', async (req, res) => {
try {
const authHeader = req.headers.authorization;
if (authHeader?.startsWith('Bearer ')) {
const token = authHeader.substring(7);
await db_js_1.prisma.session.deleteMany({ where: { token } });
}
res.json({ message: 'Logged out successfully' });
}
catch (error) {
res.status(500).json({ error: 'Failed to logout' });
}
});
//# sourceMappingURL=auth.js.map

1
server/dist/api/auth.js.map vendored Normal file

File diff suppressed because one or more lines are too long

2
server/dist/api/books.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
export declare const bookRoutes: import("express-serve-static-core").Router;
//# sourceMappingURL=books.d.ts.map

1
server/dist/api/books.d.ts.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"books.d.ts","sourceRoot":"","sources":["../../src/api/books.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,UAAU,4CAAW,CAAC"}

295
server/dist/api/books.js vendored Normal file
View File

@@ -0,0 +1,295 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.bookRoutes = void 0;
const express_1 = require("express");
const axios_1 = __importDefault(require("axios"));
const genreTemplates_js_1 = require("../prompts/genreTemplates.js");
exports.bookRoutes = (0, express_1.Router)();
// Get all available genre templates
exports.bookRoutes.get('/genres', (req, res) => {
const genres = Object.entries(genreTemplates_js_1.genreTemplates).map(([key, template]) => ({
id: key,
name: template.name,
description: template.description,
icon: template.icon
}));
res.json({ genres });
});
// Get a specific genre template
exports.bookRoutes.get('/genres/:genreId', (req, res) => {
const { genreId } = req.params;
const template = genreTemplates_js_1.genreTemplates[genreId];
if (!template) {
return res.status(404).json({ error: 'Genre not found' });
}
res.json({ template });
});
// Generate book outline based on genre and idea
exports.bookRoutes.post('/outline', async (req, res) => {
try {
const { genre, idea, title } = req.body;
if (!genre || !idea) {
return res.status(400).json({ error: 'genre and idea are required' });
}
const template = genreTemplates_js_1.genreTemplates[genre];
if (!template) {
return res.status(400).json({ error: 'Invalid genre' });
}
const prompt = `${template.prompts.outline}
Book Title: ${title || 'Untitled'}
Core Idea: ${idea}
Generate a detailed chapter outline following the structure: ${template.structure.join(' → ')}
Return the response in JSON format:
{
"title": "Book Title",
"genre": "${genre}",
"logline": "One sentence summary",
"chapters": [
{"number": 1, "title": "Chapter Title", "summary": "Brief description"},
...
]
}`;
const response = await axios_1.default.post('https://openrouter.ai/api/v1/chat/completions', {
model: 'nvidia/nemotron-3-nano-30b-a3b:free', // Free model on OpenRouter
messages: [{ role: 'user', content: prompt }],
temperature: 0.7,
max_tokens: 2000
}, {
headers: {
'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}`,
'Content-Type': 'application/json',
'HTTP-Referer': 'https://creabook.app',
'X-Title': 'CreaBook'
}
});
const content = response.data.choices[0].message.content;
console.log('=== Outline AI Response ===');
console.log('Raw content:', content);
// Try to parse JSON from response
let outline;
const jsonMatch = content.match(/\{[\s\S]*\}/);
if (jsonMatch) {
try {
outline = JSON.parse(jsonMatch[0]);
console.log('Parsed outline:', outline);
}
catch (parseError) {
console.error('JSON parse error:', parseError);
outline = { raw: content, error: 'Failed to parse JSON' };
}
}
else {
console.error('No JSON object found in response');
outline = { raw: content, error: 'No JSON found' };
}
res.json({ outline });
}
catch (error) {
console.error('Outline generation error:', error);
res.status(500).json({
error: 'Failed to generate outline',
details: error instanceof Error ? error.message : String(error)
});
}
});
// Generate a chapter based on outline
exports.bookRoutes.post('/chapter', async (req, res) => {
try {
const { genre, chapterTitle, chapterSummary, previousContent } = req.body;
if (!genre || !chapterTitle || !chapterSummary) {
return res.status(400).json({ error: 'genre, chapterTitle, and chapterSummary are required' });
}
const template = genreTemplates_js_1.genreTemplates[genre];
let prompt = `${template.prompts.chapter}
Chapter: ${chapterTitle}
Summary: ${chapterSummary}
Tone: ${template.defaults.tone}
POV: ${template.defaults.pov}
`;
if (previousContent) {
prompt += `\n\nPrevious content for context:\n${previousContent.substring(0, 2000)}...`;
}
const response = await axios_1.default.post('https://openrouter.ai/api/v1/chat/completions', {
model: 'nvidia/nemotron-3-nano-30b-a3b:free',
messages: [{ role: 'user', content: prompt }],
temperature: 0.7,
max_tokens: 3000
}, {
headers: {
'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}`,
'Content-Type': 'application/json',
'HTTP-Referer': 'https://creabook.app',
'X-Title': 'CreaBook'
}
});
const content = response.data.choices[0].message.content;
res.json({
content,
chapterTitle
});
}
catch (error) {
console.error('Chapter generation error:', error);
res.status(500).json({
error: 'Failed to generate chapter',
details: error instanceof Error ? error.message : String(error)
});
}
});
// Expand or refine text
exports.bookRoutes.post('/expand', async (req, res) => {
try {
const { text, instruction = 'Expand and improve this text' } = req.body;
if (!text) {
return res.status(400).json({ error: 'text is required' });
}
const prompt = `${instruction}:
Original text:
${text}
Provide an expanded and improved version:`;
const response = await axios_1.default.post('https://openrouter.ai/api/v1/chat/completions', {
model: 'nvidia/nemotron-3-nano-30b-a3b:free',
messages: [{ role: 'user', content: prompt }],
temperature: 0.7,
max_tokens: 2000
}, {
headers: {
'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}`,
'Content-Type': 'application/json',
'HTTP-Referer': 'https://creabook.app',
'X-Title': 'CreaBook'
}
});
const content = response.data.choices[0].message.content;
res.json({ expanded: content });
}
catch (error) {
console.error('Expand error:', error);
res.status(500).json({
error: 'Failed to expand text',
details: error instanceof Error ? error.message : String(error)
});
}
});
// Generate character suggestions
exports.bookRoutes.post('/characters', async (req, res) => {
try {
const { genre, storyIdea } = req.body;
if (!genre || !storyIdea) {
return res.status(400).json({ error: 'genre and storyIdea are required' });
}
const template = genreTemplates_js_1.genreTemplates[genre];
const prompt = `Based on this ${genre} story idea, suggest 3-5 main characters.
Story Idea: ${storyIdea}
Genre conventions: ${template.description}
Return ONLY a valid JSON array with this exact format:
[
{
"name": "Character Name",
"role": "protagonist|antagonist|supporting",
"traits": ["trait1", "trait2"],
"motivation": "What drives them",
"backstory": "Brief backstory"
}
]
Do not include any text outside the JSON array.`;
console.log('=== Character Generation Prompt ===');
console.log(prompt);
const response = await axios_1.default.post('https://openrouter.ai/api/v1/chat/completions', {
model: 'nvidia/nemotron-3-nano-30b-a3b:free',
messages: [{ role: 'user', content: prompt }],
temperature: 0.7,
max_tokens: 1500
}, {
headers: {
'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}`,
'Content-Type': 'application/json',
'HTTP-Referer': 'https://creabook.app',
'X-Title': 'CreaBook'
}
});
const content = response.data.choices[0].message.content;
console.log('=== Character AI Response ===');
console.log('Raw content:', content);
let characters;
const jsonMatch = content.match(/\[[\s\S]*\]/);
if (jsonMatch) {
try {
characters = JSON.parse(jsonMatch[0]);
console.log('Parsed characters:', characters);
}
catch (parseError) {
console.error('JSON parse error:', parseError);
characters = { raw: content, error: 'Failed to parse JSON' };
}
}
else {
console.error('No JSON array found in response');
characters = { raw: content, error: 'No JSON array found' };
}
res.json({ characters });
}
catch (error) {
console.error('Character generation error:', error);
res.status(500).json({
error: 'Failed to generate characters',
details: error instanceof Error ? error.message : String(error)
});
}
});
// Generate plot suggestions
exports.bookRoutes.post('/plot', async (req, res) => {
try {
const { genre, currentPlot, issue } = req.body;
if (!genre || !currentPlot) {
return res.status(400).json({ error: 'genre and currentPlot are required' });
}
const template = genreTemplates_js_1.genreTemplates[genre];
const prompt = `Help develop the plot for this ${genre} story:
Current Plot:
${currentPlot}
${issue ? `Specific Issue: ${issue}` : 'Suggest plot developments and twists.'}
Consider genre conventions: ${template.description}
Structure: ${template.structure.join(' → ')}
Provide specific, actionable plot suggestions:`;
const response = await axios_1.default.post('https://openrouter.ai/api/v1/chat/completions', {
model: 'nvidia/nemotron-3-nano-30b-a3b:free',
messages: [{ role: 'user', content: prompt }],
temperature: 0.7,
max_tokens: 1500
}, {
headers: {
'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}`,
'Content-Type': 'application/json',
'HTTP-Referer': 'https://creabook.app',
'X-Title': 'CreaBook'
}
});
const content = response.data.choices[0].message.content;
res.json({ suggestions: content });
}
catch (error) {
console.error('Plot suggestion error:', error);
res.status(500).json({
error: 'Failed to generate plot suggestions',
details: error instanceof Error ? error.message : String(error)
});
}
});
//# sourceMappingURL=books.js.map

1
server/dist/api/books.js.map vendored Normal file

File diff suppressed because one or more lines are too long

2
server/dist/api/covers.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
export declare const coverRoutes: import("express-serve-static-core").Router;
//# sourceMappingURL=covers.d.ts.map

1
server/dist/api/covers.d.ts.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"covers.d.ts","sourceRoot":"","sources":["../../src/api/covers.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,WAAW,4CAAW,CAAC"}

203
server/dist/api/covers.js vendored Normal file
View File

@@ -0,0 +1,203 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.coverRoutes = void 0;
const express_1 = require("express");
const multer_1 = __importDefault(require("multer"));
const path_1 = __importDefault(require("path"));
const fs_1 = __importDefault(require("fs"));
const sharp_1 = __importDefault(require("sharp"));
const axios_1 = __importDefault(require("axios"));
exports.coverRoutes = (0, express_1.Router)();
// Configure multer for file uploads
const storage = multer_1.default.diskStorage({
destination: (req, file, cb) => {
const uploadDir = path_1.default.join(process.cwd(), 'uploads', 'covers');
if (!fs_1.default.existsSync(uploadDir)) {
fs_1.default.mkdirSync(uploadDir, { recursive: true });
}
cb(null, uploadDir);
},
filename: (req, file, cb) => {
const uniqueSuffix = `${Date.now()}-${Math.round(Math.random() * 1E9)}`;
cb(null, `cover-${uniqueSuffix}${path_1.default.extname(file.originalname)}`);
}
});
const upload = (0, multer_1.default)({
storage,
limits: { fileSize: 10 * 1024 * 1024 }, // 10MB limit
fileFilter: (req, file, cb) => {
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp'];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true);
}
else {
cb(new Error('Invalid file type. Only JPEG, PNG, and WEBP are allowed.'));
}
}
});
// Upload a cover image
exports.coverRoutes.post('/upload', upload.single('image'), (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
}
res.json({
id: path_1.default.basename(req.file.filename, path_1.default.extname(req.file.filename)),
filename: req.file.filename,
path: req.file.path,
url: `/uploads/covers/${req.file.filename}`,
size: req.file.size
});
}
catch (error) {
console.error('Upload error:', error);
res.status(500).json({
error: 'Failed to upload image',
details: error instanceof Error ? error.message : String(error)
});
}
});
// Get all covers
exports.coverRoutes.get('/', (req, res) => {
try {
const coversDir = path_1.default.join(process.cwd(), 'uploads', 'covers');
if (!fs_1.default.existsSync(coversDir)) {
return res.json({ covers: [] });
}
const files = fs_1.default.readdirSync(coversDir)
.filter(file => /\.(jpg|jpeg|png|webp)$/i.test(file))
.map(file => ({
id: path_1.default.basename(file, path_1.default.extname(file)),
filename: file,
url: `/uploads/covers/${file}`,
createdAt: fs_1.default.statSync(path_1.default.join(coversDir, file)).mtime
}))
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
res.json({ covers: files });
}
catch (error) {
console.error('List covers error:', error);
res.status(500).json({
error: 'Failed to list covers',
details: error instanceof Error ? error.message : String(error)
});
}
});
// Delete a cover
exports.coverRoutes.delete('/:id', (req, res) => {
try {
const coverPath = path_1.default.join(process.cwd(), 'uploads', 'covers', `${req.params.id}*`);
const coversDir = path_1.default.join(process.cwd(), 'uploads', 'covers');
if (!fs_1.default.existsSync(coversDir)) {
return res.status(404).json({ error: 'Cover not found' });
}
const files = fs_1.default.readdirSync(coversDir)
.filter(f => f.startsWith(`${req.params.id}`));
if (files.length === 0) {
return res.status(404).json({ error: 'Cover not found' });
}
files.forEach(file => {
fs_1.default.unlinkSync(path_1.default.join(coversDir, file));
});
res.json({ message: 'Cover deleted successfully' });
}
catch (error) {
console.error('Delete error:', error);
res.status(500).json({
error: 'Failed to delete cover',
details: error instanceof Error ? error.message : String(error)
});
}
});
// Process cover image (resize, filter, etc.)
exports.coverRoutes.post('/process', upload.single('image'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
}
const { width, height, filter, brightness, contrast } = req.body;
let pipeline = (0, sharp_1.default)(req.file.path);
// Resize if dimensions provided
if (width && height) {
pipeline = pipeline.resize(parseInt(width), parseInt(height), {
fit: 'cover'
});
}
// Apply filters
if (filter === 'grayscale') {
pipeline = pipeline.grayscale();
}
else if (filter === 'sepia') {
pipeline = pipeline.modulate({ saturation: 0, brightness: 1.1 });
}
// Adjust brightness and saturation (sharp doesn't have contrast)
if (brightness) {
pipeline = pipeline.modulate({ brightness: parseFloat(brightness) });
}
if (contrast) {
pipeline = pipeline.modulate({ saturation: parseFloat(contrast) });
}
const processedBuffer = await pipeline.toBuffer();
const outputPath = path_1.default.join(process.cwd(), 'uploads', 'covers', `processed-${req.file.filename}`);
await pipeline.toFile(outputPath);
res.json({
id: `processed-${path_1.default.basename(req.file.filename, path_1.default.extname(req.file.filename))}`,
filename: `processed-${req.file.filename}`,
path: outputPath,
url: `/uploads/covers/processed-${req.file.filename}`
});
}
catch (error) {
console.error('Process error:', error);
res.status(500).json({
error: 'Failed to process image',
details: error instanceof Error ? error.message : String(error)
});
}
});
// Generate cover image using AI (Pollinations.ai - Free, no API key required)
exports.coverRoutes.post('/generate', async (req, res) => {
try {
const { prompt, genre } = req.body;
if (!prompt) {
return res.status(400).json({ error: 'prompt is required' });
}
const enhancedPrompt = genre
? `Book cover for a ${genre} novel: ${prompt}, professional book cover design, high quality, detailed illustration, 8k, masterpiece`
: `Book cover: ${prompt}, professional book cover design, high quality, detailed illustration, 8k, masterpiece`;
// Use Pollinations.ai free text-to-image API (no API key required)
const imageUrl = `https://image.pollinations.ai/prompt/${encodeURIComponent(enhancedPrompt)}?width=1024&height=1024&nologo=true&seed=${Date.now()}`;
// Fetch the generated image
const imageResponse = await axios_1.default.get(imageUrl, {
responseType: 'arraybuffer',
timeout: 30000
});
const buffer = Buffer.from(imageResponse.data);
const uploadsDir = path_1.default.join(process.cwd(), 'uploads', 'covers');
if (!fs_1.default.existsSync(uploadsDir)) {
fs_1.default.mkdirSync(uploadsDir, { recursive: true });
}
const uniqueSuffix = `${Date.now()}-${Math.round(Math.random() * 1E9)}`;
const filename = `generated-${uniqueSuffix}.png`;
const filePath = path_1.default.join(uploadsDir, filename);
fs_1.default.writeFileSync(filePath, buffer);
res.json({
id: `generated-${uniqueSuffix}`,
filename,
path: filePath,
url: `/uploads/covers/${filename}`,
generated: true
});
}
catch (error) {
console.error('Image generation error:', error);
res.status(500).json({
error: 'Failed to generate image',
details: error instanceof Error ? error.message : String(error)
});
}
});
//# sourceMappingURL=covers.js.map

1
server/dist/api/covers.js.map vendored Normal file

File diff suppressed because one or more lines are too long

2
server/dist/api/ollama.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
export declare const ollamaRoutes: import("express-serve-static-core").Router;
//# sourceMappingURL=ollama.d.ts.map

1
server/dist/api/ollama.d.ts.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"ollama.d.ts","sourceRoot":"","sources":["../../src/api/ollama.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,YAAY,4CAAW,CAAC"}

105
server/dist/api/ollama.js vendored Normal file
View File

@@ -0,0 +1,105 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ollamaRoutes = void 0;
const express_1 = require("express");
const ollama_1 = __importDefault(require("ollama"));
exports.ollamaRoutes = (0, express_1.Router)();
// Generate text with Ollama
exports.ollamaRoutes.post('/generate', async (req, res) => {
try {
const { model, prompt, stream = false } = req.body;
if (!model || !prompt) {
return res.status(400).json({ error: 'model and prompt are required' });
}
const response = await ollama_1.default.chat({
model: model || 'llama3',
messages: [{ role: 'user', content: prompt }],
stream: false,
});
res.json({
content: response.message.content,
model: response.model
});
}
catch (error) {
console.error('Ollama generate error:', error);
res.status(500).json({
error: 'Failed to generate content',
details: error instanceof Error ? error.message : String(error)
});
}
});
// Stream text generation (SSE)
exports.ollamaRoutes.post('/stream', async (req, res) => {
try {
const { model, prompt } = req.body;
if (!model || !prompt) {
return res.status(400).json({ error: 'model and prompt are required' });
}
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
const stream = await ollama_1.default.chat({
model: model || 'llama3',
messages: [{ role: 'user', content: prompt }],
stream: true,
});
for await (const chunk of stream) {
const content = chunk.message?.content || '';
if (content) {
res.write(`data: ${JSON.stringify({ content })}\n\n`);
}
}
res.write('data: [DONE]\n\n');
res.end();
}
catch (error) {
console.error('Ollama stream error:', error);
res.write(`data: ${JSON.stringify({ error: 'Streaming failed' })}\n\n`);
res.end();
}
});
// Generate image (if using a model that supports it)
exports.ollamaRoutes.post('/image', async (req, res) => {
try {
const { prompt, model = 'llama3.2-vision' } = req.body;
if (!prompt) {
return res.status(400).json({ error: 'prompt is required' });
}
// Note: Ollama's image generation depends on the model
// Available models: llama3.2-vision
const response = await ollama_1.default.generate({
model,
prompt,
});
res.json({
image: response.image,
model
});
}
catch (error) {
console.error('Image generation error:', error);
res.status(500).json({
error: 'Failed to generate image',
details: error instanceof Error ? error.message : String(error)
});
}
});
// List available models
exports.ollamaRoutes.get('/models', async (req, res) => {
try {
const response = await ollama_1.default.list();
res.json({ models: response.models });
}
catch (error) {
console.error('Model list error:', error);
res.status(500).json({
error: 'Failed to list models',
details: error instanceof Error ? error.message : String(error)
});
}
});
//# sourceMappingURL=ollama.js.map

1
server/dist/api/ollama.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"ollama.js","sourceRoot":"","sources":["../../src/api/ollama.ts"],"names":[],"mappings":"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"}

6
server/dist/db.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
import { PrismaClient } from '@prisma/client';
import { PrismaLibSql } from '@prisma/adapter-libsql';
export declare const prisma: PrismaClient<{
adapter: PrismaLibSql;
}, never, import("@prisma/client/runtime/client").DefaultArgs>;
//# sourceMappingURL=db.d.ts.map

1
server/dist/db.d.ts.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAMtD,eAAO,MAAM,MAAM;;8DAAgC,CAAC"}

10
server/dist/db.js vendored Normal file
View File

@@ -0,0 +1,10 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.prisma = void 0;
const client_1 = require("@prisma/client");
const adapter_libsql_1 = require("@prisma/adapter-libsql");
const adapter = new adapter_libsql_1.PrismaLibSql({
url: process.env.DATABASE_URL || "file:./dev.db",
});
exports.prisma = new client_1.PrismaClient({ adapter });
//# sourceMappingURL=db.js.map

1
server/dist/db.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"db.js","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":";;;AAAA,2CAA8C;AAC9C,2DAAsD;AAEtD,MAAM,OAAO,GAAG,IAAI,6BAAY,CAAC;IAC/B,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,eAAe;CACjD,CAAC,CAAC;AAEU,QAAA,MAAM,GAAG,IAAI,qBAAY,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC"}

2
server/dist/index.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
import 'dotenv/config';
//# sourceMappingURL=index.d.ts.map

1
server/dist/index.d.ts.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,eAAe,CAAC"}

32
server/dist/index.js vendored Normal file
View File

@@ -0,0 +1,32 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
require("dotenv/config");
const express_1 = __importDefault(require("express"));
const cors_1 = __importDefault(require("cors"));
const path_1 = __importDefault(require("path"));
const covers_js_1 = require("./api/covers.js");
const books_js_1 = require("./api/books.js");
const auth_js_1 = require("./api/auth.js");
const app = (0, express_1.default)();
const PORT = process.env.PORT || 5001;
// Middleware
app.use((0, cors_1.default)());
app.use(express_1.default.json());
// API Routes
app.use('/api/covers', covers_js_1.coverRoutes);
app.use('/api/books', books_js_1.bookRoutes);
app.use('/api/auth', auth_js_1.authRoutes);
// Servir archivos estáticos de uploads
app.use('/uploads', express_1.default.static(path_1.default.join(process.cwd(), 'uploads')));
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
app.listen(PORT, () => {
console.log(`🚀 CreaBook server running on http://localhost:${PORT}`);
console.log(`☁️ Cloud AI integration ready`);
});
//# sourceMappingURL=index.js.map

1
server/dist/index.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;AAAA,yBAAuB;AACvB,sDAA8B;AAC9B,gDAAwB;AACxB,gDAAwB;AACxB,+CAA8C;AAC9C,6CAA4C;AAC5C,2CAA2C;AAE3C,MAAM,GAAG,GAAG,IAAA,iBAAO,GAAE,CAAC;AACtB,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC;AAEtC,aAAa;AACb,GAAG,CAAC,GAAG,CAAC,IAAA,cAAI,GAAE,CAAC,CAAC;AAChB,GAAG,CAAC,GAAG,CAAC,iBAAO,CAAC,IAAI,EAAE,CAAC,CAAC;AAGxB,aAAa;AACb,GAAG,CAAC,GAAG,CAAC,aAAa,EAAE,uBAAW,CAAC,CAAC;AACpC,GAAG,CAAC,GAAG,CAAC,YAAY,EAAE,qBAAU,CAAC,CAAC;AAClC,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,oBAAU,CAAC,CAAC;AAEjC,uCAAuC;AACvC,GAAG,CAAC,GAAG,CAAC,UAAU,EAAE,iBAAO,CAAC,MAAM,CAAC,cAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC;AAEzE,eAAe;AACf,GAAG,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;IAC9B,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;AAClE,CAAC,CAAC,CAAC;AAGH,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE;IACpB,OAAO,CAAC,GAAG,CAAC,kDAAkD,IAAI,EAAE,CAAC,CAAC;IACtE,OAAO,CAAC,GAAG,CAAC,+BAA+B,CAAC,CAAC;AAC/C,CAAC,CAAC,CAAC"}

20
server/dist/prompts/genreTemplates.d.ts vendored Normal file
View File

@@ -0,0 +1,20 @@
export type GenreType = 'fiction' | 'mystery' | 'romance' | 'scifi' | 'fantasy' | 'horror' | 'thriller' | 'children' | 'nonfiction' | 'selfhelp' | 'business' | 'memoir';
export interface GenreTemplate {
name: string;
description: string;
icon: string;
structure: string[];
prompts: {
outline: string;
chapter: string;
character?: string;
setting?: string;
};
defaults: {
tone: string;
pov: string;
typicalLength?: string;
};
}
export declare const genreTemplates: Record<GenreType, GenreTemplate>;
//# sourceMappingURL=genreTemplates.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"genreTemplates.d.ts","sourceRoot":"","sources":["../../src/prompts/genreTemplates.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,SAAS,GACjB,SAAS,GACT,SAAS,GACT,SAAS,GACT,OAAO,GACP,SAAS,GACT,QAAQ,GACR,UAAU,GACV,UAAU,GACV,YAAY,GACZ,UAAU,GACV,UAAU,GACV,QAAQ,CAAC;AAEb,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,OAAO,EAAE;QACP,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,EAAE,MAAM,CAAC;QAChB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,QAAQ,EAAE;QACR,IAAI,EAAE,MAAM,CAAC;QACb,GAAG,EAAE,MAAM,CAAC;QACZ,aAAa,CAAC,EAAE,MAAM,CAAC;KACxB,CAAC;CACH;AAED,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,SAAS,EAAE,aAAa,CAqL3D,CAAC"}

186
server/dist/prompts/genreTemplates.js vendored Normal file
View File

@@ -0,0 +1,186 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.genreTemplates = void 0;
exports.genreTemplates = {
fiction: {
name: 'Fiction',
description: 'General literary fiction with focus on character development and narrative',
icon: '📖',
structure: ['Introduction', 'Rising Action', 'Climax', 'Falling Action', 'Resolution'],
prompts: {
outline: 'Create a compelling fiction outline with well-developed characters and a satisfying narrative arc.',
chapter: 'Write a chapter that advances the plot while deepening character development.'
},
defaults: {
tone: 'engaging and literary',
pov: 'third-person or first-person',
typicalLength: '70,000-100,000 words'
}
},
mystery: {
name: 'Mystery',
description: 'Puzzle-driven narratives with clues, suspects, and a satisfying reveal',
icon: '🔍',
structure: ['Crime/Discovery', 'Investigation', 'Red Herrings', 'Breakthrough', 'Confrontation', 'Resolution'],
prompts: {
outline: 'Create a mystery outline with a compelling crime, multiple suspects with motives, red herrings, and a surprising but logical solution.',
chapter: 'Write a mystery chapter that reveals clues while maintaining suspense. Balance investigation scenes with character development.'
},
defaults: {
tone: 'suspenseful and methodical',
pov: 'third-person limited or first-person detective',
typicalLength: '80,000-100,000 words'
}
},
romance: {
name: 'Romance',
description: 'Love-centered stories with emotional intimacy and satisfying relationship resolution',
icon: '💕',
structure: ['Meet Cute', 'Attraction', 'Conflict', 'Dark Moment', 'Grand Gesture', 'HEA/HFN'],
prompts: {
outline: 'Create a romance outline with compelling chemistry, meaningful conflict, and an emotionally satisfying happy-ever-after or happy-for-now ending.',
chapter: 'Write a romance chapter that develops the relationship, creates emotional depth, and builds romantic tension.'
},
defaults: {
tone: 'warm and emotionally engaging',
pov: 'dual POV or single POV',
typicalLength: '50,000-90,000 words'
}
},
scifi: {
name: 'Science Fiction',
description: 'Speculative fiction exploring technology, space, time, and their impact on humanity',
icon: '🚀',
structure: ['Status Quo', 'Inciting Discovery', 'Exploration', 'Crisis', 'Resolution/New Order'],
prompts: {
outline: 'Create a sci-fi outline with believable technology/science, compelling world-building, and exploration of big ideas about humanity and progress.',
chapter: 'Write a sci-fi chapter that balances technical concepts with character emotion. Show the impact of technology on human experience.'
},
defaults: {
tone: 'thoughtful and imaginative',
pov: 'third-person omniscient or limited',
typicalLength: '90,000-120,000 words'
}
},
fantasy: {
name: 'Fantasy',
description: 'Magical worlds with supernatural elements, quests, and epic stakes',
icon: '🐉',
structure: ['Ordinary World', 'Call to Adventure', 'Trials', 'Ordeal', 'Reward', 'Return'],
prompts: {
outline: 'Create a fantasy outline with consistent magic systems, rich world-building, memorable characters, and an epic quest or conflict.',
chapter: 'Write a fantasy chapter that showcases the magic system and world while advancing character arcs and plot.'
},
defaults: {
tone: 'epic and wondrous',
pov: 'third-person limited or multiple POV',
typicalLength: '100,000-150,000 words'
}
},
horror: {
name: 'Horror',
description: 'Fear-driven narratives designed to unsettle, frighten, and provoke dread',
icon: '👻',
structure: ['Normalcy', 'First Disturbance', 'Escalation', 'All Is Lost', 'Confrontation', 'Aftermath'],
prompts: {
outline: 'Create a horror outline with building dread, effective scares, and psychological depth. Balance tension with release.',
chapter: 'Write a horror chapter that builds atmosphere and dread. Use pacing and sensory details to create fear.'
},
defaults: {
tone: 'dark and unsettling',
pov: 'first-person or close third-person',
typicalLength: '70,000-90,000 words'
}
},
thriller: {
name: 'Thriller',
description: 'High-stakes, fast-paced narratives with constant tension and danger',
icon: '⚡',
structure: ['Status Quo', 'Threat Emerges', 'Cat and Mouse', 'All Is Lost', 'Final Confrontation'],
prompts: {
outline: 'Create a thriller outline with relentless pacing, high stakes, and a formidable antagonist. Keep tension high throughout.',
chapter: 'Write a thriller chapter with strong pacing, cliffhangers, and escalating stakes. Keep readers on edge.'
},
defaults: {
tone: 'intense and gripping',
pov: 'multiple POV or close third-person',
typicalLength: '80,000-100,000 words'
}
},
children: {
name: "Children's Book",
description: 'Age-appropriate stories with clear morals, simple language, and engaging characters',
icon: '🧸',
structure: ['Introduction', 'Problem', 'Attempts', 'Solution', 'Lesson'],
prompts: {
outline: 'Create a children\'s book outline with age-appropriate themes, simple but engaging plot, and a clear positive message or lesson.',
chapter: 'Write a children\'s book chapter with simple vocabulary, engaging rhythm, and clear imagery. Keep sentences short and lively.'
},
defaults: {
tone: 'warm and encouraging',
pov: 'third-person simple',
typicalLength: '500-1000 words (picture book) or 10,000-30,000 (chapter book)'
}
},
nonfiction: {
name: 'Non-Fiction',
description: 'Factual, informative content organized around a central topic or argument',
icon: '📚',
structure: ['Introduction/Thesis', 'Background', 'Main Arguments', 'Evidence', 'Conclusion'],
prompts: {
outline: 'Create a non-fiction outline with clear organization, logical flow, and well-supported arguments or information.',
chapter: 'Write a non-fiction chapter that is informative, well-organized, and engaging. Use examples and clear explanations.'
},
defaults: {
tone: 'authoritative and accessible',
pov: 'authoritative voice',
typicalLength: '60,000-80,000 words'
}
},
selfhelp: {
name: 'Self-Help',
description: 'Practical guidance for personal improvement and growth',
icon: '🌱',
structure: ['Problem Identification', 'Root Causes', 'Solution Framework', 'Implementation', 'Maintenance'],
prompts: {
outline: 'Create a self-help outline with actionable advice, practical exercises, and a clear transformation path for readers.',
chapter: 'Write a self-help chapter with clear takeaways, practical exercises, and motivating examples. Balance theory with action.'
},
defaults: {
tone: 'encouraging and practical',
pov: 'direct address to reader',
typicalLength: '50,000-70,000 words'
}
},
business: {
name: 'Business',
description: 'Professional insights, strategies, and case studies for business success',
icon: '💼',
structure: ['Current Landscape', 'Key Principles', 'Case Studies', 'Implementation Guide', 'Future Outlook'],
prompts: {
outline: 'Create a business book outline with actionable strategies, real-world examples, and clear frameworks readers can apply.',
chapter: 'Write a business chapter with data-driven insights, case studies, and actionable takeaways. Balance theory with practice.'
},
defaults: {
tone: 'professional and authoritative',
pov: 'expert voice',
typicalLength: '60,000-80,000 words'
}
},
memoir: {
name: 'Memoir',
description: 'Personal life stories focused on transformation and universal themes',
icon: '✍️',
structure: ['Before', 'Catalyst', 'Journey', 'Transformation', 'After/Reflection'],
prompts: {
outline: 'Create a memoir outline that weaves personal narrative with universal themes. Focus on transformation and meaning.',
chapter: 'Write a memoir chapter with vivid scenes, honest reflection, and emotional truth. Show, don\'t tell.'
},
defaults: {
tone: 'intimate and reflective',
pov: 'first-person',
typicalLength: '70,000-90,000 words'
}
}
};
//# sourceMappingURL=genreTemplates.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"genreTemplates.js","sourceRoot":"","sources":["../../src/prompts/genreTemplates.ts"],"names":[],"mappings":"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"}

37
server/package.json Normal file
View File

@@ -0,0 +1,37 @@
{
"name": "creabook-server",
"version": "1.0.0",
"description": "CreaBook backend API with cloud AI integration",
"main": "dist/index.js",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
},
"dependencies": {
"@libsql/client": "^0.17.2",
"@prisma/adapter-libsql": "^7.6.0",
"@prisma/client": "^7.6.0",
"@types/bcryptjs": "^2.4.6",
"@types/jsonwebtoken": "^9.0.10",
"axios": "^1.6.8",
"bcryptjs": "^3.0.3",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.18.2",
"express-session": "^1.17.3",
"jsonwebtoken": "^9.0.3",
"multer": "^1.4.5-lts.1",
"prisma": "^7.6.0",
"sharp": "^0.33.2",
"zod": "^3.22.4"
},
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/multer": "^1.4.11",
"@types/node": "^20.11.0",
"tsx": "^4.7.0",
"typescript": "^5.3.3"
}
}

14
server/prisma.config.ts Normal file
View File

@@ -0,0 +1,14 @@
// This file was generated by Prisma, and assumes you have installed the following:
// npm install --save-dev prisma dotenv
import "dotenv/config";
import { defineConfig } from "prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
},
datasource: {
url: process.env["DATABASE_URL"],
},
});

103
server/prisma/schema.prisma Normal file
View File

@@ -0,0 +1,103 @@
// This is your Prisma schema file
// CreaBook Database Schema with User Authentication and Data Persistence
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
}
// User model for authentication
model User {
id String @id @default(cuid())
email String @unique
password String // Hashed password
name String?
role String @default("USER")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
books Book[]
coverDesigns CoverDesign[]
sessions Session[]
}
// Session model for JWT token management
model Session {
id String @id @default(cuid())
userId String
token String @unique
expiresAt DateTime
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
// Book model for storing user books
model Book {
id String @id @default(cuid())
userId String
title String
genre String
idea String
logline String?
outline Json? // Stores chapter outline structure
currentChapter Int @default(1)
coverId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
chapters Chapter[]
characters Character[]
}
// Chapter model for storing chapter content
model Chapter {
id String @id @default(cuid())
bookId String
number Int
title String
summary String
content String?
isGenerated Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
book Book @relation(fields: [bookId], references: [id], onDelete: Cascade)
@@unique([bookId, number])
}
// Character model for storing character details
model Character {
id String @id @default(cuid())
bookId String
name String
role String // protagonist, antagonist, supporting
traits String // JSON array stored as string
motivation String?
backstory String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
book Book @relation(fields: [bookId], references: [id], onDelete: Cascade)
}
// CoverDesign model for storing cover designs
model CoverDesign {
id String @id @default(cuid())
userId String
filename String
url String
prompt String?
genre String?
isGenerated Boolean @default(false)
width Int?
height Int?
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}

119
server/src/api/auth.ts Normal file
View File

@@ -0,0 +1,119 @@
import { Router } from 'express';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import { prisma } from '../db.js';
export const authRoutes = Router();
const JWT_SECRET = process.env.JWT_SECRET || 'super-secret-key';
authRoutes.post('/register', async (req, res) => {
try {
const { email, password, name } = req.body;
if (!email || !password) {
return res.status(400).json({ error: 'Email and password are required' });
}
const existingUser = await prisma.user.findUnique({ where: { email } });
if (existingUser) {
return res.status(400).json({ error: 'Email already in use' });
}
const hashedPassword = await bcrypt.hash(password, 10);
const user = await prisma.user.create({
data: {
email,
password: hashedPassword,
name: name || email.split('@')[0],
role: email === 'admin@admin.com' || email === 'admin' ? 'ADMIN' : 'USER',
},
});
const token = jwt.sign({ userId: user.id }, JWT_SECRET, { expiresIn: '7d' });
await prisma.session.create({
data: {
userId: user.id,
token,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days
}
});
res.json({ token, user: { id: user.id, email: user.email, name: user.name, role: user.role } });
} catch (error) {
console.error('Register error:', error);
res.status(500).json({ error: 'Failed to register', details: String(error) });
}
});
authRoutes.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ error: 'Email and password are required' });
}
const user = await prisma.user.findUnique({ where: { email } });
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const token = jwt.sign({ userId: user.id }, JWT_SECRET, { expiresIn: '7d' });
await prisma.session.create({
data: {
userId: user.id,
token,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
}
});
res.json({ token, user: { id: user.id, email: user.email, name: user.name, role: user.role } });
} catch (error) {
console.error('Login error:', error);
res.status(500).json({ error: 'Failed to login', details: String(error) });
}
});
authRoutes.get('/me', async (req, res) => {
try {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
const token = authHeader.substring(7);
const decoded = jwt.verify(token, JWT_SECRET) as { userId: string };
const session = await prisma.session.findUnique({ where: { token } });
if (!session || session.expiresAt < new Date()) {
return res.status(401).json({ error: 'Session expired' });
}
const user = await prisma.user.findUnique({ where: { id: decoded.userId } });
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json({ user: { id: user.id, email: user.email, name: user.name, role: user.role } });
} catch (error) {
res.status(401).json({ error: 'Invalid token' });
}
});
authRoutes.post('/logout', async (req, res) => {
try {
const authHeader = req.headers.authorization;
if (authHeader?.startsWith('Bearer ')) {
const token = authHeader.substring(7);
await prisma.session.deleteMany({ where: { token } });
}
res.json({ message: 'Logged out successfully' });
} catch (error) {
res.status(500).json({ error: 'Failed to logout' });
}
});

337
server/src/api/books.ts Normal file
View File

@@ -0,0 +1,337 @@
import { Router } from 'express';
import axios from 'axios';
import { genreTemplates, GenreType } from '../prompts/genreTemplates.js';
export const bookRoutes = Router();
// Get all available genre templates
bookRoutes.get('/genres', (req, res) => {
const genres = Object.entries(genreTemplates).map(([key, template]) => ({
id: key,
name: template.name,
description: template.description,
icon: template.icon
}));
res.json({ genres });
});
// Get a specific genre template
bookRoutes.get('/genres/:genreId', (req, res) => {
const { genreId } = req.params;
const template = genreTemplates[genreId as GenreType];
if (!template) {
return res.status(404).json({ error: 'Genre not found' });
}
res.json({ template });
});
// Generate book outline based on genre and idea
bookRoutes.post('/outline', async (req, res) => {
try {
const { genre, idea, title, language } = req.body;
const targetLang = language && language.startsWith('es') ? 'Spanish' : 'English';
if (!genre || !idea) {
return res.status(400).json({ error: 'genre and idea are required' });
}
const template = genreTemplates[genre as GenreType];
if (!template) {
return res.status(400).json({ error: 'Invalid genre' });
}
const prompt = `${template.prompts.outline}
Book Title: ${title || 'Untitled'}
Core Idea: ${idea}
Generate a detailed chapter outline following the structure: ${template.structure.join(' → ')}
IMPORTANT: The response MUST be written in ${targetLang}. All text values (title, logline, chapter summaries, etc) must be translated to ${targetLang}. Keep the exact JSON KEYS in English.
Return the response in JSON format:
{
"title": "Book Title",
"genre": "${genre}",
"logline": "One sentence summary",
"chapters": [
{"number": 1, "title": "Chapter Title", "summary": "Brief description"},
...
]
}`;
const response = await axios.post('https://openrouter.ai/api/v1/chat/completions', {
model: 'nvidia/nemotron-3-nano-30b-a3b:free', // Free model on OpenRouter
messages: [{ role: 'user', content: prompt }],
temperature: 0.7,
max_tokens: 2000
}, {
headers: {
'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}`,
'Content-Type': 'application/json',
'HTTP-Referer': 'https://creabook.app',
'X-Title': 'CreaBook'
}
});
const content = response.data.choices[0].message.content;
console.log('=== Outline AI Response ===');
console.log('Raw content:', content);
// Try to parse JSON from response
let outline;
const jsonMatch = content.match(/\{[\s\S]*\}/);
if (jsonMatch) {
try {
outline = JSON.parse(jsonMatch[0]);
console.log('Parsed outline:', outline);
} catch (parseError) {
console.error('JSON parse error:', parseError);
outline = { raw: content, error: 'Failed to parse JSON' };
}
} else {
console.error('No JSON object found in response');
outline = { raw: content, error: 'No JSON found' };
}
res.json({ outline });
} catch (error) {
console.error('Outline generation error:', error);
res.status(500).json({
error: 'Failed to generate outline',
details: error instanceof Error ? error.message : String(error)
});
}
});
// Generate a chapter based on outline
bookRoutes.post('/chapter', async (req, res) => {
try {
const { genre, chapterTitle, chapterSummary, previousContent, language } = req.body;
const targetLang = language && language.startsWith('es') ? 'Spanish' : 'English';
if (!genre || !chapterTitle || !chapterSummary) {
return res.status(400).json({ error: 'genre, chapterTitle, and chapterSummary are required' });
}
const template = genreTemplates[genre as GenreType];
let prompt = `${template.prompts.chapter}
Chapter: ${chapterTitle}
Summary: ${chapterSummary}
Tone: ${template.defaults.tone}
POV: ${template.defaults.pov}
IMPORTANT: The entire chapter content MUST be written strictly in ${targetLang}.
`;
if (previousContent) {
prompt += `\n\nPrevious content for context:\n${previousContent.substring(0, 2000)}...`;
}
const response = await axios.post('https://openrouter.ai/api/v1/chat/completions', {
model: 'nvidia/nemotron-3-nano-30b-a3b:free',
messages: [{ role: 'user', content: prompt }],
temperature: 0.7,
max_tokens: 3000
}, {
headers: {
'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}`,
'Content-Type': 'application/json',
'HTTP-Referer': 'https://creabook.app',
'X-Title': 'CreaBook'
}
});
const content = response.data.choices[0].message.content;
res.json({
content,
chapterTitle
});
} catch (error) {
console.error('Chapter generation error:', error);
res.status(500).json({
error: 'Failed to generate chapter',
details: error instanceof Error ? error.message : String(error)
});
}
});
// Expand or refine text
bookRoutes.post('/expand', async (req, res) => {
try {
const { text, instruction = 'Expand and improve this text', language } = req.body;
const targetLang = language && language.startsWith('es') ? 'Spanish' : 'English';
if (!text) {
return res.status(400).json({ error: 'text is required' });
}
const prompt = `${instruction}:
Original text:
${text}
Provide an expanded and improved version. IMPORTANT: Write the expanded text entirely in ${targetLang}:`;
const response = await axios.post('https://openrouter.ai/api/v1/chat/completions', {
model: 'nvidia/nemotron-3-nano-30b-a3b:free',
messages: [{ role: 'user', content: prompt }],
temperature: 0.7,
max_tokens: 2000
}, {
headers: {
'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}`,
'Content-Type': 'application/json',
'HTTP-Referer': 'https://creabook.app',
'X-Title': 'CreaBook'
}
});
const content = response.data.choices[0].message.content;
res.json({ expanded: content });
} catch (error) {
console.error('Expand error:', error);
res.status(500).json({
error: 'Failed to expand text',
details: error instanceof Error ? error.message : String(error)
});
}
});
// Generate character suggestions
bookRoutes.post('/characters', async (req, res) => {
try {
const { genre, storyIdea, language } = req.body;
const targetLang = language && language.startsWith('es') ? 'Spanish' : 'English';
if (!genre || !storyIdea) {
return res.status(400).json({ error: 'genre and storyIdea are required' });
}
const template = genreTemplates[genre as GenreType];
const prompt = `Based on this ${genre} story idea, suggest 3-5 main characters.
Story Idea: ${storyIdea}
Genre conventions: ${template.description}
IMPORTANT: Write the names, traits, motivation, and backstory values completely in ${targetLang}. Keep the required JSON keys strictly in English.
Return ONLY a valid JSON array with this exact format:
[
{
"name": "Character Name",
"role": "protagonist|antagonist|supporting",
"traits": ["trait1", "trait2"],
"motivation": "What drives them",
"backstory": "Brief backstory"
}
]
Do not include any text outside the JSON array.`;
console.log('=== Character Generation Prompt ===');
console.log(prompt);
const response = await axios.post('https://openrouter.ai/api/v1/chat/completions', {
model: 'nvidia/nemotron-3-nano-30b-a3b:free',
messages: [{ role: 'user', content: prompt }],
temperature: 0.7,
max_tokens: 1500
}, {
headers: {
'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}`,
'Content-Type': 'application/json',
'HTTP-Referer': 'https://creabook.app',
'X-Title': 'CreaBook'
}
});
const content = response.data.choices[0].message.content;
console.log('=== Character AI Response ===');
console.log('Raw content:', content);
let characters;
const jsonMatch = content.match(/\[[\s\S]*\]/);
if (jsonMatch) {
try {
characters = JSON.parse(jsonMatch[0]);
console.log('Parsed characters:', characters);
} catch (parseError) {
console.error('JSON parse error:', parseError);
characters = { raw: content, error: 'Failed to parse JSON' };
}
} else {
console.error('No JSON array found in response');
characters = { raw: content, error: 'No JSON array found' };
}
res.json({ characters });
} catch (error) {
console.error('Character generation error:', error);
res.status(500).json({
error: 'Failed to generate characters',
details: error instanceof Error ? error.message : String(error)
});
}
});
// Generate plot suggestions
bookRoutes.post('/plot', async (req, res) => {
try {
const { genre, currentPlot, issue, language } = req.body;
const targetLang = language && language.startsWith('es') ? 'Spanish' : 'English';
if (!genre || !currentPlot) {
return res.status(400).json({ error: 'genre and currentPlot are required' });
}
const template = genreTemplates[genre as GenreType];
const prompt = `Help develop the plot for this ${genre} story:
Current Plot:
${currentPlot}
${issue ? `Specific Issue: ${issue}` : 'Suggest plot developments and twists.'}
Consider genre conventions: ${template.description}
Structure: ${template.structure.join(' → ')}
IMPORTANT: Provide all specific, actionable plot suggestions completely in ${targetLang}:`;
const response = await axios.post('https://openrouter.ai/api/v1/chat/completions', {
model: 'nvidia/nemotron-3-nano-30b-a3b:free',
messages: [{ role: 'user', content: prompt }],
temperature: 0.7,
max_tokens: 1500
}, {
headers: {
'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}`,
'Content-Type': 'application/json',
'HTTP-Referer': 'https://creabook.app',
'X-Title': 'CreaBook'
}
});
const content = response.data.choices[0].message.content;
res.json({ suggestions: content });
} catch (error) {
console.error('Plot suggestion error:', error);
res.status(500).json({
error: 'Failed to generate plot suggestions',
details: error instanceof Error ? error.message : String(error)
});
}
});

232
server/src/api/covers.ts Normal file
View File

@@ -0,0 +1,232 @@
import express, { Router } from 'express';
import multer from 'multer';
import path from 'path';
import fs from 'fs';
import sharp from 'sharp';
import axios from 'axios';
export const coverRoutes = Router();
// Configure multer for file uploads
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const uploadDir = path.join(process.cwd(), 'uploads', 'covers');
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
cb(null, uploadDir);
},
filename: (req, file, cb) => {
const uniqueSuffix = `${Date.now()}-${Math.round(Math.random() * 1E9)}`;
cb(null, `cover-${uniqueSuffix}${path.extname(file.originalname)}`);
}
});
const upload = multer({
storage,
limits: { fileSize: 10 * 1024 * 1024 }, // 10MB limit
fileFilter: (req, file, cb) => {
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp'];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('Invalid file type. Only JPEG, PNG, and WEBP are allowed.'));
}
}
});
// Upload a cover image
coverRoutes.post('/upload', upload.single('image'), (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
}
res.json({
id: path.basename(req.file.filename, path.extname(req.file.filename)),
filename: req.file.filename,
path: req.file.path,
url: `/uploads/covers/${req.file.filename}`,
size: req.file.size
});
} catch (error) {
console.error('Upload error:', error);
res.status(500).json({
error: 'Failed to upload image',
details: error instanceof Error ? error.message : String(error)
});
}
});
// Get all covers
coverRoutes.get('/', (req, res) => {
try {
const coversDir = path.join(process.cwd(), 'uploads', 'covers');
if (!fs.existsSync(coversDir)) {
return res.json({ covers: [] });
}
const files = fs.readdirSync(coversDir)
.filter(file => /\.(jpg|jpeg|png|webp)$/i.test(file))
.map(file => ({
id: path.basename(file, path.extname(file)),
filename: file,
url: `/uploads/covers/${file}`,
createdAt: fs.statSync(path.join(coversDir, file)).mtime
}))
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
res.json({ covers: files });
} catch (error) {
console.error('List covers error:', error);
res.status(500).json({
error: 'Failed to list covers',
details: error instanceof Error ? error.message : String(error)
});
}
});
// Delete a cover
coverRoutes.delete('/:id', (req, res) => {
try {
const coverPath = path.join(process.cwd(), 'uploads', 'covers', `${req.params.id}*`);
const coversDir = path.join(process.cwd(), 'uploads', 'covers');
if (!fs.existsSync(coversDir)) {
return res.status(404).json({ error: 'Cover not found' });
}
const files = fs.readdirSync(coversDir)
.filter(f => f.startsWith(`${req.params.id}`));
if (files.length === 0) {
return res.status(404).json({ error: 'Cover not found' });
}
files.forEach(file => {
fs.unlinkSync(path.join(coversDir, file));
});
res.json({ message: 'Cover deleted successfully' });
} catch (error) {
console.error('Delete error:', error);
res.status(500).json({
error: 'Failed to delete cover',
details: error instanceof Error ? error.message : String(error)
});
}
});
// Process cover image (resize, filter, etc.)
coverRoutes.post('/process', upload.single('image'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
}
const { width, height, filter, brightness, contrast } = req.body;
let pipeline = sharp(req.file.path);
// Resize if dimensions provided
if (width && height) {
pipeline = pipeline.resize(parseInt(width), parseInt(height), {
fit: 'cover'
});
}
// Apply filters
if (filter === 'grayscale') {
pipeline = pipeline.grayscale();
} else if (filter === 'sepia') {
pipeline = pipeline.modulate({ saturation: 0, brightness: 1.1 });
}
// Adjust brightness and saturation (sharp doesn't have contrast)
if (brightness) {
pipeline = pipeline.modulate({ brightness: parseFloat(brightness) });
}
if (contrast) {
pipeline = pipeline.modulate({ saturation: parseFloat(contrast) });
}
const processedBuffer = await pipeline.toBuffer();
const outputPath = path.join(process.cwd(), 'uploads', 'covers', `processed-${req.file.filename}`);
await pipeline.toFile(outputPath);
res.json({
id: `processed-${path.basename(req.file.filename, path.extname(req.file.filename))}`,
filename: `processed-${req.file.filename}`,
path: outputPath,
url: `/uploads/covers/processed-${req.file.filename}`
});
} catch (error) {
console.error('Process error:', error);
res.status(500).json({
error: 'Failed to process image',
details: error instanceof Error ? error.message : String(error)
});
}
});
// Generate cover image using AI (Pollinations.ai - Free, no API key required)
coverRoutes.post('/generate', async (req, res) => {
try {
const { prompt, genre, language } = req.body;
const targetLang = language && language.startsWith('es') ? 'es' : 'en';
if (!prompt) {
return res.status(400).json({ error: 'prompt is required' });
}
let enhancedPrompt = '';
if (targetLang === 'es') {
enhancedPrompt = genre
? `Portada de libro para una novela de ${genre}: ${prompt}, diseño profesional de portada de libro, alta calidad, ilustración detallada, 8k, obra maestra`
: `Portada de libro: ${prompt}, diseño profesional de portada de libro, alta calidad, ilustración detallada, 8k, obra maestra`;
} else {
enhancedPrompt = genre
? `Book cover for a ${genre} novel: ${prompt}, professional book cover design, high quality, detailed illustration, 8k, masterpiece`
: `Book cover: ${prompt}, professional book cover design, high quality, detailed illustration, 8k, masterpiece`;
}
// Use Pollinations.ai free text-to-image API (no API key required)
const imageUrl = `https://image.pollinations.ai/prompt/${encodeURIComponent(enhancedPrompt)}?width=1024&height=1024&nologo=true&seed=${Date.now()}`;
// Fetch the generated image
const imageResponse = await axios.get(imageUrl, {
responseType: 'arraybuffer',
timeout: 30000
});
const buffer = Buffer.from(imageResponse.data);
const uploadsDir = path.join(process.cwd(), 'uploads', 'covers');
if (!fs.existsSync(uploadsDir)) {
fs.mkdirSync(uploadsDir, { recursive: true });
}
const uniqueSuffix = `${Date.now()}-${Math.round(Math.random() * 1E9)}`;
const filename = `generated-${uniqueSuffix}.png`;
const filePath = path.join(uploadsDir, filename);
fs.writeFileSync(filePath, buffer);
res.json({
id: `generated-${uniqueSuffix}`,
filename,
path: filePath,
url: `/uploads/covers/${filename}`,
generated: true
});
} catch (error) {
console.error('Image generation error:', error);
res.status(500).json({
error: 'Failed to generate image',
details: error instanceof Error ? error.message : String(error)
});
}
});

8
server/src/db.ts Normal file
View File

@@ -0,0 +1,8 @@
import { PrismaClient } from '@prisma/client';
import { PrismaLibSql } from '@prisma/adapter-libsql';
const adapter = new PrismaLibSql({
url: process.env.DATABASE_URL || "file:./dev.db",
});
export const prisma = new PrismaClient({ adapter });

34
server/src/index.ts Normal file
View File

@@ -0,0 +1,34 @@
import 'dotenv/config';
import express from 'express';
import cors from 'cors';
import path from 'path';
import { coverRoutes } from './api/covers.js';
import { bookRoutes } from './api/books.js';
import { authRoutes } from './api/auth.js';
const app = express();
const PORT = process.env.PORT || 5001;
// Middleware
app.use(cors());
app.use(express.json());
// API Routes
app.use('/api/covers', coverRoutes);
app.use('/api/books', bookRoutes);
app.use('/api/auth', authRoutes);
// Servir archivos estáticos de uploads
app.use('/uploads', express.static(path.join(process.cwd(), 'uploads')));
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
app.listen(PORT, () => {
console.log(`🚀 CreaBook server running on http://localhost:${PORT}`);
console.log(`☁️ Cloud AI integration ready`);
});

View File

@@ -0,0 +1,214 @@
export type GenreType =
| 'fiction'
| 'mystery'
| 'romance'
| 'scifi'
| 'fantasy'
| 'horror'
| 'thriller'
| 'children'
| 'nonfiction'
| 'selfhelp'
| 'business'
| 'memoir';
export interface GenreTemplate {
name: string;
description: string;
icon: string;
structure: string[];
prompts: {
outline: string;
chapter: string;
character?: string;
setting?: string;
};
defaults: {
tone: string;
pov: string;
typicalLength?: string;
};
}
export const genreTemplates: Record<GenreType, GenreTemplate> = {
fiction: {
name: 'Fiction',
description: 'General literary fiction with focus on character development and narrative',
icon: '📖',
structure: ['Introduction', 'Rising Action', 'Climax', 'Falling Action', 'Resolution'],
prompts: {
outline: 'Create a compelling fiction outline with well-developed characters and a satisfying narrative arc.',
chapter: 'Write a chapter that advances the plot while deepening character development.'
},
defaults: {
tone: 'engaging and literary',
pov: 'third-person or first-person',
typicalLength: '70,000-100,000 words'
}
},
mystery: {
name: 'Mystery',
description: 'Puzzle-driven narratives with clues, suspects, and a satisfying reveal',
icon: '🔍',
structure: ['Crime/Discovery', 'Investigation', 'Red Herrings', 'Breakthrough', 'Confrontation', 'Resolution'],
prompts: {
outline: 'Create a mystery outline with a compelling crime, multiple suspects with motives, red herrings, and a surprising but logical solution.',
chapter: 'Write a mystery chapter that reveals clues while maintaining suspense. Balance investigation scenes with character development.'
},
defaults: {
tone: 'suspenseful and methodical',
pov: 'third-person limited or first-person detective',
typicalLength: '80,000-100,000 words'
}
},
romance: {
name: 'Romance',
description: 'Love-centered stories with emotional intimacy and satisfying relationship resolution',
icon: '💕',
structure: ['Meet Cute', 'Attraction', 'Conflict', 'Dark Moment', 'Grand Gesture', 'HEA/HFN'],
prompts: {
outline: 'Create a romance outline with compelling chemistry, meaningful conflict, and an emotionally satisfying happy-ever-after or happy-for-now ending.',
chapter: 'Write a romance chapter that develops the relationship, creates emotional depth, and builds romantic tension.'
},
defaults: {
tone: 'warm and emotionally engaging',
pov: 'dual POV or single POV',
typicalLength: '50,000-90,000 words'
}
},
scifi: {
name: 'Science Fiction',
description: 'Speculative fiction exploring technology, space, time, and their impact on humanity',
icon: '🚀',
structure: ['Status Quo', 'Inciting Discovery', 'Exploration', 'Crisis', 'Resolution/New Order'],
prompts: {
outline: 'Create a sci-fi outline with believable technology/science, compelling world-building, and exploration of big ideas about humanity and progress.',
chapter: 'Write a sci-fi chapter that balances technical concepts with character emotion. Show the impact of technology on human experience.'
},
defaults: {
tone: 'thoughtful and imaginative',
pov: 'third-person omniscient or limited',
typicalLength: '90,000-120,000 words'
}
},
fantasy: {
name: 'Fantasy',
description: 'Magical worlds with supernatural elements, quests, and epic stakes',
icon: '🐉',
structure: ['Ordinary World', 'Call to Adventure', 'Trials', 'Ordeal', 'Reward', 'Return'],
prompts: {
outline: 'Create a fantasy outline with consistent magic systems, rich world-building, memorable characters, and an epic quest or conflict.',
chapter: 'Write a fantasy chapter that showcases the magic system and world while advancing character arcs and plot.'
},
defaults: {
tone: 'epic and wondrous',
pov: 'third-person limited or multiple POV',
typicalLength: '100,000-150,000 words'
}
},
horror: {
name: 'Horror',
description: 'Fear-driven narratives designed to unsettle, frighten, and provoke dread',
icon: '👻',
structure: ['Normalcy', 'First Disturbance', 'Escalation', 'All Is Lost', 'Confrontation', 'Aftermath'],
prompts: {
outline: 'Create a horror outline with building dread, effective scares, and psychological depth. Balance tension with release.',
chapter: 'Write a horror chapter that builds atmosphere and dread. Use pacing and sensory details to create fear.'
},
defaults: {
tone: 'dark and unsettling',
pov: 'first-person or close third-person',
typicalLength: '70,000-90,000 words'
}
},
thriller: {
name: 'Thriller',
description: 'High-stakes, fast-paced narratives with constant tension and danger',
icon: '⚡',
structure: ['Status Quo', 'Threat Emerges', 'Cat and Mouse', 'All Is Lost', 'Final Confrontation'],
prompts: {
outline: 'Create a thriller outline with relentless pacing, high stakes, and a formidable antagonist. Keep tension high throughout.',
chapter: 'Write a thriller chapter with strong pacing, cliffhangers, and escalating stakes. Keep readers on edge.'
},
defaults: {
tone: 'intense and gripping',
pov: 'multiple POV or close third-person',
typicalLength: '80,000-100,000 words'
}
},
children: {
name: "Children's Book",
description: 'Age-appropriate stories with clear morals, simple language, and engaging characters',
icon: '🧸',
structure: ['Introduction', 'Problem', 'Attempts', 'Solution', 'Lesson'],
prompts: {
outline: 'Create a children\'s book outline with age-appropriate themes, simple but engaging plot, and a clear positive message or lesson.',
chapter: 'Write a children\'s book chapter with simple vocabulary, engaging rhythm, and clear imagery. Keep sentences short and lively.'
},
defaults: {
tone: 'warm and encouraging',
pov: 'third-person simple',
typicalLength: '500-1000 words (picture book) or 10,000-30,000 (chapter book)'
}
},
nonfiction: {
name: 'Non-Fiction',
description: 'Factual, informative content organized around a central topic or argument',
icon: '📚',
structure: ['Introduction/Thesis', 'Background', 'Main Arguments', 'Evidence', 'Conclusion'],
prompts: {
outline: 'Create a non-fiction outline with clear organization, logical flow, and well-supported arguments or information.',
chapter: 'Write a non-fiction chapter that is informative, well-organized, and engaging. Use examples and clear explanations.'
},
defaults: {
tone: 'authoritative and accessible',
pov: 'authoritative voice',
typicalLength: '60,000-80,000 words'
}
},
selfhelp: {
name: 'Self-Help',
description: 'Practical guidance for personal improvement and growth',
icon: '🌱',
structure: ['Problem Identification', 'Root Causes', 'Solution Framework', 'Implementation', 'Maintenance'],
prompts: {
outline: 'Create a self-help outline with actionable advice, practical exercises, and a clear transformation path for readers.',
chapter: 'Write a self-help chapter with clear takeaways, practical exercises, and motivating examples. Balance theory with action.'
},
defaults: {
tone: 'encouraging and practical',
pov: 'direct address to reader',
typicalLength: '50,000-70,000 words'
}
},
business: {
name: 'Business',
description: 'Professional insights, strategies, and case studies for business success',
icon: '💼',
structure: ['Current Landscape', 'Key Principles', 'Case Studies', 'Implementation Guide', 'Future Outlook'],
prompts: {
outline: 'Create a business book outline with actionable strategies, real-world examples, and clear frameworks readers can apply.',
chapter: 'Write a business chapter with data-driven insights, case studies, and actionable takeaways. Balance theory with practice.'
},
defaults: {
tone: 'professional and authoritative',
pov: 'expert voice',
typicalLength: '60,000-80,000 words'
}
},
memoir: {
name: 'Memoir',
description: 'Personal life stories focused on transformation and universal themes',
icon: '✍️',
structure: ['Before', 'Catalyst', 'Journey', 'Transformation', 'After/Reflection'],
prompts: {
outline: 'Create a memoir outline that weaves personal narrative with universal themes. Focus on transformation and meaning.',
chapter: 'Write a memoir chapter with vivid scenes, honest reflection, and emotional truth. Show, don\'t tell.'
},
defaults: {
tone: 'intimate and reflective',
pov: 'first-person',
typicalLength: '70,000-90,000 words'
}
}
};

20
server/tsconfig.json Normal file
View File

@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB