@tanstack/ai-devtools-core 0.0.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.
Files changed (103) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +131 -0
  3. package/dist/esm/components/ConversationDetails.d.ts +2 -0
  4. package/dist/esm/components/ConversationDetails.js +134 -0
  5. package/dist/esm/components/ConversationDetails.js.map +1 -0
  6. package/dist/esm/components/ConversationsList.d.ts +2 -0
  7. package/dist/esm/components/ConversationsList.js +30 -0
  8. package/dist/esm/components/ConversationsList.js.map +1 -0
  9. package/dist/esm/components/Shell.d.ts +1 -0
  10. package/dist/esm/components/Shell.js +94 -0
  11. package/dist/esm/components/Shell.js.map +1 -0
  12. package/dist/esm/components/conversation/ChunkBadges.d.ts +7 -0
  13. package/dist/esm/components/conversation/ChunkBadges.js +54 -0
  14. package/dist/esm/components/conversation/ChunkBadges.js.map +1 -0
  15. package/dist/esm/components/conversation/ChunkItem.d.ts +9 -0
  16. package/dist/esm/components/conversation/ChunkItem.js +247 -0
  17. package/dist/esm/components/conversation/ChunkItem.js.map +1 -0
  18. package/dist/esm/components/conversation/ChunksCollapsible.d.ts +7 -0
  19. package/dist/esm/components/conversation/ChunksCollapsible.js +77 -0
  20. package/dist/esm/components/conversation/ChunksCollapsible.js.map +1 -0
  21. package/dist/esm/components/conversation/ChunksTab.d.ts +8 -0
  22. package/dist/esm/components/conversation/ChunksTab.js +87 -0
  23. package/dist/esm/components/conversation/ChunksTab.js.map +1 -0
  24. package/dist/esm/components/conversation/ConversationHeader.d.ts +7 -0
  25. package/dist/esm/components/conversation/ConversationHeader.js +187 -0
  26. package/dist/esm/components/conversation/ConversationHeader.js.map +1 -0
  27. package/dist/esm/components/conversation/ConversationTabs.d.ts +10 -0
  28. package/dist/esm/components/conversation/ConversationTabs.js +88 -0
  29. package/dist/esm/components/conversation/ConversationTabs.js.map +1 -0
  30. package/dist/esm/components/conversation/EmbeddingsTab.d.ts +7 -0
  31. package/dist/esm/components/conversation/EmbeddingsTab.js +90 -0
  32. package/dist/esm/components/conversation/EmbeddingsTab.js.map +1 -0
  33. package/dist/esm/components/conversation/MessageCard.d.ts +7 -0
  34. package/dist/esm/components/conversation/MessageCard.js +152 -0
  35. package/dist/esm/components/conversation/MessageCard.js.map +1 -0
  36. package/dist/esm/components/conversation/MessageGroup.d.ts +15 -0
  37. package/dist/esm/components/conversation/MessageGroup.js +119 -0
  38. package/dist/esm/components/conversation/MessageGroup.js.map +1 -0
  39. package/dist/esm/components/conversation/MessagesTab.d.ts +7 -0
  40. package/dist/esm/components/conversation/MessagesTab.js +37 -0
  41. package/dist/esm/components/conversation/MessagesTab.js.map +1 -0
  42. package/dist/esm/components/conversation/SummariesTab.d.ts +7 -0
  43. package/dist/esm/components/conversation/SummariesTab.js +110 -0
  44. package/dist/esm/components/conversation/SummariesTab.js.map +1 -0
  45. package/dist/esm/components/conversation/ToolCallDisplay.d.ts +7 -0
  46. package/dist/esm/components/conversation/ToolCallDisplay.js +123 -0
  47. package/dist/esm/components/conversation/ToolCallDisplay.js.map +1 -0
  48. package/dist/esm/components/conversation/index.d.ts +7 -0
  49. package/dist/esm/components/list/ConversationRow.d.ts +7 -0
  50. package/dist/esm/components/list/ConversationRow.js +122 -0
  51. package/dist/esm/components/list/ConversationRow.js.map +1 -0
  52. package/dist/esm/components/list/index.d.ts +1 -0
  53. package/dist/esm/components/utils/format.d.ts +6 -0
  54. package/dist/esm/components/utils/format.js +68 -0
  55. package/dist/esm/components/utils/format.js.map +1 -0
  56. package/dist/esm/components/utils/index.d.ts +1 -0
  57. package/dist/esm/core.d.ts +18 -0
  58. package/dist/esm/core.js +9 -0
  59. package/dist/esm/core.js.map +1 -0
  60. package/dist/esm/index.d.ts +9 -0
  61. package/dist/esm/index.js +6 -0
  62. package/dist/esm/index.js.map +1 -0
  63. package/dist/esm/production.d.ts +2 -0
  64. package/dist/esm/production.js +5 -0
  65. package/dist/esm/production.js.map +1 -0
  66. package/dist/esm/store/ai-context.d.ts +126 -0
  67. package/dist/esm/store/ai-context.js +1081 -0
  68. package/dist/esm/store/ai-context.js.map +1 -0
  69. package/dist/esm/store/ai-store.d.ts +1 -0
  70. package/dist/esm/styles/tokens.d.ts +298 -0
  71. package/dist/esm/styles/tokens.js +107 -0
  72. package/dist/esm/styles/tokens.js.map +1 -0
  73. package/dist/esm/styles/use-styles.d.ts +226 -0
  74. package/dist/esm/styles/use-styles.js +1538 -0
  75. package/dist/esm/styles/use-styles.js.map +1 -0
  76. package/package.json +61 -0
  77. package/src/components/ConversationDetails.tsx +89 -0
  78. package/src/components/ConversationsList.tsx +21 -0
  79. package/src/components/Shell.tsx +93 -0
  80. package/src/components/conversation/ChunkBadges.tsx +51 -0
  81. package/src/components/conversation/ChunkItem.tsx +273 -0
  82. package/src/components/conversation/ChunksCollapsible.tsx +58 -0
  83. package/src/components/conversation/ChunksTab.tsx +85 -0
  84. package/src/components/conversation/ConversationHeader.tsx +130 -0
  85. package/src/components/conversation/ConversationTabs.tsx +104 -0
  86. package/src/components/conversation/EmbeddingsTab.tsx +80 -0
  87. package/src/components/conversation/MessageCard.tsx +137 -0
  88. package/src/components/conversation/MessageGroup.tsx +101 -0
  89. package/src/components/conversation/MessagesTab.tsx +30 -0
  90. package/src/components/conversation/SummariesTab.tsx +91 -0
  91. package/src/components/conversation/ToolCallDisplay.tsx +99 -0
  92. package/src/components/conversation/index.ts +8 -0
  93. package/src/components/list/ConversationRow.tsx +85 -0
  94. package/src/components/list/index.ts +1 -0
  95. package/src/components/utils/format.ts +61 -0
  96. package/src/components/utils/index.ts +1 -0
  97. package/src/core.tsx +10 -0
  98. package/src/index.ts +8 -0
  99. package/src/production.ts +3 -0
  100. package/src/store/ai-context.tsx +1784 -0
  101. package/src/store/ai-store.ts +2 -0
  102. package/src/styles/tokens.ts +305 -0
  103. package/src/styles/use-styles.ts +1537 -0
@@ -0,0 +1,273 @@
1
+ import { Show, createSignal } from 'solid-js'
2
+ import { JsonTree } from '@tanstack/devtools-ui'
3
+ import { useStyles } from '../../styles/use-styles'
4
+ import { formatDuration, formatTimestamp, getChunkTypeColor } from '../utils'
5
+ import type { Component } from 'solid-js'
6
+ import type { Chunk } from '../../store/ai-store'
7
+
8
+ interface ChunkItemProps {
9
+ chunk: Chunk
10
+ index: number
11
+ variant?: 'small' | 'large'
12
+ }
13
+
14
+ export const ChunkItem: Component<ChunkItemProps> = (props) => {
15
+ const styles = useStyles()
16
+ const [showRaw, setShowRaw] = createSignal(false)
17
+ const isLarge = () => props.variant === 'large'
18
+ const chunkCount = () => props.chunk.chunkCount || 1
19
+
20
+ const parseToolArguments = (): Record<string, unknown> => {
21
+ try {
22
+ return JSON.parse(props.chunk.arguments || '{}') as Record<
23
+ string,
24
+ unknown
25
+ >
26
+ } catch {
27
+ return { raw: props.chunk.arguments }
28
+ }
29
+ }
30
+
31
+ const parseToolResult = (): Record<string, unknown> => {
32
+ try {
33
+ if (typeof props.chunk.result === 'string') {
34
+ return JSON.parse(props.chunk.result) as Record<string, unknown>
35
+ }
36
+ return props.chunk.result as Record<string, unknown>
37
+ } catch {
38
+ return { raw: props.chunk.result }
39
+ }
40
+ }
41
+
42
+ return (
43
+ <div
44
+ class={
45
+ isLarge()
46
+ ? styles().conversationDetails.chunkItemLarge
47
+ : styles().conversationDetails.chunkItem
48
+ }
49
+ >
50
+ {/* Chunk Header */}
51
+ <div
52
+ class={
53
+ isLarge()
54
+ ? styles().conversationDetails.chunkHeaderLarge
55
+ : styles().conversationDetails.chunkHeader
56
+ }
57
+ >
58
+ {/* Chunk Number */}
59
+ <div
60
+ class={
61
+ isLarge()
62
+ ? styles().conversationDetails.chunkNumberLarge
63
+ : styles().conversationDetails.chunkNumber
64
+ }
65
+ >
66
+ #{props.index + 1}
67
+ <Show when={chunkCount() > 1}>
68
+ <span style={{ 'margin-left': '4px', opacity: 0.7 }}>
69
+ ({chunkCount()} chunks)
70
+ </span>
71
+ </Show>
72
+ </div>
73
+
74
+ {/* Type Badge */}
75
+ <div
76
+ class={
77
+ isLarge()
78
+ ? styles().conversationDetails.chunkTypeBadgeLarge
79
+ : styles().conversationDetails.chunkTypeBadge
80
+ }
81
+ >
82
+ <div
83
+ class={
84
+ isLarge()
85
+ ? styles().conversationDetails.chunkTypeDotLarge
86
+ : styles().conversationDetails.chunkTypeDot
87
+ }
88
+ style={{ background: getChunkTypeColor(props.chunk.type) }}
89
+ />
90
+ <div
91
+ class={
92
+ isLarge()
93
+ ? styles().conversationDetails.chunkTypeLabelLarge
94
+ : styles().conversationDetails.chunkTypeLabel
95
+ }
96
+ >
97
+ {props.chunk.type}
98
+ </div>
99
+ </div>
100
+
101
+ {/* Tool Name Badge */}
102
+ <Show when={props.chunk.toolName}>
103
+ <div
104
+ class={
105
+ isLarge()
106
+ ? styles().conversationDetails.chunkToolBadgeLarge
107
+ : styles().conversationDetails.chunkToolBadge
108
+ }
109
+ >
110
+ 🔧 {props.chunk.toolName}
111
+ </div>
112
+ </Show>
113
+
114
+ {/* Timestamp */}
115
+ <div
116
+ class={
117
+ isLarge()
118
+ ? styles().conversationDetails.chunkTimestampLarge
119
+ : styles().conversationDetails.chunkTimestamp
120
+ }
121
+ >
122
+ {formatTimestamp(props.chunk.timestamp)}
123
+ </div>
124
+
125
+ {/* Toggle Raw JSON Button */}
126
+ <button
127
+ onClick={() => setShowRaw(!showRaw())}
128
+ class={`${
129
+ isLarge()
130
+ ? styles().conversationDetails.rawJsonButtonLarge
131
+ : styles().conversationDetails.rawJsonButton
132
+ } ${
133
+ showRaw()
134
+ ? styles().conversationDetails.rawJsonButtonActive
135
+ : styles().conversationDetails.rawJsonButtonInactive
136
+ }`}
137
+ title={showRaw() ? 'Show formatted' : 'Show raw JSON'}
138
+ >
139
+ {'{}'}
140
+ </button>
141
+ </div>
142
+
143
+ {/* Chunk Content */}
144
+ <Show when={!showRaw()}>
145
+ <Show when={props.chunk.content || props.chunk.delta}>
146
+ <div
147
+ class={
148
+ isLarge()
149
+ ? styles().conversationDetails.chunkContentLarge
150
+ : styles().conversationDetails.chunkContent
151
+ }
152
+ >
153
+ {props.chunk.content}
154
+ </div>
155
+ </Show>
156
+ <Show when={props.chunk.error}>
157
+ <div
158
+ class={
159
+ isLarge()
160
+ ? styles().conversationDetails.chunkErrorLarge
161
+ : styles().conversationDetails.chunkError
162
+ }
163
+ >
164
+ ❌ {props.chunk.error}
165
+ </div>
166
+ </Show>
167
+ <Show when={props.chunk.finishReason}>
168
+ <div
169
+ class={
170
+ isLarge()
171
+ ? styles().conversationDetails.chunkFinishLarge
172
+ : styles().conversationDetails.chunkFinish
173
+ }
174
+ >
175
+ ✓{' '}
176
+ {isLarge()
177
+ ? `Finish: ${props.chunk.finishReason}`
178
+ : props.chunk.finishReason}
179
+ </div>
180
+ </Show>
181
+ <Show when={props.chunk.type === 'approval'}>
182
+ <div
183
+ class={
184
+ isLarge()
185
+ ? styles().conversationDetails.chunkApprovalLarge
186
+ : styles().conversationDetails.chunkApproval
187
+ }
188
+ >
189
+ <div
190
+ class={
191
+ isLarge()
192
+ ? styles().conversationDetails.chunkApprovalTitleLarge
193
+ : styles().conversationDetails.chunkApprovalTitle
194
+ }
195
+ >
196
+ ⚠️ Approval Required
197
+ </div>
198
+ <Show when={props.chunk.input}>
199
+ <div
200
+ class={
201
+ isLarge()
202
+ ? styles().conversationDetails.chunkApprovalInputLarge
203
+ : styles().conversationDetails.chunkApprovalInput
204
+ }
205
+ >
206
+ Input: {JSON.stringify(props.chunk.input, null, 2)}
207
+ </div>
208
+ </Show>
209
+ </div>
210
+ </Show>
211
+ <Show when={props.chunk.type === 'tool_call' && props.chunk.arguments}>
212
+ <div class={styles().conversationDetails.chunkToolCall}>
213
+ <div class={styles().conversationDetails.chunkToolCallHeader}>
214
+ <span class={styles().conversationDetails.chunkToolCallTitle}>
215
+ 📤 Tool Arguments
216
+ </span>
217
+ </div>
218
+ <div class={styles().conversationDetails.toolJsonContainer}>
219
+ <JsonTree
220
+ value={parseToolArguments()}
221
+ defaultExpansionDepth={2}
222
+ copyable
223
+ />
224
+ </div>
225
+ </div>
226
+ </Show>
227
+ <Show
228
+ when={
229
+ props.chunk.type === 'tool_result' &&
230
+ props.chunk.result !== undefined
231
+ }
232
+ >
233
+ <div class={styles().conversationDetails.chunkToolResult}>
234
+ <div class={styles().conversationDetails.chunkToolResultHeader}>
235
+ <span class={styles().conversationDetails.chunkToolResultTitle}>
236
+ ✓ Tool Result
237
+ </span>
238
+ <Show
239
+ when={
240
+ props.chunk.duration !== undefined && props.chunk.duration > 0
241
+ }
242
+ >
243
+ <span class={styles().conversationDetails.durationBadge}>
244
+ ⏱️ {formatDuration(props.chunk.duration)}
245
+ </span>
246
+ </Show>
247
+ </div>
248
+ <div class={styles().conversationDetails.toolJsonContainer}>
249
+ <JsonTree
250
+ value={parseToolResult()}
251
+ defaultExpansionDepth={2}
252
+ copyable
253
+ />
254
+ </div>
255
+ </div>
256
+ </Show>
257
+ </Show>
258
+
259
+ {/* Raw JSON View */}
260
+ <Show when={showRaw()}>
261
+ <div
262
+ class={
263
+ isLarge()
264
+ ? styles().conversationDetails.rawJsonLarge
265
+ : styles().conversationDetails.rawJson
266
+ }
267
+ >
268
+ {JSON.stringify(props.chunk, null, 2)}
269
+ </div>
270
+ </Show>
271
+ </div>
272
+ )
273
+ }
@@ -0,0 +1,58 @@
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 ChunksCollapsibleProps {
9
+ chunks: Array<Chunk>
10
+ }
11
+
12
+ export const ChunksCollapsible: Component<ChunksCollapsibleProps> = (props) => {
13
+ const styles = useStyles()
14
+
15
+ const accumulatedContent = () =>
16
+ props.chunks
17
+ .filter((c) => c.type === 'content' && (c.content || c.delta))
18
+ .map((c) => c.delta || c.content)
19
+ .join('')
20
+
21
+ // Total raw chunks = sum of all chunkCounts
22
+ const totalRawChunks = () =>
23
+ props.chunks.reduce((sum, c) => sum + (c.chunkCount || 1), 0)
24
+
25
+ return (
26
+ <details class={styles().conversationDetails.chunksDetails}>
27
+ <summary class={styles().conversationDetails.chunksSummary}>
28
+ <div class={styles().conversationDetails.chunksSummaryRow}>
29
+ <span class={styles().conversationDetails.chunksSummaryArrow}>
30
+ ▶
31
+ </span>
32
+ <span class={styles().conversationDetails.chunksSummaryTitle}>
33
+ 📦 {totalRawChunks()} chunks
34
+ </span>
35
+ <ChunkBadges chunks={props.chunks} />
36
+ </div>
37
+ {/* Accumulated Content Preview */}
38
+ <Show when={accumulatedContent()}>
39
+ <div
40
+ class={styles().conversationDetails.contentPreview}
41
+ title={accumulatedContent()}
42
+ >
43
+ {accumulatedContent()}
44
+ </div>
45
+ </Show>
46
+ </summary>
47
+ <div class={styles().conversationDetails.chunksContainer}>
48
+ <div class={styles().conversationDetails.chunksList}>
49
+ <For each={props.chunks}>
50
+ {(chunk, index) => (
51
+ <ChunkItem chunk={chunk} index={index()} variant="small" />
52
+ )}
53
+ </For>
54
+ </div>
55
+ </div>
56
+ </details>
57
+ )
58
+ }
@@ -0,0 +1,85 @@
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
+ if (!groups.has(key)) {
32
+ groups.set(key, [])
33
+ }
34
+ groups.get(key)!.push(chunk)
35
+ })
36
+
37
+ return Array.from(groups.entries())
38
+ }
39
+
40
+ // Calculate total raw chunks (sum of all chunkCounts)
41
+ const totalRawChunks = () =>
42
+ props.chunks.reduce((sum, c) => sum + (c.chunkCount || 1), 0)
43
+
44
+ return (
45
+ <Show
46
+ when={props.chunks.length > 0}
47
+ fallback={
48
+ <div class={styles().conversationDetails.noChunks}>No chunks yet</div>
49
+ }
50
+ >
51
+ <div class={styles().conversationDetails.streamContainer}>
52
+ {/* Stream Header */}
53
+ <div class={styles().conversationDetails.streamHeader}>
54
+ <div class={styles().conversationDetails.streamHeaderRow}>
55
+ <div class={styles().conversationDetails.streamTitle}>
56
+ Stream Responses
57
+ </div>
58
+ <div
59
+ class={`${styles().conversationDetails.chunkBadge} ${styles().conversationDetails.chunkBadgeCount}`}
60
+ >
61
+ {totalRawChunks()} chunks · {groupedChunks().length} messages
62
+ </div>
63
+ </div>
64
+ <div class={styles().conversationDetails.streamSubtitle}>
65
+ Grouped by message ID
66
+ </div>
67
+ </div>
68
+
69
+ {/* Message Groups */}
70
+ <div class={styles().conversationDetails.messageGroups}>
71
+ <For each={groupedChunks()}>
72
+ {([messageId, chunks], groupIndex) => (
73
+ <MessageGroup
74
+ messageId={messageId}
75
+ chunks={chunks}
76
+ groupIndex={groupIndex()}
77
+ usage={usageByMessageId().get(messageId)}
78
+ />
79
+ )}
80
+ </For>
81
+ </div>
82
+ </div>
83
+ </Show>
84
+ )
85
+ }
@@ -0,0 +1,130 @@
1
+ import { For, 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 toolNames = () => conv().toolNames ?? []
18
+ const options = () => conv().options
19
+ const providerOptions = () => conv().providerOptions
20
+ const iterationCount = () => conv().iterationCount
21
+
22
+ return (
23
+ <div class={styles().panelHeader}>
24
+ <div class={styles().conversationDetails.headerContent}>
25
+ <div class={styles().conversationDetails.headerRow}>
26
+ <div class={styles().conversationDetails.headerLabel}>
27
+ {conv().label}
28
+ </div>
29
+ <div
30
+ class={`${styles().conversationDetails.statusBadge} ${
31
+ conv().status === 'active'
32
+ ? styles().conversationDetails.statusActive
33
+ : conv().status === 'completed'
34
+ ? styles().conversationDetails.statusCompleted
35
+ : styles().conversationDetails.statusError
36
+ }`}
37
+ >
38
+ {conv().status}
39
+ </div>
40
+ <Show when={iterationCount() !== undefined && iterationCount()! > 1}>
41
+ <div class={styles().conversationDetails.iterationBadge}>
42
+ 🔄 {iterationCount()} iterations
43
+ </div>
44
+ </Show>
45
+ </div>
46
+ <div class={styles().conversationDetails.metaInfo}>
47
+ {conv().model && `Model: ${conv().model}`}
48
+ {conv().provider && ` • Provider: ${conv().provider}`}
49
+ {conv().completedAt &&
50
+ ` • Duration: ${formatDuration(conv().completedAt! - conv().startedAt)}`}
51
+ </div>
52
+ <Show when={conv().usage}>
53
+ <div class={styles().conversationDetails.usageInfo}>
54
+ <span class={styles().conversationDetails.usageLabel}>
55
+ 🎯 Tokens:
56
+ </span>
57
+ <span>
58
+ Prompt: {conv().usage?.promptTokens.toLocaleString() || 0}
59
+ </span>
60
+ <span>•</span>
61
+ <span>
62
+ Completion: {conv().usage?.completionTokens.toLocaleString() || 0}
63
+ </span>
64
+ <span>•</span>
65
+ <span class={styles().conversationDetails.usageBold}>
66
+ Total: {conv().usage?.totalTokens.toLocaleString() || 0}
67
+ </span>
68
+ </div>
69
+ </Show>
70
+ {/* Tools list - always visible */}
71
+ <Show when={toolNames().length > 0}>
72
+ <div class={styles().conversationDetails.toolsRow}>
73
+ <span class={styles().conversationDetails.toolsLabel}>🔧</span>
74
+ <div class={styles().conversationDetails.toolsList}>
75
+ <For each={toolNames()}>
76
+ {(toolName) => (
77
+ <span class={styles().conversationDetails.toolBadge}>
78
+ {toolName}
79
+ </span>
80
+ )}
81
+ </For>
82
+ </div>
83
+ </div>
84
+ </Show>
85
+ {/* Options - always visible in compact form */}
86
+ <Show when={options() && Object.keys(options()!).length > 0}>
87
+ <div class={styles().conversationDetails.optionsRow}>
88
+ <span class={styles().conversationDetails.optionsLabel}>
89
+ ⚙️ Options:
90
+ </span>
91
+ <div class={styles().conversationDetails.optionsCompact}>
92
+ <For each={Object.entries(options()!)}>
93
+ {([key, value]) => (
94
+ <span class={styles().conversationDetails.optionBadge}>
95
+ {key}:{' '}
96
+ {typeof value === 'object'
97
+ ? JSON.stringify(value)
98
+ : String(value)}
99
+ </span>
100
+ )}
101
+ </For>
102
+ </div>
103
+ </div>
104
+ </Show>
105
+ {/* Provider Options - always visible in compact form */}
106
+ <Show
107
+ when={providerOptions() && Object.keys(providerOptions()!).length > 0}
108
+ >
109
+ <div class={styles().conversationDetails.optionsRow}>
110
+ <span class={styles().conversationDetails.optionsLabel}>
111
+ 🏷️ Provider:
112
+ </span>
113
+ <div class={styles().conversationDetails.optionsCompact}>
114
+ <For each={Object.entries(providerOptions()!)}>
115
+ {([key, value]) => (
116
+ <span class={styles().conversationDetails.optionBadge}>
117
+ {key}:{' '}
118
+ {typeof value === 'object'
119
+ ? JSON.stringify(value)
120
+ : String(value)}
121
+ </span>
122
+ )}
123
+ </For>
124
+ </div>
125
+ </div>
126
+ </Show>
127
+ </div>
128
+ </div>
129
+ )
130
+ }
@@ -0,0 +1,104 @@
1
+ import { Show } 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 = 'messages' | 'chunks' | 'embeddings' | 'summaries'
7
+
8
+ interface ConversationTabsProps {
9
+ conversation: Conversation
10
+ activeTab: TabType
11
+ onTabChange: (tab: TabType) => void
12
+ }
13
+
14
+ export const ConversationTabs: Component<ConversationTabsProps> = (props) => {
15
+ const styles = useStyles()
16
+ const conv = () => props.conversation
17
+
18
+ // Total raw chunks = sum of all chunkCounts
19
+ const totalRawChunks = () =>
20
+ conv().chunks.reduce((sum, c) => sum + (c.chunkCount || 1), 0)
21
+
22
+ const embeddingsCount = () => conv().embeddings?.length ?? 0
23
+ const summariesCount = () => conv().summaries?.length ?? 0
24
+
25
+ // Determine if we should show any chat-related tabs
26
+ // For server conversations, don't show messages tab - only chunks
27
+ const hasMessages = () =>
28
+ conv().type === 'client' && conv().messages.length > 0
29
+ const hasChunks = () => conv().chunks.length > 0 || conv().type === 'server'
30
+ const hasEmbeddings = () => conv().hasEmbedding || embeddingsCount() > 0
31
+ const hasSummaries = () => conv().hasSummarize || summariesCount() > 0
32
+
33
+ // Count how many tabs would be visible
34
+ const visibleTabCount = () => {
35
+ let count = 0
36
+ if (hasMessages()) count++
37
+ if (hasChunks() && conv().type === 'server') count++
38
+ if (hasEmbeddings()) count++
39
+ if (hasSummaries()) count++
40
+ return count
41
+ }
42
+
43
+ // Don't render tabs if only one tab would be visible
44
+ if (visibleTabCount() <= 1) {
45
+ return null
46
+ }
47
+
48
+ return (
49
+ <div class={styles().conversationDetails.tabsContainer}>
50
+ {/* Show messages tab for client conversations or when there are messages */}
51
+ <Show when={hasMessages()}>
52
+ <button
53
+ class={`${styles().actionButton} ${
54
+ props.activeTab === 'messages'
55
+ ? styles().conversationDetails.tabButtonActive
56
+ : ''
57
+ }`}
58
+ onClick={() => props.onTabChange('messages')}
59
+ >
60
+ 💬 Messages ({conv().messages.length})
61
+ </button>
62
+ </Show>
63
+ {/* Show chunks tab for server conversations or when there are chunks */}
64
+ <Show when={hasChunks() && conv().type === 'server'}>
65
+ <button
66
+ class={`${styles().actionButton} ${
67
+ props.activeTab === 'chunks'
68
+ ? styles().conversationDetails.tabButtonActive
69
+ : ''
70
+ }`}
71
+ onClick={() => props.onTabChange('chunks')}
72
+ >
73
+ 📦 Chunks ({totalRawChunks()})
74
+ </button>
75
+ </Show>
76
+ {/* Show embeddings tab if there are embedding operations */}
77
+ <Show when={hasEmbeddings()}>
78
+ <button
79
+ class={`${styles().actionButton} ${
80
+ props.activeTab === 'embeddings'
81
+ ? styles().conversationDetails.tabButtonActive
82
+ : ''
83
+ }`}
84
+ onClick={() => props.onTabChange('embeddings')}
85
+ >
86
+ 🔢 Embeddings ({embeddingsCount()})
87
+ </button>
88
+ </Show>
89
+ {/* Show summaries tab if there are summarize operations */}
90
+ <Show when={hasSummaries()}>
91
+ <button
92
+ class={`${styles().actionButton} ${
93
+ props.activeTab === 'summaries'
94
+ ? styles().conversationDetails.tabButtonActive
95
+ : ''
96
+ }`}
97
+ onClick={() => props.onTabChange('summaries')}
98
+ >
99
+ 📝 Summaries ({summariesCount()})
100
+ </button>
101
+ </Show>
102
+ </div>
103
+ )
104
+ }