@agent-native/creative-context 0.0.0-beta-20260820202238

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 (695) hide show
  1. package/README.md +154 -0
  2. package/agent-native.eject.json +41 -0
  3. package/agent-native.package.json +119 -0
  4. package/dist/actions/confirm-canonical-logo.d.ts +6 -0
  5. package/dist/actions/confirm-canonical-logo.d.ts.map +1 -0
  6. package/dist/actions/confirm-canonical-logo.js +19 -0
  7. package/dist/actions/confirm-canonical-logo.js.map +1 -0
  8. package/dist/actions/continue-context-import.d.ts +22 -0
  9. package/dist/actions/continue-context-import.d.ts.map +1 -0
  10. package/dist/actions/continue-context-import.js +13 -0
  11. package/dist/actions/continue-context-import.js.map +1 -0
  12. package/dist/actions/creative-context-a2a.d.ts +10 -0
  13. package/dist/actions/creative-context-a2a.d.ts.map +1 -0
  14. package/dist/actions/creative-context-a2a.js +66 -0
  15. package/dist/actions/creative-context-a2a.js.map +1 -0
  16. package/dist/actions/get-brand-profile.d.ts +33 -0
  17. package/dist/actions/get-brand-profile.d.ts.map +1 -0
  18. package/dist/actions/get-brand-profile.js +13 -0
  19. package/dist/actions/get-brand-profile.js.map +1 -0
  20. package/dist/actions/get-context-import-status.d.ts +22 -0
  21. package/dist/actions/get-context-import-status.d.ts.map +1 -0
  22. package/dist/actions/get-context-import-status.js +12 -0
  23. package/dist/actions/get-context-import-status.js.map +1 -0
  24. package/dist/actions/get-context-item.d.ts +2 -0
  25. package/dist/actions/get-context-item.d.ts.map +1 -0
  26. package/dist/actions/get-context-item.js +2 -0
  27. package/dist/actions/get-context-item.js.map +1 -0
  28. package/dist/actions/get-context-pack.d.ts +7 -0
  29. package/dist/actions/get-context-pack.d.ts.map +1 -0
  30. package/dist/actions/get-context-pack.js +12 -0
  31. package/dist/actions/get-context-pack.js.map +1 -0
  32. package/dist/actions/get-creative-context.d.ts +157 -0
  33. package/dist/actions/get-creative-context.d.ts.map +1 -0
  34. package/dist/actions/get-creative-context.js +148 -0
  35. package/dist/actions/get-creative-context.js.map +1 -0
  36. package/dist/actions/get-google-picker-session.d.ts +10 -0
  37. package/dist/actions/get-google-picker-session.d.ts.map +1 -0
  38. package/dist/actions/get-google-picker-session.js +38 -0
  39. package/dist/actions/get-google-picker-session.js.map +1 -0
  40. package/dist/actions/index.d.ts +3 -0
  41. package/dist/actions/index.d.ts.map +1 -0
  42. package/dist/actions/index.js +73 -0
  43. package/dist/actions/index.js.map +1 -0
  44. package/dist/actions/infer-brand-dna.d.ts +52 -0
  45. package/dist/actions/infer-brand-dna.d.ts.map +1 -0
  46. package/dist/actions/infer-brand-dna.js +13 -0
  47. package/dist/actions/infer-brand-dna.js.map +1 -0
  48. package/dist/actions/list-canonical-logo-candidates.d.ts +9 -0
  49. package/dist/actions/list-canonical-logo-candidates.d.ts.map +1 -0
  50. package/dist/actions/list-canonical-logo-candidates.js +82 -0
  51. package/dist/actions/list-canonical-logo-candidates.js.map +1 -0
  52. package/dist/actions/list-context-connections.d.ts +22 -0
  53. package/dist/actions/list-context-connections.d.ts.map +1 -0
  54. package/dist/actions/list-context-connections.js +43 -0
  55. package/dist/actions/list-context-connections.js.map +1 -0
  56. package/dist/actions/list-context-memberships.d.ts +11 -0
  57. package/dist/actions/list-context-memberships.d.ts.map +1 -0
  58. package/dist/actions/list-context-memberships.js +47 -0
  59. package/dist/actions/list-context-memberships.js.map +1 -0
  60. package/dist/actions/list-context-packs.d.ts +10 -0
  61. package/dist/actions/list-context-packs.d.ts.map +1 -0
  62. package/dist/actions/list-context-packs.js +16 -0
  63. package/dist/actions/list-context-packs.js.map +1 -0
  64. package/dist/actions/list-context-sources.d.ts +12 -0
  65. package/dist/actions/list-context-sources.d.ts.map +1 -0
  66. package/dist/actions/list-context-sources.js +20 -0
  67. package/dist/actions/list-context-sources.js.map +1 -0
  68. package/dist/actions/list-context-suggestions.d.ts +13 -0
  69. package/dist/actions/list-context-suggestions.d.ts.map +1 -0
  70. package/dist/actions/list-context-suggestions.js +28 -0
  71. package/dist/actions/list-context-suggestions.js.map +1 -0
  72. package/dist/actions/list-creative-contexts.d.ts +12 -0
  73. package/dist/actions/list-creative-contexts.d.ts.map +1 -0
  74. package/dist/actions/list-creative-contexts.js +28 -0
  75. package/dist/actions/list-creative-contexts.js.map +1 -0
  76. package/dist/actions/manage-brand-profile.d.ts +28 -0
  77. package/dist/actions/manage-brand-profile.d.ts.map +1 -0
  78. package/dist/actions/manage-brand-profile.js +47 -0
  79. package/dist/actions/manage-brand-profile.js.map +1 -0
  80. package/dist/actions/manage-context-membership.d.ts +64 -0
  81. package/dist/actions/manage-context-membership.d.ts.map +1 -0
  82. package/dist/actions/manage-context-membership.js +70 -0
  83. package/dist/actions/manage-context-membership.js.map +1 -0
  84. package/dist/actions/manage-context-pack.d.ts +36 -0
  85. package/dist/actions/manage-context-pack.d.ts.map +1 -0
  86. package/dist/actions/manage-context-pack.js +71 -0
  87. package/dist/actions/manage-context-pack.js.map +1 -0
  88. package/dist/actions/manage-context-source.d.ts +51 -0
  89. package/dist/actions/manage-context-source.d.ts.map +1 -0
  90. package/dist/actions/manage-context-source.js +130 -0
  91. package/dist/actions/manage-context-source.js.map +1 -0
  92. package/dist/actions/manage-creative-context.d.ts +28 -0
  93. package/dist/actions/manage-creative-context.d.ts.map +1 -0
  94. package/dist/actions/manage-creative-context.js +65 -0
  95. package/dist/actions/manage-creative-context.js.map +1 -0
  96. package/dist/actions/manage-layout-template.d.ts +13 -0
  97. package/dist/actions/manage-layout-template.d.ts.map +1 -0
  98. package/dist/actions/manage-layout-template.js +43 -0
  99. package/dist/actions/manage-layout-template.js.map +1 -0
  100. package/dist/actions/preview-context-import.d.ts +23 -0
  101. package/dist/actions/preview-context-import.d.ts.map +1 -0
  102. package/dist/actions/preview-context-import.js +57 -0
  103. package/dist/actions/preview-context-import.js.map +1 -0
  104. package/dist/actions/process-context-purge.d.ts +37 -0
  105. package/dist/actions/process-context-purge.d.ts.map +1 -0
  106. package/dist/actions/process-context-purge.js +20 -0
  107. package/dist/actions/process-context-purge.js.map +1 -0
  108. package/dist/actions/propose-brand-dna.d.ts +15 -0
  109. package/dist/actions/propose-brand-dna.d.ts.map +1 -0
  110. package/dist/actions/propose-brand-dna.js +19 -0
  111. package/dist/actions/propose-brand-dna.js.map +1 -0
  112. package/dist/actions/propose-canonical-logo.d.ts +9 -0
  113. package/dist/actions/propose-canonical-logo.d.ts.map +1 -0
  114. package/dist/actions/propose-canonical-logo.js +16 -0
  115. package/dist/actions/propose-canonical-logo.js.map +1 -0
  116. package/dist/actions/publish-brand-dna.d.ts +13 -0
  117. package/dist/actions/publish-brand-dna.d.ts.map +1 -0
  118. package/dist/actions/publish-brand-dna.js +23 -0
  119. package/dist/actions/publish-brand-dna.js.map +1 -0
  120. package/dist/actions/recommend-context-roots.d.ts +14 -0
  121. package/dist/actions/recommend-context-roots.d.ts.map +1 -0
  122. package/dist/actions/recommend-context-roots.js +22 -0
  123. package/dist/actions/recommend-context-roots.js.map +1 -0
  124. package/dist/actions/record-context-feedback.d.ts +10 -0
  125. package/dist/actions/record-context-feedback.d.ts.map +1 -0
  126. package/dist/actions/record-context-feedback.js +14 -0
  127. package/dist/actions/record-context-feedback.js.map +1 -0
  128. package/dist/actions/review-context-items.d.ts +35 -0
  129. package/dist/actions/review-context-items.d.ts.map +1 -0
  130. package/dist/actions/review-context-items.js +65 -0
  131. package/dist/actions/review-context-items.js.map +1 -0
  132. package/dist/actions/run-embedding-bakeoff.d.ts +15 -0
  133. package/dist/actions/run-embedding-bakeoff.d.ts.map +1 -0
  134. package/dist/actions/run-embedding-bakeoff.js +72 -0
  135. package/dist/actions/run-embedding-bakeoff.js.map +1 -0
  136. package/dist/actions/search-creative-context.d.ts +80 -0
  137. package/dist/actions/search-creative-context.d.ts.map +1 -0
  138. package/dist/actions/search-creative-context.js +42 -0
  139. package/dist/actions/search-creative-context.js.map +1 -0
  140. package/dist/actions/set-brand-dna.d.ts +15 -0
  141. package/dist/actions/set-brand-dna.d.ts.map +1 -0
  142. package/dist/actions/set-brand-dna.js +19 -0
  143. package/dist/actions/set-brand-dna.js.map +1 -0
  144. package/dist/actions/start-context-enrichment.d.ts +25 -0
  145. package/dist/actions/start-context-enrichment.d.ts.map +1 -0
  146. package/dist/actions/start-context-enrichment.js +48 -0
  147. package/dist/actions/start-context-enrichment.js.map +1 -0
  148. package/dist/actions/start-context-import.d.ts +24 -0
  149. package/dist/actions/start-context-import.d.ts.map +1 -0
  150. package/dist/actions/start-context-import.js +37 -0
  151. package/dist/actions/start-context-import.js.map +1 -0
  152. package/dist/client/CreativeContextChip.d.ts +18 -0
  153. package/dist/client/CreativeContextChip.d.ts.map +1 -0
  154. package/dist/client/CreativeContextChip.js +80 -0
  155. package/dist/client/CreativeContextChip.js.map +1 -0
  156. package/dist/client/CreativeContextPanel.d.ts +73 -0
  157. package/dist/client/CreativeContextPanel.d.ts.map +1 -0
  158. package/dist/client/CreativeContextPanel.js +1171 -0
  159. package/dist/client/CreativeContextPanel.js.map +1 -0
  160. package/dist/client/CreativeContextSettingsLink.d.ts +4 -0
  161. package/dist/client/CreativeContextSettingsLink.d.ts.map +1 -0
  162. package/dist/client/CreativeContextSettingsLink.js +8 -0
  163. package/dist/client/CreativeContextSettingsLink.js.map +1 -0
  164. package/dist/client/CreativeContextShareTab.d.ts +56 -0
  165. package/dist/client/CreativeContextShareTab.d.ts.map +1 -0
  166. package/dist/client/CreativeContextShareTab.js +213 -0
  167. package/dist/client/CreativeContextShareTab.js.map +1 -0
  168. package/dist/client/actions.d.ts +474 -0
  169. package/dist/client/actions.d.ts.map +1 -0
  170. package/dist/client/actions.js +411 -0
  171. package/dist/client/actions.js.map +1 -0
  172. package/dist/client/agent-tab.d.ts +10 -0
  173. package/dist/client/agent-tab.d.ts.map +1 -0
  174. package/dist/client/agent-tab.js +32 -0
  175. package/dist/client/agent-tab.js.map +1 -0
  176. package/dist/client/application-state.d.ts +24 -0
  177. package/dist/client/application-state.d.ts.map +1 -0
  178. package/dist/client/application-state.js +111 -0
  179. package/dist/client/application-state.js.map +1 -0
  180. package/dist/client/google-slides-picker.d.ts +19 -0
  181. package/dist/client/google-slides-picker.d.ts.map +1 -0
  182. package/dist/client/google-slides-picker.js +106 -0
  183. package/dist/client/google-slides-picker.js.map +1 -0
  184. package/dist/client/index.d.ts +10 -0
  185. package/dist/client/index.d.ts.map +1 -0
  186. package/dist/client/index.js +10 -0
  187. package/dist/client/index.js.map +1 -0
  188. package/dist/client/messages.d.ts +5 -0
  189. package/dist/client/messages.d.ts.map +1 -0
  190. package/dist/client/messages.js +1158 -0
  191. package/dist/client/messages.js.map +1 -0
  192. package/dist/connectors/context.d.ts +9 -0
  193. package/dist/connectors/context.d.ts.map +1 -0
  194. package/dist/connectors/context.js +59 -0
  195. package/dist/connectors/context.js.map +1 -0
  196. package/dist/connectors/document-parser.d.ts +2 -0
  197. package/dist/connectors/document-parser.d.ts.map +1 -0
  198. package/dist/connectors/document-parser.js +2 -0
  199. package/dist/connectors/document-parser.js.map +1 -0
  200. package/dist/connectors/figma-native.d.ts +18 -0
  201. package/dist/connectors/figma-native.d.ts.map +1 -0
  202. package/dist/connectors/figma-native.js +883 -0
  203. package/dist/connectors/figma-native.js.map +1 -0
  204. package/dist/connectors/figma.d.ts +10 -0
  205. package/dist/connectors/figma.d.ts.map +1 -0
  206. package/dist/connectors/figma.js +166 -0
  207. package/dist/connectors/figma.js.map +1 -0
  208. package/dist/connectors/google-slides-native.d.ts +58 -0
  209. package/dist/connectors/google-slides-native.d.ts.map +1 -0
  210. package/dist/connectors/google-slides-native.js +1118 -0
  211. package/dist/connectors/google-slides-native.js.map +1 -0
  212. package/dist/connectors/google-slides.d.ts +15 -0
  213. package/dist/connectors/google-slides.d.ts.map +1 -0
  214. package/dist/connectors/google-slides.js +895 -0
  215. package/dist/connectors/google-slides.js.map +1 -0
  216. package/dist/connectors/index.d.ts +21 -0
  217. package/dist/connectors/index.d.ts.map +1 -0
  218. package/dist/connectors/index.js +45 -0
  219. package/dist/connectors/index.js.map +1 -0
  220. package/dist/connectors/manual.d.ts +9 -0
  221. package/dist/connectors/manual.d.ts.map +1 -0
  222. package/dist/connectors/manual.js +91 -0
  223. package/dist/connectors/manual.js.map +1 -0
  224. package/dist/connectors/normalize.d.ts +42 -0
  225. package/dist/connectors/normalize.d.ts.map +1 -0
  226. package/dist/connectors/normalize.js +335 -0
  227. package/dist/connectors/normalize.js.map +1 -0
  228. package/dist/connectors/notion.d.ts +10 -0
  229. package/dist/connectors/notion.d.ts.map +1 -0
  230. package/dist/connectors/notion.js +393 -0
  231. package/dist/connectors/notion.js.map +1 -0
  232. package/dist/connectors/private-artifacts.d.ts +39 -0
  233. package/dist/connectors/private-artifacts.d.ts.map +1 -0
  234. package/dist/connectors/private-artifacts.js +175 -0
  235. package/dist/connectors/private-artifacts.js.map +1 -0
  236. package/dist/connectors/provider-response.d.ts +20 -0
  237. package/dist/connectors/provider-response.d.ts.map +1 -0
  238. package/dist/connectors/provider-response.js +98 -0
  239. package/dist/connectors/provider-response.js.map +1 -0
  240. package/dist/connectors/recommendations.d.ts +25 -0
  241. package/dist/connectors/recommendations.d.ts.map +1 -0
  242. package/dist/connectors/recommendations.js +172 -0
  243. package/dist/connectors/recommendations.js.map +1 -0
  244. package/dist/connectors/registry.d.ts +10 -0
  245. package/dist/connectors/registry.d.ts.map +1 -0
  246. package/dist/connectors/registry.js +34 -0
  247. package/dist/connectors/registry.js.map +1 -0
  248. package/dist/connectors/rendered-design.d.ts +68 -0
  249. package/dist/connectors/rendered-design.d.ts.map +1 -0
  250. package/dist/connectors/rendered-design.js +362 -0
  251. package/dist/connectors/rendered-design.js.map +1 -0
  252. package/dist/connectors/rendered-page.d.ts +115 -0
  253. package/dist/connectors/rendered-page.d.ts.map +1 -0
  254. package/dist/connectors/rendered-page.js +1036 -0
  255. package/dist/connectors/rendered-page.js.map +1 -0
  256. package/dist/connectors/smart-defaults.d.ts +9 -0
  257. package/dist/connectors/smart-defaults.d.ts.map +1 -0
  258. package/dist/connectors/smart-defaults.js +72 -0
  259. package/dist/connectors/smart-defaults.js.map +1 -0
  260. package/dist/connectors/types.d.ts +79 -0
  261. package/dist/connectors/types.d.ts.map +1 -0
  262. package/dist/connectors/types.js +2 -0
  263. package/dist/connectors/types.js.map +1 -0
  264. package/dist/connectors/upload.d.ts +9 -0
  265. package/dist/connectors/upload.d.ts.map +1 -0
  266. package/dist/connectors/upload.js +440 -0
  267. package/dist/connectors/upload.js.map +1 -0
  268. package/dist/connectors/website.d.ts +20 -0
  269. package/dist/connectors/website.d.ts.map +1 -0
  270. package/dist/connectors/website.js +616 -0
  271. package/dist/connectors/website.js.map +1 -0
  272. package/dist/embeddings/index.d.ts +3 -0
  273. package/dist/embeddings/index.d.ts.map +1 -0
  274. package/dist/embeddings/index.js +3 -0
  275. package/dist/embeddings/index.js.map +1 -0
  276. package/dist/embeddings/providers.d.ts +2 -0
  277. package/dist/embeddings/providers.d.ts.map +1 -0
  278. package/dist/embeddings/providers.js +2 -0
  279. package/dist/embeddings/providers.js.map +1 -0
  280. package/dist/embeddings/types.d.ts +2 -0
  281. package/dist/embeddings/types.d.ts.map +1 -0
  282. package/dist/embeddings/types.js +2 -0
  283. package/dist/embeddings/types.js.map +1 -0
  284. package/dist/eval/acceptance-fixtures.d.ts +3 -0
  285. package/dist/eval/acceptance-fixtures.d.ts.map +1 -0
  286. package/dist/eval/acceptance-fixtures.js +84 -0
  287. package/dist/eval/acceptance-fixtures.js.map +1 -0
  288. package/dist/eval/acceptance.d.ts +114 -0
  289. package/dist/eval/acceptance.d.ts.map +1 -0
  290. package/dist/eval/acceptance.js +260 -0
  291. package/dist/eval/acceptance.js.map +1 -0
  292. package/dist/eval/fixtures.d.ts +16 -0
  293. package/dist/eval/fixtures.d.ts.map +1 -0
  294. package/dist/eval/fixtures.js +119 -0
  295. package/dist/eval/fixtures.js.map +1 -0
  296. package/dist/eval/index.d.ts +63 -0
  297. package/dist/eval/index.d.ts.map +1 -0
  298. package/dist/eval/index.js +174 -0
  299. package/dist/eval/index.js.map +1 -0
  300. package/dist/index.d.ts +5 -0
  301. package/dist/index.d.ts.map +1 -0
  302. package/dist/index.js +5 -0
  303. package/dist/index.js.map +1 -0
  304. package/dist/jobs/background-worker.d.ts +31 -0
  305. package/dist/jobs/background-worker.d.ts.map +1 -0
  306. package/dist/jobs/background-worker.js +315 -0
  307. package/dist/jobs/background-worker.js.map +1 -0
  308. package/dist/jobs/import-runner.d.ts +3 -0
  309. package/dist/jobs/import-runner.d.ts.map +1 -0
  310. package/dist/jobs/import-runner.js +613 -0
  311. package/dist/jobs/import-runner.js.map +1 -0
  312. package/dist/jobs/index.d.ts +9 -0
  313. package/dist/jobs/index.d.ts.map +1 -0
  314. package/dist/jobs/index.js +8 -0
  315. package/dist/jobs/index.js.map +1 -0
  316. package/dist/jobs/progress.d.ts +3 -0
  317. package/dist/jobs/progress.d.ts.map +1 -0
  318. package/dist/jobs/progress.js +30 -0
  319. package/dist/jobs/progress.js.map +1 -0
  320. package/dist/jobs/rebuild.d.ts +13 -0
  321. package/dist/jobs/rebuild.d.ts.map +1 -0
  322. package/dist/jobs/rebuild.js +178 -0
  323. package/dist/jobs/rebuild.js.map +1 -0
  324. package/dist/jobs/server-worker.d.ts +16 -0
  325. package/dist/jobs/server-worker.d.ts.map +1 -0
  326. package/dist/jobs/server-worker.js +262 -0
  327. package/dist/jobs/server-worker.js.map +1 -0
  328. package/dist/jobs/store-adapter.d.ts +3 -0
  329. package/dist/jobs/store-adapter.d.ts.map +1 -0
  330. package/dist/jobs/store-adapter.js +37 -0
  331. package/dist/jobs/store-adapter.js.map +1 -0
  332. package/dist/jobs/types.d.ts +138 -0
  333. package/dist/jobs/types.d.ts.map +1 -0
  334. package/dist/jobs/types.js +2 -0
  335. package/dist/jobs/types.js.map +1 -0
  336. package/dist/jobs/worker.d.ts +27 -0
  337. package/dist/jobs/worker.d.ts.map +1 -0
  338. package/dist/jobs/worker.js +90 -0
  339. package/dist/jobs/worker.js.map +1 -0
  340. package/dist/media-url.d.ts +7 -0
  341. package/dist/media-url.d.ts.map +1 -0
  342. package/dist/media-url.js +15 -0
  343. package/dist/media-url.js.map +1 -0
  344. package/dist/native-artifact-reassembly.d.ts +25 -0
  345. package/dist/native-artifact-reassembly.d.ts.map +1 -0
  346. package/dist/native-artifact-reassembly.js +262 -0
  347. package/dist/native-artifact-reassembly.js.map +1 -0
  348. package/dist/native-artifact.d.ts +88 -0
  349. package/dist/native-artifact.d.ts.map +1 -0
  350. package/dist/native-artifact.js +86 -0
  351. package/dist/native-artifact.js.map +1 -0
  352. package/dist/schema/index.d.ts +7111 -0
  353. package/dist/schema/index.d.ts.map +1 -0
  354. package/dist/schema/index.js +469 -0
  355. package/dist/schema/index.js.map +1 -0
  356. package/dist/schema/migrations.d.ts +10 -0
  357. package/dist/schema/migrations.d.ts.map +1 -0
  358. package/dist/schema/migrations.js +333 -0
  359. package/dist/schema/migrations.js.map +1 -0
  360. package/dist/search/fusion.d.ts +2 -0
  361. package/dist/search/fusion.d.ts.map +1 -0
  362. package/dist/search/fusion.js +2 -0
  363. package/dist/search/fusion.js.map +1 -0
  364. package/dist/search/mode.d.ts +4 -0
  365. package/dist/search/mode.d.ts.map +1 -0
  366. package/dist/search/mode.js +13 -0
  367. package/dist/search/mode.js.map +1 -0
  368. package/dist/search/postgres-fts.d.ts +2 -0
  369. package/dist/search/postgres-fts.d.ts.map +1 -0
  370. package/dist/search/postgres-fts.js +2 -0
  371. package/dist/search/postgres-fts.js.map +1 -0
  372. package/dist/server/brand-context.d.ts +15 -0
  373. package/dist/server/brand-context.d.ts.map +1 -0
  374. package/dist/server/brand-context.js +149 -0
  375. package/dist/server/brand-context.js.map +1 -0
  376. package/dist/server/context.d.ts +77 -0
  377. package/dist/server/context.d.ts.map +1 -0
  378. package/dist/server/context.js +63 -0
  379. package/dist/server/context.js.map +1 -0
  380. package/dist/server/enrichment.d.ts +24 -0
  381. package/dist/server/enrichment.d.ts.map +1 -0
  382. package/dist/server/enrichment.js +138 -0
  383. package/dist/server/enrichment.js.map +1 -0
  384. package/dist/server/generation-artifact-access.d.ts +21 -0
  385. package/dist/server/generation-artifact-access.d.ts.map +1 -0
  386. package/dist/server/generation-artifact-access.js +113 -0
  387. package/dist/server/generation-artifact-access.js.map +1 -0
  388. package/dist/server/generation-context.d.ts +220 -0
  389. package/dist/server/generation-context.d.ts.map +1 -0
  390. package/dist/server/generation-context.js +432 -0
  391. package/dist/server/generation-context.js.map +1 -0
  392. package/dist/server/index.d.ts +27 -0
  393. package/dist/server/index.d.ts.map +1 -0
  394. package/dist/server/index.js +115 -0
  395. package/dist/server/index.js.map +1 -0
  396. package/dist/server/isolated-a2a.d.ts +175 -0
  397. package/dist/server/isolated-a2a.d.ts.map +1 -0
  398. package/dist/server/isolated-a2a.js +312 -0
  399. package/dist/server/isolated-a2a.js.map +1 -0
  400. package/dist/server/media.d.ts +15 -0
  401. package/dist/server/media.d.ts.map +1 -0
  402. package/dist/server/media.js +108 -0
  403. package/dist/server/media.js.map +1 -0
  404. package/dist/server/native-resource-capture.d.ts +78 -0
  405. package/dist/server/native-resource-capture.d.ts.map +1 -0
  406. package/dist/server/native-resource-capture.js +94 -0
  407. package/dist/server/native-resource-capture.js.map +1 -0
  408. package/dist/server/prompt-provider.d.ts +5 -0
  409. package/dist/server/prompt-provider.d.ts.map +1 -0
  410. package/dist/server/prompt-provider.js +59 -0
  411. package/dist/server/prompt-provider.js.map +1 -0
  412. package/dist/server/public-serialization.d.ts +160 -0
  413. package/dist/server/public-serialization.d.ts.map +1 -0
  414. package/dist/server/public-serialization.js +108 -0
  415. package/dist/server/public-serialization.js.map +1 -0
  416. package/dist/server/resource-paths.d.ts +3 -0
  417. package/dist/server/resource-paths.d.ts.map +1 -0
  418. package/dist/server/resource-paths.js +5 -0
  419. package/dist/server/resource-paths.js.map +1 -0
  420. package/dist/server/retrieval.d.ts +83 -0
  421. package/dist/server/retrieval.d.ts.map +1 -0
  422. package/dist/server/retrieval.js +445 -0
  423. package/dist/server/retrieval.js.map +1 -0
  424. package/dist/server/safe-native-preview.d.ts +15 -0
  425. package/dist/server/safe-native-preview.d.ts.map +1 -0
  426. package/dist/server/safe-native-preview.js +148 -0
  427. package/dist/server/safe-native-preview.js.map +1 -0
  428. package/dist/store/brand.d.ts +47 -0
  429. package/dist/store/brand.d.ts.map +1 -0
  430. package/dist/store/brand.js +346 -0
  431. package/dist/store/brand.js.map +1 -0
  432. package/dist/store/content.d.ts +134 -0
  433. package/dist/store/content.d.ts.map +1 -0
  434. package/dist/store/content.js +1677 -0
  435. package/dist/store/content.js.map +1 -0
  436. package/dist/store/contexts.d.ts +216 -0
  437. package/dist/store/contexts.d.ts.map +1 -0
  438. package/dist/store/contexts.js +1346 -0
  439. package/dist/store/contexts.js.map +1 -0
  440. package/dist/store/generation.d.ts +28 -0
  441. package/dist/store/generation.d.ts.map +1 -0
  442. package/dist/store/generation.js +140 -0
  443. package/dist/store/generation.js.map +1 -0
  444. package/dist/store/helpers.d.ts +12 -0
  445. package/dist/store/helpers.d.ts.map +1 -0
  446. package/dist/store/helpers.js +41 -0
  447. package/dist/store/helpers.js.map +1 -0
  448. package/dist/store/index.d.ts +11 -0
  449. package/dist/store/index.d.ts.map +1 -0
  450. package/dist/store/index.js +11 -0
  451. package/dist/store/index.js.map +1 -0
  452. package/dist/store/inference.d.ts +57 -0
  453. package/dist/store/inference.d.ts.map +1 -0
  454. package/dist/store/inference.js +279 -0
  455. package/dist/store/inference.js.map +1 -0
  456. package/dist/store/jobs.d.ts +80 -0
  457. package/dist/store/jobs.d.ts.map +1 -0
  458. package/dist/store/jobs.js +428 -0
  459. package/dist/store/jobs.js.map +1 -0
  460. package/dist/store/native-artifact-summary.d.ts +7 -0
  461. package/dist/store/native-artifact-summary.d.ts.map +1 -0
  462. package/dist/store/native-artifact-summary.js +10 -0
  463. package/dist/store/native-artifact-summary.js.map +1 -0
  464. package/dist/store/packs.d.ts +36 -0
  465. package/dist/store/packs.d.ts.map +1 -0
  466. package/dist/store/packs.js +438 -0
  467. package/dist/store/packs.js.map +1 -0
  468. package/dist/store/purge.d.ts +16 -0
  469. package/dist/store/purge.d.ts.map +1 -0
  470. package/dist/store/purge.js +354 -0
  471. package/dist/store/purge.js.map +1 -0
  472. package/dist/store/sources.d.ts +76 -0
  473. package/dist/store/sources.d.ts.map +1 -0
  474. package/dist/store/sources.js +525 -0
  475. package/dist/store/sources.js.map +1 -0
  476. package/dist/store/suggestions.d.ts +29 -0
  477. package/dist/store/suggestions.d.ts.map +1 -0
  478. package/dist/store/suggestions.js +270 -0
  479. package/dist/store/suggestions.js.map +1 -0
  480. package/dist/types.d.ts +578 -0
  481. package/dist/types.d.ts.map +1 -0
  482. package/dist/types.js +10 -0
  483. package/dist/types.js.map +1 -0
  484. package/dist/untrusted-reference.d.ts +8 -0
  485. package/dist/untrusted-reference.d.ts.map +1 -0
  486. package/dist/untrusted-reference.js +19 -0
  487. package/dist/untrusted-reference.js.map +1 -0
  488. package/dist/vector/index.d.ts +2 -0
  489. package/dist/vector/index.d.ts.map +1 -0
  490. package/dist/vector/index.js +2 -0
  491. package/dist/vector/index.js.map +1 -0
  492. package/dist/vector/pgvector.d.ts +2 -0
  493. package/dist/vector/pgvector.d.ts.map +1 -0
  494. package/dist/vector/pgvector.js +2 -0
  495. package/dist/vector/pgvector.js.map +1 -0
  496. package/docs/evaluation.md +92 -0
  497. package/docs/llms-full.txt +168 -0
  498. package/docs/llms.txt +76 -0
  499. package/docs/skills/creative-context/SKILL.md +90 -0
  500. package/package.json +100 -0
  501. package/src/actions/agent-input-schemas.spec.ts +46 -0
  502. package/src/actions/confirm-canonical-logo.ts +20 -0
  503. package/src/actions/continue-context-import.ts +15 -0
  504. package/src/actions/creative-context-a2a.spec.ts +162 -0
  505. package/src/actions/creative-context-a2a.ts +88 -0
  506. package/src/actions/get-brand-profile.ts +16 -0
  507. package/src/actions/get-context-import-status.ts +13 -0
  508. package/src/actions/get-context-item.ts +1 -0
  509. package/src/actions/get-context-pack.ts +14 -0
  510. package/src/actions/get-creative-context.spec.ts +435 -0
  511. package/src/actions/get-creative-context.ts +197 -0
  512. package/src/actions/get-google-picker-session.spec.ts +62 -0
  513. package/src/actions/get-google-picker-session.ts +45 -0
  514. package/src/actions/index.ts +75 -0
  515. package/src/actions/infer-brand-dna.ts +15 -0
  516. package/src/actions/job-public-serialization.spec.ts +143 -0
  517. package/src/actions/list-canonical-logo-candidates.ts +121 -0
  518. package/src/actions/list-context-connections.spec.ts +23 -0
  519. package/src/actions/list-context-connections.ts +51 -0
  520. package/src/actions/list-context-memberships.ts +56 -0
  521. package/src/actions/list-context-packs.ts +17 -0
  522. package/src/actions/list-context-sources.ts +21 -0
  523. package/src/actions/list-context-suggestions.ts +30 -0
  524. package/src/actions/list-creative-contexts.spec.ts +38 -0
  525. package/src/actions/list-creative-contexts.ts +32 -0
  526. package/src/actions/manage-brand-profile.ts +58 -0
  527. package/src/actions/manage-context-membership.ts +80 -0
  528. package/src/actions/manage-context-pack.ts +81 -0
  529. package/src/actions/manage-context-source.ts +151 -0
  530. package/src/actions/manage-creative-context.ts +73 -0
  531. package/src/actions/manage-layout-template.ts +51 -0
  532. package/src/actions/preview-context-import.spec.ts +64 -0
  533. package/src/actions/preview-context-import.ts +61 -0
  534. package/src/actions/process-context-purge.ts +28 -0
  535. package/src/actions/propose-brand-dna.ts +22 -0
  536. package/src/actions/propose-canonical-logo.ts +19 -0
  537. package/src/actions/publish-brand-dna.ts +25 -0
  538. package/src/actions/recommend-context-roots.ts +24 -0
  539. package/src/actions/record-context-feedback.ts +16 -0
  540. package/src/actions/review-context-items.ts +69 -0
  541. package/src/actions/run-embedding-bakeoff.ts +90 -0
  542. package/src/actions/search-creative-context.ts +48 -0
  543. package/src/actions/set-brand-dna.ts +22 -0
  544. package/src/actions/start-context-enrichment.ts +52 -0
  545. package/src/actions/start-context-import.ts +38 -0
  546. package/src/client/CreativeContextChip.spec.ts +59 -0
  547. package/src/client/CreativeContextChip.tsx +243 -0
  548. package/src/client/CreativeContextPanel.tsx +3600 -0
  549. package/src/client/CreativeContextSettingsLink.tsx +30 -0
  550. package/src/client/CreativeContextShareTab.spec.ts +70 -0
  551. package/src/client/CreativeContextShareTab.tsx +666 -0
  552. package/src/client/actions.spec.ts +177 -0
  553. package/src/client/actions.ts +1024 -0
  554. package/src/client/agent-tab.tsx +56 -0
  555. package/src/client/application-state.spec.ts +46 -0
  556. package/src/client/application-state.ts +158 -0
  557. package/src/client/google-slides-picker.spec.ts +36 -0
  558. package/src/client/google-slides-picker.ts +146 -0
  559. package/src/client/index.ts +102 -0
  560. package/src/client/messages.ts +1370 -0
  561. package/src/client/source-config.spec.ts +166 -0
  562. package/src/connectors/connectors.spec.ts +778 -0
  563. package/src/connectors/context.ts +83 -0
  564. package/src/connectors/document-parser.ts +5 -0
  565. package/src/connectors/figma-native.ts +1192 -0
  566. package/src/connectors/figma-native.visual.spec.ts +221 -0
  567. package/src/connectors/figma.spec.ts +812 -0
  568. package/src/connectors/figma.ts +257 -0
  569. package/src/connectors/fixtures/google-slides-native.json +353 -0
  570. package/src/connectors/google-slides-native.spec.ts +804 -0
  571. package/src/connectors/google-slides-native.ts +1656 -0
  572. package/src/connectors/google-slides-native.visual.spec.ts +119 -0
  573. package/src/connectors/google-slides.ts +1171 -0
  574. package/src/connectors/index.ts +112 -0
  575. package/src/connectors/manual.ts +132 -0
  576. package/src/connectors/normalize.spec.ts +144 -0
  577. package/src/connectors/normalize.ts +475 -0
  578. package/src/connectors/notion.ts +529 -0
  579. package/src/connectors/private-artifacts.spec.ts +95 -0
  580. package/src/connectors/private-artifacts.ts +248 -0
  581. package/src/connectors/provider-response.ts +144 -0
  582. package/src/connectors/recommendations.ts +237 -0
  583. package/src/connectors/registry.ts +48 -0
  584. package/src/connectors/rendered-design.spec.ts +171 -0
  585. package/src/connectors/rendered-design.ts +495 -0
  586. package/src/connectors/rendered-page.spec.ts +388 -0
  587. package/src/connectors/rendered-page.ts +1387 -0
  588. package/src/connectors/smart-defaults.spec.ts +135 -0
  589. package/src/connectors/smart-defaults.ts +87 -0
  590. package/src/connectors/types.ts +113 -0
  591. package/src/connectors/upload.spec.ts +175 -0
  592. package/src/connectors/upload.ts +548 -0
  593. package/src/connectors/website.spec.ts +465 -0
  594. package/src/connectors/website.ts +777 -0
  595. package/src/embeddings/index.ts +2 -0
  596. package/src/embeddings/providers.spec.ts +73 -0
  597. package/src/embeddings/providers.ts +1 -0
  598. package/src/embeddings/types.ts +6 -0
  599. package/src/eval/acceptance-fixtures.ts +97 -0
  600. package/src/eval/acceptance.spec.ts +92 -0
  601. package/src/eval/acceptance.ts +371 -0
  602. package/src/eval/connectors-golden.spec.ts +421 -0
  603. package/src/eval/eval.spec.ts +166 -0
  604. package/src/eval/fixtures/README.md +17 -0
  605. package/src/eval/fixtures/figma-brand-system-v1.json +140 -0
  606. package/src/eval/fixtures/figma-brand-system-v2.json +127 -0
  607. package/src/eval/fixtures/launch-hero.png +0 -0
  608. package/src/eval/fixtures/launch-system-v1.pptx +0 -0
  609. package/src/eval/fixtures/launch-system-v2.pptx +0 -0
  610. package/src/eval/fixtures/notion-brand-root.json +218 -0
  611. package/src/eval/fixtures/pricing-hero.png +0 -0
  612. package/src/eval/fixtures/rendered-brand-page.html +76 -0
  613. package/src/eval/fixtures.spec.ts +43 -0
  614. package/src/eval/fixtures.ts +136 -0
  615. package/src/eval/index.ts +291 -0
  616. package/src/eval/library-flow.acceptance.spec.tsx +145 -0
  617. package/src/index.ts +4 -0
  618. package/src/jobs/background-worker.ts +423 -0
  619. package/src/jobs/import-runner.spec.ts +582 -0
  620. package/src/jobs/import-runner.ts +791 -0
  621. package/src/jobs/index.ts +40 -0
  622. package/src/jobs/progress.ts +37 -0
  623. package/src/jobs/rebuild.ts +245 -0
  624. package/src/jobs/server-worker.spec.ts +181 -0
  625. package/src/jobs/server-worker.ts +354 -0
  626. package/src/jobs/store-adapter.ts +52 -0
  627. package/src/jobs/types.ts +147 -0
  628. package/src/jobs/worker.ts +152 -0
  629. package/src/manifest.spec.ts +26 -0
  630. package/src/media-url.ts +17 -0
  631. package/src/native-artifact-reassembly.spec.ts +386 -0
  632. package/src/native-artifact-reassembly.ts +406 -0
  633. package/src/native-artifact.ts +102 -0
  634. package/src/schema/index.ts +527 -0
  635. package/src/schema/migrations.spec.ts +41 -0
  636. package/src/schema/migrations.ts +338 -0
  637. package/src/search/fusion.spec.ts +21 -0
  638. package/src/search/fusion.ts +1 -0
  639. package/src/search/mode.spec.ts +28 -0
  640. package/src/search/mode.ts +21 -0
  641. package/src/search/postgres-fts.spec.ts +63 -0
  642. package/src/search/postgres-fts.ts +1 -0
  643. package/src/server/brand-context.spec.ts +43 -0
  644. package/src/server/brand-context.ts +160 -0
  645. package/src/server/context.ts +162 -0
  646. package/src/server/enrichment.spec.ts +94 -0
  647. package/src/server/enrichment.ts +166 -0
  648. package/src/server/generation-artifact-access.spec.ts +104 -0
  649. package/src/server/generation-artifact-access.ts +217 -0
  650. package/src/server/generation-context-a2a.spec.ts +282 -0
  651. package/src/server/generation-context-off.spec.ts +213 -0
  652. package/src/server/generation-context.spec.ts +124 -0
  653. package/src/server/generation-context.ts +639 -0
  654. package/src/server/index.spec.ts +30 -0
  655. package/src/server/index.ts +163 -0
  656. package/src/server/isolated-a2a.spec.ts +184 -0
  657. package/src/server/isolated-a2a.ts +416 -0
  658. package/src/server/media.spec.ts +120 -0
  659. package/src/server/media.ts +136 -0
  660. package/src/server/native-resource-capture.spec.ts +121 -0
  661. package/src/server/native-resource-capture.ts +198 -0
  662. package/src/server/prompt-provider.spec.ts +78 -0
  663. package/src/server/prompt-provider.ts +73 -0
  664. package/src/server/public-serialization.spec.ts +150 -0
  665. package/src/server/public-serialization.ts +130 -0
  666. package/src/server/resource-paths.spec.ts +9 -0
  667. package/src/server/resource-paths.ts +5 -0
  668. package/src/server/retrieval-vector.spec.ts +180 -0
  669. package/src/server/retrieval.ts +590 -0
  670. package/src/server/safe-native-preview.spec.ts +29 -0
  671. package/src/server/safe-native-preview.ts +199 -0
  672. package/src/store/access.integration.spec.ts +3523 -0
  673. package/src/store/brand.ts +458 -0
  674. package/src/store/content.ts +2317 -0
  675. package/src/store/contexts.ts +1840 -0
  676. package/src/store/generation.ts +226 -0
  677. package/src/store/helpers.ts +53 -0
  678. package/src/store/index.ts +10 -0
  679. package/src/store/inference.spec.ts +84 -0
  680. package/src/store/inference.ts +339 -0
  681. package/src/store/invariants.spec.ts +119 -0
  682. package/src/store/jobs.ts +674 -0
  683. package/src/store/native-artifact-summary.ts +13 -0
  684. package/src/store/packs.ts +721 -0
  685. package/src/store/purge.ts +463 -0
  686. package/src/store/sources.spec.ts +102 -0
  687. package/src/store/sources.ts +692 -0
  688. package/src/store/suggestions.ts +361 -0
  689. package/src/types.ts +671 -0
  690. package/src/untrusted-reference.spec.ts +22 -0
  691. package/src/untrusted-reference.ts +24 -0
  692. package/src/vector/index.ts +1 -0
  693. package/src/vector/pgvector.spec.ts +115 -0
  694. package/src/vector/pgvector.ts +1 -0
  695. package/tsconfig.json +31 -0
@@ -0,0 +1,3600 @@
1
+ import { type AgentPageScope } from "@agent-native/core/client/agent-chat";
2
+ import { useFormatters, useT } from "@agent-native/core/client/i18n";
3
+ import { useOrg } from "@agent-native/core/client/org";
4
+ import { useUploadResource } from "@agent-native/core/client/uploads";
5
+ import {
6
+ AlertDialog,
7
+ AlertDialogAction,
8
+ AlertDialogCancel,
9
+ AlertDialogContent,
10
+ AlertDialogDescription,
11
+ AlertDialogFooter,
12
+ AlertDialogHeader,
13
+ AlertDialogTitle,
14
+ Badge,
15
+ Button,
16
+ Checkbox,
17
+ DropdownMenu,
18
+ DropdownMenuContent,
19
+ DropdownMenuItem,
20
+ DropdownMenuSeparator,
21
+ DropdownMenuTrigger,
22
+ Input,
23
+ Select,
24
+ SelectContent,
25
+ SelectItem,
26
+ SelectTrigger,
27
+ SelectValue,
28
+ Sheet,
29
+ SheetContent,
30
+ SheetDescription,
31
+ SheetHeader,
32
+ SheetTitle,
33
+ Skeleton,
34
+ Tabs,
35
+ TabsContent,
36
+ TabsList,
37
+ TabsTrigger,
38
+ Textarea,
39
+ } from "@agent-native/toolkit/ui";
40
+ import {
41
+ IconAlertTriangle,
42
+ IconArrowUpRight,
43
+ IconBooks,
44
+ IconChartBar,
45
+ IconCheck,
46
+ IconDots,
47
+ IconFileImport,
48
+ IconFileText,
49
+ IconLayout,
50
+ IconPalette,
51
+ IconPhoto,
52
+ IconPlayerPlay,
53
+ IconPin,
54
+ IconPlus,
55
+ IconRefresh,
56
+ IconSearch,
57
+ IconSlideshow,
58
+ IconSparkles,
59
+ IconUpload,
60
+ IconWorld,
61
+ } from "@tabler/icons-react";
62
+ import {
63
+ useEffect,
64
+ useMemo,
65
+ useRef,
66
+ useState,
67
+ type ChangeEvent,
68
+ type DragEvent,
69
+ type FormEvent,
70
+ type ReactNode,
71
+ } from "react";
72
+
73
+ import { creativeContextMediaUrl } from "../media-url.js";
74
+ import type {
75
+ ContextImportJobResult,
76
+ ContextPackSummary,
77
+ ContextReviewItem,
78
+ ContextSourceSummary,
79
+ } from "../types.js";
80
+ import {
81
+ useCreativeContextBrandProfile,
82
+ useCreativeContexts,
83
+ useCanonicalLogoCandidates,
84
+ useCreativeContextConnections,
85
+ useCreativeContextGooglePickerSession,
86
+ useCreativeContextRootRecommendations,
87
+ useCreativeContextImportStatus,
88
+ useCreativeContextPack,
89
+ useCreativeContextPacks,
90
+ useContextMemberships,
91
+ useManageCreativeContext,
92
+ useManageContextMembership,
93
+ useCreativeContextSearch,
94
+ useCreativeContextSuggestions,
95
+ useCreativeContextSources,
96
+ useManageCreativeContextSource,
97
+ useManageLayoutTemplate,
98
+ usePreviewCreativeContextImport,
99
+ usePublishCreativeContextBrandDna,
100
+ useConfirmCanonicalLogo,
101
+ useProposeCanonicalLogo,
102
+ useRefreshCreativeContextSource,
103
+ useReviewCreativeContextItems,
104
+ useStartCreativeContextImport,
105
+ parseCreativeContexts,
106
+ parseContextMemberships,
107
+ type CreativeContextSafePreview,
108
+ type CreativeContextConnectionProvider,
109
+ type CreativeContextRootRecommendation,
110
+ type CreativeContextRecommendationProvider,
111
+ } from "./actions.js";
112
+ import {
113
+ useCreativeContextState,
114
+ type CreativeContextMode,
115
+ } from "./application-state.js";
116
+ import { CreativeContextChip } from "./CreativeContextChip.js";
117
+ import { chooseGoogleSlidesPresentations } from "./google-slides-picker.js";
118
+
119
+ type ConnectorKind =
120
+ | "google-slides"
121
+ | "figma"
122
+ | "notion"
123
+ | "website"
124
+ | "upload";
125
+
126
+ interface ConnectorDefinition {
127
+ kind: ConnectorKind;
128
+ label: string;
129
+ referencePlaceholder: string;
130
+ referenceRequired: boolean;
131
+ icon: typeof IconBooks;
132
+ }
133
+
134
+ export interface UploadedContextFile {
135
+ id: string;
136
+ title: string;
137
+ fileName: string;
138
+ mimeType: string;
139
+ url: string;
140
+ }
141
+
142
+ const CONNECTORS: readonly ConnectorDefinition[] = [
143
+ {
144
+ kind: "google-slides",
145
+ label: "Google Slides",
146
+ referencePlaceholder: "Presentation URLs or IDs — one per line",
147
+ referenceRequired: false,
148
+ icon: IconSlideshow,
149
+ },
150
+ {
151
+ kind: "figma",
152
+ label: "Figma",
153
+ referencePlaceholder: "Team, project, or file URLs — one per line",
154
+ referenceRequired: false,
155
+ icon: IconPalette,
156
+ },
157
+ {
158
+ kind: "notion",
159
+ label: "Notion",
160
+ referencePlaceholder: "Page or teamspace root URLs / IDs — one per line",
161
+ referenceRequired: false,
162
+ icon: IconFileText,
163
+ },
164
+ {
165
+ kind: "website",
166
+ label: "Website",
167
+ referencePlaceholder: "https://example.com\nhttps://example.com/about",
168
+ referenceRequired: true,
169
+ icon: IconWorld,
170
+ },
171
+ {
172
+ kind: "upload",
173
+ label: "Uploaded files",
174
+ referencePlaceholder: "One hosted file URL per line",
175
+ referenceRequired: true,
176
+ icon: IconUpload,
177
+ },
178
+ ];
179
+
180
+ export interface CreativeContextPanelProps {
181
+ scope?: AgentPageScope;
182
+ canManageOrg?: boolean;
183
+ scopeControl?: ReactNode;
184
+ connectionsHref?: string;
185
+ }
186
+
187
+ function isVisibleInScope(
188
+ visibility: "private" | "org" | "public",
189
+ scope: AgentPageScope,
190
+ ) {
191
+ return scope === "org" ? visibility !== "private" : visibility === "private";
192
+ }
193
+
194
+ function splitReferences(value: string) {
195
+ return value
196
+ .split(/\r?\n|,/)
197
+ .map((entry) => entry.trim())
198
+ .filter(Boolean);
199
+ }
200
+
201
+ function connectionProviderForConnector(
202
+ kind: ConnectorKind | null,
203
+ ): CreativeContextConnectionProvider | null {
204
+ if (kind === "google-slides") return "google_drive";
205
+ if (kind === "figma" || kind === "notion") return kind;
206
+ return null;
207
+ }
208
+
209
+ function recommendationProviderForConnector(
210
+ kind: ConnectorKind | null,
211
+ ): CreativeContextRecommendationProvider | null {
212
+ if (kind === "google-slides" || kind === "figma" || kind === "notion") {
213
+ return kind;
214
+ }
215
+ return null;
216
+ }
217
+
218
+ export function parseFigmaRecommendationBoundary(reference: string): {
219
+ figmaProjectId?: string;
220
+ figmaTeamId?: string;
221
+ } {
222
+ for (const value of splitReferences(reference)) {
223
+ const teamId =
224
+ value.match(/\/team\/([^/?#]+)/)?.[1] ??
225
+ (value.startsWith("team:") ? value.slice("team:".length) : undefined);
226
+ if (teamId) return { figmaTeamId: teamId };
227
+ const projectId =
228
+ value.match(/\/project\/([^/?#]+)/)?.[1] ??
229
+ (value.startsWith("project:")
230
+ ? value.slice("project:".length)
231
+ : undefined);
232
+ if (projectId) return { figmaProjectId: projectId };
233
+ }
234
+ return {};
235
+ }
236
+
237
+ export function selectRenderableLayoutThumbnails<
238
+ T extends { hasThumbnail: boolean },
239
+ >(thumbnails: readonly T[]): T[] {
240
+ return thumbnails.filter((thumbnail) => thumbnail.hasThumbnail).slice(0, 3);
241
+ }
242
+
243
+ export function mergeRecommendationSelection(
244
+ current: ReadonlySet<string>,
245
+ available: ReadonlySet<string>,
246
+ previouslySeen: ReadonlySet<string>,
247
+ ): Set<string> {
248
+ const next = new Set(
249
+ [...current].filter((externalId) => available.has(externalId)),
250
+ );
251
+ for (const externalId of available) {
252
+ if (!previouslySeen.has(externalId)) next.add(externalId);
253
+ }
254
+ return next;
255
+ }
256
+
257
+ export function buildCreativeContextSourceConfig(
258
+ kind: ConnectorKind,
259
+ reference: string,
260
+ uploadedFiles: UploadedContextFile[],
261
+ recommendations: CreativeContextRootRecommendation[] = [],
262
+ ) {
263
+ const references = splitReferences(reference);
264
+ if (kind === "google-slides") {
265
+ const presentationIds = references.flatMap((value) => {
266
+ const match = value.match(/\/presentation\/d\/([^/?#]+)/);
267
+ const id = match?.[1] ?? (/^https?:\/\//.test(value) ? "" : value);
268
+ return id ? [id] : [];
269
+ });
270
+ return {
271
+ presentationIds: [
272
+ ...new Set([
273
+ ...presentationIds,
274
+ ...recommendations.map((item) => item.externalId),
275
+ ]),
276
+ ],
277
+ };
278
+ }
279
+ if (kind === "figma") {
280
+ const fileUrls: string[] = [];
281
+ const projectUrls: string[] = [];
282
+ const teamUrls: string[] = [];
283
+ for (const url of references) {
284
+ if (/\/team\//.test(url)) teamUrls.push(url);
285
+ else if (/\/project\//.test(url)) projectUrls.push(url);
286
+ else fileUrls.push(url);
287
+ }
288
+ return {
289
+ fileUrls,
290
+ projectUrls,
291
+ teamUrls,
292
+ ...(recommendations.length
293
+ ? { fileKeys: recommendations.map((item) => item.externalId) }
294
+ : {}),
295
+ };
296
+ }
297
+ if (kind === "notion") {
298
+ const rootPageIds: string[] = [];
299
+ const rootPageUrls: string[] = [];
300
+ const teamspaceRootPageIds: string[] = [];
301
+ const teamspaceRootPageUrls: string[] = [];
302
+ for (const root of references) {
303
+ const isTeamspace = root.startsWith("teamspace:");
304
+ const value = isTeamspace ? root.slice("teamspace:".length) : root;
305
+ if (/^https?:\/\//.test(value)) {
306
+ (isTeamspace ? teamspaceRootPageUrls : rootPageUrls).push(value);
307
+ } else {
308
+ (isTeamspace ? teamspaceRootPageIds : rootPageIds).push(value);
309
+ }
310
+ }
311
+ return {
312
+ rootPageIds: [
313
+ ...rootPageIds,
314
+ ...recommendations.map((item) => item.externalId),
315
+ ],
316
+ rootPageUrls,
317
+ teamspaceRootPageIds,
318
+ teamspaceRootPageUrls,
319
+ };
320
+ }
321
+ if (kind === "website") return { urls: references };
322
+ return { items: uploadedFiles };
323
+ }
324
+
325
+ function ContextModeButton({
326
+ mode,
327
+ activeMode,
328
+ label,
329
+ description,
330
+ disabled,
331
+ onSelect,
332
+ }: {
333
+ mode: CreativeContextMode;
334
+ activeMode: CreativeContextMode;
335
+ label: string;
336
+ description: string;
337
+ disabled: boolean;
338
+ onSelect: (mode: CreativeContextMode) => void;
339
+ }) {
340
+ const selected = mode === activeMode;
341
+ return (
342
+ <button
343
+ type="button"
344
+ role="radio"
345
+ aria-checked={selected}
346
+ disabled={disabled}
347
+ onClick={() => onSelect(mode)}
348
+ className={`min-w-0 flex-1 cursor-pointer rounded-md border px-3 py-2 text-start transition-colors disabled:cursor-not-allowed disabled:opacity-60 ${
349
+ selected
350
+ ? "border-foreground/25 bg-accent/70 text-foreground"
351
+ : "border-border bg-background text-muted-foreground hover:bg-accent/40 hover:text-foreground"
352
+ }`}
353
+ >
354
+ <span className="block text-sm font-medium">{label}</span>
355
+ <span className="mt-0.5 block text-xs leading-relaxed">
356
+ {description}
357
+ </span>
358
+ </button>
359
+ );
360
+ }
361
+
362
+ function ScopeControl({
363
+ scope,
364
+ onChange,
365
+ }: {
366
+ scope: AgentPageScope;
367
+ onChange: (scope: AgentPageScope) => void;
368
+ }) {
369
+ const t = useT();
370
+ return (
371
+ <div className="inline-flex rounded-md border border-border bg-muted/30 p-0.5">
372
+ {(["user", "org"] as const).map((value) => (
373
+ <button
374
+ key={value}
375
+ type="button"
376
+ aria-pressed={scope === value}
377
+ onClick={() => onChange(value)}
378
+ className={`rounded px-2.5 py-1 text-xs font-medium transition-colors ${
379
+ scope === value
380
+ ? "bg-background text-foreground shadow-sm"
381
+ : "text-muted-foreground hover:text-foreground"
382
+ }`}
383
+ >
384
+ {t(
385
+ value === "user"
386
+ ? "creativeContext.personal"
387
+ : "creativeContext.organization",
388
+ )}
389
+ </button>
390
+ ))}
391
+ </div>
392
+ );
393
+ }
394
+
395
+ function SourceRow({
396
+ source,
397
+ refreshing,
398
+ canReview,
399
+ onRefresh,
400
+ onReview,
401
+ onCurate,
402
+ canPromote,
403
+ onPromote,
404
+ onPause,
405
+ onRestore,
406
+ onDelete,
407
+ }: {
408
+ source: ContextSourceSummary;
409
+ refreshing: boolean;
410
+ canReview: boolean;
411
+ onRefresh: (sourceId: string) => void;
412
+ onReview: (source: ContextSourceSummary) => void;
413
+ onCurate: (source: ContextSourceSummary) => void;
414
+ canPromote: boolean;
415
+ onPromote: (source: ContextSourceSummary) => void;
416
+ onPause: (source: ContextSourceSummary) => void;
417
+ onRestore: (source: ContextSourceSummary) => void;
418
+ onDelete: (source: ContextSourceSummary) => void;
419
+ }) {
420
+ const t = useT();
421
+ const { formatDate, formatNumber } = useFormatters();
422
+ return (
423
+ <div className="flex items-start gap-3 border-t border-border/60 py-3 first:border-t-0">
424
+ <div className="mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground">
425
+ <IconBooks className="size-4" />
426
+ </div>
427
+ <div className="min-w-0 flex-1">
428
+ <div className="flex flex-wrap items-center gap-2">
429
+ <span className="truncate text-sm font-medium text-foreground">
430
+ {source.name}
431
+ </span>
432
+ <Badge variant="secondary" className="font-normal">
433
+ {source.kind}
434
+ </Badge>
435
+ {source.status === "error" ? (
436
+ <Badge variant="destructive" className="font-normal">
437
+ {t("creativeContext.sourceError")}
438
+ </Badge>
439
+ ) : source.status !== "active" ? (
440
+ <Badge variant="outline" className="font-normal">
441
+ {source.status}
442
+ </Badge>
443
+ ) : null}
444
+ </div>
445
+ <div className="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-xs text-muted-foreground">
446
+ <span>
447
+ {t("creativeContext.itemsLabel", {
448
+ count: formatNumber(source.itemCount),
449
+ })}
450
+ </span>
451
+ {source.lastSyncedAt ? (
452
+ <span>
453
+ {formatDate(source.lastSyncedAt, {
454
+ month: "short",
455
+ day: "numeric",
456
+ year: "numeric",
457
+ })}
458
+ </span>
459
+ ) : null}
460
+ </div>
461
+ {source.restrictedItemCount > 0 ? (
462
+ <div className="mt-2 flex flex-wrap items-center gap-2 text-xs text-amber-700 dark:text-amber-300">
463
+ <span>
464
+ {t("creativeContext.restrictedItems", {
465
+ count: formatNumber(source.restrictedItemCount),
466
+ })}
467
+ </span>
468
+ {canReview ? (
469
+ <Button
470
+ type="button"
471
+ variant="ghost"
472
+ size="sm"
473
+ className="h-6 px-2 text-xs"
474
+ onClick={() => onReview(source)}
475
+ >
476
+ {t("creativeContext.reviewRestricted")}
477
+ </Button>
478
+ ) : null}
479
+ </div>
480
+ ) : null}
481
+ {source.lastError ? (
482
+ <p className="mt-1 line-clamp-2 text-xs text-destructive">
483
+ {source.lastError}
484
+ </p>
485
+ ) : null}
486
+ </div>
487
+ {source.status !== "archived" ? (
488
+ <Button
489
+ type="button"
490
+ variant="ghost"
491
+ size="sm"
492
+ disabled={refreshing}
493
+ onClick={() => onRefresh(source.id)}
494
+ >
495
+ <IconRefresh className={refreshing ? "animate-spin" : undefined} />
496
+ {refreshing
497
+ ? t("creativeContext.refreshing")
498
+ : t("creativeContext.refresh")}
499
+ </Button>
500
+ ) : null}
501
+ <DropdownMenu>
502
+ <DropdownMenuTrigger asChild>
503
+ <Button
504
+ type="button"
505
+ variant="ghost"
506
+ size="icon"
507
+ aria-label={t("creativeContext.manage")}
508
+ >
509
+ <IconDots />
510
+ </Button>
511
+ </DropdownMenuTrigger>
512
+ <DropdownMenuContent align="end">
513
+ <DropdownMenuItem onSelect={() => onCurate(source)}>
514
+ {t("creativeContext.curateItems")}
515
+ </DropdownMenuItem>
516
+ {source.status === "paused" || source.status === "error" ? (
517
+ <DropdownMenuItem onSelect={() => onRestore(source)}>
518
+ {t("creativeContext.restore")}
519
+ </DropdownMenuItem>
520
+ ) : (
521
+ <DropdownMenuItem onSelect={() => onPause(source)}>
522
+ {t("creativeContext.pause")}
523
+ </DropdownMenuItem>
524
+ )}
525
+ {canPromote ? (
526
+ <DropdownMenuItem onSelect={() => onPromote(source)}>
527
+ {t("creativeContext.promoteToOrganization")}
528
+ </DropdownMenuItem>
529
+ ) : null}
530
+ <DropdownMenuSeparator />
531
+ <DropdownMenuItem
532
+ className="text-destructive focus:text-destructive"
533
+ onSelect={() => onDelete(source)}
534
+ >
535
+ {t("creativeContext.delete")}
536
+ </DropdownMenuItem>
537
+ </DropdownMenuContent>
538
+ </DropdownMenu>
539
+ </div>
540
+ );
541
+ }
542
+
543
+ function PackRow({
544
+ pack,
545
+ pinned,
546
+ disabled,
547
+ onPin,
548
+ onDetails,
549
+ }: {
550
+ pack: ContextPackSummary;
551
+ pinned: boolean;
552
+ disabled: boolean;
553
+ onPin: (packId: string | null) => void;
554
+ onDetails: (packId: string) => void;
555
+ }) {
556
+ const t = useT();
557
+ const { formatNumber } = useFormatters();
558
+ return (
559
+ <div className="flex items-center gap-3 border-t border-border/60 py-3 first:border-t-0">
560
+ <div className="min-w-0 flex-1">
561
+ <div className="flex items-center gap-2">
562
+ <span className="truncate text-sm font-medium">{pack.name}</span>
563
+ {pinned ? (
564
+ <Badge variant="secondary" className="font-normal">
565
+ <IconPin className="me-1 size-3" />
566
+ {t("creativeContext.pinned")}
567
+ </Badge>
568
+ ) : null}
569
+ </div>
570
+ {pack.description ? (
571
+ <p className="mt-0.5 line-clamp-2 text-xs text-muted-foreground">
572
+ {pack.description}
573
+ </p>
574
+ ) : null}
575
+ <p className="mt-1 text-xs text-muted-foreground">
576
+ {t("creativeContext.itemsLabel", {
577
+ count: formatNumber(pack.memberCount),
578
+ })}
579
+ </p>
580
+ </div>
581
+ <Button
582
+ type="button"
583
+ variant="ghost"
584
+ size="sm"
585
+ onClick={() => onDetails(pack.id)}
586
+ >
587
+ {t("creativeContext.packDetails")}
588
+ </Button>
589
+ <Button
590
+ type="button"
591
+ variant={pinned ? "secondary" : "ghost"}
592
+ size="sm"
593
+ disabled={disabled}
594
+ onClick={() => onPin(pinned ? null : pack.id)}
595
+ >
596
+ <IconPin />
597
+ {pinned ? t("creativeContext.unpin") : t("creativeContext.pin")}
598
+ </Button>
599
+ </div>
600
+ );
601
+ }
602
+
603
+ type ContextItemReviewOperation =
604
+ | "approve"
605
+ | "exclude"
606
+ | "exemplar"
607
+ | "normal"
608
+ | "ignore"
609
+ | "star"
610
+ | "unstar"
611
+ | "deprecate"
612
+ | "restore";
613
+
614
+ function AccessScopedThumbnail({
615
+ itemId,
616
+ itemVersionId,
617
+ className,
618
+ }: {
619
+ itemId: string;
620
+ itemVersionId: string;
621
+ className: string;
622
+ }) {
623
+ const [failed, setFailed] = useState(false);
624
+ if (failed) {
625
+ return (
626
+ <div
627
+ className={`flex items-center justify-center bg-muted text-muted-foreground ${className}`}
628
+ >
629
+ <IconFileText className="size-5" />
630
+ </div>
631
+ );
632
+ }
633
+ return (
634
+ <img
635
+ src={creativeContextMediaUrl({ itemId, itemVersionId })}
636
+ alt=""
637
+ className={className}
638
+ onError={() => setFailed(true)}
639
+ />
640
+ );
641
+ }
642
+
643
+ function ItemCuration({
644
+ source,
645
+ items,
646
+ busy,
647
+ onReview,
648
+ onClose,
649
+ }: {
650
+ source: ContextSourceSummary;
651
+ items: ContextReviewItem[];
652
+ busy: boolean;
653
+ onReview: (operation: ContextItemReviewOperation, itemId: string) => void;
654
+ onClose: () => void;
655
+ }) {
656
+ const t = useT();
657
+ return (
658
+ <div className="mt-4 rounded-md border border-border p-4">
659
+ <div className="flex items-start justify-between gap-3">
660
+ <div>
661
+ <h3 className="text-sm font-semibold">
662
+ {t("creativeContext.curateItems")}
663
+ </h3>
664
+ <p className="mt-0.5 text-xs text-muted-foreground">{source.name}</p>
665
+ </div>
666
+ <Button type="button" variant="ghost" size="sm" onClick={onClose}>
667
+ {t("creativeContext.cancel")}
668
+ </Button>
669
+ </div>
670
+ <div className="mt-3 grid gap-3 sm:grid-cols-2">
671
+ {items.map((item) => (
672
+ <article
673
+ key={item.id}
674
+ className="overflow-hidden rounded-md border border-border/70"
675
+ >
676
+ {item.thumbnailBlobRef ? (
677
+ <AccessScopedThumbnail
678
+ key={item.currentVersionId}
679
+ itemId={item.id}
680
+ itemVersionId={item.currentVersionId}
681
+ className="aspect-video w-full object-cover"
682
+ />
683
+ ) : (
684
+ <div className="flex aspect-video w-full items-center justify-center bg-muted text-muted-foreground">
685
+ <IconFileText className="size-5" />
686
+ </div>
687
+ )}
688
+ <div className="p-3">
689
+ <div className="flex items-start justify-between gap-2">
690
+ <div className="min-w-0">
691
+ <p className="truncate text-sm font-medium">{item.title}</p>
692
+ <p className="text-xs text-muted-foreground">{item.kind}</p>
693
+ </div>
694
+ <div className="flex flex-wrap justify-end gap-1">
695
+ {item.curationRank === "exemplar" ? (
696
+ <Badge variant="secondary">
697
+ {t("creativeContext.exemplar")}
698
+ </Badge>
699
+ ) : null}
700
+ {item.status === "deprecated" ? (
701
+ <Badge variant="outline">
702
+ {t("creativeContext.deprecated")}
703
+ </Badge>
704
+ ) : null}
705
+ {item.upstreamAccess === "unknown" ? (
706
+ <Badge variant="outline">
707
+ {t("creativeContext.unknownAccess")}
708
+ </Badge>
709
+ ) : null}
710
+ </div>
711
+ </div>
712
+ <div className="mt-3 flex flex-wrap gap-1.5">
713
+ <Button
714
+ type="button"
715
+ variant={item.starred ? "secondary" : "outline"}
716
+ size="sm"
717
+ disabled={busy}
718
+ onClick={() =>
719
+ onReview(item.starred ? "unstar" : "star", item.id)
720
+ }
721
+ >
722
+ {t(
723
+ item.starred
724
+ ? "creativeContext.unstar"
725
+ : "creativeContext.star",
726
+ )}
727
+ </Button>
728
+ <Button
729
+ type="button"
730
+ variant={
731
+ item.curationRank === "exemplar" ? "secondary" : "outline"
732
+ }
733
+ size="sm"
734
+ disabled={busy}
735
+ onClick={() =>
736
+ onReview(
737
+ item.curationRank === "exemplar" ? "normal" : "exemplar",
738
+ item.id,
739
+ )
740
+ }
741
+ >
742
+ {t("creativeContext.exemplar")}
743
+ </Button>
744
+ <Button
745
+ type="button"
746
+ variant={
747
+ item.curationRank === "ignored" ? "secondary" : "outline"
748
+ }
749
+ size="sm"
750
+ disabled={busy}
751
+ onClick={() =>
752
+ onReview(
753
+ item.curationRank === "ignored" ? "normal" : "ignore",
754
+ item.id,
755
+ )
756
+ }
757
+ >
758
+ {t("creativeContext.ignore")}
759
+ </Button>
760
+ <Button
761
+ type="button"
762
+ variant="ghost"
763
+ size="sm"
764
+ disabled={busy}
765
+ onClick={() =>
766
+ onReview(
767
+ item.status === "deprecated" ? "restore" : "deprecate",
768
+ item.id,
769
+ )
770
+ }
771
+ >
772
+ {t(
773
+ item.status === "deprecated"
774
+ ? "creativeContext.restore"
775
+ : "creativeContext.deprecate",
776
+ )}
777
+ </Button>
778
+ </div>
779
+ {item.curationStatus === "review" ? (
780
+ <div className="mt-2 flex gap-1.5 border-t border-border/60 pt-2">
781
+ <Button
782
+ type="button"
783
+ variant="outline"
784
+ size="sm"
785
+ disabled={busy}
786
+ onClick={() => onReview("exclude", item.id)}
787
+ >
788
+ {t("creativeContext.exclude")}
789
+ </Button>
790
+ <Button
791
+ type="button"
792
+ size="sm"
793
+ disabled={busy}
794
+ onClick={() => onReview("approve", item.id)}
795
+ >
796
+ {t("creativeContext.approve")}
797
+ </Button>
798
+ </div>
799
+ ) : null}
800
+ </div>
801
+ </article>
802
+ ))}
803
+ </div>
804
+ </div>
805
+ );
806
+ }
807
+
808
+ type LibraryView = "items" | "sources" | "approvals" | "settings";
809
+
810
+ interface SafePreviewManifest {
811
+ title: string;
812
+ kind: string;
813
+ itemId: string;
814
+ itemVersionId: string;
815
+ preview: CreativeContextSafePreview | null;
816
+ media?: {
817
+ kind: string;
818
+ mimeType: string | null;
819
+ url: string;
820
+ } | null;
821
+ gallery?: Array<{
822
+ kind: string;
823
+ mimeType: string | null;
824
+ url: string;
825
+ }>;
826
+ posterUrl?: string;
827
+ }
828
+
829
+ function StructuredPreview({
830
+ preview,
831
+ compact = false,
832
+ }: {
833
+ preview: CreativeContextSafePreview | null;
834
+ compact?: boolean;
835
+ }) {
836
+ if (!preview) {
837
+ return (
838
+ <div className="flex h-full min-h-28 items-center justify-center bg-muted text-muted-foreground">
839
+ <IconFileText className="size-5" />
840
+ </div>
841
+ );
842
+ }
843
+ if (preview.type === "slides") {
844
+ const visibleSlides = compact ? preview.slides.slice(0, 3) : preview.slides;
845
+ return (
846
+ <div className="grid h-full grid-cols-3 gap-1.5 bg-muted/50 p-2">
847
+ {visibleSlides.map((slide) => (
848
+ <div
849
+ key={slide.index}
850
+ className="min-w-0 rounded border border-border/70 bg-background p-1.5"
851
+ >
852
+ <span className="text-[10px] font-medium text-muted-foreground">
853
+ {slide.index}
854
+ </span>
855
+ <p className="mt-1 line-clamp-2 text-[11px] font-medium leading-tight">
856
+ {slide.title}
857
+ </p>
858
+ {!compact && slide.excerpt ? (
859
+ <p className="mt-1 line-clamp-5 text-[10px] leading-snug text-muted-foreground">
860
+ {slide.excerpt}
861
+ </p>
862
+ ) : null}
863
+ </div>
864
+ ))}
865
+ {!visibleSlides.length ? (
866
+ <div className="col-span-3 flex items-center justify-center text-xs text-muted-foreground">
867
+ {preview.slideCount} slides
868
+ </div>
869
+ ) : null}
870
+ </div>
871
+ );
872
+ }
873
+ if (preview.type === "slide") {
874
+ return (
875
+ <div className="flex h-full flex-col justify-between bg-muted/50 p-4">
876
+ <span className="text-xs text-muted-foreground">
877
+ Slide {preview.index}
878
+ </span>
879
+ <p className="line-clamp-3 text-sm font-semibold">{preview.title}</p>
880
+ {preview.excerpt ? (
881
+ <p className="line-clamp-5 text-xs leading-relaxed text-muted-foreground">
882
+ {preview.excerpt}
883
+ </p>
884
+ ) : null}
885
+ </div>
886
+ );
887
+ }
888
+ if (preview.type === "design" || preview.type === "design-frame") {
889
+ const frames =
890
+ preview.type === "design"
891
+ ? preview.frames
892
+ : [
893
+ {
894
+ title: preview.title,
895
+ fileType: preview.fileType,
896
+ excerpt: preview.excerpt,
897
+ },
898
+ ];
899
+ const visibleFrames = compact ? frames.slice(0, 4) : frames;
900
+ return (
901
+ <div className="grid h-full grid-cols-2 gap-1.5 bg-muted/50 p-2">
902
+ {visibleFrames.map((frame, index) => (
903
+ <div
904
+ key={`${frame.title}-${index}`}
905
+ className="min-w-0 rounded border border-border/70 bg-background p-2"
906
+ >
907
+ <div className="flex items-center gap-1 text-muted-foreground">
908
+ <IconLayout className="size-3 shrink-0" />
909
+ <span className="truncate text-[10px] uppercase tracking-wide">
910
+ {frame.fileType}
911
+ </span>
912
+ </div>
913
+ <p className="mt-2 line-clamp-2 text-xs font-medium leading-tight">
914
+ {frame.title}
915
+ </p>
916
+ {!compact && frame.excerpt ? (
917
+ <p className="mt-1 line-clamp-4 text-[10px] leading-snug text-muted-foreground">
918
+ {frame.excerpt}
919
+ </p>
920
+ ) : null}
921
+ </div>
922
+ ))}
923
+ </div>
924
+ );
925
+ }
926
+ if (preview.type === "document") {
927
+ const visibleBlocks = preview.blocks.slice(0, compact ? 7 : 40);
928
+ return (
929
+ <article className="h-full overflow-auto bg-background p-4">
930
+ {visibleBlocks.length ? (
931
+ <div className="space-y-2">
932
+ {visibleBlocks.map((block, index) => {
933
+ if (block.kind === "heading") {
934
+ return (
935
+ <p
936
+ key={`${block.kind}-${index}`}
937
+ className={
938
+ (block.level ?? 2) <= 2
939
+ ? "text-sm font-semibold"
940
+ : "text-xs font-medium"
941
+ }
942
+ >
943
+ {block.text}
944
+ </p>
945
+ );
946
+ }
947
+ if (block.kind === "bullet") {
948
+ return (
949
+ <p
950
+ key={`${block.kind}-${index}`}
951
+ className="flex gap-2 text-xs leading-relaxed text-muted-foreground before:content-['•']"
952
+ >
953
+ {block.text}
954
+ </p>
955
+ );
956
+ }
957
+ if (block.kind === "quote") {
958
+ return (
959
+ <blockquote
960
+ key={`${block.kind}-${index}`}
961
+ className="border-s-2 border-border ps-3 text-xs italic leading-relaxed text-muted-foreground"
962
+ >
963
+ {block.text}
964
+ </blockquote>
965
+ );
966
+ }
967
+ if (block.kind === "code") {
968
+ return (
969
+ <pre
970
+ key={`${block.kind}-${index}`}
971
+ className="overflow-hidden rounded bg-muted p-2 font-mono text-[10px] leading-relaxed"
972
+ >
973
+ {block.text}
974
+ </pre>
975
+ );
976
+ }
977
+ return (
978
+ <p
979
+ key={`${block.kind}-${index}`}
980
+ className="text-xs leading-relaxed text-muted-foreground"
981
+ >
982
+ {block.text}
983
+ </p>
984
+ );
985
+ })}
986
+ </div>
987
+ ) : preview.excerpt ? (
988
+ <p className="mt-3 line-clamp-6 whitespace-pre-wrap text-xs leading-relaxed text-muted-foreground">
989
+ {preview.excerpt}
990
+ </p>
991
+ ) : null}
992
+ </article>
993
+ );
994
+ }
995
+ if (preview.type === "asset") {
996
+ return (
997
+ <div className="flex h-full flex-col items-center justify-center gap-2 bg-muted text-muted-foreground">
998
+ {preview.mediaType === "video" ? (
999
+ <IconPlayerPlay className="size-6" />
1000
+ ) : (
1001
+ <IconPhoto className="size-6" />
1002
+ )}
1003
+ {!compact && preview.width && preview.height ? (
1004
+ <span className="text-xs">
1005
+ {preview.width} × {preview.height}
1006
+ </span>
1007
+ ) : null}
1008
+ </div>
1009
+ );
1010
+ }
1011
+ return (
1012
+ <div className="grid h-full grid-cols-2 gap-2 bg-muted/50 p-3">
1013
+ {preview.panels.slice(0, compact ? 4 : 24).map((panel) => (
1014
+ <div
1015
+ key={panel.id}
1016
+ className="rounded border border-border/70 bg-background p-2"
1017
+ >
1018
+ <div className="flex items-center gap-1 text-muted-foreground">
1019
+ <IconChartBar className="size-3" />
1020
+ <span className="truncate text-[10px] capitalize">
1021
+ {panel.visualization}
1022
+ </span>
1023
+ </div>
1024
+ <p className="mt-2 line-clamp-2 text-xs font-medium">{panel.title}</p>
1025
+ </div>
1026
+ ))}
1027
+ {!preview.panels.length ? (
1028
+ <div className="col-span-2 flex items-center justify-center text-xs text-muted-foreground">
1029
+ Synthetic dashboard preview
1030
+ </div>
1031
+ ) : null}
1032
+ </div>
1033
+ );
1034
+ }
1035
+
1036
+ function ContextPreviewVisual({
1037
+ manifest,
1038
+ compact = false,
1039
+ }: {
1040
+ manifest: SafePreviewManifest;
1041
+ compact?: boolean;
1042
+ }) {
1043
+ if (manifest.media?.mimeType?.startsWith("video/")) {
1044
+ return (
1045
+ <video
1046
+ controls={!compact}
1047
+ muted={compact}
1048
+ playsInline
1049
+ preload="metadata"
1050
+ src={manifest.media.url}
1051
+ poster={manifest.posterUrl}
1052
+ className="h-full w-full bg-black object-contain"
1053
+ />
1054
+ );
1055
+ }
1056
+ if (manifest.media) {
1057
+ return (
1058
+ <img
1059
+ src={manifest.media.url}
1060
+ alt=""
1061
+ className="h-full w-full object-contain"
1062
+ />
1063
+ );
1064
+ }
1065
+ return <StructuredPreview preview={manifest.preview} compact={compact} />;
1066
+ }
1067
+
1068
+ function ContextPreviewSheet({
1069
+ manifest,
1070
+ onOpenChange,
1071
+ }: {
1072
+ manifest: SafePreviewManifest | null;
1073
+ onOpenChange: (open: boolean) => void;
1074
+ }) {
1075
+ const [selectedMediaUrl, setSelectedMediaUrl] = useState<string | null>(null);
1076
+ useEffect(() => setSelectedMediaUrl(null), [manifest?.itemVersionId]);
1077
+ const selectedMedia =
1078
+ manifest?.gallery?.find((medium) => medium.url === selectedMediaUrl) ??
1079
+ manifest?.media ??
1080
+ null;
1081
+ return (
1082
+ <Sheet open={Boolean(manifest)} onOpenChange={onOpenChange}>
1083
+ <SheetContent side="right" className="w-full overflow-y-auto sm:max-w-xl">
1084
+ <SheetHeader>
1085
+ <SheetTitle>{manifest?.title ?? "Context preview"}</SheetTitle>
1086
+ <SheetDescription>{manifest?.kind ?? ""}</SheetDescription>
1087
+ </SheetHeader>
1088
+ {manifest && (selectedMedia || manifest.preview) ? (
1089
+ <div className="mt-5 min-h-56 overflow-hidden rounded-md border border-border">
1090
+ <ContextPreviewVisual
1091
+ manifest={{ ...manifest, media: selectedMedia }}
1092
+ />
1093
+ </div>
1094
+ ) : (
1095
+ <div className="mt-5 flex min-h-44 items-center justify-center rounded-md border border-dashed border-border text-sm text-muted-foreground">
1096
+ No safe preview is available for this item.
1097
+ </div>
1098
+ )}
1099
+ {manifest?.gallery && manifest.gallery.length > 1 ? (
1100
+ <div className="mt-3 grid grid-cols-3 gap-2 sm:grid-cols-4">
1101
+ {manifest.gallery.map((medium, index) => (
1102
+ <button
1103
+ key={`${medium.url}-${index}`}
1104
+ type="button"
1105
+ className={`aspect-video overflow-hidden rounded border bg-muted transition-colors ${
1106
+ medium.url === (selectedMedia?.url ?? manifest.media?.url)
1107
+ ? "border-foreground"
1108
+ : "border-border hover:border-foreground/50"
1109
+ }`}
1110
+ onClick={() => setSelectedMediaUrl(medium.url)}
1111
+ >
1112
+ <img
1113
+ src={medium.url}
1114
+ alt={`Preview ${index + 1}`}
1115
+ loading="lazy"
1116
+ className="h-full w-full object-contain"
1117
+ />
1118
+ </button>
1119
+ ))}
1120
+ </div>
1121
+ ) : null}
1122
+ </SheetContent>
1123
+ </Sheet>
1124
+ );
1125
+ }
1126
+
1127
+ function ContextRail({
1128
+ contexts,
1129
+ selectedContextId,
1130
+ disabled,
1131
+ canCreate,
1132
+ onSelect,
1133
+ onCreate,
1134
+ }: {
1135
+ contexts: Array<{
1136
+ id: string;
1137
+ name: string;
1138
+ description?: string | null;
1139
+ memberCount: number;
1140
+ }>;
1141
+ selectedContextId: string | null | undefined;
1142
+ disabled: boolean;
1143
+ canCreate: boolean;
1144
+ onSelect: (contextId: string) => void;
1145
+ onCreate: () => void;
1146
+ }) {
1147
+ return (
1148
+ <aside className="border-b border-border/70 pb-5">
1149
+ <div className="flex items-center justify-between gap-3">
1150
+ <div>
1151
+ <h2 className="text-sm font-semibold">Contexts</h2>
1152
+ <p className="mt-0.5 text-xs text-muted-foreground">
1153
+ Choose the reusable context that should guide this work.
1154
+ </p>
1155
+ </div>
1156
+ <Badge variant="outline">{contexts.length}</Badge>
1157
+ </div>
1158
+ <div className="mt-3 flex gap-2 overflow-x-auto pb-1">
1159
+ {contexts.map((context) => (
1160
+ <button
1161
+ key={context.id}
1162
+ type="button"
1163
+ disabled={disabled}
1164
+ aria-pressed={selectedContextId === context.id}
1165
+ onClick={() => onSelect(context.id)}
1166
+ className={`min-w-40 rounded-md border px-3 py-2 text-start transition-colors disabled:opacity-60 ${selectedContextId === context.id ? "border-foreground/25 bg-accent text-foreground" : "border-border text-muted-foreground hover:bg-accent/50 hover:text-foreground"}`}
1167
+ >
1168
+ <span className="block truncate text-sm font-medium">
1169
+ {context.name}
1170
+ </span>
1171
+ <span className="mt-0.5 block truncate text-xs">
1172
+ {context.description || `${context.memberCount} resources`}
1173
+ </span>
1174
+ </button>
1175
+ ))}
1176
+ <Button
1177
+ type="button"
1178
+ variant="outline"
1179
+ className="min-w-40 justify-start"
1180
+ disabled={disabled || !canCreate}
1181
+ onClick={onCreate}
1182
+ >
1183
+ <IconPlus /> New context
1184
+ </Button>
1185
+ {!contexts.length ? (
1186
+ <p className="py-2 text-sm text-muted-foreground">
1187
+ Create a context from a resource’s Share tab to start organizing the
1188
+ Library.
1189
+ </p>
1190
+ ) : null}
1191
+ </div>
1192
+ </aside>
1193
+ );
1194
+ }
1195
+
1196
+ export function CreativeContextPanel({
1197
+ scope = "user",
1198
+ canManageOrg = false,
1199
+ scopeControl,
1200
+ connectionsHref = "/settings/integrations",
1201
+ }: CreativeContextPanelProps) {
1202
+ const t = useT();
1203
+ const { formatNumber } = useFormatters();
1204
+ const { data: org } = useOrg();
1205
+ const [libraryScope, setLibraryScope] = useState<AgentPageScope>(scope);
1206
+ const sourcesQuery = useCreativeContextSources({ limit: 100 });
1207
+ const contextsQuery = useCreativeContexts();
1208
+ const packsQuery = useCreativeContextPacks();
1209
+ const brandProfileQuery = useCreativeContextBrandProfile();
1210
+ const suggestionsQuery = useCreativeContextSuggestions();
1211
+ const logoCandidatesQuery = useCanonicalLogoCandidates(
1212
+ brandProfileQuery.data?.profile?.id,
1213
+ suggestionsQuery.data?.capabilities.canonicalLogo === true,
1214
+ );
1215
+ const refreshSource = useRefreshCreativeContextSource();
1216
+ const manageSource = useManageCreativeContextSource();
1217
+ const uploadResource = useUploadResource();
1218
+ const startImport = useStartCreativeContextImport();
1219
+ const searchContext = useCreativeContextSearch();
1220
+ const reviewItems = useReviewCreativeContextItems();
1221
+ const publishBrandDna = usePublishCreativeContextBrandDna();
1222
+ const proposeCanonicalLogo = useProposeCanonicalLogo();
1223
+ const confirmCanonicalLogo = useConfirmCanonicalLogo();
1224
+ const manageLayoutTemplate = useManageLayoutTemplate();
1225
+ const contextState = useCreativeContextState();
1226
+ const [query, setQuery] = useState("");
1227
+ const [libraryView, setLibraryView] = useState<LibraryView>("items");
1228
+ const [previewManifest, setPreviewManifest] =
1229
+ useState<SafePreviewManifest | null>(null);
1230
+ const [savingState, setSavingState] = useState(false);
1231
+ const [stateError, setStateError] = useState<string | null>(null);
1232
+ const [refreshMessage, setRefreshMessage] = useState<string | null>(null);
1233
+ const [searchError, setSearchError] = useState<string | null>(null);
1234
+ const [connectorKind, setConnectorKind] = useState<ConnectorKind | null>(
1235
+ null,
1236
+ );
1237
+ const connectionProvider = connectionProviderForConnector(connectorKind);
1238
+ const connectionsQuery = useCreativeContextConnections(connectionProvider);
1239
+ const [selectedConnectionId, setSelectedConnectionId] = useState("");
1240
+ const [sourceName, setSourceName] = useState("");
1241
+ const [sourceReference, setSourceReference] = useState("");
1242
+ const recommendationProvider =
1243
+ recommendationProviderForConnector(connectorKind);
1244
+ const recommendationsQuery = useCreativeContextRootRecommendations(
1245
+ recommendationProvider,
1246
+ selectedConnectionId || null,
1247
+ connectorKind === "figma"
1248
+ ? parseFigmaRecommendationBoundary(sourceReference)
1249
+ : {},
1250
+ );
1251
+ const [selectedRecommendationIds, setSelectedRecommendationIds] = useState<
1252
+ Set<string>
1253
+ >(() => new Set());
1254
+ const seenRecommendationIdsRef = useRef<Set<string>>(new Set());
1255
+ const [pickerRecommendations, setPickerRecommendations] = useState<
1256
+ CreativeContextRootRecommendation[]
1257
+ >([]);
1258
+ const [openingGooglePicker, setOpeningGooglePicker] = useState(false);
1259
+ const googlePickerSession = useCreativeContextGooglePickerSession(
1260
+ connectorKind === "google-slides" && selectedConnectionId
1261
+ ? selectedConnectionId
1262
+ : null,
1263
+ );
1264
+ const [uploadedFiles, setUploadedFiles] = useState<UploadedContextFile[]>([]);
1265
+ const fileInputRef = useRef<HTMLInputElement>(null);
1266
+ const folderInputRef = useRef<HTMLInputElement>(null);
1267
+ const [setupError, setSetupError] = useState<string | null>(null);
1268
+ const [previewSourceId, setPreviewSourceId] = useState<string | null>(null);
1269
+ const [previewSourceName, setPreviewSourceName] = useState("");
1270
+ const [selectedPreviewItemIds, setSelectedPreviewItemIds] = useState<
1271
+ Set<string>
1272
+ >(() => new Set());
1273
+ const initializedPreviewSelectionRef = useRef<string | null>(null);
1274
+ const [importJobId, setImportJobId] = useState<string | null>(null);
1275
+ const [importTargetScope, setImportTargetScope] =
1276
+ useState<AgentPageScope>("user");
1277
+ const [completedJobId, setCompletedJobId] = useState<string | null>(null);
1278
+ const [promotionPreview, setPromotionPreview] = useState<NonNullable<
1279
+ Awaited<ReturnType<typeof manageSource.mutateAsync>>["promotionPreview"]
1280
+ > | null>(null);
1281
+ const [promotionSourceId, setPromotionSourceId] = useState<string | null>(
1282
+ null,
1283
+ );
1284
+ const [promotionMessage, setPromotionMessage] = useState<string | null>(null);
1285
+ const [lifecycleMessage, setLifecycleMessage] = useState<string | null>(null);
1286
+ const [deleteSource, setDeleteSource] = useState<ContextSourceSummary | null>(
1287
+ null,
1288
+ );
1289
+ const [hiddenSourceIds, setHiddenSourceIds] = useState<Set<string>>(
1290
+ () => new Set(),
1291
+ );
1292
+ const [publishedMessage, setPublishedMessage] = useState<string | null>(null);
1293
+ const [reviewSource, setReviewSource] = useState<ContextSourceSummary | null>(
1294
+ null,
1295
+ );
1296
+ const [reviewedItems, setReviewedItems] = useState<ContextReviewItem[]>([]);
1297
+ const [reviewError, setReviewError] = useState<string | null>(null);
1298
+ const [membershipUpdateCandidate, setMembershipUpdateCandidate] = useState<{
1299
+ id: string;
1300
+ title: string;
1301
+ } | null>(null);
1302
+ const [updatingMembershipId, setUpdatingMembershipId] = useState<
1303
+ string | null
1304
+ >(null);
1305
+ const [selectedPackId, setSelectedPackId] = useState<string | null>(null);
1306
+ const [suggestionError, setSuggestionError] = useState<string | null>(null);
1307
+ const previewQuery = usePreviewCreativeContextImport(previewSourceId);
1308
+ const importStatusQuery = useCreativeContextImportStatus(importJobId);
1309
+ const packQuery = useCreativeContextPack(selectedPackId);
1310
+
1311
+ useEffect(() => setLibraryScope(scope), [scope]);
1312
+
1313
+ useEffect(() => {
1314
+ if (!connectionProvider) {
1315
+ setSelectedConnectionId("");
1316
+ return;
1317
+ }
1318
+ setSelectedConnectionId(
1319
+ connectionsQuery.data?.autoSelectedConnectionId ?? "",
1320
+ );
1321
+ }, [connectionProvider, connectionsQuery.data?.autoSelectedConnectionId]);
1322
+
1323
+ const availableRecommendations = useMemo(() => {
1324
+ const byId = new Map<string, CreativeContextRootRecommendation>();
1325
+ for (const recommendation of [
1326
+ ...(recommendationsQuery.data?.recommendations ?? []),
1327
+ ...pickerRecommendations,
1328
+ ]) {
1329
+ byId.set(recommendation.externalId, recommendation);
1330
+ }
1331
+ return [...byId.values()];
1332
+ }, [pickerRecommendations, recommendationsQuery.data?.recommendations]);
1333
+
1334
+ useEffect(() => {
1335
+ const availableIds = new Set(
1336
+ availableRecommendations.map(({ externalId }) => externalId),
1337
+ );
1338
+ const previouslySeen = seenRecommendationIdsRef.current;
1339
+ setSelectedRecommendationIds((current) =>
1340
+ mergeRecommendationSelection(current, availableIds, previouslySeen),
1341
+ );
1342
+ seenRecommendationIdsRef.current = availableIds;
1343
+ }, [availableRecommendations]);
1344
+
1345
+ const sources = useMemo(
1346
+ () =>
1347
+ (sourcesQuery.data?.sources ?? []).filter(
1348
+ (source) =>
1349
+ source.status !== "archived" &&
1350
+ !hiddenSourceIds.has(source.id) &&
1351
+ isVisibleInScope(source.visibility, libraryScope),
1352
+ ),
1353
+ [hiddenSourceIds, libraryScope, sourcesQuery.data?.sources],
1354
+ );
1355
+ const packs = useMemo(
1356
+ () =>
1357
+ (packsQuery.data?.packs ?? []).filter(
1358
+ (pack) =>
1359
+ !pack.archivedAt && isVisibleInScope(pack.visibility, libraryScope),
1360
+ ),
1361
+ [libraryScope, packsQuery.data?.packs],
1362
+ );
1363
+ const contexts = useMemo(
1364
+ () => parseCreativeContexts(contextsQuery.data),
1365
+ [contextsQuery.data],
1366
+ );
1367
+ const selectedLibraryContextId =
1368
+ contextState.state.selectedContextId ?? contexts[0]?.id ?? null;
1369
+ const contextMembershipsQuery = useContextMemberships(
1370
+ selectedLibraryContextId ? { contextId: selectedLibraryContextId } : null,
1371
+ );
1372
+ const manageContext = useManageCreativeContext();
1373
+ const manageContextMembership = useManageContextMembership();
1374
+ const contextMemberships = parseContextMemberships(
1375
+ contextMembershipsQuery.data,
1376
+ );
1377
+ const publishedContextMemberships = contextMemberships.filter(
1378
+ (membership) => membership.publishedItem,
1379
+ );
1380
+ const pendingContextMemberships = contextMemberships.filter(
1381
+ (membership) => membership.pendingSubmission,
1382
+ );
1383
+ const selectedLibraryContext = contexts.find(
1384
+ (context) => context.id === selectedLibraryContextId,
1385
+ );
1386
+ const [contextSettingsName, setContextSettingsName] = useState("");
1387
+ const [contextSettingsDescription, setContextSettingsDescription] =
1388
+ useState("");
1389
+ const [contextSettingsPolicy, setContextSettingsPolicy] = useState<
1390
+ "open" | "review" | "admins-only"
1391
+ >("open");
1392
+ const [newContextName, setNewContextName] = useState("");
1393
+ const [newContextPolicy, setNewContextPolicy] = useState<
1394
+ "open" | "review" | "admins-only"
1395
+ >("open");
1396
+ const [contextSettingsError, setContextSettingsError] = useState<
1397
+ string | null
1398
+ >(null);
1399
+ const activePack = packs.find(
1400
+ (pack) => pack.id === contextState.state.currentPackId,
1401
+ );
1402
+ const selectedConnector = CONNECTORS.find(
1403
+ (connector) => connector.kind === connectorKind,
1404
+ );
1405
+ const importJob = importStatusQuery.data?.job;
1406
+ const importResult = importJob?.result as ContextImportJobResult | null;
1407
+ const brandProposal = importResult?.inference?.brandDnaProposal;
1408
+ const brandLayoutThumbnails = selectRenderableLayoutThumbnails(
1409
+ brandProposal?.layoutThumbnails ?? [],
1410
+ );
1411
+ const brandVoicePreview =
1412
+ brandProposal?.voiceDescriptors?.join(" · ") ?? brandProposal?.voiceLine;
1413
+ const canManageScope = libraryScope === "user" || canManageOrg;
1414
+ const canCreateContext =
1415
+ canManageScope && contexts.some((context) => context.access.canAdmin);
1416
+ const activeAppId = contextsQuery.data?.appId;
1417
+ const appDefaultContextId = contextsQuery.data?.appDefaultContextId ?? null;
1418
+ const canSetAppDefault = Boolean(
1419
+ activeAppId && selectedLibraryContext?.access.canAdmin && canManageScope,
1420
+ );
1421
+ const proposalCapabilities = suggestionsQuery.data?.capabilities;
1422
+ const logoCandidates = proposalCapabilities?.canonicalLogo
1423
+ ? (logoCandidatesQuery.data?.candidates ?? [])
1424
+ : [];
1425
+ const proposedLayouts = (suggestionsQuery.data?.suggestions ?? []).filter(
1426
+ (suggestion) =>
1427
+ suggestion.kind === "layout-template" &&
1428
+ (suggestion.status === "proposed" || suggestion.status === "promoted"),
1429
+ );
1430
+
1431
+ useEffect(() => {
1432
+ setContextSettingsName(selectedLibraryContext?.name ?? "");
1433
+ setContextSettingsDescription(selectedLibraryContext?.description ?? "");
1434
+ setContextSettingsPolicy(selectedLibraryContext?.approvalPolicy ?? "open");
1435
+ setContextSettingsError(null);
1436
+ }, [selectedLibraryContext]);
1437
+
1438
+ useEffect(() => {
1439
+ if (!previewSourceId || !previewQuery.data) return;
1440
+ const defaults = previewQuery.data.smartDefaultExternalIds ?? [];
1441
+ const initializationKey = `${previewSourceId}:${defaults.join("\u0000")}`;
1442
+ if (initializedPreviewSelectionRef.current === initializationKey) return;
1443
+ const discoveredIds = new Set(
1444
+ previewQuery.data.items.map((item) => item.externalId),
1445
+ );
1446
+ setSelectedPreviewItemIds(
1447
+ new Set(defaults.filter((externalId) => discoveredIds.has(externalId))),
1448
+ );
1449
+ initializedPreviewSelectionRef.current = initializationKey;
1450
+ }, [previewQuery.data, previewSourceId]);
1451
+
1452
+ useEffect(() => {
1453
+ if (importJob?.status !== "completed" || completedJobId === importJob.id)
1454
+ return;
1455
+ setCompletedJobId(importJob.id);
1456
+ void sourcesQuery.refetch();
1457
+ void packsQuery.refetch();
1458
+ void brandProfileQuery.refetch();
1459
+ if (importTargetScope === "org" && previewSourceId) {
1460
+ setPromotionSourceId(previewSourceId);
1461
+ void manageSource
1462
+ .mutateAsync({
1463
+ operation: "preview-promotion",
1464
+ sourceId: previewSourceId,
1465
+ })
1466
+ .then((result) => setPromotionPreview(result.promotionPreview ?? null))
1467
+ .catch(() => setPromotionMessage(t("creativeContext.saveFailed")));
1468
+ }
1469
+ }, [
1470
+ brandProfileQuery,
1471
+ completedJobId,
1472
+ importJob,
1473
+ importTargetScope,
1474
+ manageSource,
1475
+ packsQuery,
1476
+ previewSourceId,
1477
+ sourcesQuery,
1478
+ t,
1479
+ ]);
1480
+
1481
+ async function changeMode(mode: CreativeContextMode) {
1482
+ if (mode === contextState.state.contextMode) return;
1483
+ setSavingState(true);
1484
+ setStateError(null);
1485
+ try {
1486
+ await contextState.setState(
1487
+ mode === "off"
1488
+ ? {
1489
+ contextMode: "off",
1490
+ selectedContextId: null,
1491
+ currentPackId: null,
1492
+ pinnedPackId: null,
1493
+ }
1494
+ : {
1495
+ ...contextState.state,
1496
+ contextMode: "auto",
1497
+ selectedContextId: null,
1498
+ pinnedPackId: null,
1499
+ },
1500
+ );
1501
+ } catch {
1502
+ setStateError(t("creativeContext.stateSaveFailed"));
1503
+ } finally {
1504
+ setSavingState(false);
1505
+ }
1506
+ }
1507
+
1508
+ async function changePinnedPack(packId: string | null) {
1509
+ setSavingState(true);
1510
+ setStateError(null);
1511
+ try {
1512
+ await contextState.setState({
1513
+ ...contextState.state,
1514
+ contextMode: "auto",
1515
+ selectedContextId: null,
1516
+ pinnedPackId: packId,
1517
+ });
1518
+ } catch {
1519
+ setStateError(t("creativeContext.stateSaveFailed"));
1520
+ } finally {
1521
+ setSavingState(false);
1522
+ }
1523
+ }
1524
+
1525
+ async function selectContext(contextId: string) {
1526
+ setSavingState(true);
1527
+ setStateError(null);
1528
+ try {
1529
+ await contextState.setState({
1530
+ ...contextState.state,
1531
+ contextMode: "auto",
1532
+ selectedContextId: contextId,
1533
+ pinnedPackId: null,
1534
+ });
1535
+ } catch {
1536
+ setStateError(t("creativeContext.stateSaveFailed"));
1537
+ } finally {
1538
+ setSavingState(false);
1539
+ }
1540
+ }
1541
+
1542
+ async function reviewContextMembership(
1543
+ membershipId: string,
1544
+ operation: "approve" | "request-changes",
1545
+ ) {
1546
+ if (!selectedLibraryContextId) return;
1547
+ try {
1548
+ await manageContextMembership.mutateAsync({
1549
+ operation,
1550
+ contextId: selectedLibraryContextId,
1551
+ membershipId,
1552
+ });
1553
+ await contextMembershipsQuery.refetch();
1554
+ } catch {
1555
+ setReviewError(t("creativeContext.saveFailed"));
1556
+ }
1557
+ }
1558
+
1559
+ async function submitLatestContextMembershipUpdate() {
1560
+ if (!selectedLibraryContextId || !membershipUpdateCandidate) return;
1561
+ setReviewError(null);
1562
+ setUpdatingMembershipId(membershipUpdateCandidate.id);
1563
+ try {
1564
+ await manageContextMembership.mutateAsync({
1565
+ operation: "submit-latest",
1566
+ contextId: selectedLibraryContextId,
1567
+ membershipId: membershipUpdateCandidate.id,
1568
+ confirmBroaderPublication: true,
1569
+ });
1570
+ setMembershipUpdateCandidate(null);
1571
+ await contextMembershipsQuery.refetch();
1572
+ } catch {
1573
+ setReviewError(t("creativeContext.submitUpdateFailed"));
1574
+ } finally {
1575
+ setUpdatingMembershipId(null);
1576
+ }
1577
+ }
1578
+
1579
+ function refresh(sourceId: string) {
1580
+ setRefreshMessage(null);
1581
+ refreshSource.mutate(
1582
+ { sourceId, mode: "incremental" },
1583
+ {
1584
+ onSuccess: () => setRefreshMessage(t("creativeContext.refreshed")),
1585
+ onError: () => setRefreshMessage(t("creativeContext.refreshFailed")),
1586
+ },
1587
+ );
1588
+ }
1589
+
1590
+ async function uploadFiles(files: File[]) {
1591
+ if (!files.length) return;
1592
+ setSetupError(null);
1593
+ try {
1594
+ const uploaded = await Promise.all(
1595
+ files.map(async (file) => {
1596
+ const relativePath =
1597
+ (file as File & { webkitRelativePath?: string })
1598
+ .webkitRelativePath || file.name;
1599
+ const safePath = relativePath
1600
+ .replace(/\.\./g, "")
1601
+ .replace(/[^a-zA-Z0-9._/-]/g, "-");
1602
+ const formData = new FormData();
1603
+ formData.append("file", file, file.name);
1604
+ formData.append(
1605
+ "path",
1606
+ `/creative-context/${Date.now()}-${safePath}`,
1607
+ );
1608
+ const resource = (await uploadResource.mutateAsync(formData)) as {
1609
+ id: string;
1610
+ content: string;
1611
+ url?: string;
1612
+ };
1613
+ const url = resource.url ?? resource.content;
1614
+ if (!url) throw new Error("Upload returned no file handle");
1615
+ return {
1616
+ id: resource.id,
1617
+ title: file.name,
1618
+ fileName: file.name,
1619
+ mimeType: file.type || "application/octet-stream",
1620
+ url,
1621
+ };
1622
+ }),
1623
+ );
1624
+ setUploadedFiles((current) => [...current, ...uploaded]);
1625
+ } catch {
1626
+ setSetupError(t("creativeContext.saveFailed"));
1627
+ }
1628
+ }
1629
+
1630
+ function chooseFiles(event: ChangeEvent<HTMLInputElement>) {
1631
+ void uploadFiles(Array.from(event.target.files ?? []));
1632
+ event.target.value = "";
1633
+ }
1634
+
1635
+ function dropFiles(event: DragEvent<HTMLDivElement>) {
1636
+ event.preventDefault();
1637
+ void uploadFiles(Array.from(event.dataTransfer.files));
1638
+ }
1639
+
1640
+ async function previewImport(event: FormEvent<HTMLFormElement>) {
1641
+ event.preventDefault();
1642
+ if (!selectedConnector || !sourceName.trim()) return;
1643
+ setSetupError(null);
1644
+ setImportJobId(null);
1645
+ setCompletedJobId(null);
1646
+ setPublishedMessage(null);
1647
+ try {
1648
+ const confirmedRecommendations = availableRecommendations.filter(
1649
+ (recommendation) =>
1650
+ selectedRecommendationIds.has(recommendation.externalId),
1651
+ );
1652
+ const result = await manageSource.mutateAsync({
1653
+ operation: "create",
1654
+ name: sourceName.trim(),
1655
+ kind: selectedConnector.kind,
1656
+ connectionId: selectedConnectionId || undefined,
1657
+ externalRef:
1658
+ selectedConnector.kind === "upload"
1659
+ ? `${uploadedFiles.length} files`
1660
+ : sourceReference.trim() ||
1661
+ (confirmedRecommendations.length
1662
+ ? `${selectedConnector.kind}:${confirmedRecommendations
1663
+ .map((recommendation) => recommendation.externalId)
1664
+ .join(",")}`
1665
+ : undefined),
1666
+ config: buildCreativeContextSourceConfig(
1667
+ selectedConnector.kind,
1668
+ sourceReference,
1669
+ uploadedFiles,
1670
+ confirmedRecommendations,
1671
+ ),
1672
+ });
1673
+ if (!result.source) throw new Error("Source creation returned no source");
1674
+ setImportTargetScope(libraryScope);
1675
+ setPreviewSourceName(result.source.name);
1676
+ setPreviewSourceId(result.source.id);
1677
+ setSelectedPreviewItemIds(new Set());
1678
+ initializedPreviewSelectionRef.current = null;
1679
+ setPromotionSourceId(null);
1680
+ setPromotionPreview(null);
1681
+ setPromotionMessage(null);
1682
+ setConnectorKind(null);
1683
+ setSourceName("");
1684
+ setSourceReference("");
1685
+ setPickerRecommendations([]);
1686
+ seenRecommendationIdsRef.current.clear();
1687
+ setUploadedFiles([]);
1688
+ await sourcesQuery.refetch();
1689
+ } catch {
1690
+ setSetupError(t("creativeContext.saveFailed"));
1691
+ }
1692
+ }
1693
+
1694
+ async function chooseGoogleSlides() {
1695
+ setSetupError(null);
1696
+ setOpeningGooglePicker(true);
1697
+ try {
1698
+ const session = await googlePickerSession.refetch();
1699
+ if (!session.data) {
1700
+ throw new Error("Google Picker session is unavailable.");
1701
+ }
1702
+ const selections = await chooseGoogleSlidesPresentations(session.data);
1703
+ if (!selections.length) return;
1704
+ const selected = selections.map((selection) => ({
1705
+ ...selection,
1706
+ provider: "google-slides" as const,
1707
+ kind: "presentation" as const,
1708
+ }));
1709
+ setPickerRecommendations((current) => {
1710
+ const byId = new Map(
1711
+ current.map((recommendation) => [
1712
+ recommendation.externalId,
1713
+ recommendation,
1714
+ ]),
1715
+ );
1716
+ for (const recommendation of selected) {
1717
+ byId.set(recommendation.externalId, recommendation);
1718
+ }
1719
+ return [...byId.values()];
1720
+ });
1721
+ setSelectedRecommendationIds((current) => {
1722
+ const next = new Set(current);
1723
+ for (const recommendation of selected) {
1724
+ next.add(recommendation.externalId);
1725
+ }
1726
+ return next;
1727
+ });
1728
+ void recommendationsQuery.refetch();
1729
+ } catch (error) {
1730
+ setSetupError(error instanceof Error ? error.message : String(error));
1731
+ } finally {
1732
+ setOpeningGooglePicker(false);
1733
+ }
1734
+ }
1735
+
1736
+ async function confirmPromotion() {
1737
+ if (!promotionSourceId || !promotionPreview) return;
1738
+ setPromotionMessage(null);
1739
+ try {
1740
+ await manageSource.mutateAsync({
1741
+ operation: "promote",
1742
+ sourceId: promotionSourceId,
1743
+ confirmation: {
1744
+ containerRef: promotionPreview.containerRef,
1745
+ boundaryHash: promotionPreview.boundaryHash,
1746
+ itemCount: promotionPreview.itemCount,
1747
+ },
1748
+ });
1749
+ setPromotionMessage(t("creativeContext.promotionComplete"));
1750
+ setPromotionPreview(null);
1751
+ setPromotionSourceId(null);
1752
+ await sourcesQuery.refetch();
1753
+ } catch {
1754
+ setPromotionMessage(t("creativeContext.saveFailed"));
1755
+ }
1756
+ }
1757
+
1758
+ async function pauseSource(source: ContextSourceSummary) {
1759
+ setLifecycleMessage(null);
1760
+ try {
1761
+ await manageSource.mutateAsync({
1762
+ operation: "update",
1763
+ sourceId: source.id,
1764
+ patch: { status: "paused" },
1765
+ });
1766
+ setLifecycleMessage(t("creativeContext.sourcePaused"));
1767
+ await sourcesQuery.refetch();
1768
+ } catch {
1769
+ setLifecycleMessage(t("creativeContext.saveFailed"));
1770
+ }
1771
+ }
1772
+
1773
+ async function restoreSource(source: ContextSourceSummary) {
1774
+ setLifecycleMessage(null);
1775
+ try {
1776
+ await manageSource.mutateAsync({
1777
+ operation: "restore",
1778
+ sourceId: source.id,
1779
+ });
1780
+ setLifecycleMessage(t("creativeContext.sourceRestored"));
1781
+ await sourcesQuery.refetch();
1782
+ } catch {
1783
+ setLifecycleMessage(t("creativeContext.saveFailed"));
1784
+ }
1785
+ }
1786
+
1787
+ async function previewSourcePromotion(source: ContextSourceSummary) {
1788
+ setPromotionMessage(null);
1789
+ setPromotionPreview(null);
1790
+ setPromotionSourceId(source.id);
1791
+ try {
1792
+ const result = await manageSource.mutateAsync({
1793
+ operation: "preview-promotion",
1794
+ sourceId: source.id,
1795
+ });
1796
+ setPromotionPreview(result.promotionPreview ?? null);
1797
+ } catch {
1798
+ setPromotionSourceId(null);
1799
+ setPromotionMessage(t("creativeContext.saveFailed"));
1800
+ }
1801
+ }
1802
+
1803
+ async function confirmDeleteSource() {
1804
+ if (!deleteSource) return;
1805
+ const sourceId = deleteSource.id;
1806
+ setLifecycleMessage(null);
1807
+ setDeleteSource(null);
1808
+ setHiddenSourceIds((current) => new Set(current).add(sourceId));
1809
+ try {
1810
+ await manageSource.mutateAsync({ operation: "delete", sourceId });
1811
+ setLifecycleMessage(t("creativeContext.deletionQueued"));
1812
+ void sourcesQuery.refetch();
1813
+ } catch {
1814
+ setHiddenSourceIds((current) => {
1815
+ const next = new Set(current);
1816
+ next.delete(sourceId);
1817
+ return next;
1818
+ });
1819
+ setLifecycleMessage(t("creativeContext.saveFailed"));
1820
+ }
1821
+ }
1822
+
1823
+ async function beginImport() {
1824
+ if (!previewSourceId || !selectedPreviewItemIds.size) return;
1825
+ setSetupError(null);
1826
+ try {
1827
+ const result = await startImport.mutateAsync({
1828
+ sourceId: previewSourceId,
1829
+ mode: "incremental",
1830
+ itemExternalIds: (previewQuery.data?.items ?? [])
1831
+ .map((item) => item.externalId)
1832
+ .filter((externalId) => selectedPreviewItemIds.has(externalId)),
1833
+ });
1834
+ setImportJobId(result.job.id);
1835
+ } catch {
1836
+ setSetupError(t("creativeContext.importFailed"));
1837
+ }
1838
+ }
1839
+
1840
+ async function publishProposal() {
1841
+ const importResult = importJob?.result as ContextImportJobResult | null;
1842
+ const proposal = importResult?.inference?.brandDnaProposal;
1843
+ if (!proposal) return;
1844
+ setPublishedMessage(null);
1845
+ try {
1846
+ const result = await publishBrandDna.mutateAsync({
1847
+ profileId: proposal.profileId,
1848
+ proposalVersionId: proposal.dnaVersionId,
1849
+ confirmation: {
1850
+ proposalVersionId: proposal.dnaVersionId,
1851
+ contentHash: proposal.contentHash,
1852
+ },
1853
+ });
1854
+ setPublishedMessage(
1855
+ `${t("creativeContext.brandContextPublished")} · ${result.profile.name} · v${result.dna.versionNumber}`,
1856
+ );
1857
+ await brandProfileQuery.refetch();
1858
+ } catch {
1859
+ setPublishedMessage(t("creativeContext.saveFailed"));
1860
+ }
1861
+ }
1862
+
1863
+ async function openItemCuration(
1864
+ source: ContextSourceSummary,
1865
+ queue: "restricted" | "all" = "all",
1866
+ ) {
1867
+ setReviewSource(source);
1868
+ setReviewError(null);
1869
+ try {
1870
+ const result = await reviewItems.mutateAsync({
1871
+ sourceId: source.id,
1872
+ operation: "list",
1873
+ queue,
1874
+ limit: 100,
1875
+ });
1876
+ setReviewedItems(result.items);
1877
+ } catch {
1878
+ setReviewedItems([]);
1879
+ setReviewError(t("creativeContext.unavailable"));
1880
+ }
1881
+ }
1882
+
1883
+ async function reviewContextItem(
1884
+ operation: ContextItemReviewOperation,
1885
+ itemId: string,
1886
+ ) {
1887
+ if (!reviewSource) return;
1888
+ setReviewError(null);
1889
+ try {
1890
+ await reviewItems.mutateAsync({
1891
+ sourceId: reviewSource.id,
1892
+ operation,
1893
+ itemIds: [itemId],
1894
+ });
1895
+ const result = await reviewItems.mutateAsync({
1896
+ sourceId: reviewSource.id,
1897
+ operation: "list",
1898
+ queue: "all",
1899
+ limit: 100,
1900
+ });
1901
+ setReviewedItems(result.items);
1902
+ await sourcesQuery.refetch();
1903
+ } catch {
1904
+ setReviewError(t("creativeContext.saveFailed"));
1905
+ }
1906
+ }
1907
+
1908
+ async function chooseCanonicalLogo(
1909
+ candidate: NonNullable<
1910
+ typeof logoCandidatesQuery.data
1911
+ >["candidates"][number],
1912
+ ) {
1913
+ setSuggestionError(null);
1914
+ try {
1915
+ const suggestion = await proposeCanonicalLogo.mutateAsync({
1916
+ profileId: brandProfileQuery.data?.profile?.id,
1917
+ itemId: candidate.itemId,
1918
+ itemVersionId: candidate.itemVersionId,
1919
+ reason: "Selected from the ranked Library review card",
1920
+ payload: { mediaId: candidate.mediaId },
1921
+ });
1922
+ await confirmCanonicalLogo.mutateAsync({
1923
+ suggestionId: suggestion.id,
1924
+ decision: "confirm",
1925
+ });
1926
+ await suggestionsQuery.refetch();
1927
+ } catch {
1928
+ setSuggestionError(t("creativeContext.saveFailed"));
1929
+ }
1930
+ }
1931
+
1932
+ async function decideLayoutSuggestion(
1933
+ suggestionId: string,
1934
+ operation: "promote" | "demote" | "reject",
1935
+ ) {
1936
+ setSuggestionError(null);
1937
+ try {
1938
+ await manageLayoutTemplate.mutateAsync({ suggestionId, operation });
1939
+ await suggestionsQuery.refetch();
1940
+ } catch {
1941
+ setSuggestionError(t("creativeContext.saveFailed"));
1942
+ }
1943
+ }
1944
+
1945
+ async function saveContextSettings() {
1946
+ if (!selectedLibraryContext?.access.canAdmin || !contextSettingsName.trim())
1947
+ return;
1948
+ setContextSettingsError(null);
1949
+ try {
1950
+ await manageContext.mutateAsync({
1951
+ operation: "update",
1952
+ contextId: selectedLibraryContext.id,
1953
+ patch: {
1954
+ name: contextSettingsName.trim(),
1955
+ description: contextSettingsDescription.trim() || null,
1956
+ approvalPolicy: contextSettingsPolicy,
1957
+ },
1958
+ });
1959
+ await contextsQuery.refetch();
1960
+ } catch {
1961
+ setContextSettingsError("Could not update this context.");
1962
+ }
1963
+ }
1964
+
1965
+ async function createSpecialtyContext() {
1966
+ if (!canCreateContext || !newContextName.trim()) return;
1967
+ setContextSettingsError(null);
1968
+ try {
1969
+ const result = await manageContext.mutateAsync({
1970
+ operation: "create",
1971
+ name: newContextName.trim(),
1972
+ kind: "specialty",
1973
+ approvalPolicy: newContextPolicy,
1974
+ });
1975
+ setNewContextName("");
1976
+ setNewContextPolicy("open");
1977
+ await contextsQuery.refetch();
1978
+ if (result.context?.id) await selectContext(result.context.id);
1979
+ } catch {
1980
+ setContextSettingsError("Could not create this context.");
1981
+ }
1982
+ }
1983
+
1984
+ async function setAppDefaultContext() {
1985
+ if (!activeAppId || !selectedLibraryContext || !canSetAppDefault) return;
1986
+ setContextSettingsError(null);
1987
+ try {
1988
+ await manageContext.mutateAsync({
1989
+ operation: "set-app-default",
1990
+ contextId: selectedLibraryContext.id,
1991
+ appId: activeAppId,
1992
+ });
1993
+ await contextsQuery.refetch();
1994
+ } catch {
1995
+ setContextSettingsError(
1996
+ "Could not update the automatic context for this app.",
1997
+ );
1998
+ }
1999
+ }
2000
+
2001
+ async function search(event: FormEvent<HTMLFormElement>) {
2002
+ event.preventDefault();
2003
+ const searchText = query.trim();
2004
+ if (
2005
+ !searchText ||
2006
+ (!sources.length &&
2007
+ !selectedLibraryContextId &&
2008
+ !contextState.state.pinnedPackId)
2009
+ )
2010
+ return;
2011
+ setSearchError(null);
2012
+ try {
2013
+ const result = await searchContext.mutateAsync({
2014
+ query: searchText,
2015
+ sourceIds:
2016
+ selectedLibraryContextId || contextState.state.pinnedPackId
2017
+ ? undefined
2018
+ : sources.map((source) => source.id),
2019
+ packId: contextState.state.pinnedPackId ?? undefined,
2020
+ contextId: contextState.state.pinnedPackId
2021
+ ? undefined
2022
+ : (selectedLibraryContextId ?? undefined),
2023
+ limit: 20,
2024
+ snapshot: true,
2025
+ });
2026
+ if (result.contextPackId && contextState.state.contextMode === "auto") {
2027
+ await contextState.setState({
2028
+ ...contextState.state,
2029
+ currentPackId: result.contextPackId,
2030
+ });
2031
+ }
2032
+ } catch {
2033
+ setSearchError(t("creativeContext.unavailable"));
2034
+ }
2035
+ }
2036
+
2037
+ const loading =
2038
+ sourcesQuery.isLoading ||
2039
+ packsQuery.isLoading ||
2040
+ contextsQuery.isLoading ||
2041
+ contextState.isLoading;
2042
+ const unavailable =
2043
+ sourcesQuery.error || packsQuery.error || contextsQuery.error;
2044
+
2045
+ return (
2046
+ <div className="mx-auto flex w-full max-w-5xl flex-col gap-7 p-6 lg:p-10">
2047
+ <header className="flex flex-wrap items-start justify-between gap-4 border-b border-border/70 pb-5">
2048
+ <div>
2049
+ <h1 className="text-2xl font-semibold tracking-tight">
2050
+ {t("creativeContext.title")}
2051
+ </h1>
2052
+ <p className="mt-1 max-w-2xl text-sm text-muted-foreground">
2053
+ {t("creativeContext.description")}
2054
+ </p>
2055
+ </div>
2056
+ <div className="flex flex-wrap items-center gap-2">
2057
+ {scopeControl ??
2058
+ (org?.orgId ? (
2059
+ <ScopeControl scope={libraryScope} onChange={setLibraryScope} />
2060
+ ) : null)}
2061
+ <CreativeContextChip
2062
+ state={contextState.state}
2063
+ packs={packs}
2064
+ contexts={contexts}
2065
+ />
2066
+ </div>
2067
+ </header>
2068
+
2069
+ {loading ? (
2070
+ <div className="space-y-3" aria-label={t("creativeContext.loading")}>
2071
+ <Skeleton className="h-24 w-full" />
2072
+ <Skeleton className="h-40 w-full" />
2073
+ <Skeleton className="h-32 w-full" />
2074
+ </div>
2075
+ ) : unavailable ? (
2076
+ <div className="flex items-center gap-3 rounded-md border border-dashed border-border p-6 text-sm text-muted-foreground">
2077
+ <IconAlertTriangle className="size-5" />
2078
+ {t("creativeContext.unavailable")}
2079
+ </div>
2080
+ ) : (
2081
+ <>
2082
+ <ContextRail
2083
+ contexts={contexts}
2084
+ selectedContextId={contextState.state.selectedContextId}
2085
+ disabled={savingState}
2086
+ canCreate={canCreateContext}
2087
+ onSelect={(contextId) => void selectContext(contextId)}
2088
+ onCreate={() => setLibraryView("settings")}
2089
+ />
2090
+ <Tabs
2091
+ value={libraryView}
2092
+ onValueChange={(value) => setLibraryView(value as LibraryView)}
2093
+ >
2094
+ <TabsList className="w-full justify-start overflow-x-auto">
2095
+ <TabsTrigger value="items">Items</TabsTrigger>
2096
+ <TabsTrigger value="sources">Sources</TabsTrigger>
2097
+ <TabsTrigger value="approvals">Approvals</TabsTrigger>
2098
+ <TabsTrigger value="settings">Settings</TabsTrigger>
2099
+ </TabsList>
2100
+ <TabsContent value="items">
2101
+ <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
2102
+ {publishedContextMemberships.map((membership) => {
2103
+ const item = membership.publishedItem!;
2104
+ const imageMedium = item.media.find((medium) =>
2105
+ medium.mimeType?.startsWith("image/"),
2106
+ );
2107
+ const playbackMedium = item.media.find((medium) =>
2108
+ medium.mimeType?.startsWith("video/"),
2109
+ );
2110
+ const medium = imageMedium ?? playbackMedium ?? item.media[0];
2111
+ const sheetMedium = playbackMedium ?? medium;
2112
+ const updateAvailable =
2113
+ membership.nativeUpdateStatus?.state === "update-available";
2114
+ return (
2115
+ <article
2116
+ key={membership.id}
2117
+ className="overflow-hidden rounded-md border border-border"
2118
+ >
2119
+ <button
2120
+ type="button"
2121
+ onClick={() =>
2122
+ setPreviewManifest({
2123
+ title: item.title,
2124
+ kind: item.kind,
2125
+ itemId: item.id,
2126
+ itemVersionId: item.itemVersionId,
2127
+ preview: item.preview,
2128
+ media: sheetMedium ?? null,
2129
+ gallery: item.media.filter((candidate) =>
2130
+ candidate.mimeType?.startsWith("image/"),
2131
+ ),
2132
+ posterUrl:
2133
+ playbackMedium && imageMedium
2134
+ ? imageMedium.url
2135
+ : undefined,
2136
+ })
2137
+ }
2138
+ className="block w-full text-start transition-colors hover:bg-accent/40"
2139
+ >
2140
+ <span className="block aspect-video overflow-hidden">
2141
+ <ContextPreviewVisual
2142
+ compact
2143
+ manifest={{
2144
+ title: item.title,
2145
+ kind: item.kind,
2146
+ itemId: item.id,
2147
+ itemVersionId: item.itemVersionId,
2148
+ preview: item.preview,
2149
+ media: medium ?? null,
2150
+ gallery: item.media,
2151
+ }}
2152
+ />
2153
+ </span>
2154
+ <span className="block p-3">
2155
+ <span className="block truncate text-sm font-medium">
2156
+ {item.title}
2157
+ </span>
2158
+ <span className="mt-1 flex flex-wrap items-center gap-1.5 text-xs text-muted-foreground">
2159
+ <span>{item.kind}</span>
2160
+ <span>·</span>
2161
+ <span className="capitalize">
2162
+ {membership.rank}
2163
+ </span>
2164
+ <Badge variant="secondary">Published</Badge>
2165
+ {updateAvailable ? (
2166
+ <Badge variant="outline">
2167
+ {t("creativeContext.updateAvailable")}
2168
+ </Badge>
2169
+ ) : null}
2170
+ </span>
2171
+ <span className="mt-1 block truncate font-mono text-[10px] text-muted-foreground">
2172
+ Version {item.itemVersionId.slice(0, 12)}
2173
+ </span>
2174
+ </span>
2175
+ </button>
2176
+ {updateAvailable &&
2177
+ selectedLibraryContext?.access.canSubmit ? (
2178
+ <div className="border-t border-border/70 p-2">
2179
+ <Button
2180
+ type="button"
2181
+ variant="outline"
2182
+ size="sm"
2183
+ className="w-full"
2184
+ disabled={updatingMembershipId === membership.id}
2185
+ onClick={() =>
2186
+ setMembershipUpdateCandidate({
2187
+ id: membership.id,
2188
+ title: item.title,
2189
+ })
2190
+ }
2191
+ >
2192
+ <IconRefresh />
2193
+ {updatingMembershipId === membership.id
2194
+ ? t("creativeContext.submittingUpdate")
2195
+ : t("creativeContext.submitUpdate")}
2196
+ </Button>
2197
+ </div>
2198
+ ) : null}
2199
+ </article>
2200
+ );
2201
+ })}
2202
+ </div>
2203
+ {!publishedContextMemberships.length ? (
2204
+ <p className="py-4 text-sm text-muted-foreground">
2205
+ Approved context items appear here after publication.
2206
+ </p>
2207
+ ) : null}
2208
+ {reviewError ? (
2209
+ <p className="mt-3 text-sm text-destructive">{reviewError}</p>
2210
+ ) : null}
2211
+ </TabsContent>
2212
+ <TabsContent value="approvals">
2213
+ <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
2214
+ {pendingContextMemberships.map((membership) => {
2215
+ const submission = membership.pendingSubmission!;
2216
+ const item = submission.proposedItem;
2217
+ const medium = item?.media[0];
2218
+ return (
2219
+ <article
2220
+ key={membership.id}
2221
+ className="overflow-hidden rounded-md border border-border"
2222
+ >
2223
+ {item ? (
2224
+ <button
2225
+ type="button"
2226
+ className="block aspect-video w-full overflow-hidden text-start"
2227
+ onClick={() =>
2228
+ setPreviewManifest({
2229
+ title: item.title,
2230
+ kind: item.kind,
2231
+ itemId: item.id,
2232
+ itemVersionId: item.itemVersionId,
2233
+ preview: item.preview,
2234
+ media: medium ?? null,
2235
+ gallery: item.media.filter((candidate) =>
2236
+ candidate.mimeType?.startsWith("image/"),
2237
+ ),
2238
+ })
2239
+ }
2240
+ >
2241
+ <ContextPreviewVisual
2242
+ compact
2243
+ manifest={{
2244
+ title: item.title,
2245
+ kind: item.kind,
2246
+ itemId: item.id,
2247
+ itemVersionId: item.itemVersionId,
2248
+ preview: item.preview,
2249
+ media: medium ?? null,
2250
+ }}
2251
+ />
2252
+ </button>
2253
+ ) : null}
2254
+ <div className="p-3">
2255
+ <p className="truncate text-sm font-medium">
2256
+ {item?.title ?? "Pending context submission"}
2257
+ </p>
2258
+ <p className="mt-0.5 text-xs text-muted-foreground">
2259
+ Submitted by {submission.submittedBy}
2260
+ </p>
2261
+ {submission.note ? (
2262
+ <p className="mt-2 line-clamp-3 text-xs text-muted-foreground">
2263
+ {submission.note}
2264
+ </p>
2265
+ ) : null}
2266
+ {selectedLibraryContext?.access.canReview ? (
2267
+ <div className="mt-3 flex gap-2">
2268
+ <Button
2269
+ size="sm"
2270
+ onClick={() =>
2271
+ void reviewContextMembership(
2272
+ membership.id,
2273
+ "approve",
2274
+ )
2275
+ }
2276
+ >
2277
+ Approve
2278
+ </Button>
2279
+ <Button
2280
+ size="sm"
2281
+ variant="outline"
2282
+ onClick={() =>
2283
+ void reviewContextMembership(
2284
+ membership.id,
2285
+ "request-changes",
2286
+ )
2287
+ }
2288
+ >
2289
+ Request changes
2290
+ </Button>
2291
+ </div>
2292
+ ) : (
2293
+ <Badge variant="outline" className="mt-3">
2294
+ Awaiting review
2295
+ </Badge>
2296
+ )}
2297
+ </div>
2298
+ </article>
2299
+ );
2300
+ })}
2301
+ </div>
2302
+ {!pendingContextMemberships.length ? (
2303
+ <p className="py-4 text-sm text-muted-foreground">
2304
+ No context submissions need review.
2305
+ </p>
2306
+ ) : null}
2307
+ </TabsContent>
2308
+ <TabsContent value="sources">
2309
+ <p className="text-sm text-muted-foreground">
2310
+ Sources and their review queues are managed below.
2311
+ </p>
2312
+ </TabsContent>
2313
+ <TabsContent value="settings">
2314
+ <div className="grid gap-5 lg:grid-cols-2">
2315
+ <section className="space-y-3 rounded-md border border-border p-4">
2316
+ <div>
2317
+ <h3 className="text-sm font-semibold">Context settings</h3>
2318
+ <p className="mt-1 text-xs text-muted-foreground">
2319
+ Automatic selection uses Default plus at most one matching
2320
+ specialty. Exact packs remain available under advanced
2321
+ provenance.
2322
+ </p>
2323
+ </div>
2324
+ <Input
2325
+ value={contextSettingsName}
2326
+ disabled={!selectedLibraryContext?.access.canAdmin}
2327
+ onChange={(event) =>
2328
+ setContextSettingsName(event.target.value)
2329
+ }
2330
+ placeholder="Context name"
2331
+ />
2332
+ <Textarea
2333
+ value={contextSettingsDescription}
2334
+ disabled={!selectedLibraryContext?.access.canAdmin}
2335
+ onChange={(event) =>
2336
+ setContextSettingsDescription(event.target.value)
2337
+ }
2338
+ placeholder="When should agents use this context?"
2339
+ rows={3}
2340
+ />
2341
+ <Select
2342
+ value={contextSettingsPolicy}
2343
+ disabled={!selectedLibraryContext?.access.canAdmin}
2344
+ onValueChange={(value) =>
2345
+ setContextSettingsPolicy(
2346
+ value as "open" | "review" | "admins-only",
2347
+ )
2348
+ }
2349
+ >
2350
+ <SelectTrigger>
2351
+ <SelectValue />
2352
+ </SelectTrigger>
2353
+ <SelectContent>
2354
+ <SelectItem value="open">Open publishing</SelectItem>
2355
+ <SelectItem value="review">Require review</SelectItem>
2356
+ <SelectItem value="admins-only">Admins only</SelectItem>
2357
+ </SelectContent>
2358
+ </Select>
2359
+ <Button
2360
+ type="button"
2361
+ size="sm"
2362
+ disabled={
2363
+ !selectedLibraryContext?.access.canAdmin ||
2364
+ !contextSettingsName.trim() ||
2365
+ manageContext.isPending
2366
+ }
2367
+ onClick={() => void saveContextSettings()}
2368
+ >
2369
+ Save settings
2370
+ </Button>
2371
+ {activeAppId ? (
2372
+ <div className="border-t border-border/70 pt-3">
2373
+ <p className="text-xs text-muted-foreground">
2374
+ Automatic generations use Default plus this context for
2375
+ {` ${activeAppId}`} when no context is chosen
2376
+ explicitly.
2377
+ </p>
2378
+ <Button
2379
+ type="button"
2380
+ size="sm"
2381
+ variant="outline"
2382
+ className="mt-3"
2383
+ disabled={
2384
+ !canSetAppDefault ||
2385
+ appDefaultContextId === selectedLibraryContext?.id ||
2386
+ manageContext.isPending
2387
+ }
2388
+ onClick={() => void setAppDefaultContext()}
2389
+ >
2390
+ {appDefaultContextId === selectedLibraryContext?.id
2391
+ ? `Automatic for ${activeAppId}`
2392
+ : `Use automatically for ${activeAppId}`}
2393
+ </Button>
2394
+ </div>
2395
+ ) : null}
2396
+ </section>
2397
+ <section className="space-y-3 rounded-md border border-dashed border-border p-4">
2398
+ <div>
2399
+ <h3 className="text-sm font-semibold">New specialty</h3>
2400
+ <p className="mt-1 text-xs text-muted-foreground">
2401
+ Create a focused style such as Marketing, Product, or
2402
+ Sales. Contexts start open unless you choose review.
2403
+ </p>
2404
+ </div>
2405
+ <Input
2406
+ value={newContextName}
2407
+ disabled={!canCreateContext}
2408
+ onChange={(event) => setNewContextName(event.target.value)}
2409
+ placeholder="Marketing"
2410
+ />
2411
+ <Select
2412
+ value={newContextPolicy}
2413
+ disabled={!canCreateContext}
2414
+ onValueChange={(value) =>
2415
+ setNewContextPolicy(
2416
+ value as "open" | "review" | "admins-only",
2417
+ )
2418
+ }
2419
+ >
2420
+ <SelectTrigger>
2421
+ <SelectValue />
2422
+ </SelectTrigger>
2423
+ <SelectContent>
2424
+ <SelectItem value="open">Open publishing</SelectItem>
2425
+ <SelectItem value="review">Require review</SelectItem>
2426
+ <SelectItem value="admins-only">Admins only</SelectItem>
2427
+ </SelectContent>
2428
+ </Select>
2429
+ <Button
2430
+ type="button"
2431
+ size="sm"
2432
+ variant="outline"
2433
+ disabled={
2434
+ !canCreateContext ||
2435
+ !newContextName.trim() ||
2436
+ manageContext.isPending
2437
+ }
2438
+ onClick={() => void createSpecialtyContext()}
2439
+ >
2440
+ <IconPlus /> Create context
2441
+ </Button>
2442
+ </section>
2443
+ </div>
2444
+ {contextSettingsError ? (
2445
+ <p className="mt-3 text-xs text-destructive">
2446
+ {contextSettingsError}
2447
+ </p>
2448
+ ) : null}
2449
+ </TabsContent>
2450
+ </Tabs>
2451
+ {libraryView === "settings" ? (
2452
+ <section className="space-y-3">
2453
+ <div>
2454
+ <h2 className="text-sm font-semibold">
2455
+ {t("creativeContext.modeLabel")}
2456
+ </h2>
2457
+ <p className="mt-1 text-xs text-muted-foreground">
2458
+ {activePack
2459
+ ? `${t("creativeContext.activePack")}: ${activePack.name}`
2460
+ : t("creativeContext.noActivePack")}
2461
+ </p>
2462
+ </div>
2463
+ <div
2464
+ className="flex flex-col gap-2 sm:flex-row"
2465
+ role="radiogroup"
2466
+ >
2467
+ <ContextModeButton
2468
+ mode="auto"
2469
+ activeMode={contextState.state.contextMode}
2470
+ label={t("creativeContext.automatic")}
2471
+ description={t("creativeContext.automaticDescription")}
2472
+ disabled={savingState}
2473
+ onSelect={(mode) => void changeMode(mode)}
2474
+ />
2475
+ <ContextModeButton
2476
+ mode="off"
2477
+ activeMode={contextState.state.contextMode}
2478
+ label={t("creativeContext.off")}
2479
+ description={t("creativeContext.offDescription")}
2480
+ disabled={savingState}
2481
+ onSelect={(mode) => void changeMode(mode)}
2482
+ />
2483
+ </div>
2484
+ {stateError ? (
2485
+ <p className="text-xs text-destructive">{stateError}</p>
2486
+ ) : null}
2487
+ </section>
2488
+ ) : null}
2489
+
2490
+ {libraryView === "settings" &&
2491
+ brandProfileQuery.data?.profile &&
2492
+ brandProfileQuery.data.dna ? (
2493
+ <section className="border-t border-border/70 pt-6">
2494
+ <div className="rounded-md border border-border p-4">
2495
+ <div className="flex flex-wrap items-center justify-between gap-2">
2496
+ <div>
2497
+ <h2 className="text-sm font-semibold">
2498
+ {t("creativeContext.publishedBrandContext")}
2499
+ </h2>
2500
+ <p className="mt-0.5 text-xs text-muted-foreground">
2501
+ {brandProfileQuery.data.profile.name}
2502
+ </p>
2503
+ </div>
2504
+ <Badge variant="secondary">
2505
+ v{brandProfileQuery.data.dna.versionNumber}
2506
+ </Badge>
2507
+ </div>
2508
+ <p className="mt-3 text-sm text-muted-foreground">
2509
+ {brandProfileQuery.data.dna.payload.summary}
2510
+ </p>
2511
+ </div>
2512
+ </section>
2513
+ ) : null}
2514
+
2515
+ {libraryView === "approvals" &&
2516
+ (logoCandidates.length || proposedLayouts.length) ? (
2517
+ <section className="border-t border-border/70 pt-6">
2518
+ <div className="flex items-center gap-2">
2519
+ <IconSparkles className="size-5 text-muted-foreground" />
2520
+ <h2 className="text-lg font-semibold">
2521
+ {t("creativeContext.suggestions")}
2522
+ </h2>
2523
+ </div>
2524
+ {logoCandidates.length ? (
2525
+ <div className="mt-4">
2526
+ <h3 className="flex items-center gap-2 text-sm font-semibold">
2527
+ <IconPhoto className="size-4 text-muted-foreground" />
2528
+ {t("creativeContext.logo")}
2529
+ </h3>
2530
+ <div className="mt-2 grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
2531
+ {logoCandidates.map((candidate) => (
2532
+ <article
2533
+ key={candidate.mediaId}
2534
+ className="overflow-hidden rounded-md border border-border"
2535
+ >
2536
+ <div className="flex h-28 items-center justify-center bg-muted/40 p-3">
2537
+ <img
2538
+ src={candidate.thumbnailUrl}
2539
+ alt={candidate.title}
2540
+ className="max-h-full max-w-full object-contain"
2541
+ />
2542
+ </div>
2543
+ <div className="flex items-center justify-between gap-2 p-3">
2544
+ <p className="min-w-0 truncate text-sm font-medium">
2545
+ {candidate.title}
2546
+ </p>
2547
+ <Button
2548
+ type="button"
2549
+ size="sm"
2550
+ variant="outline"
2551
+ disabled={
2552
+ proposeCanonicalLogo.isPending ||
2553
+ confirmCanonicalLogo.isPending
2554
+ }
2555
+ onClick={() => void chooseCanonicalLogo(candidate)}
2556
+ >
2557
+ <IconCheck />
2558
+ {t("creativeContext.approve")}
2559
+ </Button>
2560
+ </div>
2561
+ </article>
2562
+ ))}
2563
+ </div>
2564
+ </div>
2565
+ ) : null}
2566
+ {proposedLayouts.length &&
2567
+ proposalCapabilities?.layoutTemplate ? (
2568
+ <div className="mt-5">
2569
+ <h3 className="flex items-center gap-2 text-sm font-semibold">
2570
+ <IconLayout className="size-4 text-muted-foreground" />
2571
+ {t("creativeContext.layouts")}
2572
+ </h3>
2573
+ <div className="mt-2 divide-y divide-border/70 rounded-md border border-border">
2574
+ {proposedLayouts.map((suggestion) => (
2575
+ <article
2576
+ key={suggestion.id}
2577
+ className="flex flex-wrap items-center justify-between gap-3 p-3"
2578
+ >
2579
+ <div className="min-w-0">
2580
+ <p className="truncate text-sm font-medium">
2581
+ {suggestion.reason ?? suggestion.itemId}
2582
+ </p>
2583
+ <p className="mt-0.5 text-xs text-muted-foreground">
2584
+ {suggestion.status}
2585
+ </p>
2586
+ </div>
2587
+ <div className="flex items-center gap-2">
2588
+ {suggestion.status === "proposed" ? (
2589
+ <>
2590
+ <Button
2591
+ type="button"
2592
+ size="sm"
2593
+ disabled={manageLayoutTemplate.isPending}
2594
+ onClick={() =>
2595
+ void decideLayoutSuggestion(
2596
+ suggestion.id,
2597
+ "promote",
2598
+ )
2599
+ }
2600
+ >
2601
+ <IconCheck />
2602
+ {t("creativeContext.approve")}
2603
+ </Button>
2604
+ <Button
2605
+ type="button"
2606
+ size="sm"
2607
+ variant="outline"
2608
+ disabled={manageLayoutTemplate.isPending}
2609
+ onClick={() =>
2610
+ void decideLayoutSuggestion(
2611
+ suggestion.id,
2612
+ "reject",
2613
+ )
2614
+ }
2615
+ >
2616
+ {t("creativeContext.exclude")}
2617
+ </Button>
2618
+ </>
2619
+ ) : (
2620
+ <Button
2621
+ type="button"
2622
+ size="sm"
2623
+ variant="outline"
2624
+ disabled={manageLayoutTemplate.isPending}
2625
+ onClick={() =>
2626
+ void decideLayoutSuggestion(
2627
+ suggestion.id,
2628
+ "demote",
2629
+ )
2630
+ }
2631
+ >
2632
+ {t("creativeContext.deprecate")}
2633
+ </Button>
2634
+ )}
2635
+ </div>
2636
+ </article>
2637
+ ))}
2638
+ </div>
2639
+ </div>
2640
+ ) : null}
2641
+ {suggestionError ? (
2642
+ <p className="mt-3 text-sm text-destructive">
2643
+ {suggestionError}
2644
+ </p>
2645
+ ) : null}
2646
+ </section>
2647
+ ) : null}
2648
+
2649
+ {libraryView === "sources" ? (
2650
+ <>
2651
+ <section className="border-t border-border/70 pt-6">
2652
+ <div>
2653
+ <h2 className="text-lg font-semibold">
2654
+ {t("creativeContext.addSource")}
2655
+ </h2>
2656
+ <p className="mt-1 text-sm text-muted-foreground">
2657
+ {t("creativeContext.sourcesDescription")}
2658
+ </p>
2659
+ </div>
2660
+ <div className="mt-4 grid gap-2 sm:grid-cols-2 lg:grid-cols-5">
2661
+ {CONNECTORS.map((connector) => {
2662
+ const Icon = connector.icon;
2663
+ return (
2664
+ <button
2665
+ key={connector.kind}
2666
+ type="button"
2667
+ disabled={!canManageScope}
2668
+ onClick={() => {
2669
+ setConnectorKind(connector.kind);
2670
+ setSourceName(connector.label);
2671
+ setSourceReference("");
2672
+ setUploadedFiles([]);
2673
+ setPickerRecommendations([]);
2674
+ seenRecommendationIdsRef.current.clear();
2675
+ setSelectedConnectionId("");
2676
+ setSelectedRecommendationIds(new Set());
2677
+ setSetupError(null);
2678
+ }}
2679
+ className="flex min-h-24 flex-col items-start justify-between rounded-md border border-border p-3 text-start transition-colors hover:bg-accent/50 disabled:cursor-not-allowed disabled:opacity-50"
2680
+ >
2681
+ <Icon className="size-5 text-muted-foreground" />
2682
+ <span className="text-sm font-medium">
2683
+ {connector.label}
2684
+ </span>
2685
+ </button>
2686
+ );
2687
+ })}
2688
+ </div>
2689
+ {selectedConnector ? (
2690
+ <form
2691
+ className="mt-4 rounded-md border border-border p-4"
2692
+ onSubmit={(event) => void previewImport(event)}
2693
+ >
2694
+ {connectionProvider ? (
2695
+ <div className="mb-3">
2696
+ {connectionsQuery.isLoading ? (
2697
+ <Skeleton className="h-9 w-full" />
2698
+ ) : connectionsQuery.data?.needsSetup ? (
2699
+ <div className="flex flex-wrap items-center justify-between gap-3 rounded-md border border-dashed border-border p-3 text-sm text-muted-foreground">
2700
+ <span>{t("creativeContext.setupConnection")}</span>
2701
+ <Button
2702
+ asChild
2703
+ type="button"
2704
+ variant="outline"
2705
+ size="sm"
2706
+ >
2707
+ <a
2708
+ href={
2709
+ connectionsQuery.data.connectPath ||
2710
+ connectionsQuery.data.connectionsPath ||
2711
+ connectionsHref
2712
+ }
2713
+ >
2714
+ {t("creativeContext.connectProvider")}
2715
+ <IconArrowUpRight />
2716
+ </a>
2717
+ </Button>
2718
+ </div>
2719
+ ) : connectionsQuery.data?.needsPicker ? (
2720
+ <label className="block space-y-1.5 text-xs font-medium">
2721
+ <span>{t("creativeContext.chooseConnection")}</span>
2722
+ <Select
2723
+ value={selectedConnectionId}
2724
+ onValueChange={setSelectedConnectionId}
2725
+ >
2726
+ <SelectTrigger>
2727
+ <SelectValue
2728
+ placeholder={t(
2729
+ "creativeContext.chooseConnection",
2730
+ )}
2731
+ />
2732
+ </SelectTrigger>
2733
+ <SelectContent>
2734
+ {connectionsQuery.data.connections.map(
2735
+ (connection) => (
2736
+ <SelectItem
2737
+ key={connection.connectionId}
2738
+ value={connection.connectionId}
2739
+ >
2740
+ {connection.label}
2741
+ </SelectItem>
2742
+ ),
2743
+ )}
2744
+ </SelectContent>
2745
+ </Select>
2746
+ </label>
2747
+ ) : null}
2748
+ </div>
2749
+ ) : null}
2750
+ {recommendationProvider && selectedConnectionId ? (
2751
+ <div className="mb-3 rounded-md border border-border p-3">
2752
+ {connectorKind === "google-slides" ? (
2753
+ <div className="mb-3 flex flex-wrap items-center justify-between gap-3 border-b border-border/60 pb-3">
2754
+ <p className="max-w-lg text-xs text-muted-foreground">
2755
+ {t("creativeContext.googlePickerDescription")}
2756
+ </p>
2757
+ <Button
2758
+ type="button"
2759
+ variant="outline"
2760
+ size="sm"
2761
+ disabled={openingGooglePicker}
2762
+ onClick={() => void chooseGoogleSlides()}
2763
+ >
2764
+ <IconSlideshow />
2765
+ {openingGooglePicker
2766
+ ? t("creativeContext.loading")
2767
+ : t("creativeContext.choosePresentations")}
2768
+ </Button>
2769
+ </div>
2770
+ ) : null}
2771
+ {recommendationsQuery.isLoading &&
2772
+ !availableRecommendations.length ? (
2773
+ <Skeleton className="h-16 w-full" />
2774
+ ) : availableRecommendations.length ? (
2775
+ <div className="space-y-2">
2776
+ <div className="flex items-center justify-between gap-3 text-xs text-muted-foreground">
2777
+ <span>
2778
+ {t("creativeContext.discoveredItems", {
2779
+ count: formatNumber(
2780
+ availableRecommendations.length,
2781
+ ),
2782
+ })}
2783
+ </span>
2784
+ <span>
2785
+ {t("creativeContext.selectedItems", {
2786
+ count: formatNumber(
2787
+ selectedRecommendationIds.size,
2788
+ ),
2789
+ })}
2790
+ </span>
2791
+ </div>
2792
+ <div className="max-h-52 divide-y divide-border/60 overflow-y-auto">
2793
+ {availableRecommendations.map(
2794
+ (recommendation) => (
2795
+ <label
2796
+ key={recommendation.externalId}
2797
+ className="flex cursor-pointer items-start gap-3 py-2"
2798
+ >
2799
+ <Checkbox
2800
+ className="mt-0.5"
2801
+ checked={selectedRecommendationIds.has(
2802
+ recommendation.externalId,
2803
+ )}
2804
+ onCheckedChange={(checked) =>
2805
+ setSelectedRecommendationIds(
2806
+ (current) => {
2807
+ const next = new Set(current);
2808
+ if (checked) {
2809
+ next.add(
2810
+ recommendation.externalId,
2811
+ );
2812
+ } else {
2813
+ next.delete(
2814
+ recommendation.externalId,
2815
+ );
2816
+ }
2817
+ return next;
2818
+ },
2819
+ )
2820
+ }
2821
+ />
2822
+ <span className="min-w-0">
2823
+ <span className="block truncate text-sm">
2824
+ {recommendation.title}
2825
+ </span>
2826
+ <span className="block text-xs text-muted-foreground">
2827
+ {recommendation.containerRef ??
2828
+ recommendation.kind}
2829
+ </span>
2830
+ </span>
2831
+ </label>
2832
+ ),
2833
+ )}
2834
+ </div>
2835
+ </div>
2836
+ ) : (
2837
+ <p className="text-xs text-muted-foreground">
2838
+ {recommendationsQuery.data?.unavailableReason ??
2839
+ t("creativeContext.unavailable")}
2840
+ </p>
2841
+ )}
2842
+ </div>
2843
+ ) : null}
2844
+ <div className="grid gap-3 sm:grid-cols-2">
2845
+ <label className="space-y-1.5 text-xs font-medium">
2846
+ <span>{t("creativeContext.sourceName")}</span>
2847
+ <Input
2848
+ value={sourceName}
2849
+ onChange={(event) =>
2850
+ setSourceName(event.target.value)
2851
+ }
2852
+ required
2853
+ />
2854
+ </label>
2855
+ {selectedConnector.kind === "upload" ? (
2856
+ <div className="space-y-1.5 text-xs font-medium">
2857
+ <span>{t("creativeContext.sourceReference")}</span>
2858
+ <input
2859
+ ref={fileInputRef}
2860
+ type="file"
2861
+ multiple
2862
+ accept=".pptx,.docx,.pdf,.png,.jpg,.jpeg,.webp,.gif,.svg,application/pdf,application/vnd.openxmlformats-officedocument.presentationml.presentation,application/vnd.openxmlformats-officedocument.wordprocessingml.document,image/png,image/jpeg,image/webp,image/gif,image/svg+xml"
2863
+ className="hidden"
2864
+ onChange={chooseFiles}
2865
+ />
2866
+ <input
2867
+ ref={folderInputRef}
2868
+ type="file"
2869
+ multiple
2870
+ className="hidden"
2871
+ onChange={chooseFiles}
2872
+ {...({ webkitdirectory: "", directory: "" } as {
2873
+ webkitdirectory: string;
2874
+ directory: string;
2875
+ })}
2876
+ />
2877
+ <div
2878
+ onDragOver={(event) => event.preventDefault()}
2879
+ onDrop={dropFiles}
2880
+ className="rounded-md border border-dashed border-border p-4"
2881
+ >
2882
+ <p className="text-xs text-muted-foreground">
2883
+ {t("creativeContext.dropFiles")}
2884
+ </p>
2885
+ <div className="mt-3 flex flex-wrap gap-2">
2886
+ <Button
2887
+ type="button"
2888
+ variant="outline"
2889
+ size="sm"
2890
+ onClick={() => fileInputRef.current?.click()}
2891
+ >
2892
+ {t("creativeContext.chooseFiles")}
2893
+ </Button>
2894
+ <Button
2895
+ type="button"
2896
+ variant="outline"
2897
+ size="sm"
2898
+ onClick={() => folderInputRef.current?.click()}
2899
+ >
2900
+ {t("creativeContext.chooseFolder")}
2901
+ </Button>
2902
+ </div>
2903
+ {uploadedFiles.length ? (
2904
+ <ul className="mt-3 space-y-1 text-xs text-muted-foreground">
2905
+ {uploadedFiles.map((file) => (
2906
+ <li key={file.id} className="truncate">
2907
+ {file.fileName}
2908
+ </li>
2909
+ ))}
2910
+ </ul>
2911
+ ) : null}
2912
+ </div>
2913
+ </div>
2914
+ ) : (
2915
+ <label className="space-y-1.5 text-xs font-medium">
2916
+ <span>{t("creativeContext.sourceReference")}</span>
2917
+ {selectedConnector.kind === "website" ||
2918
+ selectedConnector.kind === "figma" ||
2919
+ selectedConnector.kind === "notion" ? (
2920
+ <Textarea
2921
+ value={sourceReference}
2922
+ onChange={(event) =>
2923
+ setSourceReference(event.target.value)
2924
+ }
2925
+ placeholder={
2926
+ selectedConnector.referencePlaceholder
2927
+ }
2928
+ required={selectedConnector.referenceRequired}
2929
+ rows={3}
2930
+ />
2931
+ ) : (
2932
+ <Input
2933
+ value={sourceReference}
2934
+ onChange={(event) =>
2935
+ setSourceReference(event.target.value)
2936
+ }
2937
+ placeholder={
2938
+ selectedConnector.referencePlaceholder
2939
+ }
2940
+ required={selectedConnector.referenceRequired}
2941
+ />
2942
+ )}
2943
+ </label>
2944
+ )}
2945
+ </div>
2946
+ <div className="mt-3 flex justify-end gap-2">
2947
+ <Button
2948
+ type="button"
2949
+ variant="ghost"
2950
+ onClick={() => setConnectorKind(null)}
2951
+ >
2952
+ {t("creativeContext.cancel")}
2953
+ </Button>
2954
+ <Button
2955
+ type="submit"
2956
+ disabled={
2957
+ manageSource.isPending ||
2958
+ uploadResource.isPending ||
2959
+ (selectedConnector.kind === "upload" &&
2960
+ !uploadedFiles.length) ||
2961
+ (Boolean(connectionProvider) &&
2962
+ !selectedConnectionId) ||
2963
+ (selectedConnector.referenceRequired &&
2964
+ !sourceReference.trim()) ||
2965
+ (Boolean(recommendationProvider) &&
2966
+ !sourceReference.trim() &&
2967
+ !selectedRecommendationIds.size)
2968
+ }
2969
+ >
2970
+ <IconFileImport />
2971
+ {t("creativeContext.preview")}
2972
+ </Button>
2973
+ </div>
2974
+ </form>
2975
+ ) : null}
2976
+
2977
+ {previewSourceId ? (
2978
+ <div className="mt-4 rounded-md border border-border p-4">
2979
+ <div className="flex flex-wrap items-start justify-between gap-3">
2980
+ <div>
2981
+ <h3 className="text-sm font-semibold">
2982
+ {previewSourceName}
2983
+ </h3>
2984
+ {previewQuery.isLoading ? (
2985
+ <p className="mt-0.5 text-xs text-muted-foreground">
2986
+ {t("creativeContext.loading")}
2987
+ </p>
2988
+ ) : (
2989
+ <div className="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-xs text-muted-foreground">
2990
+ <span>
2991
+ {t("creativeContext.discoveredItems", {
2992
+ count: formatNumber(
2993
+ previewQuery.data?.total ??
2994
+ previewQuery.data?.items.length ??
2995
+ 0,
2996
+ ),
2997
+ })}
2998
+ </span>
2999
+ <span>
3000
+ {t("creativeContext.selectedItems", {
3001
+ count: formatNumber(
3002
+ selectedPreviewItemIds.size,
3003
+ ),
3004
+ })}
3005
+ </span>
3006
+ </div>
3007
+ )}
3008
+ </div>
3009
+ <Button
3010
+ type="button"
3011
+ disabled={
3012
+ previewQuery.isLoading ||
3013
+ !previewQuery.data?.items.length ||
3014
+ !selectedPreviewItemIds.size ||
3015
+ startImport.isPending ||
3016
+ importJob?.status === "queued" ||
3017
+ importJob?.status === "running"
3018
+ }
3019
+ onClick={() => void beginImport()}
3020
+ >
3021
+ <IconFileImport />
3022
+ {startImport.isPending
3023
+ ? t("creativeContext.importing")
3024
+ : t("creativeContext.startImport")}
3025
+ </Button>
3026
+ </div>
3027
+ {previewQuery.data?.items.length ? (
3028
+ <div className="mt-3">
3029
+ <div className="flex items-center gap-2 border-y border-border/60 py-2">
3030
+ <Button
3031
+ type="button"
3032
+ variant="ghost"
3033
+ size="sm"
3034
+ onClick={() =>
3035
+ setSelectedPreviewItemIds(
3036
+ new Set(
3037
+ previewQuery.data?.items.map(
3038
+ (item) => item.externalId,
3039
+ ) ?? [],
3040
+ ),
3041
+ )
3042
+ }
3043
+ >
3044
+ {t("creativeContext.selectAll")}
3045
+ </Button>
3046
+ <Button
3047
+ type="button"
3048
+ variant="ghost"
3049
+ size="sm"
3050
+ onClick={() => setSelectedPreviewItemIds(new Set())}
3051
+ >
3052
+ {t("creativeContext.clearSelection")}
3053
+ </Button>
3054
+ </div>
3055
+ <div className="max-h-80 divide-y divide-border/60 overflow-y-auto">
3056
+ {previewQuery.data.items.map((item) => (
3057
+ <label
3058
+ key={item.externalId}
3059
+ className="flex cursor-pointer items-start gap-3 py-2"
3060
+ >
3061
+ <Checkbox
3062
+ className="mt-0.5"
3063
+ checked={selectedPreviewItemIds.has(
3064
+ item.externalId,
3065
+ )}
3066
+ onCheckedChange={(checked) =>
3067
+ setSelectedPreviewItemIds((current) => {
3068
+ const next = new Set(current);
3069
+ if (checked) next.add(item.externalId);
3070
+ else next.delete(item.externalId);
3071
+ return next;
3072
+ })
3073
+ }
3074
+ />
3075
+ <span className="min-w-0">
3076
+ <span className="block truncate text-sm">
3077
+ {item.title}
3078
+ </span>
3079
+ <span className="block text-xs text-muted-foreground">
3080
+ {item.kind}
3081
+ </span>
3082
+ </span>
3083
+ </label>
3084
+ ))}
3085
+ </div>
3086
+ </div>
3087
+ ) : null}
3088
+ </div>
3089
+ ) : null}
3090
+
3091
+ {importJob ? (
3092
+ <div className="mt-4 rounded-md border border-border p-4">
3093
+ <div className="flex items-center gap-2">
3094
+ {importJob.status === "completed" ? (
3095
+ <IconCheck className="size-5 text-emerald-600" />
3096
+ ) : importJob.status === "failed" ? (
3097
+ <IconAlertTriangle className="size-5 text-destructive" />
3098
+ ) : (
3099
+ <IconRefresh className="size-5 animate-spin text-muted-foreground" />
3100
+ )}
3101
+ <h3 className="text-sm font-semibold">
3102
+ {importJob.status === "completed"
3103
+ ? t("creativeContext.importComplete")
3104
+ : importJob.status === "failed"
3105
+ ? t("creativeContext.importFailed")
3106
+ : t("creativeContext.importing")}
3107
+ </h3>
3108
+ </div>
3109
+ {importJob.status === "completed" && brandProposal ? (
3110
+ <div className="mt-4 rounded-md bg-muted/50 p-4">
3111
+ <div className="flex items-start gap-3">
3112
+ <IconSparkles className="mt-0.5 size-5 text-muted-foreground" />
3113
+ <div className="min-w-0 flex-1">
3114
+ <h4 className="text-sm font-semibold">
3115
+ {t("creativeContext.brandDnaTitle")}
3116
+ </h4>
3117
+ <p className="mt-1 text-sm text-muted-foreground">
3118
+ {brandProposal.summary}
3119
+ </p>
3120
+ {brandProposal.colors.length ? (
3121
+ <div className="mt-4">
3122
+ <p className="text-xs font-medium">
3123
+ {t("creativeContext.colors")}
3124
+ </p>
3125
+ <div className="mt-2 flex flex-wrap gap-2">
3126
+ {brandProposal.colors.map((color) => (
3127
+ <div
3128
+ key={color}
3129
+ className="flex items-center gap-1.5 rounded border border-border bg-background px-2 py-1 text-xs"
3130
+ >
3131
+ <span
3132
+ className="size-4 rounded-sm border border-black/10"
3133
+ style={{ backgroundColor: color }}
3134
+ />
3135
+ {color}
3136
+ </div>
3137
+ ))}
3138
+ </div>
3139
+ </div>
3140
+ ) : null}
3141
+ {brandProposal.fonts.length ? (
3142
+ <div className="mt-4">
3143
+ <p className="text-xs font-medium">
3144
+ {t("creativeContext.fonts")}
3145
+ </p>
3146
+ <div className="mt-2 flex flex-wrap gap-2">
3147
+ {brandProposal.fonts.map((font) => (
3148
+ <Badge key={font} variant="outline">
3149
+ {font}
3150
+ </Badge>
3151
+ ))}
3152
+ </div>
3153
+ </div>
3154
+ ) : null}
3155
+ {brandLayoutThumbnails.length ? (
3156
+ <div className="mt-4">
3157
+ <p className="text-xs font-medium">
3158
+ {t("creativeContext.layouts")}
3159
+ </p>
3160
+ <div className="mt-2 grid grid-cols-3 gap-2">
3161
+ {brandLayoutThumbnails.map((thumbnail) => (
3162
+ <AccessScopedThumbnail
3163
+ key={thumbnail.itemVersionId}
3164
+ itemId={thumbnail.itemId}
3165
+ itemVersionId={thumbnail.itemVersionId}
3166
+ className="aspect-video w-full rounded border border-border object-cover"
3167
+ />
3168
+ ))}
3169
+ </div>
3170
+ </div>
3171
+ ) : null}
3172
+ {brandVoicePreview ? (
3173
+ <div className="mt-4">
3174
+ <p className="text-xs font-medium">
3175
+ {t("creativeContext.voice")}
3176
+ </p>
3177
+ <blockquote className="mt-1 border-s-2 border-border ps-3 text-sm text-muted-foreground">
3178
+ {brandVoicePreview}
3179
+ </blockquote>
3180
+ </div>
3181
+ ) : null}
3182
+ <div className="mt-4 flex flex-wrap gap-2">
3183
+ <Button
3184
+ type="button"
3185
+ disabled={publishBrandDna.isPending}
3186
+ onClick={() => void publishProposal()}
3187
+ >
3188
+ <IconSparkles />
3189
+ {publishBrandDna.isPending
3190
+ ? t("creativeContext.applyingBrandContext")
3191
+ : t("creativeContext.applyBrandContext")}
3192
+ </Button>
3193
+ <Button asChild type="button" variant="outline">
3194
+ <a href="/settings/agent">
3195
+ {t("creativeContext.generateWithContext")}
3196
+ <IconArrowUpRight />
3197
+ </a>
3198
+ </Button>
3199
+ </div>
3200
+ {publishedMessage ? (
3201
+ <p className="mt-2 text-xs text-muted-foreground">
3202
+ {publishedMessage}
3203
+ </p>
3204
+ ) : null}
3205
+ </div>
3206
+ </div>
3207
+ </div>
3208
+ ) : importJob.error ? (
3209
+ <p className="mt-2 text-sm text-destructive">
3210
+ {importJob.error}
3211
+ </p>
3212
+ ) : (
3213
+ <p className="mt-2 text-xs text-muted-foreground">
3214
+ {formatNumber(importJob.progressCurrent)} /{" "}
3215
+ {formatNumber(importJob.progressTotal ?? 0)}
3216
+ </p>
3217
+ )}
3218
+ </div>
3219
+ ) : null}
3220
+ {promotionPreview ? (
3221
+ <div className="mt-4 rounded-md border border-border p-4">
3222
+ <h3 className="text-sm font-semibold">
3223
+ {t("creativeContext.promoteToOrganization")}
3224
+ </h3>
3225
+ <p className="mt-1 text-sm text-muted-foreground">
3226
+ {t("creativeContext.promotionDescription")}
3227
+ </p>
3228
+ <dl className="mt-3 grid gap-2 text-xs sm:grid-cols-3">
3229
+ <div>
3230
+ <dt className="text-muted-foreground">
3231
+ {t("creativeContext.sourceReference")}
3232
+ </dt>
3233
+ <dd className="mt-0.5 truncate font-medium">
3234
+ {promotionPreview.containerRef}
3235
+ </dd>
3236
+ </div>
3237
+ <div>
3238
+ <dt className="text-muted-foreground">
3239
+ {t("creativeContext.itemsLabel", { count: "" })}
3240
+ </dt>
3241
+ <dd className="mt-0.5 font-medium">
3242
+ {formatNumber(promotionPreview.itemCount)}
3243
+ </dd>
3244
+ </div>
3245
+ <div>
3246
+ <dt className="text-muted-foreground">
3247
+ {t("creativeContext.restrictedItems", { count: "" })}
3248
+ </dt>
3249
+ <dd className="mt-0.5 font-medium">
3250
+ {formatNumber(promotionPreview.restrictedItemCount)}
3251
+ </dd>
3252
+ </div>
3253
+ </dl>
3254
+ <Button
3255
+ type="button"
3256
+ className="mt-4"
3257
+ disabled={manageSource.isPending}
3258
+ onClick={() => void confirmPromotion()}
3259
+ >
3260
+ {t("creativeContext.promoteToOrganization")}
3261
+ </Button>
3262
+ </div>
3263
+ ) : null}
3264
+ {promotionMessage ? (
3265
+ <p className="mt-2 text-xs text-muted-foreground">
3266
+ {promotionMessage}
3267
+ </p>
3268
+ ) : null}
3269
+ {setupError ? (
3270
+ <p className="mt-2 text-sm text-destructive">{setupError}</p>
3271
+ ) : null}
3272
+ </section>
3273
+
3274
+ <section className="border-t border-border/70 pt-6">
3275
+ <h2 className="text-lg font-semibold">
3276
+ {t("creativeContext.sourcesTitle")}
3277
+ </h2>
3278
+ {sources.length ? (
3279
+ <div className="mt-3">
3280
+ {sources.map((source) => (
3281
+ <SourceRow
3282
+ key={source.id}
3283
+ source={source}
3284
+ refreshing={
3285
+ refreshSource.isPending &&
3286
+ refreshSource.variables?.sourceId === source.id
3287
+ }
3288
+ canReview={canManageScope}
3289
+ canPromote={
3290
+ libraryScope === "user" &&
3291
+ Boolean(org?.orgId) &&
3292
+ canManageOrg &&
3293
+ source.visibility === "private"
3294
+ }
3295
+ onRefresh={refresh}
3296
+ onReview={(selected) =>
3297
+ void openItemCuration(selected, "restricted")
3298
+ }
3299
+ onCurate={(selected) => void openItemCuration(selected)}
3300
+ onPromote={(selected) =>
3301
+ void previewSourcePromotion(selected)
3302
+ }
3303
+ onPause={(selected) => void pauseSource(selected)}
3304
+ onRestore={(selected) => void restoreSource(selected)}
3305
+ onDelete={setDeleteSource}
3306
+ />
3307
+ ))}
3308
+ </div>
3309
+ ) : (
3310
+ <div className="mt-4 rounded-md border border-dashed border-border p-6 text-center">
3311
+ <IconBooks className="mx-auto size-7 text-muted-foreground" />
3312
+ <h3 className="mt-3 text-sm font-semibold">
3313
+ {t("creativeContext.noSourcesTitle")}
3314
+ </h3>
3315
+ <p className="mx-auto mt-1 max-w-md text-sm text-muted-foreground">
3316
+ {t("creativeContext.noSourcesDescription")}
3317
+ </p>
3318
+ <Button
3319
+ asChild
3320
+ variant="outline"
3321
+ size="sm"
3322
+ className="mt-4"
3323
+ >
3324
+ <a href={connectionsHref}>
3325
+ {t("creativeContext.connectSources")}
3326
+ <IconArrowUpRight />
3327
+ </a>
3328
+ </Button>
3329
+ </div>
3330
+ )}
3331
+ {reviewSource ? (
3332
+ <ItemCuration
3333
+ source={reviewSource}
3334
+ items={reviewedItems}
3335
+ busy={reviewItems.isPending}
3336
+ onReview={(operation, itemId) =>
3337
+ void reviewContextItem(operation, itemId)
3338
+ }
3339
+ onClose={() => {
3340
+ setReviewSource(null);
3341
+ setReviewedItems([]);
3342
+ }}
3343
+ />
3344
+ ) : null}
3345
+ {reviewError ? (
3346
+ <p className="mt-2 text-sm text-destructive">{reviewError}</p>
3347
+ ) : null}
3348
+ {refreshMessage ? (
3349
+ <p className="mt-2 text-xs text-muted-foreground">
3350
+ {refreshMessage}
3351
+ </p>
3352
+ ) : null}
3353
+ {lifecycleMessage ? (
3354
+ <p className="mt-2 text-xs text-muted-foreground">
3355
+ {lifecycleMessage}
3356
+ </p>
3357
+ ) : null}
3358
+ </section>
3359
+ </>
3360
+ ) : null}
3361
+
3362
+ {libraryView === "settings" ? (
3363
+ <section className="border-t border-border/70 pt-6">
3364
+ <h2 className="text-lg font-semibold">
3365
+ {t("creativeContext.packsTitle")}
3366
+ </h2>
3367
+ {packs.length ? (
3368
+ <div className="mt-3">
3369
+ {packs.map((pack) => (
3370
+ <PackRow
3371
+ key={pack.id}
3372
+ pack={pack}
3373
+ pinned={contextState.state.pinnedPackId === pack.id}
3374
+ disabled={savingState}
3375
+ onPin={(packId) => void changePinnedPack(packId)}
3376
+ onDetails={setSelectedPackId}
3377
+ />
3378
+ ))}
3379
+ </div>
3380
+ ) : (
3381
+ <p className="mt-3 rounded-md border border-dashed border-border p-5 text-sm text-muted-foreground">
3382
+ {t("creativeContext.noPacks")}
3383
+ </p>
3384
+ )}
3385
+ {selectedPackId ? (
3386
+ <div className="mt-4 rounded-md border border-border p-4">
3387
+ <div className="flex items-start justify-between gap-3">
3388
+ <div>
3389
+ <h3 className="text-sm font-semibold">
3390
+ {packQuery.data?.pack?.name ??
3391
+ t("creativeContext.packDetails")}
3392
+ </h3>
3393
+ {packQuery.data?.pack?.derivedFromPackId ? (
3394
+ <p className="mt-1 text-xs text-muted-foreground">
3395
+ {t("creativeContext.influence")}:{" "}
3396
+ {packQuery.data.pack.derivedFromPackId}
3397
+ </p>
3398
+ ) : null}
3399
+ </div>
3400
+ <Button
3401
+ type="button"
3402
+ variant="ghost"
3403
+ size="sm"
3404
+ onClick={() => setSelectedPackId(null)}
3405
+ >
3406
+ {t("creativeContext.cancel")}
3407
+ </Button>
3408
+ </div>
3409
+ {packQuery.data?.pack?.members.length ? (
3410
+ <div className="mt-3 divide-y divide-border/60">
3411
+ {packQuery.data.pack.members.map((member) => (
3412
+ <div key={member.id} className="py-2 text-xs">
3413
+ <p className="font-medium">{member.itemId}</p>
3414
+ {member.reason ? (
3415
+ <p className="mt-0.5 text-muted-foreground">
3416
+ {t("creativeContext.influence")}: {member.reason}
3417
+ </p>
3418
+ ) : null}
3419
+ </div>
3420
+ ))}
3421
+ </div>
3422
+ ) : null}
3423
+ </div>
3424
+ ) : null}
3425
+ </section>
3426
+ ) : null}
3427
+
3428
+ {libraryView === "items" ? (
3429
+ <section className="border-t border-border/70 pt-6">
3430
+ <div>
3431
+ <h2 className="text-lg font-semibold">
3432
+ {t("creativeContext.searchTitle")}
3433
+ </h2>
3434
+ <p className="mt-1 text-sm text-muted-foreground">
3435
+ {t("creativeContext.searchDescription")}
3436
+ </p>
3437
+ </div>
3438
+ <form
3439
+ className="mt-4 flex gap-2"
3440
+ onSubmit={(event) => void search(event)}
3441
+ >
3442
+ <div className="relative min-w-0 flex-1">
3443
+ <IconSearch className="pointer-events-none absolute start-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
3444
+ <Input
3445
+ type="search"
3446
+ value={query}
3447
+ onChange={(event) => setQuery(event.target.value)}
3448
+ placeholder={t("creativeContext.searchPlaceholder")}
3449
+ aria-label={t("creativeContext.searchPlaceholder")}
3450
+ className="ps-9"
3451
+ />
3452
+ </div>
3453
+ <Button
3454
+ type="submit"
3455
+ variant="outline"
3456
+ disabled={
3457
+ !query.trim() ||
3458
+ (!sources.length &&
3459
+ !selectedLibraryContextId &&
3460
+ !contextState.state.pinnedPackId) ||
3461
+ searchContext.isPending
3462
+ }
3463
+ >
3464
+ <IconSearch />
3465
+ {t("creativeContext.searchTitle")}
3466
+ </Button>
3467
+ </form>
3468
+ {searchError ? (
3469
+ <p className="mt-4 text-sm text-destructive">{searchError}</p>
3470
+ ) : !searchContext.data && !searchContext.isPending ? (
3471
+ <p className="mt-4 text-sm text-muted-foreground">
3472
+ {t("creativeContext.searchPrompt")}
3473
+ </p>
3474
+ ) : searchContext.isPending ? (
3475
+ <div className="mt-4 space-y-2">
3476
+ <Skeleton className="h-20 w-full" />
3477
+ <Skeleton className="h-20 w-full" />
3478
+ </div>
3479
+ ) : searchContext.data?.results.length ? (
3480
+ <div className="mt-4 space-y-2">
3481
+ <p className="text-xs text-muted-foreground">
3482
+ {t("creativeContext.resultsLabel", {
3483
+ count: formatNumber(searchContext.data.results.length),
3484
+ })}
3485
+ </p>
3486
+ {searchContext.data.results.map((result) => (
3487
+ <article
3488
+ key={`${result.itemVersionId}:${result.chunkId ?? "item"}`}
3489
+ className="rounded-md border border-border/70 p-3"
3490
+ >
3491
+ <div className="flex items-start justify-between gap-3">
3492
+ <div className="min-w-0">
3493
+ <h3 className="truncate text-sm font-medium">
3494
+ {result.title}
3495
+ </h3>
3496
+ <p className="mt-0.5 text-xs text-muted-foreground">
3497
+ {result.sourceName} · {result.kind}
3498
+ </p>
3499
+ </div>
3500
+ {result.canonicalUrl ? (
3501
+ <Button asChild variant="ghost" size="icon">
3502
+ <a
3503
+ href={result.canonicalUrl}
3504
+ target="_blank"
3505
+ rel="noreferrer"
3506
+ aria-label={result.title}
3507
+ >
3508
+ <IconArrowUpRight />
3509
+ </a>
3510
+ </Button>
3511
+ ) : null}
3512
+ </div>
3513
+ <p className="mt-2 line-clamp-3 text-sm leading-relaxed text-muted-foreground">
3514
+ {result.excerpt}
3515
+ </p>
3516
+ </article>
3517
+ ))}
3518
+ </div>
3519
+ ) : (
3520
+ <p className="mt-4 text-sm text-muted-foreground">
3521
+ {t("creativeContext.noResults")}
3522
+ </p>
3523
+ )}
3524
+ </section>
3525
+ ) : null}
3526
+ </>
3527
+ )}
3528
+ <ContextPreviewSheet
3529
+ manifest={previewManifest}
3530
+ onOpenChange={(open) => {
3531
+ if (!open) setPreviewManifest(null);
3532
+ }}
3533
+ />
3534
+ <AlertDialog
3535
+ open={Boolean(membershipUpdateCandidate)}
3536
+ onOpenChange={(open) => {
3537
+ if (!open && !updatingMembershipId)
3538
+ setMembershipUpdateCandidate(null);
3539
+ }}
3540
+ >
3541
+ <AlertDialogContent>
3542
+ <AlertDialogHeader>
3543
+ <AlertDialogTitle>
3544
+ {t("creativeContext.submitUpdateTitle")}
3545
+ </AlertDialogTitle>
3546
+ <AlertDialogDescription>
3547
+ {t("creativeContext.submitUpdateDescription", {
3548
+ name: membershipUpdateCandidate?.title ?? "",
3549
+ })}
3550
+ </AlertDialogDescription>
3551
+ </AlertDialogHeader>
3552
+ <AlertDialogFooter>
3553
+ <AlertDialogCancel disabled={Boolean(updatingMembershipId)}>
3554
+ {t("creativeContext.cancel")}
3555
+ </AlertDialogCancel>
3556
+ <AlertDialogAction
3557
+ disabled={Boolean(updatingMembershipId)}
3558
+ onClick={(event) => {
3559
+ event.preventDefault();
3560
+ void submitLatestContextMembershipUpdate();
3561
+ }}
3562
+ >
3563
+ {updatingMembershipId
3564
+ ? t("creativeContext.submittingUpdate")
3565
+ : t("creativeContext.submitUpdate")}
3566
+ </AlertDialogAction>
3567
+ </AlertDialogFooter>
3568
+ </AlertDialogContent>
3569
+ </AlertDialog>
3570
+ <AlertDialog
3571
+ open={Boolean(deleteSource)}
3572
+ onOpenChange={(open) => {
3573
+ if (!open) setDeleteSource(null);
3574
+ }}
3575
+ >
3576
+ <AlertDialogContent>
3577
+ <AlertDialogHeader>
3578
+ <AlertDialogTitle>
3579
+ {t("creativeContext.deleteSourceTitle")}
3580
+ </AlertDialogTitle>
3581
+ <AlertDialogDescription>
3582
+ {t("creativeContext.deleteSourceDescription", {
3583
+ name: deleteSource?.name ?? "",
3584
+ })}
3585
+ </AlertDialogDescription>
3586
+ </AlertDialogHeader>
3587
+ <AlertDialogFooter>
3588
+ <AlertDialogCancel>{t("creativeContext.cancel")}</AlertDialogCancel>
3589
+ <AlertDialogAction
3590
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
3591
+ onClick={() => void confirmDeleteSource()}
3592
+ >
3593
+ {t("creativeContext.delete")}
3594
+ </AlertDialogAction>
3595
+ </AlertDialogFooter>
3596
+ </AlertDialogContent>
3597
+ </AlertDialog>
3598
+ </div>
3599
+ );
3600
+ }