@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veluai/velu",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "type": "module",
5
5
  "bin": "./dist/cli.js",
6
6
  "publishConfig": {
@@ -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
  );