@aravindc26/velu 0.13.8 → 0.13.11
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 +1 -1
- package/src/cli.ts +18 -8
- package/src/engine/app/_preview/[sessionId]/layout.tsx +14 -1
- package/src/engine/lib/preview-config.ts +67 -24
- package/src/engine/source.config.ts +2 -2
- package/src/engine-core/components/assistant.tsx +142 -9
- package/src/engine-core/css/assistant.css +8 -0
- package/src/engine-core/css/shared.css +5 -0
- package/src/engine-core/lib/remark-plugins.ts +38 -0
- package/src/engine-core/mdx-components.tsx +1 -0
- package/src/validate.ts +42 -7
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -181,10 +181,12 @@ async function lint(docsDir: string) {
|
|
|
181
181
|
interface PathEntry {
|
|
182
182
|
path: string;
|
|
183
183
|
file: string | null;
|
|
184
|
+
tab: string | null;
|
|
185
|
+
tabSlug: string | null;
|
|
184
186
|
}
|
|
185
187
|
|
|
186
188
|
async function paths(docsDir: string) {
|
|
187
|
-
const {
|
|
189
|
+
const { collectPagesWithTabsByLanguage } = await import("./validate.js");
|
|
188
190
|
const { normalizeConfigNavigation } = await import("./navigation-normalize.js");
|
|
189
191
|
const { readFileSync, existsSync } = await import("node:fs");
|
|
190
192
|
const { join } = await import("node:path");
|
|
@@ -197,23 +199,31 @@ async function paths(docsDir: string) {
|
|
|
197
199
|
|
|
198
200
|
const raw = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
199
201
|
const config = normalizeConfigNavigation(raw);
|
|
200
|
-
const pagesByLanguage =
|
|
202
|
+
const pagesByLanguage = collectPagesWithTabsByLanguage(config);
|
|
201
203
|
const groupedEntries: Record<string, PathEntry[]> = {};
|
|
202
204
|
const flatEntries: PathEntry[] = [];
|
|
203
205
|
|
|
204
206
|
for (const [language, pages] of Object.entries(pagesByLanguage)) {
|
|
205
|
-
const entries: PathEntry[] = pages.map((
|
|
207
|
+
const entries: PathEntry[] = pages.map((pageWithTab) => {
|
|
208
|
+
const pagePath = pageWithTab.page;
|
|
206
209
|
// Check for .mdx first, then .md
|
|
207
210
|
const mdxPath = join(docsDir, `${pagePath}.mdx`);
|
|
208
211
|
const mdPath = join(docsDir, `${pagePath}.md`);
|
|
209
212
|
|
|
213
|
+
const entry: PathEntry = {
|
|
214
|
+
path: pagePath,
|
|
215
|
+
file: null,
|
|
216
|
+
tab: pageWithTab.tab,
|
|
217
|
+
tabSlug: pageWithTab.tabSlug,
|
|
218
|
+
};
|
|
219
|
+
|
|
210
220
|
if (existsSync(mdxPath)) {
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
return { path: pagePath, file: `${pagePath}.md` };
|
|
221
|
+
entry.file = `${pagePath}.mdx`;
|
|
222
|
+
} else if (existsSync(mdPath)) {
|
|
223
|
+
entry.file = `${pagePath}.md`;
|
|
215
224
|
}
|
|
216
|
-
|
|
225
|
+
|
|
226
|
+
return entry;
|
|
217
227
|
});
|
|
218
228
|
groupedEntries[language] = entries;
|
|
219
229
|
flatEntries.push(...entries);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import type { Metadata } from 'next';
|
|
1
2
|
import type { ReactNode } from 'react';
|
|
2
3
|
import { loadSessionConfigSource, getSessionThemeCss } from '@/lib/preview-config';
|
|
3
|
-
import { getBannerConfig, getFontsConfig, getSiteFavicon } from '@/lib/velu';
|
|
4
|
+
import { getBannerConfig, getFontsConfig, getSiteDescription, getSiteFavicon, getSiteName } from '@/lib/velu';
|
|
4
5
|
import { VeluBanner } from '@/components/banner';
|
|
5
6
|
|
|
6
7
|
interface LayoutProps {
|
|
@@ -8,6 +9,18 @@ interface LayoutProps {
|
|
|
8
9
|
params: Promise<{ sessionId: string }>;
|
|
9
10
|
}
|
|
10
11
|
|
|
12
|
+
export async function generateMetadata({ params }: { params: Promise<{ sessionId: string }> }): Promise<Metadata> {
|
|
13
|
+
const { sessionId } = await params;
|
|
14
|
+
const configSource = loadSessionConfigSource(sessionId);
|
|
15
|
+
if (!configSource) return {};
|
|
16
|
+
const title = getSiteName(configSource);
|
|
17
|
+
const description = getSiteDescription(configSource);
|
|
18
|
+
return {
|
|
19
|
+
...(title ? { title: { default: title, template: `%s - ${title}` } } : {}),
|
|
20
|
+
...(description ? { description } : {}),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
11
24
|
/**
|
|
12
25
|
* Session layout: injects per-session theme CSS, Google Fonts, and banner.
|
|
13
26
|
* Uses React 19 resource hoisting (<style precedence> / <link precedence>)
|
|
@@ -6,6 +6,7 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
import { normalizeConfigNavigation } from './navigation-normalize';
|
|
8
8
|
import type { VeluConfigSource, VeluConfig } from './velu';
|
|
9
|
+
import { getFontsConfig } from './velu';
|
|
9
10
|
|
|
10
11
|
const WORKSPACE_DIR = process.env.WORKSPACE_DIR || '/mnt/nfs_share/editor_sessions';
|
|
11
12
|
const PRIMARY_CONFIG_NAME = 'docs.json';
|
|
@@ -91,38 +92,80 @@ function textColorFor(hex: string): string {
|
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
/**
|
|
94
|
-
* Generate CSS custom properties for a session's primary color theme.
|
|
95
|
+
* Generate CSS custom properties for a session's primary color theme and font overrides.
|
|
95
96
|
*/
|
|
96
97
|
export function getSessionThemeCss(sessionId: string): string | null {
|
|
97
98
|
const configSource = loadSessionConfigSource(sessionId);
|
|
98
|
-
|
|
99
|
-
if (!colors?.primary) return null;
|
|
99
|
+
if (!configSource) return null;
|
|
100
100
|
|
|
101
|
-
const { primary, light, dark } = colors;
|
|
102
|
-
const lightAccent = light || primary;
|
|
103
|
-
const darkAccent = dark || primary;
|
|
104
101
|
const lines: string[] = [];
|
|
105
102
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
103
|
+
// ── Color overrides ──
|
|
104
|
+
const colors = configSource.config.colors;
|
|
105
|
+
if (colors?.primary) {
|
|
106
|
+
const { primary, light, dark } = colors;
|
|
107
|
+
const lightAccent = light || primary;
|
|
108
|
+
const darkAccent = dark || primary;
|
|
109
|
+
|
|
110
|
+
if (lightAccent) {
|
|
111
|
+
const accentLow = mixColors(lightAccent, '#ffffff', 0.15);
|
|
112
|
+
lines.push(':root {');
|
|
113
|
+
lines.push(` --color-fd-primary: ${lightAccent};`);
|
|
114
|
+
lines.push(` --color-fd-primary-foreground: ${textColorFor(lightAccent)};`);
|
|
115
|
+
lines.push(` --color-fd-accent: ${accentLow};`);
|
|
116
|
+
lines.push(` --color-fd-accent-foreground: ${textColorFor(accentLow)};`);
|
|
117
|
+
lines.push(` --color-fd-ring: ${lightAccent};`);
|
|
118
|
+
lines.push('}');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (darkAccent) {
|
|
122
|
+
const accentLow = mixColors(darkAccent, '#000000', 0.3);
|
|
123
|
+
lines.push('.dark {');
|
|
124
|
+
lines.push(` --color-fd-primary: ${darkAccent};`);
|
|
125
|
+
lines.push(` --color-fd-primary-foreground: ${textColorFor(darkAccent)};`);
|
|
126
|
+
lines.push(` --color-fd-accent: ${accentLow};`);
|
|
127
|
+
lines.push(` --color-fd-accent-foreground: ${textColorFor(accentLow)};`);
|
|
128
|
+
lines.push(` --color-fd-ring: ${darkAccent};`);
|
|
129
|
+
lines.push('}');
|
|
130
|
+
}
|
|
115
131
|
}
|
|
116
132
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
133
|
+
// ── Font overrides ──
|
|
134
|
+
const fontsConfig = getFontsConfig(configSource);
|
|
135
|
+
if (fontsConfig) {
|
|
136
|
+
const { heading, body } = fontsConfig;
|
|
137
|
+
// @font-face for custom sources
|
|
138
|
+
for (const def of [heading, body]) {
|
|
139
|
+
if (def?.source) {
|
|
140
|
+
const fmt = def.format || (def.source.endsWith('.woff2') ? 'woff2' : 'woff');
|
|
141
|
+
lines.push(`@font-face {`);
|
|
142
|
+
lines.push(` font-family: '${def.family}';`);
|
|
143
|
+
lines.push(` src: url('${def.source}') format('${fmt}');`);
|
|
144
|
+
if (def.weight) lines.push(` font-weight: ${def.weight};`);
|
|
145
|
+
lines.push(` font-display: swap;`);
|
|
146
|
+
lines.push(`}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// CSS variable overrides
|
|
150
|
+
const vars: string[] = [];
|
|
151
|
+
if (body) {
|
|
152
|
+
vars.push(` --font-fd-sans: '${body.family}', ui-sans-serif, system-ui, sans-serif;`);
|
|
153
|
+
}
|
|
154
|
+
if (heading) {
|
|
155
|
+
vars.push(` --velu-font-heading: '${heading.family}', ui-sans-serif, system-ui, sans-serif;`);
|
|
156
|
+
}
|
|
157
|
+
if (vars.length) {
|
|
158
|
+
lines.push(':root {');
|
|
159
|
+
lines.push(...vars);
|
|
160
|
+
lines.push('}');
|
|
161
|
+
}
|
|
162
|
+
// Heading font-family rule
|
|
163
|
+
if (heading) {
|
|
164
|
+
lines.push('h1, h2, h3, h4, h5, h6 {');
|
|
165
|
+
lines.push(` font-family: var(--velu-font-heading);`);
|
|
166
|
+
if (heading.weight) lines.push(` font-weight: ${heading.weight};`);
|
|
167
|
+
lines.push('}');
|
|
168
|
+
}
|
|
126
169
|
}
|
|
127
170
|
|
|
128
171
|
return lines.length > 0 ? lines.join('\n') : null;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { defineConfig, defineDocs } from 'fumadocs-mdx/config';
|
|
2
2
|
import { metaSchema, pageSchema } from 'fumadocs-core/source/schema';
|
|
3
3
|
import { transformerMetaHighlight } from '@shikijs/transformers';
|
|
4
|
-
import { remarkCodeFilenameToTitle, sharedRehypeCodeOptions } from '@core/lib/remark-plugins';
|
|
4
|
+
import { remarkCodeFilenameToTitle, remarkStripJsxFromHeadings, sharedRehypeCodeOptions } from '@core/lib/remark-plugins';
|
|
5
5
|
import { z } from 'zod';
|
|
6
6
|
|
|
7
7
|
const contentDir = process.env.PREVIEW_CONTENT_DIR || 'content/docs';
|
|
@@ -27,7 +27,7 @@ export const docs = defineDocs({
|
|
|
27
27
|
|
|
28
28
|
export default defineConfig({
|
|
29
29
|
mdxOptions: {
|
|
30
|
-
remarkPlugins: [remarkCodeFilenameToTitle],
|
|
30
|
+
remarkPlugins: [remarkStripJsxFromHeadings, remarkCodeFilenameToTitle],
|
|
31
31
|
rehypeCodeOptions: ({
|
|
32
32
|
...sharedRehypeCodeOptions,
|
|
33
33
|
transformers: [transformerMetaHighlight()],
|
|
@@ -68,6 +68,7 @@ function initAssistant() {
|
|
|
68
68
|
eventSource: EventSource | null;
|
|
69
69
|
expanded: boolean;
|
|
70
70
|
bootstrapped: boolean;
|
|
71
|
+
feedback: Record<string, 'up' | 'down'>;
|
|
71
72
|
} = {
|
|
72
73
|
conversationId: null,
|
|
73
74
|
conversationToken: null,
|
|
@@ -75,6 +76,7 @@ function initAssistant() {
|
|
|
75
76
|
eventSource: null,
|
|
76
77
|
expanded: false,
|
|
77
78
|
bootstrapped: false,
|
|
79
|
+
feedback: {},
|
|
78
80
|
};
|
|
79
81
|
|
|
80
82
|
const askBar = document.getElementById('veluAskBar')!;
|
|
@@ -93,6 +95,7 @@ function initAssistant() {
|
|
|
93
95
|
sessionStorage.setItem('velu-conv-id', state.conversationId || '');
|
|
94
96
|
sessionStorage.setItem('velu-conv-token', state.conversationToken || '');
|
|
95
97
|
sessionStorage.setItem('velu-last-seq', String(state.lastSeq));
|
|
98
|
+
sessionStorage.setItem('velu-feedback', JSON.stringify(state.feedback));
|
|
96
99
|
} catch {}
|
|
97
100
|
}
|
|
98
101
|
|
|
@@ -117,6 +120,7 @@ function initAssistant() {
|
|
|
117
120
|
state.conversationId = null;
|
|
118
121
|
state.conversationToken = null;
|
|
119
122
|
state.lastSeq = 0;
|
|
123
|
+
state.feedback = {};
|
|
120
124
|
if (state.eventSource) { state.eventSource.close(); state.eventSource = null; }
|
|
121
125
|
messagesEl.innerHTML = '';
|
|
122
126
|
chatInput.value = '';
|
|
@@ -143,6 +147,28 @@ function initAssistant() {
|
|
|
143
147
|
.catch(() => {});
|
|
144
148
|
}
|
|
145
149
|
|
|
150
|
+
function submitFeedback(messageId: number, rating: 'up' | 'down') {
|
|
151
|
+
return fetch(API_BASE + '/messages/' + messageId + '/feedback', {
|
|
152
|
+
method: 'POST',
|
|
153
|
+
headers: { 'Content-Type': 'application/json' },
|
|
154
|
+
credentials: 'include',
|
|
155
|
+
body: JSON.stringify({ rating }),
|
|
156
|
+
}).then((r) => r.json());
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function retractFeedback(messageId: number) {
|
|
160
|
+
return fetch(API_BASE + '/messages/' + messageId + '/feedback', {
|
|
161
|
+
method: 'DELETE',
|
|
162
|
+
credentials: 'include',
|
|
163
|
+
}).then((r) => r.json());
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function saveFeedbackState() {
|
|
167
|
+
try {
|
|
168
|
+
sessionStorage.setItem('velu-feedback', JSON.stringify(state.feedback));
|
|
169
|
+
} catch {}
|
|
170
|
+
}
|
|
171
|
+
|
|
146
172
|
function formatContent(text: string, citations: any[]) {
|
|
147
173
|
let html = text
|
|
148
174
|
.replace(/&/g, '&')
|
|
@@ -162,9 +188,10 @@ function initAssistant() {
|
|
|
162
188
|
return html;
|
|
163
189
|
}
|
|
164
190
|
|
|
165
|
-
function addMessage(role: string, content: string, citations: any[] = []) {
|
|
191
|
+
function addMessage(role: string, content: string, citations: any[] = [], messageId?: number) {
|
|
166
192
|
const msgDiv = document.createElement('div');
|
|
167
193
|
msgDiv.className = 'velu-msg velu-msg-' + role;
|
|
194
|
+
if (messageId) msgDiv.setAttribute('data-message-id', String(messageId));
|
|
168
195
|
const bubble = document.createElement('div');
|
|
169
196
|
bubble.className = 'velu-msg-bubble velu-msg-bubble-' + role;
|
|
170
197
|
bubble.innerHTML = formatContent(content, citations);
|
|
@@ -188,17 +215,121 @@ function initAssistant() {
|
|
|
188
215
|
const actions = document.createElement('div');
|
|
189
216
|
actions.className = 'velu-msg-actions';
|
|
190
217
|
actions.innerHTML =
|
|
191
|
-
'<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>' +
|
|
192
|
-
'<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>' +
|
|
193
|
-
'<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>'
|
|
218
|
+
'<button class="velu-msg-action velu-msg-like" 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>' +
|
|
219
|
+
'<button class="velu-msg-action velu-msg-dislike" 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>' +
|
|
220
|
+
'<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>' +
|
|
221
|
+
'<button class="velu-msg-action velu-msg-regenerate" title="Regenerate"><svg width="12" height="12" 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"/><polyline points="23 20 23 14 17 14"/><path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"/></svg></button>';
|
|
194
222
|
msgDiv.appendChild(actions);
|
|
195
223
|
|
|
196
|
-
|
|
224
|
+
// Restore feedback state if we have it for this message
|
|
225
|
+
const existingRating = messageId ? state.feedback[messageId] : null;
|
|
226
|
+
const likeBtn = actions.querySelector('.velu-msg-like') as HTMLElement;
|
|
227
|
+
const dislikeBtn = actions.querySelector('.velu-msg-dislike') as HTMLElement;
|
|
228
|
+
if (existingRating === 'up') likeBtn.classList.add('velu-msg-action-active');
|
|
229
|
+
if (existingRating === 'down') dislikeBtn.classList.add('velu-msg-action-active');
|
|
230
|
+
|
|
231
|
+
// Like button
|
|
232
|
+
if (likeBtn && dislikeBtn) {
|
|
233
|
+
likeBtn.onclick = () => {
|
|
234
|
+
if (!messageId) return;
|
|
235
|
+
const isActive = likeBtn.classList.contains('velu-msg-action-active');
|
|
236
|
+
if (isActive) {
|
|
237
|
+
// Retract
|
|
238
|
+
likeBtn.classList.remove('velu-msg-action-active');
|
|
239
|
+
delete state.feedback[messageId];
|
|
240
|
+
saveFeedbackState();
|
|
241
|
+
retractFeedback(messageId).catch(() => {});
|
|
242
|
+
} else {
|
|
243
|
+
// Submit up
|
|
244
|
+
likeBtn.classList.add('velu-msg-action-active');
|
|
245
|
+
dislikeBtn.classList.remove('velu-msg-action-active');
|
|
246
|
+
state.feedback[messageId] = 'up';
|
|
247
|
+
saveFeedbackState();
|
|
248
|
+
submitFeedback(messageId, 'up').catch(() => {});
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
dislikeBtn.onclick = () => {
|
|
252
|
+
if (!messageId) return;
|
|
253
|
+
const isActive = dislikeBtn.classList.contains('velu-msg-action-active');
|
|
254
|
+
if (isActive) {
|
|
255
|
+
// Retract
|
|
256
|
+
dislikeBtn.classList.remove('velu-msg-action-active');
|
|
257
|
+
delete state.feedback[messageId];
|
|
258
|
+
saveFeedbackState();
|
|
259
|
+
retractFeedback(messageId).catch(() => {});
|
|
260
|
+
} else {
|
|
261
|
+
// Submit down
|
|
262
|
+
dislikeBtn.classList.add('velu-msg-action-active');
|
|
263
|
+
likeBtn.classList.remove('velu-msg-action-active');
|
|
264
|
+
state.feedback[messageId] = 'down';
|
|
265
|
+
saveFeedbackState();
|
|
266
|
+
submitFeedback(messageId, 'down').catch(() => {});
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Copy button
|
|
272
|
+
const copyBtn = actions.querySelector('.velu-msg-copy') as HTMLElement;
|
|
197
273
|
if (copyBtn) {
|
|
198
|
-
|
|
274
|
+
copyBtn.onclick = () => {
|
|
199
275
|
navigator.clipboard.writeText(content);
|
|
200
|
-
|
|
201
|
-
|
|
276
|
+
copyBtn.classList.add('velu-msg-action-active');
|
|
277
|
+
copyBtn.title = 'Copied!';
|
|
278
|
+
setTimeout(() => { copyBtn.classList.remove('velu-msg-action-active'); copyBtn.title = 'Copy'; }, 1500);
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Regenerate button
|
|
283
|
+
const regenBtn = actions.querySelector('.velu-msg-regenerate') as HTMLElement;
|
|
284
|
+
if (regenBtn) {
|
|
285
|
+
regenBtn.onclick = () => {
|
|
286
|
+
const allMsgs = messagesEl.querySelectorAll('.velu-msg');
|
|
287
|
+
let lastUserText = '';
|
|
288
|
+
for (let i = allMsgs.length - 1; i >= 0; i--) {
|
|
289
|
+
if (allMsgs[i].classList.contains('velu-msg-user')) {
|
|
290
|
+
const bubble = allMsgs[i].querySelector('.velu-msg-bubble');
|
|
291
|
+
if (bubble) lastUserText = bubble.textContent || '';
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (lastUserText) {
|
|
296
|
+
msgDiv.remove();
|
|
297
|
+
saveState();
|
|
298
|
+
addThinking();
|
|
299
|
+
bootstrap()
|
|
300
|
+
.then(() =>
|
|
301
|
+
fetch(API_BASE + '/messages', {
|
|
302
|
+
method: 'POST',
|
|
303
|
+
headers: { 'Content-Type': 'application/json' },
|
|
304
|
+
credentials: 'include',
|
|
305
|
+
body: JSON.stringify({
|
|
306
|
+
message: lastUserText,
|
|
307
|
+
conversation_id: state.conversationId,
|
|
308
|
+
}),
|
|
309
|
+
})
|
|
310
|
+
)
|
|
311
|
+
.then((r) => {
|
|
312
|
+
if (r.status === 429) {
|
|
313
|
+
removeThinking();
|
|
314
|
+
addMessage('assistant', 'Rate limited. Please wait a moment and try again.');
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
return r.json();
|
|
318
|
+
})
|
|
319
|
+
.then((data: any) => {
|
|
320
|
+
if (!data) return;
|
|
321
|
+
if (data.conversation_id) state.conversationId = data.conversation_id;
|
|
322
|
+
if (data.conversation_token) state.conversationToken = data.conversation_token;
|
|
323
|
+
saveState();
|
|
324
|
+
if (!state.eventSource || state.eventSource.readyState === 2) {
|
|
325
|
+
connectSSE();
|
|
326
|
+
}
|
|
327
|
+
})
|
|
328
|
+
.catch(() => {
|
|
329
|
+
removeThinking();
|
|
330
|
+
addMessage('assistant', 'Failed to connect. Please try again.');
|
|
331
|
+
});
|
|
332
|
+
}
|
|
202
333
|
};
|
|
203
334
|
}
|
|
204
335
|
}
|
|
@@ -233,7 +364,7 @@ function initAssistant() {
|
|
|
233
364
|
const data = JSON.parse(e.data);
|
|
234
365
|
const msg = data.message || data;
|
|
235
366
|
if (msg.seq) state.lastSeq = msg.seq;
|
|
236
|
-
addMessage('assistant', msg.content || '', msg.citations || []);
|
|
367
|
+
addMessage('assistant', msg.content || '', msg.citations || [], msg.id);
|
|
237
368
|
} catch {}
|
|
238
369
|
});
|
|
239
370
|
|
|
@@ -342,9 +473,11 @@ function initAssistant() {
|
|
|
342
473
|
const savedConvId = sessionStorage.getItem('velu-conv-id');
|
|
343
474
|
const savedConvToken = sessionStorage.getItem('velu-conv-token');
|
|
344
475
|
const savedSeq = sessionStorage.getItem('velu-last-seq');
|
|
476
|
+
const savedFeedback = sessionStorage.getItem('velu-feedback');
|
|
345
477
|
if (savedConvId) state.conversationId = savedConvId;
|
|
346
478
|
if (savedConvToken) state.conversationToken = savedConvToken;
|
|
347
479
|
if (savedSeq) state.lastSeq = parseInt(savedSeq, 10) || 0;
|
|
480
|
+
if (savedFeedback) try { state.feedback = JSON.parse(savedFeedback); } catch {}
|
|
348
481
|
if (savedMessages) messagesEl.innerHTML = savedMessages;
|
|
349
482
|
if (savedExpanded === '1') {
|
|
350
483
|
state.expanded = true;
|
|
@@ -225,6 +225,14 @@
|
|
|
225
225
|
background-color: var(--color-fd-accent, #27272a);
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
+
.velu-msg-action.velu-msg-action-active {
|
|
229
|
+
color: var(--color-fd-primary, #818cf8);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.velu-msg-action.velu-msg-action-active svg {
|
|
233
|
+
fill: currentColor;
|
|
234
|
+
}
|
|
235
|
+
|
|
228
236
|
/* Thinking dots */
|
|
229
237
|
.velu-thinking-dots {
|
|
230
238
|
display: inline-flex;
|
|
@@ -1008,6 +1008,11 @@ nextjs-portal {
|
|
|
1008
1008
|
margin: 0 !important;
|
|
1009
1009
|
}
|
|
1010
1010
|
|
|
1011
|
+
/* Add spacing between consecutive card groups (Mintlify compat) */
|
|
1012
|
+
.velu-card-group + .velu-card-group {
|
|
1013
|
+
margin-top: 0.75rem;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1011
1016
|
.velu-card-image {
|
|
1012
1017
|
width: 100%;
|
|
1013
1018
|
border-radius: 0.55rem;
|
|
@@ -89,6 +89,44 @@ export function remarkCodeFilenameToTitle() {
|
|
|
89
89
|
return (tree: any) => visit(tree);
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Remark plugin that strips JSX/MDX elements (like <Icon />) from headings.
|
|
94
|
+
*
|
|
95
|
+
* fumadocs' rehypeToc plugin converts heading content into a static
|
|
96
|
+
* `export const toc = [...]` at the MDX module level. MDX components
|
|
97
|
+
* (e.g. Icon) are only injected at render time via getMDXComponents(),
|
|
98
|
+
* so any JSX reference in a heading causes "ReferenceError: X is not defined"
|
|
99
|
+
* when the toc export evaluates.
|
|
100
|
+
*
|
|
101
|
+
* This plugin removes mdxJsxFlowElement / mdxJsxTextElement nodes from
|
|
102
|
+
* headings before TOC extraction runs. The heading body still renders
|
|
103
|
+
* with the component because MDX components are in scope at render time.
|
|
104
|
+
*/
|
|
105
|
+
export function remarkStripJsxFromHeadings() {
|
|
106
|
+
return (tree: any) => {
|
|
107
|
+
if (!tree || !Array.isArray(tree.children)) return;
|
|
108
|
+
|
|
109
|
+
function visitNode(node: any) {
|
|
110
|
+
if (
|
|
111
|
+
node.type === 'heading' &&
|
|
112
|
+
Array.isArray(node.children)
|
|
113
|
+
) {
|
|
114
|
+
node.children = node.children.filter(
|
|
115
|
+
(child: any) =>
|
|
116
|
+
child.type !== 'mdxJsxTextElement' &&
|
|
117
|
+
child.type !== 'mdxJsxFlowElement',
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (Array.isArray(node.children)) {
|
|
122
|
+
for (const child of node.children) visitNode(child);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
visitNode(tree);
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
92
130
|
/**
|
|
93
131
|
* Shared rehype code options for consistent syntax highlighting.
|
|
94
132
|
*/
|
|
@@ -190,6 +190,7 @@ export function getMDXComponents(opts: { iconLibrary: VeluIconLibrary; apiConfig
|
|
|
190
190
|
},
|
|
191
191
|
Accordions: FumaAccordions as any,
|
|
192
192
|
Accordion: FumaAccordion as any,
|
|
193
|
+
AccordionGroup: FumaAccordions as any,
|
|
193
194
|
CodeGroup: VeluCodeGroup as any,
|
|
194
195
|
Frame: ({ children, caption, hint, className }: any) => (
|
|
195
196
|
<>
|
package/src/validate.ts
CHANGED
|
@@ -233,30 +233,43 @@ function isPageString(item: unknown): item is string {
|
|
|
233
233
|
return typeof item === "string";
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
+
interface PageWithTab {
|
|
237
|
+
page: string;
|
|
238
|
+
tab: string | null;
|
|
239
|
+
tabSlug: string | null;
|
|
240
|
+
}
|
|
241
|
+
|
|
236
242
|
function collectPagesFromTabs(tabs: VeluTab[]): string[] {
|
|
237
|
-
|
|
243
|
+
return collectPagesWithTabsFromTabs(tabs).map((p) => p.page);
|
|
244
|
+
}
|
|
238
245
|
|
|
239
|
-
|
|
246
|
+
function collectPagesWithTabsFromTabs(tabs: VeluTab[]): PageWithTab[] {
|
|
247
|
+
const pages: PageWithTab[] = [];
|
|
248
|
+
|
|
249
|
+
function collectFromGroup(group: VeluGroup, tab: string | null, tabSlug: string | null) {
|
|
240
250
|
for (const item of group.pages) {
|
|
241
251
|
if (isPageString(item)) {
|
|
242
|
-
pages.push(item);
|
|
252
|
+
pages.push({ page: item, tab, tabSlug });
|
|
243
253
|
} else if (isGroup(item)) {
|
|
244
|
-
collectFromGroup(item);
|
|
254
|
+
collectFromGroup(item, tab, tabSlug);
|
|
245
255
|
}
|
|
246
256
|
}
|
|
247
257
|
}
|
|
248
258
|
|
|
249
259
|
for (const tab of tabs) {
|
|
260
|
+
const tabName = tab.tab || null;
|
|
261
|
+
const tabSlug = tab.slug || tabName;
|
|
262
|
+
|
|
250
263
|
if (tab.pages) {
|
|
251
264
|
for (const item of tab.pages) {
|
|
252
265
|
if (isPageString(item)) {
|
|
253
|
-
pages.push(item);
|
|
266
|
+
pages.push({ page: item, tab: tabName, tabSlug });
|
|
254
267
|
}
|
|
255
268
|
}
|
|
256
269
|
}
|
|
257
270
|
if (tab.groups) {
|
|
258
271
|
for (const group of tab.groups) {
|
|
259
|
-
collectFromGroup(group);
|
|
272
|
+
collectFromGroup(group, tabName, tabSlug);
|
|
260
273
|
}
|
|
261
274
|
}
|
|
262
275
|
}
|
|
@@ -293,6 +306,28 @@ function collectPagesByLanguage(config: VeluConfig): Record<string, string[]> {
|
|
|
293
306
|
return grouped;
|
|
294
307
|
}
|
|
295
308
|
|
|
309
|
+
function collectPagesWithTabsByLanguage(config: VeluConfig): Record<string, PageWithTab[]> {
|
|
310
|
+
const grouped: Record<string, PageWithTab[]> = {};
|
|
311
|
+
|
|
312
|
+
if (config.navigation.languages && config.navigation.languages.length > 0) {
|
|
313
|
+
for (const lang of config.navigation.languages) {
|
|
314
|
+
grouped[lang.language] = collectPagesWithTabsFromTabs(lang.tabs);
|
|
315
|
+
}
|
|
316
|
+
return grouped;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const basePages = collectPagesWithTabsFromTabs(config.navigation.tabs ?? []);
|
|
320
|
+
if (config.languages && config.languages.length > 0) {
|
|
321
|
+
for (const lang of config.languages) {
|
|
322
|
+
grouped[lang] = [...basePages];
|
|
323
|
+
}
|
|
324
|
+
return grouped;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
grouped.en = basePages;
|
|
328
|
+
return grouped;
|
|
329
|
+
}
|
|
330
|
+
|
|
296
331
|
function validateVeluConfig(docsDir: string, schemaPath: string): { valid: boolean; errors: string[] } {
|
|
297
332
|
const errors: string[] = [];
|
|
298
333
|
|
|
@@ -362,4 +397,4 @@ function validateVeluConfig(docsDir: string, schemaPath: string): { valid: boole
|
|
|
362
397
|
return { valid: errors.length === 0, errors };
|
|
363
398
|
}
|
|
364
399
|
|
|
365
|
-
export { validateVeluConfig, collectPages, collectPagesByLanguage, VeluConfig, VeluGroup, VeluTab, VeluSeparator, VeluLink, VeluAnchor };
|
|
400
|
+
export { validateVeluConfig, collectPages, collectPagesByLanguage, collectPagesWithTabsByLanguage, VeluConfig, VeluGroup, VeluTab, VeluSeparator, VeluLink, VeluAnchor, type PageWithTab };
|