@veluai/velu 0.2.4 → 0.2.6
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
|
@@ -160,8 +160,42 @@ function tokenizeAnswer(answer) {
|
|
|
160
160
|
return tokens;
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
/* A citation's in-site target. Real backend sources carry a `url` (and `path`
|
|
164
|
+
is a route like "/essentials/code"); the demo's `path` is a fake file path,
|
|
165
|
+
so those stay inert. */
|
|
166
|
+
function sourceHref(s) {
|
|
167
|
+
if (s?.url) return s.url;
|
|
168
|
+
if (s?.path && s.path.startsWith('/')) return s.path;
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* A readable label for a source: the section heading if present, else a title
|
|
173
|
+
derived from the route (e.g. "/essentials/settings" → "Settings"). */
|
|
174
|
+
function citeTitle(s) {
|
|
175
|
+
if (s?.title) return s.title;
|
|
176
|
+
const path = (s?.path || '').replace(/[#?].*$/, '');
|
|
177
|
+
const seg = path.split('/').filter(Boolean).pop();
|
|
178
|
+
if (!seg) return 'Documentation';
|
|
179
|
+
return seg.replace(/[-_]/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/* Source click: SPA-navigate same-origin targets via onNavigate (keeps the chat
|
|
183
|
+
panel open + scrolls to the heading); otherwise let the <a href> navigate. */
|
|
184
|
+
function onSourceClick(e, s, onNavigate) {
|
|
185
|
+
const href = sourceHref(s);
|
|
186
|
+
if (!href) { e.preventDefault(); return; }
|
|
187
|
+
if (!onNavigate) return; // no SPA hook → let the browser follow the href
|
|
188
|
+
try {
|
|
189
|
+
const u = new URL(href, window.location.origin);
|
|
190
|
+
if (u.origin === window.location.origin) {
|
|
191
|
+
e.preventDefault();
|
|
192
|
+
onNavigate(u.pathname + u.search + u.hash);
|
|
193
|
+
}
|
|
194
|
+
} catch { /* malformed — fall back to href */ }
|
|
195
|
+
}
|
|
196
|
+
|
|
163
197
|
/* Render partial tokens as paragraphs (split on \n\n, single \n → <br>). */
|
|
164
|
-
function renderStream(tokens, sources) {
|
|
198
|
+
function renderStream(tokens, sources, onNavigate) {
|
|
165
199
|
const paragraphs = [[]];
|
|
166
200
|
for (const tk of tokens) {
|
|
167
201
|
if (tk.kind === 't' && tk.v.includes('\n\n')) {
|
|
@@ -179,13 +213,14 @@ function renderStream(tokens, sources) {
|
|
|
179
213
|
<p key={pi}>
|
|
180
214
|
{para.map((tk, i) => {
|
|
181
215
|
if (tk.kind === 'cite') {
|
|
216
|
+
const src = sources?.find((s) => s.num === tk.n);
|
|
182
217
|
return (
|
|
183
218
|
<a
|
|
184
219
|
key={i}
|
|
185
220
|
className="velu-chatbot__cite"
|
|
186
|
-
href=
|
|
187
|
-
onClick={(e) => e
|
|
188
|
-
title={
|
|
221
|
+
href={sourceHref(src) || '#'}
|
|
222
|
+
onClick={(e) => onSourceClick(e, src, onNavigate)}
|
|
223
|
+
title={citeTitle(src)}
|
|
189
224
|
>
|
|
190
225
|
{tk.n}
|
|
191
226
|
</a>
|
|
@@ -331,6 +366,7 @@ function AiMsg({
|
|
|
331
366
|
onFollowup,
|
|
332
367
|
messageId,
|
|
333
368
|
onFeedback,
|
|
369
|
+
onNavigate,
|
|
334
370
|
}) {
|
|
335
371
|
const thinking = tokens.length === 0;
|
|
336
372
|
const [vote, setVote] = useState(null); // 'up' | 'down' | null
|
|
@@ -367,7 +403,7 @@ function AiMsg({
|
|
|
367
403
|
|
|
368
404
|
{!thinking && (
|
|
369
405
|
<div className="velu-chatbot__answer">
|
|
370
|
-
{renderStream(tokens, sources)}
|
|
406
|
+
{renderStream(tokens, sources, onNavigate)}
|
|
371
407
|
{streaming && <span className="velu-chatbot__caret" />}
|
|
372
408
|
|
|
373
409
|
{!streaming && sources?.length > 0 && (
|
|
@@ -377,12 +413,12 @@ function AiMsg({
|
|
|
377
413
|
<a
|
|
378
414
|
key={s.num}
|
|
379
415
|
className="velu-chatbot__source"
|
|
380
|
-
href=
|
|
381
|
-
onClick={(e) => e
|
|
416
|
+
href={sourceHref(s) || '#'}
|
|
417
|
+
onClick={(e) => onSourceClick(e, s, onNavigate)}
|
|
382
418
|
>
|
|
383
419
|
<span className="velu-chatbot__source-num">{s.num}</span>
|
|
384
420
|
<span className="velu-chatbot__source-meta">
|
|
385
|
-
<span className="velu-chatbot__source-title">{s
|
|
421
|
+
<span className="velu-chatbot__source-title">{citeTitle(s)}</span>
|
|
386
422
|
<span className="velu-chatbot__source-path">{s.path}</span>
|
|
387
423
|
</span>
|
|
388
424
|
</a>
|
|
@@ -489,6 +525,7 @@ export default function Chatbot({
|
|
|
489
525
|
onClose,
|
|
490
526
|
ask,
|
|
491
527
|
onFeedback,
|
|
528
|
+
onNavigate,
|
|
492
529
|
className = '',
|
|
493
530
|
...rest
|
|
494
531
|
}) {
|
|
@@ -741,6 +778,7 @@ export default function Chatbot({
|
|
|
741
778
|
streaming={m.streaming}
|
|
742
779
|
messageId={m.messageId}
|
|
743
780
|
onFeedback={onFeedback}
|
|
781
|
+
onNavigate={onNavigate}
|
|
744
782
|
onFollowup={(f) => send(f)}
|
|
745
783
|
/>
|
|
746
784
|
),
|
|
@@ -61,6 +61,31 @@ function mapCitations(cits) {
|
|
|
61
61
|
}));
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
// Renumber cited sources 1,2,3… in order of first appearance and rewrite the
|
|
65
|
+
// [n] markers in the text to match. The backend keeps the raw RAG ranks (the
|
|
66
|
+
// LLM may cite [1] and [5]); this makes them sequential and drops uncited ones.
|
|
67
|
+
function renumberCitations(content, citations) {
|
|
68
|
+
const text = content || '';
|
|
69
|
+
const order = [];
|
|
70
|
+
const re = /\[(\d+)\]/g;
|
|
71
|
+
let m;
|
|
72
|
+
while ((m = re.exec(text)) !== null) {
|
|
73
|
+
const n = Number(m[1]);
|
|
74
|
+
if (!order.includes(n)) order.push(n);
|
|
75
|
+
}
|
|
76
|
+
if (!order.length) return { content: text, citations: [] };
|
|
77
|
+
const remap = new Map(order.map((old, i) => [old, i + 1]));
|
|
78
|
+
const newContent = text.replace(/\[(\d+)\]/g, (full, d) =>
|
|
79
|
+
remap.has(Number(d)) ? `[${remap.get(Number(d))}]` : full,
|
|
80
|
+
);
|
|
81
|
+
const byNum = new Map((citations || []).map((c) => [c.num, c]));
|
|
82
|
+
const newCitations = order
|
|
83
|
+
.map((old) => byNum.get(old))
|
|
84
|
+
.filter(Boolean)
|
|
85
|
+
.map((c, i) => ({ ...c, num: i + 1 }));
|
|
86
|
+
return { content: newContent, citations: newCitations };
|
|
87
|
+
}
|
|
88
|
+
|
|
64
89
|
export function createDocsAssistant({ apiBase, host } = {}) {
|
|
65
90
|
const base = trimSlash(apiBase);
|
|
66
91
|
if (!base) return null;
|
|
@@ -130,9 +155,10 @@ export function createDocsAssistant({ apiBase, host } = {}) {
|
|
|
130
155
|
}
|
|
131
156
|
if (event === 'assistant.completed') {
|
|
132
157
|
const m = data?.message || {};
|
|
158
|
+
const renum = renumberCitations(m.content || '', mapCitations(m.citations));
|
|
133
159
|
yield {
|
|
134
|
-
delta:
|
|
135
|
-
citations:
|
|
160
|
+
delta: renum.content,
|
|
161
|
+
citations: renum.citations,
|
|
136
162
|
messageId: m.id,
|
|
137
163
|
conversationId: convId,
|
|
138
164
|
};
|
package/src/runtime/App.jsx
CHANGED
|
@@ -1558,7 +1558,7 @@ function DocsPage() {
|
|
|
1558
1558
|
approaches (see IntersectionObserver above). */}
|
|
1559
1559
|
{/* Ask-a-question bar — opens the AI chatbot, which runs on the
|
|
1560
1560
|
deployed site. Hidden in the local dev preview. */}
|
|
1561
|
-
{!IS_DEV_PREVIEW && (
|
|
1561
|
+
{!IS_DEV_PREVIEW && !chatOpen && (
|
|
1562
1562
|
<AskBar
|
|
1563
1563
|
onSubmit={askAI}
|
|
1564
1564
|
style={{
|
|
@@ -1623,6 +1623,21 @@ function DocsPage() {
|
|
|
1623
1623
|
onClose={() => setChatOpen(false)}
|
|
1624
1624
|
ask={assistant?.ask}
|
|
1625
1625
|
onFeedback={assistant?.sendFeedback}
|
|
1626
|
+
onNavigate={(to) => {
|
|
1627
|
+
navigate(to);
|
|
1628
|
+
// After the destination page mounts, scroll to the cited section
|
|
1629
|
+
// (its heading id) — or the top of the page when there's no anchor.
|
|
1630
|
+
const hash = to.includes('#') ? to.slice(to.indexOf('#') + 1) : '';
|
|
1631
|
+
let tries = 0;
|
|
1632
|
+
const go = () => {
|
|
1633
|
+
if (hash) {
|
|
1634
|
+
if (document.getElementById(hash)) { scrollTo(hash); return; }
|
|
1635
|
+
if (tries++ < 10) { setTimeout(go, 60); return; }
|
|
1636
|
+
}
|
|
1637
|
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
1638
|
+
};
|
|
1639
|
+
setTimeout(go, 50);
|
|
1640
|
+
}}
|
|
1626
1641
|
/>
|
|
1627
1642
|
</div>
|
|
1628
1643
|
);
|