@purposeinplay/payload-ai-translate 0.1.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 (301) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +714 -0
  3. package/dist/alerts-collection.d.ts +21 -0
  4. package/dist/alerts-collection.js +159 -0
  5. package/dist/api.d.ts +4 -0
  6. package/dist/api.js +918 -0
  7. package/dist/bulk-translate-batches-collection.d.ts +29 -0
  8. package/dist/bulk-translate-batches-collection.js +404 -0
  9. package/dist/bulk-translate-units-collection.d.ts +35 -0
  10. package/dist/bulk-translate-units-collection.js +310 -0
  11. package/dist/client/estimated-cost-cell.d.ts +6 -0
  12. package/dist/client/estimated-cost-cell.js +12 -0
  13. package/dist/client/excluded-fields-field.d.ts +45 -0
  14. package/dist/client/excluded-fields-field.js +553 -0
  15. package/dist/client/field-translate-button.d.ts +6 -0
  16. package/dist/client/field-translate-button.js +199 -0
  17. package/dist/client/index.d.ts +6 -0
  18. package/dist/client/index.js +6 -0
  19. package/dist/client/lib/use-global-kill-switches.d.ts +20 -0
  20. package/dist/client/lib/use-global-kill-switches.js +58 -0
  21. package/dist/client/translate-button.d.ts +2 -0
  22. package/dist/client/translate-button.js +228 -0
  23. package/dist/client/translate-modal.d.ts +16 -0
  24. package/dist/client/translate-modal.js +549 -0
  25. package/dist/client/translation-progress.d.ts +10 -0
  26. package/dist/client/translation-progress.js +297 -0
  27. package/dist/components/TranslationNavGroup.d.ts +45 -0
  28. package/dist/components/TranslationNavGroup.js +104 -0
  29. package/dist/defaults.d.ts +11 -0
  30. package/dist/defaults.js +16 -0
  31. package/dist/endpoints/client-config.d.ts +44 -0
  32. package/dist/endpoints/client-config.js +145 -0
  33. package/dist/endpoints/estimate.d.ts +5 -0
  34. package/dist/endpoints/estimate.js +237 -0
  35. package/dist/endpoints/progress.d.ts +2 -0
  36. package/dist/endpoints/progress.js +314 -0
  37. package/dist/endpoints/translate.d.ts +11 -0
  38. package/dist/endpoints/translate.js +376 -0
  39. package/dist/endpoints/translation-hub/_helpers.d.ts +140 -0
  40. package/dist/endpoints/translation-hub/_helpers.js +297 -0
  41. package/dist/endpoints/translation-hub/active.d.ts +21 -0
  42. package/dist/endpoints/translation-hub/active.js +220 -0
  43. package/dist/endpoints/translation-hub/cancel.d.ts +22 -0
  44. package/dist/endpoints/translation-hub/cancel.js +233 -0
  45. package/dist/endpoints/translation-hub/enqueue.d.ts +70 -0
  46. package/dist/endpoints/translation-hub/enqueue.js +529 -0
  47. package/dist/endpoints/translation-hub/failures.d.ts +12 -0
  48. package/dist/endpoints/translation-hub/failures.js +67 -0
  49. package/dist/endpoints/translation-hub/force-reset.d.ts +20 -0
  50. package/dist/endpoints/translation-hub/force-reset.js +144 -0
  51. package/dist/endpoints/translation-hub/index.d.ts +21 -0
  52. package/dist/endpoints/translation-hub/index.js +20 -0
  53. package/dist/endpoints/translation-hub/list.d.ts +40 -0
  54. package/dist/endpoints/translation-hub/list.js +182 -0
  55. package/dist/endpoints/translation-hub/preflight.d.ts +19 -0
  56. package/dist/endpoints/translation-hub/preflight.js +141 -0
  57. package/dist/endpoints/translation-hub/retry-failed.d.ts +38 -0
  58. package/dist/endpoints/translation-hub/retry-failed.js +235 -0
  59. package/dist/endpoints/translation-hub/revert.d.ts +88 -0
  60. package/dist/endpoints/translation-hub/revert.js +405 -0
  61. package/dist/endpoints/translation-hub/status.d.ts +45 -0
  62. package/dist/endpoints/translation-hub/status.js +391 -0
  63. package/dist/endpoints/translation-hub/usage-summary.d.ts +114 -0
  64. package/dist/endpoints/translation-hub/usage-summary.js +481 -0
  65. package/dist/exports/client.d.ts +6 -0
  66. package/dist/exports/client.js +6 -0
  67. package/dist/exports/components.d.ts +6 -0
  68. package/dist/exports/components.js +5 -0
  69. package/dist/exports/index.d.ts +8 -0
  70. package/dist/exports/index.js +7 -0
  71. package/dist/exports/providers.d.ts +9 -0
  72. package/dist/exports/providers.js +5 -0
  73. package/dist/exports/views-client.d.ts +23 -0
  74. package/dist/exports/views-client.js +22 -0
  75. package/dist/exports/views.d.ts +30 -0
  76. package/dist/exports/views.js +29 -0
  77. package/dist/hooks/after-change-global.d.ts +4 -0
  78. package/dist/hooks/after-change-global.js +109 -0
  79. package/dist/hooks/after-change.d.ts +16 -0
  80. package/dist/hooks/after-change.js +205 -0
  81. package/dist/hooks/after-delete.d.ts +30 -0
  82. package/dist/hooks/after-delete.js +95 -0
  83. package/dist/index.d.ts +5 -0
  84. package/dist/index.js +5 -0
  85. package/dist/jobs-collection.d.ts +17 -0
  86. package/dist/jobs-collection.js +139 -0
  87. package/dist/lexical/classifier.d.ts +3 -0
  88. package/dist/lexical/classifier.js +108 -0
  89. package/dist/lexical/deserializer.d.ts +4 -0
  90. package/dist/lexical/deserializer.js +263 -0
  91. package/dist/lexical/placeholder-integrity.d.ts +6 -0
  92. package/dist/lexical/placeholder-integrity.js +21 -0
  93. package/dist/lexical/placeholders.d.ts +21 -0
  94. package/dist/lexical/placeholders.js +117 -0
  95. package/dist/lexical/serializer.d.ts +21 -0
  96. package/dist/lexical/serializer.js +233 -0
  97. package/dist/lexical/types.d.ts +32 -0
  98. package/dist/lexical/types.js +1 -0
  99. package/dist/lib/auth-diagnostics.d.ts +14 -0
  100. package/dist/lib/auth-diagnostics.js +19 -0
  101. package/dist/lib/batch-counts.d.ts +58 -0
  102. package/dist/lib/batch-counts.js +105 -0
  103. package/dist/lib/bulk-translate-migrations.d.ts +92 -0
  104. package/dist/lib/bulk-translate-migrations.js +153 -0
  105. package/dist/lib/coalescing-queue.d.ts +38 -0
  106. package/dist/lib/coalescing-queue.js +69 -0
  107. package/dist/lib/content-extractor.d.ts +16 -0
  108. package/dist/lib/content-extractor.js +410 -0
  109. package/dist/lib/content-hash.d.ts +1 -0
  110. package/dist/lib/content-hash.js +19 -0
  111. package/dist/lib/content-patcher.d.ts +15 -0
  112. package/dist/lib/content-patcher.js +293 -0
  113. package/dist/lib/cost-guards.d.ts +2 -0
  114. package/dist/lib/cost-guards.js +18 -0
  115. package/dist/lib/daily-spend-cap.d.ts +58 -0
  116. package/dist/lib/daily-spend-cap.js +233 -0
  117. package/dist/lib/effective-locales.d.ts +181 -0
  118. package/dist/lib/effective-locales.js +302 -0
  119. package/dist/lib/error-messages.d.ts +245 -0
  120. package/dist/lib/error-messages.js +626 -0
  121. package/dist/lib/events.d.ts +39 -0
  122. package/dist/lib/events.js +146 -0
  123. package/dist/lib/exclude-fields.d.ts +3 -0
  124. package/dist/lib/exclude-fields.js +64 -0
  125. package/dist/lib/field-breadcrumb.d.ts +31 -0
  126. package/dist/lib/field-breadcrumb.js +227 -0
  127. package/dist/lib/field-diff.d.ts +1 -0
  128. package/dist/lib/field-diff.js +25 -0
  129. package/dist/lib/field-empty.d.ts +2 -0
  130. package/dist/lib/field-empty.js +68 -0
  131. package/dist/lib/field-resolver.d.ts +3 -0
  132. package/dist/lib/field-resolver.js +164 -0
  133. package/dist/lib/group-soft-skips.d.ts +39 -0
  134. package/dist/lib/group-soft-skips.js +45 -0
  135. package/dist/lib/locale-merge.d.ts +44 -0
  136. package/dist/lib/locale-merge.js +357 -0
  137. package/dist/lib/locale-row-check.d.ts +30 -0
  138. package/dist/lib/locale-row-check.js +64 -0
  139. package/dist/lib/logger.d.ts +74 -0
  140. package/dist/lib/logger.js +97 -0
  141. package/dist/lib/manual-edit-guard.d.ts +128 -0
  142. package/dist/lib/manual-edit-guard.js +393 -0
  143. package/dist/lib/output-validation.d.ts +48 -0
  144. package/dist/lib/output-validation.js +148 -0
  145. package/dist/lib/payload-read.d.ts +16 -0
  146. package/dist/lib/payload-read.js +51 -0
  147. package/dist/lib/per-doc-claim.d.ts +90 -0
  148. package/dist/lib/per-doc-claim.js +140 -0
  149. package/dist/lib/per-doc-lock.d.ts +94 -0
  150. package/dist/lib/per-doc-lock.js +119 -0
  151. package/dist/lib/persist-usage.d.ts +91 -0
  152. package/dist/lib/persist-usage.js +116 -0
  153. package/dist/lib/progress-store.d.ts +103 -0
  154. package/dist/lib/progress-store.js +314 -0
  155. package/dist/lib/rate-limiter.d.ts +3 -0
  156. package/dist/lib/rate-limiter.js +53 -0
  157. package/dist/lib/snapshot-select.d.ts +43 -0
  158. package/dist/lib/snapshot-select.js +108 -0
  159. package/dist/lib/translate-prompt.d.ts +31 -0
  160. package/dist/lib/translate-prompt.js +66 -0
  161. package/dist/lib/translation-token-bucket.d.ts +57 -0
  162. package/dist/lib/translation-token-bucket.js +365 -0
  163. package/dist/lib/truncate-source-value.d.ts +1 -0
  164. package/dist/lib/truncate-source-value.js +27 -0
  165. package/dist/manual-edit-collection.d.ts +22 -0
  166. package/dist/manual-edit-collection.js +124 -0
  167. package/dist/plugin.d.ts +3 -0
  168. package/dist/plugin.js +934 -0
  169. package/dist/providers/ai-sdk-adapter.d.ts +35 -0
  170. package/dist/providers/ai-sdk-adapter.js +100 -0
  171. package/dist/providers/anthropic.d.ts +31 -0
  172. package/dist/providers/anthropic.js +66 -0
  173. package/dist/providers/custom.d.ts +36 -0
  174. package/dist/providers/custom.js +24 -0
  175. package/dist/providers/gemini.d.ts +20 -0
  176. package/dist/providers/gemini.js +48 -0
  177. package/dist/providers/mock.d.ts +2 -0
  178. package/dist/providers/mock.js +29 -0
  179. package/dist/providers/openai.d.ts +28 -0
  180. package/dist/providers/openai.js +69 -0
  181. package/dist/settings-global.d.ts +74 -0
  182. package/dist/settings-global.js +216 -0
  183. package/dist/tasks/bulk-translate-coordinator.d.ts +115 -0
  184. package/dist/tasks/bulk-translate-coordinator.js +708 -0
  185. package/dist/tasks/bulk-translate-doc-task.d.ts +142 -0
  186. package/dist/tasks/bulk-translate-doc-task.js +1000 -0
  187. package/dist/tasks/bulk-translate-janitor.d.ts +87 -0
  188. package/dist/tasks/bulk-translate-janitor.js +311 -0
  189. package/dist/tasks/translate-job-task.d.ts +51 -0
  190. package/dist/tasks/translate-job-task.js +154 -0
  191. package/dist/translate.d.ts +113 -0
  192. package/dist/translate.js +911 -0
  193. package/dist/translation-daily-spend-collection.d.ts +24 -0
  194. package/dist/translation-daily-spend-collection.js +133 -0
  195. package/dist/translation-rate-limits-collection.d.ts +30 -0
  196. package/dist/translation-rate-limits-collection.js +144 -0
  197. package/dist/types.d.ts +672 -0
  198. package/dist/types.js +1 -0
  199. package/dist/usage-collection.d.ts +14 -0
  200. package/dist/usage-collection.js +377 -0
  201. package/dist/views/BulkRunsHub/BatchRow.d.ts +32 -0
  202. package/dist/views/BulkRunsHub/BatchRow.js +1222 -0
  203. package/dist/views/BulkRunsHub/BucketRow.d.ts +62 -0
  204. package/dist/views/BulkRunsHub/BucketRow.js +982 -0
  205. package/dist/views/BulkRunsHub/BulkRunsHub.client.d.ts +18 -0
  206. package/dist/views/BulkRunsHub/BulkRunsHub.client.js +331 -0
  207. package/dist/views/BulkRunsHub/EmptyState.d.ts +6 -0
  208. package/dist/views/BulkRunsHub/EmptyState.js +64 -0
  209. package/dist/views/BulkRunsHub/FilterBar.d.ts +16 -0
  210. package/dist/views/BulkRunsHub/FilterBar.js +284 -0
  211. package/dist/views/BulkRunsHub/InFlightBanner.d.ts +14 -0
  212. package/dist/views/BulkRunsHub/InFlightBanner.js +59 -0
  213. package/dist/views/BulkRunsHub/StatusBadge.d.ts +64 -0
  214. package/dist/views/BulkRunsHub/StatusBadge.js +248 -0
  215. package/dist/views/BulkRunsHub/SummaryStrip.d.ts +22 -0
  216. package/dist/views/BulkRunsHub/SummaryStrip.js +249 -0
  217. package/dist/views/BulkRunsHub/bucket-grouping.d.ts +200 -0
  218. package/dist/views/BulkRunsHub/bucket-grouping.js +344 -0
  219. package/dist/views/BulkRunsHub/bucketFailureSummary.d.ts +9 -0
  220. package/dist/views/BulkRunsHub/bucketFailureSummary.js +36 -0
  221. package/dist/views/BulkRunsHub/dedupedStatusFetch.d.ts +5 -0
  222. package/dist/views/BulkRunsHub/dedupedStatusFetch.js +45 -0
  223. package/dist/views/BulkRunsHub/index.d.ts +17 -0
  224. package/dist/views/BulkRunsHub/index.js +80 -0
  225. package/dist/views/BulkRunsHub/urlFilters.d.ts +14 -0
  226. package/dist/views/BulkRunsHub/urlFilters.js +50 -0
  227. package/dist/views/BulkRunsHub/useBulkRunsList.d.ts +26 -0
  228. package/dist/views/BulkRunsHub/useBulkRunsList.js +204 -0
  229. package/dist/views/BulkRunsHub/useUrlFilters.d.ts +10 -0
  230. package/dist/views/BulkRunsHub/useUrlFilters.js +88 -0
  231. package/dist/views/TranslationHub/ActiveJobs.d.ts +6 -0
  232. package/dist/views/TranslationHub/ActiveJobs.js +320 -0
  233. package/dist/views/TranslationHub/AdvancedPanel.d.ts +17 -0
  234. package/dist/views/TranslationHub/AdvancedPanel.js +996 -0
  235. package/dist/views/TranslationHub/AlertBanner.d.ts +6 -0
  236. package/dist/views/TranslationHub/AlertBanner.js +568 -0
  237. package/dist/views/TranslationHub/AuditPanel.d.ts +6 -0
  238. package/dist/views/TranslationHub/AuditPanel.helpers.d.ts +44 -0
  239. package/dist/views/TranslationHub/AuditPanel.helpers.js +71 -0
  240. package/dist/views/TranslationHub/AuditPanel.js +1367 -0
  241. package/dist/views/TranslationHub/BulkTranslate.types.d.ts +242 -0
  242. package/dist/views/TranslationHub/BulkTranslate.types.js +36 -0
  243. package/dist/views/TranslationHub/BulkTranslateFailureDrawer.d.ts +19 -0
  244. package/dist/views/TranslationHub/BulkTranslateFailureDrawer.js +332 -0
  245. package/dist/views/TranslationHub/BulkTranslateMonitor.d.ts +28 -0
  246. package/dist/views/TranslationHub/BulkTranslateMonitor.js +305 -0
  247. package/dist/views/TranslationHub/BulkTranslateNarrowViewportBanner.d.ts +3 -0
  248. package/dist/views/TranslationHub/BulkTranslateNarrowViewportBanner.js +42 -0
  249. package/dist/views/TranslationHub/BulkTranslatePostEnqueueTransition.d.ts +26 -0
  250. package/dist/views/TranslationHub/BulkTranslatePostEnqueueTransition.js +95 -0
  251. package/dist/views/TranslationHub/BulkTranslatePreflightModal.d.ts +22 -0
  252. package/dist/views/TranslationHub/BulkTranslatePreflightModal.js +879 -0
  253. package/dist/views/TranslationHub/BulkTranslateTerminalCard.d.ts +29 -0
  254. package/dist/views/TranslationHub/BulkTranslateTerminalCard.js +445 -0
  255. package/dist/views/TranslationHub/BulkTranslateTrigger.d.ts +66 -0
  256. package/dist/views/TranslationHub/BulkTranslateTrigger.js +161 -0
  257. package/dist/views/TranslationHub/EditorRecentRunsPanel.d.ts +33 -0
  258. package/dist/views/TranslationHub/EditorRecentRunsPanel.js +290 -0
  259. package/dist/views/TranslationHub/Hub.client.d.ts +74 -0
  260. package/dist/views/TranslationHub/Hub.client.js +357 -0
  261. package/dist/views/TranslationHub/ModelCombobox.d.ts +14 -0
  262. package/dist/views/TranslationHub/ModelCombobox.js +415 -0
  263. package/dist/views/TranslationHub/PerCollectionConfig.d.ts +10 -0
  264. package/dist/views/TranslationHub/PerCollectionConfig.helpers.d.ts +16 -0
  265. package/dist/views/TranslationHub/PerCollectionConfig.helpers.js +19 -0
  266. package/dist/views/TranslationHub/PerCollectionConfig.js +759 -0
  267. package/dist/views/TranslationHub/SettingsRail.d.ts +11 -0
  268. package/dist/views/TranslationHub/SettingsRail.js +382 -0
  269. package/dist/views/TranslationHub/StatusStrip.d.ts +6 -0
  270. package/dist/views/TranslationHub/StatusStrip.js +451 -0
  271. package/dist/views/TranslationHub/UsageTable.d.ts +6 -0
  272. package/dist/views/TranslationHub/UsageTable.helpers.d.ts +69 -0
  273. package/dist/views/TranslationHub/UsageTable.helpers.js +49 -0
  274. package/dist/views/TranslationHub/UsageTable.js +1240 -0
  275. package/dist/views/TranslationHub/alertGrouping.d.ts +70 -0
  276. package/dist/views/TranslationHub/alertGrouping.js +99 -0
  277. package/dist/views/TranslationHub/index.d.ts +20 -0
  278. package/dist/views/TranslationHub/index.js +109 -0
  279. package/dist/views/TranslationHub/tabNavigation.d.ts +53 -0
  280. package/dist/views/TranslationHub/tabNavigation.js +74 -0
  281. package/dist/views/TranslationHub/terminalBannerVisibility.d.ts +33 -0
  282. package/dist/views/TranslationHub/terminalBannerVisibility.js +124 -0
  283. package/dist/views/TranslationHub/useBulkTranslateActive.d.ts +49 -0
  284. package/dist/views/TranslationHub/useBulkTranslateActive.js +251 -0
  285. package/dist/views/TranslationHub/useFocusTrap.d.ts +6 -0
  286. package/dist/views/TranslationHub/useFocusTrap.js +81 -0
  287. package/dist/views/TranslationHub/useTranslationHubUsageSummary.d.ts +77 -0
  288. package/dist/views/TranslationHub/useTranslationHubUsageSummary.js +267 -0
  289. package/dist/views/shared/EditorError.d.ts +97 -0
  290. package/dist/views/shared/EditorError.js +205 -0
  291. package/dist/views/shared/ModelCell.d.ts +18 -0
  292. package/dist/views/shared/ModelCell.js +31 -0
  293. package/dist/views/shared/docHref.d.ts +16 -0
  294. package/dist/views/shared/docHref.js +26 -0
  295. package/dist/views/shared/fetch-error-body.d.ts +25 -0
  296. package/dist/views/shared/fetch-error-body.js +42 -0
  297. package/dist/views/shared/filterPillStyle.d.ts +35 -0
  298. package/dist/views/shared/filterPillStyle.js +40 -0
  299. package/dist/views/shared/format.d.ts +75 -0
  300. package/dist/views/shared/format.js +131 -0
  301. package/package.json +141 -0
@@ -0,0 +1,415 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import React, { useEffect, useMemo, useRef, useState } from 'react';
4
+ import { readResponseError } from '../shared/fetch-error-body.js';
5
+ const VENDOR_LABEL = {
6
+ openai: 'OpenAI',
7
+ anthropic: 'Anthropic',
8
+ google: 'Google',
9
+ meta: 'Meta',
10
+ other: 'Other'
11
+ };
12
+ const VENDOR_ORDER = [
13
+ 'openai',
14
+ 'anthropic',
15
+ 'google',
16
+ 'meta',
17
+ 'other'
18
+ ];
19
+ function filter(models, q) {
20
+ const query = q.trim().toLowerCase();
21
+ if (!query) {
22
+ return models;
23
+ }
24
+ return models.filter((m)=>m.id.toLowerCase().includes(query) || m.name.toLowerCase().includes(query));
25
+ }
26
+ function group(models) {
27
+ const buckets = new Map();
28
+ for (const m of models){
29
+ let bucket = buckets.get(m.vendor);
30
+ if (!bucket) {
31
+ bucket = [];
32
+ buckets.set(m.vendor, bucket);
33
+ }
34
+ bucket.push(m);
35
+ }
36
+ const out = [];
37
+ for (const v of VENDOR_ORDER){
38
+ const bucket = buckets.get(v);
39
+ if (bucket) {
40
+ out.push({
41
+ vendor: v,
42
+ models: [
43
+ ...bucket
44
+ ].sort((a, b)=>a.id.localeCompare(b.id))
45
+ });
46
+ }
47
+ }
48
+ return out;
49
+ }
50
+ // ROUND2-3: canonical slug pattern. Display value MUST match this so
51
+ // `:invalid` styling doesn't trip on the loaded-correct field. Free-text
52
+ // typed values that don't match show an inline `role="alert"` message
53
+ // and `aria-invalid="true"`.
54
+ const CANONICAL_SLUG_PATTERN = '^$|^[a-z0-9.\\-]+/[a-z0-9.\\-]+$';
55
+ const CANONICAL_SLUG_REGEX = /^[a-z0-9.-]+\/[a-z0-9.-]+$/;
56
+ /**
57
+ * Compact combobox for the Translation Hub's settings rail. Same data
58
+ * source as the standalone OpenRouter Settings page (`/api/openrouter/models`)
59
+ * but slimmer styling — fits the 320px rail.
60
+ */ export const ModelCombobox = ({ basePath, value, disabled, onChange })=>{
61
+ const [models, setModels] = useState(null);
62
+ const [error, setError] = useState(null);
63
+ const [fromFallback, setFromFallback] = useState(false);
64
+ const [query, setQuery] = useState('');
65
+ const [open, setOpen] = useState(false);
66
+ const [highlightIdx, setHighlightIdx] = useState(0);
67
+ // ROUND2-3: track whether the current query string (free-text typed
68
+ // by the user while the popover is open) parses to a canonical slug.
69
+ // Drives the inline alert + aria-invalid. Distinct from the
70
+ // `savedMissing` warning, which fires when the *saved* value isn't
71
+ // in the catalog.
72
+ const [showInvalidAlert, setShowInvalidAlert] = useState(false);
73
+ const ref = useRef(null);
74
+ const inputRef = useRef(null);
75
+ // ROUND3-1: the last canonical slug we know is good — either the
76
+ // initially-mounted `value` prop or whatever was most-recently picked
77
+ // from the listbox. `handleBlur` restores this into the displayed
78
+ // input string on soft-revert instead of clearing to empty. The
79
+ // empty-input-with-populated-caption gap that editors saw post-blur
80
+ // came from `setQuery('')` paired with the conditional
81
+ // `value={open ? query : currentSlug}` — when the input loses focus
82
+ // we want the *displayed* string to be the canonical slug, not the
83
+ // last typed garbage and not an empty string.
84
+ const lastPickedSlugRef = useRef(value);
85
+ useEffect(()=>{
86
+ let cancelled = false;
87
+ fetch(`${basePath}/api/openrouter/models`, {
88
+ credentials: 'include'
89
+ }).then(async (r)=>{
90
+ if (!r.ok) {
91
+ throw new Error(await readResponseError(r));
92
+ }
93
+ return await r.json();
94
+ }).then((d)=>{
95
+ if (cancelled) {
96
+ return;
97
+ }
98
+ setModels(d.models);
99
+ setFromFallback(d.fromFallback);
100
+ }).catch((e)=>{
101
+ if (cancelled) {
102
+ return;
103
+ }
104
+ setError(e instanceof Error ? e.message : String(e));
105
+ });
106
+ return ()=>{
107
+ cancelled = true;
108
+ };
109
+ }, [
110
+ basePath
111
+ ]);
112
+ useEffect(()=>{
113
+ if (!open) {
114
+ return;
115
+ }
116
+ function close(e) {
117
+ if (!ref.current?.contains(e.target)) {
118
+ setOpen(false);
119
+ }
120
+ }
121
+ document.addEventListener('mousedown', close);
122
+ return ()=>document.removeEventListener('mousedown', close);
123
+ }, [
124
+ open
125
+ ]);
126
+ const filtered = useMemo(()=>filter(models ?? [], query), [
127
+ models,
128
+ query
129
+ ]);
130
+ const grouped = useMemo(()=>group(filtered), [
131
+ filtered
132
+ ]);
133
+ const flat = useMemo(()=>grouped.flatMap((g)=>g.models), [
134
+ grouped
135
+ ]);
136
+ const savedMissing = Boolean(value && models && !models.some((m)=>m.id === value));
137
+ // ROUND2-3: the displayed input value is the canonical slug only —
138
+ // "google/gemini-2.5-flash", not "Google: Gemini 2.5 Flash
139
+ // (google/gemini-2.5-flash)". The pretty form failed the `pattern=`
140
+ // regex on its own display value, which tripped `:invalid` styling
141
+ // even when the field was correct. Human label moves to the caption
142
+ // below (aria-describedby).
143
+ const currentSlug = value;
144
+ // ROUND3-1: keep the soft-revert anchor in sync with the
145
+ // controlling parent — when the prop changes (e.g. after a server
146
+ // save), the next blur restores the new server value, not the
147
+ // pre-save one.
148
+ useEffect(()=>{
149
+ if (value) {
150
+ lastPickedSlugRef.current = value;
151
+ }
152
+ }, [
153
+ value
154
+ ]);
155
+ const humanLabel = useMemo(()=>{
156
+ if (!value) {
157
+ return '';
158
+ }
159
+ const hit = models?.find((m)=>m.id === value);
160
+ return hit ? hit.name : '';
161
+ }, [
162
+ value,
163
+ models
164
+ ]);
165
+ useEffect(()=>setHighlightIdx(0), []);
166
+ function pick(id) {
167
+ lastPickedSlugRef.current = id;
168
+ onChange(id);
169
+ setOpen(false);
170
+ setQuery('');
171
+ setShowInvalidAlert(false);
172
+ inputRef.current?.blur();
173
+ }
174
+ // ROUND2-3 / ROUND3-1: soft-revert on blur. If the user typed a
175
+ // non-canonical value (something that didn't match a listbox option),
176
+ // restore the last-known canonical slug into the visible input rather
177
+ // than leaving it empty. ROUND2-3 cleared `query` which — combined
178
+ // with `value={open ? query : currentSlug}` — looked correct on
179
+ // paper, but `setOpen(false)` runs concurrently with the input losing
180
+ // focus, so editors saw the input flash empty before the next render
181
+ // settled. Round-3 verification surfaced this as the "empty input
182
+ // next to populated caption" UX gap. Closing the popover here and
183
+ // explicitly tracking the last-picked slug fixes both: the input
184
+ // reads the ref's canonical value on the next render and the editor
185
+ // sees their typo dismissed in favour of the previously-saved model.
186
+ function handleBlur() {
187
+ setShowInvalidAlert(false);
188
+ setQuery('');
189
+ setOpen(false);
190
+ }
191
+ function keyDown(e) {
192
+ if (e.key === 'Escape') {
193
+ setOpen(false);
194
+ setQuery('');
195
+ } else if (e.key === 'ArrowDown') {
196
+ e.preventDefault();
197
+ if (!open) {
198
+ setOpen(true);
199
+ }
200
+ setHighlightIdx((i)=>Math.min(i + 1, flat.length - 1));
201
+ } else if (e.key === 'ArrowUp') {
202
+ e.preventDefault();
203
+ setHighlightIdx((i)=>Math.max(i - 1, 0));
204
+ } else if (e.key === 'Enter') {
205
+ e.preventDefault();
206
+ const t = flat[highlightIdx];
207
+ if (t) {
208
+ pick(t.id);
209
+ }
210
+ }
211
+ }
212
+ return /*#__PURE__*/ _jsxs("div", {
213
+ ref: ref,
214
+ style: {
215
+ position: 'relative'
216
+ },
217
+ children: [
218
+ fromFallback && /*#__PURE__*/ _jsx("p", {
219
+ style: {
220
+ margin: '0 0 0.5rem',
221
+ color: 'var(--theme-warning-500, #b45309)',
222
+ fontSize: '0.75rem'
223
+ },
224
+ children: "Live catalog unreachable — showing fallback."
225
+ }),
226
+ savedMissing && /*#__PURE__*/ _jsx("p", {
227
+ style: {
228
+ margin: '0 0 0.5rem',
229
+ color: 'var(--theme-warning-500, #b45309)',
230
+ fontSize: '0.75rem'
231
+ },
232
+ children: "Saved model not in current catalog — translation falls back to default."
233
+ }),
234
+ error && /*#__PURE__*/ _jsx("p", {
235
+ style: {
236
+ margin: '0 0 0.5rem',
237
+ color: 'var(--theme-error-500, #b91c1c)',
238
+ fontSize: '0.75rem'
239
+ },
240
+ children: error
241
+ }),
242
+ /*#__PURE__*/ _jsx("input", {
243
+ "aria-autocomplete": "list",
244
+ "aria-controls": open ? 'model-combobox-listbox' : undefined,
245
+ "aria-describedby": humanLabel || showInvalidAlert ? 'model-combobox-caption' : undefined,
246
+ "aria-expanded": open,
247
+ "aria-haspopup": "listbox",
248
+ // ROUND2-3: reflect `validity.patternMismatch` so AT users hear
249
+ // "invalid entry" instead of silently typing into a field that
250
+ // ignores them. Drives the inline alert too.
251
+ "aria-invalid": showInvalidAlert || undefined,
252
+ autoComplete: "off",
253
+ disabled: disabled || models === null && !error,
254
+ // Permissive pattern — accepts a typed-in vendor/slug id with
255
+ // the OpenRouter canonical shape; downstream picker still
256
+ // validates against the live `models` catalog. Empty allowed
257
+ // so the user can clear the field. ROUND2-3: the *displayed*
258
+ // value is now the canonical slug, so this pattern no longer
259
+ // trips on its own loaded value.
260
+ list: "model-combobox-known-models",
261
+ onBlur: handleBlur,
262
+ onChange: (e)=>{
263
+ const next = e.target.value;
264
+ setQuery(next);
265
+ if (!open) {
266
+ setOpen(true);
267
+ }
268
+ // ROUND2-3: visible feedback for non-canonical free-text
269
+ // typing. Empty is allowed (clears the field); anything else
270
+ // must match the canonical slug regex. The save-side guard
271
+ // already gates `onChange` on listbox-pick only — this is
272
+ // purely UI feedback.
273
+ setShowInvalidAlert(next.length > 0 && !CANONICAL_SLUG_REGEX.test(next));
274
+ },
275
+ onFocus: ()=>setOpen(true),
276
+ onKeyDown: keyDown,
277
+ pattern: CANONICAL_SLUG_PATTERN,
278
+ placeholder: models === null && !error ? 'Loading…' : 'Search models…',
279
+ ref: inputRef,
280
+ role: "combobox",
281
+ style: {
282
+ width: '100%',
283
+ padding: '0.5rem 0.75rem',
284
+ background: 'var(--theme-elevation-50)',
285
+ border: showInvalidAlert ? '1px solid var(--theme-error-500, #b91c1c)' : '1px solid var(--theme-elevation-150)',
286
+ borderRadius: '4px',
287
+ color: 'var(--theme-elevation-1000)',
288
+ font: 'inherit'
289
+ },
290
+ type: "text",
291
+ value: open ? query : currentSlug
292
+ }),
293
+ showInvalidAlert ? /*#__PURE__*/ _jsxs("span", {
294
+ id: "model-combobox-caption",
295
+ role: "alert",
296
+ style: {
297
+ display: 'block',
298
+ marginTop: '0.25rem',
299
+ fontSize: '0.7rem',
300
+ color: 'var(--theme-error-500, #b91c1c)'
301
+ },
302
+ children: [
303
+ "Use the format ",
304
+ /*#__PURE__*/ _jsx("code", {
305
+ children: "vendor/model"
306
+ }),
307
+ " — e.g. ",
308
+ /*#__PURE__*/ _jsx("code", {
309
+ children: "google/gemini-2.5-flash"
310
+ }),
311
+ "."
312
+ ]
313
+ }) : humanLabel ? /*#__PURE__*/ _jsx("span", {
314
+ id: "model-combobox-caption",
315
+ style: {
316
+ display: 'block',
317
+ marginTop: '0.25rem',
318
+ fontSize: '0.7rem',
319
+ color: 'var(--theme-elevation-500)'
320
+ },
321
+ children: humanLabel
322
+ }) : null,
323
+ models && /*#__PURE__*/ _jsx("datalist", {
324
+ id: "model-combobox-known-models",
325
+ children: models.map((m)=>/*#__PURE__*/ _jsx("option", {
326
+ value: m.id,
327
+ children: m.name
328
+ }, m.id))
329
+ }),
330
+ open && models && /*#__PURE__*/ _jsx("div", {
331
+ id: "model-combobox-listbox",
332
+ role: "listbox",
333
+ style: {
334
+ position: 'absolute',
335
+ top: '100%',
336
+ left: 0,
337
+ right: 0,
338
+ zIndex: 10,
339
+ marginTop: '4px',
340
+ maxHeight: '280px',
341
+ overflowY: 'auto',
342
+ background: 'var(--theme-elevation-50)',
343
+ border: '1px solid var(--theme-elevation-150)',
344
+ borderRadius: '4px',
345
+ boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
346
+ fontSize: '0.875rem'
347
+ },
348
+ children: grouped.length === 0 ? /*#__PURE__*/ _jsxs("div", {
349
+ style: {
350
+ padding: '0.4rem 0.75rem',
351
+ color: 'var(--theme-elevation-500)'
352
+ },
353
+ children: [
354
+ 'No models match "',
355
+ query,
356
+ '"'
357
+ ]
358
+ }) : grouped.map(({ vendor, models: mods })=>/*#__PURE__*/ _jsxs(React.Fragment, {
359
+ children: [
360
+ /*#__PURE__*/ _jsx("div", {
361
+ style: {
362
+ padding: '0.3rem 0.75rem',
363
+ background: 'var(--theme-elevation-100)',
364
+ fontSize: '0.7rem',
365
+ fontWeight: 600,
366
+ textTransform: 'uppercase',
367
+ color: 'var(--theme-elevation-500)',
368
+ position: 'sticky',
369
+ top: 0
370
+ },
371
+ children: VENDOR_LABEL[vendor]
372
+ }),
373
+ mods.map((m)=>{
374
+ const i = flat.indexOf(m);
375
+ const hl = i === highlightIdx;
376
+ const sel = m.id === value;
377
+ return /*#__PURE__*/ _jsxs("div", {
378
+ "aria-selected": sel,
379
+ onMouseDown: (e)=>{
380
+ e.preventDefault();
381
+ pick(m.id);
382
+ },
383
+ onMouseEnter: ()=>setHighlightIdx(i),
384
+ role: "option",
385
+ style: {
386
+ padding: '0.4rem 0.75rem',
387
+ cursor: 'pointer',
388
+ background: hl ? 'var(--theme-elevation-100)' : 'transparent',
389
+ fontWeight: sel ? 600 : 400,
390
+ color: 'var(--theme-elevation-1000)'
391
+ },
392
+ tabIndex: -1,
393
+ children: [
394
+ m.name,
395
+ ' ',
396
+ /*#__PURE__*/ _jsxs("span", {
397
+ style: {
398
+ color: 'var(--theme-elevation-500)',
399
+ fontSize: '0.75rem'
400
+ },
401
+ children: [
402
+ "(",
403
+ m.id,
404
+ ")"
405
+ ]
406
+ })
407
+ ]
408
+ }, m.id);
409
+ })
410
+ ]
411
+ }, vendor))
412
+ })
413
+ ]
414
+ });
415
+ };
@@ -0,0 +1,10 @@
1
+ import type React from 'react';
2
+ import type { LocaleConfig, TranslationSettings } from './Hub.client.js';
3
+ interface Props {
4
+ basePath: string;
5
+ locales: LocaleConfig;
6
+ onSettingsChange: (next: TranslationSettings) => void;
7
+ translationSettings: TranslationSettings | null;
8
+ }
9
+ export declare const PerCollectionConfig: React.FC<Props>;
10
+ export default PerCollectionConfig;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Re-exports the pure `summarize` helper from PerCollectionConfig.tsx so it
3
+ * can be unit-tested without importing the full React component.
4
+ *
5
+ * Keeping the logic here ensures a single source of truth — the component
6
+ * imports from this file rather than defining the function inline.
7
+ */
8
+ export type PerCollectionEntry = {
9
+ slug: string;
10
+ enabled?: boolean | null;
11
+ autoOnPublish?: boolean | null;
12
+ targetLocalesOverride?: string[] | null;
13
+ translateSlug?: boolean | null;
14
+ excludedFieldPaths?: string[] | null;
15
+ };
16
+ export declare function summarize(row: PerCollectionEntry, fieldCount: number | null): string;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Re-exports the pure `summarize` helper from PerCollectionConfig.tsx so it
3
+ * can be unit-tested without importing the full React component.
4
+ *
5
+ * Keeping the logic here ensures a single source of truth — the component
6
+ * imports from this file rather than defining the function inline.
7
+ */ export function summarize(row, fieldCount) {
8
+ const bits = [];
9
+ if (row.enabled === false) bits.push('DISABLED');
10
+ if (row.autoOnPublish === false) bits.push('manual only');
11
+ if (row.targetLocalesOverride?.length) {
12
+ bits.push(`locales: ${row.targetLocalesOverride.join('/')}`);
13
+ }
14
+ if (row.translateSlug) bits.push('slug translated');
15
+ if (row.excludedFieldPaths?.length) {
16
+ bits.push(fieldCount != null ? `${row.excludedFieldPaths.length}/${fieldCount} fields excluded` : `${row.excludedFieldPaths.length} fields excluded`);
17
+ }
18
+ return bits.length === 0 ? 'inherits site-wide defaults' : bits.join(' · ');
19
+ }