@aravindc26/velu 0.8.0 → 0.9.1
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/README.md +7 -0
- package/package.json +19 -7
- package/src/build.ts +121 -1503
- package/src/cli.ts +24 -25
- package/src/engine/_server.mjs +271 -0
- package/src/engine/app/(docs)/[[...slug]]/page.tsx +66 -0
- package/src/engine/app/(docs)/layout.tsx +16 -0
- package/src/engine/app/assistant.css +326 -0
- package/src/engine/app/copy-page.css +132 -0
- package/src/engine/app/global.css +21 -0
- package/src/engine/app/layout.tsx +34 -0
- package/src/engine/app/search.css +118 -0
- package/src/engine/components/assistant.tsx +350 -0
- package/src/engine/components/copy-page.tsx +96 -0
- package/src/engine/components/search.tsx +164 -0
- package/src/engine/lib/layout.shared.ts +18 -0
- package/src/engine/lib/source.ts +9 -0
- package/src/engine/lib/velu.ts +42 -0
- package/src/engine/mdx-components.tsx +9 -0
- package/src/engine/next-env.d.ts +4 -0
- package/src/engine/next.config.mjs +22 -0
- package/src/engine/postcss.config.mjs +7 -0
- package/src/engine/source.config.ts +17 -0
- package/src/engine/src/components/Footer.astro +44 -0
- package/src/engine/src/components/PageTitle.astro +451 -0
- package/src/engine/src/components/Sidebar.astro +60 -0
- package/src/engine/src/content.config.ts +6 -0
- package/src/engine/src/lib/velu.ts +153 -0
- package/src/engine/src/styles/assistant.css +351 -0
- package/src/engine/src/styles/tabs.css +183 -0
- package/src/engine/tsconfig.json +32 -0
- package/src/themes.ts +34 -29
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
interface VeluTab {
|
|
5
|
+
tab: string;
|
|
6
|
+
href?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface VeluConfig {
|
|
10
|
+
appearance?: 'system' | 'light' | 'dark';
|
|
11
|
+
navigation: {
|
|
12
|
+
tabs: VeluTab[];
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let cachedConfig: VeluConfig | null = null;
|
|
17
|
+
|
|
18
|
+
function loadVeluConfig(): VeluConfig {
|
|
19
|
+
if (cachedConfig) return cachedConfig;
|
|
20
|
+
const configPath = resolve(process.cwd(), 'velu.json');
|
|
21
|
+
const raw = readFileSync(configPath, 'utf-8');
|
|
22
|
+
cachedConfig = JSON.parse(raw) as VeluConfig;
|
|
23
|
+
return cachedConfig;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getExternalTabs(): Array<{ label: string; href: string }> {
|
|
27
|
+
const config = loadVeluConfig();
|
|
28
|
+
const tabs = config.navigation?.tabs ?? [];
|
|
29
|
+
|
|
30
|
+
return tabs
|
|
31
|
+
.filter((tab): tab is VeluTab & { href: string } => typeof tab.href === 'string' && tab.href.length > 0)
|
|
32
|
+
.map((tab) => ({
|
|
33
|
+
label: tab.tab,
|
|
34
|
+
href: tab.href,
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getAppearance(): 'system' | 'light' | 'dark' {
|
|
39
|
+
const appearance = loadVeluConfig().appearance;
|
|
40
|
+
if (appearance === 'light' || appearance === 'dark') return appearance;
|
|
41
|
+
return 'system';
|
|
42
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { createMDX } from 'fumadocs-mdx/next';
|
|
3
|
+
|
|
4
|
+
const withMDX = createMDX({
|
|
5
|
+
configPath: './source.config.ts',
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
/** @type {import('next').NextConfig} */
|
|
9
|
+
const config = {
|
|
10
|
+
reactStrictMode: true,
|
|
11
|
+
output: 'export',
|
|
12
|
+
distDir: 'dist',
|
|
13
|
+
devIndicators: false,
|
|
14
|
+
turbopack: {
|
|
15
|
+
root: resolve('..'),
|
|
16
|
+
},
|
|
17
|
+
images: {
|
|
18
|
+
unoptimized: true,
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default withMDX(config);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineConfig, defineDocs } from 'fumadocs-mdx/config';
|
|
2
|
+
import { metaSchema, pageSchema } from 'fumadocs-core/source/schema';
|
|
3
|
+
|
|
4
|
+
export const docs = defineDocs({
|
|
5
|
+
dir: 'content/docs',
|
|
6
|
+
docs: {
|
|
7
|
+
schema: pageSchema,
|
|
8
|
+
postprocess: {
|
|
9
|
+
includeProcessedMarkdown: true,
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
meta: {
|
|
13
|
+
schema: metaSchema,
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export default defineConfig();
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
import EditLink from 'virtual:starlight/components/EditLink';
|
|
3
|
+
import LastUpdated from 'virtual:starlight/components/LastUpdated';
|
|
4
|
+
import Pagination from 'virtual:starlight/components/Pagination';
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<footer class="sl-flex">
|
|
8
|
+
<div class="meta sl-flex">
|
|
9
|
+
<EditLink />
|
|
10
|
+
<LastUpdated />
|
|
11
|
+
</div>
|
|
12
|
+
<Pagination />
|
|
13
|
+
<div class="velu-powered-by">
|
|
14
|
+
<a href="https://getvelu.com" target="_blank" rel="noopener noreferrer">Powered by Velu</a>
|
|
15
|
+
</div>
|
|
16
|
+
</footer>
|
|
17
|
+
|
|
18
|
+
<style>
|
|
19
|
+
footer {
|
|
20
|
+
flex-direction: column;
|
|
21
|
+
gap: 1.5rem;
|
|
22
|
+
}
|
|
23
|
+
.meta {
|
|
24
|
+
gap: 0.75rem;
|
|
25
|
+
align-items: center;
|
|
26
|
+
flex-wrap: wrap;
|
|
27
|
+
justify-content: space-between;
|
|
28
|
+
}
|
|
29
|
+
.velu-powered-by {
|
|
30
|
+
text-align: right;
|
|
31
|
+
padding: 1rem 2rem 0.5rem 0;
|
|
32
|
+
}
|
|
33
|
+
.velu-powered-by a {
|
|
34
|
+
font-size: 1.4rem;
|
|
35
|
+
font-weight: 500;
|
|
36
|
+
letter-spacing: 0.02em;
|
|
37
|
+
color: rgba(160, 165, 180, 0.45);
|
|
38
|
+
text-decoration: none;
|
|
39
|
+
transition: color 0.25s ease;
|
|
40
|
+
}
|
|
41
|
+
.velu-powered-by a:hover {
|
|
42
|
+
color: rgba(220, 225, 240, 0.95);
|
|
43
|
+
}
|
|
44
|
+
</style>
|
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
---
|
|
2
|
+
const currentUrl = Astro.url.href;
|
|
3
|
+
const title = Astro.locals.starlightRoute.entry.data.title;
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<div class="velu-title-row">
|
|
7
|
+
<h1 id="_top">{title}</h1>
|
|
8
|
+
<div class="velu-copy-page-container">
|
|
9
|
+
<div class="velu-copy-split-btn">
|
|
10
|
+
<button class="velu-copy-main-btn" data-action="direct-copy">
|
|
11
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
|
12
|
+
<span class="velu-copy-label">Copy page</span>
|
|
13
|
+
</button>
|
|
14
|
+
<span class="velu-copy-sep"></span>
|
|
15
|
+
<button class="velu-copy-caret-btn" aria-expanded="false" aria-haspopup="true">
|
|
16
|
+
<svg class="velu-copy-chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
|
|
17
|
+
</button>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="velu-copy-dropdown" hidden>
|
|
20
|
+
<button class="velu-copy-option" data-action="copy">
|
|
21
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
|
22
|
+
<div>
|
|
23
|
+
<div class="velu-copy-option-title">Copy page</div>
|
|
24
|
+
<div class="velu-copy-option-desc">Copy page as Markdown for LLMs</div>
|
|
25
|
+
</div>
|
|
26
|
+
</button>
|
|
27
|
+
<a class="velu-copy-option" href={`https://chatgpt.com/?prompt=Read+from+${encodeURIComponent(currentUrl)}+so+I+can+ask+questions+about+it.`} target="_blank" rel="noopener noreferrer">
|
|
28
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M22.282 9.821a5.985 5.985 0 0 0-.516-4.91 6.046 6.046 0 0 0-6.51-2.9A6.065 6.065 0 0 0 4.981 4.18a5.985 5.985 0 0 0-3.998 2.9 6.046 6.046 0 0 0 .743 7.097 5.98 5.98 0 0 0 .51 4.911 6.051 6.051 0 0 0 6.515 2.9A5.985 5.985 0 0 0 13.26 24a6.056 6.056 0 0 0 5.772-4.206 5.99 5.99 0 0 0 3.997-2.9 6.056 6.056 0 0 0-.747-7.073zM13.26 22.43a4.476 4.476 0 0 1-2.876-1.04l.141-.081 4.779-2.758a.795.795 0 0 0 .392-.681v-6.737l2.02 1.168a.071.071 0 0 1 .038.052v5.583a4.504 4.504 0 0 1-4.494 4.494zM3.6 18.304a4.47 4.47 0 0 1-.535-3.014l.142.085 4.783 2.759a.771.771 0 0 0 .78 0l5.843-3.369v2.332a.08.08 0 0 1-.033.062L9.74 19.95a4.5 4.5 0 0 1-6.14-1.646zM2.34 7.896a4.485 4.485 0 0 1 2.366-1.973V11.6a.766.766 0 0 0 .388.676l5.815 3.355-2.02 1.168a.076.076 0 0 1-.071 0l-4.83-2.786A4.504 4.504 0 0 1 2.34 7.872zm16.597 3.855l-5.833-3.387L15.119 7.2a.076.076 0 0 1 .071 0l4.83 2.791a4.494 4.494 0 0 1-.676 8.105v-5.678a.79.79 0 0 0-.407-.667zm2.01-3.023l-.141-.085-4.774-2.782a.776.776 0 0 0-.785 0L9.409 9.23V6.897a.066.066 0 0 1 .028-.061l4.83-2.787a4.5 4.5 0 0 1 6.68 4.66zm-12.64 4.135l-2.02-1.164a.08.08 0 0 1-.038-.057V6.075a4.5 4.5 0 0 1 7.375-3.453l-.142.08L8.704 5.46a.795.795 0 0 0-.393.681zm1.097-2.365l2.602-1.5 2.607 1.5v2.999l-2.597 1.5-2.607-1.5z"/></svg>
|
|
29
|
+
<div>
|
|
30
|
+
<div class="velu-copy-option-title">Open in ChatGPT <span class="velu-external-arrow">↗</span></div>
|
|
31
|
+
<div class="velu-copy-option-desc">Ask questions about this page</div>
|
|
32
|
+
</div>
|
|
33
|
+
</a>
|
|
34
|
+
<a class="velu-copy-option" href={`https://claude.ai/new?q=Read+from+${encodeURIComponent(currentUrl)}+so+I+can+ask+questions+about+it.`} target="_blank" rel="noopener noreferrer">
|
|
35
|
+
<svg width="18" height="18" viewBox="0 0 200 200" style="overflow:visible" fill="currentColor"><path d="m50.228 170.321 50.357-28.257.843-2.463-.843-1.361h-2.462l-8.426-.518-28.775-.778-24.952-1.037-24.175-1.296-6.092-1.297L0 125.796l.583-3.759 5.12-3.434 7.324.648 16.202 1.101 24.304 1.685 17.629 1.037 26.118 2.722h4.148l.583-1.685-1.426-1.037-1.101-1.037-25.147-17.045-27.22-18.017-14.258-10.37-7.713-5.25-3.888-4.925-1.685-10.758 7-7.713 9.397.649 2.398.648 9.527 7.323 20.35 15.75L94.817 91.9l3.889 3.24 1.555-1.102.195-.777-1.75-2.917-14.453-26.118-15.425-26.572-6.87-11.018-1.814-6.61c-.648-2.723-1.102-4.991-1.102-7.778l7.972-10.823L71.42 0 82.05 1.426l4.472 3.888 6.61 15.101 10.694 23.786 16.591 32.34 4.861 9.592 2.592 8.879.973 2.722h1.685v-1.556l1.36-18.211 2.528-22.36 2.463-28.776.843-8.1 4.018-9.722 7.971-5.25 6.222 2.981 5.12 7.324-.713 4.73-3.046 19.768-5.962 30.98-3.889 20.739h2.268l2.593-2.593 10.499-13.934 17.628-22.036 7.778-8.749 9.073-9.657 5.833-4.601h11.018l8.1 12.055-3.628 12.443-11.342 14.388-9.398 12.184-13.48 18.147-8.426 14.518.778 1.166 2.01-.194 30.46-6.481 16.462-2.982 19.637-3.37 8.88 4.148.971 4.213-3.5 8.62-20.998 5.184-24.628 4.926-36.682 8.685-.454.324.519.648 16.526 1.555 7.065.389h17.304l32.21 2.398 8.426 5.574 5.055 6.805-.843 5.184-12.962 6.611-17.498-4.148-40.83-9.721-14-3.5h-1.944v1.167l11.666 11.406 21.387 19.314 26.767 24.887 1.36 6.157-3.434 4.86-3.63-.518-23.526-17.693-9.073-7.972-20.545-17.304h-1.36v1.814l4.73 6.935 25.017 37.59 1.296 11.536-1.814 3.76-6.481 2.268-7.13-1.297-14.647-20.544-15.1-23.138-12.185-20.739-1.49.843-7.194 77.448-3.37 3.953-7.778 2.981-6.48-4.925-3.436-7.972 3.435-15.749 4.148-20.544 3.37-16.333 3.046-20.285 1.815-6.74-.13-.454-1.49.194-15.295 20.999-23.267 31.433-18.406 19.702-4.407 1.75-7.648-3.954.713-7.064 4.277-6.286 25.47-32.405 15.36-20.092 9.917-11.6-.065-1.686h-.583L44.07 198.125l-12.055 1.555-5.185-4.86.648-7.972 2.463-2.593 20.35-13.999-.064.065Z"/></svg>
|
|
36
|
+
<div>
|
|
37
|
+
<div class="velu-copy-option-title">Open in Claude <span class="velu-external-arrow">↗</span></div>
|
|
38
|
+
<div class="velu-copy-option-desc">Ask questions about this page</div>
|
|
39
|
+
</div>
|
|
40
|
+
</a>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<script is:inline>
|
|
46
|
+
(function init() {
|
|
47
|
+
var caretBtn = document.querySelector('.velu-copy-caret-btn');
|
|
48
|
+
var mainBtn = document.querySelector('.velu-copy-main-btn');
|
|
49
|
+
var dropdown = document.querySelector('.velu-copy-dropdown');
|
|
50
|
+
var label = document.querySelector('.velu-copy-label');
|
|
51
|
+
if (!caretBtn || !mainBtn || !dropdown) return;
|
|
52
|
+
|
|
53
|
+
function doCopy() {
|
|
54
|
+
if (label) label.textContent = 'Copying...';
|
|
55
|
+
var titleEl = document.querySelector('#_top');
|
|
56
|
+
var article = document.querySelector('.sl-markdown-content') || document.querySelector('.content-panel') || document.querySelector('main');
|
|
57
|
+
var text = '';
|
|
58
|
+
if (titleEl) text = '# ' + titleEl.textContent + '\n\n';
|
|
59
|
+
if (article) text += article.innerText;
|
|
60
|
+
if (text) {
|
|
61
|
+
navigator.clipboard.writeText(text).then(function() {
|
|
62
|
+
if (label) label.textContent = 'Copied!';
|
|
63
|
+
setTimeout(function() { if (label) label.textContent = 'Copy page'; }, 1500);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
dropdown.hidden = true;
|
|
67
|
+
caretBtn.setAttribute('aria-expanded', 'false');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
mainBtn.onclick = function(e) { e.stopPropagation(); doCopy(); };
|
|
71
|
+
|
|
72
|
+
caretBtn.onclick = function(e) {
|
|
73
|
+
e.stopPropagation();
|
|
74
|
+
var open = dropdown.hidden;
|
|
75
|
+
dropdown.hidden = !open;
|
|
76
|
+
caretBtn.setAttribute('aria-expanded', String(open));
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
document.addEventListener('click', function() {
|
|
80
|
+
dropdown.hidden = true;
|
|
81
|
+
caretBtn.setAttribute('aria-expanded', 'false');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
dropdown.onclick = function(e) { e.stopPropagation(); };
|
|
85
|
+
|
|
86
|
+
var copyOpt = dropdown.querySelector('[data-action="copy"]');
|
|
87
|
+
if (copyOpt) {
|
|
88
|
+
copyOpt.onclick = function() { doCopy(); };
|
|
89
|
+
}
|
|
90
|
+
})();
|
|
91
|
+
</script>
|
|
92
|
+
|
|
93
|
+
<!-- AI Assistant Widget -->
|
|
94
|
+
<div class="velu-ask-bar" id="veluAskBar">
|
|
95
|
+
<div class="velu-ask-bar-inner">
|
|
96
|
+
<input type="text" class="velu-ask-input" id="veluAskInput" placeholder="Ask a question..." autocomplete="off" />
|
|
97
|
+
<button class="velu-ask-submit" id="veluAskSubmit" aria-label="Send">
|
|
98
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
|
|
99
|
+
</button>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<div class="velu-assistant-panel velu-panel-closed" id="veluAssistantPanel">
|
|
104
|
+
<div class="velu-assistant-header">
|
|
105
|
+
<span class="velu-assistant-title">Assistant</span>
|
|
106
|
+
<div class="velu-assistant-actions">
|
|
107
|
+
<button class="velu-assistant-action" data-velu-action="expand" title="Expand" aria-label="Expand assistant" type="button">
|
|
108
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/></svg>
|
|
109
|
+
</button>
|
|
110
|
+
<button class="velu-assistant-action" data-velu-action="reset" title="New chat" aria-label="New chat" type="button">
|
|
111
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>
|
|
112
|
+
</button>
|
|
113
|
+
<button class="velu-assistant-action" data-velu-action="close" title="Close" aria-label="Close assistant" type="button">
|
|
114
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
|
115
|
+
</button>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
<div class="velu-assistant-messages" id="veluAssistantMessages"></div>
|
|
119
|
+
<div class="velu-assistant-input-area">
|
|
120
|
+
<input type="text" class="velu-assistant-chat-input" id="veluAssistantChatInput" placeholder="Ask a question..." autocomplete="off" />
|
|
121
|
+
<button class="velu-assistant-send" id="veluAssistantSend" aria-label="Send">
|
|
122
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M3.478 2.405a.75.75 0 00-.926.94l2.432 7.905H13.5a.75.75 0 010 1.5H4.984l-2.432 7.905a.75.75 0 00.926.94l18-8.5a.75.75 0 000-1.38l-18-8.5z"/></svg>
|
|
123
|
+
</button>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<script is:inline>
|
|
128
|
+
(function veluAssistant() {
|
|
129
|
+
var API_BASE = 'https://api.getvelu.com/api/v1/public/ai-assistant';
|
|
130
|
+
var state = {
|
|
131
|
+
conversationId: null,
|
|
132
|
+
conversationToken: null,
|
|
133
|
+
lastSeq: 0,
|
|
134
|
+
eventSource: null,
|
|
135
|
+
expanded: false,
|
|
136
|
+
bootstrapped: false
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
var askBar = document.getElementById('veluAskBar');
|
|
140
|
+
var askInput = document.getElementById('veluAskInput');
|
|
141
|
+
var askSubmit = document.getElementById('veluAskSubmit');
|
|
142
|
+
var panel = document.getElementById('veluAssistantPanel');
|
|
143
|
+
var messagesEl = document.getElementById('veluAssistantMessages');
|
|
144
|
+
var chatInput = document.getElementById('veluAssistantChatInput');
|
|
145
|
+
var sendBtn = document.getElementById('veluAssistantSend');
|
|
146
|
+
var closeBtn = document.getElementById('veluAssistantClose');
|
|
147
|
+
var expandBtn = document.getElementById('veluAssistantExpand');
|
|
148
|
+
var newChatBtn = document.getElementById('veluAssistantNewChat');
|
|
149
|
+
|
|
150
|
+
if (!askBar || !panel) return;
|
|
151
|
+
|
|
152
|
+
if (panel.parentElement !== document.body) {
|
|
153
|
+
document.body.appendChild(panel);
|
|
154
|
+
}
|
|
155
|
+
if (askBar.parentElement !== document.body) {
|
|
156
|
+
document.body.appendChild(askBar);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function saveState() {
|
|
160
|
+
try {
|
|
161
|
+
sessionStorage.setItem('velu-panel-open', isPanelOpen() ? '1' : '');
|
|
162
|
+
sessionStorage.setItem('velu-panel-expanded', state.expanded ? '1' : '');
|
|
163
|
+
sessionStorage.setItem('velu-panel-messages', messagesEl.innerHTML);
|
|
164
|
+
sessionStorage.setItem('velu-conv-id', state.conversationId || '');
|
|
165
|
+
sessionStorage.setItem('velu-conv-token', state.conversationToken || '');
|
|
166
|
+
sessionStorage.setItem('velu-last-seq', String(state.lastSeq));
|
|
167
|
+
} catch(e) {}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function openPanel() {
|
|
171
|
+
panel.classList.remove('velu-panel-closed');
|
|
172
|
+
askBar.classList.add('velu-ask-bar-hidden');
|
|
173
|
+
document.documentElement.classList.add('velu-assistant-open');
|
|
174
|
+
chatInput.focus();
|
|
175
|
+
saveState();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function closePanel() {
|
|
179
|
+
panel.classList.add('velu-panel-closed');
|
|
180
|
+
askBar.classList.remove('velu-ask-bar-hidden');
|
|
181
|
+
document.documentElement.classList.remove('velu-assistant-open');
|
|
182
|
+
document.documentElement.classList.remove('velu-assistant-wide');
|
|
183
|
+
if (state.eventSource) { state.eventSource.close(); state.eventSource = null; }
|
|
184
|
+
saveState();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function resetChat() {
|
|
188
|
+
state.conversationId = null;
|
|
189
|
+
state.conversationToken = null;
|
|
190
|
+
state.lastSeq = 0;
|
|
191
|
+
if (state.eventSource) { state.eventSource.close(); state.eventSource = null; }
|
|
192
|
+
messagesEl.innerHTML = '';
|
|
193
|
+
chatInput.value = '';
|
|
194
|
+
chatInput.focus();
|
|
195
|
+
saveState();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function toggleExpand() {
|
|
199
|
+
state.expanded = !state.expanded;
|
|
200
|
+
panel.classList.toggle('velu-assistant-expanded', state.expanded);
|
|
201
|
+
document.documentElement.classList.toggle('velu-assistant-wide', state.expanded);
|
|
202
|
+
saveState();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Expose to inline onclick handlers
|
|
206
|
+
window._veluClosePanel = closePanel;
|
|
207
|
+
window._veluResetChat = resetChat;
|
|
208
|
+
window._veluToggleExpand = toggleExpand;
|
|
209
|
+
|
|
210
|
+
function bootstrap() {
|
|
211
|
+
if (state.bootstrapped) return Promise.resolve();
|
|
212
|
+
return fetch(API_BASE + '/bootstrap', { credentials: 'include' })
|
|
213
|
+
.then(function(r) { return r.json(); })
|
|
214
|
+
.then(function(d) { state.bootstrapped = true; })
|
|
215
|
+
.catch(function() {});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function isPanelOpen() {
|
|
219
|
+
return !panel.classList.contains('velu-panel-closed');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function addMessage(role, content, citations) {
|
|
223
|
+
var msgDiv = document.createElement('div');
|
|
224
|
+
msgDiv.className = 'velu-msg velu-msg-' + role;
|
|
225
|
+
var bubble = document.createElement('div');
|
|
226
|
+
bubble.className = 'velu-msg-bubble velu-msg-bubble-' + role;
|
|
227
|
+
bubble.innerHTML = formatContent(content, citations || []);
|
|
228
|
+
msgDiv.appendChild(bubble);
|
|
229
|
+
|
|
230
|
+
if (role === 'assistant' && citations && citations.length > 0) {
|
|
231
|
+
var citDiv = document.createElement('div');
|
|
232
|
+
citDiv.className = 'velu-msg-citations';
|
|
233
|
+
citations.forEach(function(c, i) {
|
|
234
|
+
var a = document.createElement('a');
|
|
235
|
+
a.href = c.url || c.route_path || '#';
|
|
236
|
+
a.className = 'velu-citation-link';
|
|
237
|
+
a.textContent = '[' + (i + 1) + '] ' + (c.title || c.route_path || 'Source');
|
|
238
|
+
a.target = '_blank';
|
|
239
|
+
citDiv.appendChild(a);
|
|
240
|
+
});
|
|
241
|
+
msgDiv.appendChild(citDiv);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (role === 'assistant') {
|
|
245
|
+
var actions = document.createElement('div');
|
|
246
|
+
actions.className = 'velu-msg-actions';
|
|
247
|
+
actions.innerHTML = '<button class="velu-msg-action" title="Like"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"/></svg></button>'
|
|
248
|
+
+ '<button class="velu-msg-action" title="Dislike"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"/></svg></button>'
|
|
249
|
+
+ '<button class="velu-msg-action velu-msg-copy" title="Copy"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button>'
|
|
250
|
+
+ '<button class="velu-msg-action velu-msg-retry" title="Retry"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg></button>';
|
|
251
|
+
msgDiv.appendChild(actions);
|
|
252
|
+
|
|
253
|
+
var copyBtn = actions.querySelector('.velu-msg-copy');
|
|
254
|
+
if (copyBtn) {
|
|
255
|
+
copyBtn.onclick = function() {
|
|
256
|
+
navigator.clipboard.writeText(content);
|
|
257
|
+
copyBtn.title = 'Copied!';
|
|
258
|
+
setTimeout(function() { copyBtn.title = 'Copy'; }, 1500);
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
messagesEl.appendChild(msgDiv);
|
|
264
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
265
|
+
saveState();
|
|
266
|
+
return bubble;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function formatContent(text, citations) {
|
|
270
|
+
var html = text
|
|
271
|
+
.replace(/&/g, '&')
|
|
272
|
+
.replace(/</g, '<')
|
|
273
|
+
.replace(/>/g, '>')
|
|
274
|
+
.replace(/\n/g, '<br>')
|
|
275
|
+
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
|
276
|
+
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
|
277
|
+
html = html.replace(/\[(\d+)\]/g, function(m, n) {
|
|
278
|
+
var idx = parseInt(n) - 1;
|
|
279
|
+
var c = citations[idx];
|
|
280
|
+
if (c) {
|
|
281
|
+
return '<a href="' + (c.url || c.route_path || '#') + '" class="velu-citation-ref" target="_blank">[' + n + ']</a>';
|
|
282
|
+
}
|
|
283
|
+
return m;
|
|
284
|
+
});
|
|
285
|
+
return html;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function addThinking() {
|
|
289
|
+
var div = document.createElement('div');
|
|
290
|
+
div.className = 'velu-msg velu-msg-assistant';
|
|
291
|
+
div.id = 'veluThinking';
|
|
292
|
+
div.innerHTML = '<div class="velu-msg-bubble velu-msg-bubble-assistant"><span class="velu-thinking-dots"><span></span><span></span><span></span></span></div>';
|
|
293
|
+
messagesEl.appendChild(div);
|
|
294
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function removeThinking() {
|
|
298
|
+
var el = document.getElementById('veluThinking');
|
|
299
|
+
if (el) el.remove();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function connectSSE() {
|
|
303
|
+
if (state.eventSource) state.eventSource.close();
|
|
304
|
+
var url = API_BASE + '/conversations/' + state.conversationId + '/events?after_seq=' + state.lastSeq + '&token=' + encodeURIComponent(state.conversationToken || '');
|
|
305
|
+
state.eventSource = new EventSource(url);
|
|
306
|
+
|
|
307
|
+
state.eventSource.addEventListener('assistant.completed', function(e) {
|
|
308
|
+
removeThinking();
|
|
309
|
+
try {
|
|
310
|
+
var data = JSON.parse(e.data);
|
|
311
|
+
var msg = data.message || data;
|
|
312
|
+
if (msg.seq) state.lastSeq = msg.seq;
|
|
313
|
+
addMessage('assistant', msg.content || '', msg.citations || []);
|
|
314
|
+
} catch(err) {}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
state.eventSource.addEventListener('assistant.error', function(e) {
|
|
318
|
+
removeThinking();
|
|
319
|
+
try {
|
|
320
|
+
var data = JSON.parse(e.data);
|
|
321
|
+
addMessage('assistant', data.error || 'Something went wrong. Please try again.', []);
|
|
322
|
+
} catch(err) {
|
|
323
|
+
addMessage('assistant', 'Something went wrong. Please try again.', []);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
state.eventSource.onerror = function() {};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function sendMessage(text) {
|
|
331
|
+
if (!text.trim()) return;
|
|
332
|
+
addMessage('user', text);
|
|
333
|
+
addThinking();
|
|
334
|
+
|
|
335
|
+
bootstrap().then(function() {
|
|
336
|
+
return fetch(API_BASE + '/messages', {
|
|
337
|
+
method: 'POST',
|
|
338
|
+
headers: { 'Content-Type': 'application/json' },
|
|
339
|
+
credentials: 'include',
|
|
340
|
+
body: JSON.stringify({
|
|
341
|
+
message: text,
|
|
342
|
+
conversation_id: state.conversationId
|
|
343
|
+
})
|
|
344
|
+
});
|
|
345
|
+
}).then(function(r) {
|
|
346
|
+
if (r.status === 429) { removeThinking(); addMessage('assistant', 'Rate limited. Please wait a moment and try again.', []); return; }
|
|
347
|
+
return r.json();
|
|
348
|
+
}).then(function(data) {
|
|
349
|
+
if (!data) return;
|
|
350
|
+
if (data.conversation_id) state.conversationId = data.conversation_id;
|
|
351
|
+
if (data.conversation_token) state.conversationToken = data.conversation_token;
|
|
352
|
+
saveState();
|
|
353
|
+
if (!state.eventSource || state.eventSource.readyState === 2) {
|
|
354
|
+
connectSSE();
|
|
355
|
+
}
|
|
356
|
+
}).catch(function() {
|
|
357
|
+
removeThinking();
|
|
358
|
+
addMessage('assistant', 'Failed to connect. Please try again.', []);
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function handleAskSubmit() {
|
|
363
|
+
var text = askInput.value.trim();
|
|
364
|
+
if (!text) return;
|
|
365
|
+
askInput.value = '';
|
|
366
|
+
openPanel();
|
|
367
|
+
sendMessage(text);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function handleChatSubmit() {
|
|
371
|
+
var text = chatInput.value.trim();
|
|
372
|
+
if (!text) return;
|
|
373
|
+
chatInput.value = '';
|
|
374
|
+
sendMessage(text);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
askInput.onkeydown = function(e) { if (e.key === 'Enter') handleAskSubmit(); };
|
|
378
|
+
askSubmit.onclick = handleAskSubmit;
|
|
379
|
+
chatInput.onkeydown = function(e) { if (e.key === 'Enter') handleChatSubmit(); };
|
|
380
|
+
sendBtn.onclick = handleChatSubmit;
|
|
381
|
+
|
|
382
|
+
panel.addEventListener('click', function(e) {
|
|
383
|
+
var actionBtn = e.target.closest('[data-velu-action]');
|
|
384
|
+
if (!actionBtn) return;
|
|
385
|
+
var action = actionBtn.getAttribute('data-velu-action');
|
|
386
|
+
if (action === 'close') {
|
|
387
|
+
closePanel();
|
|
388
|
+
} else if (action === 'expand') {
|
|
389
|
+
toggleExpand();
|
|
390
|
+
} else if (action === 'reset') {
|
|
391
|
+
resetChat();
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
document.addEventListener('click', function(e) {
|
|
396
|
+
var actionBtn = e.target.closest('[data-velu-action]');
|
|
397
|
+
if (!actionBtn) return;
|
|
398
|
+
var action = actionBtn.getAttribute('data-velu-action');
|
|
399
|
+
if (action === 'close') {
|
|
400
|
+
closePanel();
|
|
401
|
+
} else if (action === 'expand') {
|
|
402
|
+
toggleExpand();
|
|
403
|
+
} else if (action === 'reset') {
|
|
404
|
+
resetChat();
|
|
405
|
+
}
|
|
406
|
+
}, true);
|
|
407
|
+
|
|
408
|
+
// Hide ask bar only when user scrolls to the very bottom
|
|
409
|
+
window.addEventListener('scroll', function() {
|
|
410
|
+
if (isPanelOpen()) return;
|
|
411
|
+
var scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
|
412
|
+
var docHeight = document.documentElement.scrollHeight;
|
|
413
|
+
var winHeight = window.innerHeight;
|
|
414
|
+
if (docHeight <= winHeight + 10) return; // short pages: always show
|
|
415
|
+
if (docHeight - scrollTop - winHeight < 60) {
|
|
416
|
+
askBar.classList.add('velu-ask-bar-hidden');
|
|
417
|
+
} else {
|
|
418
|
+
askBar.classList.remove('velu-ask-bar-hidden');
|
|
419
|
+
}
|
|
420
|
+
}, { passive: true });
|
|
421
|
+
|
|
422
|
+
document.onkeydown = function(e) {
|
|
423
|
+
if (e.key === 'Escape' && isPanelOpen()) { closePanel(); }
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
// Restore panel state from sessionStorage on page load
|
|
427
|
+
try {
|
|
428
|
+
var savedOpen = sessionStorage.getItem('velu-panel-open');
|
|
429
|
+
var savedExpanded = sessionStorage.getItem('velu-panel-expanded');
|
|
430
|
+
var savedMessages = sessionStorage.getItem('velu-panel-messages');
|
|
431
|
+
var savedConvId = sessionStorage.getItem('velu-conv-id');
|
|
432
|
+
var savedConvToken = sessionStorage.getItem('velu-conv-token');
|
|
433
|
+
var savedSeq = sessionStorage.getItem('velu-last-seq');
|
|
434
|
+
if (savedConvId) state.conversationId = savedConvId;
|
|
435
|
+
if (savedConvToken) state.conversationToken = savedConvToken;
|
|
436
|
+
if (savedSeq) state.lastSeq = parseInt(savedSeq, 10) || 0;
|
|
437
|
+
if (savedMessages) messagesEl.innerHTML = savedMessages;
|
|
438
|
+
if (savedExpanded === '1') {
|
|
439
|
+
state.expanded = true;
|
|
440
|
+
panel.classList.add('velu-assistant-expanded');
|
|
441
|
+
document.documentElement.classList.add('velu-assistant-wide');
|
|
442
|
+
}
|
|
443
|
+
if (savedOpen === '1') {
|
|
444
|
+
openPanel();
|
|
445
|
+
if (state.conversationId) connectSSE();
|
|
446
|
+
}
|
|
447
|
+
} catch(e) {}
|
|
448
|
+
|
|
449
|
+
bootstrap();
|
|
450
|
+
})();
|
|
451
|
+
</script>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
import MobileMenuFooter from 'virtual:starlight/components/MobileMenuFooter';
|
|
3
|
+
import SidebarSublist from '@astrojs/starlight/components/SidebarSublist.astro';
|
|
4
|
+
import { getTabs, getTabSidebarMap } from '../lib/velu.ts';
|
|
5
|
+
|
|
6
|
+
const tabs = getTabs();
|
|
7
|
+
const tabSidebarMap = getTabSidebarMap();
|
|
8
|
+
const currentPath = Astro.url.pathname;
|
|
9
|
+
|
|
10
|
+
function isTabActive(tab: any, path: string): boolean {
|
|
11
|
+
if (tab.href) return false;
|
|
12
|
+
if (!tab.slugs || tab.slugs.length === 0) return false;
|
|
13
|
+
return tab.slugs.some((s: string) => path.startsWith('/' + s + '/'));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getActiveTabSlug(path: string): string {
|
|
17
|
+
const allPrefixes = Object.keys(tabSidebarMap);
|
|
18
|
+
for (const prefix of allPrefixes) {
|
|
19
|
+
if (path.startsWith('/' + prefix + '/')) return prefix;
|
|
20
|
+
}
|
|
21
|
+
return '';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const activeSlug = getActiveTabSlug(currentPath);
|
|
25
|
+
const visibleLabels = new Set(tabSidebarMap[activeSlug] || []);
|
|
26
|
+
|
|
27
|
+
const { sidebar } = Astro.locals.starlightRoute;
|
|
28
|
+
const filteredSidebar = sidebar.filter((entry: any) => {
|
|
29
|
+
if (entry.type === 'group') return visibleLabels.has(entry.label);
|
|
30
|
+
return false;
|
|
31
|
+
});
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
<div class="velu-sidebar-tabs">
|
|
35
|
+
{tabs.map((tab) => {
|
|
36
|
+
if (tab.href) {
|
|
37
|
+
return (
|
|
38
|
+
<a href={tab.href} class="velu-sidebar-tab" target="_blank" rel="noopener noreferrer">
|
|
39
|
+
{tab.icon && <span class="velu-sidebar-tab-icon" data-icon={tab.icon} />}
|
|
40
|
+
<span>{tab.label}</span>
|
|
41
|
+
<svg class="velu-external-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M7 17L17 7M17 7H7M17 7V17"/></svg>
|
|
42
|
+
</a>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
const active = isTabActive(tab, currentPath);
|
|
46
|
+
const href = tab.firstPage ? '/' + tab.firstPage + '/' : '/';
|
|
47
|
+
return (
|
|
48
|
+
<a href={href} class:list={['velu-sidebar-tab', { active }]}>
|
|
49
|
+
{tab.icon && <span class="velu-sidebar-tab-icon" data-icon={tab.icon} />}
|
|
50
|
+
<span>{tab.label}</span>
|
|
51
|
+
</a>
|
|
52
|
+
);
|
|
53
|
+
})}
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<SidebarSublist sublist={filteredSidebar} />
|
|
57
|
+
|
|
58
|
+
<div class="md:sl-hidden">
|
|
59
|
+
<MobileMenuFooter />
|
|
60
|
+
</div>
|