@veluai/velu 0.2.18 → 0.2.20

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.18",
3
+ "version": "0.2.20",
4
4
  "type": "module",
5
5
  "bin": "./dist/cli.js",
6
6
  "publishConfig": {
@@ -61,11 +61,11 @@ const REGISTRY = {
61
61
  aistudio: { icon: Sparkles, label: 'Open in AI Studio', desc: 'Ask questions about this page', kind: 'ai', external: true, url: (q) => `https://aistudio.google.com/app/prompts/new_chat?prompt=${q}` },
62
62
  devin: { icon: Bot, label: 'Open in Devin', desc: 'Ask questions about this page', kind: 'ai', external: true, url: (q) => `https://app.devin.ai/?prompt=${q}` },
63
63
  windsurf: { icon: Wind, label: 'Open in Windsurf', desc: 'Ask questions about this page', kind: 'ai', external: true, url: (q) => `https://windsurf.com/?q=${q}` },
64
- mcp: { icon: Terminal, label: 'Copy MCP install command', desc: 'Copy npx command to install MCP server', kind: 'mcp' },
64
+ mcp: { icon: Terminal, label: 'Copy MCP install command', desc: 'Copy command to install the MCP server', kind: 'mcp' },
65
65
  'add-mcp': { icon: Terminal, label: 'Add MCP server', desc: 'Copy command to add the MCP server', kind: 'mcp' },
66
66
  cursor: { icon: CursorIcon, label: 'Connect to Cursor', desc: 'Install MCP Server on Cursor', kind: 'cursor', external: true },
67
67
  vscode: { icon: VscodeIcon, label: 'Connect to VS Code', desc: 'Install MCP Server on VS Code', kind: 'vscode', external: true },
68
- 'devin-mcp': { icon: Terminal, label: 'Connect to Devin', desc: 'Install MCP Server on Devin', kind: 'cursor', external: true },
68
+ 'devin-mcp': { icon: Terminal, label: 'Connect to Devin', desc: 'Copy command to add the MCP server on Devin', kind: 'devin' },
69
69
  };
70
70
 
71
71
  export default function ContextMenu({
@@ -166,6 +166,9 @@ export default function ContextMenu({
166
166
  setOpen(false);
167
167
  const origin = window.location.origin;
168
168
  const absMd = origin + mdUrl;
169
+ const mcpUrl = `${origin}/mcp`;
170
+ // CLI-safe server name (siteName may contain spaces).
171
+ const serverName = (siteName || 'docs').trim().replace(/\s+/g, '-').toLowerCase() || 'docs';
169
172
  switch (it.kind) {
170
173
  case 'copy':
171
174
  return copyPage();
@@ -192,12 +195,12 @@ export default function ContextMenu({
192
195
  return void window.open(it.url(q), '_blank', 'noopener');
193
196
  }
194
197
  case 'mcp':
195
- // Stubbed: the Velu MCP server isn't hosted yet this is the intended
196
- // command, wired to <origin>/mcp once it exists.
197
- return copyText(`npx -y @veluai/mcp@latest ${origin}/mcp`);
198
+ // Hosted HTTP MCP server lives at <origin>/mcp. Copy a client-neutral
199
+ // command: `mcp-remote` bridges the HTTP server to stdio, so it works
200
+ // in any MCP client config (Claude, Cursor, VS Code, Windsurf, …).
201
+ return copyText(`npx mcp-remote ${mcpUrl}`);
198
202
  case 'cursor': {
199
- const cfg =
200
- typeof btoa === 'function' ? btoa(JSON.stringify({ url: `${origin}/mcp` })) : '';
203
+ const cfg = typeof btoa === 'function' ? btoa(JSON.stringify({ url: mcpUrl })) : '';
201
204
  return void window.open(
202
205
  `cursor://anysphere.cursor-deeplink/mcp/install?name=${encodeURIComponent(siteName)}&config=${cfg}`,
203
206
  '_blank',
@@ -205,13 +208,16 @@ export default function ContextMenu({
205
208
  );
206
209
  }
207
210
  case 'vscode': {
208
- const cfg = encodeURIComponent(JSON.stringify({ name: siteName, url: `${origin}/mcp` }));
209
- return void window.open(
210
- `https://insiders.vscode.dev/redirect/mcp/install?${cfg}`,
211
- '_blank',
212
- 'noopener',
211
+ // Official VS Code install URI: `vscode:mcp/install?<urlencoded JSON>`,
212
+ // with the transport `type` inside the payload.
213
+ const cfg = encodeURIComponent(
214
+ JSON.stringify({ name: siteName, type: 'http', url: mcpUrl }),
213
215
  );
216
+ return void window.open(`vscode:mcp/install?${cfg}`, '_blank', 'noopener');
214
217
  }
218
+ case 'devin':
219
+ // Devin has no install deep-link — copy its CLI add command instead.
220
+ return copyText(`devin mcp add ${serverName} ${mcpUrl}`);
215
221
  case 'custom':
216
222
  return void window.open(it.href, '_blank', 'noopener');
217
223
  default:
@@ -510,7 +510,10 @@
510
510
  opacity: 0;
511
511
  transition: opacity 0.12s ease;
512
512
  }
513
- .velu-chatbot__msg--ai:hover .velu-chatbot__msg-actions {
513
+ .velu-chatbot__msg--ai:hover .velu-chatbot__msg-actions,
514
+ /* Keep the row visible once a vote is cast, so the selection stays
515
+ confirmed after the pointer leaves the message. */
516
+ .velu-chatbot__msg-actions:has(.is-active) {
514
517
  opacity: 1;
515
518
  }
516
519
  .velu-chatbot__msg-action {
@@ -530,6 +533,11 @@
530
533
  background: var(--surface-color);
531
534
  color: var(--text-color);
532
535
  }
536
+ /* Chosen thumb (up/down) — accent tint so the click is clearly registered. */
537
+ .velu-chatbot__msg-action.is-active {
538
+ background: color-mix(in srgb, var(--accent-color) 12%, transparent);
539
+ color: var(--accent-color);
540
+ }
533
541
 
534
542
  /* ── Composer ───────────────────────────────────────────────────────── */
535
543
  .velu-chatbot__composer {
@@ -45,6 +45,7 @@ export {
45
45
  export { default as Search } from './components/Search.jsx';
46
46
  export { default as pagefindSearch } from './lib/pagefind.js';
47
47
  export { createDocsAssistant } from './lib/docs-assistant.js';
48
+ export { createPageFeedback } from './lib/page-feedback.js';
48
49
  export { default as Image } from './components/Image.jsx';
49
50
  export {
50
51
  default as CodeBlock,
@@ -0,0 +1,72 @@
1
+ /**
2
+ * page-feedback — runtime client for the "Was this page helpful?" widget on a
3
+ * published docs site. POSTs to the backend's PUBLIC feedback API:
4
+ *
5
+ * POST /api/v1/public/feedback ({ page_url, helpful, reason_text, details })
6
+ *
7
+ * Same contract as docs-assistant: the tenant is resolved from the
8
+ * `X-Velu-Site-Host` header (falls back to Origin), and the httponly visitor
9
+ * cookie rides along via `credentials: 'include'` (so the API must be same-site
10
+ * with the docs — true for *.getvelu.com subdomains).
11
+ *
12
+ * `createPageFeedback({ apiBase, host })` returns `{ submit }`, shaped to wire
13
+ * straight into <PageFeedback onVote onSubmit>. Returns null without an apiBase
14
+ * (dev preview / unconfigured site) so the widget stays purely visual.
15
+ */
16
+
17
+ const trimSlash = (s) => String(s || '').replace(/\/+$/, '');
18
+
19
+ function siteHost(explicit) {
20
+ if (explicit) return explicit;
21
+ return typeof window !== 'undefined' ? window.location.host : '';
22
+ }
23
+
24
+ // The page the feedback is about. Drop hash + query here; the backend
25
+ // normalizes again server-side so per-page aggregation stays clean.
26
+ function currentPageUrl() {
27
+ if (typeof window === 'undefined') return '';
28
+ const { origin, pathname } = window.location;
29
+ return origin + pathname;
30
+ }
31
+
32
+ export function createPageFeedback({ apiBase, host } = {}) {
33
+ const base = trimSlash(apiBase);
34
+ if (!base) return null;
35
+ const url = `${base}/api/v1/public/feedback`;
36
+
37
+ /**
38
+ * Send one feedback submission.
39
+ * - helpful: boolean (Yes / No)
40
+ * - reason: short label from the "No" form (optional)
41
+ * - detail: free-text elaboration (optional)
42
+ * Best-effort: a failed request never disrupts the reader.
43
+ */
44
+ async function submit({ helpful, reason, detail } = {}) {
45
+ const body = {
46
+ page_url: currentPageUrl(),
47
+ helpful: !!helpful,
48
+ // reason_text is required by the API (min length 1); fall back to a
49
+ // sensible default when the reader didn't pick a reason (e.g. on "Yes").
50
+ reason_text:
51
+ (reason && String(reason).trim()) || (helpful ? 'Helpful' : 'Not helpful'),
52
+ details: detail ? String(detail) : null,
53
+ };
54
+ try {
55
+ await fetch(url, {
56
+ method: 'POST',
57
+ credentials: 'include',
58
+ headers: {
59
+ 'Content-Type': 'application/json',
60
+ 'X-Velu-Site-Host': siteHost(host),
61
+ },
62
+ body: JSON.stringify(body),
63
+ });
64
+ } catch {
65
+ /* feedback is best-effort — swallow network errors */
66
+ }
67
+ }
68
+
69
+ return { submit };
70
+ }
71
+
72
+ export default createPageFeedback;
@@ -41,6 +41,7 @@ import {
41
41
  Search,
42
42
  pagefindSearch,
43
43
  createDocsAssistant,
44
+ createPageFeedback,
44
45
  Logo,
45
46
  SocialLinks,
46
47
  Tree,
@@ -779,6 +780,17 @@ function DocsPage() {
779
780
  return createDocsAssistant({ apiBase, host: site.assistant?.host });
780
781
  }, []);
781
782
 
783
+ // Page-feedback backend: reuses the same injected apiBase as the assistant
784
+ // (baked into every published velu.json), so the "Was this page helpful?"
785
+ // widget can POST to /api/v1/public/feedback. Null in the dev preview, where
786
+ // the widget stays purely visual.
787
+ const pageFeedback = React.useMemo(() => {
788
+ if (IS_DEV_PREVIEW) return null;
789
+ const apiBase = site.assistant?.apiBase;
790
+ if (!apiBase) return null;
791
+ return createPageFeedback({ apiBase, host: site.assistant?.host });
792
+ }, []);
793
+
782
794
  const frontmatter = entry?.frontmatter ?? {};
783
795
  const PageComponent = entry?.Component ?? null;
784
796
  const pageToc = entry?.toc ?? [];
@@ -1572,7 +1584,18 @@ function DocsPage() {
1572
1584
  {/* Page-foot feedback widget — ref'd so the sticky AskBar
1573
1585
  above can hide as the user scrolls near it. */}
1574
1586
  <div ref={feedbackRef} style={{ marginTop: 'var(--s3)' }}>
1575
- <PageFeedback />
1587
+ {/* key resets the widget's vote state on page change. "Yes"
1588
+ submits immediately; "No" submits once its form is sent.
1589
+ Handlers are no-ops in the dev preview (pageFeedback null). */}
1590
+ <PageFeedback
1591
+ key={pathname}
1592
+ onVote={(v) => {
1593
+ if (v === 'yes') pageFeedback?.submit({ helpful: true });
1594
+ }}
1595
+ onSubmit={({ reason, detail }) =>
1596
+ pageFeedback?.submit({ helpful: false, reason, detail })
1597
+ }
1598
+ />
1576
1599
  </div>
1577
1600
  {/* Previous / next page navigation — derived from the
1578
1601
  sidebar reading order of the active section. */}