@adecore/agents-react 0.41.1 → 0.43.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/dist/chat/actions.js +4 -0
- package/dist/chat/logic/lightbox.d.ts +31 -0
- package/dist/chat/logic/lightbox.js +61 -0
- package/dist/chat/logic/timeline.d.ts +1 -0
- package/dist/chat/logic/timeline.js +3 -0
- package/dist/chat/ui/AnsiOutput.d.ts +3 -2
- package/dist/chat/ui/AnsiOutput.js +26 -22
- package/dist/chat/ui/ImageView.d.ts +3 -0
- package/dist/chat/ui/ImageView.js +761 -437
- package/dist/chat/ui/intelligent-ui/renderers/content.js +4 -2
- package/dist/chat/ui/rows/MessageRows.js +10 -8
- package/dist/chat/ui/rows/Rows.js +42 -39
- package/dist/chat/ui/rows/WorkRows.d.ts +4 -2
- package/dist/chat/ui/rows/WorkRows.js +580 -211
- package/dist/chat/ui/rows/WorkflowRow.js +43 -41
- package/dist/host.d.ts +1 -0
- package/dist/locales/en/agent-chat.d.ts +9 -0
- package/dist/locales/en/agent-chat.js +11 -2
- package/dist/locales/nl/agent-chat.d.ts +9 -0
- package/dist/locales/nl/agent-chat.js +11 -2
- package/package.json +8 -8
- package/src/chat/actions.ts +1 -0
- package/src/chat/logic/lightbox.ts +88 -0
- package/src/chat/logic/timeline.ts +13 -2
- package/src/chat/ui/AnsiOutput.tsx +13 -2
- package/src/chat/ui/ImageView.tsx +255 -63
- package/src/chat/ui/intelligent-ui/renderers/content.tsx +4 -2
- package/src/chat/ui/rows/MessageRows.tsx +10 -8
- package/src/chat/ui/rows/Rows.tsx +9 -1
- package/src/chat/ui/rows/WorkRows.tsx +127 -27
- package/src/chat/ui/rows/WorkflowRow.tsx +1 -1
- package/src/host.ts +2 -0
- package/src/locales/en/agent-chat.ts +11 -2
- package/src/locales/nl/agent-chat.ts +11 -2
|
@@ -1,90 +1,226 @@
|
|
|
1
|
-
import { useRef, useState, type ReactNode } from 'react';
|
|
1
|
+
import { createContext, useContext, useId, useLayoutEffect, useRef, useState, useSyncExternalStore, type KeyboardEvent, type ReactNode } from 'react';
|
|
2
2
|
import { useTranslation } from 'react-i18next';
|
|
3
3
|
import clsx from 'clsx';
|
|
4
|
-
import { ImageOff, Minus, Plus, RotateCcw } from 'lucide-react';
|
|
4
|
+
import { ChevronLeft, ChevronRight, ImageOff, Minus, Plus, RotateCcw } from 'lucide-react';
|
|
5
5
|
import type { ResourceUrl } from '../../host';
|
|
6
6
|
import { ButtonGroup, CloseButton, Dialog, Icon, IconButton, Tooltip } from '@adecore/ui';
|
|
7
|
+
import { formatNumber } from '@adecore/ui/format';
|
|
8
|
+
import { BUTTON_STEP, clamped, MAX_SCALE, MIN_SCALE, START, stepped, wheeled, zoomed, type Bounds, type Point, type View } from '../logic/lightbox';
|
|
7
9
|
import { DisplayImage } from './DisplayImage';
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
interface LightboxImage {
|
|
12
|
+
readonly key: string;
|
|
13
|
+
readonly src: string;
|
|
14
|
+
readonly alt: string;
|
|
15
|
+
readonly element: HTMLElement | null;
|
|
16
|
+
}
|
|
14
17
|
|
|
15
|
-
interface
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
interface ImageGroupStore {
|
|
19
|
+
set(image: LightboxImage): void;
|
|
20
|
+
remove(key: string): void;
|
|
21
|
+
subscribe(listener: () => void): () => void;
|
|
22
|
+
snapshot(): readonly LightboxImage[];
|
|
19
23
|
}
|
|
20
24
|
|
|
21
|
-
|
|
25
|
+
/* The pictures of one group in the order they stand on the page, which is the order the arrows walk. */
|
|
26
|
+
function createImageGroupStore(): ImageGroupStore {
|
|
27
|
+
const images = new Map<string, LightboxImage>();
|
|
28
|
+
const listeners = new Set<() => void>();
|
|
29
|
+
let snapshot: readonly LightboxImage[] = [];
|
|
30
|
+
|
|
31
|
+
const changed = (): void => {
|
|
32
|
+
snapshot = [...images.values()].sort((a, b) => {
|
|
33
|
+
if (a.element === null || b.element === null) {
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return a.element.compareDocumentPosition(b.element) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
for (const listener of listeners) {
|
|
41
|
+
listener();
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
set(image) {
|
|
47
|
+
const current = images.get(image.key);
|
|
48
|
+
|
|
49
|
+
if (current?.src === image.src && current.alt === image.alt && current.element === image.element) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
images.set(image.key, image);
|
|
54
|
+
changed();
|
|
55
|
+
},
|
|
56
|
+
remove(key) {
|
|
57
|
+
if (images.delete(key)) {
|
|
58
|
+
changed();
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
subscribe(listener) {
|
|
62
|
+
listeners.add(listener);
|
|
63
|
+
|
|
64
|
+
return () => {
|
|
65
|
+
listeners.delete(listener);
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
snapshot: () => snapshot
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface ImageGroupValue {
|
|
73
|
+
readonly store: ImageGroupStore;
|
|
74
|
+
open(key: string): void;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const ImageGroupContext = createContext<ImageGroupValue | null>(null);
|
|
22
78
|
|
|
23
79
|
/*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* `x` and `y` are offsets from the middle of the frame, so a scale of 1 is always centered.
|
|
80
|
+
* Thumbnails inside open one lightbox together, where the arrow keys walk from one to the next.
|
|
81
|
+
* A thumbnail with actions of its own, or one its caller opens from a control, stays on its own.
|
|
27
82
|
*/
|
|
28
|
-
function
|
|
29
|
-
const
|
|
83
|
+
export function ImageGroup({ children }: { children: ReactNode }) {
|
|
84
|
+
const [store] = useState(createImageGroupStore);
|
|
85
|
+
const images = useSyncExternalStore(store.subscribe, store.snapshot, store.snapshot);
|
|
86
|
+
// The key stays after closing, so the picture that fades out is the one that was open.
|
|
87
|
+
const [shown, setShown] = useState<{ key: string | null; open: boolean }>({ key: null, open: false });
|
|
88
|
+
const index = images.findIndex((image) => image.key === shown.key);
|
|
30
89
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
90
|
+
return (
|
|
91
|
+
<ImageGroupContext value={{ store, open: (key) => setShown({ key, open: true }) }}>
|
|
92
|
+
{children}
|
|
93
|
+
{index !== -1 && (
|
|
94
|
+
<Lightbox
|
|
95
|
+
images={images}
|
|
96
|
+
index={index}
|
|
97
|
+
open={shown.open}
|
|
98
|
+
onOpenChange={(open) => setShown((current) => ({ ...current, open }))}
|
|
99
|
+
onIndexChange={(next) => setShown({ key: images[next]?.key ?? null, open: true })}
|
|
100
|
+
/>
|
|
101
|
+
)}
|
|
102
|
+
</ImageGroupContext>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
34
105
|
|
|
35
|
-
|
|
106
|
+
/* A press on an arrow or a zoom button is the button's, never the start of a pan or a zoom of the stage. */
|
|
107
|
+
function onControl(target: EventTarget): boolean {
|
|
108
|
+
return target instanceof Element && target.closest('button') !== null;
|
|
109
|
+
}
|
|
36
110
|
|
|
37
|
-
|
|
111
|
+
/* Where the pointer sits against the middle of the stage, which is what the transform counts from. */
|
|
112
|
+
function pointIn(stage: HTMLElement, event: { clientX: number; clientY: number }): Point {
|
|
113
|
+
const rect = stage.getBoundingClientRect();
|
|
114
|
+
|
|
115
|
+
return { x: event.clientX - rect.left - rect.width / 2, y: event.clientY - rect.top - rect.height / 2 };
|
|
38
116
|
}
|
|
39
117
|
|
|
40
|
-
|
|
118
|
+
function boundsOf(stage: HTMLElement): Bounds {
|
|
119
|
+
const picture = stage.querySelector('img');
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
image: { width: picture?.offsetWidth ?? 0, height: picture?.offsetHeight ?? 0 },
|
|
123
|
+
frame: { width: stage.clientWidth, height: stage.clientHeight }
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* Pictures large and on their own: a pinch zooms, two fingers pan, the arrow keys walk the group, Escape closes. */
|
|
41
128
|
function Lightbox({
|
|
42
|
-
|
|
43
|
-
|
|
129
|
+
images,
|
|
130
|
+
index,
|
|
44
131
|
open,
|
|
45
132
|
onOpenChange,
|
|
133
|
+
onIndexChange,
|
|
46
134
|
actions
|
|
47
135
|
}: {
|
|
48
|
-
|
|
49
|
-
|
|
136
|
+
images: readonly LightboxImage[];
|
|
137
|
+
index: number;
|
|
50
138
|
open: boolean;
|
|
51
139
|
onOpenChange(open: boolean): void;
|
|
140
|
+
onIndexChange?(index: number): void;
|
|
52
141
|
actions?: ReactNode;
|
|
53
142
|
}) {
|
|
54
143
|
const { t } = useTranslation('agent-chat');
|
|
55
144
|
const [view, setView] = useState<View>(START);
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
const title = <span className="min-w-0 truncate text-xs text-text">{name}</span>;
|
|
59
|
-
const frameRef = useRef<HTMLDivElement>(null);
|
|
145
|
+
const [viewed, setViewed] = useState(index);
|
|
146
|
+
const stageElementRef = useRef<HTMLDivElement | null>(null);
|
|
60
147
|
const dragRef = useRef<{ pointerId: number; x: number; y: number } | null>(null);
|
|
148
|
+
const movedRef = useRef(false);
|
|
149
|
+
const image = images[index];
|
|
150
|
+
const previous = stepped(index, images.length, -1);
|
|
151
|
+
const next = stepped(index, images.length, 1);
|
|
152
|
+
|
|
153
|
+
if (viewed !== index) {
|
|
154
|
+
setViewed(index);
|
|
155
|
+
setView(START);
|
|
156
|
+
}
|
|
61
157
|
|
|
62
158
|
if (!open && view !== START) {
|
|
63
159
|
setView(START);
|
|
64
160
|
}
|
|
65
161
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
162
|
+
if (image === undefined) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// The alt of an image an agent read is its path; the caption shows the file and keeps the path on a tooltip.
|
|
167
|
+
const name = image.alt.slice(image.alt.lastIndexOf('/') + 1);
|
|
168
|
+
const title = <span className="min-w-0 truncate text-xs text-text">{name}</span>;
|
|
169
|
+
|
|
170
|
+
// React listens to the wheel passively, and only an active listener keeps a pinch from zooming the whole page.
|
|
171
|
+
const stageRef = (stage: HTMLDivElement | null) => {
|
|
172
|
+
stageElementRef.current = stage;
|
|
69
173
|
|
|
70
|
-
if (
|
|
71
|
-
return
|
|
174
|
+
if (stage === null) {
|
|
175
|
+
return;
|
|
72
176
|
}
|
|
73
177
|
|
|
74
|
-
|
|
178
|
+
const onWheel = (event: WheelEvent): void => {
|
|
179
|
+
event.preventDefault();
|
|
180
|
+
const point = pointIn(stage, event);
|
|
181
|
+
const bounds = boundsOf(stage);
|
|
182
|
+
setView((current) => wheeled(current, event, point, bounds));
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
stage.addEventListener('wheel', onWheel, { passive: false });
|
|
186
|
+
|
|
187
|
+
return () => stage.removeEventListener('wheel', onWheel);
|
|
75
188
|
};
|
|
76
189
|
|
|
77
|
-
const zoomButton = (factor: number): void =>
|
|
190
|
+
const zoomButton = (factor: number): void => {
|
|
191
|
+
const stage = stageElementRef.current;
|
|
192
|
+
|
|
193
|
+
if (stage !== null) {
|
|
194
|
+
const bounds = boundsOf(stage);
|
|
195
|
+
setView((current) => zoomed(current, factor, { x: 0, y: 0 }, bounds));
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const step = (to: number | null): void => {
|
|
200
|
+
if (to !== null) {
|
|
201
|
+
onIndexChange?.(to);
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const onKeyDown = (event: KeyboardEvent): void => {
|
|
206
|
+
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const to = event.key === 'ArrowLeft' ? previous : event.key === 'ArrowRight' ? next : null;
|
|
211
|
+
|
|
212
|
+
if (to !== null) {
|
|
213
|
+
event.preventDefault();
|
|
214
|
+
step(to);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
78
217
|
|
|
79
218
|
return (
|
|
80
219
|
<Dialog.Root open={open} onOpenChange={onOpenChange}>
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
<
|
|
84
|
-
|
|
85
|
-
{name === alt ? title : <Tooltip label={alt}>{title}</Tooltip>}
|
|
86
|
-
<span className="grow" />
|
|
87
|
-
<span className="tabular-nums text-xs text-text-faint">{Math.round(view.scale * 100)}%</span>
|
|
220
|
+
<Dialog.Popup backdropClassName="lightbox-backdrop" className="lightbox" onKeyDown={onKeyDown}>
|
|
221
|
+
<Dialog.Title className="sr-only">{image.alt}</Dialog.Title>
|
|
222
|
+
<div className="lightbox-bar flex h-12 shrink-0 items-center justify-end gap-2 pl-3">
|
|
223
|
+
<span className="tabular-nums text-xs text-text-faint">{formatNumber(view.scale * 100)}%</span>
|
|
88
224
|
<ButtonGroup render={<span />}>
|
|
89
225
|
<IconButton
|
|
90
226
|
icon={Minus}
|
|
@@ -106,18 +242,21 @@ function Lightbox({
|
|
|
106
242
|
<CloseButton label={t('common.action.close')} kbd="esc" className="ml-1" dialog />
|
|
107
243
|
</div>
|
|
108
244
|
<div
|
|
109
|
-
ref={
|
|
110
|
-
className={clsx('lightbox-
|
|
111
|
-
onWheel={(e) => {
|
|
112
|
-
const point = pointIn(e);
|
|
113
|
-
setView((current) => zoomed(current, WHEEL_STEP ** -e.deltaY, point.x, point.y));
|
|
114
|
-
}}
|
|
245
|
+
ref={stageRef}
|
|
246
|
+
className={clsx('lightbox-stage relative grid min-h-0 grow place-items-center overflow-hidden', view.scale > MIN_SCALE && 'cursor-grab')}
|
|
115
247
|
onDoubleClick={(e) => {
|
|
116
|
-
|
|
117
|
-
|
|
248
|
+
if (onControl(e.target)) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const point = pointIn(e.currentTarget, e);
|
|
253
|
+
const bounds = boundsOf(e.currentTarget);
|
|
254
|
+
setView((current) => (current.scale > MIN_SCALE ? START : zoomed(current, 2, point, bounds)));
|
|
118
255
|
}}
|
|
119
256
|
onPointerDown={(e) => {
|
|
120
|
-
|
|
257
|
+
movedRef.current = false;
|
|
258
|
+
|
|
259
|
+
if (view.scale <= MIN_SCALE || onControl(e.target)) {
|
|
121
260
|
return;
|
|
122
261
|
}
|
|
123
262
|
|
|
@@ -127,9 +266,13 @@ function Lightbox({
|
|
|
127
266
|
onPointerMove={(e) => {
|
|
128
267
|
const drag = dragRef.current;
|
|
129
268
|
|
|
130
|
-
if (drag?.pointerId
|
|
131
|
-
|
|
269
|
+
if (drag?.pointerId !== e.pointerId) {
|
|
270
|
+
return;
|
|
132
271
|
}
|
|
272
|
+
|
|
273
|
+
movedRef.current = true;
|
|
274
|
+
const bounds = boundsOf(e.currentTarget);
|
|
275
|
+
setView((current) => clamped({ ...current, x: e.clientX - drag.x, y: e.clientY - drag.y }, bounds));
|
|
133
276
|
}}
|
|
134
277
|
onPointerUp={() => {
|
|
135
278
|
dragRef.current = null;
|
|
@@ -137,14 +280,49 @@ function Lightbox({
|
|
|
137
280
|
onPointerCancel={() => {
|
|
138
281
|
dragRef.current = null;
|
|
139
282
|
}}
|
|
283
|
+
onClick={(e) => {
|
|
284
|
+
// The room around the picture is the backdrop a dialog would have, unless the press panned.
|
|
285
|
+
if (e.target === e.currentTarget && !movedRef.current) {
|
|
286
|
+
onOpenChange(false);
|
|
287
|
+
}
|
|
288
|
+
}}
|
|
140
289
|
>
|
|
141
290
|
<DisplayImage
|
|
142
|
-
|
|
143
|
-
|
|
291
|
+
key={image.key}
|
|
292
|
+
src={image.src}
|
|
293
|
+
alt={image.alt}
|
|
144
294
|
draggable={false}
|
|
145
295
|
className="select-none"
|
|
146
296
|
style={{ transform: `translate(${Math.round(view.x)}px, ${Math.round(view.y)}px) scale(${view.scale})` }}
|
|
147
297
|
/>
|
|
298
|
+
{images.length > 1 && (
|
|
299
|
+
<>
|
|
300
|
+
<IconButton
|
|
301
|
+
icon={ChevronLeft}
|
|
302
|
+
label={t('image.previous')}
|
|
303
|
+
kbd="←"
|
|
304
|
+
disabled={previous === null}
|
|
305
|
+
className="absolute top-1/2 left-4 -translate-y-1/2"
|
|
306
|
+
onClick={() => step(previous)}
|
|
307
|
+
/>
|
|
308
|
+
<IconButton
|
|
309
|
+
icon={ChevronRight}
|
|
310
|
+
label={t('image.next')}
|
|
311
|
+
kbd="→"
|
|
312
|
+
disabled={next === null}
|
|
313
|
+
className="absolute top-1/2 right-4 -translate-y-1/2"
|
|
314
|
+
onClick={() => step(next)}
|
|
315
|
+
/>
|
|
316
|
+
</>
|
|
317
|
+
)}
|
|
318
|
+
</div>
|
|
319
|
+
<div className="flex h-12 shrink-0 items-center justify-center gap-3 px-3">
|
|
320
|
+
{name === image.alt ? title : <Tooltip label={image.alt}>{title}</Tooltip>}
|
|
321
|
+
{images.length > 1 && (
|
|
322
|
+
<span className="shrink-0 tabular-nums text-xs text-text-faint">
|
|
323
|
+
{t('image.position', { index: formatNumber(index + 1), count: formatNumber(images.length) })}
|
|
324
|
+
</span>
|
|
325
|
+
)}
|
|
148
326
|
</div>
|
|
149
327
|
</Dialog.Popup>
|
|
150
328
|
</Dialog.Root>
|
|
@@ -169,17 +347,30 @@ export interface ImageThumbProps {
|
|
|
169
347
|
* The image is bytes where the host keeps them; until they are here a quiet square stands in for it.
|
|
170
348
|
*/
|
|
171
349
|
export function ImageThumb({ source, alt, className, aspect, actions, open: openProp, onOpenChange }: ImageThumbProps) {
|
|
350
|
+
const key = useId();
|
|
351
|
+
const buttonRef = useRef<HTMLButtonElement>(null);
|
|
352
|
+
const context = useContext(ImageGroupContext);
|
|
353
|
+
const group = actions === undefined && openProp === undefined ? context : null;
|
|
172
354
|
const [ownOpen, setOwnOpen] = useState(false);
|
|
173
355
|
const open = openProp ?? ownOpen;
|
|
356
|
+
const { url: src, failure } = source;
|
|
357
|
+
const ratio = aspect === undefined ? undefined : { aspectRatio: aspect };
|
|
358
|
+
|
|
359
|
+
useLayoutEffect(() => {
|
|
360
|
+
if (group === null || src === null) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
group.store.set({ key, src, alt, element: buttonRef.current });
|
|
365
|
+
|
|
366
|
+
return () => group.store.remove(key);
|
|
367
|
+
}, [group, key, src, alt]);
|
|
174
368
|
|
|
175
369
|
const setOpen = (next: boolean): void => {
|
|
176
370
|
setOwnOpen(next);
|
|
177
371
|
onOpenChange?.(next);
|
|
178
372
|
};
|
|
179
373
|
|
|
180
|
-
const { url: src, failure } = source;
|
|
181
|
-
const ratio = aspect === undefined ? undefined : { aspectRatio: aspect };
|
|
182
|
-
|
|
183
374
|
if (src === null) {
|
|
184
375
|
return (
|
|
185
376
|
<span
|
|
@@ -201,14 +392,15 @@ export function ImageThumb({ source, alt, className, aspect, actions, open: open
|
|
|
201
392
|
return (
|
|
202
393
|
<>
|
|
203
394
|
<button
|
|
395
|
+
ref={buttonRef}
|
|
204
396
|
type="button"
|
|
205
397
|
className={clsx('block overflow-hidden rounded-lg border border-border', aspect !== undefined && 'w-full')}
|
|
206
398
|
style={ratio}
|
|
207
|
-
onClick={() => setOpen(true)}
|
|
399
|
+
onClick={() => (group === null ? setOpen(true) : group.open(key))}
|
|
208
400
|
>
|
|
209
401
|
<DisplayImage src={src} alt={alt} className={clsx(aspect !== undefined && 'size-full object-contain', className)} />
|
|
210
402
|
</button>
|
|
211
|
-
<Lightbox
|
|
403
|
+
{group === null && <Lightbox images={[{ key, src, alt, element: null }]} index={0} open={open} onOpenChange={setOpen} actions={actions} />}
|
|
212
404
|
</>
|
|
213
405
|
);
|
|
214
406
|
}
|
|
@@ -8,7 +8,7 @@ import { formatNumber } from '@adecore/ui/format';
|
|
|
8
8
|
import { useChatHost } from '../../../../host';
|
|
9
9
|
import { CodeBlock } from '../../CodeBlock';
|
|
10
10
|
import { CodeStreamingContext } from '../../code-streaming';
|
|
11
|
-
import { ImageThumb } from '../../ImageView';
|
|
11
|
+
import { ImageGroup, ImageThumb } from '../../ImageView';
|
|
12
12
|
import { uiChildrenOf, uiNodeLabel, uiNodeText, uiUrlDomain } from '../node-text';
|
|
13
13
|
import type { UiRendererProps } from '../render-context';
|
|
14
14
|
import { useTruncated } from '../use-truncated';
|
|
@@ -105,7 +105,9 @@ export function GalleryRenderer({ node, children }: UiRendererProps<UiProps<'Gal
|
|
|
105
105
|
return (
|
|
106
106
|
<GalleryContext value>
|
|
107
107
|
<div role="group" aria-label={t('blocks.gallery.label', { count: uiChildrenOf(node, 'Image').length })} className="@container">
|
|
108
|
-
<div className="chat-ui-nodes grid grid-cols-2 gap-2 @min-[420px]:grid-cols-3">
|
|
108
|
+
<div className="chat-ui-nodes grid grid-cols-2 gap-2 @min-[420px]:grid-cols-3">
|
|
109
|
+
<ImageGroup>{children}</ImageGroup>
|
|
110
|
+
</div>
|
|
109
111
|
</div>
|
|
110
112
|
</GalleryContext>
|
|
111
113
|
);
|
|
@@ -22,7 +22,7 @@ import { referenceTokens } from '../../chat-references';
|
|
|
22
22
|
import { presentReferences } from '../../mentions';
|
|
23
23
|
import { ReferenceChip } from '../ReferenceChip';
|
|
24
24
|
import { ReferenceLookupContext } from '../reference-lookup';
|
|
25
|
-
import { ImageThumb } from '../ImageView';
|
|
25
|
+
import { ImageGroup, ImageThumb } from '../ImageView';
|
|
26
26
|
import { Markdown, MessageMarkdown, ReplyMarkdown } from '../Markdown';
|
|
27
27
|
import { FadingWords } from '../FadingWords';
|
|
28
28
|
import { settledBlocksText } from '../markdown-blocks';
|
|
@@ -123,13 +123,15 @@ export function UserRow({ chatId, item }: { chatId: string; item: ChatUserItem }
|
|
|
123
123
|
<MessageHeading>{t('rows.user.heading')}</MessageHeading>
|
|
124
124
|
{attachments.length > 0 && (
|
|
125
125
|
<div className="mb-1.5 flex max-w-[80%] flex-wrap justify-end gap-1.5">
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
126
|
+
<ImageGroup>
|
|
127
|
+
{attachments.map((attachment) =>
|
|
128
|
+
isImageAttachment(attachment.mime) ? (
|
|
129
|
+
<AttachmentThumb key={attachment.id} chatId={chatId} attachment={attachment} />
|
|
130
|
+
) : (
|
|
131
|
+
<AttachmentLink key={attachment.id} chatId={chatId} attachment={attachment} />
|
|
132
|
+
)
|
|
133
|
+
)}
|
|
134
|
+
</ImageGroup>
|
|
133
135
|
</div>
|
|
134
136
|
)}
|
|
135
137
|
{beside.length > 0 && (
|
|
@@ -83,7 +83,15 @@ export function Row({ row, chatId, toggleGroup, toggleTurn, toggleSubagent, open
|
|
|
83
83
|
return <ForksRow chatId={chatId} turnId={row.turnId} />;
|
|
84
84
|
case 'changed-files':
|
|
85
85
|
return (
|
|
86
|
-
<ChangedFilesRow
|
|
86
|
+
<ChangedFilesRow
|
|
87
|
+
chatId={chatId}
|
|
88
|
+
turnId={row.turnId}
|
|
89
|
+
tools={row.tools}
|
|
90
|
+
diff={row.diff}
|
|
91
|
+
checkpoint={row.checkpoint}
|
|
92
|
+
baseMoved={row.baseMoved}
|
|
93
|
+
heldBack={row.heldBack === true}
|
|
94
|
+
/>
|
|
87
95
|
);
|
|
88
96
|
case 'approval':
|
|
89
97
|
return <ApprovalHistoryRow item={row.item} />;
|