|
|
import { DatabaseStore } from '$lib/stores/database'; |
|
|
import { chatService, slotsService } from '$lib/services'; |
|
|
import { serverStore } from '$lib/stores/server.svelte'; |
|
|
import { config } from '$lib/stores/settings.svelte'; |
|
|
import { filterByLeafNodeId, findLeafNode, findDescendantMessages } from '$lib/utils/branching'; |
|
|
import { browser } from '$app/environment'; |
|
|
import { goto } from '$app/navigation'; |
|
|
import { toast } from 'svelte-sonner'; |
|
|
import type { ExportedConversations } from '$lib/types/database'; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ChatStore { |
|
|
activeConversation = $state<DatabaseConversation | null>(null); |
|
|
activeMessages = $state<DatabaseMessage[]>([]); |
|
|
conversations = $state<DatabaseConversation[]>([]); |
|
|
currentResponse = $state(''); |
|
|
errorDialogState = $state<{ type: 'timeout' | 'server'; message: string } | null>(null); |
|
|
isInitialized = $state(false); |
|
|
isLoading = $state(false); |
|
|
titleUpdateConfirmationCallback?: (currentTitle: string, newTitle: string) => Promise<boolean>; |
|
|
|
|
|
constructor() { |
|
|
if (browser) { |
|
|
this.initialize(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async initialize(): Promise<void> { |
|
|
try { |
|
|
await this.loadConversations(); |
|
|
|
|
|
this.isInitialized = true; |
|
|
} catch (error) { |
|
|
console.error('Failed to initialize chat store:', error); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async loadConversations(): Promise<void> { |
|
|
this.conversations = await DatabaseStore.getAllConversations(); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async createConversation(name?: string): Promise<string> { |
|
|
const conversationName = name || `Chat ${new Date().toLocaleString()}`; |
|
|
const conversation = await DatabaseStore.createConversation(conversationName); |
|
|
|
|
|
this.conversations.unshift(conversation); |
|
|
|
|
|
this.activeConversation = conversation; |
|
|
this.activeMessages = []; |
|
|
|
|
|
await goto(`#/chat/${conversation.id}`); |
|
|
|
|
|
return conversation.id; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async loadConversation(convId: string): Promise<boolean> { |
|
|
try { |
|
|
const conversation = await DatabaseStore.getConversation(convId); |
|
|
|
|
|
if (!conversation) { |
|
|
return false; |
|
|
} |
|
|
|
|
|
this.activeConversation = conversation; |
|
|
|
|
|
if (conversation.currNode) { |
|
|
const allMessages = await DatabaseStore.getConversationMessages(convId); |
|
|
this.activeMessages = filterByLeafNodeId( |
|
|
allMessages, |
|
|
conversation.currNode, |
|
|
false |
|
|
) as DatabaseMessage[]; |
|
|
} else { |
|
|
|
|
|
this.activeMessages = await DatabaseStore.getConversationMessages(convId); |
|
|
} |
|
|
|
|
|
return true; |
|
|
} catch (error) { |
|
|
console.error('Failed to load conversation:', error); |
|
|
|
|
|
return false; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async addMessage( |
|
|
role: ChatRole, |
|
|
content: string, |
|
|
type: ChatMessageType = 'text', |
|
|
parent: string = '-1', |
|
|
extras?: DatabaseMessageExtra[] |
|
|
): Promise<DatabaseMessage | null> { |
|
|
if (!this.activeConversation) { |
|
|
console.error('No active conversation when trying to add message'); |
|
|
return null; |
|
|
} |
|
|
|
|
|
try { |
|
|
let parentId: string | null = null; |
|
|
|
|
|
if (parent === '-1') { |
|
|
if (this.activeMessages.length > 0) { |
|
|
parentId = this.activeMessages[this.activeMessages.length - 1].id; |
|
|
} else { |
|
|
const allMessages = await DatabaseStore.getConversationMessages( |
|
|
this.activeConversation.id |
|
|
); |
|
|
const rootMessage = allMessages.find((m) => m.parent === null && m.type === 'root'); |
|
|
|
|
|
if (!rootMessage) { |
|
|
const rootId = await DatabaseStore.createRootMessage(this.activeConversation.id); |
|
|
parentId = rootId; |
|
|
} else { |
|
|
parentId = rootMessage.id; |
|
|
} |
|
|
} |
|
|
} else { |
|
|
parentId = parent; |
|
|
} |
|
|
|
|
|
const message = await DatabaseStore.createMessageBranch( |
|
|
{ |
|
|
convId: this.activeConversation.id, |
|
|
role, |
|
|
content, |
|
|
type, |
|
|
timestamp: Date.now(), |
|
|
thinking: '', |
|
|
children: [], |
|
|
extra: extras |
|
|
}, |
|
|
parentId |
|
|
); |
|
|
|
|
|
this.activeMessages.push(message); |
|
|
|
|
|
await DatabaseStore.updateCurrentNode(this.activeConversation.id, message.id); |
|
|
this.activeConversation.currNode = message.id; |
|
|
|
|
|
this.updateConversationTimestamp(); |
|
|
|
|
|
return message; |
|
|
} catch (error) { |
|
|
console.error('Failed to add message:', error); |
|
|
return null; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private getApiOptions(): Record<string, unknown> { |
|
|
const currentConfig = config(); |
|
|
const hasValue = (value: unknown): boolean => |
|
|
value !== undefined && value !== null && value !== ''; |
|
|
|
|
|
const apiOptions: Record<string, unknown> = { |
|
|
stream: true, |
|
|
timings_per_token: true |
|
|
}; |
|
|
|
|
|
if (hasValue(currentConfig.temperature)) { |
|
|
apiOptions.temperature = Number(currentConfig.temperature); |
|
|
} |
|
|
if (hasValue(currentConfig.max_tokens)) { |
|
|
apiOptions.max_tokens = Number(currentConfig.max_tokens); |
|
|
} |
|
|
if (hasValue(currentConfig.dynatemp_range)) { |
|
|
apiOptions.dynatemp_range = Number(currentConfig.dynatemp_range); |
|
|
} |
|
|
if (hasValue(currentConfig.dynatemp_exponent)) { |
|
|
apiOptions.dynatemp_exponent = Number(currentConfig.dynatemp_exponent); |
|
|
} |
|
|
if (hasValue(currentConfig.top_k)) { |
|
|
apiOptions.top_k = Number(currentConfig.top_k); |
|
|
} |
|
|
if (hasValue(currentConfig.top_p)) { |
|
|
apiOptions.top_p = Number(currentConfig.top_p); |
|
|
} |
|
|
if (hasValue(currentConfig.min_p)) { |
|
|
apiOptions.min_p = Number(currentConfig.min_p); |
|
|
} |
|
|
if (hasValue(currentConfig.xtc_probability)) { |
|
|
apiOptions.xtc_probability = Number(currentConfig.xtc_probability); |
|
|
} |
|
|
if (hasValue(currentConfig.xtc_threshold)) { |
|
|
apiOptions.xtc_threshold = Number(currentConfig.xtc_threshold); |
|
|
} |
|
|
if (hasValue(currentConfig.typ_p)) { |
|
|
apiOptions.typ_p = Number(currentConfig.typ_p); |
|
|
} |
|
|
if (hasValue(currentConfig.repeat_last_n)) { |
|
|
apiOptions.repeat_last_n = Number(currentConfig.repeat_last_n); |
|
|
} |
|
|
if (hasValue(currentConfig.repeat_penalty)) { |
|
|
apiOptions.repeat_penalty = Number(currentConfig.repeat_penalty); |
|
|
} |
|
|
if (hasValue(currentConfig.presence_penalty)) { |
|
|
apiOptions.presence_penalty = Number(currentConfig.presence_penalty); |
|
|
} |
|
|
if (hasValue(currentConfig.frequency_penalty)) { |
|
|
apiOptions.frequency_penalty = Number(currentConfig.frequency_penalty); |
|
|
} |
|
|
if (hasValue(currentConfig.dry_multiplier)) { |
|
|
apiOptions.dry_multiplier = Number(currentConfig.dry_multiplier); |
|
|
} |
|
|
if (hasValue(currentConfig.dry_base)) { |
|
|
apiOptions.dry_base = Number(currentConfig.dry_base); |
|
|
} |
|
|
if (hasValue(currentConfig.dry_allowed_length)) { |
|
|
apiOptions.dry_allowed_length = Number(currentConfig.dry_allowed_length); |
|
|
} |
|
|
if (hasValue(currentConfig.dry_penalty_last_n)) { |
|
|
apiOptions.dry_penalty_last_n = Number(currentConfig.dry_penalty_last_n); |
|
|
} |
|
|
if (currentConfig.samplers) { |
|
|
apiOptions.samplers = currentConfig.samplers; |
|
|
} |
|
|
if (currentConfig.custom) { |
|
|
apiOptions.custom = currentConfig.custom; |
|
|
} |
|
|
|
|
|
return apiOptions; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private async streamChatCompletion( |
|
|
allMessages: DatabaseMessage[], |
|
|
assistantMessage: DatabaseMessage, |
|
|
onComplete?: (content: string) => Promise<void>, |
|
|
onError?: (error: Error) => void |
|
|
): Promise<void> { |
|
|
let streamedContent = ''; |
|
|
let streamedReasoningContent = ''; |
|
|
let modelCaptured = false; |
|
|
|
|
|
const captureModelIfNeeded = (updateDbImmediately = true): string | undefined => { |
|
|
if (!modelCaptured) { |
|
|
const currentModelName = serverStore.modelName; |
|
|
|
|
|
if (currentModelName) { |
|
|
if (updateDbImmediately) { |
|
|
DatabaseStore.updateMessage(assistantMessage.id, { model: currentModelName }).catch( |
|
|
console.error |
|
|
); |
|
|
} |
|
|
|
|
|
const messageIndex = this.findMessageIndex(assistantMessage.id); |
|
|
|
|
|
this.updateMessageAtIndex(messageIndex, { model: currentModelName }); |
|
|
modelCaptured = true; |
|
|
|
|
|
return currentModelName; |
|
|
} |
|
|
} |
|
|
return undefined; |
|
|
}; |
|
|
|
|
|
slotsService.startStreaming(); |
|
|
|
|
|
await chatService.sendMessage(allMessages, { |
|
|
...this.getApiOptions(), |
|
|
|
|
|
onChunk: (chunk: string) => { |
|
|
streamedContent += chunk; |
|
|
this.currentResponse = streamedContent; |
|
|
|
|
|
captureModelIfNeeded(); |
|
|
const messageIndex = this.findMessageIndex(assistantMessage.id); |
|
|
this.updateMessageAtIndex(messageIndex, { |
|
|
content: streamedContent |
|
|
}); |
|
|
}, |
|
|
|
|
|
onReasoningChunk: (reasoningChunk: string) => { |
|
|
streamedReasoningContent += reasoningChunk; |
|
|
|
|
|
captureModelIfNeeded(); |
|
|
|
|
|
const messageIndex = this.findMessageIndex(assistantMessage.id); |
|
|
|
|
|
this.updateMessageAtIndex(messageIndex, { thinking: streamedReasoningContent }); |
|
|
}, |
|
|
|
|
|
onComplete: async ( |
|
|
finalContent?: string, |
|
|
reasoningContent?: string, |
|
|
timings?: ChatMessageTimings |
|
|
) => { |
|
|
slotsService.stopStreaming(); |
|
|
|
|
|
const updateData: { |
|
|
content: string; |
|
|
thinking: string; |
|
|
timings?: ChatMessageTimings; |
|
|
model?: string; |
|
|
} = { |
|
|
content: finalContent || streamedContent, |
|
|
thinking: reasoningContent || streamedReasoningContent, |
|
|
timings: timings |
|
|
}; |
|
|
|
|
|
const capturedModel = captureModelIfNeeded(false); |
|
|
|
|
|
if (capturedModel) { |
|
|
updateData.model = capturedModel; |
|
|
} |
|
|
|
|
|
await DatabaseStore.updateMessage(assistantMessage.id, updateData); |
|
|
|
|
|
const messageIndex = this.findMessageIndex(assistantMessage.id); |
|
|
|
|
|
const localUpdateData: { timings?: ChatMessageTimings; model?: string } = { |
|
|
timings: timings |
|
|
}; |
|
|
|
|
|
if (updateData.model) { |
|
|
localUpdateData.model = updateData.model; |
|
|
} |
|
|
|
|
|
this.updateMessageAtIndex(messageIndex, localUpdateData); |
|
|
|
|
|
await DatabaseStore.updateCurrentNode(this.activeConversation!.id, assistantMessage.id); |
|
|
this.activeConversation!.currNode = assistantMessage.id; |
|
|
await this.refreshActiveMessages(); |
|
|
|
|
|
if (onComplete) { |
|
|
await onComplete(streamedContent); |
|
|
} |
|
|
|
|
|
this.isLoading = false; |
|
|
this.currentResponse = ''; |
|
|
}, |
|
|
|
|
|
onError: (error: Error) => { |
|
|
slotsService.stopStreaming(); |
|
|
|
|
|
if (error.name === 'AbortError' || error instanceof DOMException) { |
|
|
this.isLoading = false; |
|
|
this.currentResponse = ''; |
|
|
return; |
|
|
} |
|
|
|
|
|
console.error('Streaming error:', error); |
|
|
this.isLoading = false; |
|
|
this.currentResponse = ''; |
|
|
|
|
|
const messageIndex = this.activeMessages.findIndex( |
|
|
(m: DatabaseMessage) => m.id === assistantMessage.id |
|
|
); |
|
|
|
|
|
if (messageIndex !== -1) { |
|
|
const [failedMessage] = this.activeMessages.splice(messageIndex, 1); |
|
|
|
|
|
if (failedMessage) { |
|
|
DatabaseStore.deleteMessage(failedMessage.id).catch((cleanupError) => { |
|
|
console.error('Failed to remove assistant message after error:', cleanupError); |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
const dialogType = error.name === 'TimeoutError' ? 'timeout' : 'server'; |
|
|
|
|
|
this.showErrorDialog(dialogType, error.message); |
|
|
|
|
|
if (onError) { |
|
|
onError(error); |
|
|
} |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
private showErrorDialog(type: 'timeout' | 'server', message: string): void { |
|
|
this.errorDialogState = { type, message }; |
|
|
} |
|
|
|
|
|
dismissErrorDialog(): void { |
|
|
this.errorDialogState = null; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private isAbortError(error: unknown): boolean { |
|
|
return error instanceof Error && (error.name === 'AbortError' || error instanceof DOMException); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private findMessageIndex(messageId: string): number { |
|
|
return this.activeMessages.findIndex((m) => m.id === messageId); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private updateMessageAtIndex(index: number, updates: Partial<DatabaseMessage>): void { |
|
|
if (index !== -1) { |
|
|
Object.assign(this.activeMessages[index], updates); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private async createAssistantMessage(parentId?: string): Promise<DatabaseMessage | null> { |
|
|
if (!this.activeConversation) return null; |
|
|
|
|
|
return await DatabaseStore.createMessageBranch( |
|
|
{ |
|
|
convId: this.activeConversation.id, |
|
|
type: 'text', |
|
|
role: 'assistant', |
|
|
content: '', |
|
|
timestamp: Date.now(), |
|
|
thinking: '', |
|
|
children: [] |
|
|
}, |
|
|
parentId || null |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private updateConversationTimestamp(): void { |
|
|
if (!this.activeConversation) return; |
|
|
|
|
|
const chatIndex = this.conversations.findIndex((c) => c.id === this.activeConversation!.id); |
|
|
|
|
|
if (chatIndex !== -1) { |
|
|
this.conversations[chatIndex].lastModified = Date.now(); |
|
|
const updatedConv = this.conversations.splice(chatIndex, 1)[0]; |
|
|
this.conversations.unshift(updatedConv); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async sendMessage(content: string, extras?: DatabaseMessageExtra[]): Promise<void> { |
|
|
if ((!content.trim() && (!extras || extras.length === 0)) || this.isLoading) return; |
|
|
|
|
|
let isNewConversation = false; |
|
|
|
|
|
if (!this.activeConversation) { |
|
|
await this.createConversation(); |
|
|
isNewConversation = true; |
|
|
} |
|
|
|
|
|
if (!this.activeConversation) { |
|
|
console.error('No active conversation available for sending message'); |
|
|
return; |
|
|
} |
|
|
|
|
|
this.errorDialogState = null; |
|
|
this.isLoading = true; |
|
|
this.currentResponse = ''; |
|
|
|
|
|
let userMessage: DatabaseMessage | null = null; |
|
|
|
|
|
try { |
|
|
userMessage = await this.addMessage('user', content, 'text', '-1', extras); |
|
|
|
|
|
if (!userMessage) { |
|
|
throw new Error('Failed to add user message'); |
|
|
} |
|
|
|
|
|
|
|
|
if (isNewConversation && content) { |
|
|
const title = content.trim(); |
|
|
await this.updateConversationName(this.activeConversation.id, title); |
|
|
} |
|
|
|
|
|
const assistantMessage = await this.createAssistantMessage(userMessage.id); |
|
|
|
|
|
if (!assistantMessage) { |
|
|
throw new Error('Failed to create assistant message'); |
|
|
} |
|
|
|
|
|
this.activeMessages.push(assistantMessage); |
|
|
|
|
|
|
|
|
const conversationContext = this.activeMessages.slice(0, -1); |
|
|
|
|
|
await this.streamChatCompletion(conversationContext, assistantMessage); |
|
|
} catch (error) { |
|
|
if (this.isAbortError(error)) { |
|
|
this.isLoading = false; |
|
|
return; |
|
|
} |
|
|
|
|
|
console.error('Failed to send message:', error); |
|
|
this.isLoading = false; |
|
|
if (!this.errorDialogState) { |
|
|
if (error instanceof Error) { |
|
|
const dialogType = error.name === 'TimeoutError' ? 'timeout' : 'server'; |
|
|
this.showErrorDialog(dialogType, error.message); |
|
|
} else { |
|
|
this.showErrorDialog('server', 'Unknown error occurred while sending message'); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
stopGeneration(): void { |
|
|
slotsService.stopStreaming(); |
|
|
chatService.abort(); |
|
|
this.savePartialResponseIfNeeded(); |
|
|
this.isLoading = false; |
|
|
this.currentResponse = ''; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async gracefulStop(): Promise<void> { |
|
|
if (!this.isLoading) return; |
|
|
|
|
|
slotsService.stopStreaming(); |
|
|
chatService.abort(); |
|
|
await this.savePartialResponseIfNeeded(); |
|
|
this.isLoading = false; |
|
|
this.currentResponse = ''; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private async savePartialResponseIfNeeded(): Promise<void> { |
|
|
if (!this.currentResponse.trim() || !this.activeMessages.length) { |
|
|
return; |
|
|
} |
|
|
|
|
|
const lastMessage = this.activeMessages[this.activeMessages.length - 1]; |
|
|
|
|
|
if (lastMessage && lastMessage.role === 'assistant') { |
|
|
try { |
|
|
const updateData: { |
|
|
content: string; |
|
|
thinking?: string; |
|
|
timings?: ChatMessageTimings; |
|
|
} = { |
|
|
content: this.currentResponse |
|
|
}; |
|
|
|
|
|
if (lastMessage.thinking?.trim()) { |
|
|
updateData.thinking = lastMessage.thinking; |
|
|
} |
|
|
|
|
|
const lastKnownState = await slotsService.getCurrentState(); |
|
|
|
|
|
if (lastKnownState) { |
|
|
updateData.timings = { |
|
|
prompt_n: lastKnownState.promptTokens || 0, |
|
|
predicted_n: lastKnownState.tokensDecoded || 0, |
|
|
cache_n: lastKnownState.cacheTokens || 0, |
|
|
|
|
|
predicted_ms: |
|
|
lastKnownState.tokensPerSecond && lastKnownState.tokensDecoded |
|
|
? (lastKnownState.tokensDecoded / lastKnownState.tokensPerSecond) * 1000 |
|
|
: undefined |
|
|
}; |
|
|
} |
|
|
|
|
|
await DatabaseStore.updateMessage(lastMessage.id, updateData); |
|
|
|
|
|
lastMessage.content = this.currentResponse; |
|
|
if (updateData.thinking !== undefined) { |
|
|
lastMessage.thinking = updateData.thinking; |
|
|
} |
|
|
if (updateData.timings) { |
|
|
lastMessage.timings = updateData.timings; |
|
|
} |
|
|
} catch (error) { |
|
|
lastMessage.content = this.currentResponse; |
|
|
console.error('Failed to save partial response:', error); |
|
|
} |
|
|
} else { |
|
|
console.error('Last message is not an assistant message'); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async updateMessage(messageId: string, newContent: string): Promise<void> { |
|
|
if (!this.activeConversation) return; |
|
|
|
|
|
if (this.isLoading) { |
|
|
this.stopGeneration(); |
|
|
} |
|
|
|
|
|
try { |
|
|
const messageIndex = this.findMessageIndex(messageId); |
|
|
if (messageIndex === -1) { |
|
|
console.error('Message not found for update'); |
|
|
return; |
|
|
} |
|
|
|
|
|
const messageToUpdate = this.activeMessages[messageIndex]; |
|
|
const originalContent = messageToUpdate.content; |
|
|
|
|
|
if (messageToUpdate.role !== 'user') { |
|
|
console.error('Only user messages can be edited'); |
|
|
return; |
|
|
} |
|
|
|
|
|
const allMessages = await DatabaseStore.getConversationMessages(this.activeConversation.id); |
|
|
const rootMessage = allMessages.find((m) => m.type === 'root' && m.parent === null); |
|
|
const isFirstUserMessage = |
|
|
rootMessage && messageToUpdate.parent === rootMessage.id && messageToUpdate.role === 'user'; |
|
|
|
|
|
this.updateMessageAtIndex(messageIndex, { content: newContent }); |
|
|
await DatabaseStore.updateMessage(messageId, { content: newContent }); |
|
|
|
|
|
|
|
|
if (isFirstUserMessage && newContent.trim()) { |
|
|
await this.updateConversationTitleWithConfirmation( |
|
|
this.activeConversation.id, |
|
|
newContent.trim(), |
|
|
this.titleUpdateConfirmationCallback |
|
|
); |
|
|
} |
|
|
|
|
|
const messagesToRemove = this.activeMessages.slice(messageIndex + 1); |
|
|
for (const message of messagesToRemove) { |
|
|
await DatabaseStore.deleteMessage(message.id); |
|
|
} |
|
|
|
|
|
this.activeMessages = this.activeMessages.slice(0, messageIndex + 1); |
|
|
this.updateConversationTimestamp(); |
|
|
|
|
|
this.isLoading = true; |
|
|
this.currentResponse = ''; |
|
|
|
|
|
try { |
|
|
const assistantMessage = await this.createAssistantMessage(); |
|
|
if (!assistantMessage) { |
|
|
throw new Error('Failed to create assistant message'); |
|
|
} |
|
|
|
|
|
this.activeMessages.push(assistantMessage); |
|
|
await DatabaseStore.updateCurrentNode(this.activeConversation.id, assistantMessage.id); |
|
|
this.activeConversation.currNode = assistantMessage.id; |
|
|
|
|
|
await this.streamChatCompletion( |
|
|
this.activeMessages.slice(0, -1), |
|
|
assistantMessage, |
|
|
undefined, |
|
|
() => { |
|
|
const editedMessageIndex = this.findMessageIndex(messageId); |
|
|
this.updateMessageAtIndex(editedMessageIndex, { content: originalContent }); |
|
|
} |
|
|
); |
|
|
} catch (regenerateError) { |
|
|
console.error('Failed to regenerate response:', regenerateError); |
|
|
this.isLoading = false; |
|
|
|
|
|
const messageIndex = this.findMessageIndex(messageId); |
|
|
this.updateMessageAtIndex(messageIndex, { content: originalContent }); |
|
|
} |
|
|
} catch (error) { |
|
|
if (this.isAbortError(error)) { |
|
|
return; |
|
|
} |
|
|
|
|
|
console.error('Failed to update message:', error); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async regenerateMessage(messageId: string): Promise<void> { |
|
|
if (!this.activeConversation || this.isLoading) return; |
|
|
|
|
|
try { |
|
|
const messageIndex = this.findMessageIndex(messageId); |
|
|
if (messageIndex === -1) { |
|
|
console.error('Message not found for regeneration'); |
|
|
return; |
|
|
} |
|
|
|
|
|
const messageToRegenerate = this.activeMessages[messageIndex]; |
|
|
if (messageToRegenerate.role !== 'assistant') { |
|
|
console.error('Only assistant messages can be regenerated'); |
|
|
return; |
|
|
} |
|
|
|
|
|
const messagesToRemove = this.activeMessages.slice(messageIndex); |
|
|
for (const message of messagesToRemove) { |
|
|
await DatabaseStore.deleteMessage(message.id); |
|
|
} |
|
|
|
|
|
this.activeMessages = this.activeMessages.slice(0, messageIndex); |
|
|
this.updateConversationTimestamp(); |
|
|
|
|
|
this.isLoading = true; |
|
|
this.currentResponse = ''; |
|
|
|
|
|
try { |
|
|
const parentMessageId = |
|
|
this.activeMessages.length > 0 |
|
|
? this.activeMessages[this.activeMessages.length - 1].id |
|
|
: null; |
|
|
|
|
|
const assistantMessage = await this.createAssistantMessage(parentMessageId); |
|
|
|
|
|
if (!assistantMessage) { |
|
|
throw new Error('Failed to create assistant message'); |
|
|
} |
|
|
|
|
|
this.activeMessages.push(assistantMessage); |
|
|
|
|
|
const conversationContext = this.activeMessages.slice(0, -1); |
|
|
|
|
|
await this.streamChatCompletion(conversationContext, assistantMessage); |
|
|
} catch (regenerateError) { |
|
|
console.error('Failed to regenerate response:', regenerateError); |
|
|
this.isLoading = false; |
|
|
} |
|
|
} catch (error) { |
|
|
if (this.isAbortError(error)) return; |
|
|
console.error('Failed to regenerate message:', error); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async updateConversationName(convId: string, name: string): Promise<void> { |
|
|
try { |
|
|
await DatabaseStore.updateConversation(convId, { name }); |
|
|
|
|
|
const convIndex = this.conversations.findIndex((c) => c.id === convId); |
|
|
|
|
|
if (convIndex !== -1) { |
|
|
this.conversations[convIndex].name = name; |
|
|
} |
|
|
|
|
|
if (this.activeConversation?.id === convId) { |
|
|
this.activeConversation.name = name; |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('Failed to update conversation name:', error); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
setTitleUpdateConfirmationCallback( |
|
|
callback: (currentTitle: string, newTitle: string) => Promise<boolean> |
|
|
): void { |
|
|
this.titleUpdateConfirmationCallback = callback; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async updateConversationTitleWithConfirmation( |
|
|
convId: string, |
|
|
newTitle: string, |
|
|
onConfirmationNeeded?: (currentTitle: string, newTitle: string) => Promise<boolean> |
|
|
): Promise<boolean> { |
|
|
try { |
|
|
const currentConfig = config(); |
|
|
|
|
|
|
|
|
if (currentConfig.askForTitleConfirmation && onConfirmationNeeded) { |
|
|
const conversation = await DatabaseStore.getConversation(convId); |
|
|
if (!conversation) return false; |
|
|
|
|
|
const shouldUpdate = await onConfirmationNeeded(conversation.name, newTitle); |
|
|
if (!shouldUpdate) return false; |
|
|
} |
|
|
|
|
|
await this.updateConversationName(convId, newTitle); |
|
|
return true; |
|
|
} catch (error) { |
|
|
console.error('Failed to update conversation title with confirmation:', error); |
|
|
return false; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async downloadConversation(convId: string): Promise<void> { |
|
|
if (!this.activeConversation || this.activeConversation.id !== convId) { |
|
|
|
|
|
const conversation = await DatabaseStore.getConversation(convId); |
|
|
if (!conversation) return; |
|
|
|
|
|
const messages = await DatabaseStore.getConversationMessages(convId); |
|
|
const conversationData = { |
|
|
conv: conversation, |
|
|
messages |
|
|
}; |
|
|
|
|
|
this.triggerDownload(conversationData); |
|
|
} else { |
|
|
|
|
|
const conversationData: ExportedConversations = { |
|
|
conv: this.activeConversation!, |
|
|
messages: this.activeMessages |
|
|
}; |
|
|
|
|
|
this.triggerDownload(conversationData); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private triggerDownload(data: ExportedConversations, filename?: string): void { |
|
|
const conversation = |
|
|
'conv' in data ? data.conv : Array.isArray(data) ? data[0]?.conv : undefined; |
|
|
if (!conversation) { |
|
|
console.error('Invalid data: missing conversation'); |
|
|
return; |
|
|
} |
|
|
const conversationName = conversation.name ? conversation.name.trim() : ''; |
|
|
const convId = conversation.id || 'unknown'; |
|
|
const truncatedSuffix = conversationName |
|
|
.toLowerCase() |
|
|
.replace(/[^a-z0-9]/gi, '_') |
|
|
.replace(/_+/g, '_') |
|
|
.substring(0, 20); |
|
|
const downloadFilename = filename || `conversation_${convId}_${truncatedSuffix}.json`; |
|
|
|
|
|
const conversationJson = JSON.stringify(data, null, 2); |
|
|
const blob = new Blob([conversationJson], { |
|
|
type: 'application/json' |
|
|
}); |
|
|
const url = URL.createObjectURL(blob); |
|
|
const a = document.createElement('a'); |
|
|
a.href = url; |
|
|
a.download = downloadFilename; |
|
|
document.body.appendChild(a); |
|
|
a.click(); |
|
|
document.body.removeChild(a); |
|
|
URL.revokeObjectURL(url); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async exportAllConversations(): Promise<void> { |
|
|
try { |
|
|
const allConversations = await DatabaseStore.getAllConversations(); |
|
|
if (allConversations.length === 0) { |
|
|
throw new Error('No conversations to export'); |
|
|
} |
|
|
|
|
|
const allData: ExportedConversations = await Promise.all( |
|
|
allConversations.map(async (conv) => { |
|
|
const messages = await DatabaseStore.getConversationMessages(conv.id); |
|
|
return { conv, messages }; |
|
|
}) |
|
|
); |
|
|
|
|
|
const blob = new Blob([JSON.stringify(allData, null, 2)], { |
|
|
type: 'application/json' |
|
|
}); |
|
|
const url = URL.createObjectURL(blob); |
|
|
const a = document.createElement('a'); |
|
|
a.href = url; |
|
|
a.download = `all_conversations_${new Date().toISOString().split('T')[0]}.json`; |
|
|
document.body.appendChild(a); |
|
|
a.click(); |
|
|
document.body.removeChild(a); |
|
|
URL.revokeObjectURL(url); |
|
|
|
|
|
toast.success(`All conversations (${allConversations.length}) prepared for download`); |
|
|
} catch (err) { |
|
|
console.error('Failed to export conversations:', err); |
|
|
throw err; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async importConversations(): Promise<void> { |
|
|
return new Promise((resolve, reject) => { |
|
|
const input = document.createElement('input'); |
|
|
input.type = 'file'; |
|
|
input.accept = '.json'; |
|
|
|
|
|
input.onchange = async (e) => { |
|
|
const file = (e.target as HTMLInputElement)?.files?.[0]; |
|
|
if (!file) { |
|
|
reject(new Error('No file selected')); |
|
|
return; |
|
|
} |
|
|
|
|
|
try { |
|
|
const text = await file.text(); |
|
|
const parsedData = JSON.parse(text); |
|
|
let importedData: ExportedConversations; |
|
|
|
|
|
if (Array.isArray(parsedData)) { |
|
|
importedData = parsedData; |
|
|
} else if ( |
|
|
parsedData && |
|
|
typeof parsedData === 'object' && |
|
|
'conv' in parsedData && |
|
|
'messages' in parsedData |
|
|
) { |
|
|
|
|
|
importedData = [parsedData]; |
|
|
} else { |
|
|
throw new Error( |
|
|
'Invalid file format: expected array of conversations or single conversation object' |
|
|
); |
|
|
} |
|
|
|
|
|
const result = await DatabaseStore.importConversations(importedData); |
|
|
|
|
|
|
|
|
await this.loadConversations(); |
|
|
|
|
|
toast.success(`Imported ${result.imported} conversation(s), skipped ${result.skipped}`); |
|
|
|
|
|
resolve(undefined); |
|
|
} catch (err: unknown) { |
|
|
const message = err instanceof Error ? err.message : 'Unknown error'; |
|
|
console.error('Failed to import conversations:', err); |
|
|
toast.error('Import failed', { |
|
|
description: message |
|
|
}); |
|
|
reject(new Error(`Import failed: ${message}`)); |
|
|
} |
|
|
}; |
|
|
|
|
|
input.click(); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async deleteConversation(convId: string): Promise<void> { |
|
|
try { |
|
|
await DatabaseStore.deleteConversation(convId); |
|
|
|
|
|
this.conversations = this.conversations.filter((c) => c.id !== convId); |
|
|
|
|
|
if (this.activeConversation?.id === convId) { |
|
|
this.activeConversation = null; |
|
|
this.activeMessages = []; |
|
|
await goto(`?new_chat=true#/`); |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('Failed to delete conversation:', error); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async getDeletionInfo(messageId: string): Promise<{ |
|
|
totalCount: number; |
|
|
userMessages: number; |
|
|
assistantMessages: number; |
|
|
messageTypes: string[]; |
|
|
}> { |
|
|
if (!this.activeConversation) { |
|
|
return { totalCount: 0, userMessages: 0, assistantMessages: 0, messageTypes: [] }; |
|
|
} |
|
|
|
|
|
const allMessages = await DatabaseStore.getConversationMessages(this.activeConversation.id); |
|
|
const descendants = findDescendantMessages(allMessages, messageId); |
|
|
const allToDelete = [messageId, ...descendants]; |
|
|
|
|
|
const messagesToDelete = allMessages.filter((m) => allToDelete.includes(m.id)); |
|
|
|
|
|
let userMessages = 0; |
|
|
let assistantMessages = 0; |
|
|
const messageTypes: string[] = []; |
|
|
|
|
|
for (const msg of messagesToDelete) { |
|
|
if (msg.role === 'user') { |
|
|
userMessages++; |
|
|
if (!messageTypes.includes('user message')) messageTypes.push('user message'); |
|
|
} else if (msg.role === 'assistant') { |
|
|
assistantMessages++; |
|
|
if (!messageTypes.includes('assistant response')) messageTypes.push('assistant response'); |
|
|
} |
|
|
} |
|
|
|
|
|
return { |
|
|
totalCount: allToDelete.length, |
|
|
userMessages, |
|
|
assistantMessages, |
|
|
messageTypes |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async deleteMessage(messageId: string): Promise<void> { |
|
|
try { |
|
|
if (!this.activeConversation) return; |
|
|
|
|
|
|
|
|
const allMessages = await DatabaseStore.getConversationMessages(this.activeConversation.id); |
|
|
const messageToDelete = allMessages.find((m) => m.id === messageId); |
|
|
|
|
|
if (!messageToDelete) { |
|
|
console.error('Message to delete not found'); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
const currentPath = filterByLeafNodeId( |
|
|
allMessages, |
|
|
this.activeConversation.currNode || '', |
|
|
false |
|
|
); |
|
|
const isInCurrentPath = currentPath.some((m) => m.id === messageId); |
|
|
|
|
|
|
|
|
if (isInCurrentPath && messageToDelete.parent) { |
|
|
|
|
|
const siblings = allMessages.filter( |
|
|
(m) => m.parent === messageToDelete.parent && m.id !== messageId |
|
|
); |
|
|
|
|
|
if (siblings.length > 0) { |
|
|
|
|
|
const latestSibling = siblings.reduce((latest, sibling) => |
|
|
sibling.timestamp > latest.timestamp ? sibling : latest |
|
|
); |
|
|
|
|
|
|
|
|
const leafNodeId = findLeafNode(allMessages, latestSibling.id); |
|
|
|
|
|
|
|
|
await DatabaseStore.updateCurrentNode(this.activeConversation.id, leafNodeId); |
|
|
this.activeConversation.currNode = leafNodeId; |
|
|
} else { |
|
|
|
|
|
if (messageToDelete.parent) { |
|
|
const parentLeafId = findLeafNode(allMessages, messageToDelete.parent); |
|
|
await DatabaseStore.updateCurrentNode(this.activeConversation.id, parentLeafId); |
|
|
this.activeConversation.currNode = parentLeafId; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
await DatabaseStore.deleteMessageCascading(this.activeConversation.id, messageId); |
|
|
|
|
|
|
|
|
await this.refreshActiveMessages(); |
|
|
|
|
|
|
|
|
this.updateConversationTimestamp(); |
|
|
} catch (error) { |
|
|
console.error('Failed to delete message:', error); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
clearActiveConversation(): void { |
|
|
this.activeConversation = null; |
|
|
this.activeMessages = []; |
|
|
this.currentResponse = ''; |
|
|
this.isLoading = false; |
|
|
} |
|
|
|
|
|
|
|
|
async refreshActiveMessages(): Promise<void> { |
|
|
if (!this.activeConversation) return; |
|
|
|
|
|
const allMessages = await DatabaseStore.getConversationMessages(this.activeConversation.id); |
|
|
if (allMessages.length === 0) { |
|
|
this.activeMessages = []; |
|
|
return; |
|
|
} |
|
|
|
|
|
const leafNodeId = |
|
|
this.activeConversation.currNode || |
|
|
allMessages.reduce((latest, msg) => (msg.timestamp > latest.timestamp ? msg : latest)).id; |
|
|
|
|
|
const currentPath = filterByLeafNodeId(allMessages, leafNodeId, false) as DatabaseMessage[]; |
|
|
|
|
|
this.activeMessages.length = 0; |
|
|
this.activeMessages.push(...currentPath); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async navigateToSibling(siblingId: string): Promise<void> { |
|
|
if (!this.activeConversation) return; |
|
|
|
|
|
|
|
|
const allMessages = await DatabaseStore.getConversationMessages(this.activeConversation.id); |
|
|
const rootMessage = allMessages.find((m) => m.type === 'root' && m.parent === null); |
|
|
const currentFirstUserMessage = this.activeMessages.find( |
|
|
(m) => m.role === 'user' && m.parent === rootMessage?.id |
|
|
); |
|
|
|
|
|
const currentLeafNodeId = findLeafNode(allMessages, siblingId); |
|
|
|
|
|
await DatabaseStore.updateCurrentNode(this.activeConversation.id, currentLeafNodeId); |
|
|
this.activeConversation.currNode = currentLeafNodeId; |
|
|
await this.refreshActiveMessages(); |
|
|
|
|
|
|
|
|
if (rootMessage && this.activeMessages.length > 0) { |
|
|
|
|
|
const newFirstUserMessage = this.activeMessages.find( |
|
|
(m) => m.role === 'user' && m.parent === rootMessage.id |
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if ( |
|
|
newFirstUserMessage && |
|
|
newFirstUserMessage.content.trim() && |
|
|
(!currentFirstUserMessage || |
|
|
newFirstUserMessage.id !== currentFirstUserMessage.id || |
|
|
newFirstUserMessage.content.trim() !== currentFirstUserMessage.content.trim()) |
|
|
) { |
|
|
await this.updateConversationTitleWithConfirmation( |
|
|
this.activeConversation.id, |
|
|
newFirstUserMessage.content.trim(), |
|
|
this.titleUpdateConfirmationCallback |
|
|
); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async editAssistantMessage( |
|
|
messageId: string, |
|
|
newContent: string, |
|
|
shouldBranch: boolean |
|
|
): Promise<void> { |
|
|
if (!this.activeConversation || this.isLoading) return; |
|
|
|
|
|
try { |
|
|
const messageIndex = this.findMessageIndex(messageId); |
|
|
|
|
|
if (messageIndex === -1) { |
|
|
console.error('Message not found for editing'); |
|
|
return; |
|
|
} |
|
|
|
|
|
const messageToEdit = this.activeMessages[messageIndex]; |
|
|
|
|
|
if (messageToEdit.role !== 'assistant') { |
|
|
console.error('Only assistant messages can be edited with this method'); |
|
|
return; |
|
|
} |
|
|
|
|
|
if (shouldBranch) { |
|
|
const newMessage = await DatabaseStore.createMessageBranch( |
|
|
{ |
|
|
convId: messageToEdit.convId, |
|
|
type: messageToEdit.type, |
|
|
timestamp: Date.now(), |
|
|
role: messageToEdit.role, |
|
|
content: newContent, |
|
|
thinking: messageToEdit.thinking || '', |
|
|
children: [], |
|
|
model: messageToEdit.model |
|
|
}, |
|
|
messageToEdit.parent! |
|
|
); |
|
|
|
|
|
await DatabaseStore.updateCurrentNode(this.activeConversation.id, newMessage.id); |
|
|
this.activeConversation.currNode = newMessage.id; |
|
|
} else { |
|
|
await DatabaseStore.updateMessage(messageToEdit.id, { |
|
|
content: newContent, |
|
|
timestamp: Date.now() |
|
|
}); |
|
|
|
|
|
this.updateMessageAtIndex(messageIndex, { |
|
|
content: newContent, |
|
|
timestamp: Date.now() |
|
|
}); |
|
|
} |
|
|
|
|
|
this.updateConversationTimestamp(); |
|
|
await this.refreshActiveMessages(); |
|
|
} catch (error) { |
|
|
console.error('Failed to edit assistant message:', error); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async editMessageWithBranching(messageId: string, newContent: string): Promise<void> { |
|
|
if (!this.activeConversation || this.isLoading) return; |
|
|
|
|
|
try { |
|
|
const messageIndex = this.findMessageIndex(messageId); |
|
|
if (messageIndex === -1) { |
|
|
console.error('Message not found for editing'); |
|
|
return; |
|
|
} |
|
|
|
|
|
const messageToEdit = this.activeMessages[messageIndex]; |
|
|
if (messageToEdit.role !== 'user') { |
|
|
console.error('Only user messages can be edited'); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const allMessages = await DatabaseStore.getConversationMessages(this.activeConversation.id); |
|
|
const rootMessage = allMessages.find((m) => m.type === 'root' && m.parent === null); |
|
|
const isFirstUserMessage = |
|
|
rootMessage && messageToEdit.parent === rootMessage.id && messageToEdit.role === 'user'; |
|
|
|
|
|
let parentId = messageToEdit.parent; |
|
|
|
|
|
if (parentId === undefined || parentId === null) { |
|
|
const rootMessage = allMessages.find((m) => m.type === 'root' && m.parent === null); |
|
|
if (rootMessage) { |
|
|
parentId = rootMessage.id; |
|
|
} else { |
|
|
console.error('No root message found for editing'); |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
const newMessage = await DatabaseStore.createMessageBranch( |
|
|
{ |
|
|
convId: messageToEdit.convId, |
|
|
type: messageToEdit.type, |
|
|
timestamp: Date.now(), |
|
|
role: messageToEdit.role, |
|
|
content: newContent, |
|
|
thinking: messageToEdit.thinking || '', |
|
|
children: [], |
|
|
extra: messageToEdit.extra ? JSON.parse(JSON.stringify(messageToEdit.extra)) : undefined, |
|
|
model: messageToEdit.model |
|
|
}, |
|
|
parentId |
|
|
); |
|
|
|
|
|
await DatabaseStore.updateCurrentNode(this.activeConversation.id, newMessage.id); |
|
|
this.activeConversation.currNode = newMessage.id; |
|
|
this.updateConversationTimestamp(); |
|
|
|
|
|
|
|
|
if (isFirstUserMessage && newContent.trim()) { |
|
|
await this.updateConversationTitleWithConfirmation( |
|
|
this.activeConversation.id, |
|
|
newContent.trim(), |
|
|
this.titleUpdateConfirmationCallback |
|
|
); |
|
|
} |
|
|
|
|
|
await this.refreshActiveMessages(); |
|
|
|
|
|
if (messageToEdit.role === 'user') { |
|
|
await this.generateResponseForMessage(newMessage.id); |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('Failed to edit message with branching:', error); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async regenerateMessageWithBranching(messageId: string): Promise<void> { |
|
|
if (!this.activeConversation || this.isLoading) return; |
|
|
|
|
|
try { |
|
|
const messageIndex = this.findMessageIndex(messageId); |
|
|
if (messageIndex === -1) { |
|
|
console.error('Message not found for regeneration'); |
|
|
return; |
|
|
} |
|
|
|
|
|
const messageToRegenerate = this.activeMessages[messageIndex]; |
|
|
if (messageToRegenerate.role !== 'assistant') { |
|
|
console.error('Only assistant messages can be regenerated'); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
const conversationMessages = await DatabaseStore.getConversationMessages( |
|
|
this.activeConversation.id |
|
|
); |
|
|
const parentMessage = conversationMessages.find((m) => m.id === messageToRegenerate.parent); |
|
|
if (!parentMessage) { |
|
|
console.error('Parent message not found for regeneration'); |
|
|
return; |
|
|
} |
|
|
|
|
|
this.isLoading = true; |
|
|
this.currentResponse = ''; |
|
|
|
|
|
const newAssistantMessage = await DatabaseStore.createMessageBranch( |
|
|
{ |
|
|
convId: this.activeConversation.id, |
|
|
type: 'text', |
|
|
timestamp: Date.now(), |
|
|
role: 'assistant', |
|
|
content: '', |
|
|
thinking: '', |
|
|
children: [] |
|
|
}, |
|
|
parentMessage.id |
|
|
); |
|
|
|
|
|
await DatabaseStore.updateCurrentNode(this.activeConversation.id, newAssistantMessage.id); |
|
|
this.activeConversation.currNode = newAssistantMessage.id; |
|
|
this.updateConversationTimestamp(); |
|
|
await this.refreshActiveMessages(); |
|
|
|
|
|
const allConversationMessages = await DatabaseStore.getConversationMessages( |
|
|
this.activeConversation.id |
|
|
); |
|
|
const conversationPath = filterByLeafNodeId( |
|
|
allConversationMessages, |
|
|
parentMessage.id, |
|
|
false |
|
|
) as DatabaseMessage[]; |
|
|
|
|
|
await this.streamChatCompletion(conversationPath, newAssistantMessage); |
|
|
} catch (error) { |
|
|
if (this.isAbortError(error)) return; |
|
|
|
|
|
console.error('Failed to regenerate message with branching:', error); |
|
|
this.isLoading = false; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private async generateResponseForMessage(userMessageId: string): Promise<void> { |
|
|
if (!this.activeConversation) return; |
|
|
|
|
|
this.errorDialogState = null; |
|
|
this.isLoading = true; |
|
|
this.currentResponse = ''; |
|
|
|
|
|
try { |
|
|
|
|
|
const allMessages = await DatabaseStore.getConversationMessages(this.activeConversation.id); |
|
|
const conversationPath = filterByLeafNodeId( |
|
|
allMessages, |
|
|
userMessageId, |
|
|
false |
|
|
) as DatabaseMessage[]; |
|
|
|
|
|
|
|
|
const assistantMessage = await DatabaseStore.createMessageBranch( |
|
|
{ |
|
|
convId: this.activeConversation.id, |
|
|
type: 'text', |
|
|
timestamp: Date.now(), |
|
|
role: 'assistant', |
|
|
content: '', |
|
|
thinking: '', |
|
|
children: [] |
|
|
}, |
|
|
userMessageId |
|
|
); |
|
|
|
|
|
|
|
|
this.activeMessages.push(assistantMessage); |
|
|
|
|
|
|
|
|
await this.streamChatCompletion(conversationPath, assistantMessage); |
|
|
} catch (error) { |
|
|
console.error('Failed to generate response:', error); |
|
|
this.isLoading = false; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
export const chatStore = new ChatStore(); |
|
|
|
|
|
export const conversations = () => chatStore.conversations; |
|
|
export const activeConversation = () => chatStore.activeConversation; |
|
|
export const activeMessages = () => chatStore.activeMessages; |
|
|
export const isLoading = () => chatStore.isLoading; |
|
|
export const currentResponse = () => chatStore.currentResponse; |
|
|
export const isInitialized = () => chatStore.isInitialized; |
|
|
export const errorDialog = () => chatStore.errorDialogState; |
|
|
|
|
|
export const createConversation = chatStore.createConversation.bind(chatStore); |
|
|
export const downloadConversation = chatStore.downloadConversation.bind(chatStore); |
|
|
export const exportAllConversations = chatStore.exportAllConversations.bind(chatStore); |
|
|
export const importConversations = chatStore.importConversations.bind(chatStore); |
|
|
export const deleteConversation = chatStore.deleteConversation.bind(chatStore); |
|
|
export const sendMessage = chatStore.sendMessage.bind(chatStore); |
|
|
export const dismissErrorDialog = chatStore.dismissErrorDialog.bind(chatStore); |
|
|
|
|
|
export const gracefulStop = chatStore.gracefulStop.bind(chatStore); |
|
|
|
|
|
|
|
|
export const refreshActiveMessages = chatStore.refreshActiveMessages.bind(chatStore); |
|
|
export const navigateToSibling = chatStore.navigateToSibling.bind(chatStore); |
|
|
export const editAssistantMessage = chatStore.editAssistantMessage.bind(chatStore); |
|
|
export const editMessageWithBranching = chatStore.editMessageWithBranching.bind(chatStore); |
|
|
export const regenerateMessageWithBranching = |
|
|
chatStore.regenerateMessageWithBranching.bind(chatStore); |
|
|
export const deleteMessage = chatStore.deleteMessage.bind(chatStore); |
|
|
export const getDeletionInfo = chatStore.getDeletionInfo.bind(chatStore); |
|
|
export const updateConversationName = chatStore.updateConversationName.bind(chatStore); |
|
|
export const setTitleUpdateConfirmationCallback = |
|
|
chatStore.setTitleUpdateConfirmationCallback.bind(chatStore); |
|
|
|
|
|
export function stopGeneration() { |
|
|
chatStore.stopGeneration(); |
|
|
} |
|
|
export const messages = () => chatStore.activeMessages; |
|
|
|