@veluai/velu 0.2.7 → 0.2.8
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
|
@@ -287,7 +287,22 @@ function ChatHeader({ onClose, onNew, onHistory, historyOpen }) {
|
|
|
287
287
|
}
|
|
288
288
|
|
|
289
289
|
/* ── History overlay ────────────────────────────────────────────────── */
|
|
290
|
-
|
|
290
|
+
|
|
291
|
+
/* "Today, 10:24" / "Yesterday" / "Apr 28" for a conversation timestamp. */
|
|
292
|
+
function whenLabel(iso) {
|
|
293
|
+
if (!iso) return '';
|
|
294
|
+
const d = new Date(iso);
|
|
295
|
+
if (Number.isNaN(d.getTime())) return '';
|
|
296
|
+
const startOfDay = (x) => new Date(x.getFullYear(), x.getMonth(), x.getDate());
|
|
297
|
+
const days = Math.round((startOfDay(new Date()) - startOfDay(d)) / 86400000);
|
|
298
|
+
if (days <= 0) {
|
|
299
|
+
return `Today, ${d.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}`;
|
|
300
|
+
}
|
|
301
|
+
if (days === 1) return 'Yesterday';
|
|
302
|
+
return d.toLocaleDateString([], { month: 'short', day: 'numeric' });
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function HistoryPanel({ items, loading, onPick, onClose }) {
|
|
291
306
|
return (
|
|
292
307
|
<div className="velu-chatbot__history">
|
|
293
308
|
<Cluster
|
|
@@ -307,17 +322,23 @@ function HistoryPanel({ onPick, onClose }) {
|
|
|
307
322
|
</button>
|
|
308
323
|
</Cluster>
|
|
309
324
|
<Stack as="div" space="var(--s-5)" className="velu-chatbot__history-list">
|
|
310
|
-
{
|
|
311
|
-
<
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
325
|
+
{loading ? (
|
|
326
|
+
<span className="velu-chatbot__history-empty">Loading…</span>
|
|
327
|
+
) : items.length === 0 ? (
|
|
328
|
+
<span className="velu-chatbot__history-empty">No conversations yet.</span>
|
|
329
|
+
) : (
|
|
330
|
+
items.map((h) => (
|
|
331
|
+
<button
|
|
332
|
+
key={h.id}
|
|
333
|
+
type="button"
|
|
334
|
+
className="velu-chatbot__history-item"
|
|
335
|
+
onClick={() => onPick(h)}
|
|
336
|
+
>
|
|
337
|
+
<span className="velu-chatbot__history-item-title">{h.title}</span>
|
|
338
|
+
<span className="velu-chatbot__history-item-when">{h.when}</span>
|
|
339
|
+
</button>
|
|
340
|
+
))
|
|
341
|
+
)}
|
|
321
342
|
</Stack>
|
|
322
343
|
</div>
|
|
323
344
|
);
|
|
@@ -526,12 +547,16 @@ export default function Chatbot({
|
|
|
526
547
|
ask,
|
|
527
548
|
onFeedback,
|
|
528
549
|
onNavigate,
|
|
550
|
+
listHistory,
|
|
551
|
+
loadConversation,
|
|
529
552
|
className = '',
|
|
530
553
|
...rest
|
|
531
554
|
}) {
|
|
532
555
|
const [messages, setMessages] = useState([]);
|
|
533
556
|
const [input, setInput] = useState('');
|
|
534
557
|
const [historyOpen, setHistoryOpen] = useState(false);
|
|
558
|
+
const [history, setHistory] = useState([]);
|
|
559
|
+
const [historyLoading, setHistoryLoading] = useState(false);
|
|
535
560
|
const bodyRef = useRef(null);
|
|
536
561
|
const streamingRef = useRef(false);
|
|
537
562
|
const lastSeedRef = useRef(undefined);
|
|
@@ -567,6 +592,60 @@ export default function Chatbot({
|
|
|
567
592
|
streamingRef.current = false;
|
|
568
593
|
}, []);
|
|
569
594
|
|
|
595
|
+
// Real history: fetch the visitor's conversations when the panel opens.
|
|
596
|
+
// Without a backend (dev preview) the canned demo entries show instead.
|
|
597
|
+
useEffect(() => {
|
|
598
|
+
if (!historyOpen) return;
|
|
599
|
+
if (!listHistory) {
|
|
600
|
+
setHistory(HISTORY.map((h) => ({ ...h, demo: true })));
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
let stale = false;
|
|
604
|
+
setHistoryLoading(true);
|
|
605
|
+
listHistory()
|
|
606
|
+
.then((items) => {
|
|
607
|
+
if (stale) return;
|
|
608
|
+
setHistory(items.map((c) => ({
|
|
609
|
+
id: c.conversationId,
|
|
610
|
+
title: c.title,
|
|
611
|
+
when: whenLabel(c.updatedAt),
|
|
612
|
+
conversation: c,
|
|
613
|
+
})));
|
|
614
|
+
})
|
|
615
|
+
.catch(() => { if (!stale) setHistory([]); })
|
|
616
|
+
.finally(() => { if (!stale) setHistoryLoading(false); });
|
|
617
|
+
return () => { stale = true; };
|
|
618
|
+
}, [historyOpen, listHistory]);
|
|
619
|
+
|
|
620
|
+
// Resume a past conversation: load its turns into the panel and thread the
|
|
621
|
+
// conversation id so follow-up asks continue it.
|
|
622
|
+
const openConversation = useCallback(
|
|
623
|
+
async (item) => {
|
|
624
|
+
setHistoryOpen(false);
|
|
625
|
+
if (!item.conversation || !loadConversation) return; // demo entry
|
|
626
|
+
cancelInFlight();
|
|
627
|
+
try {
|
|
628
|
+
const { conversationId, messages: msgs } = await loadConversation(item.conversation);
|
|
629
|
+
convoRef.current = conversationId;
|
|
630
|
+
setMessages(msgs.map((m) =>
|
|
631
|
+
m.role === 'user'
|
|
632
|
+
? { role: 'user', text: m.content }
|
|
633
|
+
: {
|
|
634
|
+
role: 'ai',
|
|
635
|
+
tokens: textToTokens(m.content),
|
|
636
|
+
sources: m.citations || [],
|
|
637
|
+
followups: [],
|
|
638
|
+
streaming: false,
|
|
639
|
+
messageId: m.messageId,
|
|
640
|
+
},
|
|
641
|
+
));
|
|
642
|
+
} catch {
|
|
643
|
+
/* keep the current chat if the load fails */
|
|
644
|
+
}
|
|
645
|
+
},
|
|
646
|
+
[loadConversation, cancelInFlight],
|
|
647
|
+
);
|
|
648
|
+
|
|
570
649
|
const send = useCallback(
|
|
571
650
|
async (text) => {
|
|
572
651
|
const prompt = String(text ?? '').trim();
|
|
@@ -756,8 +835,10 @@ export default function Chatbot({
|
|
|
756
835
|
|
|
757
836
|
{historyOpen && (
|
|
758
837
|
<HistoryPanel
|
|
838
|
+
items={history}
|
|
839
|
+
loading={historyLoading}
|
|
759
840
|
onClose={() => setHistoryOpen(false)}
|
|
760
|
-
onPick={
|
|
841
|
+
onPick={openConversation}
|
|
761
842
|
/>
|
|
762
843
|
)}
|
|
763
844
|
|
|
@@ -237,6 +237,11 @@
|
|
|
237
237
|
font-weight: var(--weight-light);
|
|
238
238
|
color: var(--muted-color);
|
|
239
239
|
}
|
|
240
|
+
.velu-chatbot__history-empty {
|
|
241
|
+
font-size: var(--f-h6);
|
|
242
|
+
color: var(--muted-color);
|
|
243
|
+
padding: var(--s-3);
|
|
244
|
+
}
|
|
240
245
|
|
|
241
246
|
/* ── Body ───────────────────────────────────────────────────────────── */
|
|
242
247
|
.velu-chatbot__body {
|
|
@@ -195,7 +195,56 @@ export function createDocsAssistant({ apiBase, host } = {}) {
|
|
|
195
195
|
});
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
-
|
|
198
|
+
// The visitor's recent conversations on this site (the history panel).
|
|
199
|
+
// Returns [{ conversationId, token, title, updatedAt }], newest first.
|
|
200
|
+
async function listConversations() {
|
|
201
|
+
await bootstrap();
|
|
202
|
+
const res = await fetch(`${root}/conversations`, {
|
|
203
|
+
credentials: 'include',
|
|
204
|
+
headers: headers(),
|
|
205
|
+
});
|
|
206
|
+
if (!res.ok) throw new Error(`ask-ai: conversations ${res.status}`);
|
|
207
|
+
const { conversations } = await res.json();
|
|
208
|
+
return (conversations || []).map((c) => ({
|
|
209
|
+
conversationId: c.conversation_id,
|
|
210
|
+
token: c.conversation_token,
|
|
211
|
+
title: c.title,
|
|
212
|
+
lastSeq: c.last_seq,
|
|
213
|
+
updatedAt: c.updated_at,
|
|
214
|
+
}));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Load a past conversation's messages (to resume it in the panel).
|
|
218
|
+
// Returns { conversationId, messages: [{ role, content, citations,
|
|
219
|
+
// messageId }] } with citations already renumbered to the Chatbot's Source
|
|
220
|
+
// shape. Also records the conversation's last seq so a follow-up `ask` on
|
|
221
|
+
// it streams from the right offset instead of replaying history.
|
|
222
|
+
async function loadConversation({ conversationId, token, lastSeq }) {
|
|
223
|
+
const res = await fetch(`${root}/conversations/${conversationId}`, {
|
|
224
|
+
credentials: 'include',
|
|
225
|
+
headers: headers({ Authorization: `Bearer ${token}` }),
|
|
226
|
+
});
|
|
227
|
+
if (!res.ok) throw new Error(`ask-ai: conversation ${res.status}`);
|
|
228
|
+
const data = await res.json();
|
|
229
|
+
let maxSeq = lastSeq ?? 0;
|
|
230
|
+
const messages = (data.messages || []).map((m) => {
|
|
231
|
+
if (m.seq != null) maxSeq = Math.max(maxSeq, m.seq);
|
|
232
|
+
if (m.role !== 'assistant') {
|
|
233
|
+
return { role: m.role, content: m.content, messageId: m.id };
|
|
234
|
+
}
|
|
235
|
+
const renum = renumberCitations(m.content || '', mapCitations(m.citations));
|
|
236
|
+
return {
|
|
237
|
+
role: 'assistant',
|
|
238
|
+
content: renum.content,
|
|
239
|
+
citations: renum.citations,
|
|
240
|
+
messageId: m.id,
|
|
241
|
+
};
|
|
242
|
+
});
|
|
243
|
+
lastSeqByConv.set(conversationId, Math.max(lastSeqByConv.get(conversationId) ?? 0, maxSeq));
|
|
244
|
+
return { conversationId, messages };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return { ask, sendFeedback, listConversations, loadConversation };
|
|
199
248
|
}
|
|
200
249
|
|
|
201
250
|
export default createDocsAssistant;
|
package/src/runtime/App.jsx
CHANGED
|
@@ -1623,6 +1623,8 @@ function DocsPage() {
|
|
|
1623
1623
|
onClose={() => setChatOpen(false)}
|
|
1624
1624
|
ask={assistant?.ask}
|
|
1625
1625
|
onFeedback={assistant?.sendFeedback}
|
|
1626
|
+
listHistory={assistant?.listConversations}
|
|
1627
|
+
loadConversation={assistant?.loadConversation}
|
|
1626
1628
|
onNavigate={(to) => {
|
|
1627
1629
|
navigate(to);
|
|
1628
1630
|
// After the destination page mounts, scroll to the cited section
|