Skip to content

Commit 270c1be

Browse files
committed
add detail view
1 parent 127bc2a commit 270c1be

File tree

4 files changed

+238
-21
lines changed

4 files changed

+238
-21
lines changed

dashboard/ai-analytics/src/app/components/DataTable.tsx

Lines changed: 218 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use client';
22

3+
import { useState } from 'react';
34
import {
45
Table,
56
TableBody,
@@ -9,6 +10,8 @@ import {
910
TableRow,
1011
} from '@tremor/react';
1112
import { format } from 'date-fns';
13+
import { X } from 'lucide-react';
14+
import { useLLMMessages } from '@/hooks/useTinybirdData';
1215

1316
// Define the shape of the LLM message data
1417
interface LLMMessage {
@@ -18,6 +21,7 @@ interface LLMMessage {
1821
environment: string;
1922
user: string;
2023
chat_id: string;
24+
message_id?: string;
2125
model: string;
2226
provider: string;
2327
prompt_tokens: number;
@@ -28,6 +32,8 @@ interface LLMMessage {
2832
response_status: string;
2933
exception: string | null;
3034
similarity?: number;
35+
messages?: string[];
36+
response_choices?: string[];
3137
}
3238

3339
interface DataTableProps {
@@ -60,11 +66,194 @@ const MOCK_DATA = {
6066
]
6167
};
6268

69+
// Detail view component
70+
function DetailView({ message, onClose }: { message: LLMMessage, onClose: () => void }) {
71+
const { data: chatData, isLoading } = useLLMMessages({
72+
chat_id: message.chat_id,
73+
message_id: message.message_id
74+
});
75+
76+
// Parse messages from JSON string if needed
77+
const parseMessages = (msg: LLMMessage) => {
78+
if (typeof msg.messages === 'string') {
79+
try {
80+
return JSON.parse(msg.messages);
81+
} catch (e) {
82+
console.error('Failed to parse messages:', e);
83+
return [];
84+
}
85+
}
86+
return msg.messages || [];
87+
};
88+
89+
// Parse response choices from JSON string
90+
const parseResponseChoices = (msg: LLMMessage) => {
91+
if (!msg.response_choices || msg.response_status !== 'success') return [];
92+
93+
if (typeof msg.response_choices === 'string') {
94+
try {
95+
return JSON.parse(msg.response_choices);
96+
} catch (e) {
97+
try {
98+
// Handle case where response_choices is an array of JSON strings
99+
return (msg.response_choices as unknown as string[]).map(choice => JSON.parse(choice));
100+
} catch (e2) {
101+
console.error('Failed to parse response choices:', e2);
102+
return [];
103+
}
104+
}
105+
}
106+
return msg.response_choices;
107+
};
108+
109+
return (
110+
<div className="fixed inset-y-0 right-0 w-1/3 bg-gray-800 shadow-xl z-50 overflow-auto transition-transform duration-300 transform translate-x-0">
111+
<div className="p-4 border-b border-gray-700 flex justify-between items-center">
112+
<h2 className="text-xl font-semibold text-white">Conversation Details</h2>
113+
<button
114+
onClick={onClose}
115+
className="p-1 rounded-full hover:bg-gray-700 transition-colors"
116+
>
117+
<X className="h-6 w-6 text-gray-400" />
118+
</button>
119+
</div>
120+
121+
<div className="p-4">
122+
<div className="mb-6 bg-gray-900 rounded-lg p-4">
123+
<h3 className="text-lg font-medium text-white mb-2">Message Info</h3>
124+
<div className="grid grid-cols-2 gap-2 text-sm">
125+
<div className="text-gray-400">Model</div>
126+
<div className="text-white">{message.model}</div>
127+
128+
<div className="text-gray-400">Provider</div>
129+
<div className="text-white">{message.provider}</div>
130+
131+
<div className="text-gray-400">Organization</div>
132+
<div className="text-white">{message.organization}</div>
133+
134+
<div className="text-gray-400">Project</div>
135+
<div className="text-white">{message.project}</div>
136+
137+
<div className="text-gray-400">User</div>
138+
<div className="text-white">{message.user}</div>
139+
140+
<div className="text-gray-400">Timestamp</div>
141+
<div className="text-white">{format(new Date(message.timestamp), 'MMM d, yyyy HH:mm:ss')}</div>
142+
143+
<div className="text-gray-400">Total Tokens</div>
144+
<div className="text-white">{message.total_tokens.toLocaleString()}</div>
145+
146+
<div className="text-gray-400">Duration</div>
147+
<div className="text-white">{message.duration.toFixed(2)}s</div>
148+
149+
<div className="text-gray-400">Cost</div>
150+
<div className="text-white">${message.cost.toFixed(4)}</div>
151+
152+
<div className="text-gray-400">Status</div>
153+
<div className="text-white">
154+
<span className={`px-2 py-1 rounded-full text-xs ${
155+
message.response_status === 'success'
156+
? 'bg-green-100 text-green-800'
157+
: 'bg-red-100 text-red-800'
158+
}`}>
159+
{message.response_status}
160+
</span>
161+
</div>
162+
</div>
163+
</div>
164+
165+
<h3 className="text-lg font-medium text-white mb-2">Conversation</h3>
166+
167+
{isLoading ? (
168+
<div className="text-center py-4 text-gray-400">Loading conversation...</div>
169+
) : (
170+
<div className="space-y-4">
171+
{chatData?.data && chatData.data.length > 0 ? (
172+
chatData.data.map((msg: LLMMessage, idx: number) => {
173+
const messages = parseMessages(msg);
174+
175+
// If we have structured messages, render those
176+
if (messages && messages.length > 0) {
177+
return (
178+
<div key={idx} className="space-y-2">
179+
{messages.map((chatMsg: any, msgIdx: number) => (
180+
<div key={`${idx}-${msgIdx}`} className={`rounded-lg p-3 text-white ${
181+
chatMsg.role === 'user' ? 'bg-gray-700' :
182+
chatMsg.role === 'assistant' ? 'bg-blue-900' :
183+
'bg-purple-900'
184+
}`}>
185+
<div className="text-xs text-gray-400 mb-1 capitalize">{chatMsg.role}</div>
186+
<div className="whitespace-pre-wrap">{chatMsg.content}</div>
187+
</div>
188+
))}
189+
190+
{/* Show alternative responses if available */}
191+
{msg.response_status === 'success' && (
192+
<>
193+
{parseResponseChoices(msg).length > 1 && (
194+
<div className="mt-4">
195+
<div className="text-sm text-gray-400 mb-2">Alternative Responses:</div>
196+
<div className="space-y-2">
197+
{parseResponseChoices(msg).slice(1).map((choice: any, choiceIdx: number) => (
198+
<div key={`choice-${choiceIdx}`} className="bg-gray-700 rounded-lg p-3 text-white opacity-75">
199+
<div className="text-xs text-gray-400 mb-1">Alternative {choiceIdx + 1}</div>
200+
<div className="whitespace-pre-wrap">
201+
{choice.message?.content || choice.content || JSON.stringify(choice)}
202+
</div>
203+
</div>
204+
))}
205+
</div>
206+
</div>
207+
)}
208+
</>
209+
)}
210+
</div>
211+
);
212+
}
213+
214+
// Fallback to prompt/response if no structured messages
215+
return (
216+
<div key={idx} className="space-y-2">
217+
{msg.prompt && (
218+
<div className="bg-gray-700 rounded-lg p-3 text-white">
219+
<div className="text-xs text-gray-400 mb-1">User</div>
220+
<div className="whitespace-pre-wrap">{msg.prompt}</div>
221+
</div>
222+
)}
223+
224+
{msg.response && (
225+
<div className="bg-blue-900 rounded-lg p-3 text-white">
226+
<div className="text-xs text-gray-400 mb-1">Assistant</div>
227+
<div className="whitespace-pre-wrap">{msg.response}</div>
228+
</div>
229+
)}
230+
231+
{msg.response_status !== 'success' && msg.exception && (
232+
<div className="bg-red-900 rounded-lg p-3 text-white">
233+
<div className="text-xs text-gray-400 mb-1">Error</div>
234+
<div className="whitespace-pre-wrap">{msg.exception}</div>
235+
</div>
236+
)}
237+
</div>
238+
);
239+
})
240+
) : (
241+
<div className="text-center py-4 text-gray-400">No conversation data available</div>
242+
)}
243+
</div>
244+
)}
245+
</div>
246+
</div>
247+
);
248+
}
249+
63250
export default function DataTable({
64251
data = MOCK_DATA,
65252
isLoading = false,
66253
searchHighlight = null
67254
}: DataTableProps) {
255+
const [selectedMessage, setSelectedMessage] = useState<LLMMessage | null>(null);
256+
68257
if (isLoading) {
69258
return (
70259
<div className="flex items-center justify-center h-full">
@@ -73,20 +262,23 @@ export default function DataTable({
73262
);
74263
}
75264

265+
const handleRowClick = (message: LLMMessage) => {
266+
setSelectedMessage(message);
267+
};
268+
269+
const handleCloseDetail = () => {
270+
setSelectedMessage(null);
271+
};
272+
76273
return (
77-
<div className="flex flex-col h-full">
78-
<div className="flex-none">
79-
<div className="md:flex md:items-center md:justify-between md:space-x-8">
80-
{/* <div>
81-
<h3 className="font-semibold text-tremor-content-strong dark:text-dark-tremor-content-strong">
82-
Recent Messages
83-
</h3>
84-
<p className="mt-1 text-tremor-default leading-6 text-tremor-content dark:text-dark-tremor-content">
85-
Detailed view of recent LLM interactions
86-
</p>
87-
</div> */}
88-
</div>
89-
</div>
274+
<div className="flex flex-col h-full relative">
275+
{/* Overlay when detail view is open */}
276+
{selectedMessage && (
277+
<div
278+
className="fixed inset-0 bg-black bg-opacity-50 z-40"
279+
onClick={handleCloseDetail}
280+
/>
281+
)}
90282

91283
<div className="flex-1 overflow-auto min-h-0">
92284
<div className="min-w-[1024px]">
@@ -111,7 +303,11 @@ export default function DataTable({
111303
<TableBody>
112304
{data.data && data.data.length > 0 ? (
113305
data.data.map((item, idx) => (
114-
<TableRow key={idx}>
306+
<TableRow
307+
key={idx}
308+
onClick={() => handleRowClick(item)}
309+
className="cursor-pointer hover:bg-gray-800 transition-colors"
310+
>
115311
<TableCell>{format(new Date(item.timestamp), 'MMM d, yyyy HH:mm:ss')}</TableCell>
116312
<TableCell>{item.model}</TableCell>
117313
<TableCell>{item.provider}</TableCell>
@@ -154,6 +350,14 @@ export default function DataTable({
154350
</Table>
155351
</div>
156352
</div>
353+
354+
{/* Detail View */}
355+
{selectedMessage && (
356+
<DetailView
357+
message={selectedMessage}
358+
onClose={handleCloseDetail}
359+
/>
360+
)}
157361
</div>
158362
);
159363
}

dashboard/ai-analytics/src/app/containers/DataTableContainer.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ export default function DataTableContainer({ filters, isLoading = false }: DataT
5656
const messagesQuery = useLLMMessages({
5757
...filters,
5858
...(embedding ? {
59-
embedding: JSON.stringify(embedding),
60-
similarity_threshold: 0.7
59+
embedding: embedding,
60+
similarity_threshold: 0.3
6161
} : {})
6262
});
6363

dashboard/ai-analytics/src/hooks/useTinybirdData.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ export function useLLMVectorSearch(
8787
queryFn: () => searchLLMMessagesByVector(token!, {
8888
...filters,
8989
embedding: embedding || undefined,
90-
similarity_threshold: 0.7, // Adjust as needed
90+
similarity_threshold: 0.3, // Adjust as needed
9191
}),
9292
enabled: !!token && !!embedding,
9393
});

tinybird/endpoints/llm_messages.pipe

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ NODE llm_messages_node
77
SQL >
88
%
99
{% if defined(embedding) %}
10-
with
11-
if(length(embedding) > 0, cosineDistance(embedding, {{ Array(embedding, 'Float32') }}), 1.0) as similarity
10+
with cosineDistance(embedding, {{ Array(embedding, 'Float32') }}) as similarity
1211
{% end %}
1312
SELECT
1413
timestamp,
@@ -17,6 +16,7 @@ SQL >
1716
environment,
1817
user,
1918
chat_id,
19+
message_id,
2020
model,
2121
provider,
2222
prompt_tokens,
@@ -26,6 +26,16 @@ SQL >
2626
cost,
2727
response_status,
2828
exception
29+
{% if defined(embedding) %}
30+
, similarity
31+
{% else %}
32+
, 0 as similarity
33+
{% end %}
34+
{% if defined(chat_id) %}
35+
, messages, response_choices
36+
{% else %}
37+
, [] as messages, [] as response_choices
38+
{% end %}
2939
FROM llm_events
3040
WHERE 1
3141
{% if defined(organization) %}
@@ -49,6 +59,9 @@ SQL >
4959
{% if defined(chat_id) %}
5060
AND chat_id = {{String(chat_id, '')}}
5161
{% end %}
62+
{% if defined(message_id) %}
63+
AND message_id = {{String(message_id, '')}}
64+
{% end %}
5265
{% if defined(start_date) %}
5366
AND timestamp >= {{DateTime(start_date)}}
5467
{% else %}
@@ -60,10 +73,10 @@ SQL >
6073
AND timestamp < now()
6174
{% end %}
6275
{% if defined(embedding) %}
63-
AND cosineDistance(embedding, {{ Array(embedding, 'Float32') }}) > {{ Float64(similarity_threshold, 0.7) }}
76+
AND cosineDistance(embedding, {{ Array(embedding, 'Float32') }}) > {{ Float64(similarity_threshold, 0.3) }}
6477
{% end %}
6578
{% if defined(embedding) %}
66-
ORDER BY similarity DESC
79+
ORDER BY similarity ASC
6780
{% else %}
6881
ORDER BY timestamp DESC
6982
{% end %}

0 commit comments

Comments
 (0)