@retrivora-ai/rag-engine 1.8.0 → 1.8.2

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.
Files changed (71) hide show
  1. package/dist/DocumentChunker-Dh9TvmGG.d.mts +45 -0
  2. package/dist/DocumentChunker-Dh9TvmGG.d.ts +45 -0
  3. package/dist/{index-DPsQodME.d.mts → ILLMProvider-BOJFz3Na.d.mts} +104 -1
  4. package/dist/{index-DPsQodME.d.ts → ILLMProvider-BOJFz3Na.d.ts} +104 -1
  5. package/dist/MultiTablePostgresProvider-ZLGSKTJR.mjs +8 -0
  6. package/dist/chunk-ICKRMZQK.mjs +76 -0
  7. package/dist/{chunk-PV3MFHWU.mjs → chunk-LZVVLSDN.mjs} +977 -516
  8. package/dist/chunk-OZFBG4BA.mjs +291 -0
  9. package/dist/handlers/index.d.mts +2 -2
  10. package/dist/handlers/index.d.ts +2 -2
  11. package/dist/handlers/index.js +1269 -656
  12. package/dist/handlers/index.mjs +4 -1
  13. package/dist/{index-Bb2yEopi.d.mts → index-BwpcaziY.d.ts} +10 -2
  14. package/dist/{index-CkbTzj9J.d.ts → index-D3V9Et2M.d.mts} +10 -2
  15. package/dist/index.d.mts +24 -4
  16. package/dist/index.d.ts +24 -4
  17. package/dist/index.js +1354 -826
  18. package/dist/index.mjs +1284 -795
  19. package/dist/server.d.mts +47 -27
  20. package/dist/server.d.ts +47 -27
  21. package/dist/server.js +1417 -829
  22. package/dist/server.mjs +164 -176
  23. package/package.json +6 -2
  24. package/src/app/api/upload/route.ts +4 -0
  25. package/src/app/constants.tsx +2 -2
  26. package/src/app/page.tsx +12 -322
  27. package/src/components/AmbientBackground.tsx +29 -0
  28. package/src/components/ArchitectureCard.tsx +17 -0
  29. package/src/components/ArchitectureCardsSection.tsx +15 -0
  30. package/src/components/ChatWindow.tsx +32 -0
  31. package/src/components/CodeViewer.tsx +51 -0
  32. package/src/components/ConfigProvider.tsx +1 -0
  33. package/src/components/DocViewer.tsx +37 -0
  34. package/src/components/DocumentUpload.tsx +44 -1
  35. package/src/components/Documentation.tsx +58 -0
  36. package/src/components/DynamicChart.tsx +27 -2
  37. package/src/components/Hero.tsx +59 -0
  38. package/src/components/HourglassLoader.tsx +87 -0
  39. package/src/components/Lifecycle.tsx +37 -0
  40. package/src/components/MarkdownComponents.tsx +140 -0
  41. package/src/components/MessageBubble.tsx +124 -904
  42. package/src/components/Navbar.tsx +55 -0
  43. package/src/components/ObservabilityPanel.tsx +374 -0
  44. package/src/components/ProductCard.tsx +5 -3
  45. package/src/components/UIDispatcher.tsx +344 -0
  46. package/src/components/VisualizationRenderer.tsx +372 -250
  47. package/src/config/RagConfig.ts +5 -0
  48. package/src/config/serverConfig.ts +3 -1
  49. package/src/core/Pipeline.ts +240 -271
  50. package/src/core/ProviderRegistry.ts +2 -2
  51. package/src/core/VectorPlugin.ts +9 -0
  52. package/src/handlers/index.ts +91 -15
  53. package/src/hooks/useRagChat.ts +21 -11
  54. package/src/index.ts +9 -1
  55. package/src/llm/LLMFactory.ts +54 -2
  56. package/src/llm/providers/AnthropicProvider.ts +12 -8
  57. package/src/llm/providers/GeminiProvider.ts +188 -143
  58. package/src/llm/providers/OllamaProvider.ts +7 -3
  59. package/src/llm/providers/OpenAIProvider.ts +12 -8
  60. package/src/providers/vectordb/MultiTablePostgresProvider.ts +150 -64
  61. package/src/types/chat.ts +8 -0
  62. package/src/types/index.ts +132 -0
  63. package/src/types/props.ts +9 -1
  64. package/src/utils/ProductExtractor.ts +347 -0
  65. package/src/utils/SchemaMapper.ts +129 -0
  66. package/src/utils/UITransformer.ts +470 -209
  67. package/src/utils/synonyms.ts +78 -0
  68. package/dist/DocumentChunker-C1GEEosY.d.ts +0 -93
  69. package/dist/DocumentChunker-CFEiRopR.d.mts +0 -93
  70. package/dist/PostgreSQLProvider-BMOETDZA.mjs +0 -8
  71. package/dist/chunk-FLOSGE6A.mjs +0 -202
@@ -0,0 +1,55 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import { Zap, Package, GitBranch, HeartPulse } from 'lucide-react';
5
+ import { ThemeToggle } from '@/components/ThemeToggle';
6
+ import { useConfig } from '@/components/ConfigProvider';
7
+ import pkg from '../../package.json';
8
+
9
+ export function Navbar() {
10
+ const { ui } = useConfig();
11
+
12
+ return (
13
+ <nav className="sticky top-0 z-50 flex items-center justify-between px-8 py-5 border-b border-slate-200 dark:border-white/5 bg-white/80 dark:bg-[#080811]/80 backdrop-blur-md">
14
+ <div className="flex items-center gap-3 group cursor-default">
15
+ <div className="relative">
16
+ <div
17
+ className="w-10 h-10 rounded-xl flex items-center justify-center shadow-lg relative z-10 overflow-hidden transition-transform group-hover:scale-110 duration-500"
18
+ style={{ background: `linear-gradient(135deg, ${ui.primaryColor}, ${ui.accentColor})` }}
19
+ >
20
+ <Zap className="w-5 h-5 text-white animate-pulse" />
21
+ <div className="absolute inset-0 bg-white/20 opacity-0 group-hover:opacity-100 transition-opacity" />
22
+ </div>
23
+ <div
24
+ className="absolute inset-0 blur-lg opacity-40 group-hover:opacity-80 transition-opacity"
25
+ style={{ background: ui.primaryColor }}
26
+ />
27
+ </div>
28
+ <div className="flex flex-col -gap-0.5">
29
+ <span className="font-black text-transparent bg-clip-text text-xl tracking-tight leading-none"
30
+ style={{ backgroundImage: `linear-gradient(to right, ${ui.primaryColor}, ${ui.accentColor})` }}
31
+ >
32
+ Retrivora&nbsp;AI
33
+ </span>
34
+ <div className="flex items-center gap-1.5">
35
+ <span className="font-bold text-slate-900 dark:text-white text-[10px] tracking-[0.15em] uppercase opacity-70">Accelerator</span>
36
+ <span className="text-[9px] px-1.5 py-0.5 rounded-full bg-slate-100 dark:bg-white/5 text-slate-500 dark:text-white/40 font-mono border border-slate-200/50 dark:border-white/10">v{pkg?.version}</span>
37
+ </div>
38
+ </div>
39
+ </div>
40
+
41
+ <div className="flex items-center gap-3">
42
+ <ThemeToggle />
43
+ <a href="https://www.npmjs.com/package/@retrivora-ai/rag-engine" target="_blank" rel="noreferrer" className="text-slate-500 hover:text-[#CB3837] dark:text-white/40 dark:hover:text-[#CB3837] text-sm transition-colors flex items-center gap-1.5">
44
+ <Package className="w-4 h-4" /> NPM
45
+ </a>
46
+ <a href="https://github.com/abhinav1201/ai-accelerator" target="_blank" rel="noreferrer" className="text-slate-500 hover:text-slate-900 dark:text-white/40 dark:hover:text-white/70 text-sm transition-colors flex items-center gap-1.5">
47
+ <GitBranch className="w-4 h-4" /> GitHub
48
+ </a>
49
+ <a href="/api/health" target="_blank" rel="noreferrer" className="px-3 py-1.5 rounded-lg text-xs border border-slate-200 dark:border-white/10 text-slate-600 dark:text-white/50 hover:text-slate-900 dark:hover:text-white/80 hover:bg-slate-100 dark:hover:bg-white/5 transition-all flex items-center gap-1.5">
50
+ <HeartPulse className="w-3.5 h-3.5" /> Health
51
+ </a>
52
+ </div>
53
+ </nav>
54
+ );
55
+ }
@@ -0,0 +1,374 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import {
5
+ Clock, Cpu, Zap, AlertTriangle, FileText, Layers,
6
+ ChevronDown, ChevronRight, CheckCircle, AlertCircle,
7
+ } from 'lucide-react';
8
+ import { ObservabilityTrace } from '../types';
9
+
10
+ // ─── Props ────────────────────────────────────────────────────────────────────
11
+
12
+ interface ObservabilityPanelProps {
13
+ trace: ObservabilityTrace;
14
+ primaryColor?: string;
15
+ }
16
+
17
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
18
+
19
+ function fmtMs(ms: number): string {
20
+ return ms >= 1000 ? `${(ms / 1000).toFixed(2)}s` : `${Math.round(ms)}ms`;
21
+ }
22
+
23
+ function fmtTokens(n: number): string {
24
+ return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n);
25
+ }
26
+
27
+ function ScoreBadge({ score }: { score: number }) {
28
+ const pct = Math.round(score * 100);
29
+ const color =
30
+ pct <= 20 ? 'text-emerald-600 bg-emerald-50 border-emerald-200 dark:text-emerald-400 dark:bg-emerald-500/10 dark:border-emerald-500/20'
31
+ : pct <= 50 ? 'text-amber-600 bg-amber-50 border-amber-200 dark:text-amber-400 dark:bg-amber-500/10 dark:border-amber-500/20'
32
+ : 'text-red-600 bg-red-50 border-red-200 dark:text-red-400 dark:bg-red-500/10 dark:border-red-500/20';
33
+
34
+ return (
35
+ <span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[11px] font-semibold border ${color}`}>
36
+ {pct <= 20 ? <CheckCircle className="w-3 h-3" /> : <AlertCircle className="w-3 h-3" />}
37
+ {pct}% hallucination risk
38
+ </span>
39
+ );
40
+ }
41
+
42
+ // ─── Tab content renderers ────────────────────────────────────────────────────
43
+
44
+ function RetrievalTab({ trace }: { trace: ObservabilityTrace }) {
45
+ return (
46
+ <div className="space-y-2">
47
+ <p className="text-[11px] text-slate-500 dark:text-white/40">
48
+ {trace.chunks.length} chunk{trace.chunks.length !== 1 ? 's' : ''} retrieved
49
+ {trace.rewrittenQuery && (
50
+ <span className="ml-2 italic">
51
+ (query rewritten: <em className="text-indigo-500">{trace.rewrittenQuery}</em>)
52
+ </span>
53
+ )}
54
+ </p>
55
+ <div className="space-y-1.5 max-h-56 overflow-y-auto pr-1 custom-scrollbar">
56
+ {trace.chunks.map((chunk, i) => (
57
+ <div
58
+ key={String(chunk.id)}
59
+ className="rounded-lg border border-slate-100 dark:border-white/8 bg-slate-50 dark:bg-white/3 p-2.5"
60
+ >
61
+ <div className="flex items-center justify-between mb-1">
62
+ <span className="text-[10px] font-mono text-slate-400 dark:text-white/30">
63
+ [{i + 1}] {String(chunk.id).slice(0, 20)}
64
+ </span>
65
+ <span className="text-[10px] font-semibold text-indigo-600 dark:text-indigo-400">
66
+ score: {chunk.score.toFixed(3)}
67
+ </span>
68
+ </div>
69
+ {/* Score bar */}
70
+ <div className="h-1 rounded-full bg-slate-200 dark:bg-white/10 mb-2 overflow-hidden">
71
+ <div
72
+ className="h-full rounded-full bg-indigo-500 dark:bg-indigo-400 transition-all"
73
+ style={{ width: `${Math.min(100, chunk.score * 100)}%` }}
74
+ />
75
+ </div>
76
+ <p className="text-[11px] text-slate-600 dark:text-white/60 line-clamp-2 leading-relaxed">
77
+ {chunk.content}
78
+ </p>
79
+ </div>
80
+ ))}
81
+ </div>
82
+ </div>
83
+ );
84
+ }
85
+
86
+ function LatencyTab({ trace }: { trace: ObservabilityTrace }) {
87
+ const { latency } = trace;
88
+ const bars = [
89
+ { label: 'Embed', ms: latency.embedMs, color: '#6366f1' },
90
+ { label: 'Retrieve', ms: latency.retrieveMs, color: '#10b981' },
91
+ ...(latency.rerankMs != null ? [{ label: 'Rerank', ms: latency.rerankMs, color: '#f59e0b' }] : []),
92
+ { label: 'Generate', ms: latency.generateMs, color: '#8b5cf6' },
93
+ ];
94
+ const maxMs = Math.max(...bars.map(b => b.ms), 1);
95
+
96
+ return (
97
+ <div className="space-y-3">
98
+ <div className="flex items-center justify-between">
99
+ <span className="text-[11px] text-slate-500 dark:text-white/40">Pipeline stages</span>
100
+ <span className="text-[11px] font-semibold text-slate-700 dark:text-white/70">
101
+ Total: {fmtMs(latency.totalMs)}
102
+ </span>
103
+ </div>
104
+ <div className="space-y-2">
105
+ {bars.map((bar) => (
106
+ <div key={bar.label} className="space-y-0.5">
107
+ <div className="flex items-center justify-between">
108
+ <span className="text-[11px] text-slate-600 dark:text-white/60">{bar.label}</span>
109
+ <span className="text-[11px] font-mono text-slate-500 dark:text-white/40">{fmtMs(bar.ms)}</span>
110
+ </div>
111
+ <div className="h-2 rounded-full bg-slate-100 dark:bg-white/8 overflow-hidden">
112
+ <div
113
+ className="h-full rounded-full transition-all duration-700"
114
+ style={{
115
+ width: `${(bar.ms / maxMs) * 100}%`,
116
+ background: bar.color,
117
+ opacity: 0.85,
118
+ }}
119
+ />
120
+ </div>
121
+ </div>
122
+ ))}
123
+ </div>
124
+ </div>
125
+ );
126
+ }
127
+
128
+ function TokensTab({ trace }: { trace: ObservabilityTrace }) {
129
+ const { tokens } = trace;
130
+ if (!tokens) {
131
+ return <p className="text-[11px] text-slate-400 dark:text-white/30 italic">Token data not available.</p>;
132
+ }
133
+
134
+ const stats = [
135
+ { label: 'Prompt Tokens', value: fmtTokens(tokens.promptTokens), color: 'text-indigo-600 dark:text-indigo-400' },
136
+ { label: 'Completion Tokens', value: fmtTokens(tokens.completionTokens), color: 'text-emerald-600 dark:text-emerald-400' },
137
+ { label: 'Total Tokens', value: fmtTokens(tokens.totalTokens), color: 'text-slate-700 dark:text-white/80' },
138
+ ];
139
+
140
+ return (
141
+ <div className="space-y-3">
142
+ <div className="grid grid-cols-3 gap-2">
143
+ {stats.map((s) => (
144
+ <div key={s.label} className="rounded-lg border border-slate-100 dark:border-white/8 bg-slate-50 dark:bg-white/3 p-2.5 text-center">
145
+ <p className={`text-lg font-bold ${s.color}`}>{s.value}</p>
146
+ <p className="text-[10px] text-slate-400 dark:text-white/30 mt-0.5">{s.label}</p>
147
+ </div>
148
+ ))}
149
+ </div>
150
+ {tokens.estimatedCostUsd != null && (
151
+ <div className="flex items-center justify-between rounded-lg border border-emerald-100 dark:border-emerald-500/20 bg-emerald-50 dark:bg-emerald-500/10 px-3 py-2">
152
+ <span className="text-[11px] text-emerald-700 dark:text-emerald-400 font-medium">Estimated Cost</span>
153
+ <span className="text-[11px] font-bold text-emerald-700 dark:text-emerald-400">
154
+ ${tokens.estimatedCostUsd < 0.0001
155
+ ? tokens.estimatedCostUsd.toExponential(2)
156
+ : tokens.estimatedCostUsd.toFixed(5)} USD
157
+ </span>
158
+ </div>
159
+ )}
160
+ {/* Visual token proportion bar */}
161
+ <div className="h-2 rounded-full overflow-hidden flex">
162
+ <div
163
+ className="h-full bg-indigo-500 dark:bg-indigo-400"
164
+ style={{ width: `${(tokens.promptTokens / tokens.totalTokens) * 100}%` }}
165
+ title="Prompt tokens"
166
+ />
167
+ <div
168
+ className="h-full bg-emerald-500 dark:bg-emerald-400"
169
+ style={{ width: `${(tokens.completionTokens / tokens.totalTokens) * 100}%` }}
170
+ title="Completion tokens"
171
+ />
172
+ </div>
173
+ <div className="flex gap-3 text-[10px] text-slate-400 dark:text-white/30">
174
+ <span className="flex items-center gap-1"><span className="inline-block w-2 h-2 rounded-full bg-indigo-500" /> Prompt</span>
175
+ <span className="flex items-center gap-1"><span className="inline-block w-2 h-2 rounded-full bg-emerald-500" /> Completion</span>
176
+ </div>
177
+ </div>
178
+ );
179
+ }
180
+
181
+ function HallucinationTab({ trace }: { trace: ObservabilityTrace }) {
182
+ if (trace.hallucinationScore == null) {
183
+ return <p className="text-[11px] text-slate-400 dark:text-white/30 italic">Hallucination scoring not available for this response.</p>;
184
+ }
185
+
186
+ const score = trace.hallucinationScore;
187
+ const pct = Math.round(score * 100);
188
+ const barColor = pct <= 20 ? '#10b981' : pct <= 50 ? '#f59e0b' : '#ef4444';
189
+
190
+ return (
191
+ <div className="space-y-3">
192
+ <div className="flex items-center justify-between">
193
+ <ScoreBadge score={score} />
194
+ <span className="text-[11px] font-mono text-slate-500 dark:text-white/40">{pct}/100</span>
195
+ </div>
196
+ <div className="h-3 rounded-full bg-slate-100 dark:bg-white/8 overflow-hidden">
197
+ <div
198
+ className="h-full rounded-full transition-all duration-700"
199
+ style={{ width: `${pct}%`, background: barColor }}
200
+ />
201
+ </div>
202
+ {trace.hallucinationReason && (
203
+ <div className="rounded-lg border border-slate-100 dark:border-white/8 bg-slate-50 dark:bg-white/3 p-2.5">
204
+ <p className="text-[11px] text-slate-500 dark:text-white/40 font-medium mb-0.5">AI self-assessment</p>
205
+ <p className="text-[11px] text-slate-700 dark:text-white/70 leading-relaxed">{trace.hallucinationReason}</p>
206
+ </div>
207
+ )}
208
+ <div className="flex gap-2 text-[10px] text-slate-400 dark:text-white/30">
209
+ <span className="flex items-center gap-1"><span className="inline-block w-2 h-2 rounded-full bg-emerald-500" /> 0–20%: Grounded</span>
210
+ <span className="flex items-center gap-1"><span className="inline-block w-2 h-2 rounded-full bg-amber-500" /> 21–50%: Uncertain</span>
211
+ <span className="flex items-center gap-1"><span className="inline-block w-2 h-2 rounded-full bg-red-500" /> 51–100%: Risk</span>
212
+ </div>
213
+ </div>
214
+ );
215
+ }
216
+
217
+ function PromptTab({ trace }: { trace: ObservabilityTrace }) {
218
+ const [showSystem, setShowSystem] = React.useState(false);
219
+ return (
220
+ <div className="space-y-3">
221
+ <div>
222
+ <button
223
+ onClick={() => setShowSystem(s => !s)}
224
+ className="flex items-center gap-1.5 text-[11px] font-semibold text-slate-600 dark:text-white/60 hover:text-slate-900 dark:hover:text-white/80 transition-colors mb-1.5"
225
+ >
226
+ {showSystem ? <ChevronDown className="w-3 h-3" /> : <ChevronRight className="w-3 h-3" />}
227
+ System Prompt
228
+ </button>
229
+ {showSystem && (
230
+ <pre className="text-[10px] font-mono text-slate-600 dark:text-white/60 bg-slate-900 dark:bg-black/30 rounded-lg p-3 overflow-auto max-h-40 whitespace-pre-wrap leading-relaxed">
231
+ {trace.systemPrompt || '(empty)'}
232
+ </pre>
233
+ )}
234
+ </div>
235
+ <div>
236
+ <p className="text-[11px] font-semibold text-slate-600 dark:text-white/60 mb-1.5">User Prompt</p>
237
+ <pre className="text-[10px] font-mono text-slate-600 dark:text-white/60 bg-slate-900 dark:bg-black/30 rounded-lg p-3 overflow-auto max-h-40 whitespace-pre-wrap leading-relaxed">
238
+ {trace.userPrompt}
239
+ </pre>
240
+ </div>
241
+ </div>
242
+ );
243
+ }
244
+
245
+ function ChunksTab({ trace }: { trace: ObservabilityTrace }) {
246
+ const [expanded, setExpanded] = React.useState<Set<number>>(new Set());
247
+ const toggle = (i: number) => setExpanded(prev => {
248
+ const next = new Set(prev);
249
+ if (next.has(i)) {
250
+ next.delete(i);
251
+ } else {
252
+ next.add(i);
253
+ }
254
+ return next;
255
+ });
256
+
257
+ return (
258
+ <div className="space-y-1.5 max-h-64 overflow-y-auto pr-1 custom-scrollbar">
259
+ {trace.chunks.map((chunk, i) => {
260
+ const isOpen = expanded.has(i);
261
+ return (
262
+ <div
263
+ key={String(chunk.id)}
264
+ className="rounded-lg border border-slate-100 dark:border-white/8 bg-slate-50 dark:bg-white/3 overflow-hidden"
265
+ >
266
+ <button
267
+ onClick={() => toggle(i)}
268
+ className="w-full flex items-center justify-between px-3 py-2 text-left hover:bg-slate-100 dark:hover:bg-white/5 transition-colors"
269
+ >
270
+ <div className="flex items-center gap-2">
271
+ {isOpen ? <ChevronDown className="w-3 h-3 text-slate-400" /> : <ChevronRight className="w-3 h-3 text-slate-400" />}
272
+ <span className="text-[11px] font-medium text-slate-700 dark:text-white/70">
273
+ Chunk {i + 1}
274
+ </span>
275
+ <span className="text-[10px] font-mono text-slate-400 dark:text-white/30">
276
+ {String(chunk.id).slice(0, 16)}
277
+ </span>
278
+ </div>
279
+ <span className="text-[10px] font-semibold text-indigo-600 dark:text-indigo-400">
280
+ {chunk.score.toFixed(3)}
281
+ </span>
282
+ </button>
283
+
284
+ {isOpen && (
285
+ <div className="px-3 pb-3 space-y-2 border-t border-slate-100 dark:border-white/5 pt-2">
286
+ <div>
287
+ <p className="text-[10px] text-slate-400 dark:text-white/30 font-semibold uppercase tracking-wider mb-1">Content</p>
288
+ <p className="text-[11px] text-slate-600 dark:text-white/60 leading-relaxed whitespace-pre-wrap">
289
+ {chunk.content}
290
+ </p>
291
+ </div>
292
+ {Object.keys(chunk.metadata).length > 0 && (
293
+ <div>
294
+ <p className="text-[10px] text-slate-400 dark:text-white/30 font-semibold uppercase tracking-wider mb-1">Metadata</p>
295
+ <pre className="text-[10px] font-mono text-slate-500 dark:text-white/40 bg-slate-900 dark:bg-black/30 rounded-md p-2 overflow-auto max-h-24">
296
+ {JSON.stringify(chunk.metadata, null, 2)}
297
+ </pre>
298
+ </div>
299
+ )}
300
+ </div>
301
+ )}
302
+ </div>
303
+ );
304
+ })}
305
+ </div>
306
+ );
307
+ }
308
+
309
+ // ─── Main component ───────────────────────────────────────────────────────────
310
+
311
+ type TabId = 'retrieval' | 'latency' | 'tokens' | 'hallucination' | 'prompt' | 'chunks';
312
+
313
+ const TABS: { id: TabId; label: string; icon: React.ReactNode }[] = [
314
+ { id: 'retrieval', label: 'Retrieval', icon: <Layers className="w-3 h-3" /> },
315
+ { id: 'latency', label: 'Latency', icon: <Clock className="w-3 h-3" /> },
316
+ { id: 'tokens', label: 'Tokens', icon: <Cpu className="w-3 h-3" /> },
317
+ { id: 'hallucination',label: 'Grounding', icon: <AlertTriangle className="w-3 h-3" /> },
318
+ { id: 'prompt', label: 'Prompt', icon: <FileText className="w-3 h-3" /> },
319
+ { id: 'chunks', label: 'Chunks', icon: <Zap className="w-3 h-3" /> },
320
+ ];
321
+
322
+ export function ObservabilityPanel({ trace, primaryColor = '#6366f1' }: ObservabilityPanelProps) {
323
+ const [activeTab, setActiveTab] = React.useState<TabId>('retrieval');
324
+
325
+ return (
326
+ <div className="mt-2 w-full rounded-xl border border-slate-200 dark:border-white/8 bg-white dark:bg-slate-900/60 shadow-sm overflow-hidden text-[12px]">
327
+ {/* Header */}
328
+ <div
329
+ className="flex items-center gap-2 px-3 py-2 border-b border-slate-100 dark:border-white/8"
330
+ style={{ background: `${primaryColor}08` }}
331
+ >
332
+ <span
333
+ className="inline-flex items-center gap-1 text-[10px] font-bold uppercase tracking-wider"
334
+ style={{ color: primaryColor }}
335
+ >
336
+ <Zap className="w-3 h-3" />
337
+ Observability Trace
338
+ </span>
339
+ <span className="text-[10px] text-slate-400 dark:text-white/30 ml-auto font-mono">
340
+ {trace.requestId}
341
+ </span>
342
+ </div>
343
+
344
+ {/* Tabs */}
345
+ <div className="flex border-b border-slate-100 dark:border-white/8 overflow-x-auto">
346
+ {TABS.map((tab) => (
347
+ <button
348
+ key={tab.id}
349
+ onClick={() => setActiveTab(tab.id)}
350
+ className={`flex items-center gap-1.5 px-3 py-2 text-[10px] font-semibold whitespace-nowrap transition-colors border-b-2 ${
351
+ activeTab === tab.id
352
+ ? 'border-b-2 text-indigo-600 dark:text-indigo-400'
353
+ : 'border-transparent text-slate-400 dark:text-white/30 hover:text-slate-600 dark:hover:text-white/60'
354
+ }`}
355
+ style={activeTab === tab.id ? { borderBottomColor: primaryColor, color: primaryColor } : {}}
356
+ >
357
+ {tab.icon}
358
+ {tab.label}
359
+ </button>
360
+ ))}
361
+ </div>
362
+
363
+ {/* Tab body */}
364
+ <div className="p-3">
365
+ {activeTab === 'retrieval' && <RetrievalTab trace={trace} />}
366
+ {activeTab === 'latency' && <LatencyTab trace={trace} />}
367
+ {activeTab === 'tokens' && <TokensTab trace={trace} />}
368
+ {activeTab === 'hallucination'&& <HallucinationTab trace={trace} />}
369
+ {activeTab === 'prompt' && <PromptTab trace={trace} />}
370
+ {activeTab === 'chunks' && <ChunksTab trace={trace} />}
371
+ </div>
372
+ </div>
373
+ );
374
+ }
@@ -24,7 +24,7 @@ export function ProductCard({ product, primaryColor = '#6366f1', onAddToCart }:
24
24
  <ShoppingCart className="w-12 h-12" />
25
25
  </div>
26
26
  )}
27
-
27
+
28
28
  {/* Brand Badge */}
29
29
  {product.brand && (
30
30
  <div className="absolute top-2 left-2 px-2 py-1 bg-white/90 dark:bg-black/60 backdrop-blur-md rounded-md text-[10px] font-bold uppercase tracking-wider text-slate-700 dark:text-white/90 shadow-sm">
@@ -41,11 +41,13 @@ export function ProductCard({ product, primaryColor = '#6366f1', onAddToCart }:
41
41
  </h3>
42
42
  {product.price && (
43
43
  <span className="text-sm font-bold" style={{ color: primaryColor }}>
44
- {typeof product.price === 'number' ? `$${product.price}` : product.price}
44
+ {typeof product.price === 'number'
45
+ ? `$${product.price}`
46
+ : (String(product.price).match(/^[\d.]/) ? `$${product.price}` : product.price)}
45
47
  </span>
46
48
  )}
47
49
  </div>
48
-
50
+
49
51
  {product.description && (
50
52
  <p className="text-[11px] text-slate-500 dark:text-white/50 line-clamp-2 leading-relaxed">
51
53
  {product.description}