@tanstack/ai-devtools-core 0.3.37 → 0.4.0

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 (52) hide show
  1. package/dist/Shell/DKCFIT6H.js +12142 -0
  2. package/dist/Shell/SLOX2TPU.js +12119 -0
  3. package/dist/Shell/Z6HLJOVZ.js +10170 -0
  4. package/dist/dev.js +1 -1
  5. package/dist/index.js +1 -1
  6. package/dist/production/Shell/DKCFIT6H.js +12142 -0
  7. package/dist/production/Shell/SLOX2TPU.js +12119 -0
  8. package/dist/production/Shell/Z6HLJOVZ.js +10170 -0
  9. package/dist/production/dev.js +1 -1
  10. package/dist/production/index.js +1 -1
  11. package/dist/production/server.js +1 -1
  12. package/dist/server.js +1 -1
  13. package/package.json +4 -4
  14. package/src/components/Shell.tsx +74 -30
  15. package/src/components/conversation/IterationCard.tsx +261 -12
  16. package/src/components/conversation/IterationTimeline.tsx +66 -16
  17. package/src/components/conversation/index.ts +0 -9
  18. package/src/components/hooks/FixtureNamePopover.tsx +69 -0
  19. package/src/components/hooks/GenerationPanel.tsx +847 -0
  20. package/src/components/hooks/HookDashboard.tsx +292 -0
  21. package/src/components/hooks/HookDetails.tsx +1757 -0
  22. package/src/components/hooks/ToolFixtureForm.tsx +347 -0
  23. package/src/components/hooks/hook-dashboard-model.ts +134 -0
  24. package/src/components/hooks/index.ts +4 -0
  25. package/src/components/hooks/preview-messages.ts +117 -0
  26. package/src/components/hooks/preview-model.ts +195 -0
  27. package/src/components/utils/format.ts +0 -58
  28. package/src/store/ai-context.tsx +1165 -159
  29. package/src/store/ai-store.ts +0 -1
  30. package/src/store/hook-registry.ts +815 -0
  31. package/src/store/message-event-utils.ts +60 -0
  32. package/src/styles/use-styles.ts +1558 -99
  33. package/dist/Shell/MRWZEHAJ.js +0 -6225
  34. package/dist/Shell/RRRNZJ5E.js +0 -7560
  35. package/dist/production/Shell/MRWZEHAJ.js +0 -6225
  36. package/dist/production/Shell/RRRNZJ5E.js +0 -7560
  37. package/src/components/ConversationDetails.tsx +0 -216
  38. package/src/components/ConversationsList.tsx +0 -21
  39. package/src/components/conversation/ActivityEventsTab.tsx +0 -57
  40. package/src/components/conversation/ChunkBadges.tsx +0 -51
  41. package/src/components/conversation/ChunkItem.tsx +0 -273
  42. package/src/components/conversation/ChunksCollapsible.tsx +0 -56
  43. package/src/components/conversation/ChunksTab.tsx +0 -87
  44. package/src/components/conversation/ConversationHeader.tsx +0 -106
  45. package/src/components/conversation/ConversationTabs.tsx +0 -205
  46. package/src/components/conversation/MessageCard.tsx +0 -174
  47. package/src/components/conversation/MessageGroup.tsx +0 -101
  48. package/src/components/conversation/MessagesTab.tsx +0 -30
  49. package/src/components/conversation/SummariesTab.tsx +0 -91
  50. package/src/components/conversation/ToolCallDisplay.tsx +0 -99
  51. package/src/components/list/ConversationRow.tsx +0 -85
  52. package/src/components/list/index.ts +0 -1
@@ -1,87 +0,0 @@
1
- import { For, Show } from 'solid-js'
2
- import { useStyles } from '../../styles/use-styles'
3
- import { MessageGroup } from './MessageGroup'
4
- import type { Component } from 'solid-js'
5
- import type { Chunk, Message } from '../../store/ai-store'
6
-
7
- interface ChunksTabProps {
8
- chunks: Array<Chunk>
9
- messages?: Array<Message>
10
- }
11
-
12
- export const ChunksTab: Component<ChunksTabProps> = (props) => {
13
- const styles = useStyles()
14
-
15
- // Create a map of messageId to usage for quick lookup
16
- const usageByMessageId = () => {
17
- const map = new Map<string, Message['usage']>()
18
- props.messages?.forEach((msg) => {
19
- if (msg.usage) {
20
- map.set(msg.id, msg.usage)
21
- }
22
- })
23
- return map
24
- }
25
-
26
- const groupedChunks = () => {
27
- const groups = new Map<string, Array<Chunk>>()
28
-
29
- props.chunks.forEach((chunk) => {
30
- const key = chunk.messageId || 'no-message-id'
31
- let group = groups.get(key)
32
- if (!group) {
33
- group = []
34
- groups.set(key, group)
35
- }
36
- group.push(chunk)
37
- })
38
-
39
- return Array.from(groups.entries())
40
- }
41
-
42
- // Calculate total raw chunks (sum of all chunkCounts)
43
- const totalRawChunks = () =>
44
- props.chunks.reduce((sum, c) => sum + (c.chunkCount || 1), 0)
45
-
46
- return (
47
- <Show
48
- when={props.chunks.length > 0}
49
- fallback={
50
- <div class={styles().conversationDetails.noChunks}>No chunks yet</div>
51
- }
52
- >
53
- <div class={styles().conversationDetails.streamContainer}>
54
- {/* Stream Header */}
55
- <div class={styles().conversationDetails.streamHeader}>
56
- <div class={styles().conversationDetails.streamHeaderRow}>
57
- <div class={styles().conversationDetails.streamTitle}>
58
- Stream Responses
59
- </div>
60
- <div
61
- class={`${styles().conversationDetails.chunkBadge} ${styles().conversationDetails.chunkBadgeCount}`}
62
- >
63
- {totalRawChunks()} chunks · {groupedChunks().length} messages
64
- </div>
65
- </div>
66
- <div class={styles().conversationDetails.streamSubtitle}>
67
- Grouped by message ID
68
- </div>
69
- </div>
70
-
71
- {/* Message Groups */}
72
- <div class={styles().conversationDetails.messageGroups}>
73
- <For each={groupedChunks()}>
74
- {([messageId, chunks], groupIndex) => (
75
- <MessageGroup
76
- messageId={messageId}
77
- chunks={chunks}
78
- groupIndex={groupIndex()}
79
- usage={usageByMessageId().get(messageId)}
80
- />
81
- )}
82
- </For>
83
- </div>
84
- </div>
85
- </Show>
86
- )
87
- }
@@ -1,106 +0,0 @@
1
- import { Show } from 'solid-js'
2
- import { useStyles } from '../../styles/use-styles'
3
- import { formatDuration } from '../utils'
4
- import type { Component } from 'solid-js'
5
- import type { Conversation } from '../../store/ai-context'
6
-
7
- interface ConversationHeaderProps {
8
- conversation: Conversation
9
- }
10
-
11
- export const ConversationHeader: Component<ConversationHeaderProps> = (
12
- props,
13
- ) => {
14
- const styles = useStyles()
15
- const conv = () => props.conversation
16
-
17
- const iterationCount = () => conv().iterationCount ?? conv().iterations.length
18
- const totalDuration = () => {
19
- const completedAt = conv().completedAt
20
- if (!completedAt) return undefined
21
- return completedAt - conv().startedAt
22
- }
23
-
24
- const totalMessages = () => conv().messages.length
25
-
26
- const totalToolCalls = () => {
27
- let count = 0
28
- for (const iter of conv().iterations) {
29
- if (iter.finishReason === 'tool_calls') count++
30
- }
31
- return count
32
- }
33
-
34
- // Sum usage across all iterations
35
- const totalUsage = () => {
36
- if (conv().usage) return conv().usage
37
- if (conv().iterations.length === 0) return undefined
38
- let promptTokens = 0
39
- let completionTokens = 0
40
- for (const iter of conv().iterations) {
41
- if (iter.usage) {
42
- promptTokens += iter.usage.promptTokens
43
- completionTokens += iter.usage.completionTokens
44
- }
45
- }
46
- if (promptTokens === 0 && completionTokens === 0) return undefined
47
- return {
48
- promptTokens,
49
- completionTokens,
50
- totalTokens: promptTokens + completionTokens,
51
- }
52
- }
53
-
54
- return (
55
- <div class={styles().panelHeader}>
56
- <div class={styles().conversationDetails.headerContent}>
57
- <div class={styles().conversationDetails.headerRow}>
58
- <div class={styles().conversationDetails.headerLabel}>
59
- {conv().label}
60
- </div>
61
- <div
62
- class={`${styles().conversationDetails.statusBadge} ${
63
- conv().status === 'active'
64
- ? styles().conversationDetails.statusActive
65
- : conv().status === 'completed'
66
- ? styles().conversationDetails.statusCompleted
67
- : styles().conversationDetails.statusError
68
- }`}
69
- >
70
- {conv().status}
71
- </div>
72
- </div>
73
- <div class={styles().conversationDetails.metaInfo}>
74
- {totalDuration() !== undefined && formatDuration(totalDuration())}
75
- <Show when={iterationCount() > 0}>
76
- {totalDuration() !== undefined && ' · '}
77
- {iterationCount()}{' '}
78
- {iterationCount() === 1 ? 'iteration' : 'iterations'}
79
- </Show>
80
- <Show when={totalMessages() > 0}>
81
- {(totalDuration() !== undefined || iterationCount() > 0) && ' · '}
82
- {totalMessages()} {totalMessages() === 1 ? 'message' : 'messages'}
83
- </Show>
84
- <Show when={totalToolCalls() > 0}>
85
- {' · '}
86
- {totalToolCalls()} tool {totalToolCalls() === 1 ? 'call' : 'calls'}
87
- </Show>
88
- </div>
89
- <Show when={totalUsage()}>
90
- <div class={styles().conversationDetails.usageInfo}>
91
- <span class={styles().conversationDetails.usageLabel}>Tokens:</span>
92
- <span>{totalUsage()?.promptTokens.toLocaleString() || 0} in</span>
93
- <span>·</span>
94
- <span>
95
- {totalUsage()?.completionTokens.toLocaleString() || 0} out
96
- </span>
97
- <span>·</span>
98
- <span class={styles().conversationDetails.usageBold}>
99
- {totalUsage()?.totalTokens.toLocaleString() || 0} total
100
- </span>
101
- </div>
102
- </Show>
103
- </div>
104
- </div>
105
- )
106
- }
@@ -1,205 +0,0 @@
1
- import { Show, createEffect, createSignal } from 'solid-js'
2
- import { useStyles } from '../../styles/use-styles'
3
- import type { Component } from 'solid-js'
4
- import type { Conversation } from '../../store/ai-context'
5
-
6
- export type TabType =
7
- | 'messages'
8
- | 'chunks'
9
- | 'summaries'
10
- | 'image'
11
- | 'speech'
12
- | 'transcription'
13
- | 'video'
14
-
15
- interface ConversationTabsProps {
16
- conversation: Conversation
17
- activeTab: TabType
18
- onTabChange: (tab: TabType) => void
19
- }
20
-
21
- export const ConversationTabs: Component<ConversationTabsProps> = (props) => {
22
- const styles = useStyles()
23
- const conv = () => props.conversation
24
- const hasIterations = () => conv().iterations.length > 0
25
-
26
- // Total raw chunks = sum of all chunkCounts
27
- const totalRawChunks = () =>
28
- conv().chunks.reduce((sum, c) => sum + (c.chunkCount || 1), 0)
29
-
30
- const summariesCount = () => conv().summaries?.length ?? 0
31
- const imageCount = () => conv().imageEvents?.length ?? 0
32
- const speechCount = () => conv().speechEvents?.length ?? 0
33
- const transcriptionCount = () => conv().transcriptionEvents?.length ?? 0
34
- const videoCount = () => conv().videoEvents?.length ?? 0
35
-
36
- const [imagePulse, setImagePulse] = createSignal(false)
37
- const [speechPulse, setSpeechPulse] = createSignal(false)
38
- const [transcriptionPulse, setTranscriptionPulse] = createSignal(false)
39
- const [videoPulse, setVideoPulse] = createSignal(false)
40
-
41
- const triggerPulse = (setter: (value: boolean) => void) => {
42
- setter(true)
43
- setTimeout(() => setter(false), 2000)
44
- }
45
-
46
- let previousImageCount = 0
47
- let previousSpeechCount = 0
48
- let previousTranscriptionCount = 0
49
- let previousVideoCount = 0
50
-
51
- createEffect(() => {
52
- const count = imageCount()
53
- if (count > 0 && previousImageCount === 0) {
54
- triggerPulse(setImagePulse)
55
- }
56
- previousImageCount = count
57
- })
58
-
59
- createEffect(() => {
60
- const count = speechCount()
61
- if (count > 0 && previousSpeechCount === 0) {
62
- triggerPulse(setSpeechPulse)
63
- }
64
- previousSpeechCount = count
65
- })
66
-
67
- createEffect(() => {
68
- const count = transcriptionCount()
69
- if (count > 0 && previousTranscriptionCount === 0) {
70
- triggerPulse(setTranscriptionPulse)
71
- }
72
- previousTranscriptionCount = count
73
- })
74
-
75
- createEffect(() => {
76
- const count = videoCount()
77
- if (count > 0 && previousVideoCount === 0) {
78
- triggerPulse(setVideoPulse)
79
- }
80
- previousVideoCount = count
81
- })
82
-
83
- // When iterations exist, only show activity tabs (no messages/chunks)
84
- const hasMessages = () =>
85
- !hasIterations() && conv().type === 'client' && conv().messages.length > 0
86
- const hasChunks = () =>
87
- !hasIterations() && (conv().chunks.length > 0 || conv().type === 'server')
88
- const hasSummaries = () => conv().hasSummarize || summariesCount() > 0
89
- const hasImage = () => conv().hasImage || imageCount() > 0
90
- const hasSpeech = () => conv().hasSpeech || speechCount() > 0
91
- const hasTranscription = () =>
92
- conv().hasTranscription || transcriptionCount() > 0
93
- const hasVideo = () => conv().hasVideo || videoCount() > 0
94
-
95
- // Count how many tabs would be visible
96
- const visibleTabCount = () => {
97
- let count = 0
98
- if (hasMessages()) count++
99
- if (hasChunks() && conv().type === 'server') count++
100
- if (hasSummaries()) count++
101
- if (hasImage()) count++
102
- if (hasSpeech()) count++
103
- if (hasTranscription()) count++
104
- if (hasVideo()) count++
105
- return count
106
- }
107
-
108
- // Don't render tabs if only one tab would be visible
109
- if (visibleTabCount() <= 1) {
110
- return null
111
- }
112
-
113
- return (
114
- <div class={styles().conversationDetails.tabsContainer}>
115
- <Show when={hasMessages()}>
116
- <button
117
- class={`${styles().actionButton} ${
118
- props.activeTab === 'messages'
119
- ? styles().conversationDetails.tabButtonActive
120
- : ''
121
- }`}
122
- onClick={() => props.onTabChange('messages')}
123
- >
124
- Messages ({conv().messages.length})
125
- </button>
126
- </Show>
127
- <Show when={hasChunks() && conv().type === 'server'}>
128
- <button
129
- class={`${styles().actionButton} ${
130
- props.activeTab === 'chunks'
131
- ? styles().conversationDetails.tabButtonActive
132
- : ''
133
- }`}
134
- onClick={() => props.onTabChange('chunks')}
135
- >
136
- Chunks ({totalRawChunks()})
137
- </button>
138
- </Show>
139
- <Show when={hasSummaries()}>
140
- <button
141
- class={`${styles().actionButton} ${
142
- props.activeTab === 'summaries'
143
- ? styles().conversationDetails.tabButtonActive
144
- : ''
145
- }`}
146
- onClick={() => props.onTabChange('summaries')}
147
- >
148
- Summaries ({summariesCount()})
149
- </button>
150
- </Show>
151
- <Show when={hasImage()}>
152
- <button
153
- class={`${styles().actionButton} ${
154
- props.activeTab === 'image'
155
- ? styles().conversationDetails.tabButtonActive
156
- : ''
157
- } ${imagePulse() ? styles().conversationDetails.tabButtonPulse : ''}`}
158
- onClick={() => props.onTabChange('image')}
159
- >
160
- Image ({imageCount()})
161
- </button>
162
- </Show>
163
- <Show when={hasSpeech()}>
164
- <button
165
- class={`${styles().actionButton} ${
166
- props.activeTab === 'speech'
167
- ? styles().conversationDetails.tabButtonActive
168
- : ''
169
- } ${speechPulse() ? styles().conversationDetails.tabButtonPulse : ''}`}
170
- onClick={() => props.onTabChange('speech')}
171
- >
172
- Speech ({speechCount()})
173
- </button>
174
- </Show>
175
- <Show when={hasTranscription()}>
176
- <button
177
- class={`${styles().actionButton} ${
178
- props.activeTab === 'transcription'
179
- ? styles().conversationDetails.tabButtonActive
180
- : ''
181
- } ${
182
- transcriptionPulse()
183
- ? styles().conversationDetails.tabButtonPulse
184
- : ''
185
- }`}
186
- onClick={() => props.onTabChange('transcription')}
187
- >
188
- Transcription ({transcriptionCount()})
189
- </button>
190
- </Show>
191
- <Show when={hasVideo()}>
192
- <button
193
- class={`${styles().actionButton} ${
194
- props.activeTab === 'video'
195
- ? styles().conversationDetails.tabButtonActive
196
- : ''
197
- } ${videoPulse() ? styles().conversationDetails.tabButtonPulse : ''}`}
198
- onClick={() => props.onTabChange('video')}
199
- >
200
- Video ({videoCount()})
201
- </button>
202
- </Show>
203
- </div>
204
- )
205
- }
@@ -1,174 +0,0 @@
1
- import { For, Show } from 'solid-js'
2
- import { JsonTree } from '@tanstack/devtools-ui'
3
- import { useStyles } from '../../styles/use-styles'
4
- import { formatDuration, formatTimestamp } from '../utils'
5
- import { ToolCallDisplay } from './ToolCallDisplay'
6
- import { ChunksCollapsible } from './ChunksCollapsible'
7
- import type { Message } from '../../store/ai-store'
8
- import type { Component } from 'solid-js'
9
-
10
- interface MessageCardProps {
11
- message: Message
12
- }
13
-
14
- export const MessageCard: Component<MessageCardProps> = (props) => {
15
- const styles = useStyles()
16
- // Access message through props directly for proper SolidJS reactivity
17
- const msg = () => props.message
18
-
19
- // Check if message is from client (explicitly marked) or server (default)
20
- const isClientMessage = () => msg().source === 'client'
21
-
22
- // Determine card class based on role and source
23
- const getCardClass = () => {
24
- const base = styles().conversationDetails.messageCard
25
- if (msg().role === 'user') {
26
- return `${base} ${styles().conversationDetails.messageCardUser}`
27
- }
28
- // For assistant messages: client if explicitly marked, otherwise server
29
- if (isClientMessage()) {
30
- return `${base} ${styles().conversationDetails.messageCardClient}`
31
- }
32
- return `${base} ${styles().conversationDetails.messageCardServer}`
33
- }
34
-
35
- const parseJsonContent = () => {
36
- const content = msg().content
37
- if (typeof content !== 'string') return null
38
- const trimmed = content.trim()
39
- if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) return null
40
- try {
41
- return JSON.parse(trimmed) as Record<string, unknown> | Array<unknown>
42
- } catch {
43
- return null
44
- }
45
- }
46
-
47
- const toolDuration = () => {
48
- const durations = msg()
49
- .toolCalls?.map((tool) => tool.duration || 0)
50
- .filter((duration) => duration > 0)
51
- if (!durations || durations.length === 0) return 0
52
- return durations.reduce((total, duration) => total + duration, 0)
53
- }
54
-
55
- return (
56
- <div class={getCardClass()}>
57
- {/* Source indicator banner at top of card */}
58
- <Show when={msg().role === 'assistant'}>
59
- <div
60
- class={`${styles().conversationDetails.sourceBanner} ${
61
- isClientMessage()
62
- ? styles().conversationDetails.sourceBannerClient
63
- : styles().conversationDetails.sourceBannerServer
64
- }`}
65
- >
66
- <span class={styles().conversationDetails.sourceBannerIcon}>
67
- {isClientMessage() ? '📱' : '☁️'}
68
- </span>
69
- <span class={styles().conversationDetails.sourceBannerText}>
70
- {isClientMessage() ? 'Client Message' : 'Server Message'}
71
- </span>
72
- </div>
73
- </Show>
74
- {/* Content wrapper with padding */}
75
- <div
76
- class={
77
- msg().role === 'user'
78
- ? ''
79
- : styles().conversationDetails.messageCardContent
80
- }
81
- >
82
- <div class={styles().conversationDetails.messageHeader}>
83
- <div
84
- class={
85
- msg().role === 'user'
86
- ? styles().conversationDetails.avatarUser
87
- : isClientMessage()
88
- ? styles().conversationDetails.avatarClient
89
- : styles().conversationDetails.avatarServer
90
- }
91
- >
92
- {msg().role === 'user' ? 'U' : msg().role === 'tool' ? '🔧' : '🤖'}
93
- </div>
94
- <div class={styles().conversationDetails.roleLabel}>
95
- <div
96
- class={
97
- msg().role === 'user'
98
- ? styles().conversationDetails.roleLabelUser
99
- : isClientMessage()
100
- ? styles().conversationDetails.roleLabelClient
101
- : styles().conversationDetails.roleLabelServer
102
- }
103
- >
104
- {msg().role}
105
- </div>
106
- </div>
107
- <div class={styles().conversationDetails.timestamp}>
108
- {formatTimestamp(msg().timestamp)}
109
- </div>
110
- {/* Per-message token usage */}
111
- <Show when={msg().usage}>
112
- <div class={styles().conversationDetails.messageUsage}>
113
- <span class={styles().conversationDetails.messageUsageIcon}>
114
- 🎯
115
- </span>
116
- <span>{msg().usage?.promptTokens.toLocaleString()} in</span>
117
- <span>•</span>
118
- <span>{msg().usage?.completionTokens.toLocaleString()} out</span>
119
- </div>
120
- </Show>
121
- <Show when={toolDuration() > 0}>
122
- <div class={styles().conversationDetails.messageUsage}>
123
- <span class={styles().conversationDetails.messageUsageIcon}>
124
- ⏱️
125
- </span>
126
- <span>{formatDuration(toolDuration())}</span>
127
- </div>
128
- </Show>
129
- </div>
130
-
131
- {/* Thinking content (for extended thinking models) */}
132
- <Show when={msg().thinkingContent}>
133
- <details class={styles().conversationDetails.thinkingDetails}>
134
- <summary class={styles().conversationDetails.thinkingSummary}>
135
- 💭 Thinking...
136
- </summary>
137
- <div class={styles().conversationDetails.thinkingContent}>
138
- {msg().thinkingContent}
139
- </div>
140
- </details>
141
- </Show>
142
-
143
- <Show
144
- when={msg().role === 'tool' && parseJsonContent()}
145
- fallback={
146
- <div class={styles().conversationDetails.messageContent}>
147
- {msg().content}
148
- </div>
149
- }
150
- >
151
- {(parsed) => (
152
- <div class={styles().conversationDetails.toolJsonContainer}>
153
- <JsonTree value={parsed()} />
154
- </div>
155
- )}
156
- </Show>
157
-
158
- {/* Tool Calls Display */}
159
- <Show when={msg().toolCalls?.length}>
160
- <div class={styles().conversationDetails.toolCallsContainer}>
161
- <For each={msg().toolCalls}>
162
- {(tool) => <ToolCallDisplay tool={tool} />}
163
- </For>
164
- </div>
165
- </Show>
166
-
167
- {/* Chunks Display (for client conversations with server chunks) */}
168
- <Show when={msg().chunks?.length ? msg().chunks : null}>
169
- {(chunks) => <ChunksCollapsible chunks={chunks()} />}
170
- </Show>
171
- </div>
172
- </div>
173
- )
174
- }
@@ -1,101 +0,0 @@
1
- import { For, Show } from 'solid-js'
2
- import { useStyles } from '../../styles/use-styles'
3
- import { ChunkItem } from './ChunkItem'
4
- import { ChunkBadges } from './ChunkBadges'
5
- import type { Component } from 'solid-js'
6
- import type { Chunk } from '../../store/ai-store'
7
-
8
- interface TokenUsage {
9
- promptTokens: number
10
- completionTokens: number
11
- totalTokens: number
12
- }
13
-
14
- interface MessageGroupProps {
15
- messageId: string
16
- chunks: Array<Chunk>
17
- groupIndex: number
18
- usage?: TokenUsage
19
- }
20
-
21
- export const MessageGroup: Component<MessageGroupProps> = (props) => {
22
- const styles = useStyles()
23
-
24
- const accumulatedContent = () =>
25
- props.chunks
26
- .filter((c) => c.type === 'content' && (c.content || c.delta))
27
- .map((c) => c.content)
28
- .join('')
29
-
30
- // Total raw chunks = sum of all chunkCounts
31
- const totalRawChunks = () =>
32
- props.chunks.reduce((sum, c) => sum + (c.chunkCount || 1), 0)
33
- // Consolidated entries = number of entries in the chunks array
34
- const consolidatedEntries = () => props.chunks.length
35
-
36
- return (
37
- <details class={styles().conversationDetails.messageGroupDetails}>
38
- <summary class={styles().conversationDetails.messageGroupSummary}>
39
- <div class={styles().conversationDetails.messageGroupContent}>
40
- {/* Header */}
41
- <div class={styles().conversationDetails.messageGroupHeader}>
42
- <span>Message #{props.groupIndex + 1}</span>
43
- <div
44
- class={`${styles().conversationDetails.chunkBadge} ${styles().conversationDetails.chunkBadgeCount}`}
45
- >
46
- 📦 {totalRawChunks()} chunks
47
- <Show when={consolidatedEntries() !== totalRawChunks()}>
48
- <span style={{ opacity: 0.7, 'margin-left': '4px' }}>
49
- ({consolidatedEntries()} entries)
50
- </span>
51
- </Show>
52
- </div>
53
- <ChunkBadges chunks={props.chunks} />
54
- {/* Usage info */}
55
- <Show when={props.usage}>
56
- <div class={styles().conversationDetails.messageUsage}>
57
- <span class={styles().conversationDetails.messageUsageIcon}>
58
- 🎯
59
- </span>
60
- <span>{props.usage?.promptTokens.toLocaleString()} in</span>
61
- <span>•</span>
62
- <span>
63
- {props.usage?.completionTokens.toLocaleString()} out
64
- </span>
65
- </div>
66
- </Show>
67
- </div>
68
-
69
- {/* Message ID */}
70
- <div
71
- class={styles().conversationDetails.messageId}
72
- title={props.messageId}
73
- >
74
- ID: {props.messageId}
75
- </div>
76
-
77
- {/* Accumulated Content Preview */}
78
- <Show when={accumulatedContent()}>
79
- <div
80
- class={styles().conversationDetails.contentPreview}
81
- title={accumulatedContent()}
82
- >
83
- {accumulatedContent()}
84
- </div>
85
- </Show>
86
- </div>
87
- </summary>
88
-
89
- {/* Chunks in this group */}
90
- <div class={styles().conversationDetails.chunksContainer}>
91
- <div class={styles().conversationDetails.chunksList}>
92
- <For each={props.chunks}>
93
- {(chunk, chunkIndex) => (
94
- <ChunkItem chunk={chunk} index={chunkIndex()} variant="large" />
95
- )}
96
- </For>
97
- </div>
98
- </div>
99
- </details>
100
- )
101
- }