feat: stream persistent

This commit is contained in:
2026-04-28 22:41:03 -04:00
parent fad8ed865a
commit eb66801f58
7 changed files with 485 additions and 133 deletions

View File

@@ -6,6 +6,7 @@ import {
getSettings,
getModels,
sendMessage,
streamChatUpdates,
getChatMessages,
listChats,
deleteChat,
@@ -41,6 +42,7 @@ Alpine.data('chatManager', () => ({
selectedChatID: null as string | null,
chatListOpen: false,
loading: false,
activeStreamChatID: null as string | null,
async init() {
// Acquire Data
@@ -109,66 +111,27 @@ Alpine.data('chatManager', () => ({
this.selectedChatID = IN_PROGRESS_UUID;
}
// New User Message
let userMessage: Message = {
// Add Optimistic User Message
const currentChat: Chat = this.chats.find(
(c) => c.id === this.selectedChatID,
)!;
currentChat.messages.push({
id: IN_PROGRESS_UUID,
chat_id: this.selectedChatID,
role: 'user',
thinking: '',
content: message,
created_at: new Date().toISOString(),
};
// Get Chat
let currentChat: Chat = this.chats.find(
(c) => c.id === this.selectedChatID,
)!;
// Add User Message
currentChat.messages.push(userMessage);
});
currentChat.message_count += 1;
// Assistant Message Placeholder
let assistantMessage: Message | undefined;
try {
await sendMessage(
this.selectedChatID === IN_PROGRESS_UUID ? '' : this.selectedChatID,
{ model: this.selectedModel, prompt: message },
(chunk: MessageChunk) => {
// Handle Chat
if (chunk.chat) {
Object.assign(currentChat, {
...chunk.chat,
messages: currentChat.messages,
});
this.selectedChatID = chunk.chat.id;
this.updateHash(chunk.chat.id);
}
// Handle User Message
if (chunk.user_message) {
Object.assign(userMessage, chunk.user_message);
}
// Handle Assistant Message
if (chunk.assistant_message) {
if (!assistantMessage) {
assistantMessage = chunk.assistant_message;
currentChat.messages.push(assistantMessage);
} else {
const index = currentChat.messages.findIndex(
(m) => m.id === assistantMessage!.id,
);
if (index !== -1) {
currentChat.messages[index] = {
...assistantMessage,
...chunk.assistant_message,
};
currentChat.messages = [...currentChat.messages];
}
}
}
if (chunk.chat) this.activeStreamChatID = chunk.chat.id;
this.applyMessageChunk(chunk);
},
);
} catch (err) {
@@ -176,9 +139,56 @@ Alpine.data('chatManager', () => ({
this.error = parseError(err);
} finally {
this.loading = false;
this.activeStreamChatID = null;
}
},
applyMessageChunk(chunk: MessageChunk) {
// Handle Chat
if (chunk.chat) {
let chat = this.chats.find((c) => c.id === chunk.chat!.id);
if (!chat) chat = this.chats.find((c) => c.id === IN_PROGRESS_UUID);
if (!chat) {
chat = { ...chunk.chat, messages: chunk.chat.messages || [] };
this.chats.unshift(chat);
} else {
Object.assign(chat, chunk.chat);
chat.messages = chunk.chat.messages?.length
? chunk.chat.messages
: chat.messages;
}
this.selectedChatID = chunk.chat.id;
this.updateHash(chunk.chat.id);
}
const chatID = chunk.chat?.id || this.selectedChatID;
const currentChat = this.chats.find((c) => c.id === chatID);
if (!currentChat) return;
// Handle Messages
if (chunk.user_message) this.upsertMessage(currentChat, chunk.user_message);
if (chunk.assistant_message)
this.upsertMessage(currentChat, chunk.assistant_message);
},
upsertMessage(chat: Chat, message: Message) {
// Upsert Message
const existingIndex = chat.messages.findIndex(
(m) =>
m.id === message.id ||
(m.id === IN_PROGRESS_UUID && m.role === message.role),
);
if (existingIndex === -1) {
chat.messages.push(message);
} else {
chat.messages[existingIndex] = {
...chat.messages[existingIndex],
...message,
};
}
chat.messages = [...chat.messages];
},
updateHash(chatID: string | null) {
const newRoute = CHAT_ROUTE + (chatID ? '/' + chatID : '');
window.history.pushState(null, '', newRoute);
@@ -202,13 +212,46 @@ Alpine.data('chatManager', () => ({
(c) => c.id == this.selectedChatID,
);
this.chats[chatIndex].messages = response.messages || [];
if (chatIndex === -1) return;
this.chats[chatIndex] = {
...this.chats[chatIndex],
...response,
messages: response.messages || [],
};
await this.reconnectChatStream(response);
} catch (err) {
console.error('Error loading chat messages:', err);
this.error = 'Failed to load messages';
}
},
async reconnectChatStream(chat: Chat) {
const latestMessage = chat.messages[chat.messages.length - 1];
if (
!latestMessage ||
latestMessage.role !== 'assistant' ||
latestMessage.status !== 'streaming' ||
this.activeStreamChatID === chat.id
) {
return;
}
// Reconnect Stream
this.loading = true;
this.activeStreamChatID = chat.id;
try {
await streamChatUpdates(chat.id, (chunk: MessageChunk) =>
this.applyMessageChunk(chunk),
);
} catch (err) {
console.error('Error reconnecting chat stream:', err);
this.error = parseError(err);
} finally {
this.loading = false;
this.activeStreamChatID = null;
}
},
get models(): Model[] {
if (!this.settings.text_generation_selector) return this._models;
return applyFilter(this._models, this.settings.text_generation_selector);