@dowel-ui/react 0.4.0 → 0.7.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.
Files changed (335) hide show
  1. package/README.md +9 -5
  2. package/dist/blocks/admin-dashboard/meta.js +31 -0
  3. package/dist/blocks/admin-dashboard/meta.js.map +1 -0
  4. package/dist/blocks/agent-console/meta.js +27 -0
  5. package/dist/blocks/agent-console/meta.js.map +1 -0
  6. package/dist/blocks/ai-dashboard/meta.js +25 -0
  7. package/dist/blocks/ai-dashboard/meta.js.map +1 -0
  8. package/dist/blocks/ai-workspace/meta.js +33 -0
  9. package/dist/blocks/ai-workspace/meta.js.map +1 -0
  10. package/dist/blocks/analytics/meta.js +24 -0
  11. package/dist/blocks/analytics/meta.js.map +1 -0
  12. package/dist/blocks/billing/meta.js +25 -0
  13. package/dist/blocks/billing/meta.js.map +1 -0
  14. package/dist/blocks/command-center/meta.js +28 -0
  15. package/dist/blocks/command-center/meta.js.map +1 -0
  16. package/dist/blocks/crm/meta.js +32 -0
  17. package/dist/blocks/crm/meta.js.map +1 -0
  18. package/dist/blocks/onboarding/meta.js +23 -0
  19. package/dist/blocks/onboarding/meta.js.map +1 -0
  20. package/dist/components/accordion/accordion.js +1 -1
  21. package/dist/components/accordion/accordion.js.map +1 -1
  22. package/dist/components/activity-feed/activity-feed.js +1 -1
  23. package/dist/components/activity-feed/activity-feed.js.map +1 -1
  24. package/dist/components/ai-agent-plan/ai-agent-plan.js +1 -1
  25. package/dist/components/ai-agent-plan/ai-agent-plan.js.map +1 -1
  26. package/dist/components/ai-extraction-review/ai-extraction-review.d.ts +52 -0
  27. package/dist/components/ai-extraction-review/ai-extraction-review.d.ts.map +1 -0
  28. package/dist/components/ai-extraction-review/ai-extraction-review.js +249 -0
  29. package/dist/components/ai-extraction-review/ai-extraction-review.js.map +1 -0
  30. package/dist/components/ai-extraction-review/extraction-model.d.ts +94 -0
  31. package/dist/components/ai-extraction-review/extraction-model.d.ts.map +1 -0
  32. package/dist/components/ai-extraction-review/extraction-model.js +91 -0
  33. package/dist/components/ai-extraction-review/extraction-model.js.map +1 -0
  34. package/dist/components/ai-extraction-review/index.d.ts +3 -0
  35. package/dist/components/ai-extraction-review/index.js +3 -0
  36. package/dist/components/ai-extraction-review/meta.js +17 -0
  37. package/dist/components/ai-extraction-review/meta.js.map +1 -0
  38. package/dist/components/ai-prompt-input/ai-prompt-input.js +1 -1
  39. package/dist/components/ai-prompt-input/ai-prompt-input.js.map +1 -1
  40. package/dist/components/ai-reasoning/ai-reasoning.js +1 -1
  41. package/dist/components/ai-reasoning/ai-reasoning.js.map +1 -1
  42. package/dist/components/ai-response/ai-response.js +2 -2
  43. package/dist/components/ai-response/ai-response.js.map +1 -1
  44. package/dist/components/ai-suggested-value/ai-suggested-value.d.ts +72 -0
  45. package/dist/components/ai-suggested-value/ai-suggested-value.d.ts.map +1 -0
  46. package/dist/components/ai-suggested-value/ai-suggested-value.js +126 -0
  47. package/dist/components/ai-suggested-value/ai-suggested-value.js.map +1 -0
  48. package/dist/components/ai-suggested-value/index.d.ts +2 -0
  49. package/dist/components/ai-suggested-value/index.js +2 -0
  50. package/dist/components/ai-suggested-value/meta.js +17 -0
  51. package/dist/components/ai-suggested-value/meta.js.map +1 -0
  52. package/dist/components/ai-tool/ai-tool.js +2 -2
  53. package/dist/components/ai-tool/ai-tool.js.map +1 -1
  54. package/dist/components/breadcrumb/breadcrumb.d.ts +52 -0
  55. package/dist/components/breadcrumb/breadcrumb.d.ts.map +1 -0
  56. package/dist/components/breadcrumb/breadcrumb.js +141 -0
  57. package/dist/components/breadcrumb/breadcrumb.js.map +1 -0
  58. package/dist/components/breadcrumb/index.d.ts +2 -0
  59. package/dist/components/breadcrumb/index.js +2 -0
  60. package/dist/components/breadcrumb/meta.js +17 -0
  61. package/dist/components/breadcrumb/meta.js.map +1 -0
  62. package/dist/components/calendar/calendar.js +2 -2
  63. package/dist/components/calendar/calendar.js.map +1 -1
  64. package/dist/components/code-block/code-block.js +1 -1
  65. package/dist/components/code-block/code-block.js.map +1 -1
  66. package/dist/components/collapsible/collapsible.d.ts +30 -0
  67. package/dist/components/collapsible/collapsible.d.ts.map +1 -0
  68. package/dist/components/collapsible/collapsible.js +38 -0
  69. package/dist/components/collapsible/collapsible.js.map +1 -0
  70. package/dist/components/collapsible/index.d.ts +2 -0
  71. package/dist/components/collapsible/index.js +2 -0
  72. package/dist/components/collapsible/meta.js +17 -0
  73. package/dist/components/collapsible/meta.js.map +1 -0
  74. package/dist/components/combobox/combobox.js +2 -2
  75. package/dist/components/combobox/combobox.js.map +1 -1
  76. package/dist/components/command/command.js +1 -1
  77. package/dist/components/command/command.js.map +1 -1
  78. package/dist/components/confirm-typed/confirm-typed.d.ts +47 -0
  79. package/dist/components/confirm-typed/confirm-typed.d.ts.map +1 -0
  80. package/dist/components/confirm-typed/confirm-typed.js +106 -0
  81. package/dist/components/confirm-typed/confirm-typed.js.map +1 -0
  82. package/dist/components/confirm-typed/index.d.ts +2 -0
  83. package/dist/components/confirm-typed/index.js +2 -0
  84. package/dist/components/confirm-typed/meta.js +17 -0
  85. package/dist/components/confirm-typed/meta.js.map +1 -0
  86. package/dist/components/cron-editor/cron-editor.d.ts +36 -0
  87. package/dist/components/cron-editor/cron-editor.d.ts.map +1 -0
  88. package/dist/components/cron-editor/cron-editor.js +493 -0
  89. package/dist/components/cron-editor/cron-editor.js.map +1 -0
  90. package/dist/components/cron-editor/cron-expression.d.ts +82 -0
  91. package/dist/components/cron-editor/cron-expression.d.ts.map +1 -0
  92. package/dist/components/cron-editor/cron-expression.js +352 -0
  93. package/dist/components/cron-editor/cron-expression.js.map +1 -0
  94. package/dist/components/cron-editor/index.d.ts +3 -0
  95. package/dist/components/cron-editor/index.js +3 -0
  96. package/dist/components/cron-editor/meta.js +17 -0
  97. package/dist/components/cron-editor/meta.js.map +1 -0
  98. package/dist/components/data-table/data-table.d.ts.map +1 -1
  99. package/dist/components/data-table/data-table.js +4 -1
  100. package/dist/components/data-table/data-table.js.map +1 -1
  101. package/dist/components/dialog/dialog.js +2 -2
  102. package/dist/components/dialog/dialog.js.map +1 -1
  103. package/dist/components/diff-viewer/diff-viewer.js +4 -4
  104. package/dist/components/diff-viewer/diff-viewer.js.map +1 -1
  105. package/dist/components/direction/direction.d.ts +35 -0
  106. package/dist/components/direction/direction.d.ts.map +1 -0
  107. package/dist/components/direction/direction.js +14 -0
  108. package/dist/components/direction/direction.js.map +1 -0
  109. package/dist/components/direction/index.d.ts +2 -0
  110. package/dist/components/direction/index.js +2 -0
  111. package/dist/components/direction/meta.js +17 -0
  112. package/dist/components/direction/meta.js.map +1 -0
  113. package/dist/components/dns-record/dns-record.d.ts +61 -0
  114. package/dist/components/dns-record/dns-record.d.ts.map +1 -0
  115. package/dist/components/dns-record/dns-record.js +227 -0
  116. package/dist/components/dns-record/dns-record.js.map +1 -0
  117. package/dist/components/dns-record/index.d.ts +2 -0
  118. package/dist/components/dns-record/index.js +2 -0
  119. package/dist/components/dns-record/meta.js +17 -0
  120. package/dist/components/dns-record/meta.js.map +1 -0
  121. package/dist/components/drawer/drawer.js +1 -1
  122. package/dist/components/drawer/drawer.js.map +1 -1
  123. package/dist/components/dropdown-menu/dropdown-menu.d.ts.map +1 -1
  124. package/dist/components/dropdown-menu/dropdown-menu.js +9 -8
  125. package/dist/components/dropdown-menu/dropdown-menu.js.map +1 -1
  126. package/dist/components/input/input.js +1 -1
  127. package/dist/components/input/input.js.map +1 -1
  128. package/dist/components/log-viewer/log-viewer.js +2 -2
  129. package/dist/components/log-viewer/log-viewer.js.map +1 -1
  130. package/dist/components/meter/meter.js +1 -1
  131. package/dist/components/meter/meter.js.map +1 -1
  132. package/dist/components/pagination/pagination.d.ts.map +1 -1
  133. package/dist/components/pagination/pagination.js +3 -2
  134. package/dist/components/pagination/pagination.js.map +1 -1
  135. package/dist/components/permission-matrix/index.d.ts +3 -0
  136. package/dist/components/permission-matrix/index.js +3 -0
  137. package/dist/components/permission-matrix/meta.js +17 -0
  138. package/dist/components/permission-matrix/meta.js.map +1 -0
  139. package/dist/components/permission-matrix/permission-matrix.d.ts +46 -0
  140. package/dist/components/permission-matrix/permission-matrix.d.ts.map +1 -0
  141. package/dist/components/permission-matrix/permission-matrix.js +199 -0
  142. package/dist/components/permission-matrix/permission-matrix.js.map +1 -0
  143. package/dist/components/permission-matrix/permission-model.d.ts +53 -0
  144. package/dist/components/permission-matrix/permission-model.d.ts.map +1 -0
  145. package/dist/components/permission-matrix/permission-model.js +64 -0
  146. package/dist/components/permission-matrix/permission-model.js.map +1 -0
  147. package/dist/components/record-diff/record-diff.js +4 -4
  148. package/dist/components/record-diff/record-diff.js.map +1 -1
  149. package/dist/components/secret-field/index.d.ts +2 -0
  150. package/dist/components/secret-field/index.js +2 -0
  151. package/dist/components/secret-field/meta.js +17 -0
  152. package/dist/components/secret-field/meta.js.map +1 -0
  153. package/dist/components/secret-field/secret-field.d.ts +58 -0
  154. package/dist/components/secret-field/secret-field.d.ts.map +1 -0
  155. package/dist/components/secret-field/secret-field.js +168 -0
  156. package/dist/components/secret-field/secret-field.js.map +1 -0
  157. package/dist/components/select/select.js +2 -2
  158. package/dist/components/select/select.js.map +1 -1
  159. package/dist/components/session-expiry/index.d.ts +2 -0
  160. package/dist/components/session-expiry/index.js +2 -0
  161. package/dist/components/session-expiry/meta.js +17 -0
  162. package/dist/components/session-expiry/meta.js.map +1 -0
  163. package/dist/components/session-expiry/session-expiry.d.ts +64 -0
  164. package/dist/components/session-expiry/session-expiry.d.ts.map +1 -0
  165. package/dist/components/session-expiry/session-expiry.js +185 -0
  166. package/dist/components/session-expiry/session-expiry.js.map +1 -0
  167. package/dist/components/sheet/sheet.d.ts.map +1 -1
  168. package/dist/components/sheet/sheet.js +2 -2
  169. package/dist/components/sheet/sheet.js.map +1 -1
  170. package/dist/components/shortcut-recorder/index.d.ts +3 -0
  171. package/dist/components/shortcut-recorder/index.js +3 -0
  172. package/dist/components/shortcut-recorder/meta.js +17 -0
  173. package/dist/components/shortcut-recorder/meta.js.map +1 -0
  174. package/dist/components/shortcut-recorder/shortcut-model.d.ts +67 -0
  175. package/dist/components/shortcut-recorder/shortcut-model.d.ts.map +1 -0
  176. package/dist/components/shortcut-recorder/shortcut-model.js +168 -0
  177. package/dist/components/shortcut-recorder/shortcut-model.js.map +1 -0
  178. package/dist/components/shortcut-recorder/shortcut-recorder.d.ts +47 -0
  179. package/dist/components/shortcut-recorder/shortcut-recorder.d.ts.map +1 -0
  180. package/dist/components/shortcut-recorder/shortcut-recorder.js +212 -0
  181. package/dist/components/shortcut-recorder/shortcut-recorder.js.map +1 -0
  182. package/dist/components/sidebar/index.d.ts +2 -0
  183. package/dist/components/sidebar/index.js +2 -0
  184. package/dist/components/sidebar/meta.js +17 -0
  185. package/dist/components/sidebar/meta.js.map +1 -0
  186. package/dist/components/sidebar/sidebar.d.ts +69 -0
  187. package/dist/components/sidebar/sidebar.d.ts.map +1 -0
  188. package/dist/components/sidebar/sidebar.js +256 -0
  189. package/dist/components/sidebar/sidebar.js.map +1 -0
  190. package/dist/components/sync-status/index.d.ts +2 -0
  191. package/dist/components/sync-status/index.js +2 -0
  192. package/dist/components/sync-status/meta.js +17 -0
  193. package/dist/components/sync-status/meta.js.map +1 -0
  194. package/dist/components/sync-status/sync-status.d.ts +65 -0
  195. package/dist/components/sync-status/sync-status.d.ts.map +1 -0
  196. package/dist/components/sync-status/sync-status.js +103 -0
  197. package/dist/components/sync-status/sync-status.js.map +1 -0
  198. package/dist/components/table/table.js +2 -2
  199. package/dist/components/table/table.js.map +1 -1
  200. package/dist/components/tags-input/tags-input.js +1 -1
  201. package/dist/components/tags-input/tags-input.js.map +1 -1
  202. package/dist/components/textarea/index.d.ts +2 -0
  203. package/dist/components/textarea/index.js +2 -0
  204. package/dist/components/textarea/meta.js +17 -0
  205. package/dist/components/textarea/meta.js.map +1 -0
  206. package/dist/components/textarea/textarea.d.ts +43 -0
  207. package/dist/components/textarea/textarea.d.ts.map +1 -0
  208. package/dist/components/textarea/textarea.js +119 -0
  209. package/dist/components/textarea/textarea.js.map +1 -0
  210. package/dist/components/time-range-picker/time-range-picker.js +3 -3
  211. package/dist/components/time-range-picker/time-range-picker.js.map +1 -1
  212. package/dist/components/toast/toast.d.ts.map +1 -1
  213. package/dist/components/toast/toast.js +2 -2
  214. package/dist/components/toast/toast.js.map +1 -1
  215. package/dist/index.d.ts +36 -2
  216. package/dist/index.js +22 -3
  217. package/dist/lib/styles.d.ts +16 -1
  218. package/dist/lib/styles.d.ts.map +1 -1
  219. package/dist/lib/styles.js +16 -1
  220. package/dist/lib/styles.js.map +1 -1
  221. package/dist/registry/blocks.d.ts.map +1 -1
  222. package/dist/registry/blocks.js +27 -9
  223. package/dist/registry/blocks.js.map +1 -1
  224. package/dist/registry/components.d.ts.map +1 -1
  225. package/dist/registry/components.js +84 -54
  226. package/dist/registry/components.js.map +1 -1
  227. package/dist/registry/schema.d.ts +8 -0
  228. package/dist/registry/schema.d.ts.map +1 -1
  229. package/dist/registry/schema.js.map +1 -1
  230. package/package.json +5 -4
  231. package/src/components/accordion/accordion.tsx +1 -1
  232. package/src/components/activity-feed/activity-feed.tsx +1 -1
  233. package/src/components/ai-agent-plan/ai-agent-plan.tsx +0 -0
  234. package/src/components/ai-extraction-review/ai-extraction-review.tsx +492 -0
  235. package/src/components/ai-extraction-review/extraction-model.ts +159 -0
  236. package/src/components/ai-extraction-review/index.ts +13 -0
  237. package/src/components/ai-extraction-review/meta.ts +24 -0
  238. package/src/components/ai-prompt-input/ai-prompt-input.tsx +1 -1
  239. package/src/components/ai-reasoning/ai-reasoning.tsx +1 -1
  240. package/src/components/ai-response/ai-response.tsx +2 -2
  241. package/src/components/ai-suggested-value/ai-suggested-value.tsx +295 -0
  242. package/src/components/ai-suggested-value/index.ts +6 -0
  243. package/src/components/ai-suggested-value/meta.ts +24 -0
  244. package/src/components/ai-tool/ai-tool.tsx +2 -2
  245. package/src/components/breadcrumb/breadcrumb.tsx +157 -0
  246. package/src/components/breadcrumb/index.ts +10 -0
  247. package/src/components/breadcrumb/meta.ts +19 -0
  248. package/src/components/calendar/calendar.tsx +2 -2
  249. package/src/components/code-block/code-block.tsx +1 -1
  250. package/src/components/collapsible/collapsible.tsx +63 -0
  251. package/src/components/collapsible/index.ts +6 -0
  252. package/src/components/collapsible/meta.ts +16 -0
  253. package/src/components/combobox/combobox.tsx +2 -2
  254. package/src/components/command/command.tsx +1 -1
  255. package/src/components/confirm-typed/confirm-typed.tsx +191 -0
  256. package/src/components/confirm-typed/index.ts +1 -0
  257. package/src/components/confirm-typed/meta.ts +22 -0
  258. package/src/components/cron-editor/cron-editor.tsx +676 -0
  259. package/src/components/cron-editor/cron-expression.ts +552 -0
  260. package/src/components/cron-editor/index.ts +23 -0
  261. package/src/components/cron-editor/meta.ts +23 -0
  262. package/src/components/data-table/data-table.tsx +14 -3
  263. package/src/components/dialog/dialog.tsx +2 -2
  264. package/src/components/diff-viewer/diff-viewer.tsx +4 -4
  265. package/src/components/direction/direction.tsx +38 -0
  266. package/src/components/direction/index.ts +1 -0
  267. package/src/components/direction/meta.ts +19 -0
  268. package/src/components/dns-record/dns-record.tsx +315 -0
  269. package/src/components/dns-record/index.ts +7 -0
  270. package/src/components/dns-record/meta.ts +21 -0
  271. package/src/components/drawer/drawer.tsx +1 -1
  272. package/src/components/dropdown-menu/dropdown-menu.tsx +14 -8
  273. package/src/components/input/input.tsx +1 -1
  274. package/src/components/log-viewer/log-viewer.tsx +2 -2
  275. package/src/components/meter/meter.tsx +1 -1
  276. package/src/components/pagination/pagination.tsx +13 -2
  277. package/src/components/permission-matrix/index.ts +14 -0
  278. package/src/components/permission-matrix/meta.ts +23 -0
  279. package/src/components/permission-matrix/permission-matrix.tsx +368 -0
  280. package/src/components/permission-matrix/permission-model.ts +120 -0
  281. package/src/components/record-diff/record-diff.tsx +4 -4
  282. package/src/components/secret-field/index.ts +6 -0
  283. package/src/components/secret-field/meta.ts +21 -0
  284. package/src/components/secret-field/secret-field.tsx +284 -0
  285. package/src/components/select/select.tsx +2 -2
  286. package/src/components/session-expiry/index.ts +9 -0
  287. package/src/components/session-expiry/meta.ts +23 -0
  288. package/src/components/session-expiry/session-expiry.tsx +321 -0
  289. package/src/components/sheet/sheet.tsx +8 -4
  290. package/src/components/shortcut-recorder/index.ts +19 -0
  291. package/src/components/shortcut-recorder/meta.ts +22 -0
  292. package/src/components/shortcut-recorder/shortcut-model.ts +204 -0
  293. package/src/components/shortcut-recorder/shortcut-recorder.tsx +336 -0
  294. package/src/components/sidebar/index.ts +19 -0
  295. package/src/components/sidebar/meta.ts +25 -0
  296. package/src/components/sidebar/sidebar.tsx +374 -0
  297. package/src/components/sync-status/index.ts +9 -0
  298. package/src/components/sync-status/meta.ts +21 -0
  299. package/src/components/sync-status/sync-status.tsx +217 -0
  300. package/src/components/table/table.tsx +3 -3
  301. package/src/components/tags-input/tags-input.tsx +1 -1
  302. package/src/components/textarea/index.ts +1 -0
  303. package/src/components/textarea/meta.ts +21 -0
  304. package/src/components/textarea/textarea.tsx +210 -0
  305. package/src/components/time-range-picker/time-range-picker.tsx +3 -3
  306. package/src/components/toast/toast.tsx +14 -6
  307. package/src/index.ts +15 -0
  308. package/src/lib/styles.ts +16 -0
  309. package/src/registry/blocks.ts +18 -0
  310. package/src/registry/components.ts +30 -0
  311. package/src/registry/schema.ts +8 -0
  312. package/src/blocks/admin-users/admin-users.tsx +0 -251
  313. package/src/blocks/admin-users/index.ts +0 -1
  314. package/src/blocks/admin-users/meta.ts +0 -27
  315. package/src/blocks/ai-chat/ai-chat.tsx +0 -280
  316. package/src/blocks/ai-chat/index.ts +0 -7
  317. package/src/blocks/ai-chat/meta.ts +0 -32
  318. package/src/blocks/dashboard/dashboard.tsx +0 -192
  319. package/src/blocks/dashboard/index.ts +0 -6
  320. package/src/blocks/dashboard/meta.ts +0 -18
  321. package/src/blocks/forgot-password/forgot-password.tsx +0 -158
  322. package/src/blocks/forgot-password/index.ts +0 -1
  323. package/src/blocks/forgot-password/meta.ts +0 -18
  324. package/src/blocks/login/index.ts +0 -1
  325. package/src/blocks/login/login.tsx +0 -178
  326. package/src/blocks/login/meta.ts +0 -18
  327. package/src/blocks/pricing/index.ts +0 -1
  328. package/src/blocks/pricing/meta.ts +0 -18
  329. package/src/blocks/pricing/pricing.tsx +0 -176
  330. package/src/blocks/settings/index.ts +0 -6
  331. package/src/blocks/settings/meta.ts +0 -29
  332. package/src/blocks/settings/settings.tsx +0 -317
  333. package/src/blocks/signup/index.ts +0 -1
  334. package/src/blocks/signup/meta.ts +0 -18
  335. package/src/blocks/signup/signup.tsx +0 -240
@@ -0,0 +1,249 @@
1
+ "use client";
2
+ import { disabledStyles, focusRing } from "../../lib/styles.js";
3
+ import { cn } from "../../lib/utils.js";
4
+ import { evidenceOf, isSourced, segmentSource, summarizeReview } from "./extraction-model.js";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ import { useEffect, useId, useMemo, useRef, useState } from "react";
7
+ //#region src/components/ai-extraction-review/ai-extraction-review.tsx
8
+ function ExtractionReview({ className, source, sourceLabel = "Source", heading, fields, decisions = {}, onDecision, streaming = false, lowConfidenceBelow = .7, children, ...props }) {
9
+ const headingId = useId();
10
+ const [activeField, setActiveField] = useState(null);
11
+ const runs = useMemo(() => segmentSource(source, fields), [source, fields]);
12
+ const summary = useMemo(() => summarizeReview(source, fields, decisions), [
13
+ source,
14
+ fields,
15
+ decisions
16
+ ]);
17
+ return /* @__PURE__ */ jsxs("section", {
18
+ "data-slot": "extraction-review",
19
+ "data-complete": summary.complete || void 0,
20
+ "aria-labelledby": headingId,
21
+ "aria-busy": streaming || void 0,
22
+ className: cn("flex flex-col gap-3", className),
23
+ ...props,
24
+ children: [
25
+ /* @__PURE__ */ jsxs("div", {
26
+ className: "flex flex-wrap items-baseline justify-between gap-2",
27
+ children: [/* @__PURE__ */ jsx("h3", {
28
+ id: headingId,
29
+ className: "text-sm font-medium",
30
+ children: heading
31
+ }), /* @__PURE__ */ jsxs("p", {
32
+ "data-slot": "extraction-review-summary",
33
+ "aria-live": "polite",
34
+ className: "text-xs text-muted-foreground",
35
+ children: [streaming ? "Fields are still arriving" : `${String(summary.reviewed)} of ${String(summary.total)} reviewed`, summary.unsourced.length > 0 ? ` · ${String(summary.unsourced.length)} without evidence` : null]
36
+ })]
37
+ }),
38
+ children,
39
+ /* @__PURE__ */ jsxs("div", {
40
+ className: "grid gap-4 md:grid-cols-2",
41
+ children: [/* @__PURE__ */ jsx(SourcePanel, {
42
+ label: sourceLabel,
43
+ runs,
44
+ active: activeField,
45
+ decisions
46
+ }), /* @__PURE__ */ jsx("ol", {
47
+ "data-slot": "extraction-fields",
48
+ className: "m-0 flex list-none flex-col gap-3 p-0",
49
+ children: fields.map((field) => /* @__PURE__ */ jsx(ReviewField, {
50
+ field,
51
+ source,
52
+ decision: decisions[field.name],
53
+ streaming,
54
+ lowConfidenceBelow,
55
+ active: activeField === field.name,
56
+ onActivate: () => {
57
+ setActiveField(field.name);
58
+ },
59
+ onDecision
60
+ }, field.name))
61
+ })]
62
+ })
63
+ ]
64
+ });
65
+ }
66
+ function SourcePanel({ label, runs, active, decisions }) {
67
+ const ref = useRef(null);
68
+ useEffect(() => {
69
+ if (!active || !ref.current) return;
70
+ ref.current.querySelector(`mark[data-active]`)?.scrollIntoView({ block: "nearest" });
71
+ }, [active]);
72
+ return /* @__PURE__ */ jsx("div", {
73
+ ref,
74
+ "data-slot": "extraction-source",
75
+ role: "region",
76
+ "aria-label": label,
77
+ tabIndex: 0,
78
+ className: cn("max-h-96 overflow-auto rounded-lg border border-border bg-muted/30 p-3 md:sticky md:top-0 md:self-start", focusRing),
79
+ children: runs.length === 0 ? /* @__PURE__ */ jsx("p", {
80
+ className: "text-xs text-muted-foreground",
81
+ children: "No source text."
82
+ }) : /* @__PURE__ */ jsx("pre", {
83
+ className: "m-0 font-sans text-sm leading-relaxed break-words whitespace-pre-wrap",
84
+ children: runs.map((run, index) => run.fields.length === 0 ? /* @__PURE__ */ jsx("span", { children: run.text }, index) : /* @__PURE__ */ jsx("mark", {
85
+ "data-fields": run.fields.join(" "),
86
+ "data-active": active !== null && run.fields.includes(active) ? "" : void 0,
87
+ className: cn("rounded-[2px] text-inherit", run.fields.every((name) => decisions[name]?.kind === "rejected") ? "bg-muted line-through decoration-muted-foreground" : "bg-primary/15", active !== null && run.fields.includes(active) && "bg-primary/30 ring-1 ring-primary ring-inset"),
88
+ children: run.text
89
+ }, index))
90
+ })
91
+ });
92
+ }
93
+ /** Whitespace collapsed and cut short, for quoting evidence under a field. */
94
+ function quote(text, limit = 80) {
95
+ const collapsed = text.replace(/\s+/g, " ").trim();
96
+ return collapsed.length > limit ? `${collapsed.slice(0, limit - 1)}…` : collapsed;
97
+ }
98
+ function ReviewField({ field, source, decision, streaming, lowConfidenceBelow, active, onActivate, onDecision }) {
99
+ const inputId = useId();
100
+ const evidenceId = useId();
101
+ const statusId = useId();
102
+ const confidenceId = useId();
103
+ const [draft, setDraft] = useState(null);
104
+ const proposed = field.value ?? "";
105
+ const decided = decision && decision.kind !== "rejected" ? decision.value : void 0;
106
+ const current = draft ?? decided ?? proposed;
107
+ const edited = current !== proposed;
108
+ const changedSince = decided !== void 0 && current !== decided;
109
+ const arriving = streaming && field.value === void 0;
110
+ const sourced = isSourced(source, field);
111
+ const evidence = sourced ? evidenceOf(source, field.span) : "";
112
+ const unsourced = !arriving && field.value !== void 0 && !sourced;
113
+ const accept = () => {
114
+ onDecision?.(field.name, edited ? {
115
+ kind: "corrected",
116
+ value: current,
117
+ proposed
118
+ } : {
119
+ kind: "accepted",
120
+ value: current
121
+ });
122
+ };
123
+ const onKeyDown = (event) => {
124
+ if (event.key !== "Enter") return;
125
+ if (event.nativeEvent.isComposing) return;
126
+ if (field.multiline && !(event.ctrlKey || event.metaKey)) return;
127
+ event.preventDefault();
128
+ accept();
129
+ };
130
+ const status = arriving ? "Arriving" : !decision ? "Not reviewed" : decision.kind === "rejected" ? "Rejected" : changedSince ? `Changed since it was ${decision.kind}` : decision.kind === "accepted" ? "Accepted" : decision.proposed ? `Corrected from “${quote(decision.proposed, 40)}”` : "Supplied by the reviewer";
131
+ const low = field.confidence !== void 0 && field.confidence < lowConfidenceBelow;
132
+ const percent = field.confidence === void 0 ? null : new Intl.NumberFormat(void 0, {
133
+ style: "percent",
134
+ maximumFractionDigits: 0
135
+ }).format(field.confidence);
136
+ const describedBy = [
137
+ evidenceId,
138
+ statusId,
139
+ percent !== null ? confidenceId : null
140
+ ].filter(Boolean).join(" ");
141
+ const controlClass = cn("w-full rounded-md border border-input bg-background px-2 py-1.5 text-sm", edited && "border-warning", focusRing);
142
+ return /* @__PURE__ */ jsxs("li", {
143
+ "data-slot": "extraction-field",
144
+ "data-field": field.name,
145
+ "data-state": arriving ? "arriving" : decision?.kind ?? "undecided",
146
+ "data-active": active || void 0,
147
+ "data-unsourced": unsourced || void 0,
148
+ onFocus: onActivate,
149
+ className: cn("flex flex-col gap-1 rounded-lg border p-3", active ? "border-primary" : "border-border", decision?.kind === "rejected" && "bg-muted/40"),
150
+ children: [
151
+ /* @__PURE__ */ jsxs("div", {
152
+ className: "flex flex-wrap items-baseline justify-between gap-2",
153
+ children: [onDecision && !arriving ? /* @__PURE__ */ jsx("label", {
154
+ htmlFor: inputId,
155
+ className: "text-xs font-medium text-muted-foreground",
156
+ children: field.label
157
+ }) : /* @__PURE__ */ jsx("span", {
158
+ className: "text-xs font-medium text-muted-foreground",
159
+ children: field.label
160
+ }), /* @__PURE__ */ jsx("span", {
161
+ id: statusId,
162
+ "data-slot": "extraction-field-status",
163
+ className: cn("text-xs", decision?.kind === "rejected" ? "text-destructive" : "text-muted-foreground", changedSince && "text-warning"),
164
+ children: status
165
+ })]
166
+ }),
167
+ arriving ? /* @__PURE__ */ jsx("span", {
168
+ "aria-hidden": "true",
169
+ className: "block h-8 w-full animate-pulse-soft rounded-md bg-muted"
170
+ }) : !onDecision ? /* @__PURE__ */ jsx("p", {
171
+ className: "rounded-md bg-muted px-2 py-1.5 text-sm break-words",
172
+ children: current || /* @__PURE__ */ jsx("span", {
173
+ className: "text-muted-foreground",
174
+ children: "Empty"
175
+ })
176
+ }) : field.multiline ? /* @__PURE__ */ jsx("textarea", {
177
+ id: inputId,
178
+ rows: 3,
179
+ value: current,
180
+ "aria-describedby": describedBy,
181
+ onChange: (event) => {
182
+ setDraft(event.target.value);
183
+ },
184
+ onKeyDown,
185
+ className: cn(controlClass, "resize-none")
186
+ }) : /* @__PURE__ */ jsx("input", {
187
+ id: inputId,
188
+ type: "text",
189
+ value: current,
190
+ "aria-describedby": describedBy,
191
+ onChange: (event) => {
192
+ setDraft(event.target.value);
193
+ },
194
+ onKeyDown,
195
+ className: controlClass
196
+ }),
197
+ /* @__PURE__ */ jsx("p", {
198
+ id: evidenceId,
199
+ "data-slot": "extraction-field-evidence",
200
+ className: cn("text-xs", unsourced ? "text-warning" : "text-muted-foreground"),
201
+ children: arriving ? "Waiting for the model" : sourced ? `In the source: “${quote(evidence)}”` : field.value === void 0 ? "The model found nothing for this field." : "Not in the source — the model supplied this without evidence."
202
+ }),
203
+ percent !== null ? /* @__PURE__ */ jsx("p", {
204
+ id: confidenceId,
205
+ "data-slot": "extraction-field-confidence",
206
+ "data-low": low || void 0,
207
+ className: cn("text-xs", low ? "text-warning" : "text-muted-foreground"),
208
+ children: low ? `Low confidence, ${percent} — worth checking` : `${percent} confidence`
209
+ }) : null,
210
+ onDecision && !arriving ? /* @__PURE__ */ jsxs("div", {
211
+ className: "mt-1 flex flex-wrap items-center gap-2",
212
+ children: [
213
+ /* @__PURE__ */ jsx(FieldButton, {
214
+ variant: "primary",
215
+ "aria-label": `${edited ? "Accept correction" : "Accept"} — ${field.label}`,
216
+ "aria-pressed": decision !== void 0 && decision.kind !== "rejected" && !changedSince,
217
+ onClick: accept,
218
+ children: edited ? "Accept correction" : "Accept"
219
+ }),
220
+ /* @__PURE__ */ jsx(FieldButton, {
221
+ "aria-label": `Reject — ${field.label}`,
222
+ "aria-pressed": decision?.kind === "rejected",
223
+ onClick: () => {
224
+ onDecision(field.name, { kind: "rejected" });
225
+ },
226
+ children: "Reject"
227
+ }),
228
+ sourced ? /* @__PURE__ */ jsx(FieldButton, {
229
+ "aria-label": `Show in source — ${field.label}`,
230
+ className: "ms-auto",
231
+ onClick: onActivate,
232
+ children: "Show in source"
233
+ }) : null
234
+ ]
235
+ }) : null
236
+ ]
237
+ });
238
+ }
239
+ function FieldButton({ className, variant = "default", ...props }) {
240
+ return /* @__PURE__ */ jsx("button", {
241
+ type: "button",
242
+ className: cn("rounded-md border px-2.5 py-1 text-xs font-medium transition-colors", variant === "primary" && "border-primary bg-primary text-primary-foreground hover:bg-primary-hover aria-pressed:bg-primary-active", variant === "default" && "border-input bg-background hover:bg-accent hover:text-accent-foreground aria-pressed:bg-secondary aria-pressed:text-secondary-foreground", focusRing, disabledStyles, className),
243
+ ...props
244
+ });
245
+ }
246
+ //#endregion
247
+ export { ExtractionReview };
248
+
249
+ //# sourceMappingURL=ai-extraction-review.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ai-extraction-review.js","names":[],"sources":["../../../src/components/ai-extraction-review/ai-extraction-review.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n type ComponentPropsWithRef,\n type KeyboardEvent,\n type ReactNode,\n} from \"react\";\n\nimport { disabledStyles, focusRing } from \"@/lib/styles\";\nimport { cn } from \"@/lib/utils\";\n\nimport {\n evidenceOf,\n isSourced,\n segmentSource,\n summarizeReview,\n type ExtractionField,\n type FieldDecision,\n type SourceRun,\n} from \"./extraction-model\";\n\n/**\n * The check after extraction: the document on one side, what the model read\n * out of it on the other, and a decision about every field.\n *\n * Every extraction demo shows the filled object and stops. The products that\n * run extraction for a living — invoice capture, KYC onboarding, claims intake —\n * all have this screen, each built from scratch, because a value that cannot\n * be checked can only be trusted, and nobody who is paying an invoice trusts a\n * model that far. No component library ships it.\n *\n * The link is the component. Each field carries where in the source it was\n * read from, quoted in text under the value as well as highlighted in the\n * document, so a reviewer who cannot see the highlight still has the evidence.\n * And a value with no evidence is said outright — \"the model supplied this\n * without evidence\" — because that is the case a review exists to catch, and\n * the one every filled-object view renders identically to a good value.\n *\n * Evidence is a text offset, not a bounding box. A language model reads text,\n * and a text layer with offsets is what every OCR pipeline already yields.\n * Boxes over a rendered page need page rendering, zoom and geometry, and are a\n * different component; this one does not pretend at them.\n *\n * Decisions are controlled and the component writes nothing. It reports that\n * a field was accepted as proposed, corrected from what was proposed, or\n * rejected — a richer record than a form's submitted values, and the one an\n * audit later needs — and leaves what to do with it to the application.\n */\n\nexport interface ExtractionReviewProps extends Omit<\n ComponentPropsWithRef<\"section\">,\n \"children\"\n> {\n /** What the model read. Every span indexes into it. */\n source: string;\n /** Names the source — \"Invoice 4471.pdf, page 1\". */\n sourceLabel?: string;\n /** What is being reviewed, as the heading. */\n heading: string;\n fields: ExtractionField[];\n /** Per-field decisions, keyed by field name. Controlled. */\n decisions?: Record<string, FieldDecision>;\n /** Omit to render the review as a record rather than something to operate. */\n onDecision?: (name: string, decision: FieldDecision) => void;\n /** True while fields are still arriving. Missing values wait instead of reading as not found. */\n streaming?: boolean;\n /** Confidence below this is called low, in words. */\n lowConfidenceBelow?: number;\n children?: ReactNode;\n}\n\nexport function ExtractionReview({\n className,\n source,\n sourceLabel = \"Source\",\n heading,\n fields,\n decisions = {},\n onDecision,\n streaming = false,\n lowConfidenceBelow = 0.7,\n children,\n ...props\n}: ExtractionReviewProps) {\n const headingId = useId();\n const [activeField, setActiveField] = useState<string | null>(null);\n\n const runs = useMemo(() => segmentSource(source, fields), [source, fields]);\n const summary = useMemo(\n () => summarizeReview(source, fields, decisions),\n [source, fields, decisions],\n );\n\n return (\n <section\n data-slot=\"extraction-review\"\n data-complete={summary.complete || undefined}\n aria-labelledby={headingId}\n aria-busy={streaming || undefined}\n className={cn(\"flex flex-col gap-3\", className)}\n {...props}\n >\n <div className=\"flex flex-wrap items-baseline justify-between gap-2\">\n <h3 id={headingId} className=\"text-sm font-medium\">\n {heading}\n </h3>\n {/* Live from the start and short, so each decision is followed by\n where the review stands rather than by silence. Evidence gaps are\n counted here too: they are the reason to read carefully, and worth\n knowing before starting rather than discovering on field six. */}\n <p\n data-slot=\"extraction-review-summary\"\n aria-live=\"polite\"\n className=\"text-xs text-muted-foreground\"\n >\n {streaming\n ? \"Fields are still arriving\"\n : `${String(summary.reviewed)} of ${String(summary.total)} reviewed`}\n {summary.unsourced.length > 0\n ? ` · ${String(summary.unsourced.length)} without evidence`\n : null}\n </p>\n </div>\n\n {children}\n\n <div className=\"grid gap-4 md:grid-cols-2\">\n <SourcePanel\n label={sourceLabel}\n runs={runs}\n active={activeField}\n decisions={decisions}\n />\n\n <ol data-slot=\"extraction-fields\" className=\"m-0 flex list-none flex-col gap-3 p-0\">\n {fields.map((field) => (\n <ReviewField\n key={field.name}\n field={field}\n source={source}\n decision={decisions[field.name]}\n streaming={streaming}\n lowConfidenceBelow={lowConfidenceBelow}\n active={activeField === field.name}\n onActivate={() => {\n setActiveField(field.name);\n }}\n onDecision={onDecision}\n />\n ))}\n </ol>\n </div>\n </section>\n );\n}\n\nfunction SourcePanel({\n label,\n runs,\n active,\n decisions,\n}: {\n label: string;\n runs: SourceRun[];\n active: string | null;\n decisions: Record<string, FieldDecision>;\n}) {\n const ref = useRef<HTMLDivElement>(null);\n\n // Bring the active field's evidence into view. Focus stays where it is —\n // in the field — so a keyboard user is shown the source without being\n // moved into it.\n useEffect(() => {\n if (!active || !ref.current) return;\n const mark = ref.current.querySelector(`mark[data-active]`);\n mark?.scrollIntoView({ block: \"nearest\" });\n }, [active]);\n\n return (\n <div\n ref={ref}\n data-slot=\"extraction-source\"\n // A scrolling box has to be focusable to be scrolled by keyboard, and a\n // focusable box needs a name.\n role=\"region\"\n aria-label={label}\n tabIndex={0}\n // Sticks beside the fields on a wide screen, so the evidence for field\n // nine is still in view when the reviewer reaches it.\n className={cn(\n \"max-h-96 overflow-auto rounded-lg border border-border bg-muted/30 p-3 md:sticky md:top-0 md:self-start\",\n focusRing,\n )}\n >\n {runs.length === 0 ? (\n <p className=\"text-xs text-muted-foreground\">No source text.</p>\n ) : (\n <pre className=\"m-0 font-sans text-sm leading-relaxed break-words whitespace-pre-wrap\">\n {runs.map((run, index) =>\n run.fields.length === 0 ? (\n <span key={index}>{run.text}</span>\n ) : (\n // A mark element, so the evidence survives as structure rather\n // than only as a tint. No label is inserted into the text: a\n // document read aloud with field names spliced into it is not\n // the document, and the field already quotes its evidence.\n <mark\n key={index}\n data-fields={run.fields.join(\" \")}\n data-active={active !== null && run.fields.includes(active) ? \"\" : undefined}\n className={cn(\n \"rounded-[2px] text-inherit\",\n run.fields.every((name) => decisions[name]?.kind === \"rejected\")\n ? \"bg-muted line-through decoration-muted-foreground\"\n : \"bg-primary/15\",\n active !== null &&\n run.fields.includes(active) &&\n \"bg-primary/30 ring-1 ring-primary ring-inset\",\n )}\n >\n {run.text}\n </mark>\n ),\n )}\n </pre>\n )}\n </div>\n );\n}\n\n/** Whitespace collapsed and cut short, for quoting evidence under a field. */\nfunction quote(text: string, limit = 80): string {\n const collapsed = text.replace(/\\s+/g, \" \").trim();\n return collapsed.length > limit ? `${collapsed.slice(0, limit - 1)}…` : collapsed;\n}\n\nfunction ReviewField({\n field,\n source,\n decision,\n streaming,\n lowConfidenceBelow,\n active,\n onActivate,\n onDecision,\n}: {\n field: ExtractionField;\n source: string;\n decision?: FieldDecision;\n streaming: boolean;\n lowConfidenceBelow: number;\n active: boolean;\n onActivate: () => void;\n onDecision?: (name: string, decision: FieldDecision) => void;\n}) {\n const inputId = useId();\n const evidenceId = useId();\n const statusId = useId();\n const confidenceId = useId();\n\n // The reviewer's typing, kept apart from the model's value so the decision\n // can say which is which.\n const [draft, setDraft] = useState<string | null>(null);\n\n const proposed = field.value ?? \"\";\n const decided = decision && decision.kind !== \"rejected\" ? decision.value : undefined;\n const current = draft ?? decided ?? proposed;\n const edited = current !== proposed;\n const changedSince = decided !== undefined && current !== decided;\n\n const arriving = streaming && field.value === undefined;\n const sourced = isSourced(source, field);\n const evidence = sourced ? evidenceOf(source, field.span) : \"\";\n const unsourced = !arriving && field.value !== undefined && !sourced;\n\n const accept = () => {\n onDecision?.(\n field.name,\n edited\n ? { kind: \"corrected\", value: current, proposed }\n : { kind: \"accepted\", value: current },\n );\n };\n\n const onKeyDown = (event: KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => {\n if (event.key !== \"Enter\") return;\n // Enter confirms an IME candidate before it means anything else. Without\n // this, a reviewer typing a Japanese correction accepts a fragment of it.\n if (event.nativeEvent.isComposing) return;\n if (field.multiline && !(event.ctrlKey || event.metaKey)) return;\n event.preventDefault();\n accept();\n };\n\n const status = arriving\n ? \"Arriving\"\n : !decision\n ? \"Not reviewed\"\n : decision.kind === \"rejected\"\n ? \"Rejected\"\n : changedSince\n ? `Changed since it was ${decision.kind}`\n : decision.kind === \"accepted\"\n ? \"Accepted\"\n : decision.proposed\n ? `Corrected from “${quote(decision.proposed, 40)}”`\n : \"Supplied by the reviewer\";\n\n const low = field.confidence !== undefined && field.confidence < lowConfidenceBelow;\n const percent =\n field.confidence === undefined\n ? null\n : new Intl.NumberFormat(undefined, { style: \"percent\", maximumFractionDigits: 0 }).format(\n field.confidence,\n );\n\n const describedBy = [evidenceId, statusId, percent !== null ? confidenceId : null]\n .filter(Boolean)\n .join(\" \");\n\n const controlClass = cn(\n \"w-full rounded-md border border-input bg-background px-2 py-1.5 text-sm\",\n edited && \"border-warning\",\n focusRing,\n );\n\n return (\n <li\n data-slot=\"extraction-field\"\n data-field={field.name}\n data-state={arriving ? \"arriving\" : (decision?.kind ?? \"undecided\")}\n data-active={active || undefined}\n data-unsourced={unsourced || undefined}\n // Focus anywhere in the field — value, either button — is the reviewer\n // looking at it, so the source follows without a separate step.\n onFocus={onActivate}\n className={cn(\n \"flex flex-col gap-1 rounded-lg border p-3\",\n active ? \"border-primary\" : \"border-border\",\n decision?.kind === \"rejected\" && \"bg-muted/40\",\n )}\n >\n <div className=\"flex flex-wrap items-baseline justify-between gap-2\">\n {onDecision && !arriving ? (\n <label htmlFor={inputId} className=\"text-xs font-medium text-muted-foreground\">\n {field.label}\n </label>\n ) : (\n <span className=\"text-xs font-medium text-muted-foreground\">{field.label}</span>\n )}\n {/* Status as a word, outside the label so editing never renames the\n control under a screen reader user. */}\n <span\n id={statusId}\n data-slot=\"extraction-field-status\"\n className={cn(\n \"text-xs\",\n decision?.kind === \"rejected\" ? \"text-destructive\" : \"text-muted-foreground\",\n changedSince && \"text-warning\",\n )}\n >\n {status}\n </span>\n </div>\n\n {arriving ? (\n <span\n aria-hidden=\"true\"\n className=\"block h-8 w-full animate-pulse-soft rounded-md bg-muted\"\n />\n ) : !onDecision ? (\n <p className=\"rounded-md bg-muted px-2 py-1.5 text-sm break-words\">\n {current || <span className=\"text-muted-foreground\">Empty</span>}\n </p>\n ) : field.multiline ? (\n <textarea\n id={inputId}\n rows={3}\n value={current}\n aria-describedby={describedBy}\n onChange={(event) => {\n setDraft(event.target.value);\n }}\n onKeyDown={onKeyDown}\n className={cn(controlClass, \"resize-none\")}\n />\n ) : (\n <input\n id={inputId}\n type=\"text\"\n value={current}\n aria-describedby={describedBy}\n onChange={(event) => {\n setDraft(event.target.value);\n }}\n onKeyDown={onKeyDown}\n className={controlClass}\n />\n )}\n\n {/* The evidence in text. This is the accessible path to the source, and\n for a sighted reviewer it is the comparison itself — \"1 March 2026\"\n beside \"2026-03-01\" is a normalisation, not an error, and only reads\n that way with both in view. */}\n <p\n id={evidenceId}\n data-slot=\"extraction-field-evidence\"\n className={cn(\"text-xs\", unsourced ? \"text-warning\" : \"text-muted-foreground\")}\n >\n {arriving\n ? \"Waiting for the model\"\n : sourced\n ? `In the source: “${quote(evidence)}”`\n : field.value === undefined\n ? \"The model found nothing for this field.\"\n : \"Not in the source — the model supplied this without evidence.\"}\n </p>\n\n {percent !== null ? (\n <p\n id={confidenceId}\n data-slot=\"extraction-field-confidence\"\n data-low={low || undefined}\n className={cn(\"text-xs\", low ? \"text-warning\" : \"text-muted-foreground\")}\n >\n {low ? `Low confidence, ${percent} — worth checking` : `${percent} confidence`}\n </p>\n ) : null}\n\n {onDecision && !arriving ? (\n <div className=\"mt-1 flex flex-wrap items-center gap-2\">\n <FieldButton\n variant=\"primary\"\n aria-label={`${edited ? \"Accept correction\" : \"Accept\"} — ${field.label}`}\n aria-pressed={\n decision !== undefined && decision.kind !== \"rejected\" && !changedSince\n }\n onClick={accept}\n >\n {edited ? \"Accept correction\" : \"Accept\"}\n </FieldButton>\n <FieldButton\n aria-label={`Reject — ${field.label}`}\n aria-pressed={decision?.kind === \"rejected\"}\n onClick={() => {\n onDecision(field.name, { kind: \"rejected\" });\n }}\n >\n Reject\n </FieldButton>\n {sourced ? (\n <FieldButton\n aria-label={`Show in source — ${field.label}`}\n className=\"ms-auto\"\n onClick={onActivate}\n >\n Show in source\n </FieldButton>\n ) : null}\n </div>\n ) : null}\n </li>\n );\n}\n\nfunction FieldButton({\n className,\n variant = \"default\",\n ...props\n}: ComponentPropsWithRef<\"button\"> & { variant?: \"default\" | \"primary\" }) {\n return (\n <button\n type=\"button\"\n className={cn(\n \"rounded-md border px-2.5 py-1 text-xs font-medium transition-colors\",\n variant === \"primary\" &&\n \"border-primary bg-primary text-primary-foreground hover:bg-primary-hover aria-pressed:bg-primary-active\",\n variant === \"default\" &&\n \"border-input bg-background hover:bg-accent hover:text-accent-foreground aria-pressed:bg-secondary aria-pressed:text-secondary-foreground\",\n focusRing,\n disabledStyles,\n className,\n )}\n {...props}\n />\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,94 @@
1
+ //#region src/components/ai-extraction-review/extraction-model.d.ts
2
+ /**
3
+ * The model behind a review of extracted fields: where each value was read
4
+ * from, what the reviewer decided about it, and how far through the review
5
+ * they are.
6
+ *
7
+ * Pure, so overlap handling and the completion rule are tested without
8
+ * rendering anything — and so an application can answer "is this review
9
+ * finished" on the server from the same decisions the component reports,
10
+ * rather than trusting a flag the client sent.
11
+ */
12
+ interface SourceSpan {
13
+ /** Offset into the source text in UTF-16 code units. Inclusive. */
14
+ start: number;
15
+ /** Exclusive. */
16
+ end: number;
17
+ }
18
+ interface ExtractionField {
19
+ /** Key in the extracted object. */
20
+ name: string;
21
+ label: string;
22
+ /**
23
+ * What the model extracted. Absent while streaming means it has not arrived;
24
+ * absent once streaming has ended means the model found nothing.
25
+ */
26
+ value?: string;
27
+ /**
28
+ * Where in the source the value was read from. Absent means the model
29
+ * supplied the value without evidence, which the review says outright.
30
+ */
31
+ span?: SourceSpan;
32
+ /** 0 to 1. Shown as a number, never only as a colour. */
33
+ confidence?: number;
34
+ /** A textarea instead of a single line. For addresses and descriptions. */
35
+ multiline?: boolean;
36
+ }
37
+ type FieldDecision =
38
+ /** The model's value, taken as it was. */
39
+ {
40
+ kind: "accepted";
41
+ value: string;
42
+ } |
43
+ /** The reviewer's value, with the model's kept so a record can tell them apart. */
44
+ {
45
+ kind: "corrected";
46
+ value: string;
47
+ proposed: string;
48
+ } |
49
+ /** Wrong and not corrected — the field does not apply, or cannot be known. */
50
+ {
51
+ kind: "rejected";
52
+ };
53
+ /** A run of source text and the fields whose evidence covers it. */
54
+ interface SourceRun {
55
+ text: string;
56
+ /** Field names, in declaration order. Empty for text nobody cites. */
57
+ fields: string[];
58
+ }
59
+ /**
60
+ * A span brought within the source, or null if nothing of it is.
61
+ *
62
+ * Offsets come from a model or an OCR layer and are wrong often enough that
63
+ * refusing to render on a bad one would leave the whole review blank. A span
64
+ * past the end is clamped; an inverted or empty one is treated as no evidence,
65
+ * which is what it is.
66
+ */
67
+ declare function clampSpan(span: SourceSpan, length: number): SourceSpan | null;
68
+ /** The text a span points at, or an empty string if it points at nothing. */
69
+ declare function evidenceOf(source: string, span: SourceSpan | undefined): string;
70
+ /** Whether a field's value can be checked against the source at all. */
71
+ declare function isSourced(source: string, field: ExtractionField): boolean;
72
+ /**
73
+ * Cuts the source into runs at every span boundary, so overlapping evidence —
74
+ * a total that sits inside the line that contains it — renders as nested
75
+ * coverage rather than as two marks fighting over the same characters.
76
+ */
77
+ declare function segmentSource(source: string, fields: ExtractionField[]): SourceRun[];
78
+ interface ReviewSummary {
79
+ total: number;
80
+ reviewed: number;
81
+ accepted: number;
82
+ corrected: number;
83
+ rejected: number;
84
+ /** Fields with a value the model could not point to in the source. */
85
+ unsourced: string[];
86
+ /** Fields still awaiting a decision. */
87
+ remaining: string[];
88
+ /** Every field has a decision. The only completion signal worth trusting. */
89
+ complete: boolean;
90
+ }
91
+ declare function summarizeReview(source: string, fields: ExtractionField[], decisions?: Record<string, FieldDecision>): ReviewSummary;
92
+ //#endregion
93
+ export { ExtractionField, FieldDecision, ReviewSummary, SourceRun, SourceSpan, clampSpan, evidenceOf, isSourced, segmentSource, summarizeReview };
94
+ //# sourceMappingURL=extraction-model.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extraction-model.d.ts","names":[],"sources":["../../../src/components/ai-extraction-review/extraction-model.ts"],"mappings":";;;;;;;;;;;UAWiB;;EAEf;;EAEA;;UAGe;;EAEf;EACA;;;;;EAKA;;;;;EAKA,OAAO;;EAEP;;EAEA;;KAGU;;;EAEN;EAAkB;;;;EAElB;EAAmB;EAAe;;;;EAElC;;;UAGW;EACf;;EAEA;;;;;;;;;;iBAWc,UAAU,MAAM,YAAY,iBAAiB;;iBAO7C,WAAW,gBAAgB,MAAM;;iBAOjC,UAAU,gBAAgB,OAAO;;;;;;iBASjC,cAAc,gBAAgB,QAAQ,oBAAoB;UA6BzD;EACf;EACA;EACA;EACA;EACA;;EAEA;;EAEA;;EAEA;;iBAGc,gBACd,gBACA,QAAQ,mBACR,YAAW,eAAe,iBACzB"}
@@ -0,0 +1,91 @@
1
+ //#region src/components/ai-extraction-review/extraction-model.ts
2
+ /**
3
+ * A span brought within the source, or null if nothing of it is.
4
+ *
5
+ * Offsets come from a model or an OCR layer and are wrong often enough that
6
+ * refusing to render on a bad one would leave the whole review blank. A span
7
+ * past the end is clamped; an inverted or empty one is treated as no evidence,
8
+ * which is what it is.
9
+ */
10
+ function clampSpan(span, length) {
11
+ const start = Math.max(0, Math.min(span.start, length));
12
+ const end = Math.max(0, Math.min(span.end, length));
13
+ return end > start ? {
14
+ start,
15
+ end
16
+ } : null;
17
+ }
18
+ /** The text a span points at, or an empty string if it points at nothing. */
19
+ function evidenceOf(source, span) {
20
+ if (!span) return "";
21
+ const clamped = clampSpan(span, source.length);
22
+ return clamped ? source.slice(clamped.start, clamped.end) : "";
23
+ }
24
+ /** Whether a field's value can be checked against the source at all. */
25
+ function isSourced(source, field) {
26
+ return field.span !== void 0 && clampSpan(field.span, source.length) !== null;
27
+ }
28
+ /**
29
+ * Cuts the source into runs at every span boundary, so overlapping evidence —
30
+ * a total that sits inside the line that contains it — renders as nested
31
+ * coverage rather than as two marks fighting over the same characters.
32
+ */
33
+ function segmentSource(source, fields) {
34
+ const spans = fields.flatMap((field) => {
35
+ const clamped = field.span ? clampSpan(field.span, source.length) : null;
36
+ return clamped ? [{
37
+ name: field.name,
38
+ ...clamped
39
+ }] : [];
40
+ });
41
+ if (source.length === 0) return [];
42
+ if (spans.length === 0) return [{
43
+ text: source,
44
+ fields: []
45
+ }];
46
+ const cuts = /* @__PURE__ */ new Set([0, source.length]);
47
+ for (const span of spans) {
48
+ cuts.add(span.start);
49
+ cuts.add(span.end);
50
+ }
51
+ const points = [...cuts].sort((a, b) => a - b);
52
+ const runs = [];
53
+ for (let index = 0; index < points.length - 1; index += 1) {
54
+ const start = points[index] ?? 0;
55
+ const end = points[index + 1] ?? start;
56
+ if (end <= start) continue;
57
+ runs.push({
58
+ text: source.slice(start, end),
59
+ fields: spans.filter((span) => span.start <= start && span.end >= end).map((s) => s.name)
60
+ });
61
+ }
62
+ return runs;
63
+ }
64
+ function summarizeReview(source, fields, decisions = {}) {
65
+ const summary = {
66
+ total: fields.length,
67
+ reviewed: 0,
68
+ accepted: 0,
69
+ corrected: 0,
70
+ rejected: 0,
71
+ unsourced: [],
72
+ remaining: [],
73
+ complete: false
74
+ };
75
+ for (const field of fields) {
76
+ if (field.value !== void 0 && !isSourced(source, field)) summary.unsourced.push(field.name);
77
+ const decision = decisions[field.name];
78
+ if (!decision) {
79
+ summary.remaining.push(field.name);
80
+ continue;
81
+ }
82
+ summary.reviewed += 1;
83
+ summary[decision.kind] += 1;
84
+ }
85
+ summary.complete = fields.length > 0 && summary.remaining.length === 0;
86
+ return summary;
87
+ }
88
+ //#endregion
89
+ export { clampSpan, evidenceOf, isSourced, segmentSource, summarizeReview };
90
+
91
+ //# sourceMappingURL=extraction-model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extraction-model.js","names":[],"sources":["../../../src/components/ai-extraction-review/extraction-model.ts"],"sourcesContent":["/**\n * The model behind a review of extracted fields: where each value was read\n * from, what the reviewer decided about it, and how far through the review\n * they are.\n *\n * Pure, so overlap handling and the completion rule are tested without\n * rendering anything — and so an application can answer \"is this review\n * finished\" on the server from the same decisions the component reports,\n * rather than trusting a flag the client sent.\n */\n\nexport interface SourceSpan {\n /** Offset into the source text in UTF-16 code units. Inclusive. */\n start: number;\n /** Exclusive. */\n end: number;\n}\n\nexport interface ExtractionField {\n /** Key in the extracted object. */\n name: string;\n label: string;\n /**\n * What the model extracted. Absent while streaming means it has not arrived;\n * absent once streaming has ended means the model found nothing.\n */\n value?: string;\n /**\n * Where in the source the value was read from. Absent means the model\n * supplied the value without evidence, which the review says outright.\n */\n span?: SourceSpan;\n /** 0 to 1. Shown as a number, never only as a colour. */\n confidence?: number;\n /** A textarea instead of a single line. For addresses and descriptions. */\n multiline?: boolean;\n}\n\nexport type FieldDecision =\n /** The model's value, taken as it was. */\n | { kind: \"accepted\"; value: string }\n /** The reviewer's value, with the model's kept so a record can tell them apart. */\n | { kind: \"corrected\"; value: string; proposed: string }\n /** Wrong and not corrected — the field does not apply, or cannot be known. */\n | { kind: \"rejected\" };\n\n/** A run of source text and the fields whose evidence covers it. */\nexport interface SourceRun {\n text: string;\n /** Field names, in declaration order. Empty for text nobody cites. */\n fields: string[];\n}\n\n/**\n * A span brought within the source, or null if nothing of it is.\n *\n * Offsets come from a model or an OCR layer and are wrong often enough that\n * refusing to render on a bad one would leave the whole review blank. A span\n * past the end is clamped; an inverted or empty one is treated as no evidence,\n * which is what it is.\n */\nexport function clampSpan(span: SourceSpan, length: number): SourceSpan | null {\n const start = Math.max(0, Math.min(span.start, length));\n const end = Math.max(0, Math.min(span.end, length));\n return end > start ? { start, end } : null;\n}\n\n/** The text a span points at, or an empty string if it points at nothing. */\nexport function evidenceOf(source: string, span: SourceSpan | undefined): string {\n if (!span) return \"\";\n const clamped = clampSpan(span, source.length);\n return clamped ? source.slice(clamped.start, clamped.end) : \"\";\n}\n\n/** Whether a field's value can be checked against the source at all. */\nexport function isSourced(source: string, field: ExtractionField): boolean {\n return field.span !== undefined && clampSpan(field.span, source.length) !== null;\n}\n\n/**\n * Cuts the source into runs at every span boundary, so overlapping evidence —\n * a total that sits inside the line that contains it — renders as nested\n * coverage rather than as two marks fighting over the same characters.\n */\nexport function segmentSource(source: string, fields: ExtractionField[]): SourceRun[] {\n const spans = fields.flatMap((field) => {\n const clamped = field.span ? clampSpan(field.span, source.length) : null;\n return clamped ? [{ name: field.name, ...clamped }] : [];\n });\n\n if (source.length === 0) return [];\n if (spans.length === 0) return [{ text: source, fields: [] }];\n\n const cuts = new Set<number>([0, source.length]);\n for (const span of spans) {\n cuts.add(span.start);\n cuts.add(span.end);\n }\n const points = [...cuts].sort((a, b) => a - b);\n\n const runs: SourceRun[] = [];\n for (let index = 0; index < points.length - 1; index += 1) {\n const start = points[index] ?? 0;\n const end = points[index + 1] ?? start;\n if (end <= start) continue;\n runs.push({\n text: source.slice(start, end),\n fields: spans.filter((span) => span.start <= start && span.end >= end).map((s) => s.name),\n });\n }\n return runs;\n}\n\nexport interface ReviewSummary {\n total: number;\n reviewed: number;\n accepted: number;\n corrected: number;\n rejected: number;\n /** Fields with a value the model could not point to in the source. */\n unsourced: string[];\n /** Fields still awaiting a decision. */\n remaining: string[];\n /** Every field has a decision. The only completion signal worth trusting. */\n complete: boolean;\n}\n\nexport function summarizeReview(\n source: string,\n fields: ExtractionField[],\n decisions: Record<string, FieldDecision> = {},\n): ReviewSummary {\n const summary: ReviewSummary = {\n total: fields.length,\n reviewed: 0,\n accepted: 0,\n corrected: 0,\n rejected: 0,\n unsourced: [],\n remaining: [],\n complete: false,\n };\n\n for (const field of fields) {\n if (field.value !== undefined && !isSourced(source, field)) {\n summary.unsourced.push(field.name);\n }\n const decision = decisions[field.name];\n if (!decision) {\n summary.remaining.push(field.name);\n continue;\n }\n summary.reviewed += 1;\n summary[decision.kind] += 1;\n }\n\n summary.complete = fields.length > 0 && summary.remaining.length === 0;\n return summary;\n}\n"],"mappings":";;;;;;;;;AA6DA,SAAgB,UAAU,MAAkB,QAAmC;CAC7E,MAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,OAAO,MAAM,CAAC;CACtD,MAAM,MAAM,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,KAAK,MAAM,CAAC;CAClD,OAAO,MAAM,QAAQ;EAAE;EAAO;CAAI,IAAI;AACxC;;AAGA,SAAgB,WAAW,QAAgB,MAAsC;CAC/E,IAAI,CAAC,MAAM,OAAO;CAClB,MAAM,UAAU,UAAU,MAAM,OAAO,MAAM;CAC7C,OAAO,UAAU,OAAO,MAAM,QAAQ,OAAO,QAAQ,GAAG,IAAI;AAC9D;;AAGA,SAAgB,UAAU,QAAgB,OAAiC;CACzE,OAAO,MAAM,SAAS,KAAA,KAAa,UAAU,MAAM,MAAM,OAAO,MAAM,MAAM;AAC9E;;;;;;AAOA,SAAgB,cAAc,QAAgB,QAAwC;CACpF,MAAM,QAAQ,OAAO,SAAS,UAAU;EACtC,MAAM,UAAU,MAAM,OAAO,UAAU,MAAM,MAAM,OAAO,MAAM,IAAI;EACpE,OAAO,UAAU,CAAC;GAAE,MAAM,MAAM;GAAM,GAAG;EAAQ,CAAC,IAAI,CAAC;CACzD,CAAC;CAED,IAAI,OAAO,WAAW,GAAG,OAAO,CAAC;CACjC,IAAI,MAAM,WAAW,GAAG,OAAO,CAAC;EAAE,MAAM;EAAQ,QAAQ,CAAC;CAAE,CAAC;CAE5D,MAAM,uBAAO,IAAI,IAAY,CAAC,GAAG,OAAO,MAAM,CAAC;CAC/C,KAAK,MAAM,QAAQ,OAAO;EACxB,KAAK,IAAI,KAAK,KAAK;EACnB,KAAK,IAAI,KAAK,GAAG;CACnB;CACA,MAAM,SAAS,CAAC,GAAG,IAAI,CAAC,CAAC,MAAM,GAAG,MAAM,IAAI,CAAC;CAE7C,MAAM,OAAoB,CAAC;CAC3B,KAAK,IAAI,QAAQ,GAAG,QAAQ,OAAO,SAAS,GAAG,SAAS,GAAG;EACzD,MAAM,QAAQ,OAAO,UAAU;EAC/B,MAAM,MAAM,OAAO,QAAQ,MAAM;EACjC,IAAI,OAAO,OAAO;EAClB,KAAK,KAAK;GACR,MAAM,OAAO,MAAM,OAAO,GAAG;GAC7B,QAAQ,MAAM,QAAQ,SAAS,KAAK,SAAS,SAAS,KAAK,OAAO,GAAG,CAAC,CAAC,KAAK,MAAM,EAAE,IAAI;EAC1F,CAAC;CACH;CACA,OAAO;AACT;AAgBA,SAAgB,gBACd,QACA,QACA,YAA2C,CAAC,GAC7B;CACf,MAAM,UAAyB;EAC7B,OAAO,OAAO;EACd,UAAU;EACV,UAAU;EACV,WAAW;EACX,UAAU;EACV,WAAW,CAAC;EACZ,WAAW,CAAC;EACZ,UAAU;CACZ;CAEA,KAAK,MAAM,SAAS,QAAQ;EAC1B,IAAI,MAAM,UAAU,KAAA,KAAa,CAAC,UAAU,QAAQ,KAAK,GACvD,QAAQ,UAAU,KAAK,MAAM,IAAI;EAEnC,MAAM,WAAW,UAAU,MAAM;EACjC,IAAI,CAAC,UAAU;GACb,QAAQ,UAAU,KAAK,MAAM,IAAI;GACjC;EACF;EACA,QAAQ,YAAY;EACpB,QAAQ,SAAS,SAAS;CAC5B;CAEA,QAAQ,WAAW,OAAO,SAAS,KAAK,QAAQ,UAAU,WAAW;CACrE,OAAO;AACT"}
@@ -0,0 +1,3 @@
1
+ import { ExtractionField, FieldDecision, ReviewSummary, SourceRun, SourceSpan, clampSpan, evidenceOf, isSourced, segmentSource, summarizeReview } from "./extraction-model.js";
2
+ import { ExtractionReview, ExtractionReviewProps } from "./ai-extraction-review.js";
3
+ export { type ExtractionField, ExtractionReview, type ExtractionReviewProps, type FieldDecision, type ReviewSummary, type SourceRun, type SourceSpan, clampSpan, evidenceOf, isSourced, segmentSource, summarizeReview };
@@ -0,0 +1,3 @@
1
+ import { clampSpan, evidenceOf, isSourced, segmentSource, summarizeReview } from "./extraction-model.js";
2
+ import { ExtractionReview } from "./ai-extraction-review.js";
3
+ export { ExtractionReview, clampSpan, evidenceOf, isSourced, segmentSource, summarizeReview };
@@ -0,0 +1,17 @@
1
+ import { defineMeta } from "../../registry/schema.js";
2
+ //#region src/components/ai-extraction-review/meta.ts
3
+ const meta = defineMeta({
4
+ name: "ai-extraction-review",
5
+ title: "AI Extraction Review",
6
+ description: "Check extracted fields against the document they came from, and decide each one.",
7
+ category: "ai",
8
+ status: "stable",
9
+ dependencies: [],
10
+ registryDependencies: [],
11
+ files: ["extraction-model.ts", "ai-extraction-review.tsx"],
12
+ a11y: "Every field quotes its evidence in text under the value, so a reviewer who cannot see the highlight in the source still has what the model read. A value with no evidence is said in words — never left to look like a good one. The source is a focusable named region because it scrolls, and its highlights are mark elements with nothing spliced into the text, since a document read aloud with field names inserted is no longer the document. Focus anywhere in a field brings its evidence into view without moving focus, so a keyboard user is shown the source rather than sent into it. Each control's name carries the field it belongs to, so ten Accept buttons are ten different buttons. Status is a word, outside the label so editing never renames the control, and the running count is a polite live region present from the start. Enter accepts, except while an IME is composing."
13
+ });
14
+ //#endregion
15
+ export { meta };
16
+
17
+ //# sourceMappingURL=meta.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"meta.js","names":[],"sources":["../../../src/components/ai-extraction-review/meta.ts"],"sourcesContent":["import { defineMeta } from \"@/registry/schema\";\n\nexport const meta = defineMeta({\n name: \"ai-extraction-review\",\n title: \"AI Extraction Review\",\n description:\n \"Check extracted fields against the document they came from, and decide each one.\",\n category: \"ai\",\n status: \"stable\",\n dependencies: [],\n registryDependencies: [],\n files: [\"extraction-model.ts\", \"ai-extraction-review.tsx\"],\n a11y:\n \"Every field quotes its evidence in text under the value, so a reviewer who cannot see the \" +\n \"highlight in the source still has what the model read. A value with no evidence is said in \" +\n \"words — never left to look like a good one. The source is a focusable named region because \" +\n \"it scrolls, and its highlights are mark elements with nothing spliced into the text, since a \" +\n \"document read aloud with field names inserted is no longer the document. Focus anywhere in a \" +\n \"field brings its evidence into view without moving focus, so a keyboard user is shown the \" +\n \"source rather than sent into it. Each control's name carries the field it belongs to, so \" +\n \"ten Accept buttons are ten different buttons. Status is a word, outside the label so editing \" +\n \"never renames the control, and the running count is a polite live region present from the \" +\n \"start. Enter accepts, except while an IME is composing.\",\n});\n"],"mappings":";;AAEA,MAAa,OAAO,WAAW;CAC7B,MAAM;CACN,OAAO;CACP,aACE;CACF,UAAU;CACV,QAAQ;CACR,cAAc,CAAC;CACf,sBAAsB,CAAC;CACvB,OAAO,CAAC,uBAAuB,0BAA0B;CACzD,MACE;AAUJ,CAAC"}
@@ -101,7 +101,7 @@ function PromptInputSubmit({ className, label = "Send message", stopLabel = "Sto
101
101
  "aria-label": busy ? stopLabel : label,
102
102
  disabled,
103
103
  onClick: busy ? onStop : void 0,
104
- className: cn("ml-auto grid size-8 shrink-0 place-items-center rounded-lg", "bg-primary text-primary-foreground transition-colors duration-[var(--duration-fast)]", "hover:bg-primary-hover disabled:pointer-events-none disabled:opacity-55", focusRing, className),
104
+ className: cn("ms-auto grid size-8 shrink-0 place-items-center rounded-lg", "bg-primary text-primary-foreground transition-colors duration-[var(--duration-fast)]", "hover:bg-primary-hover disabled:pointer-events-none disabled:opacity-55", focusRing, className),
105
105
  ...props,
106
106
  children: busy ? /* @__PURE__ */ jsx("svg", {
107
107
  viewBox: "0 0 24 24",
@@ -1 +1 @@
1
- {"version":3,"file":"ai-prompt-input.js","names":[],"sources":["../../../src/components/ai-prompt-input/ai-prompt-input.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n createContext,\n useContext,\n useId,\n useLayoutEffect,\n useMemo,\n useRef,\n type ComponentPropsWithRef,\n type FormEvent,\n type KeyboardEvent as ReactKeyboardEvent,\n} from \"react\";\n\nimport { focusRing, focusRingInset } from \"@/lib/styles\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * The composer.\n *\n * Handles the parts of a chat input that are easy to get subtly wrong:\n * auto-growing to fit the text up to a ceiling, sending on Enter without\n * breaking multi-line input, and — the one almost everybody misses —\n * not sending mid-composition when someone is typing with an IME.\n */\n\ninterface PromptInputContextValue {\n textareaId: string;\n submit: () => void;\n disabled: boolean;\n busy: boolean;\n}\n\nconst PromptInputContext = createContext<PromptInputContextValue | null>(null);\n\nfunction usePromptInput(component: string): PromptInputContextValue {\n const context = useContext(PromptInputContext);\n if (!context) {\n throw new Error(`${component} must be rendered inside <PromptInput>.`);\n }\n return context;\n}\n\nexport interface PromptInputProps extends Omit<ComponentPropsWithRef<\"form\">, \"onSubmit\"> {\n onSubmit?: (event: FormEvent<HTMLFormElement>) => void;\n /** Blocks submission and dims the composer. */\n disabled?: boolean;\n /** A response is in flight: submission is blocked and Stop is shown instead. */\n busy?: boolean;\n}\n\nexport function PromptInput({\n className,\n onSubmit,\n disabled = false,\n busy = false,\n children,\n ...props\n}: PromptInputProps) {\n const uid = useId();\n const formRef = useRef<HTMLFormElement | null>(null);\n\n const context = useMemo<PromptInputContextValue>(\n () => ({\n textareaId: `${uid}-textarea`,\n disabled,\n busy,\n submit: () => {\n formRef.current?.requestSubmit();\n },\n }),\n [uid, disabled, busy],\n );\n\n return (\n <PromptInputContext.Provider value={context}>\n <form\n ref={formRef}\n data-slot=\"prompt-input\"\n data-busy={busy || undefined}\n onSubmit={(event) => {\n if (disabled || busy) {\n event.preventDefault();\n return;\n }\n onSubmit?.(event);\n }}\n className={cn(\n \"rounded-xl border border-input bg-background shadow-xs\",\n \"transition-[border-color,box-shadow] duration-[var(--duration-fast)]\",\n \"focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/55\",\n disabled && \"opacity-60\",\n className,\n )}\n {...props}\n >\n {children}\n </form>\n </PromptInputContext.Provider>\n );\n}\n\n/** Rows the textarea will grow to before it starts scrolling. */\nconst DEFAULT_MAX_ROWS = 8;\n\nexport interface PromptInputTextareaProps extends Omit<\n ComponentPropsWithRef<\"textarea\">,\n \"rows\"\n> {\n maxRows?: number;\n /**\n * Send on Enter, newline on Shift+Enter.\n *\n * Turn it off for a composer where Enter should always insert a newline and\n * sending is only ever an explicit button press.\n */\n submitOnEnter?: boolean;\n}\n\nexport function PromptInputTextarea({\n className,\n maxRows = DEFAULT_MAX_ROWS,\n submitOnEnter = true,\n onKeyDown,\n onChange,\n ...props\n}: PromptInputTextareaProps) {\n const { textareaId, submit, disabled, busy } = usePromptInput(\"PromptInputTextarea\");\n const ref = useRef<HTMLTextAreaElement | null>(null);\n\n // Grows with the content up to maxRows, then scrolls. Measured after layout\n // so the height is right on the first paint rather than one frame late.\n useLayoutEffect(() => {\n const textarea = ref.current;\n if (!textarea) return;\n\n textarea.style.height = \"auto\";\n const lineHeight = Number.parseFloat(getComputedStyle(textarea).lineHeight) || 20;\n const maxHeight = lineHeight * maxRows;\n textarea.style.height = `${String(Math.min(textarea.scrollHeight, maxHeight))}px`;\n textarea.style.overflowY = textarea.scrollHeight > maxHeight ? \"auto\" : \"hidden\";\n });\n\n function handleKeyDown(event: ReactKeyboardEvent<HTMLTextAreaElement>) {\n onKeyDown?.(event);\n if (event.defaultPrevented || !submitOnEnter) return;\n if (event.key !== \"Enter\" || event.shiftKey) return;\n\n // Mid-composition Enter confirms the candidate an IME is offering — for\n // Japanese, Chinese, Korean and others it is part of typing a word, not a\n // request to send. Sending here truncates the sentence and is the single\n // most common way a chat composer breaks for those users.\n if (event.nativeEvent.isComposing) return;\n\n event.preventDefault();\n submit();\n }\n\n return (\n <textarea\n ref={ref}\n id={textareaId}\n data-slot=\"prompt-input-textarea\"\n rows={1}\n disabled={disabled}\n aria-disabled={busy || undefined}\n onKeyDown={handleKeyDown}\n onChange={onChange}\n className={cn(\n \"block w-full resize-none bg-transparent px-4 py-3 text-sm outline-none\",\n \"placeholder:text-muted-foreground disabled:cursor-not-allowed\",\n focusRingInset,\n \"focus-visible:ring-0\",\n className,\n )}\n {...props}\n />\n );\n}\n\n/** The row beneath the textarea: attachments, model picker, send. */\nexport function PromptInputToolbar({ className, ...props }: ComponentPropsWithRef<\"div\">) {\n return (\n <div\n data-slot=\"prompt-input-toolbar\"\n className={cn(\"flex items-center gap-2 px-2 pb-2\", className)}\n {...props}\n />\n );\n}\n\nexport interface PromptInputSubmitProps extends ComponentPropsWithRef<\"button\"> {\n /** Label while idle. */\n label?: string;\n /** Label while a response is streaming. */\n stopLabel?: string;\n /** Called instead of submitting when busy. */\n onStop?: () => void;\n}\n\n/**\n * Send, or stop.\n *\n * One control with two jobs, because that is what the space affords and what\n * people expect. The accessible name changes with the state, so it is never\n * \"Send\" while it actually stops.\n */\nexport function PromptInputSubmit({\n className,\n label = \"Send message\",\n stopLabel = \"Stop generating\",\n onStop,\n ...props\n}: PromptInputSubmitProps) {\n const { busy, disabled } = usePromptInput(\"PromptInputSubmit\");\n\n return (\n <button\n type={busy ? \"button\" : \"submit\"}\n data-slot=\"prompt-input-submit\"\n aria-label={busy ? stopLabel : label}\n disabled={disabled}\n onClick={busy ? onStop : undefined}\n className={cn(\n \"ml-auto grid size-8 shrink-0 place-items-center rounded-lg\",\n \"bg-primary text-primary-foreground transition-colors duration-[var(--duration-fast)]\",\n \"hover:bg-primary-hover disabled:pointer-events-none disabled:opacity-55\",\n focusRing,\n className,\n )}\n {...props}\n >\n {busy ? (\n <svg viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\" className=\"size-3.5\">\n <rect x=\"6\" y=\"6\" width=\"12\" height=\"12\" rx=\"2\" fill=\"currentColor\" />\n </svg>\n ) : (\n <svg viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\" className=\"size-4\">\n <path\n d=\"M12 19V5m0 0-6 6m6-6 6 6\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n )}\n </button>\n );\n}\n\nexport interface PromptInputCounterProps extends ComponentPropsWithRef<\"div\"> {\n value: number;\n max: number;\n /** Fraction of the limit past which the counter starts warning. */\n warnAt?: number;\n}\n\n/**\n * Characters used against a limit.\n *\n * The visible count updates on every keystroke; the live region does not.\n *\n * The naive version toggles `aria-live` on once the count matters, but a region\n * only announces changes that happen *while* it is live — switching it on at\n * the same moment the content changes means the one update worth hearing is the\n * one that is missed. So the region is live from the start and simply has\n * nothing in it until the limit is close, which is also what keeps ordinary\n * typing silent.\n */\nexport function PromptInputCounter({\n className,\n value,\n max,\n warnAt = 0.9,\n ...props\n}: PromptInputCounterProps) {\n const warning = value >= max * warnAt;\n const over = value > max;\n const remaining = max - value;\n\n return (\n <div\n data-slot=\"prompt-input-counter\"\n data-warning={warning || undefined}\n data-over={over || undefined}\n className={cn(\n \"text-xs tabular-nums\",\n over ? \"text-destructive\" : warning ? \"text-warning\" : \"text-muted-foreground\",\n className,\n )}\n {...props}\n >\n <span aria-hidden=\"true\">\n {warning ? `${String(value)} / ${String(max)}` : String(value)}\n </span>\n <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n {over\n ? `Over the limit by ${String(-remaining)} characters`\n : warning\n ? `${String(remaining)} characters remaining`\n : \"\"}\n </span>\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ai-prompt-input.js","names":[],"sources":["../../../src/components/ai-prompt-input/ai-prompt-input.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n createContext,\n useContext,\n useId,\n useLayoutEffect,\n useMemo,\n useRef,\n type ComponentPropsWithRef,\n type FormEvent,\n type KeyboardEvent as ReactKeyboardEvent,\n} from \"react\";\n\nimport { focusRing, focusRingInset } from \"@/lib/styles\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * The composer.\n *\n * Handles the parts of a chat input that are easy to get subtly wrong:\n * auto-growing to fit the text up to a ceiling, sending on Enter without\n * breaking multi-line input, and — the one almost everybody misses —\n * not sending mid-composition when someone is typing with an IME.\n */\n\ninterface PromptInputContextValue {\n textareaId: string;\n submit: () => void;\n disabled: boolean;\n busy: boolean;\n}\n\nconst PromptInputContext = createContext<PromptInputContextValue | null>(null);\n\nfunction usePromptInput(component: string): PromptInputContextValue {\n const context = useContext(PromptInputContext);\n if (!context) {\n throw new Error(`${component} must be rendered inside <PromptInput>.`);\n }\n return context;\n}\n\nexport interface PromptInputProps extends Omit<ComponentPropsWithRef<\"form\">, \"onSubmit\"> {\n onSubmit?: (event: FormEvent<HTMLFormElement>) => void;\n /** Blocks submission and dims the composer. */\n disabled?: boolean;\n /** A response is in flight: submission is blocked and Stop is shown instead. */\n busy?: boolean;\n}\n\nexport function PromptInput({\n className,\n onSubmit,\n disabled = false,\n busy = false,\n children,\n ...props\n}: PromptInputProps) {\n const uid = useId();\n const formRef = useRef<HTMLFormElement | null>(null);\n\n const context = useMemo<PromptInputContextValue>(\n () => ({\n textareaId: `${uid}-textarea`,\n disabled,\n busy,\n submit: () => {\n formRef.current?.requestSubmit();\n },\n }),\n [uid, disabled, busy],\n );\n\n return (\n <PromptInputContext.Provider value={context}>\n <form\n ref={formRef}\n data-slot=\"prompt-input\"\n data-busy={busy || undefined}\n onSubmit={(event) => {\n if (disabled || busy) {\n event.preventDefault();\n return;\n }\n onSubmit?.(event);\n }}\n className={cn(\n \"rounded-xl border border-input bg-background shadow-xs\",\n \"transition-[border-color,box-shadow] duration-[var(--duration-fast)]\",\n \"focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/55\",\n disabled && \"opacity-60\",\n className,\n )}\n {...props}\n >\n {children}\n </form>\n </PromptInputContext.Provider>\n );\n}\n\n/** Rows the textarea will grow to before it starts scrolling. */\nconst DEFAULT_MAX_ROWS = 8;\n\nexport interface PromptInputTextareaProps extends Omit<\n ComponentPropsWithRef<\"textarea\">,\n \"rows\"\n> {\n maxRows?: number;\n /**\n * Send on Enter, newline on Shift+Enter.\n *\n * Turn it off for a composer where Enter should always insert a newline and\n * sending is only ever an explicit button press.\n */\n submitOnEnter?: boolean;\n}\n\nexport function PromptInputTextarea({\n className,\n maxRows = DEFAULT_MAX_ROWS,\n submitOnEnter = true,\n onKeyDown,\n onChange,\n ...props\n}: PromptInputTextareaProps) {\n const { textareaId, submit, disabled, busy } = usePromptInput(\"PromptInputTextarea\");\n const ref = useRef<HTMLTextAreaElement | null>(null);\n\n // Grows with the content up to maxRows, then scrolls. Measured after layout\n // so the height is right on the first paint rather than one frame late.\n useLayoutEffect(() => {\n const textarea = ref.current;\n if (!textarea) return;\n\n textarea.style.height = \"auto\";\n const lineHeight = Number.parseFloat(getComputedStyle(textarea).lineHeight) || 20;\n const maxHeight = lineHeight * maxRows;\n textarea.style.height = `${String(Math.min(textarea.scrollHeight, maxHeight))}px`;\n textarea.style.overflowY = textarea.scrollHeight > maxHeight ? \"auto\" : \"hidden\";\n });\n\n function handleKeyDown(event: ReactKeyboardEvent<HTMLTextAreaElement>) {\n onKeyDown?.(event);\n if (event.defaultPrevented || !submitOnEnter) return;\n if (event.key !== \"Enter\" || event.shiftKey) return;\n\n // Mid-composition Enter confirms the candidate an IME is offering — for\n // Japanese, Chinese, Korean and others it is part of typing a word, not a\n // request to send. Sending here truncates the sentence and is the single\n // most common way a chat composer breaks for those users.\n if (event.nativeEvent.isComposing) return;\n\n event.preventDefault();\n submit();\n }\n\n return (\n <textarea\n ref={ref}\n id={textareaId}\n data-slot=\"prompt-input-textarea\"\n rows={1}\n disabled={disabled}\n aria-disabled={busy || undefined}\n onKeyDown={handleKeyDown}\n onChange={onChange}\n className={cn(\n \"block w-full resize-none bg-transparent px-4 py-3 text-sm outline-none\",\n \"placeholder:text-muted-foreground disabled:cursor-not-allowed\",\n focusRingInset,\n \"focus-visible:ring-0\",\n className,\n )}\n {...props}\n />\n );\n}\n\n/** The row beneath the textarea: attachments, model picker, send. */\nexport function PromptInputToolbar({ className, ...props }: ComponentPropsWithRef<\"div\">) {\n return (\n <div\n data-slot=\"prompt-input-toolbar\"\n className={cn(\"flex items-center gap-2 px-2 pb-2\", className)}\n {...props}\n />\n );\n}\n\nexport interface PromptInputSubmitProps extends ComponentPropsWithRef<\"button\"> {\n /** Label while idle. */\n label?: string;\n /** Label while a response is streaming. */\n stopLabel?: string;\n /** Called instead of submitting when busy. */\n onStop?: () => void;\n}\n\n/**\n * Send, or stop.\n *\n * One control with two jobs, because that is what the space affords and what\n * people expect. The accessible name changes with the state, so it is never\n * \"Send\" while it actually stops.\n */\nexport function PromptInputSubmit({\n className,\n label = \"Send message\",\n stopLabel = \"Stop generating\",\n onStop,\n ...props\n}: PromptInputSubmitProps) {\n const { busy, disabled } = usePromptInput(\"PromptInputSubmit\");\n\n return (\n <button\n type={busy ? \"button\" : \"submit\"}\n data-slot=\"prompt-input-submit\"\n aria-label={busy ? stopLabel : label}\n disabled={disabled}\n onClick={busy ? onStop : undefined}\n className={cn(\n \"ms-auto grid size-8 shrink-0 place-items-center rounded-lg\",\n \"bg-primary text-primary-foreground transition-colors duration-[var(--duration-fast)]\",\n \"hover:bg-primary-hover disabled:pointer-events-none disabled:opacity-55\",\n focusRing,\n className,\n )}\n {...props}\n >\n {busy ? (\n <svg viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\" className=\"size-3.5\">\n <rect x=\"6\" y=\"6\" width=\"12\" height=\"12\" rx=\"2\" fill=\"currentColor\" />\n </svg>\n ) : (\n <svg viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\" className=\"size-4\">\n <path\n d=\"M12 19V5m0 0-6 6m6-6 6 6\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n )}\n </button>\n );\n}\n\nexport interface PromptInputCounterProps extends ComponentPropsWithRef<\"div\"> {\n value: number;\n max: number;\n /** Fraction of the limit past which the counter starts warning. */\n warnAt?: number;\n}\n\n/**\n * Characters used against a limit.\n *\n * The visible count updates on every keystroke; the live region does not.\n *\n * The naive version toggles `aria-live` on once the count matters, but a region\n * only announces changes that happen *while* it is live — switching it on at\n * the same moment the content changes means the one update worth hearing is the\n * one that is missed. So the region is live from the start and simply has\n * nothing in it until the limit is close, which is also what keeps ordinary\n * typing silent.\n */\nexport function PromptInputCounter({\n className,\n value,\n max,\n warnAt = 0.9,\n ...props\n}: PromptInputCounterProps) {\n const warning = value >= max * warnAt;\n const over = value > max;\n const remaining = max - value;\n\n return (\n <div\n data-slot=\"prompt-input-counter\"\n data-warning={warning || undefined}\n data-over={over || undefined}\n className={cn(\n \"text-xs tabular-nums\",\n over ? \"text-destructive\" : warning ? \"text-warning\" : \"text-muted-foreground\",\n className,\n )}\n {...props}\n >\n <span aria-hidden=\"true\">\n {warning ? `${String(value)} / ${String(max)}` : String(value)}\n </span>\n <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n {over\n ? `Over the limit by ${String(-remaining)} characters`\n : warning\n ? `${String(remaining)} characters remaining`\n : \"\"}\n </span>\n </div>\n );\n}\n"],"mappings":"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"}
@@ -53,7 +53,7 @@ function ReasoningContent({ className, children, ...props }) {
53
53
  className: cn("overflow-hidden", "data-[state=closed]:animate-accordion-close data-[state=open]:animate-accordion-open", className),
54
54
  ...props,
55
55
  children: /* @__PURE__ */ jsx("div", {
56
- className: "mt-2 border-l-2 border-border pl-3 text-sm whitespace-pre-wrap text-muted-foreground",
56
+ className: "mt-2 border-s-2 border-border ps-3 text-sm whitespace-pre-wrap text-muted-foreground",
57
57
  children
58
58
  })
59
59
  });