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.
This commit is contained in:
302
novel-writer/src/store.ts
Normal file
302
novel-writer/src/store.ts
Normal file
@@ -0,0 +1,302 @@
|
||||
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',
|
||||
}
|
||||
)
|
||||
);
|
||||
Reference in New Issue
Block a user