@d34dman/flowdrop 0.0.61 → 0.0.62

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.
Files changed (204) hide show
  1. package/README.md +6 -0
  2. package/dist/adapters/WorkflowAdapter.d.ts +1 -1
  3. package/dist/adapters/agentspec/AgentSpecAdapter.js +3 -1
  4. package/dist/api/client.d.ts +4 -0
  5. package/dist/api/client.js +6 -1
  6. package/dist/api/enhanced-client.js +7 -6
  7. package/dist/components/App.svelte +143 -219
  8. package/dist/components/CanvasBanner.stories.svelte +25 -0
  9. package/dist/components/CanvasBanner.stories.svelte.d.ts +27 -0
  10. package/dist/components/CanvasBanner.svelte +2 -2
  11. package/dist/components/ConfigForm.svelte +37 -36
  12. package/dist/components/ConfigPanel.stories.svelte +38 -0
  13. package/dist/components/ConfigPanel.stories.svelte.d.ts +27 -0
  14. package/dist/components/ConfigPanel.svelte +2 -2
  15. package/dist/components/ConnectionLine.svelte +2 -2
  16. package/dist/components/FlowDropZone.svelte +18 -2
  17. package/dist/components/FlowDropZone.svelte.d.ts +2 -0
  18. package/dist/components/LoadingSpinner.stories.svelte +30 -0
  19. package/dist/components/LoadingSpinner.stories.svelte.d.ts +27 -0
  20. package/dist/components/Logo.stories.svelte +22 -0
  21. package/dist/components/Logo.stories.svelte.d.ts +27 -0
  22. package/dist/components/Logo.svelte +33 -13
  23. package/dist/components/Logo.svelte.d.ts +1 -1
  24. package/dist/components/MarkdownDisplay.stories.svelte +21 -0
  25. package/dist/components/MarkdownDisplay.stories.svelte.d.ts +27 -0
  26. package/dist/components/MarkdownDisplay.svelte +4 -3
  27. package/dist/components/Navbar.stories.svelte +41 -0
  28. package/dist/components/Navbar.stories.svelte.d.ts +27 -0
  29. package/dist/components/Navbar.svelte +4 -4
  30. package/dist/components/NodeSidebar.svelte +12 -12
  31. package/dist/components/NodeStatusOverlay.stories.svelte +74 -0
  32. package/dist/components/NodeStatusOverlay.stories.svelte.d.ts +27 -0
  33. package/dist/components/PipelineStatus.svelte +11 -4
  34. package/dist/components/PortCoordinateTracker.svelte +1 -1
  35. package/dist/components/SchemaForm.stories.svelte +101 -0
  36. package/dist/components/SchemaForm.stories.svelte.d.ts +27 -0
  37. package/dist/components/SchemaForm.svelte +17 -12
  38. package/dist/components/SettingsModal.svelte +3 -3
  39. package/dist/components/SettingsPanel.svelte +23 -22
  40. package/dist/components/StatusIcon.stories.svelte +60 -0
  41. package/dist/components/StatusIcon.stories.svelte.d.ts +27 -0
  42. package/dist/components/StatusIcon.svelte +7 -0
  43. package/dist/components/StatusLabel.stories.svelte +17 -0
  44. package/dist/components/StatusLabel.stories.svelte.d.ts +27 -0
  45. package/dist/components/ThemeToggle.stories.svelte +25 -0
  46. package/dist/components/ThemeToggle.stories.svelte.d.ts +27 -0
  47. package/dist/components/ThemeToggle.svelte +8 -8
  48. package/dist/components/UniversalNode.svelte +1 -1
  49. package/dist/components/WorkflowEditor.svelte +298 -294
  50. package/dist/components/form/FormAutocomplete.svelte +20 -19
  51. package/dist/components/form/FormCheckboxGroup.stories.svelte +28 -0
  52. package/dist/components/form/FormCheckboxGroup.stories.svelte.d.ts +27 -0
  53. package/dist/components/form/FormField.svelte +3 -3
  54. package/dist/components/form/FormFieldLight.svelte +2 -2
  55. package/dist/components/form/FormFieldWrapper.stories.svelte +31 -0
  56. package/dist/components/form/FormFieldWrapper.stories.svelte.d.ts +27 -0
  57. package/dist/components/form/FormFieldset.svelte +7 -7
  58. package/dist/components/form/FormNumberField.stories.svelte +33 -0
  59. package/dist/components/form/FormNumberField.stories.svelte.d.ts +27 -0
  60. package/dist/components/form/FormRangeField.stories.svelte +31 -0
  61. package/dist/components/form/FormRangeField.stories.svelte.d.ts +27 -0
  62. package/dist/components/form/FormSelect.stories.svelte +50 -0
  63. package/dist/components/form/FormSelect.stories.svelte.d.ts +27 -0
  64. package/dist/components/form/FormTemplateEditor.svelte +2 -1
  65. package/dist/components/form/FormTextField.stories.svelte +30 -0
  66. package/dist/components/form/FormTextField.stories.svelte.d.ts +27 -0
  67. package/dist/components/form/FormTextarea.stories.svelte +31 -0
  68. package/dist/components/form/FormTextarea.stories.svelte.d.ts +27 -0
  69. package/dist/components/form/FormToggle.stories.svelte +30 -0
  70. package/dist/components/form/FormToggle.stories.svelte.d.ts +27 -0
  71. package/dist/components/form/FormUISchemaRenderer.svelte +1 -1
  72. package/dist/components/form/types.d.ts +15 -47
  73. package/dist/components/interrupt/ChoicePrompt.stories.svelte +43 -0
  74. package/dist/components/interrupt/ChoicePrompt.stories.svelte.d.ts +27 -0
  75. package/dist/components/interrupt/ChoicePrompt.svelte +24 -24
  76. package/dist/components/interrupt/ConfirmationPrompt.stories.svelte +49 -0
  77. package/dist/components/interrupt/ConfirmationPrompt.stories.svelte.d.ts +27 -0
  78. package/dist/components/interrupt/ConfirmationPrompt.svelte +19 -19
  79. package/dist/components/interrupt/FormPrompt.svelte +15 -15
  80. package/dist/components/interrupt/InterruptBubble.svelte +202 -236
  81. package/dist/components/interrupt/InterruptBubble.svelte.d.ts +1 -1
  82. package/dist/components/interrupt/ReviewPrompt.stories.svelte +46 -0
  83. package/dist/components/interrupt/ReviewPrompt.stories.svelte.d.ts +27 -0
  84. package/dist/components/interrupt/ReviewPrompt.svelte +842 -0
  85. package/dist/components/interrupt/ReviewPrompt.svelte.d.ts +23 -0
  86. package/dist/components/interrupt/TextInputPrompt.stories.svelte +34 -0
  87. package/dist/components/interrupt/TextInputPrompt.stories.svelte.d.ts +27 -0
  88. package/dist/components/interrupt/TextInputPrompt.svelte +21 -21
  89. package/dist/components/nodes/GatewayNode.stories.svelte +76 -0
  90. package/dist/components/nodes/GatewayNode.stories.svelte.d.ts +26 -0
  91. package/dist/components/nodes/GatewayNode.svelte +19 -17
  92. package/dist/components/nodes/IdeaNode.stories.svelte +48 -0
  93. package/dist/components/nodes/IdeaNode.stories.svelte.d.ts +26 -0
  94. package/dist/components/nodes/IdeaNode.svelte +10 -26
  95. package/dist/components/nodes/NotesNode.stories.svelte +69 -0
  96. package/dist/components/nodes/NotesNode.stories.svelte.d.ts +26 -0
  97. package/dist/components/nodes/NotesNode.svelte +8 -8
  98. package/dist/components/nodes/SimpleNode.stories.svelte +101 -0
  99. package/dist/components/nodes/SimpleNode.stories.svelte.d.ts +26 -0
  100. package/dist/components/nodes/SimpleNode.svelte +16 -24
  101. package/dist/components/nodes/SquareNode.stories.svelte +56 -0
  102. package/dist/components/nodes/SquareNode.stories.svelte.d.ts +26 -0
  103. package/dist/components/nodes/SquareNode.svelte +13 -21
  104. package/dist/components/nodes/TerminalNode.stories.svelte +25 -0
  105. package/dist/components/nodes/TerminalNode.stories.svelte.d.ts +26 -0
  106. package/dist/components/nodes/TerminalNode.svelte +6 -6
  107. package/dist/components/nodes/ToolNode.stories.svelte +71 -0
  108. package/dist/components/nodes/ToolNode.stories.svelte.d.ts +26 -0
  109. package/dist/components/nodes/ToolNode.svelte +7 -15
  110. package/dist/components/nodes/WorkflowNode.stories.svelte +50 -0
  111. package/dist/components/nodes/WorkflowNode.stories.svelte.d.ts +26 -0
  112. package/dist/components/nodes/WorkflowNode.svelte +13 -13
  113. package/dist/components/playground/ChatPanel.svelte +48 -48
  114. package/dist/components/playground/ExecutionLogs.svelte +23 -23
  115. package/dist/components/playground/InputCollector.svelte +24 -24
  116. package/dist/components/playground/MessageBubble.stories.svelte +49 -0
  117. package/dist/components/playground/MessageBubble.stories.svelte.d.ts +27 -0
  118. package/dist/components/playground/MessageBubble.svelte +49 -46
  119. package/dist/components/playground/Playground.svelte +194 -129
  120. package/dist/components/playground/PlaygroundModal.svelte +5 -5
  121. package/dist/components/playground/SessionManager.svelte +26 -26
  122. package/dist/config/constants.d.ts +22 -0
  123. package/dist/config/constants.js +22 -0
  124. package/dist/config/endpoints.d.ts +19 -0
  125. package/dist/config/runtimeConfig.js +2 -1
  126. package/dist/core/index.d.ts +5 -2
  127. package/dist/core/index.js +9 -1
  128. package/dist/editor/index.d.ts +13 -9
  129. package/dist/editor/index.js +15 -11
  130. package/dist/form/code.d.ts +2 -1
  131. package/dist/form/code.js +1 -3
  132. package/dist/form/markdown.d.ts +2 -1
  133. package/dist/form/markdown.js +1 -3
  134. package/dist/helpers/workflowEditorHelper.js +13 -9
  135. package/dist/mocks/app-forms.js +1 -0
  136. package/dist/mocks/app-navigation.js +3 -1
  137. package/dist/mocks/app-stores.d.ts +4 -4
  138. package/dist/playground/index.d.ts +4 -3
  139. package/dist/playground/index.js +12 -10
  140. package/dist/playground/mount.js +6 -13
  141. package/dist/services/agentSpecExecutionService.js +2 -1
  142. package/dist/services/api.js +10 -18
  143. package/dist/services/apiVariableService.js +2 -1
  144. package/dist/services/autoSaveService.d.ts +3 -3
  145. package/dist/services/autoSaveService.js +21 -17
  146. package/dist/services/categoriesApi.js +13 -5
  147. package/dist/services/draftStorage.js +5 -4
  148. package/dist/services/dynamicSchemaService.js +4 -4
  149. package/dist/services/globalSave.d.ts +60 -11
  150. package/dist/services/globalSave.js +160 -83
  151. package/dist/services/historyService.d.ts +2 -1
  152. package/dist/services/historyService.js +7 -3
  153. package/dist/services/interruptService.js +9 -8
  154. package/dist/services/nodeExecutionService.js +14 -6
  155. package/dist/services/playgroundService.js +2 -1
  156. package/dist/services/portConfigApi.js +11 -7
  157. package/dist/services/toastService.d.ts +1 -1
  158. package/dist/services/toastService.js +6 -5
  159. package/dist/services/variableService.js +3 -2
  160. package/dist/settings/index.d.ts +1 -1
  161. package/dist/settings/index.js +1 -1
  162. package/dist/stores/{categoriesStore.d.ts → categoriesStore.svelte.d.ts} +3 -3
  163. package/dist/stores/{categoriesStore.js → categoriesStore.svelte.js} +15 -18
  164. package/dist/stores/editorStateMachine.svelte.d.ts +42 -0
  165. package/dist/stores/editorStateMachine.svelte.js +132 -0
  166. package/dist/stores/{historyStore.d.ts → historyStore.svelte.d.ts} +18 -15
  167. package/dist/stores/{historyStore.js → historyStore.svelte.js} +40 -21
  168. package/dist/stores/{interruptStore.d.ts → interruptStore.svelte.d.ts} +16 -15
  169. package/dist/stores/{interruptStore.js → interruptStore.svelte.js} +85 -94
  170. package/dist/stores/{playgroundStore.d.ts → playgroundStore.svelte.d.ts} +41 -33
  171. package/dist/stores/{playgroundStore.js → playgroundStore.svelte.js} +164 -84
  172. package/dist/stores/{portCoordinateStore.d.ts → portCoordinateStore.svelte.d.ts} +10 -4
  173. package/dist/stores/{portCoordinateStore.js → portCoordinateStore.svelte.js} +38 -35
  174. package/dist/stores/{settingsStore.d.ts → settingsStore.svelte.d.ts} +45 -28
  175. package/dist/stores/{settingsStore.js → settingsStore.svelte.js} +169 -128
  176. package/dist/stores/{workflowStore.d.ts → workflowStore.svelte.d.ts} +101 -65
  177. package/dist/stores/{workflowStore.js → workflowStore.svelte.js} +285 -239
  178. package/dist/stories/CanvasDecorator.svelte +50 -0
  179. package/dist/stories/CanvasDecorator.svelte.d.ts +8 -0
  180. package/dist/stories/NodeDecorator.svelte +74 -0
  181. package/dist/stories/NodeDecorator.svelte.d.ts +8 -0
  182. package/dist/stories/utils.d.ts +93 -0
  183. package/dist/stories/utils.js +122 -0
  184. package/dist/styles/base.css +114 -61
  185. package/dist/styles/toast.css +2 -2
  186. package/dist/styles/tokens.css +250 -185
  187. package/dist/svelte-app.d.ts +0 -6
  188. package/dist/svelte-app.js +13 -31
  189. package/dist/types/index.d.ts +2 -0
  190. package/dist/types/interrupt.d.ts +89 -5
  191. package/dist/types/interrupt.js +13 -1
  192. package/dist/types/playground.d.ts +5 -0
  193. package/dist/types/settings.js +1 -1
  194. package/dist/utils/colors.js +4 -4
  195. package/dist/utils/connections.js +33 -8
  196. package/dist/utils/icons.js +1 -1
  197. package/dist/utils/logger.d.ts +47 -0
  198. package/dist/utils/logger.js +72 -0
  199. package/dist/utils/nodeWrapper.js +1 -1
  200. package/dist/utils/sanitize.d.ts +19 -0
  201. package/dist/utils/sanitize.js +31 -0
  202. package/dist/utils/validation.d.ts +29 -0
  203. package/dist/utils/validation.js +39 -0
  204. package/package.json +243 -232
@@ -0,0 +1,842 @@
1
+ <!--
2
+ ReviewPrompt Component
3
+
4
+ Renders a review prompt for review-type interrupts.
5
+ Displays proposed field changes with accept/reject toggles per field.
6
+ Supports bulk "Accept All" / "Reject All" actions.
7
+ Shows the review decisions when resolved.
8
+ Styled with BEM syntax.
9
+ -->
10
+
11
+ <script lang="ts">
12
+ import Icon from '@iconify/svelte';
13
+ import { diffWords, diffArrays, diffJson } from 'diff';
14
+ import type { Change } from 'diff';
15
+ import { sanitizeHtml } from '../../utils/sanitize.js';
16
+ import type {
17
+ ReviewConfig,
18
+ ReviewResolution,
19
+ ReviewFieldDecision
20
+ } from '../../types/interrupt.js';
21
+
22
+ /**
23
+ * Component props
24
+ */
25
+ interface Props {
26
+ /** Review configuration from the interrupt */
27
+ config: ReviewConfig;
28
+ /** Whether this interrupt has been resolved */
29
+ isResolved: boolean;
30
+ /** The resolved value if resolved */
31
+ resolvedValue?: ReviewResolution;
32
+ /** Whether the form is currently submitting */
33
+ isSubmitting: boolean;
34
+ /** Error message if submission failed */
35
+ error?: string;
36
+ /** Username of the person who resolved the interrupt */
37
+ resolvedByUserName?: string;
38
+ /** Callback when user submits review */
39
+ onSubmit: (value: ReviewResolution) => void;
40
+ }
41
+
42
+ let {
43
+ config,
44
+ isResolved,
45
+ resolvedValue,
46
+ isSubmitting,
47
+ error,
48
+ resolvedByUserName,
49
+ onSubmit
50
+ }: Props = $props();
51
+
52
+ /** Local state: map of field -> accepted boolean. Default all to true (accept). */
53
+ let decisions = $state<Record<string, boolean>>(
54
+ Object.fromEntries(config.changes.map((c) => [c.field, true]))
55
+ );
56
+
57
+ /** Local state: map of field -> HTML view mode ('rendered' or 'raw'). Default to 'rendered'. */
58
+ let htmlViewMode = $state<Record<string, 'rendered' | 'raw'>>(
59
+ Object.fromEntries(config.changes.map((c) => [c.field, 'rendered']))
60
+ );
61
+
62
+ /** Count of accepted fields */
63
+ const acceptedCount = $derived(Object.values(decisions).filter((v) => v).length);
64
+
65
+ /** Count of rejected fields */
66
+ const rejectedCount = $derived(Object.values(decisions).filter((v) => !v).length);
67
+
68
+ /** Total number of changes */
69
+ const totalCount = $derived(config.changes.length);
70
+
71
+ /** Button labels with defaults */
72
+ const acceptAllLabel = $derived(config.acceptAllLabel ?? 'Accept All');
73
+ const rejectAllLabel = $derived(config.rejectAllLabel ?? 'Reject All');
74
+ const submitLabel = $derived(config.submitLabel ?? 'Submit Review');
75
+
76
+ /**
77
+ * Set a specific field's decision
78
+ */
79
+ function setFieldDecision(field: string, accepted: boolean): void {
80
+ if (isResolved || isSubmitting) return;
81
+ decisions = { ...decisions, [field]: accepted };
82
+ }
83
+
84
+ /**
85
+ * Accept all changes
86
+ */
87
+ function handleAcceptAll(): void {
88
+ if (isResolved || isSubmitting) return;
89
+ decisions = Object.fromEntries(config.changes.map((c) => [c.field, true]));
90
+ }
91
+
92
+ /**
93
+ * Reject all changes
94
+ */
95
+ function handleRejectAll(): void {
96
+ if (isResolved || isSubmitting) return;
97
+ decisions = Object.fromEntries(config.changes.map((c) => [c.field, false]));
98
+ }
99
+
100
+ /**
101
+ * Submit the review
102
+ */
103
+ function handleSubmit(): void {
104
+ if (isResolved || isSubmitting) return;
105
+
106
+ const fieldDecisions: Record<string, ReviewFieldDecision> = {};
107
+ for (const change of config.changes) {
108
+ const accepted = decisions[change.field] ?? true;
109
+ fieldDecisions[change.field] = {
110
+ accepted,
111
+ value: accepted ? change.proposed : change.original
112
+ };
113
+ }
114
+
115
+ const resolution: ReviewResolution = {
116
+ decisions: fieldDecisions,
117
+ summary: {
118
+ accepted: acceptedCount,
119
+ rejected: rejectedCount,
120
+ total: totalCount
121
+ }
122
+ };
123
+
124
+ onSubmit(resolution);
125
+ }
126
+
127
+ /**
128
+ * Toggle HTML view mode between rendered and raw for a field.
129
+ */
130
+ function toggleHtmlView(field: string): void {
131
+ htmlViewMode = {
132
+ ...htmlViewMode,
133
+ [field]: htmlViewMode[field] === 'rendered' ? 'raw' : 'rendered'
134
+ };
135
+ }
136
+
137
+ /**
138
+ * Check if a string contains HTML tags.
139
+ */
140
+ function containsHtml(value: unknown): boolean {
141
+ return typeof value === 'string' && /<[a-z][\s\S]*?>/i.test(value);
142
+ }
143
+
144
+ /**
145
+ * Strip HTML tags from a string, preserving text content.
146
+ * Collapses whitespace and trims the result.
147
+ */
148
+ function stripHtmlTags(html: string): string {
149
+ return html
150
+ .replace(/<br\s*\/?>/gi, '\n')
151
+ .replace(/<\/(?:p|div|li|h[1-6])>/gi, '\n')
152
+ .replace(/<[^>]+>/g, '')
153
+ .replace(/&amp;/g, '&')
154
+ .replace(/&lt;/g, '<')
155
+ .replace(/&gt;/g, '>')
156
+ .replace(/&quot;/g, '"')
157
+ .replace(/&#39;/g, "'")
158
+ .replace(/&nbsp;/g, ' ')
159
+ .replace(/\n{3,}/g, '\n\n')
160
+ .trim();
161
+ }
162
+
163
+ /**
164
+ * Format a value for display
165
+ */
166
+ function formatValue(value: unknown): string {
167
+ if (value === null || value === undefined) return '(empty)';
168
+ if (typeof value === 'string') return value;
169
+ if (typeof value === 'boolean') return value ? 'Yes' : 'No';
170
+ if (typeof value === 'object') return JSON.stringify(value, null, 2);
171
+ return String(value);
172
+ }
173
+
174
+ /**
175
+ * Compute a diff between two values.
176
+ * Supports strings (word-level), arrays (element-level), and objects (JSON line-level).
177
+ * For HTML strings, strips tags and diffs the plain text content.
178
+ */
179
+ function computeDiff(original: unknown, proposed: unknown, rawMode: boolean = false): Change[] | null {
180
+ if (typeof original === 'string' && typeof proposed === 'string') {
181
+ const origText = !rawMode && containsHtml(original) ? stripHtmlTags(original) : original;
182
+ const propText = !rawMode && containsHtml(proposed) ? stripHtmlTags(proposed) : proposed;
183
+ return diffWords(origText, propText);
184
+ }
185
+ if (Array.isArray(original) && Array.isArray(proposed)) {
186
+ const arrayChanges = diffArrays(original, proposed);
187
+ return arrayChanges.map((part) => ({
188
+ value: part.value.map((v: unknown) => JSON.stringify(v)).join(', '),
189
+ added: part.added,
190
+ removed: part.removed,
191
+ count: part.count
192
+ }));
193
+ }
194
+ if (
195
+ typeof original === 'object' &&
196
+ original !== null &&
197
+ !Array.isArray(original) &&
198
+ typeof proposed === 'object' &&
199
+ proposed !== null &&
200
+ !Array.isArray(proposed)
201
+ ) {
202
+ return diffJson(original, proposed);
203
+ }
204
+ return null;
205
+ }
206
+
207
+ /**
208
+ * Check if a diff result contains multi-line content (e.g. JSON diffs).
209
+ */
210
+ function isMultiLineDiff(changes: Change[]): boolean {
211
+ return changes.some((part) => part.value.includes('\n'));
212
+ }
213
+ </script>
214
+
215
+ <div
216
+ class="review-prompt"
217
+ class:review-prompt--resolved={isResolved}
218
+ class:review-prompt--submitting={isSubmitting}
219
+ >
220
+ <!-- Message -->
221
+ <p class="review-prompt__message">{config.message}</p>
222
+
223
+ <!-- Error message -->
224
+ {#if error}
225
+ <div class="review-prompt__error">
226
+ <Icon icon="mdi:alert-circle" />
227
+ <span>{error}</span>
228
+ </div>
229
+ {/if}
230
+
231
+ <!-- Bulk actions & counter (pending state only) -->
232
+ {#if !isResolved}
233
+ <div class="review-prompt__toolbar">
234
+ <div class="review-prompt__bulk-actions">
235
+ <button
236
+ type="button"
237
+ class="review-prompt__bulk-btn review-prompt__bulk-btn--accept"
238
+ onclick={handleAcceptAll}
239
+ disabled={isSubmitting}
240
+ >
241
+ <Icon icon="mdi:check-all" />
242
+ <span>{acceptAllLabel}</span>
243
+ </button>
244
+ <button
245
+ type="button"
246
+ class="review-prompt__bulk-btn review-prompt__bulk-btn--reject"
247
+ onclick={handleRejectAll}
248
+ disabled={isSubmitting}
249
+ >
250
+ <Icon icon="mdi:close-circle-multiple-outline" />
251
+ <span>{rejectAllLabel}</span>
252
+ </button>
253
+ </div>
254
+ <span class="review-prompt__counter">
255
+ {acceptedCount} of {totalCount} accepted
256
+ </span>
257
+ </div>
258
+ {/if}
259
+
260
+ <!-- Changes list -->
261
+ <div class="review-prompt__changes">
262
+ {#each config.changes as change (change.field)}
263
+ {@const isAccepted = isResolved
264
+ ? (resolvedValue?.decisions[change.field]?.accepted ?? true)
265
+ : (decisions[change.field] ?? true)}
266
+ {@const isHtml = containsHtml(change.original) || containsHtml(change.proposed)}
267
+ {@const isRawView = htmlViewMode[change.field] === 'raw'}
268
+ {@const diff = computeDiff(change.original, change.proposed, isRawView)}
269
+ <div
270
+ class="review-prompt__change"
271
+ class:review-prompt__change--accepted={isAccepted}
272
+ class:review-prompt__change--rejected={!isAccepted}
273
+ >
274
+ <!-- Change header: label + toggle -->
275
+ <div class="review-prompt__change-header">
276
+ <span class="review-prompt__change-label">{change.label}</span>
277
+ {#if !isResolved}
278
+ <div class="review-prompt__toggle-group">
279
+ <button
280
+ type="button"
281
+ class="review-prompt__toggle-btn review-prompt__toggle-btn--accept"
282
+ class:review-prompt__toggle-btn--active={isAccepted}
283
+ onclick={() => setFieldDecision(change.field, true)}
284
+ disabled={isSubmitting}
285
+ aria-label="Accept {change.label}"
286
+ title="Accept"
287
+ >
288
+ <Icon icon="mdi:check" />
289
+ <span>Accept</span>
290
+ </button>
291
+ <button
292
+ type="button"
293
+ class="review-prompt__toggle-btn review-prompt__toggle-btn--reject"
294
+ class:review-prompt__toggle-btn--active={!isAccepted}
295
+ onclick={() => setFieldDecision(change.field, false)}
296
+ disabled={isSubmitting}
297
+ aria-label="Reject {change.label}"
298
+ title="Reject"
299
+ >
300
+ <Icon icon="mdi:close" />
301
+ <span>Reject</span>
302
+ </button>
303
+ </div>
304
+ {:else}
305
+ <span
306
+ class="review-prompt__decision-badge"
307
+ class:review-prompt__decision-badge--accepted={isAccepted}
308
+ class:review-prompt__decision-badge--rejected={!isAccepted}
309
+ >
310
+ {#if isAccepted}
311
+ <Icon icon="mdi:check-circle" />
312
+ <span>Accepted</span>
313
+ {:else}
314
+ <Icon icon="mdi:close-circle" />
315
+ <span>Rejected</span>
316
+ {/if}
317
+ </span>
318
+ {/if}
319
+ </div>
320
+
321
+ <!-- Change diff content -->
322
+ <div class="review-prompt__change-body">
323
+ {#if isHtml}
324
+ <div class="review-prompt__html-toggle-row">
325
+ <button
326
+ type="button"
327
+ class="review-prompt__html-toggle-btn"
328
+ onclick={() => toggleHtmlView(change.field)}
329
+ >
330
+ <Icon icon={isRawView ? 'mdi:eye' : 'mdi:code-tags'} />
331
+ <span>{isRawView ? 'Rendered' : 'Raw HTML'}</span>
332
+ </button>
333
+ </div>
334
+ {/if}
335
+ <div class="review-prompt__diff-row">
336
+ <span class="review-prompt__diff-label">Original:</span>
337
+ {#if isHtml && !isRawView}
338
+ <span class="review-prompt__diff-value review-prompt__html-content">{@html sanitizeHtml(String(change.original))}</span>
339
+ {:else if isHtml && isRawView}
340
+ <code class="review-prompt__diff-value review-prompt__raw-html">{change.original}</code>
341
+ {:else}
342
+ <span class="review-prompt__diff-value">
343
+ {formatValue(change.original)}
344
+ </span>
345
+ {/if}
346
+ </div>
347
+ <div class="review-prompt__diff-row">
348
+ <span class="review-prompt__diff-label">Proposed:</span>
349
+ {#if isHtml && !isRawView}
350
+ <span class="review-prompt__diff-value review-prompt__diff-value--proposed review-prompt__html-content">{@html sanitizeHtml(String(change.proposed))}</span>
351
+ {:else if isHtml && isRawView}
352
+ <code class="review-prompt__diff-value review-prompt__diff-value--proposed review-prompt__raw-html">{change.proposed}</code>
353
+ {:else}
354
+ <span class="review-prompt__diff-value review-prompt__diff-value--proposed">
355
+ {formatValue(change.proposed)}
356
+ </span>
357
+ {/if}
358
+ </div>
359
+ {#if diff}
360
+ <div class="review-prompt__diff-row">
361
+ <span class="review-prompt__diff-label">Diff:</span>
362
+ {#if isMultiLineDiff(diff)}
363
+ <pre class="review-prompt__diff-value review-prompt__diff-block">{#each diff as part}{#if part.added}<span class="review-prompt__diff-token--added">{part.value}</span>{:else if part.removed}<span class="review-prompt__diff-token--removed">{part.value}</span>{:else}<span>{part.value}</span>{/if}{/each}</pre>
364
+ {:else}
365
+ <span class="review-prompt__diff-value review-prompt__diff-inline">
366
+ {#each diff as part}
367
+ {#if part.added}
368
+ <span class="review-prompt__diff-token--added">{part.value}</span>
369
+ {:else if part.removed}
370
+ <span class="review-prompt__diff-token--removed">{part.value}</span>
371
+ {:else}
372
+ <span>{part.value}</span>
373
+ {/if}
374
+ {/each}
375
+ </span>
376
+ {/if}
377
+ </div>
378
+ {/if}
379
+ </div>
380
+ </div>
381
+ {/each}
382
+ </div>
383
+
384
+ <!-- Submit button (pending state only) -->
385
+ {#if !isResolved}
386
+ <div class="review-prompt__actions">
387
+ <button
388
+ type="button"
389
+ class="review-prompt__submit"
390
+ onclick={handleSubmit}
391
+ disabled={isSubmitting}
392
+ >
393
+ {#if isSubmitting}
394
+ <span class="review-prompt__spinner"></span>
395
+ {:else}
396
+ <Icon icon="mdi:check" />
397
+ {/if}
398
+ <span>{submitLabel}</span>
399
+ </button>
400
+ </div>
401
+ {/if}
402
+
403
+ <!-- Resolved summary -->
404
+ {#if isResolved && resolvedValue}
405
+ <div class="review-prompt__summary">
406
+ <span class="review-prompt__summary-text">
407
+ {resolvedValue.summary.accepted} accepted, {resolvedValue.summary.rejected} rejected
408
+ out of {resolvedValue.summary.total} changes
409
+ </span>
410
+ </div>
411
+ {/if}
412
+
413
+ <!-- Resolved indicator -->
414
+ {#if isResolved}
415
+ <div class="review-prompt__resolved-badge">
416
+ <Icon icon="mdi:check-circle" />
417
+ <span>
418
+ {resolvedByUserName
419
+ ? `Response submitted by ${resolvedByUserName}`
420
+ : 'Response submitted'}
421
+ </span>
422
+ </div>
423
+ {/if}
424
+ </div>
425
+
426
+ <style>
427
+ /* Uses design tokens from tokens.css / base.css
428
+ Component tokens: --fd-review-* defined in base.css */
429
+ .review-prompt {
430
+ display: flex;
431
+ flex-direction: column;
432
+ gap: var(--fd-space-md);
433
+ }
434
+
435
+ .review-prompt--resolved {
436
+ opacity: 0.85;
437
+ }
438
+
439
+ .review-prompt--submitting {
440
+ pointer-events: none;
441
+ }
442
+
443
+ .review-prompt__message {
444
+ margin: 0;
445
+ font-size: var(--fd-review-font-size-message);
446
+ line-height: var(--fd-review-line-height);
447
+ color: var(--fd-foreground);
448
+ }
449
+
450
+ .review-prompt__error {
451
+ display: flex;
452
+ align-items: center;
453
+ gap: var(--fd-review-space-sm);
454
+ padding: var(--fd-space-xs) var(--fd-space-md);
455
+ background-color: var(--fd-error-muted);
456
+ border-radius: var(--fd-radius-md);
457
+ color: var(--fd-error);
458
+ font-size: var(--fd-review-font-size-error);
459
+ }
460
+
461
+ /* Toolbar: bulk actions + counter */
462
+ .review-prompt__toolbar {
463
+ display: flex;
464
+ align-items: center;
465
+ justify-content: space-between;
466
+ gap: var(--fd-space-md);
467
+ flex-wrap: wrap;
468
+ }
469
+
470
+ .review-prompt__bulk-actions {
471
+ display: flex;
472
+ gap: var(--fd-space-xs);
473
+ }
474
+
475
+ .review-prompt__bulk-btn {
476
+ display: inline-flex;
477
+ align-items: center;
478
+ gap: var(--fd-review-space-sm);
479
+ padding: var(--fd-review-space-sm) var(--fd-space-md);
480
+ font-size: var(--fd-text-xs);
481
+ font-weight: 500;
482
+ font-family: inherit;
483
+ border-radius: var(--fd-radius-md);
484
+ cursor: pointer;
485
+ transition: all var(--fd-transition-fast);
486
+ border: 1px solid var(--fd-border);
487
+ background-color: var(--fd-muted);
488
+ color: var(--fd-foreground);
489
+ }
490
+
491
+ .review-prompt__bulk-btn:hover:not(:disabled) {
492
+ border-color: var(--fd-border-strong);
493
+ }
494
+
495
+ .review-prompt__bulk-btn--accept:hover:not(:disabled) {
496
+ background-color: var(--fd-success-muted);
497
+ border-color: var(--fd-success);
498
+ color: var(--fd-success);
499
+ }
500
+
501
+ .review-prompt__bulk-btn--reject:hover:not(:disabled) {
502
+ background-color: var(--fd-error-muted);
503
+ border-color: var(--fd-error);
504
+ color: var(--fd-error);
505
+ }
506
+
507
+ .review-prompt__bulk-btn:disabled {
508
+ opacity: 0.5;
509
+ cursor: not-allowed;
510
+ }
511
+
512
+ .review-prompt__counter {
513
+ font-size: var(--fd-text-xs);
514
+ color: var(--fd-muted-foreground);
515
+ }
516
+
517
+ /* Change cards */
518
+ .review-prompt__changes {
519
+ display: flex;
520
+ flex-direction: column;
521
+ gap: var(--fd-space-xs);
522
+ }
523
+
524
+ .review-prompt__change {
525
+ border: 1px solid var(--fd-border);
526
+ border-radius: var(--fd-radius-lg);
527
+ background-color: var(--fd-background);
528
+ overflow: hidden;
529
+ transition: all var(--fd-transition-fast);
530
+ }
531
+
532
+ .review-prompt__change--accepted {
533
+ border-color: var(--fd-success);
534
+ }
535
+
536
+ .review-prompt__change--rejected {
537
+ border-color: var(--fd-error);
538
+ }
539
+
540
+ .review-prompt__change-header {
541
+ display: flex;
542
+ align-items: center;
543
+ justify-content: space-between;
544
+ padding: var(--fd-review-space-md) var(--fd-review-space-lg);
545
+ border-bottom: 1px solid var(--fd-border);
546
+ }
547
+
548
+ .review-prompt__change--accepted .review-prompt__change-header {
549
+ background-color: var(--fd-success-muted);
550
+ border-bottom-color: var(--fd-success);
551
+ }
552
+
553
+ .review-prompt__change--rejected .review-prompt__change-header {
554
+ background-color: var(--fd-error-muted);
555
+ border-bottom-color: var(--fd-error);
556
+ }
557
+
558
+ .review-prompt__change-label {
559
+ font-size: var(--fd-text-sm);
560
+ font-weight: 600;
561
+ color: var(--fd-foreground);
562
+ }
563
+
564
+ /* Accept/Reject toggle buttons */
565
+ .review-prompt__toggle-group {
566
+ display: flex;
567
+ gap: var(--fd-space-3xs);
568
+ }
569
+
570
+ .review-prompt__toggle-btn {
571
+ display: inline-flex;
572
+ align-items: center;
573
+ justify-content: center;
574
+ gap: var(--fd-space-3xs);
575
+ height: var(--fd-review-toggle-height);
576
+ padding: 0 var(--fd-space-xs);
577
+ border-radius: var(--fd-radius-md);
578
+ border: 1px solid var(--fd-border);
579
+ background-color: var(--fd-background);
580
+ color: var(--fd-muted-foreground);
581
+ cursor: pointer;
582
+ transition: all var(--fd-transition-fast);
583
+ font-size: var(--fd-text-xs);
584
+ font-weight: 500;
585
+ font-family: inherit;
586
+ }
587
+
588
+ .review-prompt__toggle-btn:hover:not(:disabled) {
589
+ border-color: var(--fd-border-strong);
590
+ }
591
+
592
+ .review-prompt__toggle-btn--accept.review-prompt__toggle-btn--active {
593
+ background-color: var(--fd-success);
594
+ border-color: var(--fd-success);
595
+ color: var(--fd-success-foreground);
596
+ }
597
+
598
+ .review-prompt__toggle-btn--reject.review-prompt__toggle-btn--active {
599
+ background-color: var(--fd-error);
600
+ border-color: var(--fd-error);
601
+ color: var(--fd-error-foreground);
602
+ }
603
+
604
+ .review-prompt__toggle-btn:disabled {
605
+ opacity: 0.5;
606
+ cursor: not-allowed;
607
+ }
608
+
609
+ /* Decision badge (resolved state) */
610
+ .review-prompt__decision-badge {
611
+ display: inline-flex;
612
+ align-items: center;
613
+ gap: var(--fd-space-3xs);
614
+ font-size: var(--fd-text-xs);
615
+ font-weight: 500;
616
+ }
617
+
618
+ .review-prompt__decision-badge--accepted {
619
+ color: var(--fd-success);
620
+ }
621
+
622
+ .review-prompt__decision-badge--rejected {
623
+ color: var(--fd-error);
624
+ }
625
+
626
+ /* Diff content */
627
+ .review-prompt__change-body {
628
+ display: flex;
629
+ flex-direction: column;
630
+ }
631
+
632
+ .review-prompt__diff-row {
633
+ display: flex;
634
+ align-items: baseline;
635
+ gap: var(--fd-space-xs);
636
+ padding: var(--fd-space-xs) var(--fd-review-space-lg);
637
+ border-bottom: 1px solid var(--fd-border);
638
+ }
639
+
640
+ .review-prompt__diff-row:last-child {
641
+ border-bottom: none;
642
+ }
643
+
644
+ .review-prompt__diff-label {
645
+ font-size: var(--fd-text-xs);
646
+ font-weight: 500;
647
+ color: var(--fd-muted-foreground);
648
+ min-width: var(--fd-review-diff-label-width);
649
+ flex-shrink: 0;
650
+ }
651
+
652
+ .review-prompt__diff-value {
653
+ font-size: var(--fd-text-sm);
654
+ color: var(--fd-foreground);
655
+ word-break: break-word;
656
+ white-space: pre-wrap;
657
+ }
658
+
659
+ .review-prompt__diff-value--proposed {
660
+ font-weight: 500;
661
+ }
662
+
663
+ /* Inline diff display */
664
+ .review-prompt__diff-inline {
665
+ line-height: var(--fd-review-line-height-content);
666
+ }
667
+
668
+ /* HTML view toggle */
669
+ .review-prompt__html-toggle-row {
670
+ display: flex;
671
+ justify-content: flex-end;
672
+ padding: var(--fd-review-space-sm) var(--fd-review-space-lg) 0;
673
+ }
674
+
675
+ .review-prompt__html-toggle-btn {
676
+ display: inline-flex;
677
+ align-items: center;
678
+ gap: var(--fd-space-3xs);
679
+ padding: var(--fd-review-space-xs) var(--fd-space-xs);
680
+ font-size: var(--fd-review-font-size-html-toggle);
681
+ font-weight: 500;
682
+ font-family: inherit;
683
+ color: var(--fd-muted-foreground);
684
+ background: none;
685
+ border: 1px solid var(--fd-border);
686
+ border-radius: var(--fd-radius-md);
687
+ cursor: pointer;
688
+ transition: all var(--fd-transition-fast);
689
+ }
690
+
691
+ .review-prompt__html-toggle-btn:hover {
692
+ color: var(--fd-foreground);
693
+ border-color: var(--fd-border-strong);
694
+ }
695
+
696
+ /* Raw HTML code display */
697
+ .review-prompt__raw-html {
698
+ font-family: var(--fd-review-font-mono);
699
+ font-size: var(--fd-text-xs);
700
+ line-height: var(--fd-review-line-height);
701
+ white-space: pre-wrap;
702
+ word-break: break-word;
703
+ }
704
+
705
+ /* Rendered HTML content */
706
+ .review-prompt__html-content {
707
+ font-size: var(--fd-text-sm);
708
+ line-height: var(--fd-review-line-height-content);
709
+ }
710
+
711
+ .review-prompt__html-content :global(p) {
712
+ margin: 0 0 0.5em 0;
713
+ }
714
+
715
+ .review-prompt__html-content :global(p:last-child) {
716
+ margin-bottom: 0;
717
+ }
718
+
719
+ .review-prompt__html-content :global(ul),
720
+ .review-prompt__html-content :global(ol) {
721
+ margin: 0 0 0.5em 0;
722
+ padding-left: 1.25em;
723
+ }
724
+
725
+ .review-prompt__html-content :global(h1),
726
+ .review-prompt__html-content :global(h2),
727
+ .review-prompt__html-content :global(h3),
728
+ .review-prompt__html-content :global(h4),
729
+ .review-prompt__html-content :global(h5),
730
+ .review-prompt__html-content :global(h6) {
731
+ margin: 0 0 0.25em 0;
732
+ font-size: 1em;
733
+ font-weight: 600;
734
+ }
735
+
736
+ /* Block diff display (for JSON/multi-line diffs) */
737
+ .review-prompt__diff-block {
738
+ margin: 0;
739
+ font-family: var(--fd-review-font-mono);
740
+ font-size: var(--fd-text-xs);
741
+ line-height: var(--fd-review-line-height);
742
+ white-space: pre-wrap;
743
+ word-break: break-word;
744
+ overflow-x: auto;
745
+ }
746
+
747
+ .review-prompt__diff-token--added {
748
+ background-color: var(--fd-success-muted);
749
+ color: var(--fd-success);
750
+ padding: var(--fd-review-diff-token-padding);
751
+ border-radius: var(--fd-radius-sm);
752
+ }
753
+
754
+ .review-prompt__diff-token--removed {
755
+ background-color: var(--fd-error-muted);
756
+ color: var(--fd-error);
757
+ text-decoration: line-through;
758
+ padding: var(--fd-review-diff-token-padding);
759
+ border-radius: var(--fd-radius-sm);
760
+ }
761
+
762
+ /* Actions */
763
+ .review-prompt__actions {
764
+ display: flex;
765
+ gap: var(--fd-space-md);
766
+ margin-top: var(--fd-space-3xs);
767
+ }
768
+
769
+ .review-prompt__submit {
770
+ display: inline-flex;
771
+ align-items: center;
772
+ justify-content: center;
773
+ gap: var(--fd-space-xs);
774
+ padding: var(--fd-review-space-md) var(--fd-space-2xl);
775
+ border-radius: var(--fd-radius-lg);
776
+ font-size: var(--fd-text-sm);
777
+ font-weight: 600;
778
+ font-family: inherit;
779
+ cursor: pointer;
780
+ transition: all var(--fd-transition-normal);
781
+ border: none;
782
+ min-height: var(--fd-space-5xl);
783
+ background: var(--fd-interrupt-btn-primary-bg);
784
+ color: var(--fd-primary-foreground);
785
+ box-shadow: var(--fd-shadow-sm);
786
+ }
787
+
788
+ .review-prompt__submit:hover:not(:disabled) {
789
+ background: var(--fd-interrupt-btn-primary-bg-hover);
790
+ box-shadow: var(--fd-shadow-md);
791
+ transform: translateY(-1px);
792
+ }
793
+
794
+ .review-prompt__submit:disabled {
795
+ opacity: 0.5;
796
+ cursor: not-allowed;
797
+ transform: none;
798
+ box-shadow: none;
799
+ }
800
+
801
+ .review-prompt__spinner {
802
+ width: var(--fd-space-xl);
803
+ height: var(--fd-space-xl);
804
+ border: 2px solid var(--fd-border);
805
+ border-top-color: currentColor;
806
+ border-radius: 50%;
807
+ animation: review-spin 0.6s linear infinite;
808
+ }
809
+
810
+ @keyframes review-spin {
811
+ to {
812
+ transform: rotate(360deg);
813
+ }
814
+ }
815
+
816
+ /* Summary */
817
+ .review-prompt__summary {
818
+ padding: var(--fd-space-xs) var(--fd-space-md);
819
+ background-color: var(--fd-primary-muted);
820
+ border: 1px solid var(--fd-interrupt-completed-border);
821
+ border-radius: var(--fd-radius-md);
822
+ }
823
+
824
+ .review-prompt__summary-text {
825
+ font-size: var(--fd-text-sm);
826
+ color: var(--fd-interrupt-completed-text);
827
+ }
828
+
829
+ /* Resolved badge - neutral blue theme */
830
+ .review-prompt__resolved-badge {
831
+ display: inline-flex;
832
+ align-items: center;
833
+ gap: var(--fd-review-space-sm);
834
+ padding: var(--fd-review-space-sm) var(--fd-space-md);
835
+ background-color: var(--fd-interrupt-badge-completed-bg);
836
+ border-radius: var(--fd-radius-full);
837
+ color: var(--fd-interrupt-badge-completed-text);
838
+ font-size: var(--fd-text-xs);
839
+ font-weight: 500;
840
+ align-self: flex-start;
841
+ }
842
+ </style>