@aravindc26/velu 0.13.15 → 0.13.17

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": "@aravindc26/velu",
3
- "version": "0.13.15",
3
+ "version": "0.13.17",
4
4
  "description": "A modern documentation site generator powered by Markdown and JSON configuration",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,9 +1,9 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import { RootProvider } from 'fumadocs-ui/provider/next';
3
3
  import './global.css';
4
- import '@core/css/shared.css';
5
- import '@core/css/search.css';
6
- import '@core/css/copy-page.css';
4
+ import '../../engine-core/css/shared.css';
5
+ import '../../engine-core/css/search.css';
6
+ import '../../engine-core/css/copy-page.css';
7
7
 
8
8
  export const metadata = {
9
9
  title: 'Preview',
@@ -1,14 +1,14 @@
1
1
  import type { Metadata } from 'next';
2
2
  import type { ReactNode } from 'react';
3
- import { getAppearance, getBannerConfig, getFontsConfig, getSeoConfig, getSiteDescription, getSiteFavicon, getSiteName, getSiteOrigin, getSitePrimaryColor } from '@/lib/velu';
3
+ import { getAppearance, getBannerConfig, getCliVersion, getFontsConfig, getSeoConfig, getSiteDescription, getSiteFavicon, getSiteName, getSiteOrigin, getSitePrimaryColor } from '@/lib/velu';
4
4
  import { Providers } from '@/components/providers';
5
5
  import { VeluAssistant } from '@/components/assistant';
6
6
  import { VeluBanner } from '@/components/banner';
7
7
  import './global.css';
8
- import '@core/css/shared.css';
9
- import '@core/css/search.css';
10
- import '@core/css/assistant.css';
11
- import '@core/css/copy-page.css';
8
+ import '../engine-core/css/shared.css';
9
+ import '../engine-core/css/search.css';
10
+ import '../engine-core/css/assistant.css';
11
+ import '../engine-core/css/copy-page.css';
12
12
 
13
13
  function toAbsoluteUrl(origin: string, value: string): string {
14
14
  const trimmed = value.trim();
@@ -80,6 +80,7 @@ export default function RootLayout({ children }: { children: ReactNode }) {
80
80
  return (
81
81
  <html lang="en" suppressHydrationWarning>
82
82
  <head>
83
+ <meta name="generator" content={`velu-cli ${getCliVersion()}`} />
83
84
  <link rel="sitemap" type="application/xml" href={`${siteOrigin}/sitemap.xml`} />
84
85
  {fontsConfig && (() => {
85
86
  // Collect Google Fonts families that don't use a custom source
@@ -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, '&amp;')
@@ -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
- const copyBtn = actions.querySelector('.velu-msg-copy');
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
- (copyBtn as HTMLElement).onclick = () => {
274
+ copyBtn.onclick = () => {
199
275
  navigator.clipboard.writeText(content);
200
- (copyBtn as HTMLElement).title = 'Copied!';
201
- setTimeout(() => { (copyBtn as HTMLElement).title = 'Copy'; }, 1500);
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,10 +473,64 @@ 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;
348
- if (savedMessages) messagesEl.innerHTML = savedMessages;
480
+ if (savedFeedback) try { state.feedback = JSON.parse(savedFeedback); } catch {}
481
+ if (savedMessages) {
482
+ messagesEl.innerHTML = savedMessages;
483
+ // Re-bind action handlers on restored messages
484
+ messagesEl.querySelectorAll('.velu-msg-assistant').forEach((msgDiv) => {
485
+ const messageId = msgDiv.getAttribute('data-message-id');
486
+ const mid = messageId ? parseInt(messageId, 10) : null;
487
+ const likeBtn = msgDiv.querySelector('.velu-msg-like') as HTMLElement | null;
488
+ const dislikeBtn = msgDiv.querySelector('.velu-msg-dislike') as HTMLElement | null;
489
+ const copyBtn = msgDiv.querySelector('.velu-msg-copy') as HTMLElement | null;
490
+ const bubble = msgDiv.querySelector('.velu-msg-bubble') as HTMLElement | null;
491
+
492
+ if (likeBtn && dislikeBtn && mid) {
493
+ likeBtn.onclick = () => {
494
+ const isActive = likeBtn.classList.contains('velu-msg-action-active');
495
+ if (isActive) {
496
+ likeBtn.classList.remove('velu-msg-action-active');
497
+ delete state.feedback[mid];
498
+ saveFeedbackState();
499
+ retractFeedback(mid).catch(() => {});
500
+ } else {
501
+ likeBtn.classList.add('velu-msg-action-active');
502
+ dislikeBtn.classList.remove('velu-msg-action-active');
503
+ state.feedback[mid] = 'up';
504
+ saveFeedbackState();
505
+ submitFeedback(mid, 'up').catch(() => {});
506
+ }
507
+ };
508
+ dislikeBtn.onclick = () => {
509
+ const isActive = dislikeBtn.classList.contains('velu-msg-action-active');
510
+ if (isActive) {
511
+ dislikeBtn.classList.remove('velu-msg-action-active');
512
+ delete state.feedback[mid];
513
+ saveFeedbackState();
514
+ retractFeedback(mid).catch(() => {});
515
+ } else {
516
+ dislikeBtn.classList.add('velu-msg-action-active');
517
+ likeBtn.classList.remove('velu-msg-action-active');
518
+ state.feedback[mid] = 'down';
519
+ saveFeedbackState();
520
+ submitFeedback(mid, 'down').catch(() => {});
521
+ }
522
+ };
523
+ }
524
+ if (copyBtn && bubble) {
525
+ copyBtn.onclick = () => {
526
+ navigator.clipboard.writeText(bubble.textContent || '');
527
+ copyBtn.classList.add('velu-msg-action-active');
528
+ copyBtn.title = 'Copied!';
529
+ setTimeout(() => { copyBtn.classList.remove('velu-msg-action-active'); copyBtn.title = 'Copy'; }, 1500);
530
+ };
531
+ }
532
+ });
533
+ }
349
534
  if (savedExpanded === '1') {
350
535
  state.expanded = true;
351
536
  panel.classList.add('velu-assistant-expanded');
@@ -1032,3 +1032,14 @@ export function getSiteOrigin(src?: VeluConfigSource): string {
1032
1032
 
1033
1033
  return 'http://localhost:4321';
1034
1034
  }
1035
+
1036
+ export function getCliVersion(): string {
1037
+ try {
1038
+ const constPath = resolve(process.cwd(), 'public', 'const.json');
1039
+ if (existsSync(constPath)) {
1040
+ const parsed = JSON.parse(readFileSync(constPath, 'utf-8'));
1041
+ if (typeof parsed.version === 'string') return parsed.version;
1042
+ }
1043
+ } catch {}
1044
+ return 'unknown';
1045
+ }