Continue mode for chat
This commit is contained in:
parent
7148254b35
commit
eaa79c6c49
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue