@adea-ai/ui 0.67.1 → 0.68.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.
- package/README.md +58 -0
- package/dist/NOTICE +34 -0
- package/dist/components/conversation/index.d.ts +1 -0
- package/dist/components/conversation/index.d.ts.map +1 -1
- package/dist/components/conversation/index.js +2 -1
- package/dist/components/conversation/transcript-composition.d.ts +66 -0
- package/dist/components/conversation/transcript-composition.d.ts.map +1 -0
- package/dist/components/conversation/transcript-composition.js +395 -0
- package/dist/components/conversation/transcript-composition.js.map +1 -0
- package/dist/components/conversation/transcript-folding.d.ts +22 -0
- package/dist/components/conversation/transcript-folding.d.ts.map +1 -0
- package/dist/components/conversation/transcript-folding.js +43 -0
- package/dist/components/conversation/transcript-folding.js.map +1 -0
- package/dist/components/conversation/transcript-grouping.d.ts +54 -0
- package/dist/components/conversation/transcript-grouping.d.ts.map +1 -0
- package/dist/components/conversation/transcript-grouping.js +155 -0
- package/dist/components/conversation/transcript-grouping.js.map +1 -0
- package/dist/index.js +2 -1
- package/dist/r/conversation.json +15 -0
- package/dist/r/registry.json +15 -0
- package/dist/r/src/components/conversation/index.ts +17 -0
- package/dist/r/src/components/conversation/transcript-composition.tsx +555 -0
- package/dist/r/src/components/conversation/transcript-folding.ts +61 -0
- package/dist/r/src/components/conversation/transcript-grouping.ts +204 -0
- package/package.json +8 -1
- package/registry.json +15 -0
- package/src/components/conversation/index.ts +17 -0
- package/src/components/conversation/transcript-composition.tsx +555 -0
- package/src/components/conversation/transcript-folding.ts +61 -0
- package/src/components/conversation/transcript-grouping.ts +204 -0
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
import type { Accessor, Component, ComponentProps } from 'solid-js'
|
|
2
|
+
import {
|
|
3
|
+
For,
|
|
4
|
+
Show,
|
|
5
|
+
createComponent,
|
|
6
|
+
createMemo,
|
|
7
|
+
createSignal,
|
|
8
|
+
createUniqueId,
|
|
9
|
+
splitProps,
|
|
10
|
+
} from 'solid-js'
|
|
11
|
+
import { ChevronDown } from 'lucide-solid'
|
|
12
|
+
import { cn } from '../../lib/utils'
|
|
13
|
+
import { Button } from '../ui/button/button'
|
|
14
|
+
import {
|
|
15
|
+
applyTranscriptRunningState,
|
|
16
|
+
createTranscriptGrouper,
|
|
17
|
+
type TranscriptDisplayItem,
|
|
18
|
+
type TranscriptRow,
|
|
19
|
+
type TranscriptTurnItem,
|
|
20
|
+
} from './transcript-grouping'
|
|
21
|
+
import {
|
|
22
|
+
planTranscriptFold,
|
|
23
|
+
type TranscriptFoldMode,
|
|
24
|
+
type TranscriptMounting,
|
|
25
|
+
} from './transcript-folding'
|
|
26
|
+
|
|
27
|
+
export type TranscriptFoldState = 'default' | 'collapsed' | 'expanded'
|
|
28
|
+
export type TranscriptDisplayKind = 'single' | 'group' | 'turn'
|
|
29
|
+
|
|
30
|
+
export type {
|
|
31
|
+
GroupedTranscript,
|
|
32
|
+
TranscriptDisplayItem,
|
|
33
|
+
TranscriptRow,
|
|
34
|
+
TranscriptTurnItem,
|
|
35
|
+
} from './transcript-grouping'
|
|
36
|
+
export type {
|
|
37
|
+
TranscriptFoldMode,
|
|
38
|
+
TranscriptFoldPlan,
|
|
39
|
+
TranscriptMounting,
|
|
40
|
+
} from './transcript-folding'
|
|
41
|
+
|
|
42
|
+
export type TranscriptCompositionFold = Readonly<{
|
|
43
|
+
/** Stable host persistence key. Use with `transcriptDisclosureKey` to seed a store. */
|
|
44
|
+
key: string
|
|
45
|
+
mode: TranscriptFoldMode
|
|
46
|
+
mounting: TranscriptMounting
|
|
47
|
+
/** Distinct host-classified request IDs in this contiguous fold segment. */
|
|
48
|
+
callCount: number
|
|
49
|
+
}>
|
|
50
|
+
|
|
51
|
+
export type TranscriptCompositionRowContext = Readonly<{
|
|
52
|
+
/** Original index in the caller's row array, including rows hidden by the host. */
|
|
53
|
+
index: number
|
|
54
|
+
/** The row's current position in the generic display composition. */
|
|
55
|
+
displayKind: TranscriptDisplayKind
|
|
56
|
+
group?: Readonly<{ index: number; size: number }>
|
|
57
|
+
turn?: Readonly<{
|
|
58
|
+
/** Derived from the first stable row ID; presentation bookkeeping only. */
|
|
59
|
+
id: string
|
|
60
|
+
/** A presentation state, never proof of successful runtime completion. */
|
|
61
|
+
complete: boolean
|
|
62
|
+
interim: boolean
|
|
63
|
+
}>
|
|
64
|
+
fold?: TranscriptCompositionFold
|
|
65
|
+
}>
|
|
66
|
+
|
|
67
|
+
export type TranscriptCompositionRendererProps<T> = Readonly<{
|
|
68
|
+
/** Reactive Solid prop; a row component keeps its DOM owner stable as values update. */
|
|
69
|
+
row: TranscriptRow<T>
|
|
70
|
+
context: TranscriptCompositionRowContext
|
|
71
|
+
}>
|
|
72
|
+
|
|
73
|
+
export type TranscriptCompositionProps<T> = Omit<ComponentProps<'div'>, 'children'> & {
|
|
74
|
+
/** Ordered, already redacted rows with host-supplied semantic classifications. */
|
|
75
|
+
rows: readonly TranscriptRow<T>[]
|
|
76
|
+
/** Render one host row. Permission, session, event and protocol authority stay outside this component. */
|
|
77
|
+
renderRow: Component<TranscriptCompositionRendererProps<T>>
|
|
78
|
+
/** The host's current run state. Live rows stay mounted and expanded until settled. */
|
|
79
|
+
running?: boolean
|
|
80
|
+
/** Reset scope for row identity and disclosure. It is not inferred from display content. */
|
|
81
|
+
resetKey?: string | number | null
|
|
82
|
+
/** Default settled fold policy. `tools` keeps prose visible; `prose` retains a proven conclusion. */
|
|
83
|
+
foldMode?: TranscriptFoldMode
|
|
84
|
+
/** Global command state. `collapsed` keeps hidden content mounted; prose folds only before a host-marked conclusion. */
|
|
85
|
+
foldState?: TranscriptFoldState
|
|
86
|
+
onFoldStateChange?: (state: TranscriptFoldState) => void
|
|
87
|
+
/** Optional controlled disclosure map keyed by `transcriptDisclosureKey`. */
|
|
88
|
+
disclosure?: ReadonlyMap<string, boolean>
|
|
89
|
+
onDisclosureChange?: (key: string, expanded: boolean) => void
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
type TurnPresentation = NonNullable<TranscriptCompositionRowContext['turn']>
|
|
93
|
+
type GroupPresentation = NonNullable<TranscriptCompositionRowContext['group']>
|
|
94
|
+
|
|
95
|
+
type FoldGroup<T> = Readonly<{
|
|
96
|
+
key: string
|
|
97
|
+
mode: TranscriptFoldMode
|
|
98
|
+
callCount: number
|
|
99
|
+
rows: readonly TranscriptRow<T>[]
|
|
100
|
+
}>
|
|
101
|
+
|
|
102
|
+
type RowRecord<T> = {
|
|
103
|
+
row: TranscriptRow<T>
|
|
104
|
+
index: number
|
|
105
|
+
displayKind: TranscriptDisplayKind
|
|
106
|
+
group?: GroupPresentation
|
|
107
|
+
turn?: TurnPresentation
|
|
108
|
+
fold?: TranscriptCompositionFold
|
|
109
|
+
foldControl?: FoldGroup<T>
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
type TranscriptLayout<T> = Readonly<{
|
|
113
|
+
rows: readonly RowRecord<T>[]
|
|
114
|
+
controls: readonly FoldGroup<T>[]
|
|
115
|
+
}>
|
|
116
|
+
|
|
117
|
+
type RowHandle<T> = {
|
|
118
|
+
key: string
|
|
119
|
+
get: Accessor<RowRecord<T>>
|
|
120
|
+
set: (next: RowRecord<T>) => void
|
|
121
|
+
last: RowRecord<T>
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
type DisclosureSignal = Readonly<{
|
|
125
|
+
get: Accessor<boolean | undefined>
|
|
126
|
+
set: (next: boolean | undefined) => void
|
|
127
|
+
}>
|
|
128
|
+
|
|
129
|
+
const encodeId = (value: string): string => encodeURIComponent(value)
|
|
130
|
+
|
|
131
|
+
/** Stable, transcript-scoped key for host-owned disclosure restoration. */
|
|
132
|
+
export function transcriptDisclosureKey(
|
|
133
|
+
resetKey: string | number | null | undefined,
|
|
134
|
+
mode: TranscriptFoldMode,
|
|
135
|
+
firstRowId: string
|
|
136
|
+
): string {
|
|
137
|
+
return `transcript:${encodeId(String(resetKey ?? ''))}:${mode}:${encodeId(firstRowId)}`
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function panelId(instanceId: string, resetKey: string, rowId: string): string {
|
|
141
|
+
return `${instanceId}-row-${encodeId(resetKey)}-${encodeId(rowId)}`
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function buttonId(instanceId: string, resetKey: string, segmentKey: string): string {
|
|
145
|
+
return `${instanceId}-fold-${encodeId(resetKey)}-${encodeId(segmentKey)}`
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function sameGroup(a?: GroupPresentation, b?: GroupPresentation): boolean {
|
|
149
|
+
return a === b || (!!a && !!b && a.index === b.index && a.size === b.size)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function sameTurn(a?: TurnPresentation, b?: TurnPresentation): boolean {
|
|
153
|
+
return (
|
|
154
|
+
a === b || (!!a && !!b && a.id === b.id && a.complete === b.complete && a.interim === b.interim)
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function sameFold(a?: TranscriptCompositionFold, b?: TranscriptCompositionFold): boolean {
|
|
159
|
+
return (
|
|
160
|
+
a === b ||
|
|
161
|
+
(!!a &&
|
|
162
|
+
!!b &&
|
|
163
|
+
a.key === b.key &&
|
|
164
|
+
a.mode === b.mode &&
|
|
165
|
+
a.mounting === b.mounting &&
|
|
166
|
+
a.callCount === b.callCount)
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function sameFoldGroup<T>(a?: FoldGroup<T>, b?: FoldGroup<T>): boolean {
|
|
171
|
+
return (
|
|
172
|
+
a === b ||
|
|
173
|
+
(!!a &&
|
|
174
|
+
!!b &&
|
|
175
|
+
a.key === b.key &&
|
|
176
|
+
a.callCount === b.callCount &&
|
|
177
|
+
a.rows.length === b.rows.length &&
|
|
178
|
+
a.rows.every((row, index) => row === b.rows[index]))
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function sameRecord<T>(a: RowRecord<T>, b: RowRecord<T>): boolean {
|
|
183
|
+
return (
|
|
184
|
+
a.row === b.row &&
|
|
185
|
+
a.index === b.index &&
|
|
186
|
+
a.displayKind === b.displayKind &&
|
|
187
|
+
sameGroup(a.group, b.group) &&
|
|
188
|
+
sameTurn(a.turn, b.turn) &&
|
|
189
|
+
sameFold(a.fold, b.fold) &&
|
|
190
|
+
sameFoldGroup(a.foldControl, b.foldControl)
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function firstRow<T>(item: TranscriptTurnItem<T>): TranscriptRow<T> | undefined {
|
|
195
|
+
return item.kind === 'single' ? item.row : item.rows[0]
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function appendTurnItem<T>(
|
|
199
|
+
item: TranscriptTurnItem<T>,
|
|
200
|
+
displayKind: 'single' | 'group' | 'turn',
|
|
201
|
+
turn: TurnPresentation | undefined,
|
|
202
|
+
indices: ReadonlyMap<TranscriptRow<T>, number>,
|
|
203
|
+
into: RowRecord<T>[]
|
|
204
|
+
): void {
|
|
205
|
+
if (item.kind === 'single') {
|
|
206
|
+
into.push({
|
|
207
|
+
row: item.row,
|
|
208
|
+
index: indices.get(item.row) ?? item.index,
|
|
209
|
+
displayKind,
|
|
210
|
+
turn,
|
|
211
|
+
})
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
item.rows.forEach((row, index) => {
|
|
215
|
+
into.push({
|
|
216
|
+
row,
|
|
217
|
+
index: indices.get(row) ?? item.startIndex + index,
|
|
218
|
+
displayKind,
|
|
219
|
+
group: { index, size: item.rows.length },
|
|
220
|
+
turn,
|
|
221
|
+
})
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function appendDisplayItem<T>(
|
|
226
|
+
item: TranscriptDisplayItem<T>,
|
|
227
|
+
indices: ReadonlyMap<TranscriptRow<T>, number>,
|
|
228
|
+
into: RowRecord<T>[]
|
|
229
|
+
): void {
|
|
230
|
+
if (item.kind !== 'turn') {
|
|
231
|
+
appendTurnItem(item, item.kind, undefined, indices, into)
|
|
232
|
+
return
|
|
233
|
+
}
|
|
234
|
+
const turnId = item.items.map(firstRow).find((row) => row)?.id ?? 'empty-turn'
|
|
235
|
+
const turn: TurnPresentation = {
|
|
236
|
+
id: turnId,
|
|
237
|
+
complete: item.complete,
|
|
238
|
+
interim: !!item.interim,
|
|
239
|
+
}
|
|
240
|
+
for (const child of item.items) appendTurnItem(child, 'turn', turn, indices, into)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function buildDisclosures<T>(
|
|
244
|
+
records: readonly RowRecord<T>[],
|
|
245
|
+
defaultMode: TranscriptFoldMode,
|
|
246
|
+
foldState: TranscriptFoldState,
|
|
247
|
+
resetKey: string
|
|
248
|
+
): FoldGroup<T>[] {
|
|
249
|
+
const controls: FoldGroup<T>[] = []
|
|
250
|
+
let scope: RowRecord<T>[] = []
|
|
251
|
+
const flushScope = () => {
|
|
252
|
+
if (!scope.length) return
|
|
253
|
+
const rows = scope.map((record) => record.row)
|
|
254
|
+
const hasInterimTurn = scope.some((record) => record.turn?.interim)
|
|
255
|
+
const hasConclusion = scope.some((record) => record.row.conclusion)
|
|
256
|
+
const mode: TranscriptFoldMode = hasInterimTurn
|
|
257
|
+
? 'interim'
|
|
258
|
+
: foldState === 'default'
|
|
259
|
+
? defaultMode
|
|
260
|
+
: foldState === 'collapsed'
|
|
261
|
+
? hasConclusion
|
|
262
|
+
? 'prose'
|
|
263
|
+
: 'tools'
|
|
264
|
+
: 'interim'
|
|
265
|
+
const plan = planTranscriptFold(rows, { mode, settled: true })
|
|
266
|
+
const foldable = plan.entries.flatMap((entry, index) =>
|
|
267
|
+
entry.mounting === 'always' ? [] : [{ record: scope[index]!, mounting: entry.mounting }]
|
|
268
|
+
)
|
|
269
|
+
if (foldable.length) {
|
|
270
|
+
const anchorId = scope[0]?.row.id ?? foldable[0]!.record.row.id
|
|
271
|
+
const control: FoldGroup<T> = {
|
|
272
|
+
key: transcriptDisclosureKey(resetKey, mode, anchorId),
|
|
273
|
+
mode,
|
|
274
|
+
callCount: plan.callCount,
|
|
275
|
+
rows: foldable.map(({ record }) => record.row),
|
|
276
|
+
}
|
|
277
|
+
controls.push(control)
|
|
278
|
+
foldable.forEach(({ record, mounting }, index) => {
|
|
279
|
+
record.fold = {
|
|
280
|
+
key: control.key,
|
|
281
|
+
mode: control.mode,
|
|
282
|
+
mounting: foldState === 'collapsed' ? 'hide-when-collapsed' : mounting,
|
|
283
|
+
callCount: control.callCount,
|
|
284
|
+
}
|
|
285
|
+
if (index === 0) record.foldControl = control
|
|
286
|
+
})
|
|
287
|
+
}
|
|
288
|
+
scope = []
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
for (const record of records) {
|
|
292
|
+
if (record.row.opensTurn && scope.length) flushScope()
|
|
293
|
+
scope.push(record)
|
|
294
|
+
}
|
|
295
|
+
flushScope()
|
|
296
|
+
return controls
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function foldLabel(mode: TranscriptFoldMode, count: number, expanded: boolean): string {
|
|
300
|
+
const subject =
|
|
301
|
+
mode === 'tools'
|
|
302
|
+
? count === 1
|
|
303
|
+
? '1 tool call'
|
|
304
|
+
: count > 1
|
|
305
|
+
? `${count} tool calls`
|
|
306
|
+
: 'tool details'
|
|
307
|
+
: mode === 'prose'
|
|
308
|
+
? 'earlier activity'
|
|
309
|
+
: 'interim activity'
|
|
310
|
+
return `${expanded ? 'Hide' : 'Show'} ${subject}`
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function TranscriptRowEntry<T>(props: {
|
|
314
|
+
record: Accessor<RowRecord<T>>
|
|
315
|
+
renderRow: Component<TranscriptCompositionRendererProps<T>>
|
|
316
|
+
prefix: string
|
|
317
|
+
resetKey: Accessor<string>
|
|
318
|
+
expanded: (key: string) => Accessor<boolean>
|
|
319
|
+
running: Accessor<boolean>
|
|
320
|
+
foldState: Accessor<TranscriptFoldState>
|
|
321
|
+
onToggle: (key: string, expanded: boolean) => void
|
|
322
|
+
}) {
|
|
323
|
+
const current = props.record
|
|
324
|
+
const id = () => panelId(props.prefix, props.resetKey(), current().row.id)
|
|
325
|
+
const foldControl = () => current().foldControl
|
|
326
|
+
const disclosureKey = () => current().fold?.key
|
|
327
|
+
const expanded = () => {
|
|
328
|
+
const key = disclosureKey()
|
|
329
|
+
return key ? props.expanded(key)() : true
|
|
330
|
+
}
|
|
331
|
+
const collapsed = () => !!current().fold && !expanded()
|
|
332
|
+
const disabled = () => props.running() || props.foldState() !== 'default'
|
|
333
|
+
const context = (): TranscriptCompositionRowContext => ({
|
|
334
|
+
index: current().index,
|
|
335
|
+
displayKind: current().displayKind,
|
|
336
|
+
group: current().group,
|
|
337
|
+
turn: current().turn,
|
|
338
|
+
fold: current().fold,
|
|
339
|
+
})
|
|
340
|
+
const groupIndex = () => current().group?.index
|
|
341
|
+
const groupSize = () => current().group?.size
|
|
342
|
+
const renderable = () => current().fold?.mounting !== 'unmount-when-collapsed' || !collapsed()
|
|
343
|
+
const renderHostRow = () =>
|
|
344
|
+
createComponent(props.renderRow, {
|
|
345
|
+
get row() {
|
|
346
|
+
return current().row
|
|
347
|
+
},
|
|
348
|
+
get context() {
|
|
349
|
+
return context()
|
|
350
|
+
},
|
|
351
|
+
})
|
|
352
|
+
const controlLabel = () => {
|
|
353
|
+
const control = foldControl()
|
|
354
|
+
return control ? foldLabel(control.mode, control.callCount, expanded()) : ''
|
|
355
|
+
}
|
|
356
|
+
const controlRows = () =>
|
|
357
|
+
foldControl()
|
|
358
|
+
?.rows.map((row) => panelId(props.prefix, props.resetKey(), row.id))
|
|
359
|
+
.join(' ')
|
|
360
|
+
|
|
361
|
+
return (
|
|
362
|
+
<div
|
|
363
|
+
data-slot="transcript-row"
|
|
364
|
+
data-display-kind={current().displayKind}
|
|
365
|
+
data-group-index={groupIndex()}
|
|
366
|
+
data-group-size={groupSize()}
|
|
367
|
+
class="flex min-w-0 flex-col gap-1"
|
|
368
|
+
>
|
|
369
|
+
<Show when={foldControl()}>
|
|
370
|
+
{(control) => (
|
|
371
|
+
<Button
|
|
372
|
+
id={buttonId(props.prefix, props.resetKey(), control().key)}
|
|
373
|
+
type="button"
|
|
374
|
+
variant="ghost"
|
|
375
|
+
size="sm"
|
|
376
|
+
class="w-full justify-start"
|
|
377
|
+
aria-label={controlLabel()}
|
|
378
|
+
aria-controls={controlRows()}
|
|
379
|
+
aria-expanded={expanded()}
|
|
380
|
+
aria-disabled={disabled() || undefined}
|
|
381
|
+
onClick={() => props.onToggle(control().key, !expanded())}
|
|
382
|
+
>
|
|
383
|
+
<ChevronDown aria-hidden="true" class="size-4 shrink-0" />
|
|
384
|
+
<span>{controlLabel()}</span>
|
|
385
|
+
</Button>
|
|
386
|
+
)}
|
|
387
|
+
</Show>
|
|
388
|
+
<div
|
|
389
|
+
id={id()}
|
|
390
|
+
data-slot="transcript-row-content"
|
|
391
|
+
hidden={collapsed()}
|
|
392
|
+
aria-hidden={collapsed() || undefined}
|
|
393
|
+
>
|
|
394
|
+
<Show when={renderable() ? current() : undefined}>
|
|
395
|
+
{(record) => {
|
|
396
|
+
record()
|
|
397
|
+
return renderHostRow()
|
|
398
|
+
}}
|
|
399
|
+
</Show>
|
|
400
|
+
</div>
|
|
401
|
+
</div>
|
|
402
|
+
)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Compose host-classified rows into stable turns, contiguous groups and disclosures.
|
|
407
|
+
* Row render owners are keyed by `id` in a single list, so loose rows keep their DOM
|
|
408
|
+
* owners when the grouping threshold promotes them into a turn. The caller owns row
|
|
409
|
+
* identity, protocol meaning, redaction, permissions and durable conversation state.
|
|
410
|
+
*/
|
|
411
|
+
export function TranscriptComposition<T>(props: TranscriptCompositionProps<T>) {
|
|
412
|
+
const [local, rest] = splitProps(props, [
|
|
413
|
+
'class',
|
|
414
|
+
'rows',
|
|
415
|
+
'renderRow',
|
|
416
|
+
'running',
|
|
417
|
+
'resetKey',
|
|
418
|
+
'foldMode',
|
|
419
|
+
'foldState',
|
|
420
|
+
'onFoldStateChange',
|
|
421
|
+
'disclosure',
|
|
422
|
+
'onDisclosureChange',
|
|
423
|
+
])
|
|
424
|
+
const instanceId = createUniqueId()
|
|
425
|
+
const grouper = createTranscriptGrouper<T>()
|
|
426
|
+
const [internalFoldState, setInternalFoldState] = createSignal<TranscriptFoldState>('default')
|
|
427
|
+
const rowHandles = new Map<string, RowHandle<T>>()
|
|
428
|
+
const disclosureSignals = new Map<string, DisclosureSignal>()
|
|
429
|
+
let lastResetKey = String(local.resetKey ?? '')
|
|
430
|
+
|
|
431
|
+
const resetKey = () => String(local.resetKey ?? '')
|
|
432
|
+
const foldState = () => local.foldState ?? internalFoldState()
|
|
433
|
+
const running = () => !!local.running
|
|
434
|
+
const localDisclosure = (key: string): DisclosureSignal => {
|
|
435
|
+
let signal = disclosureSignals.get(key)
|
|
436
|
+
if (!signal) {
|
|
437
|
+
const [get, set] = createSignal<boolean | undefined>(undefined)
|
|
438
|
+
signal = { get, set }
|
|
439
|
+
disclosureSignals.set(key, signal)
|
|
440
|
+
}
|
|
441
|
+
return signal
|
|
442
|
+
}
|
|
443
|
+
const disclosureExpanded = (key: string): Accessor<boolean> => {
|
|
444
|
+
const signal = localDisclosure(key)
|
|
445
|
+
return () => {
|
|
446
|
+
if (running()) return true
|
|
447
|
+
if (foldState() === 'collapsed') return false
|
|
448
|
+
if (foldState() === 'expanded') return true
|
|
449
|
+
return local.disclosure?.get(key) ?? signal.get() ?? false
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
const grouped = createMemo(() => grouper(local.rows))
|
|
453
|
+
const layout = createMemo<TranscriptLayout<T>>(() => {
|
|
454
|
+
const identity = resetKey()
|
|
455
|
+
if (identity !== lastResetKey) {
|
|
456
|
+
rowHandles.clear()
|
|
457
|
+
disclosureSignals.clear()
|
|
458
|
+
lastResetKey = identity
|
|
459
|
+
}
|
|
460
|
+
const indexByRow = new Map(local.rows.map((row, index) => [row, index]))
|
|
461
|
+
const display = applyTranscriptRunningState(grouped(), running())
|
|
462
|
+
const records: RowRecord<T>[] = []
|
|
463
|
+
display.forEach((item) => appendDisplayItem(item, indexByRow, records))
|
|
464
|
+
const controls = buildDisclosures(records, local.foldMode ?? 'tools', foldState(), identity)
|
|
465
|
+
return { rows: records, controls }
|
|
466
|
+
})
|
|
467
|
+
const handles = createMemo(() => {
|
|
468
|
+
const live = new Set<string>()
|
|
469
|
+
const identity = resetKey()
|
|
470
|
+
const result = layout().rows.map((record) => {
|
|
471
|
+
const key = `${identity}\u001f${record.row.id}`
|
|
472
|
+
live.add(key)
|
|
473
|
+
let handle = rowHandles.get(key)
|
|
474
|
+
if (!handle) {
|
|
475
|
+
const [get, set] = createSignal(record)
|
|
476
|
+
handle = { key, get, set, last: record }
|
|
477
|
+
rowHandles.set(key, handle)
|
|
478
|
+
} else if (!sameRecord(handle.last, record)) {
|
|
479
|
+
handle.last = record
|
|
480
|
+
handle.set(record)
|
|
481
|
+
}
|
|
482
|
+
return handle
|
|
483
|
+
})
|
|
484
|
+
for (const key of rowHandles.keys()) if (!live.has(key)) rowHandles.delete(key)
|
|
485
|
+
return result
|
|
486
|
+
})
|
|
487
|
+
const allControlIds = () =>
|
|
488
|
+
layout()
|
|
489
|
+
.controls.flatMap((control) =>
|
|
490
|
+
control.rows.map((row) => panelId(instanceId, resetKey(), row.id))
|
|
491
|
+
)
|
|
492
|
+
.join(' ')
|
|
493
|
+
const updateFoldState = () => {
|
|
494
|
+
if (running()) return
|
|
495
|
+
const current = foldState()
|
|
496
|
+
const next: TranscriptFoldState =
|
|
497
|
+
current === 'default' ? 'collapsed' : current === 'collapsed' ? 'expanded' : 'default'
|
|
498
|
+
if (local.foldState === undefined) setInternalFoldState(next)
|
|
499
|
+
local.onFoldStateChange?.(next)
|
|
500
|
+
}
|
|
501
|
+
const updateDisclosure = (key: string, expanded: boolean) => {
|
|
502
|
+
if (running() || foldState() !== 'default') return
|
|
503
|
+
if (local.disclosure) {
|
|
504
|
+
local.onDisclosureChange?.(key, expanded)
|
|
505
|
+
return
|
|
506
|
+
}
|
|
507
|
+
localDisclosure(key).set(expanded)
|
|
508
|
+
local.onDisclosureChange?.(key, expanded)
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return (
|
|
512
|
+
<div
|
|
513
|
+
data-slot="transcript-composition"
|
|
514
|
+
data-fold-state={foldState()}
|
|
515
|
+
class={cn('flex w-full min-w-0 flex-col gap-1', local.class)}
|
|
516
|
+
{...rest}
|
|
517
|
+
>
|
|
518
|
+
<Show when={local.onFoldStateChange}>
|
|
519
|
+
<div class="flex justify-end">
|
|
520
|
+
<Button
|
|
521
|
+
id={`${instanceId}-fold-all`}
|
|
522
|
+
type="button"
|
|
523
|
+
variant="ghost"
|
|
524
|
+
size="sm"
|
|
525
|
+
aria-controls={allControlIds() || undefined}
|
|
526
|
+
aria-disabled={running() || undefined}
|
|
527
|
+
onClick={updateFoldState}
|
|
528
|
+
>
|
|
529
|
+
{foldState() === 'default'
|
|
530
|
+
? 'Collapse all activity'
|
|
531
|
+
: foldState() === 'collapsed'
|
|
532
|
+
? 'Expand all activity'
|
|
533
|
+
: 'Restore default folding'}
|
|
534
|
+
</Button>
|
|
535
|
+
</div>
|
|
536
|
+
</Show>
|
|
537
|
+
<div data-slot="transcript-rows" class="flex min-w-0 flex-col gap-1">
|
|
538
|
+
<For each={handles()}>
|
|
539
|
+
{(handle) => (
|
|
540
|
+
<TranscriptRowEntry
|
|
541
|
+
record={handle.get}
|
|
542
|
+
renderRow={local.renderRow}
|
|
543
|
+
prefix={instanceId}
|
|
544
|
+
resetKey={resetKey}
|
|
545
|
+
expanded={disclosureExpanded}
|
|
546
|
+
running={running}
|
|
547
|
+
foldState={foldState}
|
|
548
|
+
onToggle={updateDisclosure}
|
|
549
|
+
/>
|
|
550
|
+
)}
|
|
551
|
+
</For>
|
|
552
|
+
</div>
|
|
553
|
+
</div>
|
|
554
|
+
)
|
|
555
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
3
|
+
* Licensed under the Apache License, Version 2.0.
|
|
4
|
+
*
|
|
5
|
+
* Adapted from KiroCrew TurnBlock.tsx at
|
|
6
|
+
* 283e136c0f902e965a535a7c9548c57c7504fed0. Host classifications replace
|
|
7
|
+
* protocol parsing; approval capability and payload ownership stay outside UI.
|
|
8
|
+
*/
|
|
9
|
+
import type { TranscriptRow } from './transcript-grouping'
|
|
10
|
+
|
|
11
|
+
export type TranscriptFoldMode = 'tools' | 'prose' | 'interim'
|
|
12
|
+
export type TranscriptMounting = 'always' | 'unmount-when-collapsed' | 'hide-when-collapsed'
|
|
13
|
+
export type TranscriptFoldPlan<T> = Readonly<{
|
|
14
|
+
entries: readonly Readonly<{ row: TranscriptRow<T>; mounting: TranscriptMounting }>[]
|
|
15
|
+
callCount: number
|
|
16
|
+
foldableCount: number
|
|
17
|
+
}>
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Ordered mount policy, shared by tool and prose disclosure. Unknown rows,
|
|
21
|
+
* actionable payloads and host-marked conclusions remain visible in every mode.
|
|
22
|
+
* A missing answer boundary keeps prose visible rather than implying that an
|
|
23
|
+
* answer appears elsewhere.
|
|
24
|
+
*/
|
|
25
|
+
export function planTranscriptFold<T>(
|
|
26
|
+
rows: readonly TranscriptRow<T>[],
|
|
27
|
+
options: Readonly<{ mode: TranscriptFoldMode; settled: boolean }>
|
|
28
|
+
): TranscriptFoldPlan<T> {
|
|
29
|
+
let conclusion = -1
|
|
30
|
+
for (let index = 0; index < rows.length; index++) if (rows[index]?.conclusion) conclusion = index
|
|
31
|
+
const foldEligible = (row: TranscriptRow<T>) =>
|
|
32
|
+
!row.alwaysVisible && !row.conclusion && row.fold !== undefined
|
|
33
|
+
const requestIds = new Set<string>()
|
|
34
|
+
let callCount = 0
|
|
35
|
+
for (const row of rows) {
|
|
36
|
+
if (foldEligible(row) && row.fold === 'tool' && row.call?.phase === 'request') {
|
|
37
|
+
const id = row.call.id
|
|
38
|
+
if (id === undefined || !requestIds.has(id)) {
|
|
39
|
+
callCount++
|
|
40
|
+
if (id !== undefined) requestIds.add(id)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
let foldableCount = 0
|
|
45
|
+
const entries = rows.map((row, index) => {
|
|
46
|
+
const eligible = foldEligible(row)
|
|
47
|
+
let mounting: TranscriptMounting = 'always'
|
|
48
|
+
if (options.settled && eligible) {
|
|
49
|
+
if (options.mode === 'tools' && callCount > 0 && row.fold === 'tool')
|
|
50
|
+
mounting = 'unmount-when-collapsed'
|
|
51
|
+
else if (
|
|
52
|
+
options.mode === 'interim' ||
|
|
53
|
+
(options.mode === 'prose' && conclusion >= 0 && index < conclusion)
|
|
54
|
+
)
|
|
55
|
+
mounting = 'hide-when-collapsed'
|
|
56
|
+
}
|
|
57
|
+
if (mounting !== 'always') foldableCount++
|
|
58
|
+
return { row, mounting }
|
|
59
|
+
})
|
|
60
|
+
return { entries, callCount, foldableCount }
|
|
61
|
+
}
|