@opengeni/react 3.7.0-canary.7 → 3.8.0-canary.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/README.md +6 -0
- package/dist/chat.js +1 -1
- package/dist/chunk-5M6VTFJ5.js +171 -0
- package/dist/chunk-5M6VTFJ5.js.map +1 -0
- package/dist/{chunk-IOLVN74D.js → chunk-6J56T3MG.js} +15 -1
- package/dist/chunk-6J56T3MG.js.map +1 -0
- package/dist/{chunk-BQGIXRKV.js → chunk-7BWFU6H4.js} +22 -4
- package/dist/chunk-7BWFU6H4.js.map +1 -0
- package/dist/{chunk-3HIF5IFC.js → chunk-7W6PVVTK.js} +63 -18
- package/dist/chunk-7W6PVVTK.js.map +1 -0
- package/dist/{chunk-ZFOFY5ST.js → chunk-NLTS7JHI.js} +16 -31
- package/dist/chunk-NLTS7JHI.js.map +1 -0
- package/dist/chunk-NOSYBS2D.js +208 -0
- package/dist/chunk-NOSYBS2D.js.map +1 -0
- package/dist/{chunk-F2CFHN3Y.js → chunk-YAIPFO5D.js} +2 -1
- package/dist/chunk-YAIPFO5D.js.map +1 -0
- package/dist/chunk-YBM6CQRU.js +292 -0
- package/dist/chunk-YBM6CQRU.js.map +1 -0
- package/dist/chunk-ZYRGEKWS.js +216 -0
- package/dist/chunk-ZYRGEKWS.js.map +1 -0
- package/dist/components/composer.d.ts +4 -2
- package/dist/components/message-timeline.d.ts +9 -2
- package/dist/components/timeline-annotation-chrome.d.ts +25 -0
- package/dist/components/timeline-annotation-layout.d.ts +42 -0
- package/dist/components/timeline-annotation-markers.d.ts +8 -0
- package/dist/components/timeline-annotation-reveal-context.d.ts +4 -0
- package/dist/components/timeline-annotation-shared.d.ts +31 -0
- package/dist/components/timeline-annotations-dialog.d.ts +1 -1
- package/dist/components/timeline-annotations.d.ts +8 -2
- package/dist/composer.js +2 -2
- package/dist/hooks/use-composer.d.ts +2 -0
- package/dist/index.js +9 -6
- package/dist/index.js.map +1 -1
- package/dist/interaction.js +6 -6
- package/dist/machines.js +1 -1
- package/dist/{queue-surface-implementation-XL2A6PBM.js → queue-surface-implementation-LK3S4BIW.js} +5 -2
- package/dist/{queue-surface-implementation-XL2A6PBM.js.map → queue-surface-implementation-LK3S4BIW.js.map} +1 -1
- package/dist/session-ui.js +8 -5
- package/dist/session.js +1 -1
- package/dist/timeline-annotation-markers-DRSTLEM6.js +165 -0
- package/dist/timeline-annotation-markers-DRSTLEM6.js.map +1 -0
- package/dist/timeline-annotation-selection-IHLUGVND.js +236 -0
- package/dist/timeline-annotation-selection-IHLUGVND.js.map +1 -0
- package/dist/timeline-annotations-dialog-6Q53N5FG.js +262 -0
- package/dist/timeline-annotations-dialog-6Q53N5FG.js.map +1 -0
- package/package.json +2 -2
- package/src/components/chat-composer.tsx +5 -2
- package/src/components/composer.tsx +40 -5
- package/src/components/copy-button.tsx +1 -0
- package/src/components/machines-dashboard.tsx +1 -16
- package/src/components/message-timeline.tsx +357 -297
- package/src/components/timeline-annotation-chrome.tsx +183 -0
- package/src/components/timeline-annotation-layout.ts +268 -0
- package/src/components/timeline-annotation-markers.tsx +185 -0
- package/src/components/timeline-annotation-reveal-context.ts +9 -0
- package/src/components/timeline-annotation-selection.tsx +99 -54
- package/src/components/timeline-annotation-shared.ts +245 -0
- package/src/components/timeline-annotations-dialog.tsx +172 -128
- package/src/components/timeline-annotations.tsx +178 -45
- package/src/hooks/use-composer.ts +17 -0
- package/styles/compiled.css +86 -22
- package/dist/chunk-3HIF5IFC.js.map +0 -1
- package/dist/chunk-BQGIXRKV.js.map +0 -1
- package/dist/chunk-F2CFHN3Y.js.map +0 -1
- package/dist/chunk-IOLVN74D.js.map +0 -1
- package/dist/chunk-W3OUAH6M.js +0 -170
- package/dist/chunk-W3OUAH6M.js.map +0 -1
- package/dist/chunk-ZFOFY5ST.js.map +0 -1
- package/dist/timeline-annotation-selection-3L7LHGG2.js +0 -187
- package/dist/timeline-annotation-selection-3L7LHGG2.js.map +0 -1
- package/dist/timeline-annotations-dialog-U7EVLR75.js +0 -196
- package/dist/timeline-annotations-dialog-U7EVLR75.js.map +0 -1
|
@@ -1,40 +1,28 @@
|
|
|
1
1
|
import type { TimelineAnnotationSource } from "@opengeni/sdk";
|
|
2
|
+
import { XIcon } from "lucide-react";
|
|
2
3
|
import { useEffect, useLayoutEffect, useRef, useState, type RefObject } from "react";
|
|
3
4
|
import { createPortal } from "react-dom";
|
|
4
|
-
import
|
|
5
|
+
import { usePortalTokenStyle } from "../lib/use-portal-token-style";
|
|
6
|
+
import {
|
|
7
|
+
AnnotationAccentRow,
|
|
8
|
+
AnnotationNoteField,
|
|
9
|
+
AnnotationNotePreview,
|
|
10
|
+
AnnotationQuoteSourceButton,
|
|
11
|
+
type TimelineAnnotationLike,
|
|
12
|
+
} from "./timeline-annotation-chrome";
|
|
13
|
+
import {
|
|
14
|
+
ANNOTATION_REVIEW_DIALOG_WIDTH_PX,
|
|
15
|
+
clampAnnotationDialogPlacement,
|
|
16
|
+
scrollAnnotationRowIntoList,
|
|
17
|
+
} from "./timeline-annotation-layout";
|
|
18
|
+
import { annotationDisplayOrdinal, cssEscapeAttribute } from "./timeline-annotation-shared";
|
|
5
19
|
|
|
6
|
-
function
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
if (typeof document === "undefined") return false;
|
|
13
|
-
const element = document.querySelector(
|
|
14
|
-
`[data-og-annotation-source-key="${cssEscapeAttribute(source.eventId)}"]`,
|
|
15
|
-
);
|
|
16
|
-
if (!(element instanceof HTMLElement)) return false;
|
|
17
|
-
element.scrollIntoView({ block: "center", behavior: "smooth" });
|
|
18
|
-
element.animate?.(
|
|
19
|
-
[
|
|
20
|
-
{ outline: "2px solid color-mix(in srgb, currentColor 55%, transparent)" },
|
|
21
|
-
{ outline: "2px solid transparent" },
|
|
22
|
-
],
|
|
23
|
-
{ duration: 1600, easing: "ease-out" },
|
|
24
|
-
);
|
|
25
|
-
return true;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function sourceLabel(annotation: TimelineAnnotationLike): string {
|
|
29
|
-
if (annotation.source.label) return annotation.source.label;
|
|
30
|
-
switch (annotation.source.kind) {
|
|
31
|
-
case "user_message":
|
|
32
|
-
return "User message";
|
|
33
|
-
case "assistant_message":
|
|
34
|
-
return "Assistant message";
|
|
35
|
-
case "tool_output":
|
|
36
|
-
return "Tool output";
|
|
37
|
-
}
|
|
20
|
+
function focusableElements(root: HTMLElement): HTMLElement[] {
|
|
21
|
+
return [
|
|
22
|
+
...root.querySelectorAll<HTMLElement>(
|
|
23
|
+
'button:not([disabled]),textarea:not([disabled]),input:not([disabled]),[href],[tabindex]:not([tabindex="-1"])',
|
|
24
|
+
),
|
|
25
|
+
].filter((element) => element.tabIndex >= 0 && !element.closest("[inert]"));
|
|
38
26
|
}
|
|
39
27
|
|
|
40
28
|
export function TimelineAnnotationsDialog({
|
|
@@ -61,31 +49,90 @@ export function TimelineAnnotationsDialog({
|
|
|
61
49
|
onDismiss: (restoreFocus: boolean) => void;
|
|
62
50
|
}) {
|
|
63
51
|
const [unavailableId, setUnavailableId] = useState<string | null>(null);
|
|
64
|
-
const [position, setPosition] = useState({ left: 12, top: 12,
|
|
52
|
+
const [position, setPosition] = useState({ left: 12, top: 12, maxHeight: 0 });
|
|
65
53
|
const panelRef = useRef<HTMLDivElement | null>(null);
|
|
54
|
+
const headerRef = useRef<HTMLDivElement | null>(null);
|
|
55
|
+
const listRef = useRef<HTMLDivElement | null>(null);
|
|
66
56
|
const noteRefs = useRef(new Map<string, HTMLTextAreaElement>());
|
|
57
|
+
const portalStyle = usePortalTokenStyle(triggerRef.current);
|
|
58
|
+
const revealNote = (id: string) => {
|
|
59
|
+
const row = listRef.current?.querySelector<HTMLElement>(
|
|
60
|
+
`[data-og-annotation-id="${cssEscapeAttribute(id)}"]`,
|
|
61
|
+
);
|
|
62
|
+
scrollAnnotationRowIntoList(listRef.current, row);
|
|
63
|
+
noteRefs.current.get(id)?.focus();
|
|
64
|
+
};
|
|
67
65
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
66
|
+
const commitNotes = (fromId: string) => {
|
|
67
|
+
const incomplete = annotations.filter((annotation) => {
|
|
68
|
+
const live = noteRefs.current.get(annotation.id);
|
|
69
|
+
return (live?.value ?? annotation.note).trim().length === 0;
|
|
70
|
+
});
|
|
71
|
+
if (incomplete.length === 0) {
|
|
72
|
+
onDismiss(true);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const fromIndex = annotations.findIndex((annotation) => annotation.id === fromId);
|
|
76
|
+
const next =
|
|
77
|
+
incomplete.find(
|
|
78
|
+
(item) => annotations.findIndex((annotation) => annotation.id === item.id) > fromIndex,
|
|
79
|
+
) ?? incomplete[0]!;
|
|
80
|
+
revealNote(next.id);
|
|
81
|
+
};
|
|
74
82
|
|
|
75
|
-
|
|
83
|
+
useLayoutEffect(() => {
|
|
76
84
|
const updatePosition = () => {
|
|
77
85
|
const rect = triggerRef.current?.getBoundingClientRect();
|
|
78
86
|
if (!rect) return;
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
87
|
+
const contentHeight =
|
|
88
|
+
(headerRef.current?.offsetHeight ?? 44) + (listRef.current?.scrollHeight ?? 200);
|
|
89
|
+
setPosition(
|
|
90
|
+
clampAnnotationDialogPlacement({
|
|
91
|
+
triggerLeft: rect.left,
|
|
92
|
+
triggerTop: rect.top,
|
|
93
|
+
triggerBottom: rect.bottom,
|
|
94
|
+
contentHeight,
|
|
95
|
+
viewportWidth: window.innerWidth,
|
|
96
|
+
viewportHeight: window.innerHeight,
|
|
97
|
+
panelWidth: ANNOTATION_REVIEW_DIALOG_WIDTH_PX,
|
|
98
|
+
}),
|
|
83
99
|
);
|
|
84
|
-
const above = rect.top > Math.min(360, window.innerHeight * 0.55);
|
|
85
|
-
setPosition({ left, top: above ? rect.top - 8 : rect.bottom + 8, above });
|
|
86
100
|
};
|
|
101
|
+
updatePosition();
|
|
102
|
+
window.addEventListener("resize", updatePosition);
|
|
103
|
+
window.addEventListener("scroll", updatePosition, true);
|
|
104
|
+
return () => {
|
|
105
|
+
window.removeEventListener("resize", updatePosition);
|
|
106
|
+
window.removeEventListener("scroll", updatePosition, true);
|
|
107
|
+
};
|
|
108
|
+
}, [annotations, triggerRef]);
|
|
109
|
+
|
|
110
|
+
useLayoutEffect(() => {
|
|
111
|
+
if (!focusAnnotationId || position.maxHeight <= 0) return;
|
|
112
|
+
revealNote(focusAnnotationId);
|
|
113
|
+
const note = noteRefs.current.get(focusAnnotationId);
|
|
114
|
+
if (note && document.activeElement === note) onFocusConsumed?.();
|
|
115
|
+
}, [focusAnnotationId, onFocusConsumed, annotations.length, position.maxHeight]);
|
|
116
|
+
|
|
117
|
+
useEffect(() => {
|
|
87
118
|
const onKeyDown = (event: KeyboardEvent) => {
|
|
88
|
-
if (event.key === "Escape")
|
|
119
|
+
if (event.key === "Escape") {
|
|
120
|
+
event.stopPropagation();
|
|
121
|
+
onDismiss(true);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (event.key !== "Tab" || !panelRef.current) return;
|
|
125
|
+
const focusable = focusableElements(panelRef.current);
|
|
126
|
+
if (focusable.length === 0) return;
|
|
127
|
+
const first = focusable[0]!;
|
|
128
|
+
const last = focusable[focusable.length - 1]!;
|
|
129
|
+
if (event.shiftKey && document.activeElement === first) {
|
|
130
|
+
event.preventDefault();
|
|
131
|
+
last.focus();
|
|
132
|
+
} else if (!event.shiftKey && document.activeElement === last) {
|
|
133
|
+
event.preventDefault();
|
|
134
|
+
first.focus();
|
|
135
|
+
}
|
|
89
136
|
};
|
|
90
137
|
const onPointerDown = (event: PointerEvent) => {
|
|
91
138
|
if (!(event.target instanceof Node)) return;
|
|
@@ -94,9 +141,6 @@ export function TimelineAnnotationsDialog({
|
|
|
94
141
|
}
|
|
95
142
|
onDismiss(false);
|
|
96
143
|
};
|
|
97
|
-
updatePosition();
|
|
98
|
-
window.addEventListener("resize", updatePosition);
|
|
99
|
-
window.addEventListener("scroll", updatePosition, true);
|
|
100
144
|
document.addEventListener("keydown", onKeyDown);
|
|
101
145
|
document.addEventListener("pointerdown", onPointerDown);
|
|
102
146
|
if (!focusAnnotationId) {
|
|
@@ -105,8 +149,6 @@ export function TimelineAnnotationsDialog({
|
|
|
105
149
|
}, 0);
|
|
106
150
|
}
|
|
107
151
|
return () => {
|
|
108
|
-
window.removeEventListener("resize", updatePosition);
|
|
109
|
-
window.removeEventListener("scroll", updatePosition, true);
|
|
110
152
|
document.removeEventListener("keydown", onKeyDown);
|
|
111
153
|
document.removeEventListener("pointerdown", onPointerDown);
|
|
112
154
|
};
|
|
@@ -118,92 +160,94 @@ export function TimelineAnnotationsDialog({
|
|
|
118
160
|
ref={panelRef}
|
|
119
161
|
role="dialog"
|
|
120
162
|
tabIndex={-1}
|
|
163
|
+
data-og-annotation-review=""
|
|
121
164
|
style={{
|
|
122
165
|
left: position.left,
|
|
123
166
|
top: position.top,
|
|
124
|
-
|
|
167
|
+
maxHeight: position.maxHeight,
|
|
168
|
+
...portalStyle,
|
|
125
169
|
}}
|
|
126
|
-
className="og-root fixed z-[75] box-border
|
|
127
|
-
aria-label={editable ? "
|
|
170
|
+
className="og-root fixed z-[75] box-border flex w-[min(25rem,calc(100vw-1.5rem))] flex-col overflow-hidden rounded-og-lg border border-og-border bg-og-surface-1 text-og-fg shadow-xl outline-hidden"
|
|
171
|
+
aria-label={editable ? "Edit quoted notes" : "Quoted notes"}
|
|
128
172
|
>
|
|
129
|
-
<div
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
</div>
|
|
173
|
+
<div
|
|
174
|
+
ref={headerRef}
|
|
175
|
+
data-og-annotation-review-header=""
|
|
176
|
+
className="flex shrink-0 items-start justify-between gap-3 px-3 pt-3 pb-2"
|
|
177
|
+
>
|
|
178
|
+
<p className="min-w-0 pt-0.5 text-og-sm font-medium text-og-fg-muted">{countLabel}</p>
|
|
136
179
|
<button
|
|
137
180
|
type="button"
|
|
138
|
-
className="
|
|
181
|
+
className="inline-flex size-8 shrink-0 items-center justify-center rounded-md border-0 bg-transparent text-og-fg-muted outline-hidden hover:bg-og-surface-2 hover:text-og-fg focus-visible:ring-2 focus-visible:ring-og-accent pointer-coarse:size-11"
|
|
182
|
+
aria-label="Close"
|
|
139
183
|
onClick={() => onDismiss(true)}
|
|
140
184
|
>
|
|
141
|
-
|
|
185
|
+
<XIcon className="size-3.5" aria-hidden="true" />
|
|
142
186
|
</button>
|
|
143
187
|
</div>
|
|
144
|
-
<div
|
|
145
|
-
{
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
</
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
)
|
|
188
|
+
<div
|
|
189
|
+
ref={listRef}
|
|
190
|
+
data-og-annotation-review-list=""
|
|
191
|
+
className="grid min-h-0 flex-1 gap-2.5 overflow-x-hidden overflow-y-auto overscroll-contain px-3 pb-3"
|
|
192
|
+
>
|
|
193
|
+
{annotations.map((annotation, index) => {
|
|
194
|
+
const ordinal = annotationDisplayOrdinal(annotation, index);
|
|
195
|
+
return (
|
|
196
|
+
<section
|
|
197
|
+
key={annotation.id}
|
|
198
|
+
data-og-annotation-id={annotation.id}
|
|
199
|
+
aria-label={`Annotation ${ordinal}`}
|
|
200
|
+
>
|
|
201
|
+
<AnnotationAccentRow>
|
|
202
|
+
<div className="flex items-start gap-1">
|
|
203
|
+
<div className="min-w-0 flex-1">
|
|
204
|
+
<p className="mb-0.5 text-og-xs font-medium tabular-nums text-og-fg-subtle">
|
|
205
|
+
Annotation {ordinal}
|
|
206
|
+
</p>
|
|
207
|
+
<AnnotationQuoteSourceButton
|
|
208
|
+
annotation={annotation}
|
|
209
|
+
lines={2}
|
|
210
|
+
onRevealSource={onRevealSource}
|
|
211
|
+
onUnavailable={setUnavailableId}
|
|
212
|
+
/>
|
|
213
|
+
</div>
|
|
214
|
+
{editable && onRemove ? (
|
|
215
|
+
<button
|
|
216
|
+
type="button"
|
|
217
|
+
className="inline-flex size-7 shrink-0 items-center justify-center rounded-md border-0 bg-transparent text-og-fg-subtle outline-hidden hover:bg-og-surface-1 hover:text-og-status-failed focus-visible:ring-2 focus-visible:ring-og-accent pointer-coarse:size-11"
|
|
218
|
+
aria-label={`Remove annotation ${ordinal}`}
|
|
219
|
+
onClick={() => onRemove(annotation.id)}
|
|
220
|
+
>
|
|
221
|
+
<XIcon className="size-3.5" aria-hidden="true" />
|
|
222
|
+
</button>
|
|
223
|
+
) : null}
|
|
224
|
+
</div>
|
|
225
|
+
{unavailableId === annotation.id ? (
|
|
226
|
+
<p role="status" className="text-og-xs text-og-status-waiting">
|
|
227
|
+
Source is outside the loaded timeline window.
|
|
228
|
+
</p>
|
|
229
|
+
) : null}
|
|
230
|
+
{editable && onUpdate ? (
|
|
231
|
+
<AnnotationNoteField
|
|
232
|
+
annotation={annotation}
|
|
233
|
+
inputRef={(node) => {
|
|
234
|
+
if (node) noteRefs.current.set(annotation.id, node);
|
|
235
|
+
else noteRefs.current.delete(annotation.id);
|
|
236
|
+
}}
|
|
237
|
+
onUpdate={onUpdate}
|
|
238
|
+
onCommit={() => commitNotes(annotation.id)}
|
|
239
|
+
/>
|
|
240
|
+
) : (
|
|
241
|
+
<AnnotationNotePreview
|
|
242
|
+
note={annotation.note}
|
|
243
|
+
annotationId={annotation.id}
|
|
244
|
+
ordinal={ordinal}
|
|
245
|
+
/>
|
|
246
|
+
)}
|
|
247
|
+
</AnnotationAccentRow>
|
|
248
|
+
</section>
|
|
249
|
+
);
|
|
250
|
+
})}
|
|
207
251
|
</div>
|
|
208
252
|
</div>,
|
|
209
253
|
document.body,
|
|
@@ -1,15 +1,114 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import type { TimelineAnnotationSource } from "@opengeni/sdk";
|
|
2
|
+
import { QuoteIcon, XIcon } from "lucide-react";
|
|
3
|
+
import {
|
|
4
|
+
Suspense,
|
|
5
|
+
lazy,
|
|
6
|
+
useEffect,
|
|
7
|
+
useLayoutEffect,
|
|
8
|
+
useRef,
|
|
9
|
+
useState,
|
|
10
|
+
type ReactNode,
|
|
11
|
+
type RefObject,
|
|
12
|
+
} from "react";
|
|
7
13
|
import { cn } from "../lib/cn";
|
|
14
|
+
import {
|
|
15
|
+
AnnotationAccentRow,
|
|
16
|
+
AnnotationNotePreview,
|
|
17
|
+
AnnotationQuoteSourceButton,
|
|
18
|
+
type TimelineAnnotationLike,
|
|
19
|
+
} from "./timeline-annotation-chrome";
|
|
20
|
+
import { ANNOTATION_CARD_STACK_SCROLL_AT } from "./timeline-annotation-layout";
|
|
21
|
+
import {
|
|
22
|
+
TimelineAnnotationSourceRootContext,
|
|
23
|
+
useTimelineAnnotationSourceRoot,
|
|
24
|
+
} from "./timeline-annotation-reveal-context";
|
|
25
|
+
import {
|
|
26
|
+
annotationDisplayOrdinal,
|
|
27
|
+
annotationHasNote,
|
|
28
|
+
resolveAnnotationRevealRoot,
|
|
29
|
+
} from "./timeline-annotation-shared";
|
|
8
30
|
|
|
9
|
-
export type TimelineAnnotationLike
|
|
31
|
+
export type { TimelineAnnotationLike } from "./timeline-annotation-chrome";
|
|
10
32
|
|
|
11
33
|
const TimelineAnnotationsDialog = lazy(() => import("./timeline-annotations-dialog"));
|
|
12
34
|
|
|
35
|
+
function AnnotationSourceRootBridge({
|
|
36
|
+
triggerRef,
|
|
37
|
+
children,
|
|
38
|
+
}: {
|
|
39
|
+
triggerRef: RefObject<HTMLElement | null>;
|
|
40
|
+
children: ReactNode;
|
|
41
|
+
}) {
|
|
42
|
+
const inheritedRoot = useTimelineAnnotationSourceRoot();
|
|
43
|
+
const conversationRootRef = useRef<HTMLElement | null>(null);
|
|
44
|
+
if (!inheritedRoot) {
|
|
45
|
+
conversationRootRef.current = resolveAnnotationRevealRoot(triggerRef.current);
|
|
46
|
+
}
|
|
47
|
+
useLayoutEffect(() => {
|
|
48
|
+
if (inheritedRoot) return;
|
|
49
|
+
conversationRootRef.current = resolveAnnotationRevealRoot(triggerRef.current);
|
|
50
|
+
});
|
|
51
|
+
if (inheritedRoot) return children;
|
|
52
|
+
return (
|
|
53
|
+
<TimelineAnnotationSourceRootContext.Provider value={conversationRootRef}>
|
|
54
|
+
{children}
|
|
55
|
+
</TimelineAnnotationSourceRootContext.Provider>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function TimelineAnnotationCards({
|
|
60
|
+
annotations,
|
|
61
|
+
onRevealSource,
|
|
62
|
+
className,
|
|
63
|
+
}: {
|
|
64
|
+
annotations: readonly TimelineAnnotationLike[];
|
|
65
|
+
onRevealSource?: ((source: TimelineAnnotationSource) => boolean) | undefined;
|
|
66
|
+
className?: string | undefined;
|
|
67
|
+
}) {
|
|
68
|
+
const [unavailableId, setUnavailableId] = useState<string | null>(null);
|
|
69
|
+
if (annotations.length === 0) return null;
|
|
70
|
+
return (
|
|
71
|
+
<div
|
|
72
|
+
data-og-annotation-cards=""
|
|
73
|
+
className={cn(
|
|
74
|
+
"grid gap-2",
|
|
75
|
+
annotations.length >= ANNOTATION_CARD_STACK_SCROLL_AT &&
|
|
76
|
+
"max-h-[min(28rem,55vh)] overflow-x-hidden overflow-y-auto overscroll-contain pr-1",
|
|
77
|
+
className,
|
|
78
|
+
)}
|
|
79
|
+
>
|
|
80
|
+
{annotations.map((annotation, index) => {
|
|
81
|
+
const ordinal = annotationDisplayOrdinal(annotation, index);
|
|
82
|
+
return (
|
|
83
|
+
<section key={annotation.id} aria-label={`Annotation ${ordinal}`}>
|
|
84
|
+
<AnnotationAccentRow>
|
|
85
|
+
<p className="mb-0.5 text-og-xs font-medium tabular-nums text-og-fg-subtle">
|
|
86
|
+
Annotation {ordinal}
|
|
87
|
+
</p>
|
|
88
|
+
<AnnotationQuoteSourceButton
|
|
89
|
+
annotation={annotation}
|
|
90
|
+
lines={2}
|
|
91
|
+
onRevealSource={onRevealSource}
|
|
92
|
+
onUnavailable={setUnavailableId}
|
|
93
|
+
/>
|
|
94
|
+
{unavailableId === annotation.id ? (
|
|
95
|
+
<p role="status" className="text-og-xs text-og-status-waiting">
|
|
96
|
+
Source is outside the loaded timeline window.
|
|
97
|
+
</p>
|
|
98
|
+
) : null}
|
|
99
|
+
<AnnotationNotePreview
|
|
100
|
+
note={annotation.note}
|
|
101
|
+
annotationId={annotation.id}
|
|
102
|
+
ordinal={ordinal}
|
|
103
|
+
/>
|
|
104
|
+
</AnnotationAccentRow>
|
|
105
|
+
</section>
|
|
106
|
+
);
|
|
107
|
+
})}
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
13
112
|
export function TimelineAnnotationsChip({
|
|
14
113
|
annotations,
|
|
15
114
|
editable = false,
|
|
@@ -29,55 +128,89 @@ export function TimelineAnnotationsChip({
|
|
|
29
128
|
onRevealSource?: ((source: TimelineAnnotationSource) => boolean) | undefined;
|
|
30
129
|
className?: string | undefined;
|
|
31
130
|
}) {
|
|
32
|
-
const [open, setOpen] = useState(false);
|
|
33
|
-
const triggerRef = useRef<HTMLButtonElement | null>(null);
|
|
34
|
-
|
|
35
|
-
if (annotations.length === 0) return null;
|
|
36
131
|
const focusRequested = Boolean(
|
|
37
132
|
focusAnnotationId && annotations.some((item) => item.id === focusAnnotationId),
|
|
38
133
|
);
|
|
39
|
-
const
|
|
134
|
+
const [open, setOpen] = useState(focusRequested);
|
|
135
|
+
const triggerRef = useRef<HTMLButtonElement | null>(null);
|
|
136
|
+
const openedFocusId = useRef<string | null>(focusRequested ? (focusAnnotationId ?? null) : null);
|
|
137
|
+
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
if (!focusRequested || !focusAnnotationId) {
|
|
140
|
+
if (!focusRequested) openedFocusId.current = null;
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (openedFocusId.current === focusAnnotationId) return;
|
|
144
|
+
openedFocusId.current = focusAnnotationId;
|
|
145
|
+
setOpen(true);
|
|
146
|
+
}, [focusAnnotationId, focusRequested]);
|
|
147
|
+
|
|
148
|
+
if (annotations.length === 0) return null;
|
|
40
149
|
const countLabel = `${annotations.length} ${annotations.length === 1 ? "annotation" : "annotations"}`;
|
|
150
|
+
const incomplete = annotations.some((annotation) => !annotationHasNote(annotation.note));
|
|
151
|
+
const canClear = Boolean(editable && onRemove);
|
|
152
|
+
|
|
153
|
+
const dismiss = (restoreFocus: boolean) => {
|
|
154
|
+
setOpen(false);
|
|
155
|
+
onFocusConsumed?.();
|
|
156
|
+
if (restoreFocus) triggerRef.current?.focus();
|
|
157
|
+
};
|
|
41
158
|
|
|
42
159
|
return (
|
|
43
|
-
|
|
44
|
-
<
|
|
45
|
-
ref={triggerRef}
|
|
46
|
-
type="button"
|
|
160
|
+
<span className={cn("inline-flex max-w-full items-center", className)}>
|
|
161
|
+
<span
|
|
47
162
|
className={cn(
|
|
48
|
-
"inline-flex
|
|
49
|
-
|
|
163
|
+
"inline-flex max-w-full items-center rounded-full border bg-og-surface-1 text-og-fg-muted",
|
|
164
|
+
incomplete ? "border-og-accent/45 bg-og-accent-soft/50" : "border-og-border",
|
|
50
165
|
)}
|
|
51
|
-
aria-label={`Review ${countLabel}`}
|
|
52
|
-
aria-haspopup="dialog"
|
|
53
|
-
aria-expanded={visible}
|
|
54
|
-
onClick={() => setOpen((current) => !current)}
|
|
55
166
|
>
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
167
|
+
<button
|
|
168
|
+
ref={triggerRef}
|
|
169
|
+
type="button"
|
|
170
|
+
className={cn(
|
|
171
|
+
"inline-flex min-h-8 min-w-0 max-w-full items-center gap-1.5 rounded-full border-0 bg-transparent py-1 text-og-sm font-medium outline-hidden transition hover:text-og-fg focus-visible:ring-2 focus-visible:ring-og-accent pointer-coarse:min-h-[44px]",
|
|
172
|
+
canClear ? "pr-1 pl-2.5" : "px-2.5",
|
|
173
|
+
)}
|
|
174
|
+
aria-label={`Review ${countLabel}`}
|
|
175
|
+
aria-haspopup="dialog"
|
|
176
|
+
aria-expanded={open}
|
|
177
|
+
onClick={() => setOpen((current) => !current)}
|
|
178
|
+
>
|
|
179
|
+
<QuoteIcon className="size-3.5 shrink-0 text-og-accent" aria-hidden="true" />
|
|
180
|
+
<span className="min-w-0 truncate">{countLabel}</span>
|
|
181
|
+
</button>
|
|
182
|
+
{canClear ? (
|
|
183
|
+
<button
|
|
184
|
+
type="button"
|
|
185
|
+
className="inline-flex size-7 shrink-0 items-center justify-center rounded-full border-0 bg-transparent text-og-fg-subtle outline-hidden hover:text-og-status-failed focus-visible:ring-2 focus-visible:ring-og-accent pointer-coarse:size-11"
|
|
186
|
+
aria-label="Remove all annotations"
|
|
187
|
+
onClick={() => {
|
|
188
|
+
for (const annotation of annotations) onRemove?.(annotation.id);
|
|
76
189
|
}}
|
|
77
|
-
|
|
78
|
-
|
|
190
|
+
>
|
|
191
|
+
<XIcon className="size-3.5" aria-hidden="true" />
|
|
192
|
+
</button>
|
|
193
|
+
) : null}
|
|
194
|
+
</span>
|
|
195
|
+
{open ? (
|
|
196
|
+
<AnnotationSourceRootBridge triggerRef={triggerRef}>
|
|
197
|
+
<Suspense fallback={null}>
|
|
198
|
+
<TimelineAnnotationsDialog
|
|
199
|
+
annotations={annotations}
|
|
200
|
+
editable={editable}
|
|
201
|
+
focusAnnotationId={focusAnnotationId}
|
|
202
|
+
onFocusConsumed={onFocusConsumed}
|
|
203
|
+
onUpdate={onUpdate}
|
|
204
|
+
onRemove={onRemove}
|
|
205
|
+
onRevealSource={onRevealSource}
|
|
206
|
+
triggerRef={triggerRef}
|
|
207
|
+
countLabel={countLabel}
|
|
208
|
+
onDismiss={dismiss}
|
|
209
|
+
/>
|
|
210
|
+
</Suspense>
|
|
211
|
+
</AnnotationSourceRootBridge>
|
|
79
212
|
) : null}
|
|
80
|
-
|
|
213
|
+
</span>
|
|
81
214
|
);
|
|
82
215
|
}
|
|
83
216
|
|
|
@@ -424,6 +424,8 @@ export type ComposerState = {
|
|
|
424
424
|
/** Newly captured annotation the review surface should focus. */
|
|
425
425
|
annotationReviewTargetId?: string | null | undefined;
|
|
426
426
|
clearAnnotationReviewTarget?: (() => void) | undefined;
|
|
427
|
+
/** Focus an annotation, or the first incomplete note when omitted. */
|
|
428
|
+
requestAnnotationReview?: ((id?: string) => void) | undefined;
|
|
427
429
|
/** Read the current draft synchronously before a destructive replacement. */
|
|
428
430
|
hasDraftContent: () => boolean;
|
|
429
431
|
/** Append the draft behind prompts already visible in the queue. */
|
|
@@ -2458,6 +2460,20 @@ export function useComposer(
|
|
|
2458
2460
|
setAnnotationReviewTargetId(null);
|
|
2459
2461
|
}, []);
|
|
2460
2462
|
|
|
2463
|
+
const requestAnnotationReview = useCallback((id?: string) => {
|
|
2464
|
+
const current = annotationsRef.current;
|
|
2465
|
+
if (current.length === 0) {
|
|
2466
|
+
setAnnotationReviewTargetId(null);
|
|
2467
|
+
return;
|
|
2468
|
+
}
|
|
2469
|
+
if (id && current.some((annotation) => annotation.id === id)) {
|
|
2470
|
+
setAnnotationReviewTargetId(id);
|
|
2471
|
+
return;
|
|
2472
|
+
}
|
|
2473
|
+
const incomplete = current.find((annotation) => annotation.note.trim().length === 0);
|
|
2474
|
+
setAnnotationReviewTargetId(incomplete?.id ?? current[0]!.id);
|
|
2475
|
+
}, []);
|
|
2476
|
+
|
|
2461
2477
|
const updatePolicy = useCallback(
|
|
2462
2478
|
(next: ComposerPolicy): void => {
|
|
2463
2479
|
if (targetKeyRef.current !== targetKey) return;
|
|
@@ -2643,6 +2659,7 @@ export function useComposer(
|
|
|
2643
2659
|
removeAnnotation,
|
|
2644
2660
|
annotationReviewTargetId: identityMatches ? annotationReviewTargetId : null,
|
|
2645
2661
|
clearAnnotationReviewTarget,
|
|
2662
|
+
requestAnnotationReview,
|
|
2646
2663
|
hasDraftContent,
|
|
2647
2664
|
send,
|
|
2648
2665
|
optimisticMessages: visibleOptimisticMessages,
|