@veluai/velu 0.2.20 → 0.2.24

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.
@@ -91,6 +91,11 @@ export function createDocsAssistant({ apiBase, host } = {}) {
91
91
  if (!base) return null;
92
92
  const root = `${base}/api/v1/public/ai-assistant`;
93
93
  let bootstrapped = false;
94
+ // Whether the docs owner's plan + credit balance allow Ask-AI right now.
95
+ // Set from /bootstrap; the host UI hides the widget when false. Fail-open
96
+ // (stays true) if bootstrap can't be reached so a transient error doesn't
97
+ // wrongly hide a working assistant.
98
+ let askEnabled = true;
94
99
  // Highest event seq seen per conversation. The events endpoint replays history
95
100
  // after `after_seq`; without this a reused (multi-turn) conversation replays
96
101
  // from 0 and the first (old) assistant.completed is returned for every
@@ -104,15 +109,33 @@ export function createDocsAssistant({ apiBase, host } = {}) {
104
109
 
105
110
  async function bootstrap() {
106
111
  if (bootstrapped) return;
107
- // Sets the httponly visitor cookie used by every subsequent call.
108
- await fetch(`${root}/bootstrap`, {
112
+ // Sets the httponly visitor cookie used by every subsequent call, and tells
113
+ // us whether Ask-AI is currently enabled for this site (plan + credits).
114
+ const res = await fetch(`${root}/bootstrap`, {
109
115
  method: 'GET',
110
116
  credentials: 'include',
111
117
  headers: headers(),
112
118
  });
119
+ try {
120
+ const data = await res.json();
121
+ if (typeof data?.ask_ai_enabled === 'boolean') askEnabled = data.ask_ai_enabled;
122
+ } catch {
123
+ /* keep fail-open default */
124
+ }
113
125
  bootstrapped = true;
114
126
  }
115
127
 
128
+ // Resolve whether the widget should be shown (runs bootstrap once). Returns
129
+ // true on any error so a transient failure never hides a working assistant.
130
+ async function isEnabled() {
131
+ try {
132
+ await bootstrap();
133
+ } catch {
134
+ return true;
135
+ }
136
+ return askEnabled;
137
+ }
138
+
116
139
  // Async generator: yields { conversationId } early, then one
117
140
  // { delta, citations, messageId, conversationId } when the answer is ready.
118
141
  async function* ask(prompt, { conversationId, signal } = {}) {
@@ -244,7 +267,7 @@ export function createDocsAssistant({ apiBase, host } = {}) {
244
267
  return { conversationId, messages };
245
268
  }
246
269
 
247
- return { ask, sendFeedback, listConversations, loadConversation };
270
+ return { ask, sendFeedback, listConversations, loadConversation, isEnabled };
248
271
  }
249
272
 
250
273
  export default createDocsAssistant;
@@ -16,7 +16,10 @@ async function loadRuntime() {
16
16
  if (_loading) return _loading;
17
17
  _loading = (async () => {
18
18
  const importer = new Function('p', 'return import(p)');
19
- const mod = await importer('/pagefind/pagefind.js');
19
+ // BASE_URL is '/' normally, '/docs/' under subpath hosting — the index is
20
+ // emitted relative to the served root, so prefix the runtime URL to match.
21
+ const base = (import.meta.env.BASE_URL || '/').replace(/\/$/, '');
22
+ const mod = await importer(`${base}/pagefind/pagefind.js`);
20
23
  if (typeof mod.init === 'function') await mod.init();
21
24
  _pf = mod;
22
25
  return mod;
@@ -7,6 +7,7 @@ import Field from './components/Field.jsx';
7
7
  import Prompt from './components/Prompt.jsx';
8
8
  import Steps, { Step } from './components/Steps.jsx';
9
9
  import Tree, { Folder, File } from './components/Tree.jsx';
10
+ import Update from './components/Update.jsx';
10
11
  import Image from './components/Image.jsx';
11
12
  import CodeBlock, { CodeGroup } from './components/CodeBlock.jsx';
12
13
  import MethodBadge from './components/MethodBadge.jsx';
@@ -91,6 +92,7 @@ export const defaultMdxComponents = {
91
92
  Tree,
92
93
  Folder,
93
94
  File,
95
+ Update,
94
96
  Image: checked('Image', Image),
95
97
  CodeBlock,
96
98
  CodeGroup,
@@ -30,6 +30,8 @@
30
30
  @import './components/prompt.css';
31
31
  @import './components/steps.css';
32
32
  @import './components/tree.css';
33
+ @import './components/update.css';
34
+ @import './components/changelog-filters.css';
33
35
  @import './components/api.css';
34
36
  @import './components/api-page.css';
35
37
  @import './components/not-found.css';
@@ -171,10 +171,6 @@
171
171
  "$ref": "#/$defs/navContainer",
172
172
  "description": "Site navigation. Mintlify-compatible: products > versions > languages > tabs > groups > pages. Switchable axes (product/version/language) become URL path prefixes; the default value of each is unprefixed. Dropdowns are intentionally unsupported."
173
173
  },
174
- "url": {
175
- "type": "string",
176
- "description": "Production site origin (e.g. \"https://docs.example.com\"), no trailing slash. Used by the static build for canonical URLs, og:url, and sitemap.xml, and by llms.txt for absolute links."
177
- },
178
174
  "thumbnails": {
179
175
  "type": "object",
180
176
  "additionalProperties": false,
@@ -20,6 +20,7 @@ export const KNOWN_COMPONENTS = [
20
20
  'Tree',
21
21
  'Folder',
22
22
  'File',
23
+ 'Update',
23
24
  'Image',
24
25
  'CodeBlock',
25
26
  'CodeGroup',
@@ -14,6 +14,7 @@ import {
14
14
  NavSelect,
15
15
  Toc,
16
16
  TocBar,
17
+ ChangelogFilters,
17
18
  ContextMenu,
18
19
  Callout,
19
20
  Accordion,
@@ -64,6 +65,12 @@ import ErrorBoundary from './ErrorBoundary.jsx';
64
65
  // preview. Identical on SSR + client in each mode, so no hydration mismatch.
65
66
  const IS_DEV_PREVIEW = import.meta.env.DEV;
66
67
 
68
+ // Subpath-hosting prefix (e.g. "/docs"), from Vite's BASE_URL. Empty at the
69
+ // origin root. The router handles internal <Link>s on its own, but server-served
70
+ // resources the router doesn't own — the .md twins, the /mcp endpoint — are
71
+ // root-relative and need this prefix to resolve under a subpath.
72
+ const BASE_PATH = (import.meta.env.BASE_URL || '/').replace(/\/$/, '');
73
+
67
74
  // Flatten a sidebar section's items (which may nest into sub-groups) into the
68
75
  // flat endpoint list <ApiSidebar> wants. API tabs are flat in practice; this
69
76
  // only guards the rare case of a normal nested group living beside generated
@@ -780,6 +787,19 @@ function DocsPage() {
780
787
  return createDocsAssistant({ apiBase, host: site.assistant?.host });
781
788
  }, []);
782
789
 
790
+ // Hide the Ask-AI widget when the docs owner's plan/credits disable it (the
791
+ // backend bootstrap returns ask_ai_enabled:false). Only applies to a real
792
+ // configured assistant — the dev-preview/demo widget stays visible.
793
+ const [askAiHidden, setAskAiHidden] = React.useState(false);
794
+ React.useEffect(() => {
795
+ if (!assistant) return undefined;
796
+ let alive = true;
797
+ assistant.isEnabled().then((ok) => {
798
+ if (alive && !ok) setAskAiHidden(true);
799
+ }).catch(() => {});
800
+ return () => { alive = false; };
801
+ }, [assistant]);
802
+
783
803
  // Page-feedback backend: reuses the same injected apiBase as the assistant
784
804
  // (baked into every published velu.json), so the "Was this page helpful?"
785
805
  // widget can POST to /api/v1/public/feedback. Null in the dev preview, where
@@ -794,6 +814,11 @@ function DocsPage() {
794
814
  const frontmatter = entry?.frontmatter ?? {};
795
815
  const PageComponent = entry?.Component ?? null;
796
816
  const pageToc = entry?.toc ?? [];
817
+ // Changelog pages (any page with <Update> entries) carry a `changelog`
818
+ // export. We swap the right-rail TOC for tag filters and expose an RSS link.
819
+ const changelog = entry?.changelog ?? null;
820
+ const isChangelog = !!changelog;
821
+ const changelogTags = changelog?.tags ?? [];
797
822
  // No renderable page for this route → show the 404 page (a clean centered
798
823
  // takeover: header + footer stay, the docs sidebar/TOC are hidden).
799
824
  const isNotFound = !PageComponent;
@@ -911,6 +936,14 @@ function DocsPage() {
911
936
  document.removeEventListener('keydown', onKey);
912
937
  };
913
938
  }, [navOpen]);
939
+
940
+ // Close the mobile drawer (and its nav dropdown) whenever the route changes,
941
+ // so picking a page from the sidebar collapses the drawer instead of leaving
942
+ // it open over the article.
943
+ React.useEffect(() => {
944
+ setDrawerOpen(false);
945
+ setNavOpen(false);
946
+ }, [pathname]);
914
947
  // Mobile drawer's section picker = the resolved tabs (top-level
915
948
  // navigation), so it mirrors the desktop tabs row instead of a
916
949
  // hardcoded list. Its button shows the active tab's label.
@@ -1197,8 +1230,9 @@ function DocsPage() {
1197
1230
  onSelect={(item) => item.href && navigate(item.href)}
1198
1231
  />
1199
1232
  {/* Ask AI talks to the deployed site's AI backend — hidden in
1200
- the local dev preview where there's nothing to talk to. */}
1201
- {!IS_DEV_PREVIEW && (
1233
+ the local dev preview where there's nothing to talk to, and
1234
+ when the owner's plan/credits disable the assistant. */}
1235
+ {!IS_DEV_PREVIEW && !askAiHidden && (
1202
1236
  <button
1203
1237
  type="button"
1204
1238
  className="velu-header__action velu-header__action--outlined"
@@ -1453,6 +1487,8 @@ function DocsPage() {
1453
1487
  samples={entry.samples}
1454
1488
  responses={entry.operation.responses}
1455
1489
  />
1490
+ ) : isChangelog && changelogTags.length ? (
1491
+ <ChangelogFilters tags={changelogTags} />
1456
1492
  ) : (
1457
1493
  <Toc items={toc} activeId={activeId} onSelect={scrollTo} />
1458
1494
  )}
@@ -1474,7 +1510,7 @@ function DocsPage() {
1474
1510
  new KeyboardEvent('keydown', { key: 'k', ctrlKey: true, metaKey: true }),
1475
1511
  )
1476
1512
  }
1477
- onAskAI={IS_DEV_PREVIEW ? undefined : () => askAI('')}
1513
+ onAskAI={IS_DEV_PREVIEW || askAiHidden ? undefined : () => askAI('')}
1478
1514
  />
1479
1515
  </main>
1480
1516
  ) : (
@@ -1485,6 +1521,8 @@ function DocsPage() {
1485
1521
  right-rail <Toc> so both views are driven by the same
1486
1522
  data; whichever one is visible at a given width responds
1487
1523
  to the same scroll-spy state. */}
1524
+ {/* Changelog tag filters are a wide-layout (right-rail) affordance only;
1525
+ on tab/mobile we keep the normal TOC bar (no filters). */}
1488
1526
  <TocBar items={toc} activeId={activeId} onSelect={scrollTo} />
1489
1527
  <main
1490
1528
  className="velu-docs-layout__main"
@@ -1533,12 +1571,14 @@ function DocsPage() {
1533
1571
  : undefined
1534
1572
  }
1535
1573
  pageUrl={pathname}
1574
+ basePath={BASE_PATH}
1536
1575
  title={frontmatter.title}
1537
1576
  isApi={entry?.api === true}
1538
1577
  siteName={site.name}
1539
1578
  options={site.contextual?.options ?? []}
1540
- onAssistant={IS_DEV_PREVIEW ? undefined : () => askAI('')}
1579
+ onAssistant={IS_DEV_PREVIEW || askAiHidden ? undefined : () => askAI('')}
1541
1580
  getMarkdown={entry?.getSource}
1581
+ rssHref={isChangelog ? `${BASE_PATH}/rss.xml` : undefined}
1542
1582
  />
1543
1583
 
1544
1584
  {/* Page hero from MDX frontmatter. `.velu-hero` rules
@@ -1614,7 +1654,7 @@ function DocsPage() {
1614
1654
  approaches (see IntersectionObserver above). */}
1615
1655
  {/* Ask-a-question bar — opens the AI chatbot, which runs on the
1616
1656
  deployed site. Hidden in the local dev preview. */}
1617
- {!IS_DEV_PREVIEW && !chatOpen && (
1657
+ {!IS_DEV_PREVIEW && !chatOpen && !askAiHidden && (
1618
1658
  <AskBar
1619
1659
  onSubmit={askAI}
1620
1660
  style={{
@@ -1672,7 +1712,9 @@ function DocsPage() {
1672
1712
  </div>
1673
1713
  )}
1674
1714
 
1675
- {/* Ask-AI chatbot — slides in from the inline-end edge. */}
1715
+ {/* Ask-AI chatbot — slides in from the inline-end edge. Hidden when the
1716
+ docs owner's plan/credits disable Ask-AI. */}
1717
+ {!askAiHidden && (
1676
1718
  <Chatbot
1677
1719
  open={chatOpen}
1678
1720
  seedQuestion={chatQuestion}
@@ -1698,6 +1740,7 @@ function DocsPage() {
1698
1740
  setTimeout(go, 50);
1699
1741
  }}
1700
1742
  />
1743
+ )}
1701
1744
  </div>
1702
1745
  );
1703
1746
  }
@@ -4,9 +4,14 @@ import { BrowserRouter } from 'react-router-dom';
4
4
  import 'velu-ui/styles.css';
5
5
  import App from './App.jsx';
6
6
 
7
+ // Subpath hosting: Vite sets BASE_URL from the build's `base` (e.g. "/docs/").
8
+ // React Router wants a basename WITHOUT the trailing slash ("/docs"); the root
9
+ // case ("/") becomes undefined so the router behaves as if mounted at origin.
10
+ const basename = (import.meta.env.BASE_URL || '/').replace(/\/$/, '') || undefined;
11
+
7
12
  hydrateRoot(
8
13
  document.getElementById('root'),
9
- <BrowserRouter>
14
+ <BrowserRouter basename={basename}>
10
15
  <App />
11
16
  </BrowserRouter>
12
17
  );
@@ -7,9 +7,17 @@ import { StaticRouter } from 'react-router-dom/server';
7
7
  import 'velu-ui/styles.css';
8
8
  import App from './App.jsx';
9
9
 
10
+ // Subpath hosting: mirror the client's basename (from Vite's BASE_URL). The
11
+ // prerender is driven by root-relative route paths ("/foo"), so the location
12
+ // handed to StaticRouter must carry the basename prefix ("/docs/foo") for it to
13
+ // match — and so the rendered <Link> hrefs come out prefixed too, matching what
14
+ // the hydrated BrowserRouter produces.
15
+ const basename = (import.meta.env.BASE_URL || '/').replace(/\/$/, '');
16
+
10
17
  export async function render(url) {
18
+ const location = basename ? `${basename}${url}` : url;
11
19
  return renderToString(
12
- <StaticRouter location={url}>
20
+ <StaticRouter basename={basename || undefined} location={location}>
13
21
  <App />
14
22
  </StaticRouter>
15
23
  );
@@ -16,16 +16,16 @@ save.
16
16
  ## Get going
17
17
 
18
18
  <CardGroup>
19
- <Card title="Quickstart" icon="rocket" href="/quickstart">
19
+ <Card title="Quickstart" icon="rocket" cta={{ label: "Get started", href: "/quickstart" }}>
20
20
  Run the dev server and make your first edit.
21
21
  </Card>
22
- <Card title="Customize" icon="sliders-horizontal" href="/essentials/settings">
22
+ <Card title="Customize" icon="sliders-horizontal" cta={{ label: "Customize", href: "/essentials/settings" }}>
23
23
  Set your colors, font, and favicon.
24
24
  </Card>
25
- <Card title="Write content" icon="pen-line" href="/essentials/markdown">
25
+ <Card title="Write content" icon="pen-line" cta={{ label: "Write content", href: "/essentials/markdown" }}>
26
26
  Markdown plus a library of components.
27
27
  </Card>
28
- <Card title="API reference" icon="terminal" href="/api-reference/introduction">
28
+ <Card title="API reference" icon="terminal" cta={{ label: "View reference", href: "/api-reference/introduction" }}>
29
29
  Document your endpoints.
30
30
  </Card>
31
31
  </CardGroup>
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "$schema": "https://veludocs.com/velu.schema.json",
3
3
  "name": "Starter",
4
- "url": "https://veludocs.com",
5
4
  "colors": {
6
5
  "primary": "#dc143c"
7
6
  },