1
0
Fork 0

Continue mode for chat

This commit is contained in:
Pabloader 2026-04-10 16:01:21 +00:00
parent 7148254b35
commit eaa79c6c49
4 changed files with 85 additions and 68 deletions

View File

@ -43,18 +43,6 @@
.messageActions { .messageActions {
display: flex; display: flex;
gap: 4px; gap: 4px;
opacity: 0;
transition: opacity 0.15s ease;
}
.message:hover .messageActions {
opacity: 1;
}
@media (max-width: 1000px) {
.messageActions {
opacity: 1;
}
} }
.iconButton { .iconButton {

View File

@ -115,17 +115,17 @@ export const ChatPanel = ({ visible }: { visible: boolean }) => {
const countTokens = async () => { const countTokens = async () => {
try { try {
const messages: ChatMessage[] = []; const newMessages: ChatMessage[] = [];
if (input.trim()) { if (input.trim()) {
messages.push({ newMessages.push({
id: crypto.randomUUID(), id: crypto.randomUUID(),
role: 'user', role: 'user',
content: input.trim(), content: input.trim(),
}); });
} }
const chatRequest = Prompt.compilePrompt(appStateRef.current, messages); const chatRequest = Prompt.compilePrompt(appStateRef.current, { newMessages });
const countRequest: LLM.CountTokensRequest = { const countRequest: LLM.CountTokensRequest = {
model: model.id, model: model.id,
input: chatRequest?.messages ?? [], input: chatRequest?.messages ?? [],
@ -148,14 +148,14 @@ export const ChatPanel = ({ visible }: { visible: boolean }) => {
return () => clearTimeout(timeoutId); return () => clearTimeout(timeoutId);
}, [currentStory, connection, model, input, currentStory?.chatMessages.length]); }, [currentStory, connection, model, input, currentStory?.chatMessages.length]);
const sendMessage = useCallback(async ( const sendMessage = useCallback(async (config: Prompt.CompileConfig = {}) => {
newMessages: Iterable<ChatMessage>,
excludedMessageIds: string[] = [],
) => {
if (!currentStory || !currentWorld || !connection || !model) return; if (!currentStory || !currentWorld || !connection || !model) return;
const { newMessages = [], excludedMessageIds = [] } = config;
const excludedSet = new Set(excludedMessageIds);
for (const message of newMessages) { for (const message of newMessages) {
if (excludedMessageIds.includes(message.id)) continue; if (excludedSet.has(message.id)) continue;
dispatch({ dispatch({
type: 'ADD_CHAT_MESSAGE', type: 'ADD_CHAT_MESSAGE',
worldId: currentWorld.id, worldId: currentWorld.id,
@ -164,20 +164,25 @@ export const ChatPanel = ({ visible }: { visible: boolean }) => {
}); });
} }
const assistantMessageId = crypto.randomUUID(); const continuedMessage = config.continueLast ? currentStory.chatMessages.at(-1) : null;
dispatch({ const targetMessageId = continuedMessage?.id ?? crypto.randomUUID();
type: 'ADD_CHAT_MESSAGE', const targetRole = continuedMessage?.role ?? 'assistant';
worldId: currentWorld.id,
storyId: currentStory.id,
message: {
id: assistantMessageId,
role: 'assistant',
content: '',
reasoning_content: 'Generating...',
},
});
const request = Prompt.compilePrompt(appStateRef.current, newMessages, excludedMessageIds); if (!continuedMessage) {
dispatch({
type: 'ADD_CHAT_MESSAGE',
worldId: currentWorld.id,
storyId: currentStory.id,
message: {
id: targetMessageId,
role: 'assistant',
content: '',
reasoning_content: 'Generating...',
},
});
}
const request = Prompt.compilePrompt(appStateRef.current, config);
if (!request) { if (!request) {
setError('Failed to compile prompt'); setError('Failed to compile prompt');
@ -188,8 +193,8 @@ export const ChatPanel = ({ visible }: { visible: boolean }) => {
try { try {
const charName = currentWorld.title ?? 'Assistant'; const charName = currentWorld.title ?? 'Assistant';
const prefix = `${charName}: `; const prefix = `${charName}: `;
let accumulatedContent = ''; let accumulatedContent = continuedMessage?.content ?? '';
let accumulatedReasoning = ''; let accumulatedReasoning = continuedMessage?.role === 'assistant' ? continuedMessage.reasoning_content ?? '' : '';
let tool_calls: LLM.ToolCall[] | undefined; let tool_calls: LLM.ToolCall[] | undefined;
for await (const chunk of LLM.generateStream(connection, request)) { for await (const chunk of LLM.generateStream(connection, request)) {
@ -219,8 +224,8 @@ export const ChatPanel = ({ visible }: { visible: boolean }) => {
worldId: currentWorld.id, worldId: currentWorld.id,
storyId: currentStory.id, storyId: currentStory.id,
message: { message: {
id: assistantMessageId, id: targetMessageId,
role: 'assistant', role: targetRole,
content: accumulatedContent, content: accumulatedContent,
reasoning_content: accumulatedReasoning, reasoning_content: accumulatedReasoning,
tool_calls, tool_calls,
@ -228,9 +233,9 @@ export const ChatPanel = ({ visible }: { visible: boolean }) => {
}); });
} }
} }
const assistantMessage: ChatMessage = { const finalMessage: ChatMessage = {
id: assistantMessageId, id: targetMessageId,
role: 'assistant', role: targetRole,
content: accumulatedContent, content: accumulatedContent,
reasoning_content: accumulatedReasoning, reasoning_content: accumulatedReasoning,
tool_calls, tool_calls,
@ -239,7 +244,7 @@ export const ChatPanel = ({ visible }: { visible: boolean }) => {
type: 'ADD_CHAT_MESSAGE', type: 'ADD_CHAT_MESSAGE',
worldId: currentWorld.id, worldId: currentWorld.id,
storyId: currentStory.id, storyId: currentStory.id,
message: assistantMessage, message: finalMessage,
}); });
if (tool_calls) { if (tool_calls) {
@ -265,7 +270,13 @@ export const ChatPanel = ({ visible }: { visible: boolean }) => {
} }
if (!abortControllerRef.current?.signal.aborted) { if (!abortControllerRef.current?.signal.aborted) {
return sendMessage([...newMessages, assistantMessage, ...toolMessages]); return sendMessage({
newMessages: [
...newMessages,
finalMessage,
...toolMessages,
]
});
} }
} }
} catch (err) { } catch (err) {
@ -288,7 +299,7 @@ export const ChatPanel = ({ visible }: { visible: boolean }) => {
setError(null); setError(null);
abortControllerRef.current = new AbortController(); abortControllerRef.current = new AbortController();
const excludedMessages: string[] = []; const excludedMessageIds = new Set<string>();
try { try {
if (isAssistant) { if (isAssistant) {
// Delete the last assistant message and regenerate // Delete the last assistant message and regenerate
@ -298,9 +309,9 @@ export const ChatPanel = ({ visible }: { visible: boolean }) => {
storyId: currentStory.id, storyId: currentStory.id,
messageId: lastMessage.id, messageId: lastMessage.id,
}); });
excludedMessages.push(lastMessage.id); excludedMessageIds.add(lastMessage.id);
} }
await sendMessage([], excludedMessages); await sendMessage({ excludedMessageIds });
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
@ -322,11 +333,13 @@ export const ChatPanel = ({ visible }: { visible: boolean }) => {
abortControllerRef.current = new AbortController(); abortControllerRef.current = new AbortController();
try { try {
await sendMessage([{ await sendMessage({
id: crypto.randomUUID(), newMessages: [{
role: 'user' as const, id: crypto.randomUUID(),
content: input.trim(), role: 'user' as const,
}]); content: input.trim(),
}]
});
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
@ -341,15 +354,21 @@ export const ChatPanel = ({ visible }: { visible: boolean }) => {
abortControllerRef.current = new AbortController(); abortControllerRef.current = new AbortController();
try { try {
await sendMessage([{ if (currentWorld?.chatOnly) {
id: crypto.randomUUID(), await sendMessage({ continueLast: true });
role: 'user' as const, } else {
content: (continuePrompt + '\n\n' + input).trim(), await sendMessage({
}]); newMessages: [{
id: crypto.randomUUID(),
role: 'user' as const,
content: (continuePrompt + '\n\n' + input).trim(),
}]
});
}
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}, [currentStory, input, connection, model, isLoading, sendMessage]); }, [currentStory, currentWorld, input, connection, model, isLoading, sendMessage]);
const handleStopGeneration = useCallback(() => { const handleStopGeneration = useCallback(() => {
abortControllerRef.current?.abort(); abortControllerRef.current?.abort();
@ -599,16 +618,14 @@ export const ChatPanel = ({ visible }: { visible: boolean }) => {
<Sparkles size={14} /> <Sparkles size={14} />
</button> </button>
)} )}
{!currentWorld?.chatOnly && ( <button
<button class={styles.actionButton}
class={styles.actionButton} onClick={handleContinue}
onClick={handleContinue} disabled={isDisabled}
disabled={isDisabled} title="Continue"
title="Continue" >
> <ChevronsRight size={14} />
<ChevronsRight size={14} /> </button>
</button>
)}
<button <button
class={styles.actionButton} class={styles.actionButton}
onClick={handleRegenerate} onClick={handleRegenerate}

View File

@ -72,6 +72,7 @@ namespace LLM {
max_tokens?: number; max_tokens?: number;
}; };
add_generation_prompt?: boolean; add_generation_prompt?: boolean;
remove_last_eos?: boolean;
} }
export interface ChatCompletionChoice { export interface ChatCompletionChoice {

View File

@ -346,12 +346,22 @@ namespace Prompt {
return parts.join('\n\n'); return parts.join('\n\n');
} }
export interface CompileConfig {
newMessages?: Iterable<ChatMessage>;
excludedMessageIds?: Iterable<string>;
continueLast?: boolean;
}
export function compilePrompt( export function compilePrompt(
state: AppState, state: AppState,
newMessages: Iterable<ChatMessage> = [], config: CompileConfig = {},
excludedMessageIds: Iterable<string> = [],
): LLM.ChatCompletionRequest | null { ): LLM.ChatCompletionRequest | null {
const { currentStory, model, enableThinking, currentWorld } = state; const { currentStory, model, enableThinking, currentWorld } = state;
const {
newMessages = [],
excludedMessageIds = [],
continueLast = false,
} = config;
if (!currentStory || !model) { if (!currentStory || !model) {
return null; return null;
@ -397,6 +407,7 @@ namespace Prompt {
messages: applyVars(formattedMessages), messages: applyVars(formattedMessages),
max_tokens: model.top_provider.max_completion_tokens || 2048, max_tokens: model.top_provider.max_completion_tokens || 2048,
banned_tokens: state.bannedTokens, banned_tokens: state.bannedTokens,
...(continueLast && { remove_last_eos: true }),
}; };
} }