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;