- 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.
303 lines
8.0 KiB
TypeScript
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',
|
|
}
|
|
)
|
|
);
|