@veluai/velu 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -31,20 +31,23 @@ function siteHost(explicit) {
|
|
|
31
31
|
return typeof window !== 'undefined' ? window.location.host : '';
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
// Parse one raw SSE record ("
|
|
34
|
+
// Parse one raw SSE record ("id: N\nevent: x\ndata: {…}") into { event, data, seq }.
|
|
35
35
|
function parseSseRecord(raw) {
|
|
36
36
|
let event = 'message';
|
|
37
|
+
let seq = null;
|
|
37
38
|
const dataLines = [];
|
|
38
39
|
for (const line of raw.split('\n')) {
|
|
39
40
|
if (line.startsWith('event:')) event = line.slice(6).trim();
|
|
40
|
-
else if (line.startsWith('
|
|
41
|
+
else if (line.startsWith('id:')) {
|
|
42
|
+
const n = Number(line.slice(3).trim());
|
|
43
|
+
if (Number.isFinite(n)) seq = n;
|
|
44
|
+
} else if (line.startsWith('data:')) dataLines.push(line.slice(5).replace(/^ /, ''));
|
|
41
45
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
} catch {
|
|
46
|
-
return { event, data: null };
|
|
46
|
+
let data = null;
|
|
47
|
+
if (dataLines.length) {
|
|
48
|
+
try { data = JSON.parse(dataLines.join('\n')); } catch { data = null; }
|
|
47
49
|
}
|
|
50
|
+
return { event, data, seq };
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
// Backend citation → the Chatbot's Source shape.
|
|
@@ -63,6 +66,11 @@ export function createDocsAssistant({ apiBase, host } = {}) {
|
|
|
63
66
|
if (!base) return null;
|
|
64
67
|
const root = `${base}/api/v1/public/ai-assistant`;
|
|
65
68
|
let bootstrapped = false;
|
|
69
|
+
// Highest event seq seen per conversation. The events endpoint replays history
|
|
70
|
+
// after `after_seq`; without this a reused (multi-turn) conversation replays
|
|
71
|
+
// from 0 and the first (old) assistant.completed is returned for every
|
|
72
|
+
// follow-up turn — i.e. the answer never updates after the first message.
|
|
73
|
+
const lastSeqByConv = new Map();
|
|
66
74
|
|
|
67
75
|
const headers = (extra) => ({
|
|
68
76
|
'X-Velu-Site-Host': siteHost(host),
|
|
@@ -98,8 +106,9 @@ export function createDocsAssistant({ apiBase, host } = {}) {
|
|
|
98
106
|
|
|
99
107
|
// Manual fetch-stream SSE parse (not EventSource): supports credentials,
|
|
100
108
|
// the X-Velu-Site-Host header, and AbortController cancellation.
|
|
109
|
+
const afterSeq = lastSeqByConv.get(convId) ?? 0;
|
|
101
110
|
const evRes = await fetch(
|
|
102
|
-
`${root}/conversations/${convId}/events?after_seq
|
|
111
|
+
`${root}/conversations/${convId}/events?after_seq=${afterSeq}&token=${encodeURIComponent(token)}`,
|
|
103
112
|
{ credentials: 'include', headers: headers({ Accept: 'text/event-stream' }), signal },
|
|
104
113
|
);
|
|
105
114
|
if (!evRes.ok || !evRes.body) throw new Error(`ask-ai: stream ${evRes.status}`);
|
|
@@ -114,8 +123,11 @@ export function createDocsAssistant({ apiBase, host } = {}) {
|
|
|
114
123
|
buf += decoder.decode(value, { stream: true });
|
|
115
124
|
let sep;
|
|
116
125
|
while ((sep = buf.indexOf('\n\n')) !== -1) {
|
|
117
|
-
const { event, data } = parseSseRecord(buf.slice(0, sep));
|
|
126
|
+
const { event, data, seq } = parseSseRecord(buf.slice(0, sep));
|
|
118
127
|
buf = buf.slice(sep + 2);
|
|
128
|
+
if (seq != null) {
|
|
129
|
+
lastSeqByConv.set(convId, Math.max(lastSeqByConv.get(convId) ?? 0, seq));
|
|
130
|
+
}
|
|
119
131
|
if (event === 'assistant.completed') {
|
|
120
132
|
const m = data?.message || {};
|
|
121
133
|
yield {
|