1
0
Fork 0

Multiple stories

This commit is contained in:
Pabloader 2026-02-17 11:03:28 +00:00
parent 21a26859da
commit 77802634ae
3 changed files with 158 additions and 15 deletions

View File

@ -64,4 +64,28 @@
flex-direction: row; flex-direction: row;
gap: 8px; gap: 8px;
flex-wrap: wrap; flex-wrap: wrap;
}
.lore {
display: flex;
flex-direction: column;
gap: 10px;
min-height: 80dvh;
.currentStory {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
gap: 10px;
.storiesSelector {
height: 24px;
flex-grow: 1;
}
}
.loreText {
flex-grow: 1;
}
} }

View File

@ -1,8 +1,9 @@
import { useCallback, useContext, useMemo } from "preact/hooks"; import { useCallback, useContext, useMemo } from "preact/hooks";
import { useBool } from "@common/hooks/useBool"; import { useBool } from "@common/hooks/useBool";
import { Modal } from "@common/components/modal/Modal"; import { Modal } from "@common/components/modal/Modal";
import { useInputCallback } from "@common/hooks/useInputCallback";
import { StateContext } from "../../contexts/state"; import { DEFAULT_STORY, StateContext } from "../../contexts/state";
import { LLMContext } from "../../contexts/llm"; import { LLMContext } from "../../contexts/llm";
import { MiniChat } from "../minichat/minichat"; import { MiniChat } from "../minichat/minichat";
import { AutoTextarea } from "../autoTextarea"; import { AutoTextarea } from "../autoTextarea";
@ -14,8 +15,29 @@ import styles from './header.module.css';
export const Header = () => { export const Header = () => {
const { contextLength, promptTokens, modelName, spentKudos } = useContext(LLMContext); const { contextLength, promptTokens, modelName, spentKudos } = useContext(LLMContext);
const { const {
messages, connection, systemPrompt, lore, userPrompt, bannedWords, summarizePrompt, summaryEnabled, totalSpentKudos, messages,
setSystemPrompt, setLore, setUserPrompt, addSwipe, setBannedWords, setInstruct, setSummarizePrompt, setSummaryEnabled, setConnection, connection,
systemPrompt,
lore,
userPrompt,
bannedWords,
summarizePrompt,
summaryEnabled,
totalSpentKudos,
stories,
currentStory,
setSystemPrompt,
setLore,
setUserPrompt,
addSwipe,
setBannedWords,
setInstruct,
setSummarizePrompt,
setSummaryEnabled,
setConnection,
setCurrentStory,
createStory,
deleteStory,
} = useContext(StateContext); } = useContext(StateContext);
const connectionsOpen = useBool(); const connectionsOpen = useBool();
@ -53,6 +75,24 @@ export const Header = () => {
} }
}, [setSummaryEnabled]); }, [setSummaryEnabled]);
const handleChangeStory = useInputCallback((story) => {
if (story === '@new') {
const id = prompt('Story id');
if (id) {
createStory(id);
setCurrentStory(id);
}
} else {
setCurrentStory(story);
}
}, []);
const handleDeleteStory = useCallback(() => {
if (confirm(`Delete story "${currentStory}"?`)) {
deleteStory(currentStory);
}
}, [currentStory]);
return ( return (
<div class={styles.header}> <div class={styles.header}>
<div class={styles.inputs}> <div class={styles.inputs}>
@ -90,12 +130,26 @@ export const Header = () => {
<h3 class={styles.modalTitle}>Connection settings</h3> <h3 class={styles.modalTitle}>Connection settings</h3>
<ConnectionEditor connection={connection} setConnection={setConnection} /> <ConnectionEditor connection={connection} setConnection={setConnection} />
</Modal> </Modal>
<Modal open={loreOpen.value} onClose={loreOpen.setFalse}> <Modal open={loreOpen.value} onClose={loreOpen.setFalse} class={styles.lore}>
<h3 class={styles.modalTitle}>Lore Editor</h3> <h3 class={styles.modalTitle}>Lore Editor</h3>
<div class={styles.currentStory}>
<select value={currentStory} onChange={handleChangeStory} class={styles.storiesSelector}>
{Object.keys(stories).map((story) => (
<option key={story} value={story}>{story}</option>
))}
<option value='@new'>New Story...</option>
</select>
{currentStory !== DEFAULT_STORY
? <button class='icon' onClick={handleDeleteStory}>
🗑
</button>
: null}
</div>
<AutoTextarea <AutoTextarea
value={lore} value={lore}
onInput={setLore} onInput={setLore}
placeholder="Describe your world, for example: World of Awoo has big mountains and wide rivers." placeholder="Describe your world, for example: World of Awoo has big mountains and wide rivers."
class={styles.loreText}
/> />
</Modal> </Modal>
<Modal open={genparamsOpen.value} onClose={genparamsOpen.setFalse}> <Modal open={genparamsOpen.value} onClose={genparamsOpen.setFalse}>

View File

@ -4,19 +4,25 @@ import { MessageTools, type IMessage } from "../tools/messages";
import { useInputState } from "@common/hooks/useInputState"; import { useInputState } from "@common/hooks/useInputState";
import { type IConnection } from "../tools/connection"; import { type IConnection } from "../tools/connection";
import { loadObject, saveObject } from "../tools/storage"; import { loadObject, saveObject } from "../tools/storage";
import { useInputCallback } from "@common/hooks/useInputCallback";
interface IStory {
lore: string;
messages: IMessage[];
}
interface IContext { interface IContext {
currentConnection: number; currentConnection: number;
availableConnections: IConnection[]; availableConnections: IConnection[];
input: string; input: string;
systemPrompt: string; systemPrompt: string;
lore: string;
userPrompt: string; userPrompt: string;
summarizePrompt: string; summarizePrompt: string;
summaryEnabled: boolean; summaryEnabled: boolean;
bannedWords: string[]; bannedWords: string[];
messages: IMessage[];
totalSpentKudos: number; totalSpentKudos: number;
stories: Record<string, IStory>;
currentStory: string;
// //
triggerNext: boolean; triggerNext: boolean;
continueLast: boolean; continueLast: boolean;
@ -24,6 +30,8 @@ interface IContext {
interface IComputableContext { interface IComputableContext {
connection: IConnection; connection: IConnection;
lore: string;
messages: IMessage[];
} }
interface IActions { interface IActions {
@ -52,9 +60,14 @@ interface IActions {
addSwipe: (index: number, content: string) => void; addSwipe: (index: number, content: string) => void;
continueMessage: (continueLast?: boolean) => void; continueMessage: (continueLast?: boolean) => void;
setCurrentStory: (id: string) => void;
createStory: (id: string) => void;
deleteStory: (id: string) => void;
} }
const SAVE_KEY = 'ai_game_save_state'; const SAVE_KEY = 'ai_game_save_state';
export const DEFAULT_STORY = 'default';
export enum Instruct { export enum Instruct {
CHATML = `{% for message in messages %}{{'<|im_start|>' + message['role'] + '\\n\\n' + message['content'] + '<|im_end|>' + '\\n'}}{% endfor %}{% if add_generation_prompt %}{{ '<|im_start|>assistant\\n\\n' }}{% endif %}`, CHATML = `{% for message in messages %}{{'<|im_start|>' + message['role'] + '\\n\\n' + message['content'] + '<|im_end|>' + '\\n'}}{% endfor %}{% if add_generation_prompt %}{{ '<|im_start|>assistant\\n\\n' }}{% endif %}`,
@ -79,7 +92,8 @@ const DEFAULT_CONTEXT: IContext = {
}], }],
input: '', input: '',
systemPrompt: 'You are a creative writer. Write a story based on the world description below. Story should be adult and mature; and could include swearing, violence and unfairness. Portray characters realistically and stay in the lore.', systemPrompt: 'You are a creative writer. Write a story based on the world description below. Story should be adult and mature; and could include swearing, violence and unfairness. Portray characters realistically and stay in the lore.',
lore: '', stories: {},
currentStory: DEFAULT_STORY,
userPrompt: `{% if isStart -%} userPrompt: `{% if isStart -%}
Write a novel using information above as a reference. Write a novel using information above as a reference.
{%- else -%} {%- else -%}
@ -94,16 +108,23 @@ Make sure to follow the world description and rules exactly. Avoid cliffhangers
summarizePrompt: 'Summarize following text in one paragraph:\n\n{{ message }}\n\nAnswer with shortened text only.', summarizePrompt: 'Summarize following text in one paragraph:\n\n{{ message }}\n\nAnswer with shortened text only.',
summaryEnabled: true, summaryEnabled: true,
bannedWords: [], bannedWords: [],
messages: [],
totalSpentKudos: 0, totalSpentKudos: 0,
triggerNext: false, triggerNext: false,
continueLast: false, continueLast: false,
}; };
export const saveContext = async (context: IContext) => { const EMPTY_STORY: IStory = {
const contextToSave: Partial<IContext> = { ...context }; lore: '',
messages: [],
};
const saveContext = async (context: IContext & IComputableContext) => {
const contextToSave: Partial<IContext & IComputableContext> = { ...context };
delete contextToSave.connection;
delete contextToSave.triggerNext; delete contextToSave.triggerNext;
delete contextToSave.continueLast; delete contextToSave.continueLast;
delete contextToSave.lore;
delete contextToSave.messages;
return saveObject(SAVE_KEY, contextToSave); return saveObject(SAVE_KEY, contextToSave);
} }
@ -118,12 +139,12 @@ export const StateContextProvider = ({ children }: { children?: any }) => {
const [currentConnection, setCurrentConnection] = useState<number>(loadedContext.currentConnection); const [currentConnection, setCurrentConnection] = useState<number>(loadedContext.currentConnection);
const [availableConnections, setAvailableConnections] = useState<IConnection[]>(loadedContext.availableConnections); const [availableConnections, setAvailableConnections] = useState<IConnection[]>(loadedContext.availableConnections);
const [input, setInput] = useInputState(loadedContext.input); const [input, setInput] = useInputState(loadedContext.input);
const [lore, setLore] = useInputState(loadedContext.lore); const [stories, setStories] = useState(loadedContext.stories);
const [currentStory, setCurrentStory] = useState(loadedContext.currentStory);
const [systemPrompt, setSystemPrompt] = useInputState(loadedContext.systemPrompt); const [systemPrompt, setSystemPrompt] = useInputState(loadedContext.systemPrompt);
const [userPrompt, setUserPrompt] = useInputState(loadedContext.userPrompt); const [userPrompt, setUserPrompt] = useInputState(loadedContext.userPrompt);
const [summarizePrompt, setSummarizePrompt] = useInputState(loadedContext.summarizePrompt); const [summarizePrompt, setSummarizePrompt] = useInputState(loadedContext.summarizePrompt);
const [bannedWords, setBannedWords] = useState<string[]>(loadedContext.bannedWords); const [bannedWords, setBannedWords] = useState<string[]>(loadedContext.bannedWords);
const [messages, setMessages] = useState(loadedContext.messages);
const [summaryEnabled, setSummaryEnabled] = useState(loadedContext.summaryEnabled); const [summaryEnabled, setSummaryEnabled] = useState(loadedContext.summaryEnabled);
const [totalSpentKudos, setTotalSpentKudos] = useState(loadedContext.totalSpentKudos); const [totalSpentKudos, setTotalSpentKudos] = useState(loadedContext.totalSpentKudos);
@ -145,6 +166,35 @@ export const StateContextProvider = ({ children }: { children?: any }) => {
useEffect(() => setConnection({ ...connection, instruct }), [instruct]); useEffect(() => setConnection({ ...connection, instruct }), [instruct]);
const setLore = useInputCallback((lore) => {
if (!currentStory) return;
setStories(ss => ({
...ss,
[currentStory]: {
...EMPTY_STORY,
...stories[currentStory],
lore,
}
}));
}, [currentStory]);
const setMessages = useCallback((msg: StateUpdater<IMessage[]>) => {
if (!currentStory) return;
let messages = (typeof msg === 'function')
? msg(stories[currentStory]?.messages ?? EMPTY_STORY.messages)
: msg;
setStories(ss => ({
...ss,
[currentStory]: {
...EMPTY_STORY,
...stories[currentStory],
messages,
}
}));
}, [currentStory]);
const actions: IActions = useMemo(() => ({ const actions: IActions = useMemo(() => ({
setConnection, setConnection,
setCurrentConnection, setCurrentConnection,
@ -159,6 +209,7 @@ export const StateContextProvider = ({ children }: { children?: any }) => {
setTriggerNext, setTriggerNext,
setContinueLast, setContinueLast,
setTotalSpentKudos, setTotalSpentKudos,
setCurrentStory,
setBannedWords: (words) => setBannedWords(words.slice()), setBannedWords: (words) => setBannedWords(words.slice()),
setAvailableConnections: (connections) => setAvailableConnections(connections.slice()), setAvailableConnections: (connections) => setAvailableConnections(connections.slice()),
@ -233,7 +284,19 @@ export const StateContextProvider = ({ children }: { children?: any }) => {
setTriggerNext(true); setTriggerNext(true);
setContinueLast(c); setContinueLast(c);
}, },
}), []); createStory: (id: string) => {
setStories(ss => ({
...ss,
[id]: { ...EMPTY_STORY }
}))
},
deleteStory: (id: string) => {
if (id === DEFAULT_STORY) return;
setStories(ss => Object.fromEntries(Object.entries(ss).filter(([k]) => k !== id)));
setCurrentStory(cs => cs === id ? DEFAULT_STORY : cs);
}
}), [setLore, setMessages]);
const rawContext: IContext & IComputableContext = { const rawContext: IContext & IComputableContext = {
connection, connection,
@ -241,16 +304,18 @@ export const StateContextProvider = ({ children }: { children?: any }) => {
availableConnections, availableConnections,
input, input,
systemPrompt, systemPrompt,
lore,
userPrompt, userPrompt,
summarizePrompt, summarizePrompt,
summaryEnabled, summaryEnabled,
bannedWords, bannedWords,
messages,
totalSpentKudos, totalSpentKudos,
stories,
currentStory,
// //
triggerNext, triggerNext,
continueLast, continueLast,
lore: stories[currentStory]?.lore ?? '',
messages: stories[currentStory]?.messages ?? [],
}; };
const context = useMemo(() => rawContext, Object.values(rawContext)); const context = useMemo(() => rawContext, Object.values(rawContext));