@retrivora-ai/rag-engine 1.8.1 → 1.8.3
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/dist/{ILLMProvider-BfRgI1Xh.d.mts → ILLMProvider-BOJFz3Na.d.mts} +47 -1
- package/dist/{ILLMProvider-BfRgI1Xh.d.ts → ILLMProvider-BOJFz3Na.d.ts} +47 -1
- package/dist/handlers/index.d.mts +2 -2
- package/dist/handlers/index.d.ts +2 -2
- package/dist/handlers/index.js +509 -162
- package/dist/handlers/index.mjs +5849 -15
- package/dist/{index-1Z4GuYBi.d.ts → index-BwpcaziY.d.ts} +4 -2
- package/dist/{index-BV0z5mb6.d.mts → index-D3V9Et2M.d.mts} +4 -2
- package/dist/index.d.mts +30 -16
- package/dist/index.d.ts +30 -16
- package/dist/index.js +1999 -1210
- package/dist/index.mjs +2041 -1205
- package/dist/server.d.mts +21 -25
- package/dist/server.d.ts +21 -25
- package/dist/server.js +536 -163
- package/dist/server.mjs +5923 -67
- package/package.json +9 -7
- package/src/app/api/upload/route.ts +4 -0
- package/src/app/constants.tsx +2 -2
- package/src/app/globals.css +35 -11
- package/src/app/page.tsx +12 -321
- package/src/components/AmbientBackground.tsx +29 -0
- package/src/components/ArchitectureCard.tsx +17 -0
- package/src/components/ArchitectureCardsSection.tsx +15 -0
- package/src/components/ChatWidget.tsx +0 -1
- package/src/components/ChatWindow.tsx +32 -0
- package/src/components/CodeViewer.tsx +51 -0
- package/src/components/ConfigProvider.tsx +1 -0
- package/src/components/DocViewer.tsx +37 -0
- package/src/components/DocumentUpload.tsx +44 -1
- package/src/components/Documentation.tsx +58 -0
- package/src/components/DynamicChart.tsx +27 -2
- package/src/components/Hero.tsx +59 -0
- package/src/components/HourglassLoader.tsx +87 -0
- package/src/components/Lifecycle.tsx +37 -0
- package/src/components/MarkdownComponents.tsx +143 -0
- package/src/components/MessageBubble.tsx +91 -1012
- package/src/components/Navbar.tsx +55 -0
- package/src/components/ObservabilityPanel.tsx +374 -0
- package/src/components/ProductCard.tsx +3 -1
- package/src/components/UIDispatcher.tsx +341 -0
- package/src/components/VisualizationRenderer.tsx +48 -26
- package/src/config/ConfigBuilder.ts +38 -1
- package/src/core/LangChainAgent.ts +1 -4
- package/src/core/Pipeline.ts +209 -86
- package/src/core/QueryProcessor.ts +65 -0
- package/src/handlers/index.ts +72 -12
- package/src/hooks/useRagChat.ts +19 -9
- package/src/index.ts +9 -1
- package/src/providers/vectordb/MultiTablePostgresProvider.ts +150 -64
- package/src/rag/Reranker.ts +99 -6
- package/src/types/chat.ts +2 -0
- package/src/types/index.ts +52 -0
- package/src/types/props.ts +9 -1
- package/src/utils/ProductExtractor.ts +347 -0
- package/src/utils/UITransformer.ts +4 -53
- package/src/utils/synonyms.ts +78 -0
- package/dist/ChromaDBProvider-MIDOR4FW.mjs +0 -8
- package/dist/MilvusProvider-U7SKC27V.mjs +0 -8
- package/dist/MongoDBProvider-YNKC7EJ6.mjs +0 -8
- package/dist/MultiTablePostgresProvider-YY7LPNJK.mjs +0 -8
- package/dist/PineconeProvider-QZNRKTN2.mjs +0 -8
- package/dist/QdrantProvider-RLJTNGPY.mjs +0 -8
- package/dist/RedisProvider-SR65SCKV.mjs +0 -8
- package/dist/SimpleGraphProvider-SLOXO4M7.mjs +0 -62
- package/dist/UniversalVectorProvider-IN67OS56.mjs +0 -9
- package/dist/WeaviateProvider-5FWDFITI.mjs +0 -8
- package/dist/chunk-5AJ4XHLW.mjs +0 -201
- package/dist/chunk-5YGUXK7Z.mjs +0 -80
- package/dist/chunk-BFYLQYQU.mjs +0 -3985
- package/dist/chunk-CFVEZTBJ.mjs +0 -102
- package/dist/chunk-IMP6FUCY.mjs +0 -30
- package/dist/chunk-LR3VMDVK.mjs +0 -157
- package/dist/chunk-M6JSPGAR.mjs +0 -117
- package/dist/chunk-PSFPZXHX.mjs +0 -245
- package/dist/chunk-R3RGUMHE.mjs +0 -218
- package/dist/chunk-U55XRW3U.mjs +0 -96
- package/dist/chunk-VUQJVIJT.mjs +0 -148
- package/dist/chunk-X4TOT24V.mjs +0 -89
- package/dist/chunk-YLTMFW4M.mjs +0 -49
|
@@ -1,714 +1,26 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import React from 'react';
|
|
4
|
-
import { Bot, User, ChevronDown, ChevronRight } from 'lucide-react';
|
|
5
|
-
import ReactMarkdown
|
|
4
|
+
import { Bot, User, ChevronDown, ChevronRight, Activity } from 'lucide-react';
|
|
5
|
+
import ReactMarkdown from 'react-markdown';
|
|
6
6
|
import { MessageBubbleProps, Product, UITransformationResponse } from '../types';
|
|
7
|
-
import { ChatViewportSize } from '../types/props';
|
|
8
7
|
import { SourceCard } from './SourceCard';
|
|
9
8
|
import { ProductCarousel } from './ProductCarousel';
|
|
10
|
-
import { DynamicChart } from './DynamicChart';
|
|
11
9
|
import { VisualizationRenderer } from './VisualizationRenderer';
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
op === '+' ? n1 + n2
|
|
27
|
-
: op === '-' ? n1 - n2
|
|
28
|
-
: op === '*' ? n1 * n2
|
|
29
|
-
: n2 !== 0 ? n1 / n2
|
|
30
|
-
: n1;
|
|
31
|
-
return `: ${result}`;
|
|
32
|
-
},
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
// 2. Strip JS-style comments while preserving strings (e.g. URLs)
|
|
36
|
-
s = s.replace(/("(?:\\.|[^\\"])*")|\/\/[^\n]*|\/\*[\s\S]*?\*\//g, (m, g1) => g1 || '');
|
|
37
|
-
|
|
38
|
-
// 3. Quote bare (unquoted) object keys
|
|
39
|
-
s = s.replace(/([{,]\s*)([A-Za-z_$][A-Za-z0-9_$]*)\s*:/g, '$1"$2":');
|
|
40
|
-
|
|
41
|
-
// 4. Replace single-quoted strings with double-quoted ones
|
|
42
|
-
s = s.replace(/'([^'\\]*(?:\\.[^'\\]*)*)'/g, '"$1"');
|
|
43
|
-
|
|
44
|
-
// 5. Remove trailing commas before ] or }
|
|
45
|
-
s = s.replace(/,\s*([}\]])/g, '$1');
|
|
46
|
-
|
|
47
|
-
// 6. Remove completely empty key-value pairs left by prior cleanup
|
|
48
|
-
s = s.replace(/([{,])\s*""\s*(?=[,}])/g, '$1');
|
|
49
|
-
|
|
50
|
-
// 7. Stack-based structure balancer.
|
|
51
|
-
// Walk every character, maintain a stack of openers so we close each
|
|
52
|
-
// unclosed structure with its exact matching closer — in the right order.
|
|
53
|
-
// A flat counter approach appends all ]'s then all }'s, which corrupts
|
|
54
|
-
// nested structures like {..., [..., {...}]}.
|
|
55
|
-
{
|
|
56
|
-
const stack: ('{' | '[')[] = [];
|
|
57
|
-
let inString = false;
|
|
58
|
-
let escape = false;
|
|
59
|
-
|
|
60
|
-
for (const ch of s) {
|
|
61
|
-
if (escape) { escape = false; continue; }
|
|
62
|
-
if (ch === '\\' && inString) { escape = true; continue; }
|
|
63
|
-
if (ch === '"') { inString = !inString; continue; }
|
|
64
|
-
if (inString) continue;
|
|
65
|
-
|
|
66
|
-
if (ch === '{' || ch === '[') {
|
|
67
|
-
stack.push(ch);
|
|
68
|
-
} else if (ch === '}') {
|
|
69
|
-
if (stack[stack.length - 1] === '{') stack.pop();
|
|
70
|
-
} else if (ch === ']') {
|
|
71
|
-
if (stack[stack.length - 1] === '[') stack.pop();
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Close any unterminated string literal first
|
|
76
|
-
if (inString) s += '"';
|
|
77
|
-
|
|
78
|
-
// Close every unclosed opener in reverse (innermost first)
|
|
79
|
-
for (let i = stack.length - 1; i >= 0; i--) {
|
|
80
|
-
s += stack[i] === '{' ? '}' : ']';
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// 8. Trim trailing garbage that appears after the outermost closing char.
|
|
85
|
-
// Only fires when something non-whitespace follows the last } or ].
|
|
86
|
-
const lastClose = Math.max(s.lastIndexOf('}'), s.lastIndexOf(']'));
|
|
87
|
-
if (lastClose !== -1 && lastClose < s.length - 1 && /\S/.test(s.slice(lastClose + 1))) {
|
|
88
|
-
s = s.substring(0, lastClose + 1);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return s;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function extractLikelyJsonBlock(raw: string): string {
|
|
95
|
-
const trimmed = raw.trim();
|
|
96
|
-
const objectStart = trimmed.indexOf('{');
|
|
97
|
-
const arrayStart = trimmed.indexOf('[');
|
|
98
|
-
|
|
99
|
-
const starts = [objectStart, arrayStart].filter((index) => index >= 0);
|
|
100
|
-
if (starts.length === 0) return trimmed;
|
|
101
|
-
|
|
102
|
-
return trimmed.slice(Math.min(...starts));
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function looksLikeStructuredPayload(raw: string): boolean {
|
|
106
|
-
return /"?(view|chartType|type|data|items|rows|products|results)"?\s*:/.test(raw);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function stripMarkdownTables(raw: string): string {
|
|
110
|
-
return raw.replace(
|
|
111
|
-
/(?:^|\n)\|[^\n]+\|\n\|(?:\s*:?-+:?\s*\|)+\n(?:\|[^\n]*\|\n?)*/g,
|
|
112
|
-
'\n\n',
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function stripStructuredUiLabels(raw: string): string {
|
|
117
|
-
return raw.replace(
|
|
118
|
-
/(?:^|\n)\*\*(?:Chart Data|Table Data|Visualization|Visual Representation)\*\:\s*(?=\n|$)/gi,
|
|
119
|
-
'\n',
|
|
120
|
-
).replace(
|
|
121
|
-
/(?:^|\n)(?:Chart Data|Table Data|Visualization|Visual Representation)\s*\:\s*(?=\n|$)/gi,
|
|
122
|
-
'\n',
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function extractBareJsonObject(raw: string): string | null {
|
|
127
|
-
// Find the first occurrence of '{'
|
|
128
|
-
let start = raw.indexOf('{');
|
|
129
|
-
if (start === -1) return null;
|
|
130
|
-
|
|
131
|
-
// We want to find the LARGEST valid JSON object in the string.
|
|
132
|
-
// Sometimes there might be a small JSON object followed by a larger one.
|
|
133
|
-
// We'll iterate through all { and find the one that spans the most.
|
|
134
|
-
let bestJson: string | null = null;
|
|
135
|
-
let maxLen = 0;
|
|
136
|
-
|
|
137
|
-
while (start !== -1) {
|
|
138
|
-
let depth = 0;
|
|
139
|
-
let inString = false;
|
|
140
|
-
let escape = false;
|
|
141
|
-
|
|
142
|
-
for (let i = start; i < raw.length; i += 1) {
|
|
143
|
-
const ch = raw[i];
|
|
144
|
-
if (escape) { escape = false; continue; }
|
|
145
|
-
if (ch === '\\' && inString) { escape = true; continue; }
|
|
146
|
-
if (ch === '"') { inString = !inString; continue; }
|
|
147
|
-
if (inString) continue;
|
|
148
|
-
|
|
149
|
-
if (ch === '{') depth += 1;
|
|
150
|
-
else if (ch === '}') {
|
|
151
|
-
depth -= 1;
|
|
152
|
-
if (depth === 0) {
|
|
153
|
-
const candidate = raw.slice(start, i + 1);
|
|
154
|
-
if (candidate.length > maxLen) {
|
|
155
|
-
maxLen = candidate.length;
|
|
156
|
-
bestJson = candidate;
|
|
157
|
-
}
|
|
158
|
-
break; // Found the end of this object
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
start = raw.indexOf('{', start + 1);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
return bestJson;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function extractStructuredPayload(raw: string): { payload: string | null; text: string } {
|
|
169
|
-
let working = raw;
|
|
170
|
-
const fenceRegex = /```(?:[a-z]+)?\s*([\s\S]*?)```/gi;
|
|
171
|
-
|
|
172
|
-
for (const match of Array.from(raw.matchAll(fenceRegex))) {
|
|
173
|
-
const body = match[1]?.trim() ?? '';
|
|
174
|
-
if (!looksLikeStructuredPayload(body)) continue;
|
|
175
|
-
|
|
176
|
-
working = working.replace(match[0], '');
|
|
177
|
-
return {
|
|
178
|
-
payload: body,
|
|
179
|
-
text: stripStructuredUiLabels(working).replace(/\n{3,}/g, '\n\n').trim(),
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const bareJson = extractBareJsonObject(raw);
|
|
184
|
-
if (bareJson && looksLikeStructuredPayload(bareJson)) {
|
|
185
|
-
working = working.replace(bareJson, '');
|
|
186
|
-
return {
|
|
187
|
-
payload: bareJson,
|
|
188
|
-
text: stripStructuredUiLabels(working).replace(/\n{3,}/g, '\n\n').trim(),
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
return {
|
|
193
|
-
payload: null,
|
|
194
|
-
text: stripStructuredUiLabels(raw).replace(/\n{3,}/g, '\n\n').trim(),
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function isLikelyUiOnlyMessage(text: string): boolean {
|
|
199
|
-
const trimmed = text.trim();
|
|
200
|
-
return (
|
|
201
|
-
trimmed.length === 0 ||
|
|
202
|
-
trimmed.length < 30 ||
|
|
203
|
-
/^(chart data|table data|visualization|visual representation|product catalog|product recommendations|catalog summary|availability|inventory status|details)\s*:?\s*$/i.test(trimmed)
|
|
204
|
-
);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// ─── Tiny helpers ─────────────────────────────────────────────────────────────
|
|
208
|
-
|
|
209
|
-
function resolveImage(data: Record<string, unknown>): string | undefined {
|
|
210
|
-
const isPlaceholder = (val: unknown) =>
|
|
211
|
-
typeof val === 'string' && (val === '...' || val === '' || val.toLowerCase() === 'null' || val.toLowerCase() === 'undefined');
|
|
212
|
-
|
|
213
|
-
for (const key of ['image', 'img', 'thumbnail', 'images', 'product_image', 'Image', 'Thumbnail']) {
|
|
214
|
-
const v = data[key];
|
|
215
|
-
if (typeof v === 'string' && v && !isPlaceholder(v)) return v;
|
|
216
|
-
if (Array.isArray(v) && typeof v[0] === 'string' && v[0] && !isPlaceholder(v[0])) return v[0];
|
|
217
|
-
}
|
|
218
|
-
return undefined;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/** Robustly extract a value from metadata by checking synonyms and case-insensitivity. */
|
|
222
|
-
function getMetadataValue(meta: Record<string, unknown>, keys: string[]): unknown {
|
|
223
|
-
const searchKeys = keys.map(k => k.toLowerCase());
|
|
224
|
-
const metaKeys = Object.keys(meta);
|
|
225
|
-
|
|
226
|
-
// Try direct case-insensitive match
|
|
227
|
-
const foundKey = metaKeys.find(k => searchKeys.includes(k.toLowerCase()));
|
|
228
|
-
if (foundKey) return meta[foundKey];
|
|
229
|
-
|
|
230
|
-
// Try partial match
|
|
231
|
-
const partialKey = metaKeys.find(k => {
|
|
232
|
-
const kLow = k.toLowerCase();
|
|
233
|
-
return searchKeys.some(sk => kLow.includes(sk) || sk.includes(kLow));
|
|
234
|
-
});
|
|
235
|
-
return partialKey ? meta[partialKey] : undefined;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/** Safely render table cell children that might be plain objects. */
|
|
239
|
-
function normaliseChild(children: unknown): React.ReactNode {
|
|
240
|
-
if (
|
|
241
|
-
typeof children === 'object' &&
|
|
242
|
-
children !== null &&
|
|
243
|
-
!Array.isArray(children) &&
|
|
244
|
-
!React.isValidElement(children)
|
|
245
|
-
) {
|
|
246
|
-
return JSON.stringify(children);
|
|
247
|
-
}
|
|
248
|
-
return children as React.ReactNode;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function normaliseFieldName(value: string): string {
|
|
252
|
-
return value.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function formatColumnLabel(value: string): string {
|
|
256
|
-
return value
|
|
257
|
-
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
258
|
-
.replace(/[_-]+/g, ' ')
|
|
259
|
-
.replace(/\s+/g, ' ')
|
|
260
|
-
.trim()
|
|
261
|
-
.replace(/\b\w/g, (char) => char.toUpperCase());
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// ─── Inline data table renderer ───────────────────────────────────────────────
|
|
265
|
-
// Handles chart configs where type === "table". The LLM may emit any of:
|
|
266
|
-
// columns, dataKeys (+optional xAxisKey), or no key hints (auto-detect from row).
|
|
267
|
-
|
|
268
|
-
interface TableConfig {
|
|
269
|
-
type: 'table';
|
|
270
|
-
title?: string;
|
|
271
|
-
description?: string;
|
|
272
|
-
columns?: string[]; // preferred: explicit ordered column labels
|
|
273
|
-
dataKeys?: string[]; // fallback column keys (may omit xAxisKey)
|
|
274
|
-
xAxisKey?: string; // row-label key to prepend when using dataKeys
|
|
275
|
-
data: Record<string, unknown>[];
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function DataTable({ config, viewportSize = 'large' }: { config: TableConfig; viewportSize?: ChatViewportSize }) {
|
|
279
|
-
const isCompact = viewportSize === 'compact';
|
|
280
|
-
const isMedium = viewportSize === 'medium';
|
|
281
|
-
const columns = React.useMemo<Array<{ label: string; accessor: string }>>(() => {
|
|
282
|
-
const rowKeys = Array.from(
|
|
283
|
-
new Set(
|
|
284
|
-
config.data.flatMap((row) => Object.keys(row)),
|
|
285
|
-
),
|
|
286
|
-
);
|
|
287
|
-
|
|
288
|
-
const findAccessor = (label: string) => {
|
|
289
|
-
const exact = rowKeys.find((key) => key === label);
|
|
290
|
-
if (exact) return exact;
|
|
291
|
-
|
|
292
|
-
const normalisedLabel = normaliseFieldName(label);
|
|
293
|
-
const normalisedMatch = rowKeys.find((key) => normaliseFieldName(key) === normalisedLabel);
|
|
294
|
-
if (normalisedMatch) return normalisedMatch;
|
|
295
|
-
|
|
296
|
-
return label;
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
if (Array.isArray(config.columns) && config.columns.length) {
|
|
300
|
-
return config.columns.map((label) => ({
|
|
301
|
-
label: formatColumnLabel(label),
|
|
302
|
-
accessor: findAccessor(label),
|
|
303
|
-
}));
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
if (Array.isArray(config.dataKeys) && config.dataKeys.length) {
|
|
307
|
-
const keys = config.xAxisKey && !config.dataKeys.includes(config.xAxisKey)
|
|
308
|
-
? [config.xAxisKey, ...config.dataKeys]
|
|
309
|
-
: config.dataKeys;
|
|
310
|
-
|
|
311
|
-
return keys.map((key) => ({
|
|
312
|
-
label: formatColumnLabel(key),
|
|
313
|
-
accessor: findAccessor(key),
|
|
314
|
-
}));
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
return rowKeys.map((key) => ({ label: formatColumnLabel(key), accessor: key }));
|
|
318
|
-
}, [config]);
|
|
319
|
-
|
|
320
|
-
if (!config.data.length || !columns.length) {
|
|
321
|
-
return <p className="text-xs text-slate-400 italic my-2">No data to display.</p>;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
const formatCell = (val: unknown): string => {
|
|
325
|
-
if (val === null || val === undefined) return '—';
|
|
326
|
-
if (typeof val === 'boolean') return val ? '✓' : '✗';
|
|
327
|
-
return String(val);
|
|
328
|
-
};
|
|
329
|
-
|
|
330
|
-
return (
|
|
331
|
-
<div className="my-4 rounded-xl border border-slate-200 dark:border-white/10 shadow-sm overflow-hidden">
|
|
332
|
-
{config.title && (
|
|
333
|
-
<div className="px-4 py-2.5 bg-slate-50 dark:bg-white/5 border-b border-slate-200 dark:border-white/10">
|
|
334
|
-
<p className="text-xs font-semibold text-slate-600 dark:text-white/70 uppercase tracking-wide">
|
|
335
|
-
{config.title}
|
|
336
|
-
</p>
|
|
337
|
-
</div>
|
|
338
|
-
)}
|
|
339
|
-
|
|
340
|
-
<div className="overflow-x-auto">
|
|
341
|
-
<table className={`w-full text-left border-collapse ${isCompact ? 'min-w-[240px] text-xs' : isMedium ? 'min-w-[280px] text-[13px]' : 'min-w-[320px] text-sm'}`}>
|
|
342
|
-
<thead className="bg-slate-50 dark:bg-white/5 border-b border-slate-200 dark:border-white/10">
|
|
343
|
-
<tr>
|
|
344
|
-
{columns.map((column) => (
|
|
345
|
-
<th
|
|
346
|
-
key={column.label}
|
|
347
|
-
className={`${isCompact ? 'px-2.5 py-2' : 'px-4 py-3'} font-semibold text-slate-700 dark:text-white/90 whitespace-nowrap`}
|
|
348
|
-
>
|
|
349
|
-
{column.label}
|
|
350
|
-
</th>
|
|
351
|
-
))}
|
|
352
|
-
</tr>
|
|
353
|
-
</thead>
|
|
354
|
-
<tbody>
|
|
355
|
-
{config.data.map((row, ri) => (
|
|
356
|
-
<tr
|
|
357
|
-
key={ri}
|
|
358
|
-
className="border-b border-slate-100 dark:border-white/5 last:border-0 hover:bg-slate-50/60 dark:hover:bg-white/5 transition-colors"
|
|
359
|
-
>
|
|
360
|
-
{columns.map((column) => (
|
|
361
|
-
<td
|
|
362
|
-
key={column.label}
|
|
363
|
-
className={`${isCompact ? 'px-2.5 py-2' : 'px-4 py-3'} text-slate-600 dark:text-white/70 ${column.accessor.toLowerCase().includes('description') ? 'whitespace-normal break-words' : 'whitespace-nowrap'}`}
|
|
364
|
-
>
|
|
365
|
-
{formatCell(row[column.accessor])}
|
|
366
|
-
</td>
|
|
367
|
-
))}
|
|
368
|
-
</tr>
|
|
369
|
-
))}
|
|
370
|
-
</tbody>
|
|
371
|
-
</table>
|
|
372
|
-
</div>
|
|
373
|
-
|
|
374
|
-
<div className="px-4 py-2 bg-slate-50 dark:bg-white/5 border-t border-slate-100 dark:border-white/5">
|
|
375
|
-
<p className="text-[11px] text-slate-400 dark:text-white/30">
|
|
376
|
-
{config.data.length} row{config.data.length !== 1 ? 's' : ''}
|
|
377
|
-
</p>
|
|
378
|
-
</div>
|
|
379
|
-
</div>
|
|
380
|
-
);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// ─── Universal UI Dispatcher ────────────────────────────────────────────────
|
|
384
|
-
// Handles charts, product carousels, and data tables via a single protocol.
|
|
385
|
-
|
|
386
|
-
interface UIConfig {
|
|
387
|
-
view: 'chart' | 'carousel' | 'table';
|
|
388
|
-
chartType?: 'pie' | 'bar' | 'line';
|
|
389
|
-
xAxisKey?: string;
|
|
390
|
-
dataKeys?: string[];
|
|
391
|
-
columns?: string[];
|
|
392
|
-
colors?: string[];
|
|
393
|
-
data: Record<string, unknown>[];
|
|
394
|
-
title?: string;
|
|
395
|
-
description?: string;
|
|
396
|
-
insights?: string[];
|
|
397
|
-
type?: string;
|
|
398
|
-
items?: Record<string, unknown>[];
|
|
399
|
-
rows?: Record<string, unknown>[];
|
|
400
|
-
products?: Record<string, unknown>[];
|
|
401
|
-
results?: Record<string, unknown>[];
|
|
402
|
-
|
|
403
|
-
// V7 Protocol Fields
|
|
404
|
-
carousel?: {
|
|
405
|
-
items: Record<string, unknown>[];
|
|
406
|
-
};
|
|
407
|
-
chart?: {
|
|
408
|
-
type: 'pie' | 'bar' | 'line';
|
|
409
|
-
xKey: string;
|
|
410
|
-
yKeys: string[];
|
|
411
|
-
data: Record<string, unknown>[];
|
|
412
|
-
};
|
|
413
|
-
table?: {
|
|
414
|
-
columns: string[];
|
|
415
|
-
rows?: Record<string, unknown>[];
|
|
416
|
-
data?: Record<string, unknown>[];
|
|
417
|
-
};
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
interface RawUIConfig extends Omit<UIConfig, 'data' | 'items' | 'rows' | 'products' | 'results' | 'carousel' | 'chart' | 'table'> {
|
|
421
|
-
data: Array<Record<string, unknown> | unknown[]>;
|
|
422
|
-
items?: Array<Record<string, unknown> | unknown[]>;
|
|
423
|
-
rows?: Array<Record<string, unknown> | unknown[]>;
|
|
424
|
-
products?: Array<Record<string, unknown> | unknown[]>;
|
|
425
|
-
results?: Array<Record<string, unknown> | unknown[]>;
|
|
426
|
-
|
|
427
|
-
// V7 Protocol Fields (Raw)
|
|
428
|
-
carousel?: {
|
|
429
|
-
items: Array<Record<string, unknown> | unknown[]>;
|
|
430
|
-
};
|
|
431
|
-
chart?: {
|
|
432
|
-
type: 'pie' | 'bar' | 'line';
|
|
433
|
-
xKey: string;
|
|
434
|
-
yKeys: string[];
|
|
435
|
-
data: Array<Record<string, unknown> | unknown[]>;
|
|
436
|
-
};
|
|
437
|
-
table?: {
|
|
438
|
-
columns: string[];
|
|
439
|
-
rows?: Array<Record<string, unknown> | unknown[]>;
|
|
440
|
-
data?: Array<Record<string, unknown> | unknown[]>;
|
|
441
|
-
};
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
function normalizeTabularRows(
|
|
445
|
-
rows: Array<Record<string, unknown> | unknown[]> | undefined,
|
|
446
|
-
columns?: string[],
|
|
447
|
-
): Record<string, unknown>[] {
|
|
448
|
-
if (!Array.isArray(rows)) return [];
|
|
449
|
-
|
|
450
|
-
// Common synonyms for mapping natural language headers to technical keys
|
|
451
|
-
const SYNONYMS: Record<string, string[]> = {
|
|
452
|
-
name: ['product', 'item', 'title', 'label', 'heading', 'product name', 'item name'],
|
|
453
|
-
price: ['cost', 'amount', 'msrp', 'price', 'rate', 'value', 'price_usd'],
|
|
454
|
-
brand: ['manufacturer', 'vendor', 'make', 'company', 'brand_name', 'supplier'],
|
|
455
|
-
image: ['imageUrl', 'thumbnail', 'img', 'url', 'photo', 'picture', 'media'],
|
|
456
|
-
stock: ['inventory', 'quantity', 'count', 'availability', 'stock_level', 'in stock', 'status'],
|
|
457
|
-
};
|
|
458
|
-
|
|
459
|
-
return rows
|
|
460
|
-
.map((row) => {
|
|
461
|
-
if (Array.isArray(row)) {
|
|
462
|
-
const keys = Array.isArray(columns) && columns.length > 0
|
|
463
|
-
? columns
|
|
464
|
-
: row.map((_, index) => `column_${index + 1}`);
|
|
465
|
-
|
|
466
|
-
return keys.reduce<Record<string, unknown>>((acc, key, index) => {
|
|
467
|
-
acc[key] = row[index];
|
|
468
|
-
return acc;
|
|
469
|
-
}, {});
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
if (row && typeof row === 'object') {
|
|
473
|
-
const result: Record<string, unknown> = { ...row };
|
|
474
|
-
|
|
475
|
-
// Self-Healing: If columns are provided, ensure the row has keys matching those columns
|
|
476
|
-
if (Array.isArray(columns)) {
|
|
477
|
-
columns.forEach(col => {
|
|
478
|
-
if (result[col] !== undefined) return; // Already matches
|
|
479
|
-
|
|
480
|
-
// Try to find a match among row keys
|
|
481
|
-
const rowKeys = Object.keys(row);
|
|
482
|
-
const colLower = col.toLowerCase().replace(/_/g, ' ');
|
|
483
|
-
|
|
484
|
-
// 1. Direct case-insensitive / space-to-underscore match
|
|
485
|
-
const foundKey = rowKeys.find(rk => {
|
|
486
|
-
const rkLower = rk.toLowerCase().replace(/_/g, ' ');
|
|
487
|
-
return rkLower === colLower || rkLower.includes(colLower) || colLower.includes(rkLower);
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
if (foundKey) {
|
|
491
|
-
result[col] = row[foundKey];
|
|
492
|
-
return;
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
// 2. Synonym match
|
|
496
|
-
for (const [target, aliases] of Object.entries(SYNONYMS)) {
|
|
497
|
-
const isMatch = target === colLower || aliases.some(a => colLower.includes(a) || a.includes(colLower));
|
|
498
|
-
if (isMatch) {
|
|
499
|
-
const mappedKey = rowKeys.find(rk => {
|
|
500
|
-
const rkL = rk.toLowerCase();
|
|
501
|
-
return rkL === target || aliases.some(a => rkL.includes(a) || a.includes(rkL));
|
|
502
|
-
});
|
|
503
|
-
if (mappedKey) {
|
|
504
|
-
result[col] = row[mappedKey];
|
|
505
|
-
break;
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
return result;
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
return { value: row };
|
|
516
|
-
})
|
|
517
|
-
.filter((row) => Object.keys(row).length > 0);
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
function resolveStructuredRows(config: Partial<RawUIConfig> & Record<string, unknown>) {
|
|
521
|
-
// V7 Nested Structures
|
|
522
|
-
if (config.carousel?.items && Array.isArray(config.carousel.items)) return config.carousel.items;
|
|
523
|
-
if (config.chart?.data && Array.isArray(config.chart.data)) return config.chart.data;
|
|
524
|
-
if (config.table?.rows && Array.isArray(config.table.rows)) return config.table.rows;
|
|
525
|
-
if (config.table?.data && Array.isArray(config.table.data)) return config.table.data;
|
|
526
|
-
|
|
527
|
-
// V6 Flat Structures
|
|
528
|
-
if (Array.isArray(config.data) && config.data.length > 0) return config.data;
|
|
529
|
-
if (Array.isArray(config.items) && config.items.length > 0) return config.items;
|
|
530
|
-
if (Array.isArray(config.products) && config.products.length > 0) return config.products;
|
|
531
|
-
if (Array.isArray(config.rows) && config.rows.length > 0) return config.rows;
|
|
532
|
-
if (Array.isArray(config.results) && config.results.length > 0) return config.results;
|
|
533
|
-
return [];
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, onAddToCart, viewportSize = 'large' }: {
|
|
537
|
-
rawContent: string;
|
|
538
|
-
primaryColor?: string;
|
|
539
|
-
accentColor?: string;
|
|
540
|
-
isStreaming?: boolean;
|
|
541
|
-
onAddToCart?: (product: Product) => void;
|
|
542
|
-
viewportSize?: ChatViewportSize;
|
|
543
|
-
}) {
|
|
544
|
-
const result = React.useMemo<{ config: UIConfig } | { error: string } | { loading: true }>(() => {
|
|
545
|
-
if (isStreaming) return { loading: true };
|
|
546
|
-
try {
|
|
547
|
-
const clean = rawContent
|
|
548
|
-
.replace(/^```[a-z]*\s*/i, '')
|
|
549
|
-
.replace(/```\s*$/, '')
|
|
550
|
-
.trim();
|
|
551
|
-
const parsed = JSON.parse(sanitizeJson(extractLikelyJsonBlock(clean)));
|
|
552
|
-
const config = { ...parsed } as Partial<RawUIConfig> & Record<string, unknown>;
|
|
553
|
-
|
|
554
|
-
// V7 Field Mapping
|
|
555
|
-
if (config.chart) {
|
|
556
|
-
if (!config.chartType) config.chartType = config.chart.type;
|
|
557
|
-
if (!config.xAxisKey) config.xAxisKey = config.chart.xKey;
|
|
558
|
-
if (!config.dataKeys) config.dataKeys = config.chart.yKeys;
|
|
559
|
-
}
|
|
560
|
-
if (config.table) {
|
|
561
|
-
if (!config.columns) config.columns = config.table.columns;
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
const resolvedRows = resolveStructuredRows(config);
|
|
565
|
-
|
|
566
|
-
// ─── Intent Healing & Auto-Detection ───────────────────────────────────
|
|
567
|
-
|
|
568
|
-
const hasProductLikeData = resolvedRows.length > 0 &&
|
|
569
|
-
typeof resolvedRows[0] === 'object' && resolvedRows[0] !== null &&
|
|
570
|
-
!Array.isArray(resolvedRows[0]) &&
|
|
571
|
-
(['price', 'brand', 'image', 'link', 'thumbnail'].some(key => key in resolvedRows[0]) ||
|
|
572
|
-
Object.keys(resolvedRows[0]).length >= 4);
|
|
573
|
-
|
|
574
|
-
const hasAggregationLikeData = resolvedRows.length > 0 &&
|
|
575
|
-
typeof resolvedRows[0] === 'object' && resolvedRows[0] !== null &&
|
|
576
|
-
!Array.isArray(resolvedRows[0]) &&
|
|
577
|
-
(['value', 'count', 'total', 'percentage'].some(key => key in resolvedRows[0]) ||
|
|
578
|
-
(Object.keys(resolvedRows[0]).length <= 3 && Object.values(resolvedRows[0]).some(v => typeof v === 'number')));
|
|
579
|
-
|
|
580
|
-
// 1. Healing: Override explicit view if it contradicts the data shape
|
|
581
|
-
if (config.view === 'chart' && hasProductLikeData && !hasAggregationLikeData) {
|
|
582
|
-
config.view = 'carousel';
|
|
583
|
-
} else if (config.view === 'carousel' && hasAggregationLikeData && !hasProductLikeData) {
|
|
584
|
-
config.view = 'chart';
|
|
585
|
-
config.chartType = (config.chartType as 'pie' | 'bar' | 'line') || 'bar';
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
// 2. Auto-Detection: Fill in missing or dynamic view
|
|
589
|
-
if (!config.view || ['pie', 'bar', 'line', 'pie_chart', 'bar_chart', 'line_chart'].includes(String(config.view))) {
|
|
590
|
-
const viewStr = String(config.view || '').toLowerCase();
|
|
591
|
-
|
|
592
|
-
if (viewStr.includes('carousel') || config.type === 'products' || Array.isArray(config.items) || hasProductLikeData) {
|
|
593
|
-
config.view = 'carousel';
|
|
594
|
-
} else if (
|
|
595
|
-
viewStr.includes('chart') ||
|
|
596
|
-
viewStr.includes('pie') || viewStr.includes('bar') || viewStr.includes('line') ||
|
|
597
|
-
hasAggregationLikeData ||
|
|
598
|
-
(typeof config.type === 'string' && ['pie', 'bar', 'line'].includes(config.type)) ||
|
|
599
|
-
(typeof config.chartType === 'string' && ['pie', 'bar', 'line'].includes(config.chartType))
|
|
600
|
-
) {
|
|
601
|
-
config.view = 'chart';
|
|
602
|
-
config.chartType = (config.chartType as 'pie' | 'bar' | 'line') ||
|
|
603
|
-
(viewStr.replace('_chart', '') as 'pie' | 'bar' | 'line') ||
|
|
604
|
-
(config.type as 'pie' | 'bar' | 'line') || 'bar';
|
|
605
|
-
} else if (viewStr.includes('table')) {
|
|
606
|
-
config.view = 'table';
|
|
607
|
-
} else {
|
|
608
|
-
config.view = 'table';
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
const normalizedConfig: UIConfig = {
|
|
613
|
-
...(config as Omit<UIConfig, 'data'>),
|
|
614
|
-
data: normalizeTabularRows(resolvedRows, config.columns),
|
|
615
|
-
};
|
|
616
|
-
|
|
617
|
-
return { config: normalizedConfig };
|
|
618
|
-
} catch (err) {
|
|
619
|
-
return { error: String(err) };
|
|
620
|
-
}
|
|
621
|
-
}, [rawContent, isStreaming]);
|
|
622
|
-
|
|
623
|
-
if ('loading' in result) {
|
|
624
|
-
return (
|
|
625
|
-
<div className="my-4 p-8 bg-slate-50 dark:bg-white/5 rounded-xl border border-dashed border-slate-300 dark:border-white/10 flex flex-col items-center justify-center gap-3 animate-pulse">
|
|
626
|
-
<div className="w-5 h-5 border-2 border-indigo-500 border-t-transparent rounded-full animate-spin" />
|
|
627
|
-
<p className="text-xs text-slate-500 font-medium italic">Preparing view...</p>
|
|
628
|
-
</div>
|
|
629
|
-
);
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
if ('error' in result) {
|
|
633
|
-
return <pre className="p-4 my-2 bg-slate-900 text-slate-100 rounded-lg text-[10px] overflow-auto max-h-40"><code>{rawContent}</code></pre>;
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
const { config } = result;
|
|
637
|
-
const isCompact = viewportSize === 'compact';
|
|
638
|
-
const hasInsights = Array.isArray(config.insights) && config.insights.length > 0;
|
|
639
|
-
const hasDescription = typeof config.description === 'string' && config.description.trim().length > 0;
|
|
640
|
-
|
|
641
|
-
switch (config.view) {
|
|
642
|
-
case 'chart':
|
|
643
|
-
return (
|
|
644
|
-
<div className={`${isCompact ? 'my-4 p-3' : 'my-6 p-4'} bg-white dark:bg-slate-900 rounded-2xl border border-slate-200 dark:border-white/10 shadow-sm overflow-hidden`}>
|
|
645
|
-
{config.title && <h4 className={`${isCompact ? 'text-[11px] mb-3' : 'text-xs mb-4'} font-semibold text-slate-500 px-2`}>{config.title}</h4>}
|
|
646
|
-
{hasDescription && (
|
|
647
|
-
<p className={`px-2 ${isCompact ? 'text-xs' : 'text-sm'} text-slate-500 dark:text-white/60`}>{config.description}</p>
|
|
648
|
-
)}
|
|
649
|
-
<DynamicChart
|
|
650
|
-
config={{
|
|
651
|
-
type: (config.chartType || 'bar') as 'bar' | 'line' | 'pie',
|
|
652
|
-
data: config.data as unknown as Record<string, string | number>[],
|
|
653
|
-
xAxisKey: config.xAxisKey,
|
|
654
|
-
dataKeys: config.dataKeys,
|
|
655
|
-
colors: config.colors,
|
|
656
|
-
}}
|
|
657
|
-
primaryColor={primaryColor}
|
|
658
|
-
accentColor={accentColor}
|
|
659
|
-
viewportSize={viewportSize}
|
|
660
|
-
/>
|
|
661
|
-
{hasInsights && (
|
|
662
|
-
<div className="mt-4 flex flex-wrap gap-2 px-2">
|
|
663
|
-
{config.insights?.map((insight, i) => (
|
|
664
|
-
<span
|
|
665
|
-
key={`insight-${i}`}
|
|
666
|
-
className={`rounded-full border border-emerald-200 bg-emerald-50 ${isCompact ? 'px-2.5 py-1 text-[10px]' : 'px-3 py-1 text-[11px]'} font-medium text-emerald-700 dark:border-emerald-500/20 dark:bg-emerald-500/10 dark:text-emerald-300`}
|
|
667
|
-
>
|
|
668
|
-
{String(insight)}
|
|
669
|
-
</span>
|
|
670
|
-
))}
|
|
671
|
-
</div>
|
|
672
|
-
)}
|
|
673
|
-
</div>
|
|
674
|
-
);
|
|
675
|
-
case 'carousel':
|
|
676
|
-
return (
|
|
677
|
-
<div className="my-4">
|
|
678
|
-
{config.title && <h4 className={`${isCompact ? 'mb-1.5 text-[11px]' : 'mb-2 text-xs'} font-semibold text-slate-500`}>{config.title}</h4>}
|
|
679
|
-
{hasDescription && (
|
|
680
|
-
<p className={`mb-3 ${isCompact ? 'text-xs' : 'text-sm'} text-slate-500 dark:text-white/60`}>{config.description}</p>
|
|
681
|
-
)}
|
|
682
|
-
<ProductCarousel products={(config.data as unknown as Product[]).map(item => ({
|
|
683
|
-
...item,
|
|
684
|
-
image: item.image ?? (typeof resolveImage === 'function' ? resolveImage(item as unknown as Record<string, unknown>) : undefined)
|
|
685
|
-
}))} primaryColor={primaryColor} onAddToCart={onAddToCart} />
|
|
686
|
-
</div>
|
|
687
|
-
);
|
|
688
|
-
case 'table':
|
|
689
|
-
return (
|
|
690
|
-
<div className={`${isCompact ? 'my-3 p-3 max-h-[320px]' : 'my-4 p-4 max-h-[400px]'} bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-white/10 shadow-sm overflow-auto`}>
|
|
691
|
-
{config.title && <h4 className={`${isCompact ? 'text-[11px]' : 'text-xs'} font-semibold text-slate-500 mb-2`}>{config.title}</h4>}
|
|
692
|
-
{hasDescription && (
|
|
693
|
-
<p className={`mb-3 ${isCompact ? 'text-xs' : 'text-sm'} text-slate-500 dark:text-white/60`}>{config.description}</p>
|
|
694
|
-
)}
|
|
695
|
-
<DataTable config={{
|
|
696
|
-
type: 'table',
|
|
697
|
-
title: config.title,
|
|
698
|
-
description: config.description,
|
|
699
|
-
columns: config.columns,
|
|
700
|
-
dataKeys: config.dataKeys,
|
|
701
|
-
xAxisKey: config.xAxisKey,
|
|
702
|
-
data: config.data,
|
|
703
|
-
}} viewportSize={viewportSize} />
|
|
704
|
-
</div>
|
|
705
|
-
);
|
|
706
|
-
default:
|
|
707
|
-
return null;
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
|
|
10
|
+
import { ObservabilityPanel } from './ObservabilityPanel';
|
|
11
|
+
import HourglassLoader from './HourglassLoader';
|
|
12
|
+
import {
|
|
13
|
+
extractStructuredPayload,
|
|
14
|
+
isLikelyUiOnlyMessage,
|
|
15
|
+
extractProductsFromSources,
|
|
16
|
+
extractProductsFromContent,
|
|
17
|
+
deduplicateProducts,
|
|
18
|
+
looksLikeStructuredPayload,
|
|
19
|
+
stripStructuredUiLabels,
|
|
20
|
+
stripMarkdownTables
|
|
21
|
+
} from '../utils/ProductExtractor';
|
|
22
|
+
import { createMarkdownComponents } from './MarkdownComponents';
|
|
23
|
+
import { UIDispatcher } from './UIDispatcher';
|
|
712
24
|
|
|
713
25
|
// ─── Main component ───────────────────────────────────────────────────────────
|
|
714
26
|
|
|
@@ -724,15 +36,18 @@ export function MessageBubble({
|
|
|
724
36
|
const isUser = message.role === 'user';
|
|
725
37
|
const isCompact = viewportSize === 'compact';
|
|
726
38
|
const isMedium = viewportSize === 'medium';
|
|
39
|
+
|
|
727
40
|
const [showSources, setShowSources] = React.useState(false);
|
|
41
|
+
const [showTrace, setShowTrace] = React.useState(false);
|
|
42
|
+
|
|
728
43
|
const structuredContent = React.useMemo(
|
|
729
44
|
() => isUser ? { payload: null, text: message.content } : extractStructuredPayload(message.content),
|
|
730
45
|
[isUser, message.content],
|
|
731
46
|
);
|
|
47
|
+
|
|
732
48
|
const hasRichUI = React.useMemo(() => {
|
|
733
49
|
if (message.uiTransformation) {
|
|
734
50
|
const type = (message.uiTransformation as UITransformationResponse).type;
|
|
735
|
-
// Table is not considered "Rich enough" to block a product carousel
|
|
736
51
|
if (type && !['text', 'table'].includes(type)) return true;
|
|
737
52
|
}
|
|
738
53
|
if (structuredContent.payload) {
|
|
@@ -743,156 +58,33 @@ export function MessageBubble({
|
|
|
743
58
|
|
|
744
59
|
const shouldRenderStructuredOnly = !isUser && hasRichUI && isLikelyUiOnlyMessage(structuredContent.text);
|
|
745
60
|
|
|
746
|
-
// ──
|
|
747
|
-
const productsFromSources = React.useMemo<Product[]>(() => {
|
|
748
|
-
if (isUser || !sources) return [];
|
|
749
|
-
return sources
|
|
750
|
-
.filter(s => {
|
|
751
|
-
const m = s.metadata ?? {};
|
|
752
|
-
const keys = Object.keys(m).map(k => k.toLowerCase());
|
|
753
|
-
const hasProductKey = keys.some(k =>
|
|
754
|
-
['price', 'image', 'img', 'thumbnail', 'images', 'brand', 'product', 'sku', 'category', 'model', 'cost'].includes(k)
|
|
755
|
-
);
|
|
756
|
-
const hasPricePattern = /\$\s*\d+/.test(s.content);
|
|
757
|
-
return hasProductKey || hasPricePattern || m.type === 'product';
|
|
758
|
-
})
|
|
759
|
-
.map(s => {
|
|
760
|
-
const m = (s.metadata ?? {}) as Record<string, unknown>;
|
|
761
|
-
const name = getMetadataValue(m, ['name', 'product', 'title', 'label', 'item']) as string;
|
|
762
|
-
const brand = getMetadataValue(m, ['brand', 'manufacturer', 'vendor', 'make']) as string;
|
|
763
|
-
const price = getMetadataValue(m, ['price', 'cost', 'amount', 'msrp', 'rate']) as string | number;
|
|
61
|
+
// ── Extraction ─────────────────────────────────────────────────────────────
|
|
764
62
|
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
name: name ?? s.content.split('\n')[0] ?? 'Unknown Product',
|
|
768
|
-
brand: brand,
|
|
769
|
-
price: price,
|
|
770
|
-
image: resolveImage(m),
|
|
771
|
-
link: getMetadataValue(m, ['link', 'url', 'product_url']) as string,
|
|
772
|
-
description: s.content,
|
|
773
|
-
};
|
|
774
|
-
});
|
|
63
|
+
const productsFromSources = React.useMemo<Product[]>(() => {
|
|
64
|
+
return extractProductsFromSources(sources, isUser);
|
|
775
65
|
}, [sources, isUser]);
|
|
776
66
|
|
|
777
|
-
// ── Products + cleaned content from structured JSON blocks in the message ──
|
|
778
67
|
const { productsFromContent, cleanContent } = React.useMemo(() => {
|
|
779
68
|
if (isUser) {
|
|
780
69
|
return { productsFromContent: [] as Product[], cleanContent: message.content };
|
|
781
70
|
}
|
|
782
71
|
|
|
783
|
-
const
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
: null;
|
|
797
|
-
|
|
798
|
-
if (
|
|
799
|
-
(data.type === 'products' || data.view === 'carousel' || data.view === 'table' || Array.isArray(data.products)) &&
|
|
800
|
-
itemSet
|
|
801
|
-
) {
|
|
802
|
-
products.push(
|
|
803
|
-
...itemSet.map((item: Record<string, unknown>) => ({
|
|
804
|
-
...item,
|
|
805
|
-
image: (item.image as string) ?? resolveImage(item),
|
|
806
|
-
})) as Product[],
|
|
807
|
-
);
|
|
808
|
-
}
|
|
809
|
-
} catch {
|
|
810
|
-
// Ignore malformed product payloads
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
if (content.includes('"type": "products"') || content.includes('"type":"products"')) {
|
|
815
|
-
for (const match of Array.from(content.matchAll(jsonRegex))) {
|
|
816
|
-
try {
|
|
817
|
-
const data = JSON.parse(sanitizeJson(match[1]));
|
|
818
|
-
if (data.type === 'products' && Array.isArray(data.items)) {
|
|
819
|
-
products.push(
|
|
820
|
-
...data.items.map((item: Record<string, unknown>) => ({
|
|
821
|
-
...item,
|
|
822
|
-
image: (item.image as string) ?? resolveImage(item),
|
|
823
|
-
})) as Product[],
|
|
824
|
-
);
|
|
825
|
-
content = content.replace(match[0], '');
|
|
826
|
-
}
|
|
827
|
-
} catch {
|
|
828
|
-
// Malformed product block — skip silently
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
// 4. Fallback: Scan for plain text bulleted products (e.g. • Name - $Price)
|
|
834
|
-
// Support various bullets (optional) and dash types (-, –, —)
|
|
835
|
-
const bulletRegex = /(?:[•*-]\s*)?([^•\n\-\$*–—\(]+?)(?:\s*\(?Price\s*[:\-–—]?\s*)?(?:\s*[:\-–—]\s*|\s+)\$?([\d,.]+)(?:\s*USD)?/gi;
|
|
836
|
-
const matches = Array.from(content.matchAll(bulletRegex));
|
|
837
|
-
|
|
838
|
-
if (matches.length >= 2) {
|
|
839
|
-
const normalize = (s: string) => s.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
840
|
-
|
|
841
|
-
for (const match of matches) {
|
|
842
|
-
let name = match[1]?.trim() || '';
|
|
843
|
-
// Clean up common leftovers
|
|
844
|
-
name = name.replace(/\s*\(?Price\s*$/i, '').replace(/[:\-–—]\s*$/g, '').trim();
|
|
845
|
-
|
|
846
|
-
let price = match[2]?.trim() || '';
|
|
847
|
-
price = price.replace(/[,\s]+$/, '').trim(); // Remove trailing commas or spaces
|
|
848
|
-
|
|
849
|
-
if (name && price) {
|
|
850
|
-
const newProduct: Product = {
|
|
851
|
-
id: `text-prod-${name}-${price}`,
|
|
852
|
-
name,
|
|
853
|
-
price: `$${price}`,
|
|
854
|
-
};
|
|
855
|
-
|
|
856
|
-
// Rehydration: Fuzzy match in sources to get image/brand/link
|
|
857
|
-
const normName = normalize(name);
|
|
858
|
-
const sourceMatch = productsFromSources.find(s => {
|
|
859
|
-
const sn = normalize(s.name);
|
|
860
|
-
return sn.includes(normName) || normName.includes(sn);
|
|
861
|
-
});
|
|
862
|
-
|
|
863
|
-
if (sourceMatch) {
|
|
864
|
-
newProduct.image = sourceMatch.image;
|
|
865
|
-
newProduct.brand = sourceMatch.brand;
|
|
866
|
-
newProduct.link = sourceMatch.link;
|
|
867
|
-
newProduct.description = sourceMatch.description;
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
products.push(newProduct);
|
|
871
|
-
content = content.replace(match[0], `\n**${name}** — $${price}`);
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
content = content.replace(/\n{3,}/g, '\n\n').trim();
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
return { productsFromContent: products, cleanContent: content.trim() };
|
|
878
|
-
}, [message.content, isUser, structuredContent, productsFromSources]);
|
|
72
|
+
const payloadCandidates = [
|
|
73
|
+
structuredContent.payload,
|
|
74
|
+
...Array.from(structuredContent.text.matchAll(/```(?:json|ui)?\s*([\s\S]*?)\s*```/g)).map(m => m[1])
|
|
75
|
+
].filter(Boolean) as string[];
|
|
76
|
+
|
|
77
|
+
return extractProductsFromContent(
|
|
78
|
+
message.content,
|
|
79
|
+
structuredContent.text,
|
|
80
|
+
payloadCandidates,
|
|
81
|
+
productsFromSources,
|
|
82
|
+
isStreaming
|
|
83
|
+
);
|
|
84
|
+
}, [message.content, isUser, structuredContent, productsFromSources, isStreaming]);
|
|
879
85
|
|
|
880
|
-
// ── Merge & deduplicate products ───────────────────────────────────────────
|
|
881
86
|
const allProducts = React.useMemo<Product[]>(() => {
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
const seen = new Set<string>();
|
|
885
|
-
const contentLower = message.content.toLowerCase();
|
|
886
|
-
|
|
887
|
-
return productsFromSources.filter(p => {
|
|
888
|
-
const id = String(p.id ?? p.name);
|
|
889
|
-
if (seen.has(id)) return false;
|
|
890
|
-
const mentioned =
|
|
891
|
-
contentLower.includes(p.name.toLowerCase()) ||
|
|
892
|
-
(p.brand ? contentLower.includes(p.brand.toLowerCase()) : false);
|
|
893
|
-
if (mentioned) { seen.add(id); return true; }
|
|
894
|
-
return false;
|
|
895
|
-
});
|
|
87
|
+
return deduplicateProducts(productsFromSources, productsFromContent, message.content);
|
|
896
88
|
}, [productsFromSources, productsFromContent, message.content]);
|
|
897
89
|
|
|
898
90
|
const processedMarkdown = React.useMemo<string>(() => {
|
|
@@ -903,12 +95,9 @@ export function MessageBubble({
|
|
|
903
95
|
raw = raw.replace(/([^\n])\n\|/g, '$1\n\n|') // Ensure blank line before table
|
|
904
96
|
.replace(/(\|[^\n]*\|)\n\n+(?=\|)/g, '$1\n'); // Remove blank lines between rows
|
|
905
97
|
|
|
906
|
-
// 2. HEALING:
|
|
907
|
-
// This handles models (like Llama 3.2) that often omit the ```chart markers.
|
|
98
|
+
// 2. HEALING: Wrap naked JSON blocks
|
|
908
99
|
if (!isStreaming) {
|
|
909
|
-
// Look for objects that look like chart or product data
|
|
910
100
|
raw = raw.replace(/(?:^|\n)\s*\{[\s\S]*\}\s*(?:\n|$)/g, (match) => {
|
|
911
|
-
// Only wrap if not already in a code block
|
|
912
101
|
if (match.includes('```')) return match;
|
|
913
102
|
if (!looksLikeStructuredPayload(match)) return match;
|
|
914
103
|
const type = match.includes('"view"') || match.includes('"chartType"') || match.includes('"type": "pie"') || match.includes('"type":"pie"') || match.includes('"type": "bar"') || match.includes('"type":"bar"') || match.includes('"type": "line"') || match.includes('"type":"line"')
|
|
@@ -918,156 +107,19 @@ export function MessageBubble({
|
|
|
918
107
|
});
|
|
919
108
|
}
|
|
920
109
|
|
|
921
|
-
// 2b.
|
|
922
|
-
// markdown tables that often follow as a fallback explanation.
|
|
110
|
+
// 2b. Suppress duplicated markdown tables if a UI block is present
|
|
923
111
|
if (hasStructuredUiBlock) {
|
|
924
112
|
raw = stripStructuredUiLabels(stripMarkdownTables(raw))
|
|
925
113
|
.replace(/\n{3,}/g, '\n\n')
|
|
926
114
|
.trim();
|
|
927
115
|
}
|
|
928
116
|
|
|
929
|
-
// 3. HIJACKING: If streaming, hide table-like content to prevent raw markdown flashing.
|
|
930
|
-
if (isStreaming) {
|
|
931
|
-
raw = raw.replace(/((?:^|\n)\|.*)+/g, (match) => {
|
|
932
|
-
return `\n\n\`\`\`table-loading\n${match.trim()}\n\`\`\`\n\n`;
|
|
933
|
-
});
|
|
934
|
-
}
|
|
935
|
-
|
|
936
117
|
return raw;
|
|
937
118
|
}, [cleanContent, message.content, isStreaming, structuredContent]);
|
|
938
119
|
|
|
939
120
|
// ── Markdown component overrides ───────────────────────────────────────────
|
|
940
|
-
const markdownComponents
|
|
941
|
-
() => ({
|
|
942
|
-
// Wrap in not-prose so Tailwind Typography resets don't clobber our styles.
|
|
943
|
-
// prose applies display:block and padding:0 to table/th/td — not-prose opts out.
|
|
944
|
-
table: ({ ...props }: React.HTMLAttributes<HTMLTableElement>) => {
|
|
945
|
-
if (isStreaming) {
|
|
946
|
-
return (
|
|
947
|
-
<div className="my-5 p-6 bg-slate-50 dark:bg-white/5 rounded-xl border border-dashed border-slate-300 dark:border-white/10 flex items-center justify-center gap-3 select-none">
|
|
948
|
-
<div className="w-4 h-4 border-2 border-indigo-500 border-t-transparent rounded-full animate-spin" />
|
|
949
|
-
<p className="text-xs text-slate-500 font-medium italic animate-pulse">
|
|
950
|
-
Generating data table...
|
|
951
|
-
</p>
|
|
952
|
-
</div>
|
|
953
|
-
);
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
return (
|
|
957
|
-
<div className="not-prose overflow-hidden my-5 rounded-xl border border-slate-200 dark:border-white/10 shadow-sm bg-white dark:bg-slate-900/50">
|
|
958
|
-
<div className="overflow-x-auto">
|
|
959
|
-
<table
|
|
960
|
-
className="!table w-full text-left border-collapse min-w-[400px] text-sm"
|
|
961
|
-
{...props}
|
|
962
|
-
/>
|
|
963
|
-
</div>
|
|
964
|
-
</div>
|
|
965
|
-
);
|
|
966
|
-
},
|
|
967
|
-
thead: ({ ...props }: React.HTMLAttributes<HTMLTableSectionElement>) => (
|
|
968
|
-
<thead
|
|
969
|
-
className="!table-header-group bg-slate-50 dark:bg-white/5 border-b border-slate-200 dark:border-white/10"
|
|
970
|
-
{...props}
|
|
971
|
-
/>
|
|
972
|
-
),
|
|
973
|
-
tbody: ({ ...props }: React.HTMLAttributes<HTMLTableSectionElement>) => (
|
|
974
|
-
<tbody className="!table-row-group divide-y divide-slate-100 dark:divide-white/5" {...props} />
|
|
975
|
-
),
|
|
976
|
-
tr: ({ ...props }: React.HTMLAttributes<HTMLTableRowElement>) => (
|
|
977
|
-
<tr
|
|
978
|
-
className="!table-row hover:bg-slate-50/70 dark:hover:bg-white/5 transition-colors"
|
|
979
|
-
{...props}
|
|
980
|
-
/>
|
|
981
|
-
),
|
|
982
|
-
th: ({ children, ...props }: React.ThHTMLAttributes<HTMLTableCellElement>) => (
|
|
983
|
-
<th
|
|
984
|
-
className="!table-cell px-4 py-3 font-bold text-slate-700 dark:text-white/90 whitespace-nowrap text-[11px] uppercase tracking-wider text-left"
|
|
985
|
-
{...props}
|
|
986
|
-
>
|
|
987
|
-
{normaliseChild(children)}
|
|
988
|
-
</th>
|
|
989
|
-
),
|
|
990
|
-
td: ({ children, ...props }: React.TdHTMLAttributes<HTMLTableCellElement>) => (
|
|
991
|
-
<td
|
|
992
|
-
className="!table-cell px-4 py-3 text-slate-600 dark:text-white/70 whitespace-nowrap text-sm"
|
|
993
|
-
{...props}
|
|
994
|
-
>
|
|
995
|
-
{normaliseChild(children)}
|
|
996
|
-
</td>
|
|
997
|
-
),
|
|
998
|
-
code({
|
|
999
|
-
inline,
|
|
1000
|
-
className,
|
|
1001
|
-
children,
|
|
1002
|
-
...props
|
|
1003
|
-
}: React.HTMLAttributes<HTMLElement> & { inline?: boolean }) {
|
|
1004
|
-
const lang = /language-(\w+)/.exec(className ?? '')?.[1];
|
|
1005
|
-
|
|
1006
|
-
if (!inline && (lang === 'ui' || lang === 'chart')) {
|
|
1007
|
-
return (
|
|
1008
|
-
<UIDispatcher
|
|
1009
|
-
rawContent={String(children ?? '').trim()}
|
|
1010
|
-
primaryColor={primaryColor}
|
|
1011
|
-
accentColor={accentColor}
|
|
1012
|
-
isStreaming={isStreaming}
|
|
1013
|
-
onAddToCart={onAddToCart}
|
|
1014
|
-
viewportSize={viewportSize}
|
|
1015
|
-
/>
|
|
1016
|
-
);
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
if (!inline && lang === 'json') {
|
|
1020
|
-
const content = String(children ?? '').trim();
|
|
1021
|
-
if (looksLikeStructuredPayload(content)) {
|
|
1022
|
-
return (
|
|
1023
|
-
<UIDispatcher
|
|
1024
|
-
rawContent={content}
|
|
1025
|
-
primaryColor={primaryColor}
|
|
1026
|
-
accentColor={accentColor}
|
|
1027
|
-
isStreaming={isStreaming}
|
|
1028
|
-
onAddToCart={onAddToCart}
|
|
1029
|
-
viewportSize={viewportSize}
|
|
1030
|
-
/>
|
|
1031
|
-
);
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
if (!inline) {
|
|
1036
|
-
const content = String(children ?? '').trim();
|
|
1037
|
-
if (looksLikeStructuredPayload(content)) {
|
|
1038
|
-
return (
|
|
1039
|
-
<UIDispatcher
|
|
1040
|
-
rawContent={content}
|
|
1041
|
-
primaryColor={primaryColor}
|
|
1042
|
-
accentColor={accentColor}
|
|
1043
|
-
isStreaming={isStreaming}
|
|
1044
|
-
onAddToCart={onAddToCart}
|
|
1045
|
-
viewportSize={viewportSize}
|
|
1046
|
-
/>
|
|
1047
|
-
);
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
if (!inline && lang === 'table-loading') {
|
|
1052
|
-
return (
|
|
1053
|
-
<div className="my-5 p-6 bg-slate-50 dark:bg-white/5 rounded-xl border border-dashed border-slate-300 dark:border-white/10 flex items-center justify-center gap-3 select-none">
|
|
1054
|
-
<div className="w-4 h-4 border-2 border-indigo-500 border-t-transparent rounded-full animate-spin" />
|
|
1055
|
-
<p className="text-xs text-slate-500 font-medium italic animate-pulse">
|
|
1056
|
-
Generating data table...
|
|
1057
|
-
</p>
|
|
1058
|
-
</div>
|
|
1059
|
-
);
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
return (
|
|
1063
|
-
<code className={className} {...props}>
|
|
1064
|
-
{typeof children === 'string' || typeof children === 'number'
|
|
1065
|
-
? children
|
|
1066
|
-
: JSON.stringify(children)}
|
|
1067
|
-
</code>
|
|
1068
|
-
);
|
|
1069
|
-
},
|
|
1070
|
-
}),
|
|
121
|
+
const markdownComponents = React.useMemo(
|
|
122
|
+
() => createMarkdownComponents({ primaryColor, accentColor, isStreaming, onAddToCart, viewportSize }),
|
|
1071
123
|
[primaryColor, accentColor, isStreaming, onAddToCart, viewportSize],
|
|
1072
124
|
);
|
|
1073
125
|
|
|
@@ -1085,7 +137,7 @@ export function MessageBubble({
|
|
|
1085
137
|
{isUser ? <User className={`${isCompact ? 'w-3.5 h-3.5' : 'w-4 h-4'} text-white`} /> : <Bot className={`${isCompact ? 'w-3.5 h-3.5' : 'w-4 h-4'}`} />}
|
|
1086
138
|
</div>
|
|
1087
139
|
|
|
1088
|
-
<div className={`flex flex-col gap-1 ${isCompact ? 'max-w-[94%]' : isMedium ? 'max-w-[92%]' : 'max-w-[90%]'} ${isUser ? 'items-end' : 'items-start'}`}>
|
|
140
|
+
<div className={`flex flex-col gap-1 min-w-0 ${isCompact ? 'max-w-[94%]' : isMedium ? 'max-w-[92%]' : 'max-w-[90%]'} ${isUser ? 'items-end' : 'items-start'}`}>
|
|
1089
141
|
{/* Bubble */}
|
|
1090
142
|
<div
|
|
1091
143
|
className={`relative ${isCompact ? 'px-3 py-2.5 text-[13px]' : 'px-4 py-3 text-sm'} rounded-2xl leading-relaxed shadow-sm dark:shadow-lg ${isUser
|
|
@@ -1121,15 +173,22 @@ export function MessageBubble({
|
|
|
1121
173
|
</ReactMarkdown>
|
|
1122
174
|
)}
|
|
1123
175
|
|
|
1124
|
-
{/*
|
|
1125
|
-
{isStreaming &&
|
|
1126
|
-
<span className="inline-
|
|
176
|
+
{/* Fix: Always show streaming cursor when isStreaming is true */}
|
|
177
|
+
{isStreaming && (
|
|
178
|
+
<span className="inline-flex items-center gap-1.5 ml-2 text-emerald-500 animate-pulse align-middle text-xs font-medium">
|
|
179
|
+
<HourglassLoader
|
|
180
|
+
size={18}
|
|
181
|
+
primaryColor={primaryColor}
|
|
182
|
+
accentColor={accentColor}
|
|
183
|
+
glow={false} />
|
|
184
|
+
Thinking...
|
|
185
|
+
</span>
|
|
1127
186
|
)}
|
|
1128
187
|
</div>
|
|
1129
188
|
)}
|
|
1130
189
|
</div>
|
|
1131
190
|
|
|
1132
|
-
{/* Auto-generated visualization fallback
|
|
191
|
+
{/* Auto-generated visualization fallback */}
|
|
1133
192
|
{(() => {
|
|
1134
193
|
if (isUser || structuredContent.payload || !message.uiTransformation) return null;
|
|
1135
194
|
const ui = message.uiTransformation as UITransformationResponse;
|
|
@@ -1147,13 +206,13 @@ export function MessageBubble({
|
|
|
1147
206
|
primaryColor={primaryColor}
|
|
1148
207
|
onAddToCart={onAddToCart}
|
|
1149
208
|
className="rounded-3xl border border-slate-200/60 dark:border-white/10 bg-white/90 dark:bg-slate-900/80 p-4"
|
|
209
|
+
isStreaming={isStreaming}
|
|
1150
210
|
/>
|
|
1151
211
|
</div>
|
|
1152
212
|
);
|
|
1153
213
|
})()}
|
|
1154
214
|
|
|
1155
|
-
|
|
1156
|
-
{/* Product Carousel (only if no other rich UI is present) */}
|
|
215
|
+
{/* Product Carousel */}
|
|
1157
216
|
{!isUser && !hasRichUI && allProducts.length > 0 && (
|
|
1158
217
|
<div className="w-full mt-1">
|
|
1159
218
|
<ProductCarousel
|
|
@@ -1164,26 +223,46 @@ export function MessageBubble({
|
|
|
1164
223
|
</div>
|
|
1165
224
|
)}
|
|
1166
225
|
|
|
1167
|
-
{/* Sources */}
|
|
1168
|
-
{!isUser &&
|
|
1169
|
-
<div className="w-full">
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
226
|
+
{/* Footer actions: Sources and Trace */}
|
|
227
|
+
{!isUser && (
|
|
228
|
+
<div className="flex items-center gap-4 mt-1 w-full flex-wrap">
|
|
229
|
+
{sources && sources.length > 0 && (
|
|
230
|
+
<button
|
|
231
|
+
onClick={() => setShowSources(s => !s)}
|
|
232
|
+
className="text-[11px] text-indigo-400 hover:text-indigo-300 transition-colors flex items-center gap-1"
|
|
233
|
+
>
|
|
234
|
+
{showSources ? <ChevronDown className="w-3 h-3" /> : <ChevronRight className="w-3 h-3" />}
|
|
235
|
+
{sources.length} source{sources.length !== 1 ? 's' : ''} used
|
|
236
|
+
</button>
|
|
237
|
+
)}
|
|
1177
238
|
|
|
1178
|
-
{
|
|
1179
|
-
<
|
|
1180
|
-
{
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
239
|
+
{message.trace && (
|
|
240
|
+
<button
|
|
241
|
+
onClick={() => setShowTrace(t => !t)}
|
|
242
|
+
className="text-[11px] text-emerald-500 hover:text-emerald-400 transition-colors flex items-center gap-1"
|
|
243
|
+
>
|
|
244
|
+
<Activity className="w-3 h-3" />
|
|
245
|
+
{showTrace ? 'Hide Trace' : 'Trace'}
|
|
246
|
+
</button>
|
|
1184
247
|
)}
|
|
1185
248
|
</div>
|
|
1186
249
|
)}
|
|
250
|
+
|
|
251
|
+
{/* Expandable areas */}
|
|
252
|
+
<div className="w-full flex flex-col gap-2 min-w-0">
|
|
253
|
+
{showSources && sources && sources.length > 0 && (
|
|
254
|
+
<div className="mt-1 flex flex-col gap-2">
|
|
255
|
+
{sources.map((src, i) => (
|
|
256
|
+
<SourceCard key={src.id} source={src} index={i} />
|
|
257
|
+
))}
|
|
258
|
+
</div>
|
|
259
|
+
)}
|
|
260
|
+
|
|
261
|
+
{showTrace && message.trace && (
|
|
262
|
+
<ObservabilityPanel trace={message.trace} primaryColor={primaryColor} />
|
|
263
|
+
)}
|
|
264
|
+
</div>
|
|
265
|
+
|
|
1187
266
|
</div>
|
|
1188
267
|
</div>
|
|
1189
268
|
);
|