@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veluai/velu",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "type": "module",
5
5
  "bin": "./dist/cli.js",
6
6
  "publishConfig": {
@@ -243,16 +243,20 @@
243
243
  margin-block-start: var(--s5);
244
244
  }
245
245
 
246
- /* Sidebars / TOC scroll container: discreet thin scrollbar with a
247
- tokenised colour in every state.
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
- `scrollbar-color` consistently, and the in-between interpolation +
251
- Firefox's auto-derived hover/active colours can flash a yellow/amber
252
- accent (it picks an OS / system accent when interpolating). Hard
253
- switches avoid that. Same reason every WebKit thumb pseudo-state is
254
- set explicitly otherwise Chromium can fall back to the Windows 11
255
- system accent on hover. */
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?.title}
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.title}</span>
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: m.content || '',
135
- citations: mapCitations(m.citations),
160
+ delta: renum.content,
161
+ citations: renum.citations,
136
162
  messageId: m.id,
137
163
  conversationId: convId,
138
164
  };
@@ -1623,7 +1623,21 @@ function DocsPage() {
1623
1623
  onClose={() => setChatOpen(false)}
1624
1624
  ask={assistant?.ask}
1625
1625
  onFeedback={assistant?.sendFeedback}
1626
- onNavigate={navigate}
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
  );