Spaces:
Paused
Paused
| import type { ChatMessage, ChatSession, ModelId, ToolEvent } from './contracts'; | |
| const MODEL_IDS: readonly ModelId[] = ['bonsai-1.7b', 'bonsai-4b', 'bonsai-8b', 'bonsai-27b']; | |
| export function conversationTitle(messages: ChatMessage[]): string { | |
| const firstPrompt = messages.find((message) => message.role === 'user' && message.content.trim())?.content.trim(); | |
| return firstPrompt ? firstPrompt.replace(/\s+/g, ' ').slice(0, 80) : 'New chat'; | |
| } | |
| export function conversationPreview(messages: ChatMessage[]): string { | |
| const latest = [...messages].reverse().find((message) => message.content.trim())?.content.trim(); | |
| return latest ? latest.replace(/\s+/g, ' ').slice(0, 140) : 'No messages yet'; | |
| } | |
| export function normalizeConversationModelId(value: string): ModelId { | |
| return MODEL_IDS.includes(value as ModelId) ? value as ModelId : 'bonsai-27b'; | |
| } | |
| export function normalizeConversationToolsEnabled(value: unknown): boolean { | |
| return value === true; | |
| } | |
| export function upsertConversation(sessions: ChatSession[], next: ChatSession): ChatSession[] { | |
| return [next, ...sessions.filter((session) => session.id !== next.id)] | |
| .sort((left, right) => right.updatedAt - left.updatedAt); | |
| } | |
| export function normalizeConversationToolEvents( | |
| value: unknown, | |
| messages: ChatMessage[], | |
| ): ToolEvent[] { | |
| const events = new Map<string, ToolEvent>(); | |
| for (const message of messages) { | |
| for (const tool of message.tools ?? []) { | |
| const interrupted = tool.state === 'queued' || tool.state === 'running'; | |
| events.set(tool.id, { | |
| id: tool.id, | |
| timestamp: message.timestamp, | |
| label: tool.name, | |
| detail: interrupted | |
| ? 'interrupted before completion' | |
| : tool.state === 'error' | |
| ? 'tool returned a bounded error' | |
| : 'local result restored from chat history', | |
| state: tool.state === 'complete' ? 'complete' : 'error', | |
| }); | |
| } | |
| } | |
| if (Array.isArray(value)) { | |
| for (const candidate of value) { | |
| if (!candidate || typeof candidate !== 'object') continue; | |
| const event = candidate as Partial<ToolEvent>; | |
| if (typeof event.id !== 'string' | |
| || typeof event.timestamp !== 'string' | |
| || typeof event.label !== 'string' | |
| || typeof event.detail !== 'string' | |
| || !['running', 'complete', 'error'].includes(event.state ?? '')) continue; | |
| events.set(event.id, event as ToolEvent); | |
| } | |
| } | |
| return [...events.values()].slice(-20); | |
| } | |
| export interface MessageRegenerationPlan { | |
| baseMessages: ChatMessage[]; | |
| userMessage: ChatMessage; | |
| discardedMessageIds: string[]; | |
| } | |
| export function planMessageRegeneration( | |
| messages: ChatMessage[], | |
| messageId: string, | |
| ): MessageRegenerationPlan | null { | |
| const targetIndex = messages.findIndex((message) => message.id === messageId); | |
| if (targetIndex < 0) return null; | |
| const target = messages[targetIndex]; | |
| if (!target || target.role === 'system') return null; | |
| let userIndex = target.role === 'user' ? targetIndex : -1; | |
| for (let index = targetIndex - 1; userIndex < 0 && index >= 0; index -= 1) { | |
| if (messages[index]?.role === 'user') userIndex = index; | |
| } | |
| const userMessage = messages[userIndex]; | |
| if (!userMessage || !userMessage.content.trim()) return null; | |
| return { | |
| baseMessages: messages.slice(0, userIndex), | |
| userMessage, | |
| discardedMessageIds: messages.slice(userIndex + 1).map((message) => message.id), | |
| }; | |
| } | |
| export function planUserMessageEdit( | |
| messages: ChatMessage[], | |
| messageId: string, | |
| content: string, | |
| editedAt = Date.now(), | |
| ): MessageRegenerationPlan | null { | |
| const targetIndex = messages.findIndex((message) => message.id === messageId); | |
| const target = messages[targetIndex]; | |
| const normalized = content.trim(); | |
| if (targetIndex < 0 || target?.role !== 'user' || !normalized) return null; | |
| return { | |
| baseMessages: messages.slice(0, targetIndex), | |
| userMessage: { ...target, content: normalized, editedAt }, | |
| discardedMessageIds: messages.slice(targetIndex + 1).map((message) => message.id), | |
| }; | |
| } | |