@dbx-tools/ui-mastra 0.3.18 → 0.3.19
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/package.json +7 -7
- package/src/react/chat-view.tsx +92 -46
package/package.json
CHANGED
|
@@ -26,19 +26,19 @@
|
|
|
26
26
|
"shiki": "^3.0.0",
|
|
27
27
|
"sql-formatter": "^15.6.9",
|
|
28
28
|
"streamdown": "^2.5.0",
|
|
29
|
-
"@dbx-tools/shared-
|
|
30
|
-
"@dbx-tools/shared-
|
|
31
|
-
"@dbx-tools/
|
|
32
|
-
"@dbx-tools/
|
|
33
|
-
"@dbx-tools/shared-
|
|
34
|
-
"@dbx-tools/ui-branding": "0.3.
|
|
29
|
+
"@dbx-tools/shared-core": "0.3.19",
|
|
30
|
+
"@dbx-tools/shared-genie": "0.3.19",
|
|
31
|
+
"@dbx-tools/shared-mastra": "0.3.19",
|
|
32
|
+
"@dbx-tools/ui-appkit": "0.3.19",
|
|
33
|
+
"@dbx-tools/shared-model": "0.3.19",
|
|
34
|
+
"@dbx-tools/ui-branding": "0.3.19"
|
|
35
35
|
},
|
|
36
36
|
"main": "index.ts",
|
|
37
37
|
"license": "UNLICENSED",
|
|
38
38
|
"publishConfig": {
|
|
39
39
|
"access": "public"
|
|
40
40
|
},
|
|
41
|
-
"version": "0.3.
|
|
41
|
+
"version": "0.3.19",
|
|
42
42
|
"types": "index.ts",
|
|
43
43
|
"type": "module",
|
|
44
44
|
"exports": {
|
package/src/react/chat-view.tsx
CHANGED
|
@@ -140,9 +140,20 @@ export const ChatView = ({
|
|
|
140
140
|
onFeedback,
|
|
141
141
|
}: ChatViewProps) => {
|
|
142
142
|
const [input, setInput] = useState("");
|
|
143
|
-
// Id of the queued steer currently being dragged (
|
|
144
|
-
//
|
|
143
|
+
// Id of the queued steer currently being dragged (pointer drag), for the
|
|
144
|
+
// reorder affordance + drop styling. Null when not dragging. Pointer Events
|
|
145
|
+
// (not native HTML5 drag) so the grip works on touch as well as mouse -
|
|
146
|
+
// `draggable`/`onDrag*` never fire on a touchscreen.
|
|
145
147
|
const [draggingSteerId, setDraggingSteerId] = useState<string | null>(null);
|
|
148
|
+
// Live DOM refs to each queued-steer chip, keyed by steer id, so a pointer
|
|
149
|
+
// drag can hit-test the pointer's Y against each chip's midpoint and reorder
|
|
150
|
+
// as the finger/cursor moves over a neighbour.
|
|
151
|
+
const steerChipRefs = useRef(new Map<string, HTMLDivElement>());
|
|
152
|
+
// Id of the steer under an active pointer drag, mirrored in a ref so the
|
|
153
|
+
// pointermove handler reads it synchronously. React state (`draggingSteerId`)
|
|
154
|
+
// only drives styling and lags a render behind the pointerdown, which on
|
|
155
|
+
// touch dropped the first moves and made the drag feel dead.
|
|
156
|
+
const draggingIdRef = useRef<string | null>(null);
|
|
146
157
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
147
158
|
const contentRef = useRef<HTMLDivElement>(null);
|
|
148
159
|
// Composer textarea, auto-grown with its content up to the CSS `max-h`.
|
|
@@ -166,6 +177,10 @@ export const ChatView = ({
|
|
|
166
177
|
);
|
|
167
178
|
const loadMoreRef = useRef(onLoadMore);
|
|
168
179
|
loadMoreRef.current = onLoadMore;
|
|
180
|
+
// Latest queued steers, read by the pointer-drag move handler so it reorders
|
|
181
|
+
// against the current queue rather than the order captured when the drag began.
|
|
182
|
+
const queuedSteersRef = useRef(queuedSteers);
|
|
183
|
+
queuedSteersRef.current = queuedSteers;
|
|
169
184
|
|
|
170
185
|
// Jump the transcript to the bottom, marking the write as programmatic so
|
|
171
186
|
// the resulting scroll event isn't mistaken for the user scrolling away.
|
|
@@ -176,6 +191,40 @@ export const ChatView = ({
|
|
|
176
191
|
el.scrollTop = el.scrollHeight;
|
|
177
192
|
}, []);
|
|
178
193
|
|
|
194
|
+
// Reorder the queued steers to place `draggingId` at the slot whose chip the
|
|
195
|
+
// pointer is currently over, hit-testing the pointer Y against each chip's
|
|
196
|
+
// vertical midpoint. Called continuously during a pointer drag so the queue
|
|
197
|
+
// reflows live under the finger/cursor, then committed via `onReorderSteers`.
|
|
198
|
+
const reorderSteersByPointer = useCallback(
|
|
199
|
+
(draggingId: string, pointerY: number) => {
|
|
200
|
+
if (!onReorderSteers) return;
|
|
201
|
+
const order = queuedSteersRef.current.map((s) => s.id);
|
|
202
|
+
// Build the target order: everything except the dragged id, then insert
|
|
203
|
+
// the dragged id before the first chip whose midpoint is below the
|
|
204
|
+
// pointer (or at the end if the pointer is past them all).
|
|
205
|
+
const rest = order.filter((id) => id !== draggingId);
|
|
206
|
+
let insertAt = rest.length;
|
|
207
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
208
|
+
const chip = steerChipRefs.current.get(rest[i]);
|
|
209
|
+
if (!chip) continue;
|
|
210
|
+
const box = chip.getBoundingClientRect();
|
|
211
|
+
if (pointerY < box.top + box.height / 2) {
|
|
212
|
+
insertAt = i;
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const next = [...rest];
|
|
217
|
+
next.splice(insertAt, 0, draggingId);
|
|
218
|
+
// Skip the commit when the order is unchanged, so we don't thrash the
|
|
219
|
+
// parent state on every pointermove tick.
|
|
220
|
+
if (next.length === order.length && next.every((id, i) => id === order[i])) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
onReorderSteers(next);
|
|
224
|
+
},
|
|
225
|
+
[onReorderSteers],
|
|
226
|
+
);
|
|
227
|
+
|
|
179
228
|
// Follow the bottom as streamed content grows, as long as we're "pinned".
|
|
180
229
|
// A ResizeObserver on the transcript catches every height change - new
|
|
181
230
|
// messages, token-by-token text, async markdown/chart layout, the waiting
|
|
@@ -655,59 +704,56 @@ export const ChatView = ({
|
|
|
655
704
|
<div className="mb-2 flex flex-col gap-1">
|
|
656
705
|
{queuedSteers.map((steer) => {
|
|
657
706
|
const reorderable = Boolean(onReorderSteers);
|
|
658
|
-
// Move the dragged steer to just before `steer` (or to the
|
|
659
|
-
// end when dropped on itself at the tail) and report the new
|
|
660
|
-
// id order to the driver.
|
|
661
|
-
const dropBefore = () => {
|
|
662
|
-
if (!onReorderSteers || !draggingSteerId || draggingSteerId === steer.id) {
|
|
663
|
-
return;
|
|
664
|
-
}
|
|
665
|
-
const ids = queuedSteers.map((s) => s.id).filter((id) => id !== draggingSteerId);
|
|
666
|
-
const at = ids.indexOf(steer.id);
|
|
667
|
-
ids.splice(at === -1 ? ids.length : at, 0, draggingSteerId);
|
|
668
|
-
onReorderSteers(ids);
|
|
669
|
-
};
|
|
670
707
|
return (
|
|
671
708
|
<div
|
|
672
709
|
key={steer.id}
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
setDraggingSteerId(steer.id);
|
|
678
|
-
e.dataTransfer.effectAllowed = "move";
|
|
679
|
-
}
|
|
680
|
-
: undefined
|
|
681
|
-
}
|
|
682
|
-
onDragOver={
|
|
683
|
-
reorderable
|
|
684
|
-
? (e) => {
|
|
685
|
-
e.preventDefault();
|
|
686
|
-
e.dataTransfer.dropEffect = "move";
|
|
687
|
-
}
|
|
688
|
-
: undefined
|
|
689
|
-
}
|
|
690
|
-
onDrop={
|
|
691
|
-
reorderable
|
|
692
|
-
? (e) => {
|
|
693
|
-
e.preventDefault();
|
|
694
|
-
dropBefore();
|
|
695
|
-
setDraggingSteerId(null);
|
|
696
|
-
}
|
|
697
|
-
: undefined
|
|
698
|
-
}
|
|
699
|
-
onDragEnd={reorderable ? () => setDraggingSteerId(null) : undefined}
|
|
710
|
+
ref={(el) => {
|
|
711
|
+
if (el) steerChipRefs.current.set(steer.id, el);
|
|
712
|
+
else steerChipRefs.current.delete(steer.id);
|
|
713
|
+
}}
|
|
700
714
|
className={cn(
|
|
701
715
|
"flex items-center gap-1.5 rounded-lg border border-border/70 bg-muted/40 px-2 py-1 text-xs",
|
|
702
|
-
reorderable && "cursor-grab active:cursor-grabbing",
|
|
703
716
|
draggingSteerId === steer.id && "opacity-50",
|
|
704
717
|
)}
|
|
705
718
|
>
|
|
706
719
|
{reorderable && (
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
720
|
+
// Drag handle. Pointer Events (not native HTML5 drag) so
|
|
721
|
+
// it works on touch: `touch-none` (touch-action: none)
|
|
722
|
+
// stops the browser treating the drag as a scroll, and
|
|
723
|
+
// pointer capture keeps move/up events flowing to the grip
|
|
724
|
+
// even as the finger slides over sibling chips. The active
|
|
725
|
+
// id lives in a ref (`draggingIdRef`) so the first
|
|
726
|
+
// pointermove isn't dropped waiting for a state re-render.
|
|
727
|
+
// `-m-1 p-1` enlarges the tap target to ~28px without
|
|
728
|
+
// widening the visible grip - a 12px icon is too small to
|
|
729
|
+
// reliably grab on touch.
|
|
730
|
+
<span
|
|
731
|
+
role="button"
|
|
732
|
+
tabIndex={-1}
|
|
733
|
+
aria-label="Drag to reorder"
|
|
734
|
+
className="-m-1 shrink-0 cursor-grab touch-none p-1 text-muted-foreground active:cursor-grabbing"
|
|
735
|
+
onPointerDown={(e) => {
|
|
736
|
+
e.preventDefault();
|
|
737
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
738
|
+
draggingIdRef.current = steer.id;
|
|
739
|
+
setDraggingSteerId(steer.id);
|
|
740
|
+
}}
|
|
741
|
+
onPointerMove={(e) => {
|
|
742
|
+
if (draggingIdRef.current !== steer.id) return;
|
|
743
|
+
reorderSteersByPointer(steer.id, e.clientY);
|
|
744
|
+
}}
|
|
745
|
+
onPointerUp={(e) => {
|
|
746
|
+
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
747
|
+
draggingIdRef.current = null;
|
|
748
|
+
setDraggingSteerId(null);
|
|
749
|
+
}}
|
|
750
|
+
onPointerCancel={() => {
|
|
751
|
+
draggingIdRef.current = null;
|
|
752
|
+
setDraggingSteerId(null);
|
|
753
|
+
}}
|
|
754
|
+
>
|
|
755
|
+
<GripVerticalIcon className="size-3" aria-hidden="true" />
|
|
756
|
+
</span>
|
|
711
757
|
)}
|
|
712
758
|
<span className="text-muted-foreground">Queued</span>
|
|
713
759
|
<span className="min-w-0 flex-1 truncate">{steer.text}</span>
|