@veluai/velu 0.2.5 → 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
|
@@ -169,6 +169,16 @@ function sourceHref(s) {
|
|
|
169
169
|
return null;
|
|
170
170
|
}
|
|
171
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
|
+
|
|
172
182
|
/* Source click: SPA-navigate same-origin targets via onNavigate (keeps the chat
|
|
173
183
|
panel open + scrolls to the heading); otherwise let the <a href> navigate. */
|
|
174
184
|
function onSourceClick(e, s, onNavigate) {
|
|
@@ -210,7 +220,7 @@ function renderStream(tokens, sources, onNavigate) {
|
|
|
210
220
|
className="velu-chatbot__cite"
|
|
211
221
|
href={sourceHref(src) || '#'}
|
|
212
222
|
onClick={(e) => onSourceClick(e, src, onNavigate)}
|
|
213
|
-
title={src
|
|
223
|
+
title={citeTitle(src)}
|
|
214
224
|
>
|
|
215
225
|
{tk.n}
|
|
216
226
|
</a>
|
|
@@ -408,7 +418,7 @@ function AiMsg({
|
|
|
408
418
|
>
|
|
409
419
|
<span className="velu-chatbot__source-num">{s.num}</span>
|
|
410
420
|
<span className="velu-chatbot__source-meta">
|
|
411
|
-
<span className="velu-chatbot__source-title">{s
|
|
421
|
+
<span className="velu-chatbot__source-title">{citeTitle(s)}</span>
|
|
412
422
|
<span className="velu-chatbot__source-path">{s.path}</span>
|
|
413
423
|
</span>
|
|
414
424
|
</a>
|
|
@@ -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
|
@@ -1623,7 +1623,21 @@ function DocsPage() {
|
|
|
1623
1623
|
onClose={() => setChatOpen(false)}
|
|
1624
1624
|
ask={assistant?.ask}
|
|
1625
1625
|
onFeedback={assistant?.sendFeedback}
|
|
1626
|
-
onNavigate={
|
|
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
|
+
}}
|
|
1627
1641
|
/>
|
|
1628
1642
|
</div>
|
|
1629
1643
|
);
|