1
0
Fork 0

Compare commits

..

2 Commits

8 changed files with 138 additions and 53 deletions

View File

@ -13,12 +13,13 @@ import { Ace } from "../ace";
export const Header = () => { export const Header = () => {
const { modelName, modelTemplate, contextLength, promptTokens, blockConnection } = useContext(LLMContext); const { modelName, modelTemplate, contextLength, promptTokens, blockConnection } = useContext(LLMContext);
const { const {
messages, connectionUrl, systemPrompt, lore, userPrompt, bannedWords, instruct, messages, connectionUrl, systemPrompt, lore, userPrompt, bannedWords, instruct, summarizePrompt,
setConnectionUrl, setSystemPrompt, setLore, setUserPrompt, addSwipe, setBannedWords, setInstruct setConnectionUrl, setSystemPrompt, setLore, setUserPrompt, addSwipe, setBannedWords, setInstruct, setSummarizePrompt,
} = useContext(StateContext); } = useContext(StateContext);
const loreOpen = useBool(); const loreOpen = useBool();
const promptsOpen = useBool(); const promptsOpen = useBool();
const genparamsOpen = useBool();
const assistantOpen = useBool(); const assistantOpen = useBool();
const bannedWordsInput = useMemo(() => bannedWords.join('\n'), [bannedWords]); const bannedWordsInput = useMemo(() => bannedWords.join('\n'), [bannedWords]);
@ -46,7 +47,7 @@ export const Header = () => {
const handleBlurBannedWords = useCallback((e: Event) => { const handleBlurBannedWords = useCallback((e: Event) => {
if (e.target instanceof HTMLTextAreaElement) { if (e.target instanceof HTMLTextAreaElement) {
const words = e.target.value.split('\n').sort(); const words = e.target.value.toLowerCase().split('\n').sort();
setBannedWords(words); setBannedWords(words);
} }
}, [setBannedWords]); }, [setBannedWords]);
@ -83,6 +84,9 @@ export const Header = () => {
<button class='icon color' title='Edit lore' onClick={loreOpen.setTrue}> <button class='icon color' title='Edit lore' onClick={loreOpen.setTrue}>
🌍 🌍
</button> </button>
<button class='icon color' title='Generation parameters' onClick={genparamsOpen.setTrue}>
</button>
<button class='icon color' title='Edit prompts' onClick={promptsOpen.setTrue}> <button class='icon color' title='Edit prompts' onClick={promptsOpen.setTrue}>
📃 📃
</button> </button>
@ -100,6 +104,19 @@ export const Header = () => {
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."
/> />
</Modal> </Modal>
<Modal open={genparamsOpen.value} onClose={genparamsOpen.setFalse}>
<h3 class={styles.modalTitle}>Generation Parameters</h3>
<div className={styles.scrollPane}>
<h4 class={styles.modalTitle}>Banned phrases</h4>
<AutoTextarea
placeholder="Each phrase on separate line"
value={bannedWordsInput}
onInput={handleSetBannedWords}
onBlur={handleBlurBannedWords}
class={styles.template}
/>
</div>
</Modal>
<Modal open={promptsOpen.value} onClose={promptsOpen.setFalse}> <Modal open={promptsOpen.value} onClose={promptsOpen.setFalse}>
<h3 class={styles.modalTitle}>Prompts Editor</h3> <h3 class={styles.modalTitle}>Prompts Editor</h3>
<div className={styles.scrollPane}> <div className={styles.scrollPane}>
@ -109,17 +126,11 @@ export const Header = () => {
<h4 class={styles.modalTitle}>User prompt template</h4> <h4 class={styles.modalTitle}>User prompt template</h4>
<Ace value={userPrompt} onInput={setUserPrompt} /> <Ace value={userPrompt} onInput={setUserPrompt} />
<hr /> <hr />
<h4 class={styles.modalTitle}>Summary template</h4>
<Ace value={summarizePrompt} onInput={setSummarizePrompt} />
<hr />
<h4 class={styles.modalTitle}>Instruct template</h4> <h4 class={styles.modalTitle}>Instruct template</h4>
<Ace value={instruct} onInput={setInstruct} /> <Ace value={instruct} onInput={setInstruct} />
<hr />
<h4 class={styles.modalTitle}>Banned phrases</h4>
<AutoTextarea
placeholder="Each phrase on separate line"
value={bannedWordsInput}
onInput={handleSetBannedWords}
onBlur={handleBlurBannedWords}
class={styles.template}
/>
</div> </div>
</Modal> </Modal>
<MiniChat <MiniChat

View File

@ -27,10 +27,14 @@
border: var(--border); border: var(--border);
min-height: 100px; min-height: 100px;
height: unset; height: unset;
resize: vertical;
line-height: 1.5; line-height: 1.5;
padding: 5px; padding: 5px;
border-radius: var(--border-radius); border-radius: var(--border-radius);
overflow: hidden;
}
.summary {
opacity: 0.7;
} }
.buttons { .buttons {

View File

@ -14,25 +14,31 @@ interface IProps {
} }
export const Message = ({ message, index, isLastUser, isLastAssistant }: IProps) => { export const Message = ({ message, index, isLastUser, isLastAssistant }: IProps) => {
const { editMessage, deleteMessage, setCurrentSwipe, addSwipe } = useContext(StateContext); const { messages, editMessage, editSummary, deleteMessage, setCurrentSwipe, setMessages } = useContext(StateContext);
const [editing, setEditing] = useState(false); const [editing, setEditing] = useState(false);
const [savedMessage, setSavedMessage] = useState(''); const [editedMessage, setEditedMessage] = useState('');
const textRef = useRef<HTMLDivElement>(null); const textRef = useRef<HTMLDivElement>(null);
const content = useMemo(() => MessageTools.getSwipe(message)?.content, [message]); const swipe = useMemo(() => MessageTools.getSwipe(message), [message]);
const content = swipe?.content;
const summary = swipe?.summary;
const htmlContent = useMemo(() => MessageTools.format(content ?? ''), [content]); const htmlContent = useMemo(() => MessageTools.format(content ?? ''), [content]);
const handleToggleEdit = useCallback(() => { const handleEnableEdit = useCallback(() => {
setEditing(!editing); setEditing(true);
if (!editing) { setEditedMessage(content ?? '');
setSavedMessage(content ?? ''); }, [content]);
}
}, [editing, content]); const handleSaveEdit = useCallback(() => {
editMessage(index, editedMessage.trim());
editSummary(index, '');
setEditing(false);
}, [editMessage, editSummary, index, editedMessage]);
const handleCancelEdit = useCallback(() => { const handleCancelEdit = useCallback(() => {
setEditing(false); setEditing(false);
editMessage(index, savedMessage); }, [editMessage, index]);
}, [editMessage, index, savedMessage]);
const handleDeleteMessage = useCallback(() => { const handleDeleteMessage = useCallback(() => {
if (confirm('Delete message?')) { if (confirm('Delete message?')) {
@ -41,12 +47,19 @@ export const Message = ({ message, index, isLastUser, isLastAssistant }: IProps)
} }
}, [deleteMessage, index]); }, [deleteMessage, index]);
const handleStopHere = useCallback(() => {
if (confirm('Delete all messages after that?')) {
setMessages(messages.filter((_, i) => i <= index));
setEditing(false);
}
}, [messages, setMessages, index]);
const handleEdit = useCallback((e: InputEvent) => { const handleEdit = useCallback((e: InputEvent) => {
if (e.target instanceof HTMLTextAreaElement) { if (e.target instanceof HTMLTextAreaElement) {
const newContent = e.target.value; const newContent = e.target.value;
editMessage(index, newContent); setEditedMessage(newContent);
} }
}, [editMessage, index]); }, []);
const handleSwipeLeft = useCallback(() => { const handleSwipeLeft = useCallback(() => {
setCurrentSwipe(index, message.currentSwipe - 1); setCurrentSwipe(index, message.currentSwipe - 1);
@ -62,16 +75,20 @@ export const Message = ({ message, index, isLastUser, isLastAssistant }: IProps)
<div class={`${styles.message} ${styles[message.role]} ${isLastUser ? styles.lastUser : ''}`}> <div class={`${styles.message} ${styles[message.role]} ${isLastUser ? styles.lastUser : ''}`}>
<div class={styles.content}> <div class={styles.content}>
{editing {editing
? <AutoTextarea onInput={handleEdit} value={content} /> ? <AutoTextarea onInput={handleEdit} value={editedMessage} />
: <div class={styles.text} dangerouslySetInnerHTML={{ __html: htmlContent }} ref={textRef} /> : <>
<div class={styles.text} dangerouslySetInnerHTML={{ __html: htmlContent }} ref={textRef} />
{summary && <small class={styles.summary}>{summary}</small>}
</>
} }
{(isLastUser || message.role === 'assistant') && {(isLastUser || message.role === 'assistant') &&
<div class={styles.buttons}> <div class={styles.buttons}>
{editing {editing
? <> ? <>
<button class='icon' onClick={handleToggleEdit}></button> <button class='icon' onClick={handleSaveEdit} title='Save'></button>
<button class='icon' onClick={handleDeleteMessage}>🗑</button> <button class='icon' onClick={handleDeleteMessage} title='Delete'>🗑</button>
<button class='icon' onClick={handleCancelEdit}></button> <button class='icon' onClick={handleStopHere} title='Stop here'></button>
<button class='icon' onClick={handleCancelEdit} title='Cancel'></button>
</> </>
: <> : <>
{isLastAssistant && {isLastAssistant &&
@ -81,7 +98,7 @@ export const Message = ({ message, index, isLastUser, isLastAssistant }: IProps)
<div onClick={handleSwipeRight}></div> <div onClick={handleSwipeRight}></div>
</div> </div>
} }
<button class='icon' onClick={handleToggleEdit} title="Edit">🖊</button> <button class='icon' onClick={handleEnableEdit} title="Edit">🖊</button>
</> </>
} }
</div> </div>

View File

@ -58,11 +58,11 @@ export const MiniChat = ({ history = [], buttons = {}, open, onClose }: IProps)
for await (const chunk of generate(prompt)) { for await (const chunk of generate(prompt)) {
text += chunk; text += chunk;
setMessages(MessageTools.updateContent(newMessages, messageId, text)); setMessages(MessageTools.updateSwipe(newMessages, messageId, text));
} }
setMessages([ setMessages([
...MessageTools.updateContent(newMessages, messageId, MessageTools.trimSentence(text)), ...MessageTools.updateSwipe(newMessages, messageId, MessageTools.trimSentence(text)),
MessageTools.create('', 'user', true), MessageTools.create('', 'user', true),
]); ]);
MessageTools.playReady(); MessageTools.playReady();
@ -78,7 +78,7 @@ export const MiniChat = ({ history = [], buttons = {}, open, onClose }: IProps)
const handleChange = useCallback((i: number, e: InputEvent) => { const handleChange = useCallback((i: number, e: InputEvent) => {
if (e.target instanceof HTMLTextAreaElement) { if (e.target instanceof HTMLTextAreaElement) {
setMessages(MessageTools.updateContent(messages, i, e.target.value)); setMessages(MessageTools.updateSwipe(messages, i, e.target.value));
} }
}, [messages]); }, [messages]);

View File

@ -10,6 +10,7 @@ import { Huggingface } from "../huggingface";
interface ICompileArgs { interface ICompileArgs {
keepUsers?: number; keepUsers?: number;
raw?: boolean;
} }
interface ICompiledPrompt { interface ICompiledPrompt {
@ -47,6 +48,7 @@ type IGenerationSettings = Partial<typeof DEFAULT_GENERATION_SETTINGS>;
interface IActions { interface IActions {
compilePrompt: (messages: IMessage[], args?: ICompileArgs) => Promise<ICompiledPrompt>; compilePrompt: (messages: IMessage[], args?: ICompileArgs) => Promise<ICompiledPrompt>;
generate: (prompt: string, extraSettings?: IGenerationSettings) => AsyncGenerator<string>; generate: (prompt: string, extraSettings?: IGenerationSettings) => AsyncGenerator<string>;
summarize: (content: string) => Promise<string>;
countTokens: (prompt: string) => Promise<number>; countTokens: (prompt: string) => Promise<number>;
} }
export type ILLMContext = IContext & IActions; export type ILLMContext = IContext & IActions;
@ -81,11 +83,12 @@ export const LLMContext = createContext<ILLMContext>({} as ILLMContext);
export const LLMContextProvider = ({ children }: { children?: any }) => { export const LLMContextProvider = ({ children }: { children?: any }) => {
const { const {
connectionUrl, messages, triggerNext, lore, userPrompt, systemPrompt, bannedWords, instruct, connectionUrl, messages, triggerNext, lore, userPrompt, systemPrompt, bannedWords, instruct, summarizePrompt,
setTriggerNext, addMessage, editMessage, setInstruct, setTriggerNext, addMessage, editMessage, editSummary, setInstruct,
} = useContext(StateContext); } = useContext(StateContext);
const generating = useBool(false); const generating = useBool(false);
const summarizing = useBool(false);
const blockConnection = useBool(false); const blockConnection = useBool(false);
const [promptTokens, setPromptTokens] = useState(0); const [promptTokens, setPromptTokens] = useState(0);
const [contextLength, setContextLength] = useState(0); const [contextLength, setContextLength] = useState(0);
@ -285,6 +288,14 @@ export const LLMContextProvider = ({ children }: { children?: any }) => {
generating.setFalse(); generating.setFalse();
} }
}, },
summarize: async (message) => {
const content = Huggingface.applyTemplate(summarizePrompt, { message });
const prompt = Huggingface.applyChatTemplate(instruct, [{ role: 'user', content }]);
const tokens = await Array.fromAsync(actions.generate(prompt));
return MessageTools.trimSentence(tokens.join(''));
},
countTokens: async (prompt) => { countTokens: async (prompt) => {
if (!connectionUrl) { if (!connectionUrl) {
return 0; return 0;
@ -305,7 +316,7 @@ export const LLMContextProvider = ({ children }: { children?: any }) => {
return 0; return 0;
}, },
}), [connectionUrl, lore, userPromptTemplate, systemPrompt, bannedWords, instruct]); }), [connectionUrl, lore, userPromptTemplate, systemPrompt, bannedWords, instruct, summarizePrompt]);
useEffect(() => void (async () => { useEffect(() => void (async () => {
if (triggerNext && !generating.value) { if (triggerNext && !generating.value) {
@ -323,14 +334,16 @@ export const LLMContextProvider = ({ children }: { children?: any }) => {
messageId++; messageId++;
} }
editSummary(messageId, 'Generating...');
for await (const chunk of actions.generate(prompt)) { for await (const chunk of actions.generate(prompt)) {
text += chunk; text += chunk;
setPromptTokens(tokens + Math.round(text.length * 0.25)); setPromptTokens(tokens + Math.round(text.length * 0.25));
editMessage(messageId, text); editMessage(messageId, text.trim());
} }
text = MessageTools.trimSentence(text); text = MessageTools.trimSentence(text);
editMessage(messageId, text); editMessage(messageId, text);
editSummary(messageId, '');
setPromptTokens(0); // trigger calculation setPromptTokens(0); // trigger calculation
@ -338,6 +351,21 @@ export const LLMContextProvider = ({ children }: { children?: any }) => {
} }
})(), [actions, triggerNext, messages, generating.value]); })(), [actions, triggerNext, messages, generating.value]);
useEffect(() => void (async () => {
if (!generating.value && !summarizing.value) {
summarizing.setTrue();
for (let id = 0; id < messages.length; id++) {
const message = messages[id];
const swipe = MessageTools.getSwipe(message);
if (message.role === 'assistant' && swipe?.content?.includes('\n') && !swipe.summary) {
const summary = await actions.summarize(swipe.content);
editSummary(id, summary);
}
}
summarizing.setFalse();
}
})(), [messages, generating.value, summarizing.value]);
useEffect(() => { useEffect(() => {
if (!blockConnection.value) { if (!blockConnection.value) {
setPromptTokens(0); setPromptTokens(0);

View File

@ -10,6 +10,7 @@ interface IContext {
systemPrompt: string; systemPrompt: string;
lore: string; lore: string;
userPrompt: string; userPrompt: string;
summarizePrompt: string;
bannedWords: string[]; bannedWords: string[];
messages: IMessage[]; messages: IMessage[];
triggerNext: boolean; triggerNext: boolean;
@ -22,12 +23,14 @@ interface IActions {
setLore: (lore: string | Event) => void; setLore: (lore: string | Event) => void;
setSystemPrompt: (prompt: string | Event) => void; setSystemPrompt: (prompt: string | Event) => void;
setUserPrompt: (prompt: string | Event) => void; setUserPrompt: (prompt: string | Event) => void;
setSummarizePrompt: (prompt: string | Event) => void;
setBannedWords: (words: string[]) => void; setBannedWords: (words: string[]) => void;
setTriggerNext: (triggerNext: boolean) => void; setTriggerNext: (triggerNext: boolean) => void;
setMessages: (messages: IMessage[]) => void; setMessages: (messages: IMessage[]) => void;
addMessage: (content: string, role: IMessage['role'], triggerNext?: boolean) => void; addMessage: (content: string, role: IMessage['role'], triggerNext?: boolean) => void;
editMessage: (index: number, content: string, triggerNext?: boolean) => void; editMessage: (index: number, content: string) => void;
editSummary: (index: number, summary: string) => void;
deleteMessage: (index: number) => void; deleteMessage: (index: number) => void;
setCurrentSwipe: (index: number, swipe: number) => void; setCurrentSwipe: (index: number, swipe: number) => void;
addSwipe: (index: number, content: string) => void; addSwipe: (index: number, content: string) => void;
@ -63,6 +66,7 @@ export const loadContext = (): IContext => {
lore: '', lore: '',
userPrompt: `{% if prompt %}{% if isStart %}Start{% else %}Continue{% endif %} this story, taking information into account: {{ prompt | trim }} userPrompt: `{% if prompt %}{% if isStart %}Start{% else %}Continue{% endif %} this story, taking information into account: {{ prompt | trim }}
Remember that this story should be infinite and go forever. Avoid cliffhangers and pauses, be creative.{% elif isStart %}Write a novel using information above as a reference. Make sure to follow the lore exactly and avoid cliffhangers.{% else %}Continue the story forward. Avoid cliffhangers and pauses.{% endif %}`, Remember that this story should be infinite and go forever. Avoid cliffhangers and pauses, be creative.{% elif isStart %}Write a novel using information above as a reference. Make sure to follow the lore exactly and avoid cliffhangers.{% else %}Continue the story forward. Avoid cliffhangers and pauses.{% endif %}`,
summarizePrompt: 'Make the following text shorter, keeping all important details:\n\n{{ message }}\n\nYour answer should only contain the shortened text.',
bannedWords: [], bannedWords: [],
messages: [], messages: [],
triggerNext: false, triggerNext: false,
@ -92,6 +96,7 @@ export const StateContextProvider = ({ children }: { children?: any }) => {
const [lore, setLore] = useInputState(loadedContext.lore); const [lore, setLore] = useInputState(loadedContext.lore);
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 [bannedWords, setBannedWords] = useState<string[]>(loadedContext.bannedWords); const [bannedWords, setBannedWords] = useState<string[]>(loadedContext.bannedWords);
const [messages, setMessages] = useState(loadedContext.messages); const [messages, setMessages] = useState(loadedContext.messages);
@ -103,6 +108,7 @@ export const StateContextProvider = ({ children }: { children?: any }) => {
setInstruct, setInstruct,
setSystemPrompt, setSystemPrompt,
setUserPrompt, setUserPrompt,
setSummarizePrompt,
setLore, setLore,
setTriggerNext, setTriggerNext,
setBannedWords: (words) => setBannedWords([...words]), setBannedWords: (words) => setBannedWords([...words]),
@ -115,9 +121,11 @@ export const StateContextProvider = ({ children }: { children?: any }) => {
]); ]);
setTriggerNext(triggerNext); setTriggerNext(triggerNext);
}, },
editMessage: (index, content, triggerNext = false) => { editMessage: (index, content) => {
setMessages(messages => MessageTools.updateContent(messages, index, content)); setMessages(messages => MessageTools.updateSwipe(messages, index, { content }));
setTriggerNext(triggerNext); },
editSummary: (index, summary) => {
setMessages(messages => MessageTools.updateSwipe(messages, index, { summary }));
}, },
deleteMessage: (index) => setMessages(messages => deleteMessage: (index) => setMessages(messages =>
messages.filter((_, i) => i !== index) messages.filter((_, i) => i !== index)
@ -181,6 +189,7 @@ export const StateContextProvider = ({ children }: { children?: any }) => {
systemPrompt, systemPrompt,
lore, lore,
userPrompt, userPrompt,
summarizePrompt,
bannedWords, bannedWords,
messages, messages,
triggerNext, triggerNext,

View File

@ -79,6 +79,7 @@ export namespace Huggingface {
}; };
const templateCache: Record<string, string> = loadCache(); const templateCache: Record<string, string> = loadCache();
const compiledTemplates = new Map<string, Template>();
const hasField = <T extends string>(obj: unknown, field: T): obj is Record<T, unknown> => ( const hasField = <T extends string>(obj: unknown, field: T): obj is Record<T, unknown> => (
obj != null && typeof obj === 'object' && (field in obj) obj != null && typeof obj === 'object' && (field in obj)
@ -256,15 +257,29 @@ export namespace Huggingface {
return template; return template;
} }
export const applyChatTemplate = (templateString: string, messages: ITemplateMessage[], functions?: IFunction[]) => { export const applyChatTemplate = (templateString: string, messages: ITemplateMessage[], functions?: IFunction[]) => (
const template = new Template(templateString); applyTemplate(templateString, {
const prompt = template.render({
messages, messages,
add_generation_prompt: true, add_generation_prompt: true,
tools: functions?.map(convertFunctionToTool), tools: functions?.map(convertFunctionToTool),
}); })
);
return prompt; export const applyTemplate = (templateString: string, args: Record<string, any>): string => {
}; try {
let template = compiledTemplates.get(templateString);
if (!template) {
template = new Template(templateString);
compiledTemplates.set(templateString, template);
}
const result = template.render(args);
return result;
} catch (e) {
console.error('[applyTemplate] error:', e);
}
return '';
}
} }

View File

@ -3,6 +3,7 @@ import messageSound from './assets/message.mp3';
export interface ISwipe { export interface ISwipe {
content: string; content: string;
summary?: string;
} }
export interface IMessage { export interface IMessage {
@ -94,15 +95,15 @@ export namespace MessageTools {
if (latestEnd > 0) { if (latestEnd > 0) {
text = text.slice(0, latestEnd + 1); text = text.slice(0, latestEnd + 1);
} }
return text.trimEnd(); return text.trim();
} }
export const updateContent = (messages: IMessage[], index: number, content: string) => ( export const updateSwipe = (messages: IMessage[], index: number, update: Partial<ISwipe>) => (
messages.map( messages.map(
(m, i) => ({ (m, i) => ({
...m, ...m,
swipes: i === index swipes: i === index
? m.swipes.map((s, si) => (si === m.currentSwipe ? { content } : s)) ? m.swipes.map((s, si) => (si === m.currentSwipe ? { ...s, ...update } : s))
: m.swipes : m.swipes
}) })
) )