Multiple stories
This commit is contained in:
parent
21a26859da
commit
77802634ae
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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}>
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue