@veluai/velu 0.2.5 → 0.2.7
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
package/runtime/velu-ui/base.css
CHANGED
|
@@ -243,16 +243,20 @@
|
|
|
243
243
|
margin-block-start: var(--s5);
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
-
/*
|
|
247
|
-
|
|
246
|
+
/* Discreet thin scrollbar with a tokenised colour in every state — applied
|
|
247
|
+
GLOBALLY (the `*` selector) so every scrollable element inherits it by
|
|
248
|
+
default: the chatbot, dialogs, and any future component, with no
|
|
249
|
+
per-component opt-in. Component-specific scrollbar rules win via
|
|
250
|
+
specificity (e.g. the nav/TOC reveal-on-hover overrides below).
|
|
251
|
+
`.velu-hide-scrollbar` stays as an explicit alias for existing markup.
|
|
248
252
|
|
|
249
|
-
No transitions: Firefox doesn't honour CSS transitions on
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
253
|
+
No transitions: Firefox doesn't honour CSS transitions on `scrollbar-color`
|
|
254
|
+
consistently, and the in-between interpolation + its auto-derived
|
|
255
|
+
hover/active colours can flash a yellow/amber accent (an OS / system accent
|
|
256
|
+
when interpolating). Hard switches avoid that. Same reason every WebKit
|
|
257
|
+
thumb pseudo-state is set explicitly — otherwise Chromium can fall back to
|
|
258
|
+
the Windows 11 system accent on hover. */
|
|
259
|
+
*,
|
|
256
260
|
.velu-hide-scrollbar {
|
|
257
261
|
scrollbar-width: thin;
|
|
258
262
|
/* Single colour both at rest and on hover. Firefox auto-darkens on
|
|
@@ -260,17 +264,22 @@
|
|
|
260
264
|
derived shade in the same family (no yellow flash). */
|
|
261
265
|
scrollbar-color: var(--border-color) transparent;
|
|
262
266
|
}
|
|
267
|
+
*::-webkit-scrollbar,
|
|
263
268
|
.velu-hide-scrollbar::-webkit-scrollbar {
|
|
264
269
|
inline-size: 6px;
|
|
265
270
|
block-size: 6px;
|
|
266
271
|
}
|
|
272
|
+
*::-webkit-scrollbar-track,
|
|
267
273
|
.velu-hide-scrollbar::-webkit-scrollbar-track {
|
|
268
274
|
background: transparent;
|
|
269
275
|
}
|
|
276
|
+
*::-webkit-scrollbar-thumb,
|
|
270
277
|
.velu-hide-scrollbar::-webkit-scrollbar-thumb {
|
|
271
278
|
background: var(--border-color);
|
|
272
279
|
border-radius: 999px;
|
|
273
280
|
}
|
|
281
|
+
*::-webkit-scrollbar-thumb:hover,
|
|
282
|
+
*::-webkit-scrollbar-thumb:active,
|
|
274
283
|
.velu-hide-scrollbar::-webkit-scrollbar-thumb:hover,
|
|
275
284
|
.velu-hide-scrollbar::-webkit-scrollbar-thumb:active {
|
|
276
285
|
background: var(--muted-color);
|
|
@@ -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
|
);
|