@fabrika/canvas-kernel 0.1.45 → 0.3.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 +33 -67
- package/index.ts +20 -83
- package/package.json +69 -72
- package/src/client/AnnotationProvider.tsx +218 -255
- package/src/client/AnnotationSidebar.tsx +393 -338
- package/src/client/FileIcon.tsx +105 -66
- package/src/client/PlanRenderer.tsx +607 -551
- package/src/client/RenderErrorContext.ts +7 -7
- package/src/client/ResponsePreview.tsx +192 -173
- package/src/client/annotationDom.ts +43 -97
- package/src/client/annotationDraft.ts +106 -96
- package/src/client/appContext.tsx +40 -40
- package/src/client/components/Callout.tsx +20 -20
- package/src/client/components/Checklist.tsx +27 -29
- package/src/client/components/Choice.tsx +178 -159
- package/src/client/components/CodeBlock.tsx +27 -26
- package/src/client/components/Diff.tsx +16 -17
- package/src/client/components/FilePreview.tsx +102 -74
- package/src/client/components/Image.tsx +41 -39
- package/src/client/components/Markdown.tsx +33 -32
- package/src/client/components/Mermaid.tsx +593 -542
- package/src/client/components/Note.tsx +8 -8
- package/src/client/components/Priority.tsx +10 -12
- package/src/client/components/ResponseNote.tsx +16 -15
- package/src/client/components/Section.tsx +28 -26
- package/src/client/components/Table.tsx +33 -30
- package/src/client/components/Task.tsx +58 -60
- package/src/client/components/UserInput.tsx +169 -152
- package/src/client/components/index.ts +16 -16
- package/src/client/components/renderMarkdownChildren.tsx +86 -68
- package/src/client/diffLines.ts +51 -39
- package/src/client/generateMarkdown.ts +197 -191
- package/src/client/hostContext.tsx +39 -34
- package/src/client/index.ts +44 -125
- package/src/client/pageGlobals.ts +25 -0
- package/src/client/persistenceSchemas.ts +41 -0
- package/src/client/preact-all.ts +4 -4
- package/src/client/runtime.ts +58 -111
- package/src/client/styles.css +1 -74
- package/src/engine/compiler.ts +411 -323
- package/src/engine/file-admission.ts +25 -0
- package/src/engine/paths.ts +44 -44
- package/src/engine/session.ts +515 -626
- package/src/engine/sessionSchemas.ts +71 -0
- package/src/engine/watcher.ts +70 -60
- package/src/engine/websocket.ts +134 -132
- package/annotate.ts +0 -94
- package/daemon.ts +0 -46
- package/src/client/AnnotationDraftFooter.tsx +0 -76
- package/src/client/AnnotationEditor.tsx +0 -320
- package/src/client/AnnotationList.tsx +0 -190
- package/src/client/Popover.tsx +0 -361
- package/src/client/SessionContext.tsx +0 -3
- package/src/client/annotationContext.ts +0 -130
- package/src/client/annotationMarkdown.ts +0 -102
- package/src/client/annotationTarget.ts +0 -73
- package/src/client/blockTarget.ts +0 -127
- package/src/client/highlightRange.ts +0 -262
- package/src/client/popoverPosition.ts +0 -46
- package/src/client/regionTarget.ts +0 -180
- package/src/client/testing/dom.ts +0 -74
- package/src/client/textTarget.ts +0 -31
- package/src/client/useRegionAnnotation.tsx +0 -188
- package/src/client/useTextAnnotation.tsx +0 -194
- package/src/client/utils.ts +0 -33
- package/src/engine/channel.ts +0 -278
- package/src/engine/http-utils.ts +0 -14
- package/src/engine/lifecycle.ts +0 -302
- package/src/engine/router.ts +0 -16
- package/src/engine/waiter.ts +0 -188
|
@@ -1,55 +1,58 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
1
|
+
import { AnnotationCtx } from '#canvas/runtime'
|
|
2
|
+
import type { Annotation, AnnotationContext, AnnotationContextValue, FeedbackEntry, PlanResponse } from '#canvas/runtime'
|
|
3
|
+
import { useAnnotationState } from '@fabrika/annotations'
|
|
4
|
+
import { AnnotationCtx as SurfaceAnnotationCtx, AnnotationHostContext } from '@fabrika/annotations/runtime'
|
|
5
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
6
|
+
import { annotationDraftKey, type AnnotationDraftPhase, clearPersistedDraft } from './annotationDraft'
|
|
7
|
+
import { canPruneResponses, pruneStaleResponses } from './generateMarkdown'
|
|
8
|
+
import { useCanvasHost } from './hostContext'
|
|
9
|
+
import { parsePersistedState } from './persistenceSchemas'
|
|
7
10
|
|
|
8
11
|
// Re-export types for convenience
|
|
9
|
-
export type { Annotation, AnnotationContext,
|
|
10
|
-
export { useAnnotations } from
|
|
12
|
+
export type { Annotation, AnnotationContext, AnnotationContextValue, FeedbackEntry, PlanResponse }
|
|
13
|
+
export { useAnnotations } from '#canvas/runtime'
|
|
11
14
|
|
|
12
15
|
export interface PersistedState {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
annotations: Annotation[]
|
|
17
|
+
generalNote: string
|
|
18
|
+
responses: [string, PlanResponse][]
|
|
19
|
+
feedbackEntries?: [string, FeedbackEntry][]
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
interface AnnotationProviderProps {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
23
|
+
sessionId: string
|
|
24
|
+
revision: number
|
|
25
|
+
isReadOnly: boolean
|
|
26
|
+
draftPhase: AnnotationDraftPhase
|
|
27
|
+
/** Remote annotations fetched from shared views. Merged read-only into
|
|
28
|
+
* the annotation list so they render alongside the author's own. */
|
|
29
|
+
remoteAnnotations?: Annotation[]
|
|
30
|
+
/** Canvas files this revision is made of. Supplying it enables pruning of
|
|
31
|
+
* answers to questions the revision no longer asks; omit it and every
|
|
32
|
+
* answer is submitted, which is what a host without canvas tabs wants. */
|
|
33
|
+
canvasFiles?: readonly string[]
|
|
34
|
+
/** Server-authoritative persistence. When supplied, the draft is loaded and
|
|
35
|
+
* saved through these instead of localStorage, so it survives reloads and
|
|
36
|
+
* follows the author across browsers. Omit for the localStorage default. */
|
|
37
|
+
loadState?: (sessionId: string, revision: number, phase: AnnotationDraftPhase) => Promise<PersistedState | null>
|
|
38
|
+
saveState?: (sessionId: string, revision: number, state: PersistedState, phase: AnnotationDraftPhase) => void
|
|
39
|
+
children: React.ReactNode
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
function loadPersisted(sessionId: string, revision: number, phase: AnnotationDraftPhase): PersistedState | null {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
try {
|
|
44
|
+
const raw = localStorage.getItem(annotationDraftKey(sessionId, revision, phase))
|
|
45
|
+
if (!raw) return null
|
|
46
|
+
return parsePersistedState(JSON.parse(raw))
|
|
47
|
+
} catch {
|
|
48
|
+
return null
|
|
49
|
+
}
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
function savePersisted(sessionId: string, revision: number, phase: AnnotationDraftPhase, state: PersistedState) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
try {
|
|
54
|
+
localStorage.setItem(annotationDraftKey(sessionId, revision, phase), JSON.stringify(state))
|
|
55
|
+
} catch {}
|
|
53
56
|
}
|
|
54
57
|
|
|
55
58
|
/** What the reader has been shown for one draft. Stamped with the draft it
|
|
@@ -57,238 +60,198 @@ function savePersisted(sessionId: string, revision: number, phase: AnnotationDra
|
|
|
57
60
|
* their own effects, which run before the provider's, so an effect-based reset
|
|
58
61
|
* would wipe registrations that already happened. */
|
|
59
62
|
interface SeenState {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
draft: string
|
|
64
|
+
responseIds: Set<string>
|
|
65
|
+
canvases: Set<string>
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
function seenFor(draft: string): SeenState {
|
|
66
|
-
|
|
69
|
+
return { draft, responseIds: new Set(), canvases: new Set() }
|
|
67
70
|
}
|
|
68
71
|
|
|
69
|
-
export function AnnotationProvider(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
export function AnnotationProvider(
|
|
73
|
+
{ sessionId, revision, isReadOnly, draftPhase, remoteAnnotations, canvasFiles, loadState, saveState, children }: AnnotationProviderProps,
|
|
74
|
+
) {
|
|
75
|
+
// Server-authoritative persistence loads asynchronously, so that mode starts
|
|
76
|
+
// empty and hydrates in an effect. localStorage mode seeds synchronously.
|
|
77
|
+
const serverMode = !!loadState
|
|
78
|
+
const seed = serverMode ? null : loadPersisted(sessionId, revision, draftPhase)
|
|
79
|
+
const savedState = useRef<PersistedState | null>(seed)
|
|
74
80
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
81
|
+
const [localAnnotations, setAnnotations] = useState<Annotation[]>(() =>
|
|
82
|
+
(seed?.annotations ?? []).map((a) => ({ ...a, source: a.source ?? 'local' as const }))
|
|
83
|
+
)
|
|
78
84
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const [generalNote, setGeneralNote] = useState(() => seed?.generalNote ?? "");
|
|
89
|
-
const [activeAnnotationId, setActiveAnnotationId] = useState<string | null>(null);
|
|
90
|
-
const [responses, setResponses] = useState<Map<string, PlanResponse>>(() =>
|
|
91
|
-
seed?.responses ? new Map(seed.responses) : new Map(),
|
|
92
|
-
);
|
|
93
|
-
const draftId = `${sessionId}:${revision}:${draftPhase}`;
|
|
94
|
-
const draftIdRef = useRef(draftId);
|
|
95
|
-
draftIdRef.current = draftId;
|
|
96
|
-
const [seen, setSeen] = useState<SeenState>(() => seenFor(draftId));
|
|
97
|
-
const [feedbackEntries, setFeedbackEntries] = useState<Map<string, FeedbackEntry>>(() =>
|
|
98
|
-
seed?.feedbackEntries ? new Map(seed.feedbackEntries) : new Map(),
|
|
99
|
-
);
|
|
85
|
+
const [generalNote, setGeneralNote] = useState(() => seed?.generalNote ?? '')
|
|
86
|
+
const [responses, setResponses] = useState<Map<string, PlanResponse>>(() => seed?.responses ? new Map(seed.responses) : new Map())
|
|
87
|
+
const draftId = `${sessionId}:${revision}:${draftPhase}`
|
|
88
|
+
const draftIdRef = useRef(draftId)
|
|
89
|
+
draftIdRef.current = draftId
|
|
90
|
+
const [seen, setSeen] = useState<SeenState>(() => seenFor(draftId))
|
|
91
|
+
const [feedbackEntries, setFeedbackEntries] = useState<Map<string, FeedbackEntry>>(() =>
|
|
92
|
+
seed?.feedbackEntries ? new Map(seed.feedbackEntries) : new Map()
|
|
93
|
+
)
|
|
100
94
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
95
|
+
// Re-hydrate when the draft identity changes. Hosts that remount the provider
|
|
96
|
+
// per revision (via a key) get this for free from the initializers, but hosts
|
|
97
|
+
// that keep it mounted need it here — otherwise one revision's answers leak
|
|
98
|
+
// into the next and the persist effect writes them over the new revision's
|
|
99
|
+
// saved draft. `hydrated` gates that effect so an async server load cannot be
|
|
100
|
+
// clobbered by the empty initial state.
|
|
101
|
+
const [hydrated, setHydrated] = useState(!serverMode)
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
const apply = (saved: PersistedState | null) => {
|
|
104
|
+
savedState.current = saved
|
|
105
|
+
setAnnotations((saved?.annotations ?? []).map((a) => ({ ...a, source: a.source ?? 'local' as const })))
|
|
106
|
+
setGeneralNote(saved?.generalNote ?? '')
|
|
107
|
+
setResponses(saved?.responses ? new Map(saved.responses) : new Map())
|
|
108
|
+
setFeedbackEntries(saved?.feedbackEntries ? new Map(saved.feedbackEntries) : new Map())
|
|
109
|
+
setActiveAnnotationId(null)
|
|
110
|
+
}
|
|
111
|
+
if (serverMode) {
|
|
112
|
+
let cancelled = false
|
|
113
|
+
setHydrated(false)
|
|
114
|
+
loadState!(sessionId, revision, draftPhase).then((saved) => {
|
|
115
|
+
if (cancelled) return
|
|
116
|
+
apply(parsePersistedState(saved))
|
|
117
|
+
setHydrated(true)
|
|
118
|
+
})
|
|
119
|
+
return () => {
|
|
120
|
+
cancelled = true
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
apply(loadPersisted(sessionId, revision, draftPhase))
|
|
124
|
+
setHydrated(true)
|
|
125
|
+
return undefined
|
|
126
|
+
}, [sessionId, revision, draftPhase, serverMode, loadState])
|
|
130
127
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
128
|
+
// Post-feedback drafts save immediately so a new revision can carry the latest edit.
|
|
129
|
+
const persistTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
if (isReadOnly && draftPhase !== 'next') return
|
|
132
|
+
if (!hydrated) return
|
|
133
|
+
if (persistTimerRef.current) clearTimeout(persistTimerRef.current)
|
|
134
|
+
const state: PersistedState = {
|
|
135
|
+
...savedState.current,
|
|
136
|
+
// Persist only local annotations — remote ones are re-fetched on load.
|
|
137
|
+
annotations: localAnnotations,
|
|
138
|
+
generalNote,
|
|
139
|
+
responses: Array.from(responses.entries()),
|
|
140
|
+
feedbackEntries: Array.from(feedbackEntries.entries()),
|
|
141
|
+
}
|
|
142
|
+
const persist = () => {
|
|
143
|
+
if (saveState) saveState(sessionId, revision, state, draftPhase)
|
|
144
|
+
else savePersisted(sessionId, revision, draftPhase, state)
|
|
145
|
+
}
|
|
146
|
+
if (draftPhase === 'next') {
|
|
147
|
+
persist()
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
persistTimerRef.current = setTimeout(persist, 300)
|
|
151
|
+
return () => {
|
|
152
|
+
if (persistTimerRef.current) clearTimeout(persistTimerRef.current)
|
|
153
|
+
}
|
|
154
|
+
}, [localAnnotations, generalNote, responses, feedbackEntries, sessionId, revision, isReadOnly, draftPhase, hydrated, saveState])
|
|
155
155
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
156
|
+
const setResponse = useCallback((id: string, response: PlanResponse) => {
|
|
157
|
+
setResponses((prev) => {
|
|
158
|
+
const next = new Map(prev)
|
|
159
|
+
next.set(id, response)
|
|
160
|
+
return next
|
|
161
|
+
})
|
|
162
|
+
}, [])
|
|
159
163
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
164
|
+
// Both registrations accumulate for the life of one draft and are idempotent,
|
|
165
|
+
// so re-running the caller's effect cannot loop.
|
|
166
|
+
const registerResponse = useCallback((id: string) => {
|
|
167
|
+
setSeen((prev) => {
|
|
168
|
+
const base = prev.draft === draftIdRef.current ? prev : seenFor(draftIdRef.current)
|
|
169
|
+
if (base.responseIds.has(id)) return base
|
|
170
|
+
return { ...base, responseIds: new Set(base.responseIds).add(id) }
|
|
171
|
+
})
|
|
172
|
+
}, [])
|
|
163
173
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
174
|
+
const registerCanvasRendered = useCallback((filename: string) => {
|
|
175
|
+
setSeen((prev) => {
|
|
176
|
+
const base = prev.draft === draftIdRef.current ? prev : seenFor(draftIdRef.current)
|
|
177
|
+
if (base.canvases.has(filename)) return base
|
|
178
|
+
return { ...base, canvases: new Set(base.canvases).add(filename) }
|
|
179
|
+
})
|
|
180
|
+
}, [])
|
|
167
181
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
182
|
+
// Answers survive until a canvas proves the question is gone. `seen` can lag
|
|
183
|
+
// the draft by a render when the host keeps the provider mounted across
|
|
184
|
+
// revisions, and stale evidence must never prune.
|
|
185
|
+
const current = seen.draft === draftId ? seen : null
|
|
186
|
+
const submittableResponses = useMemo(() => {
|
|
187
|
+
if (!current || !canPruneResponses(canvasFiles, current.canvases)) return responses
|
|
188
|
+
return pruneStaleResponses(responses, current.responseIds)
|
|
189
|
+
}, [current, canvasFiles, responses])
|
|
173
190
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
191
|
+
const setFeedbackEntry = useCallback((id: string, entry: FeedbackEntry) => {
|
|
192
|
+
setFeedbackEntries((prev) => {
|
|
193
|
+
const existing = prev.get(id)
|
|
194
|
+
if (existing && existing.markdown === entry.markdown && existing.label === entry.label && existing.required === entry.required) {
|
|
195
|
+
return prev // same reference → no re-render
|
|
196
|
+
}
|
|
197
|
+
const next = new Map(prev)
|
|
198
|
+
next.set(id, entry)
|
|
199
|
+
return next
|
|
200
|
+
})
|
|
201
|
+
}, [])
|
|
179
202
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
203
|
+
const removeFeedbackEntry = useCallback((id: string) => {
|
|
204
|
+
setFeedbackEntries((prev) => {
|
|
205
|
+
if (!prev.has(id)) return prev
|
|
206
|
+
const next = new Map(prev)
|
|
207
|
+
next.delete(id)
|
|
208
|
+
return next
|
|
209
|
+
})
|
|
210
|
+
}, [])
|
|
185
211
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
212
|
+
const clearCanvasState = useCallback(() => {
|
|
213
|
+
setResponses(new Map())
|
|
214
|
+
setFeedbackEntries(new Map())
|
|
215
|
+
if (saveState) {
|
|
216
|
+
saveState(sessionId, revision, { annotations: [], generalNote: '', responses: [], feedbackEntries: [] }, draftPhase)
|
|
217
|
+
} else if (draftPhase === 'next') {
|
|
218
|
+
localStorage.removeItem(annotationDraftKey(sessionId, revision, draftPhase))
|
|
219
|
+
} else {
|
|
220
|
+
clearPersistedDraft(localStorage, sessionId, revision)
|
|
221
|
+
}
|
|
222
|
+
}, [sessionId, revision, draftPhase, saveState])
|
|
193
223
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
224
|
+
const annotationState = useAnnotationState({
|
|
225
|
+
annotations: localAnnotations,
|
|
226
|
+
setAnnotations,
|
|
227
|
+
generalNote,
|
|
228
|
+
setGeneralNote,
|
|
229
|
+
remoteAnnotations,
|
|
230
|
+
isReadOnly,
|
|
231
|
+
onClear: clearCanvasState,
|
|
232
|
+
})
|
|
233
|
+
const { setActiveAnnotationId } = annotationState
|
|
234
|
+
const host = useCanvasHost()
|
|
203
235
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
return prev; // same reference → no re-render
|
|
226
|
-
}
|
|
227
|
-
const next = new Map(prev);
|
|
228
|
-
next.set(id, entry);
|
|
229
|
-
return next;
|
|
230
|
-
});
|
|
231
|
-
}, []);
|
|
232
|
-
|
|
233
|
-
const removeFeedbackEntry = useCallback((id: string) => {
|
|
234
|
-
setFeedbackEntries((prev) => {
|
|
235
|
-
if (!prev.has(id)) return prev;
|
|
236
|
-
const next = new Map(prev);
|
|
237
|
-
next.delete(id);
|
|
238
|
-
return next;
|
|
239
|
-
});
|
|
240
|
-
}, []);
|
|
241
|
-
|
|
242
|
-
const clearAll = useCallback(() => {
|
|
243
|
-
for (const mark of document.querySelectorAll("[data-annotation-id]")) {
|
|
244
|
-
unwrapMark(mark as HTMLElement);
|
|
245
|
-
}
|
|
246
|
-
setAnnotations([]);
|
|
247
|
-
setGeneralNote("");
|
|
248
|
-
setActiveAnnotationId(null);
|
|
249
|
-
setResponses(new Map());
|
|
250
|
-
setFeedbackEntries(new Map());
|
|
251
|
-
if (saveState) {
|
|
252
|
-
saveState(sessionId, revision, { annotations: [], generalNote: "", responses: [], feedbackEntries: [] }, draftPhase);
|
|
253
|
-
} else if (draftPhase === "next") {
|
|
254
|
-
localStorage.removeItem(annotationDraftKey(sessionId, revision, draftPhase));
|
|
255
|
-
} else {
|
|
256
|
-
clearPersistedDraft(localStorage, sessionId, revision);
|
|
257
|
-
}
|
|
258
|
-
}, [sessionId, revision, draftPhase, saveState]);
|
|
259
|
-
|
|
260
|
-
return (
|
|
261
|
-
<AnnotationCtx.Provider
|
|
262
|
-
value={{
|
|
263
|
-
annotations, addAnnotation, addAnnotationWithId, updateAnnotation, removeAnnotation, addAnnotationImage, removeAnnotationImage,
|
|
264
|
-
generalNote, setGeneralNote, clearAll,
|
|
265
|
-
activeAnnotationId, setActiveAnnotationId,
|
|
266
|
-
responses, setResponse, submittableResponses,
|
|
267
|
-
registerResponse, registerCanvasRendered,
|
|
268
|
-
feedbackEntries, setFeedbackEntry, removeFeedbackEntry,
|
|
269
|
-
isReadOnly,
|
|
270
|
-
}}
|
|
271
|
-
>
|
|
272
|
-
{children}
|
|
273
|
-
</AnnotationCtx.Provider>
|
|
274
|
-
);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
function removeMarksFromDom(id: string) {
|
|
278
|
-
const marks = document.querySelectorAll(`[data-annotation-id="${id}"]`);
|
|
279
|
-
for (const mark of marks) unwrapMark(mark as HTMLElement);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
function unwrapMark(mark: HTMLElement) {
|
|
283
|
-
const parent = mark.parentNode;
|
|
284
|
-
if (!parent) return;
|
|
285
|
-
// A region overlay wraps no text; swapping it for its text content would
|
|
286
|
-
// leave a stray empty node inside the image.
|
|
287
|
-
if (mark.tagName !== "MARK") {
|
|
288
|
-
parent.removeChild(mark);
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
const text = document.createTextNode(mark.textContent || "");
|
|
292
|
-
parent.replaceChild(text, mark);
|
|
293
|
-
parent.normalize();
|
|
236
|
+
return (
|
|
237
|
+
<AnnotationHostContext.Provider value={host}>
|
|
238
|
+
<SurfaceAnnotationCtx.Provider value={annotationState}>
|
|
239
|
+
<AnnotationCtx.Provider
|
|
240
|
+
value={{
|
|
241
|
+
...annotationState,
|
|
242
|
+
responses,
|
|
243
|
+
setResponse,
|
|
244
|
+
submittableResponses,
|
|
245
|
+
registerResponse,
|
|
246
|
+
registerCanvasRendered,
|
|
247
|
+
feedbackEntries,
|
|
248
|
+
setFeedbackEntry,
|
|
249
|
+
removeFeedbackEntry,
|
|
250
|
+
}}
|
|
251
|
+
>
|
|
252
|
+
{children}
|
|
253
|
+
</AnnotationCtx.Provider>
|
|
254
|
+
</SurfaceAnnotationCtx.Provider>
|
|
255
|
+
</AnnotationHostContext.Provider>
|
|
256
|
+
)
|
|
294
257
|
}
|