Files
opencode-project-1/novel-writer/src/store.ts
ImGKTZ27 8af220f873 feat: initialize novel writer application with React, Zustand, and Vite
- Added main application structure with React and TypeScript.
- Implemented Zustand for state management, including novel, chapters, characters, and relationships.
- Created initial CSS styles for the application layout and components.
- Integrated SVG assets for branding.
- Set up Vite configuration for development and build processes.
- Established TypeScript configurations for app and node environments.
2026-01-06 18:39:16 +05:00

303 lines
8.0 KiB
TypeScript

import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { v4 as uuidv4 } from 'uuid';
export interface Character {
id: string;
name: string;
description: string;
role: 'protagonist' | 'antagonist' | 'supporting' | 'minor';
color: string;
}
export interface Relationship {
id: string;
sourceId: string;
targetId: string;
type: 'friend' | 'enemy' | 'family' | 'romantic' | 'colleague' | 'rival';
description: string;
strength: number;
}
export interface Chapter {
id: string;
title: string;
content: string;
order: number;
}
export interface Volume {
id: string;
title: string;
order: number;
chapters: Chapter[];
}
export interface Message {
id: string;
role: 'user' | 'assistant';
content: string;
timestamp: number;
}
export interface Novel {
id: string;
title: string;
description: string;
volumes: Volume[];
characters: Character[];
relationships: Relationship[];
}
interface NovelState {
novel: Novel;
selectedChapterId: string | null;
selectedCharacterId: string | null;
chatMessages: Message[];
isChatLoading: boolean;
chatModel: string;
ollamaUrl: string;
availableModels: string[];
ollamaConnected: boolean;
setNovel: (novel: Novel) => void;
addVolume: (title: string) => void;
addChapter: (volumeId: string, title: string) => void;
updateChapter: (chapterId: string, content: string, title?: string) => void;
selectChapter: (chapterId: string | null) => void;
addCharacter: (name: string, description: string, role: Character['role']) => void;
updateCharacter: (id: string, updates: Partial<Character>) => void;
deleteCharacter: (id: string) => void;
selectCharacter: (id: string | null) => void;
addRelationship: (sourceId: string, targetId: string, type: Relationship['type'], description: string, strength: number) => void;
updateRelationship: (id: string, updates: Partial<Relationship>) => void;
deleteRelationship: (id: string) => void;
addChatMessage: (message: Omit<Message, 'id' | 'timestamp'>) => void;
clearChat: () => void;
setChatLoading: (loading: boolean) => void;
setChatModel: (model: string) => void;
setOllamaUrl: (url: string) => void;
setAvailableModels: (models: string[]) => void;
setOllamaConnected: (connected: boolean) => void;
getContextForOllama: () => string;
}
const defaultNovel: Novel = {
id: uuidv4(),
title: 'My Webnovel',
description: '',
volumes: [],
characters: [],
relationships: [],
};
export const useNovelStore = create<NovelState>()(
persist(
(set, get) => ({
novel: defaultNovel,
selectedChapterId: null,
selectedCharacterId: null,
chatMessages: [],
isChatLoading: false,
chatModel: 'llama3.2',
ollamaUrl: 'http://localhost:11434',
availableModels: [],
ollamaConnected: false,
setNovel: (novel) => set({ novel }),
addVolume: (title) => {
const { novel } = get();
const newVolume: Volume = {
id: uuidv4(),
title,
order: novel.volumes.length,
chapters: [],
};
set({
novel: {
...novel,
volumes: [...novel.volumes, newVolume],
},
});
},
addChapter: (volumeId, title) => {
const { novel } = get();
const newChapter: Chapter = {
id: uuidv4(),
title,
content: '',
order: novel.volumes.find(v => v.id === volumeId)?.chapters.length || 0,
};
set({
novel: {
...novel,
volumes: novel.volumes.map(v =>
v.id === volumeId
? { ...v, chapters: [...v.chapters, newChapter] }
: v
),
},
selectedChapterId: newChapter.id,
});
},
updateChapter: (chapterId, content, title) => {
const { novel } = get();
set({
novel: {
...novel,
volumes: novel.volumes.map(v => ({
...v,
chapters: v.chapters.map(c =>
c.id === chapterId ? { ...c, content, title: title || c.title } : c
),
})),
},
});
},
selectChapter: (chapterId) => set({ selectedChapterId: chapterId }),
addCharacter: (name, description, role) => {
const { novel } = get();
const colors = ['#6366f1', '#ec4899', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#06b6d4'];
const newCharacter: Character = {
id: uuidv4(),
name,
description,
role,
color: colors[novel.characters.length % colors.length],
};
set({
novel: {
...novel,
characters: [...novel.characters, newCharacter],
},
});
},
updateCharacter: (id, updates) => {
const { novel } = get();
set({
novel: {
...novel,
characters: novel.characters.map(c =>
c.id === id ? { ...c, ...updates } : c
),
},
});
},
deleteCharacter: (id) => {
const { novel } = get();
set({
novel: {
...novel,
characters: novel.characters.filter(c => c.id !== id),
relationships: novel.relationships.filter(r => r.sourceId !== id && r.targetId !== id),
},
});
},
selectCharacter: (id) => set({ selectedCharacterId: id }),
addRelationship: (sourceId, targetId, type, description, strength) => {
const { novel } = get();
const newRelationship: Relationship = {
id: uuidv4(),
sourceId,
targetId,
type,
description,
strength,
};
set({
novel: {
...novel,
relationships: [...novel.relationships, newRelationship],
},
});
},
updateRelationship: (id, updates) => {
const { novel } = get();
set({
novel: {
...novel,
relationships: novel.relationships.map(r =>
r.id === id ? { ...r, ...updates } : r
),
},
});
},
deleteRelationship: (id) => {
const { novel } = get();
set({
novel: {
...novel,
relationships: novel.relationships.filter(r => r.id !== id),
},
});
},
addChatMessage: (message) => {
const newMessage: Message = {
...message,
id: uuidv4(),
timestamp: Date.now(),
};
set(state => ({
chatMessages: [...state.chatMessages, newMessage],
}));
},
clearChat: () => set({ chatMessages: [] }),
setChatLoading: (loading) => set({ isChatLoading: loading }),
setChatModel: (model) => set({ chatModel: model }),
setOllamaUrl: (url) => set({ ollamaUrl: url }),
setAvailableModels: (models) => set({ availableModels: models }),
setOllamaConnected: (connected) => set({ ollamaConnected: connected }),
getContextForOllama: () => {
const { novel } = get();
let context = `=== NOVEL OVERVIEW ===
Title: ${novel.title}
Description: ${novel.description}
=== CHARACTERS ===
${novel.characters.map(c => `- ${c.name} (${c.role}): ${c.description}`).join('\n')}
=== RELATIONSHIPS ===
${novel.relationships.map(r => {
const source = novel.characters.find(c => c.id === r.sourceId);
const target = novel.characters.find(c => c.id === r.targetId);
return `- ${source?.name} --[${r.type}]--> ${target?.name}: ${r.description}`;
}).join('\n')}
=== CHAPTERS CONTENT ===
`;
novel.volumes.forEach(volume => {
context += `\n--- VOLUME: ${volume.title} ---\n`;
volume.chapters.forEach(chapter => {
context += `\n## ${chapter.title}\n${chapter.content}\n`;
});
});
return context;
},
}),
{
name: 'novel-writer-storage',
}
)
);