diff --git a/src/games/ai-story/components/header/header.module.css b/src/games/ai-story/components/header/header.module.css index 8dba6d8..134acd7 100644 --- a/src/games/ai-story/components/header/header.module.css +++ b/src/games/ai-story/components/header/header.module.css @@ -22,6 +22,11 @@ .info { margin: 0 8px; line-height: 36px; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + gap: 16px; } .buttons { diff --git a/src/games/ai-story/components/header/header.tsx b/src/games/ai-story/components/header/header.tsx index 3d778e2..29febaa 100644 --- a/src/games/ai-story/components/header/header.tsx +++ b/src/games/ai-story/components/header/header.tsx @@ -12,9 +12,9 @@ import { ConnectionEditor } from "./connectionEditor"; import styles from './header.module.css'; export const Header = () => { - const { contextLength, promptTokens, modelName } = useContext(LLMContext); + const { contextLength, promptTokens, modelName, spentKudos } = useContext(LLMContext); const { - messages, connection, systemPrompt, lore, userPrompt, bannedWords, summarizePrompt, summaryEnabled, + messages, connection, systemPrompt, lore, userPrompt, bannedWords, summarizePrompt, summaryEnabled, totalSpentKudos, setSystemPrompt, setLore, setUserPrompt, addSwipe, setBannedWords, setInstruct, setSummarizePrompt, setSummaryEnabled, setConnection, } = useContext(StateContext); @@ -62,7 +62,10 @@ export const Header = () => {
- {modelName} - {promptTokens} / {contextLength} + {modelName} + 📃{promptTokens}/{contextLength} + 💲{spentKudos} + 💰{totalSpentKudos}
diff --git a/src/games/ai-story/contexts/llm.tsx b/src/games/ai-story/contexts/llm.tsx index 078dfa2..3580c89 100644 --- a/src/games/ai-story/contexts/llm.tsx +++ b/src/games/ai-story/contexts/llm.tsx @@ -26,6 +26,7 @@ interface IContext { hasToolCalls: boolean; promptTokens: number; contextLength: number; + spentKudos: number; } const MESSAGES_TO_KEEP = 10; @@ -49,7 +50,7 @@ const processing = { export const LLMContextProvider = ({ children }: { children?: any }) => { const { connection, messages, triggerNext, continueLast, lore, userPrompt, systemPrompt, bannedWords, summarizePrompt, summaryEnabled, - setTriggerNext, setContinueLast, addMessage, editMessage, editSummary, + setTriggerNext, setContinueLast, addMessage, editMessage, editSummary, setTotalSpentKudos, } = useContext(StateContext); const generating = useBool(false); @@ -57,6 +58,7 @@ export const LLMContextProvider = ({ children }: { children?: any }) => { const [contextLength, setContextLength] = useState(0); const [modelName, setModelName] = useState(''); const [hasToolCalls, setHasToolCalls] = useState(false); + const [spentKudos, setSpentKudos] = useState(0); const isOnline = useMemo(() => contextLength > 0, [contextLength]); @@ -170,10 +172,15 @@ export const LLMContextProvider = ({ children }: { children?: any }) => { try { console.log('[LLM.generate]', prompt); - yield* Connection.generate(connection, prompt, { + setSpentKudos(0); + for await (const { text, cost } of Connection.generate(connection, prompt, { ...extraSettings, banned_tokens: bannedWords.filter(w => w.trim()), - }); + })) { + setSpentKudos(sk => sk + cost); + setTotalSpentKudos(sk => sk + cost); + yield text; + } } catch (e) { if (e instanceof Error && e.name !== 'AbortError') { alert(e.message); @@ -297,6 +304,7 @@ export const LLMContextProvider = ({ children }: { children?: any }) => { hasToolCalls, promptTokens, contextLength, + spentKudos, }; const context = useMemo(() => rawContext, Object.values(rawContext)); diff --git a/src/games/ai-story/contexts/state.tsx b/src/games/ai-story/contexts/state.tsx index 2e4fa41..893ecfe 100644 --- a/src/games/ai-story/contexts/state.tsx +++ b/src/games/ai-story/contexts/state.tsx @@ -1,5 +1,5 @@ import { createContext } from "preact"; -import { useCallback, useEffect, useMemo, useState } from "preact/hooks"; +import { useCallback, useEffect, useMemo, useState, type Dispatch, type StateUpdater } from "preact/hooks"; import { MessageTools, type IMessage } from "../tools/messages"; import { useInputState } from "@common/hooks/useInputState"; import { type IConnection } from "../tools/connection"; @@ -15,6 +15,7 @@ interface IContext { summaryEnabled: boolean; bannedWords: string[]; messages: IMessage[]; + totalSpentKudos: number; // triggerNext: boolean; continueLast: boolean; @@ -36,6 +37,7 @@ interface IActions { setSummarizePrompt: (prompt: string | Event) => void; setBannedWords: (words: string[]) => void; setSummaryEnabled: (summaryEnabled: boolean) => void; + setTotalSpentKudos: Dispatch>; setTriggerNext: (triggerNext: boolean) => void; setContinueLast: (continueLast: boolean) => void; @@ -91,6 +93,7 @@ Make sure to follow the world description and rules exactly. Avoid cliffhangers summaryEnabled: true, bannedWords: [], messages: [], + totalSpentKudos: 0, triggerNext: false, continueLast: false, }; @@ -132,6 +135,7 @@ export const StateContextProvider = ({ children }: { children?: any }) => { const [bannedWords, setBannedWords] = useState(loadedContext.bannedWords); const [messages, setMessages] = useState(loadedContext.messages); const [summaryEnabled, setSummaryEnabled] = useState(loadedContext.summaryEnabled); + const [totalSpentKudos, setTotalSpentKudos] = useState(loadedContext.totalSpentKudos); const connection = availableConnections[currentConnection] ?? DEFAULT_CONTEXT.availableConnections[0]; @@ -164,6 +168,7 @@ export const StateContextProvider = ({ children }: { children?: any }) => { setTriggerNext, setContinueLast, + setTotalSpentKudos, setBannedWords: (words) => setBannedWords(words.slice()), setAvailableConnections: (connections) => setAvailableConnections(connections.slice()), @@ -252,6 +257,7 @@ export const StateContextProvider = ({ children }: { children?: any }) => { summaryEnabled, bannedWords, messages, + totalSpentKudos, // triggerNext, continueLast, diff --git a/src/games/ai-story/tools/connection.ts b/src/games/ai-story/tools/connection.ts index a3a4717..b24acf8 100644 --- a/src/games/ai-story/tools/connection.ts +++ b/src/games/ai-story/tools/connection.ts @@ -51,6 +51,7 @@ interface IHordeResult { faulted: boolean; done: boolean; finished: number; + kudos: number; generations?: { text: string; }[]; @@ -88,7 +89,12 @@ export namespace Connection { let abortController = new AbortController(); - async function* generateKobold(url: string, prompt: string, extraSettings: IGenerationSettings = {}): AsyncGenerator { + export interface TextChunk { + text: string; + cost: number; + } + + async function* generateKobold(url: string, prompt: string, extraSettings: IGenerationSettings = {}): AsyncGenerator { const sse = new SSE(`${url}/api/extra/generate/stream`, { payload: JSON.stringify({ ...DEFAULT_GENERATION_SETTINGS, @@ -130,10 +136,10 @@ export namespace Connection { while (!end || messages.length) { while (messages.length > 0) { - const message = messages.shift(); - if (message != null) { + const text = messages.shift(); + if (text != null) { try { - yield message; + yield { text, cost: 0 }; } catch { } } } @@ -145,7 +151,7 @@ export namespace Connection { sse.close(); } - async function* generateHorde(connection: IHordeConnection, prompt: string, extraSettings: IGenerationSettings = {}): AsyncGenerator { + async function* generateHorde(connection: IHordeConnection, prompt: string, extraSettings: IGenerationSettings = {}): AsyncGenerator { if (!connection.model) { throw new Error('Horde not connected'); } @@ -190,14 +196,14 @@ export namespace Connection { } const { id } = await generateResponse.json() as { id: string }; - const request = async (method = 'GET'): Promise => { + const request = async (method = 'GET'): Promise => { const response = await fetch(`${AIHORDE}/api/v2/generate/text/status/${id}`, { method }); if (response.ok && response.status < 400) { const result: IHordeResult = await response.json(); if (result.generations?.length === 1) { const { text } = result.generations[0]; - return text; + return { text, cost: result.kudos }; } } else { throw new Error(await response.text()); @@ -206,16 +212,17 @@ export namespace Connection { return null; }; - const deleteRequest = async () => (await request('DELETE')) ?? ''; + const deleteRequest = async () => (await request('DELETE')) ?? { text: '', cost: 0 }; let text: string | null = null; while (!text) { try { await delay(2500, { signal }); - text = await request(); + const response = await request(); - if (text) { + if (response?.text) { + text = response.text; for (const sequence of requestData.params.stop_sequence) { const stopIdx = text.indexOf(sequence); if (stopIdx >= 0) { @@ -233,7 +240,7 @@ export namespace Connection { } } - yield unsloppedText; + yield { text: unsloppedText, cost: response.cost }; requestData.prompt += unsloppedText;