@tanstack/ai-devtools-core 0.3.38 → 0.4.1
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/Shell/DKCFIT6H.js +12142 -0
- package/dist/Shell/SLOX2TPU.js +12119 -0
- package/dist/Shell/Z6HLJOVZ.js +10170 -0
- package/dist/dev.js +1 -1
- package/dist/index.js +1 -1
- package/dist/production/Shell/DKCFIT6H.js +12142 -0
- package/dist/production/Shell/SLOX2TPU.js +12119 -0
- package/dist/production/Shell/Z6HLJOVZ.js +10170 -0
- package/dist/production/dev.js +1 -1
- package/dist/production/index.js +1 -1
- package/dist/production/server.js +1 -1
- package/dist/server.js +1 -1
- package/package.json +4 -4
- package/src/components/Shell.tsx +74 -30
- package/src/components/conversation/IterationCard.tsx +261 -12
- package/src/components/conversation/IterationTimeline.tsx +66 -16
- package/src/components/conversation/index.ts +0 -9
- package/src/components/hooks/FixtureNamePopover.tsx +69 -0
- package/src/components/hooks/GenerationPanel.tsx +847 -0
- package/src/components/hooks/HookDashboard.tsx +292 -0
- package/src/components/hooks/HookDetails.tsx +1757 -0
- package/src/components/hooks/ToolFixtureForm.tsx +347 -0
- package/src/components/hooks/hook-dashboard-model.ts +134 -0
- package/src/components/hooks/index.ts +4 -0
- package/src/components/hooks/preview-messages.ts +117 -0
- package/src/components/hooks/preview-model.ts +195 -0
- package/src/components/utils/format.ts +0 -58
- package/src/env.d.ts +5 -0
- package/src/store/ai-context.tsx +1165 -159
- package/src/store/ai-store.ts +0 -1
- package/src/store/hook-registry.ts +815 -0
- package/src/store/message-event-utils.ts +60 -0
- package/src/styles/use-styles.ts +1558 -99
- package/dist/Shell/MRWZEHAJ.js +0 -6225
- package/dist/Shell/RRRNZJ5E.js +0 -7560
- package/dist/production/Shell/MRWZEHAJ.js +0 -6225
- package/dist/production/Shell/RRRNZJ5E.js +0 -7560
- package/src/components/ConversationDetails.tsx +0 -216
- package/src/components/ConversationsList.tsx +0 -21
- package/src/components/conversation/ActivityEventsTab.tsx +0 -57
- package/src/components/conversation/ChunkBadges.tsx +0 -51
- package/src/components/conversation/ChunkItem.tsx +0 -273
- package/src/components/conversation/ChunksCollapsible.tsx +0 -56
- package/src/components/conversation/ChunksTab.tsx +0 -87
- package/src/components/conversation/ConversationHeader.tsx +0 -106
- package/src/components/conversation/ConversationTabs.tsx +0 -205
- package/src/components/conversation/MessageCard.tsx +0 -174
- package/src/components/conversation/MessageGroup.tsx +0 -101
- package/src/components/conversation/MessagesTab.tsx +0 -30
- package/src/components/conversation/SummariesTab.tsx +0 -91
- package/src/components/conversation/ToolCallDisplay.tsx +0 -99
- package/src/components/list/ConversationRow.tsx +0 -85
- 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
|
-
}
|