feat: chat stream messages

This commit is contained in:
Shivering Ocelot committed 2026-05-31 22:24:07 -05:00
1 parent bc71fa4d59
commit 5d2f7609f4
5 files changed
+131 -10

No files matched your search

+26 -2
View File
@@ -1,5 +1,6 @@
import * as ui from './ui';
import Stream from './stream';
export class ChatInfo {
constructor({ id, name }) {
@@ -42,6 +43,16 @@ function createDeferred() {
return {promise, resolve, reject};
}
function createDeferredStream(callback) {
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
let deferred = {promise, resolve, reject};
return {promise, resolve (value) { callback(value, deferred) }, reject}
}
function initState () {
backendUrl = window.location.origin;
const tk = localStorage.getItem('loginToken');
@@ -126,12 +137,12 @@ export async function newToken () {
// --- Communication ---
export function sendAction(action, body = {}) {
export function sendAction(action, body = {}, callback = null) {
if (socket?.readyState !== WebSocket.OPEN) {
return Promise.reject(new Error("WebSocket is not open. Cannot send action."));
}
const nonce = nextNonce++;
const deferred = createDeferred();
const deferred = callback ? createDeferredStream(callback) : createDeferred();
pendingActions[nonce] = deferred;
deferred.promise.finally(() => {
@@ -320,6 +331,19 @@ export async function getAgent(agentId) {
return allAgents.find(it => it.id == agentId);
}
export function readStream(streamId) {
let stream = new Stream(streamId);
sendAction("read-stream", {streamId}, (packet, deferred) => {
if (packet.eof) {
stream.close();
deferred.resolve();
} else if (packet.data != null) {
stream.feed(packet.data);
}
});
return stream;
}
export function invalidateCache({ chats=false, agents=false } = {}) {
if (chats) cache.chats = null;
if (agents) cache.agents = null;
+31 -4
View File
@@ -1,5 +1,7 @@
<script setup>
import { ref, watch } from 'vue';
import { marked } from "marked";
import * as backend from '../backend';
marked.use({
renderer: {
code (code, infostring, escaped) {
@@ -36,6 +38,31 @@ function chatEntryClass(msg) {
return ['info'];
}
}
const format = ref(null);
const content = ref("");
let currentStreamId = null;
watch(() => [props.message.format, props.message.content], ([newFormat, newContent]) => {
// TODO: if format.startsWith("stream:")
if (newFormat == "stream") {
format.value = null;
currentStreamId = parseInt(newContent);
let stream = backend.readStream(currentStreamId);
stream.on('data', (chunk) => {
if (stream.id == currentStreamId) {
content.value = stream.partialContent;
}
});
} else {
format.value = newFormat;
content.value = newContent;
currentStreamId = null;
}
}, {immediate: true})
</script>
<template>
@@ -43,18 +70,18 @@ function chatEntryClass(msg) {
<template v-if="message.type == 'message' || message.type == null">
<div v-if="message.sender != 'user'" class="sender">{{message.sender}}</div>
<div v-if="message.format == 'markdown'" v-html="marked.parse(message.content)" class="content"></div>
<div v-else class="content">{{message.content}}</div>
<div v-if="format == 'markdown'" v-html="marked.parse(message.content)" class="content"></div>
<div v-else class="content">{{content}}</div>
</template>
<template v-if="message.type == 'note'">
<div class="sender">{{message.sender}}</div>
<div class="content">{{message.content}}</div>
<div class="content">{{content}}</div>
</template>
<template v-if="message.type == 'log'">
<div class="sender">Log ({{message.sender}})</div>
<div class="content">{{message.content}}</div>
<div class="content">{{content}}</div>
</template>
<template v-if="message.type == 'thread-header'">
+62
View File
@@ -0,0 +1,62 @@
export default class Stream {
constructor (id) {
this.id = id;
this.partialContent = "";
this.eof = false;
this.listeners = {
data: [],
end: [],
};
let onResponse = (msg) => {
if (this.eof) return;
if (msg.eof) {
this.eof = true;
this.handle('end');
} else {
this.partialContent += msg.data;
this.handle('data', msg.data);
}
};
this.contentPromise = new Promise((resolve, reject) => {
this.handle("end", () => {
resolve(this.partialContent);
});
});
}
feed (chunk) {
if (this.eof) return;
this.partialContent += chunk;
this.handle('data', chunk);
}
close () {
this.eof = true;
this.handle('end', this.partialContent);
}
getContent () {
return this.contentPromise;
}
handle (event, ...args) {
for (let fn of this.listeners[event]) {
fn(...args)
}
}
on (event, callback) {
this.listeners[event].push(callback);
if (event == "data" && this.partialContent) {
callback(this.partialContent);
}
if (event == "end" && this.eof) {
callback();
}
}
}
+3 -2
View File
@@ -84,6 +84,7 @@
for (let entry of messages.value) {
if (entry.id == it.id) {
entry.type = it.kind;
entry.format = it.format;
entry.content = it.content;
entry.threadId = it.threadId;
return;
@@ -108,6 +109,7 @@
addMessage({
id: it.id,
type: it.kind,
format: it.format,
sender: it.sender,
content: it.content,
threadId: it.threadId,
@@ -163,7 +165,6 @@
}
runListener.addRunId(event.runId);
messageRunMap.value[event.messageId] = event.runId;
ui.alert("Chat Run Message: " + JSON.stringify(event));
}
function onRunUpdate (newRun) {
@@ -335,7 +336,7 @@
async function startNewThread () {
const res = await backend.sendAction("create-chat-thread", {chatId});
console.log("Starting a nedeleteChatw thread", res);
console.log("Starting a new thread", res);
if (res.threadId != null) {
currentThreadId.value = res.threadId;
expandThread(res.threadId, true);