@opengeni/react 3.8.0-canary.0 → 4.0.1-canary.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 +11 -24
- package/dist/artifacts.js +7 -7
- package/dist/{browser-viewer-SS46LUAC.js → browser-viewer-NTD6UIPR.js} +3 -3
- package/dist/{chunk-VEPNNDNG.js → chunk-24M4YBCL.js} +4 -4
- 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-ELIYWBZR.js → chunk-BV5A4OTE.js} +4 -4
- package/dist/{chunk-3HIF5IFC.js → chunk-HO6X4FHG.js} +2597 -1030
- package/dist/chunk-HO6X4FHG.js.map +1 -0
- package/dist/{chunk-BXJOQFCD.js → chunk-HUMIRRYI.js} +84 -221
- package/dist/chunk-HUMIRRYI.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-YBM6CQRU.js +292 -0
- package/dist/chunk-YBM6CQRU.js.map +1 -0
- package/dist/{chunk-BQGIXRKV.js → chunk-Z7WBDB4E.js} +416 -290
- package/dist/chunk-Z7WBDB4E.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/human-input-form.d.ts +3 -1
- package/dist/components/human-input-surface.d.ts +3 -2
- package/dist/components/message-timeline.d.ts +9 -2
- package/dist/components/session-conversation.d.ts +2 -0
- 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 +3 -4
- package/dist/{computer-viewer-FWEZISW4.js → computer-viewer-7TEZC6Y2.js} +3 -3
- package/dist/hooks/use-codex-accounts.d.ts +11 -1
- package/dist/hooks/use-composer.d.ts +2 -0
- package/dist/hooks/use-session-events.d.ts +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +50 -37
- package/dist/index.js.map +1 -1
- package/dist/interaction.js +3 -3
- package/dist/machines.js +1 -1
- package/dist/older-history.d.ts +3 -2
- package/dist/{platform-activity-row-BJHUHXAT.js → platform-activity-row-27LKAWZD.js} +2 -2
- package/dist/platform-activity-row-27LKAWZD.js.map +1 -0
- 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/realtime.js +3 -3
- package/dist/session-ui.js +11 -11
- package/dist/session.js +2 -2
- package/dist/timeline/types.d.ts +3 -0
- 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 -6
- 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/human-input-form.tsx +185 -53
- package/src/components/human-input-surface.tsx +3 -0
- package/src/components/machines-dashboard.tsx +1 -16
- package/src/components/message-timeline.tsx +397 -302
- package/src/components/session-commands-panel.tsx +7 -1
- package/src/components/session-conversation.tsx +4 -1
- 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-codex-accounts.ts +32 -5
- package/src/hooks/use-composer.ts +17 -0
- package/src/hooks/use-session-events.ts +16 -277
- package/src/index.ts +0 -1
- package/src/older-history.ts +22 -4
- package/src/timeline/platform-activity-row.tsx +4 -1
- package/src/timeline/projection.ts +99 -9
- package/src/timeline/types.ts +3 -0
- package/styles/compiled.css +86 -22
- package/dist/chat.d.ts +0 -37
- package/dist/chat.js +0 -315
- package/dist/chat.js.map +0 -1
- package/dist/chunk-3HIF5IFC.js.map +0 -1
- package/dist/chunk-BQGIXRKV.js.map +0 -1
- package/dist/chunk-BXJOQFCD.js.map +0 -1
- package/dist/chunk-F2CFHN3Y.js +0 -1415
- package/dist/chunk-F2CFHN3Y.js.map +0 -1
- package/dist/chunk-HQV2I5HV.js +0 -124
- package/dist/chunk-HQV2I5HV.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/platform-activity-row-BJHUHXAT.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
- package/src/chat.tsx +0 -393
- /package/dist/{browser-viewer-SS46LUAC.js.map → browser-viewer-NTD6UIPR.js.map} +0 -0
- /package/dist/{chunk-VEPNNDNG.js.map → chunk-24M4YBCL.js.map} +0 -0
- /package/dist/{chunk-ELIYWBZR.js.map → chunk-BV5A4OTE.js.map} +0 -0
- /package/dist/{computer-viewer-FWEZISW4.js.map → computer-viewer-7TEZC6Y2.js.map} +0 -0
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import type { DraftTimelineAnnotation } from "@opengeni/sdk";
|
|
2
|
+
import { QuoteIcon } from "lucide-react";
|
|
2
3
|
import { useEffect, useRef, useState, type RefObject } from "react";
|
|
3
4
|
import { createPortal } from "react-dom";
|
|
5
|
+
import { usePortalTokenStyle } from "../lib/use-portal-token-style";
|
|
4
6
|
import type { TimelineAnnotationSourceDescriptor } from "../timeline";
|
|
7
|
+
import {
|
|
8
|
+
clipRangeToSource,
|
|
9
|
+
matchingQuoteInSource,
|
|
10
|
+
occurrenceOffsets,
|
|
11
|
+
quoteOccurrenceIndex,
|
|
12
|
+
truncateAnnotationQuote,
|
|
13
|
+
visibleClientRect,
|
|
14
|
+
} from "./timeline-annotation-shared";
|
|
5
15
|
|
|
6
16
|
type SelectionCandidate = {
|
|
7
17
|
annotation: DraftTimelineAnnotation;
|
|
@@ -10,10 +20,11 @@ type SelectionCandidate = {
|
|
|
10
20
|
keyboard: boolean;
|
|
11
21
|
};
|
|
12
22
|
|
|
13
|
-
const
|
|
14
|
-
'a,button,input,textarea,select,summary,[role="button"],[contenteditable="true"],[tabindex]:not([tabindex="-1"])';
|
|
23
|
+
const CONTROL_SELECTOR = "button,input,textarea,select,summary,[contenteditable='true']";
|
|
15
24
|
const SOURCE_CONTEXT_BYTES = 160;
|
|
16
25
|
const MAX_QUOTE_BYTES = 16 * 1024;
|
|
26
|
+
const POPOVER_WIDTH = 280;
|
|
27
|
+
const POPOVER_HEIGHT = 64;
|
|
17
28
|
|
|
18
29
|
function utf8Prefix(value: string, maxBytes: number): string {
|
|
19
30
|
let output = "";
|
|
@@ -49,33 +60,21 @@ function containingSource(node: Node | null): HTMLElement | null {
|
|
|
49
60
|
return source instanceof HTMLElement ? source : null;
|
|
50
61
|
}
|
|
51
62
|
|
|
52
|
-
function
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
|
|
63
|
+
function resolveSourceBoundary(range: Range, root: HTMLElement): HTMLElement | null {
|
|
64
|
+
const start = containingSource(range.startContainer);
|
|
65
|
+
const end = containingSource(range.endContainer);
|
|
66
|
+
if (start && end && start !== end) return null;
|
|
67
|
+
const boundary = start ?? end;
|
|
68
|
+
if (!boundary || !root.contains(boundary)) return null;
|
|
69
|
+
return boundary;
|
|
56
70
|
}
|
|
57
71
|
|
|
58
|
-
function
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
offsets.push(offset);
|
|
65
|
-
cursor = offset + 1;
|
|
66
|
-
}
|
|
67
|
-
return offsets;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function selectedOccurrence(boundary: HTMLElement, range: Range, quote: string): number {
|
|
71
|
-
try {
|
|
72
|
-
const prefix = range.cloneRange();
|
|
73
|
-
prefix.selectNodeContents(boundary);
|
|
74
|
-
prefix.setEnd(range.startContainer, range.startOffset);
|
|
75
|
-
return occurrenceOffsets(prefix.toString(), quote).length;
|
|
76
|
-
} catch {
|
|
77
|
-
return 0;
|
|
78
|
-
}
|
|
72
|
+
function isWhollyInsideControl(range: Range, boundary: HTMLElement): boolean {
|
|
73
|
+
const start = selectionElement(range.startContainer)?.closest(CONTROL_SELECTOR);
|
|
74
|
+
const end = selectionElement(range.endContainer)?.closest(CONTROL_SELECTOR);
|
|
75
|
+
return (
|
|
76
|
+
start instanceof Element && start === end && boundary.contains(start) && start !== boundary
|
|
77
|
+
);
|
|
79
78
|
}
|
|
80
79
|
|
|
81
80
|
function annotationId(): string {
|
|
@@ -85,6 +84,18 @@ function annotationId(): string {
|
|
|
85
84
|
return `annotation-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
86
85
|
}
|
|
87
86
|
|
|
87
|
+
function clamp(value: number, min: number, max: number): number {
|
|
88
|
+
return Math.min(Math.max(value, min), max);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function popoverPosition(rect: DOMRect): { left: number; top: number } {
|
|
92
|
+
const half = POPOVER_WIDTH / 2;
|
|
93
|
+
const left = clamp(rect.left + rect.width / 2, 12 + half, window.innerWidth - 12 - half);
|
|
94
|
+
const preferBelow = rect.bottom + 8 + POPOVER_HEIGHT <= window.innerHeight - 12;
|
|
95
|
+
const top = preferBelow ? rect.bottom + 8 : Math.max(12, rect.top - POPOVER_HEIGHT - 8);
|
|
96
|
+
return { left, top };
|
|
97
|
+
}
|
|
98
|
+
|
|
88
99
|
function buildCandidate(
|
|
89
100
|
root: HTMLElement,
|
|
90
101
|
sources: ReadonlyMap<string, TimelineAnnotationSourceDescriptor>,
|
|
@@ -93,18 +104,15 @@ function buildCandidate(
|
|
|
93
104
|
const selection = document.getSelection();
|
|
94
105
|
if (!selection || selection.rangeCount !== 1 || selection.isCollapsed) return null;
|
|
95
106
|
const range = selection.getRangeAt(0);
|
|
96
|
-
const startBoundary =
|
|
97
|
-
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
isInteractiveSelection(range.endContainer, startBoundary)
|
|
102
|
-
) {
|
|
103
|
-
return null;
|
|
104
|
-
}
|
|
107
|
+
const startBoundary = resolveSourceBoundary(range, root);
|
|
108
|
+
if (!startBoundary) return null;
|
|
109
|
+
if (isWhollyInsideControl(range, startBoundary)) return null;
|
|
110
|
+
const clipped = clipRangeToSource(range, startBoundary);
|
|
111
|
+
if (!clipped) return null;
|
|
105
112
|
const sourceKey = startBoundary.dataset.ogAnnotationSourceKey;
|
|
106
113
|
const source = sourceKey ? sources.get(sourceKey) : undefined;
|
|
107
|
-
const
|
|
114
|
+
const rawQuote = clipped.toString();
|
|
115
|
+
const quote = (source ? matchingQuoteInSource(source.text, rawQuote) : null) ?? rawQuote;
|
|
108
116
|
if (
|
|
109
117
|
!source ||
|
|
110
118
|
quote.trim().length === 0 ||
|
|
@@ -114,11 +122,12 @@ function buildCandidate(
|
|
|
114
122
|
}
|
|
115
123
|
const sourceOffsets = occurrenceOffsets(source.text, quote);
|
|
116
124
|
if (sourceOffsets.length === 0) return null;
|
|
117
|
-
const occurrence =
|
|
125
|
+
const occurrence = quoteOccurrenceIndex(startBoundary, clipped, quote);
|
|
118
126
|
const startOffset = sourceOffsets[Math.min(occurrence, sourceOffsets.length - 1)]!;
|
|
119
127
|
const endOffset = startOffset + quote.length;
|
|
120
|
-
const rect = range
|
|
121
|
-
if (rect
|
|
128
|
+
const rect = visibleClientRect(range) ?? visibleClientRect(clipped);
|
|
129
|
+
if (!rect) return null;
|
|
130
|
+
const position = popoverPosition(rect);
|
|
122
131
|
return {
|
|
123
132
|
annotation: {
|
|
124
133
|
id: annotationId(),
|
|
@@ -137,8 +146,8 @@ function buildCandidate(
|
|
|
137
146
|
...(source.label ? { label: source.label } : {}),
|
|
138
147
|
},
|
|
139
148
|
},
|
|
140
|
-
left:
|
|
141
|
-
top:
|
|
149
|
+
left: position.left,
|
|
150
|
+
top: position.top,
|
|
142
151
|
keyboard,
|
|
143
152
|
};
|
|
144
153
|
}
|
|
@@ -154,6 +163,8 @@ export function TimelineAnnotationSelection({
|
|
|
154
163
|
}) {
|
|
155
164
|
const [candidate, setCandidate] = useState<SelectionCandidate | null>(null);
|
|
156
165
|
const buttonRef = useRef<HTMLButtonElement | null>(null);
|
|
166
|
+
const pointerOriginRef = useRef<"unset" | "inside" | "outside">("unset");
|
|
167
|
+
const portalStyle = usePortalTokenStyle(rootRef.current);
|
|
157
168
|
|
|
158
169
|
useEffect(() => {
|
|
159
170
|
const root = rootRef.current;
|
|
@@ -164,7 +175,20 @@ export function TimelineAnnotationSelection({
|
|
|
164
175
|
const update = (keyboard: boolean) => {
|
|
165
176
|
setCandidate(buildCandidate(root, sources, keyboard));
|
|
166
177
|
};
|
|
167
|
-
const
|
|
178
|
+
const onRootPointerUp = (event: PointerEvent) => {
|
|
179
|
+
if (event.target instanceof Node && buttonRef.current?.contains(event.target)) return;
|
|
180
|
+
// Tests may fire only pointerup on the source. A real click that started
|
|
181
|
+
// outside the timeline must not resurrect leftover selection.
|
|
182
|
+
if (pointerOriginRef.current === "outside") return;
|
|
183
|
+
window.setTimeout(() => update(false), 0);
|
|
184
|
+
};
|
|
185
|
+
const onDocumentPointerUp = (event: PointerEvent) => {
|
|
186
|
+
if (event.target instanceof Node && buttonRef.current?.contains(event.target)) return;
|
|
187
|
+
const startedInRoot = pointerOriginRef.current === "inside";
|
|
188
|
+
pointerOriginRef.current = "unset";
|
|
189
|
+
if (!startedInRoot) return;
|
|
190
|
+
window.setTimeout(() => update(false), 0);
|
|
191
|
+
};
|
|
168
192
|
const onKeyUp = (event: KeyboardEvent) => {
|
|
169
193
|
if (event.key === "Escape") {
|
|
170
194
|
setCandidate(null);
|
|
@@ -173,46 +197,67 @@ export function TimelineAnnotationSelection({
|
|
|
173
197
|
window.setTimeout(() => update(true), 0);
|
|
174
198
|
};
|
|
175
199
|
const onPointerDown = (event: PointerEvent) => {
|
|
176
|
-
if (
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
) {
|
|
180
|
-
setCandidate(null);
|
|
200
|
+
if (event.target instanceof Node && buttonRef.current?.contains(event.target)) {
|
|
201
|
+
event.preventDefault();
|
|
202
|
+
return;
|
|
181
203
|
}
|
|
204
|
+
pointerOriginRef.current =
|
|
205
|
+
event.target instanceof Node && root.contains(event.target) ? "inside" : "outside";
|
|
206
|
+
setCandidate(null);
|
|
182
207
|
};
|
|
183
208
|
const onDocumentKeyDown = (event: KeyboardEvent) => {
|
|
184
209
|
if (event.key === "Escape") setCandidate(null);
|
|
185
210
|
};
|
|
186
|
-
|
|
211
|
+
const onSelectionChange = () => {
|
|
212
|
+
if (buttonRef.current && document.activeElement === buttonRef.current) return;
|
|
213
|
+
const selection = document.getSelection();
|
|
214
|
+
if (!selection || selection.isCollapsed) setCandidate(null);
|
|
215
|
+
};
|
|
216
|
+
root.addEventListener("pointerup", onRootPointerUp);
|
|
187
217
|
root.addEventListener("keyup", onKeyUp);
|
|
218
|
+
document.addEventListener("pointerup", onDocumentPointerUp);
|
|
188
219
|
document.addEventListener("pointerdown", onPointerDown);
|
|
189
220
|
document.addEventListener("keydown", onDocumentKeyDown);
|
|
221
|
+
document.addEventListener("selectionchange", onSelectionChange);
|
|
190
222
|
return () => {
|
|
191
|
-
root.removeEventListener("pointerup",
|
|
223
|
+
root.removeEventListener("pointerup", onRootPointerUp);
|
|
192
224
|
root.removeEventListener("keyup", onKeyUp);
|
|
225
|
+
document.removeEventListener("pointerup", onDocumentPointerUp);
|
|
193
226
|
document.removeEventListener("pointerdown", onPointerDown);
|
|
194
227
|
document.removeEventListener("keydown", onDocumentKeyDown);
|
|
228
|
+
document.removeEventListener("selectionchange", onSelectionChange);
|
|
195
229
|
};
|
|
196
|
-
}, [
|
|
230
|
+
}, [onAnnotate, rootRef, sources]);
|
|
197
231
|
|
|
198
232
|
useEffect(() => {
|
|
199
233
|
if (candidate?.keyboard) buttonRef.current?.focus();
|
|
200
234
|
}, [candidate]);
|
|
201
235
|
|
|
202
236
|
if (!candidate || !onAnnotate || typeof document === "undefined") return null;
|
|
237
|
+
const preview = truncateAnnotationQuote(candidate.annotation.quote, 72);
|
|
203
238
|
return createPortal(
|
|
204
239
|
<button
|
|
205
240
|
ref={buttonRef}
|
|
206
241
|
type="button"
|
|
207
|
-
|
|
208
|
-
|
|
242
|
+
data-og-annotation-popover=""
|
|
243
|
+
style={{ left: candidate.left, top: candidate.top, ...portalStyle }}
|
|
244
|
+
className="og-root fixed z-[80] flex max-w-[min(18rem,calc(100vw-1.5rem))] -translate-x-1/2 items-start gap-2 rounded-2xl border border-og-border bg-og-surface-1 px-3 py-2 text-og-sm font-medium text-og-fg shadow-xl outline-hidden transition hover:border-og-accent/40 hover:bg-og-surface-2 focus-visible:ring-2 focus-visible:ring-og-accent pointer-coarse:min-h-[44px]"
|
|
245
|
+
aria-label={`Add a note about “${preview}”`}
|
|
246
|
+
title={candidate.annotation.quote}
|
|
247
|
+
onPointerDown={(event) => event.preventDefault()}
|
|
209
248
|
onClick={() => {
|
|
210
249
|
onAnnotate(candidate.annotation);
|
|
211
250
|
document.getSelection()?.removeAllRanges();
|
|
212
251
|
setCandidate(null);
|
|
213
252
|
}}
|
|
214
253
|
>
|
|
215
|
-
|
|
254
|
+
<QuoteIcon className="mt-0.5 size-3.5 shrink-0 text-og-accent" aria-hidden="true" />
|
|
255
|
+
<span className="min-w-0 text-left">
|
|
256
|
+
<span className="block leading-5">Add note</span>
|
|
257
|
+
<span className="mt-0.5 block truncate text-og-xs font-normal text-og-fg-muted">
|
|
258
|
+
{preview}
|
|
259
|
+
</span>
|
|
260
|
+
</span>
|
|
216
261
|
</button>,
|
|
217
262
|
document.body,
|
|
218
263
|
);
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import type { TimelineAnnotationSource } from "@opengeni/sdk";
|
|
2
|
+
|
|
3
|
+
export const ANNOTATION_CHROME_SELECTOR =
|
|
4
|
+
"[data-og-annotation-chrome], [data-og-annotation-badge], [data-og-annotation-popover]";
|
|
5
|
+
|
|
6
|
+
export function annotationHasNote(note: string): boolean {
|
|
7
|
+
return note.trim().length > 0;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function annotationDisplayOrdinal(
|
|
11
|
+
annotation: { readonly id?: string; readonly ordinal?: number | undefined },
|
|
12
|
+
index: number,
|
|
13
|
+
): number {
|
|
14
|
+
return typeof annotation.ordinal === "number" ? annotation.ordinal : index + 1;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function sourceKindLabel(kind: TimelineAnnotationSource["kind"]): string {
|
|
18
|
+
switch (kind) {
|
|
19
|
+
case "user_message":
|
|
20
|
+
return "Your message";
|
|
21
|
+
case "assistant_message":
|
|
22
|
+
return "Assistant";
|
|
23
|
+
case "tool_output":
|
|
24
|
+
return "Tool output";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function annotationSourceLabel(source: TimelineAnnotationSource): string {
|
|
29
|
+
return source.label?.trim() || sourceKindLabel(source.kind);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function truncateAnnotationQuote(quote: string, maxChars = 160): string {
|
|
33
|
+
const normalized = quote.replace(/\s+/g, " ").trim();
|
|
34
|
+
if (normalized.length <= maxChars) return normalized;
|
|
35
|
+
return `${normalized.slice(0, Math.max(0, maxChars - 1)).trimEnd()}…`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function cssEscapeAttribute(value: string): string {
|
|
39
|
+
if (typeof CSS !== "undefined" && typeof CSS.escape === "function") return CSS.escape(value);
|
|
40
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function isAnnotationChromeNode(node: Node | null): boolean {
|
|
44
|
+
const element = node instanceof Element ? node : node?.parentElement;
|
|
45
|
+
return Boolean(element?.closest(ANNOTATION_CHROME_SELECTOR));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function occurrenceOffsets(text: string, quote: string): number[] {
|
|
49
|
+
if (!quote) return [];
|
|
50
|
+
const offsets: number[] = [];
|
|
51
|
+
let cursor = 0;
|
|
52
|
+
while (cursor <= text.length - quote.length) {
|
|
53
|
+
const offset = text.indexOf(quote, cursor);
|
|
54
|
+
if (offset < 0) break;
|
|
55
|
+
offsets.push(offset);
|
|
56
|
+
cursor = offset + 1;
|
|
57
|
+
}
|
|
58
|
+
return offsets;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function uniqueNonEmpty(values: string[]): string[] {
|
|
62
|
+
const seen = new Set<string>();
|
|
63
|
+
const out: string[] = [];
|
|
64
|
+
for (const value of values) {
|
|
65
|
+
if (!value || seen.has(value)) continue;
|
|
66
|
+
seen.add(value);
|
|
67
|
+
out.push(value);
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function matchingQuoteInSource(sourceText: string, quote: string): string | null {
|
|
73
|
+
for (const candidate of uniqueNonEmpty([quote, quote.trim()])) {
|
|
74
|
+
if (sourceText.includes(candidate)) return candidate;
|
|
75
|
+
}
|
|
76
|
+
const trimmed = quote.trim();
|
|
77
|
+
for (let length = trimmed.length; length >= 8; length--) {
|
|
78
|
+
const slice = trimmed.slice(0, length);
|
|
79
|
+
if (sourceText.includes(slice)) return slice;
|
|
80
|
+
}
|
|
81
|
+
for (let start = 1; start <= trimmed.length - 8; start++) {
|
|
82
|
+
const slice = trimmed.slice(start);
|
|
83
|
+
if (sourceText.includes(slice)) return slice;
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
type AnnotatableSpan = {
|
|
89
|
+
node: Text;
|
|
90
|
+
start: number;
|
|
91
|
+
length: number;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export function annotatableText(root: HTMLElement): { text: string; spans: AnnotatableSpan[] } {
|
|
95
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
|
|
96
|
+
acceptNode(node) {
|
|
97
|
+
if (!(node instanceof Text) || node.data.length === 0) return NodeFilter.FILTER_REJECT;
|
|
98
|
+
if (isAnnotationChromeNode(node)) return NodeFilter.FILTER_REJECT;
|
|
99
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
const spans: AnnotatableSpan[] = [];
|
|
103
|
+
let text = "";
|
|
104
|
+
let current: Node | null;
|
|
105
|
+
while ((current = walker.nextNode())) {
|
|
106
|
+
if (!(current instanceof Text)) continue;
|
|
107
|
+
spans.push({ node: current, start: text.length, length: current.data.length });
|
|
108
|
+
text += current.data;
|
|
109
|
+
}
|
|
110
|
+
return { text, spans };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function pointAt(
|
|
114
|
+
spans: AnnotatableSpan[],
|
|
115
|
+
offset: number,
|
|
116
|
+
stickEnd: boolean,
|
|
117
|
+
): { node: Text; offset: number } | null {
|
|
118
|
+
if (spans.length === 0) return null;
|
|
119
|
+
for (let index = 0; index < spans.length; index++) {
|
|
120
|
+
const span = spans[index]!;
|
|
121
|
+
const local = offset - span.start;
|
|
122
|
+
if (local < 0) continue;
|
|
123
|
+
if (
|
|
124
|
+
local < span.length ||
|
|
125
|
+
(local === span.length && (stickEnd || index === spans.length - 1))
|
|
126
|
+
) {
|
|
127
|
+
return { node: span.node, offset: local };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const last = spans[spans.length - 1]!;
|
|
131
|
+
return { node: last.node, offset: last.length };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function rangeFromOffsets(
|
|
135
|
+
spans: AnnotatableSpan[],
|
|
136
|
+
startOffset: number,
|
|
137
|
+
endOffset: number,
|
|
138
|
+
): Range | null {
|
|
139
|
+
const start = pointAt(spans, startOffset, false);
|
|
140
|
+
const end = pointAt(spans, endOffset, true);
|
|
141
|
+
if (!start || !end) return null;
|
|
142
|
+
const range = document.createRange();
|
|
143
|
+
range.setStart(start.node, start.offset);
|
|
144
|
+
range.setEnd(end.node, end.offset);
|
|
145
|
+
return range.collapsed ? null : range;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function buildQuoteRange(
|
|
149
|
+
sourceEl: HTMLElement,
|
|
150
|
+
quote: string,
|
|
151
|
+
occurrenceIndex = 0,
|
|
152
|
+
): Range | null {
|
|
153
|
+
if (!quote) return null;
|
|
154
|
+
const { text, spans } = annotatableText(sourceEl);
|
|
155
|
+
const offsets = occurrenceOffsets(text, quote);
|
|
156
|
+
if (offsets.length === 0) return null;
|
|
157
|
+
const start = offsets[Math.min(Math.max(0, occurrenceIndex), offsets.length - 1)]!;
|
|
158
|
+
return rangeFromOffsets(spans, start, start + quote.length);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function quoteOccurrenceIndex(sourceEl: HTMLElement, range: Range, quote: string): number {
|
|
162
|
+
try {
|
|
163
|
+
const prefix = range.cloneRange();
|
|
164
|
+
prefix.selectNodeContents(sourceEl);
|
|
165
|
+
prefix.setEnd(range.startContainer, range.startOffset);
|
|
166
|
+
return occurrenceOffsets(prefix.toString(), quote).length;
|
|
167
|
+
} catch {
|
|
168
|
+
return 0;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function clipRangeToSource(range: Range, sourceEl: HTMLElement): Range | null {
|
|
173
|
+
const startOk =
|
|
174
|
+
sourceEl.contains(range.startContainer) && !isAnnotationChromeNode(range.startContainer);
|
|
175
|
+
const endOk =
|
|
176
|
+
sourceEl.contains(range.endContainer) && !isAnnotationChromeNode(range.endContainer);
|
|
177
|
+
if (!startOk && !endOk) return null;
|
|
178
|
+
const { spans } = annotatableText(sourceEl);
|
|
179
|
+
if (spans.length === 0) return null;
|
|
180
|
+
const next = range.cloneRange();
|
|
181
|
+
const first = spans[0]!;
|
|
182
|
+
const last = spans[spans.length - 1]!;
|
|
183
|
+
if (!startOk) next.setStart(first.node, 0);
|
|
184
|
+
if (!endOk) next.setEnd(last.node, last.node.data.length);
|
|
185
|
+
return next.collapsed ? null : next;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function visibleClientRect(range: Range): DOMRect | null {
|
|
189
|
+
try {
|
|
190
|
+
const bounding = range.getBoundingClientRect();
|
|
191
|
+
if (bounding.width > 0 || bounding.height > 0) return bounding;
|
|
192
|
+
} catch {
|
|
193
|
+
// jsdom ranges may omit layout geometry.
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
const rects = range.getClientRects();
|
|
197
|
+
for (let index = 0; index < rects.length; index++) {
|
|
198
|
+
const rect = rects.item(index);
|
|
199
|
+
if (rect && (rect.width > 0 || rect.height > 0)) return rect;
|
|
200
|
+
}
|
|
201
|
+
} catch {
|
|
202
|
+
// jsdom ranges may omit client rects.
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function resolveAnnotationRevealRoot(from: Element | null | undefined): HTMLElement | null {
|
|
208
|
+
const conversation = from?.closest("[data-og-conversation]");
|
|
209
|
+
if (conversation instanceof HTMLElement) {
|
|
210
|
+
const nested = conversation.querySelector("[data-og-timeline-scroller]");
|
|
211
|
+
return nested instanceof HTMLElement ? nested : conversation;
|
|
212
|
+
}
|
|
213
|
+
const scroller = from?.closest("[data-og-timeline-scroller]");
|
|
214
|
+
return scroller instanceof HTMLElement ? scroller : null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function revealLoadedAnnotationSource(
|
|
218
|
+
source: TimelineAnnotationSource,
|
|
219
|
+
root?: ParentNode | null,
|
|
220
|
+
): boolean {
|
|
221
|
+
if (typeof document === "undefined") return false;
|
|
222
|
+
// An explicit null root means the caller already scoped the search and found
|
|
223
|
+
// no owning timeline. Do not fall back to `document`, or a second mounted
|
|
224
|
+
// timeline with the same event id would steal the reveal.
|
|
225
|
+
if (root === null) return false;
|
|
226
|
+
const element = (root ?? document).querySelector(
|
|
227
|
+
`[data-og-annotation-source-key="${cssEscapeAttribute(source.eventId)}"]`,
|
|
228
|
+
);
|
|
229
|
+
if (!(element instanceof HTMLElement)) return false;
|
|
230
|
+
element.scrollIntoView({ block: "center", behavior: "smooth" });
|
|
231
|
+
element.animate?.(
|
|
232
|
+
[
|
|
233
|
+
{
|
|
234
|
+
boxShadow: "inset 0 0 0 2px color-mix(in oklch, var(--og-accent) 70%, transparent)",
|
|
235
|
+
backgroundColor: "color-mix(in oklch, var(--og-accent) 14%, transparent)",
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
boxShadow: "inset 0 0 0 2px transparent",
|
|
239
|
+
backgroundColor: "transparent",
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
{ duration: 1400, easing: "ease-out" },
|
|
243
|
+
);
|
|
244
|
+
return true;
|
|
245
|
+
}
|