@agent-native/creative-context 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (618) hide show
  1. package/README.md +108 -0
  2. package/agent-native.package.json +104 -0
  3. package/dist/actions/confirm-canonical-logo.d.ts +6 -0
  4. package/dist/actions/confirm-canonical-logo.d.ts.map +1 -0
  5. package/dist/actions/confirm-canonical-logo.js +19 -0
  6. package/dist/actions/confirm-canonical-logo.js.map +1 -0
  7. package/dist/actions/continue-context-import.d.ts +7 -0
  8. package/dist/actions/continue-context-import.d.ts.map +1 -0
  9. package/dist/actions/continue-context-import.js +10 -0
  10. package/dist/actions/continue-context-import.js.map +1 -0
  11. package/dist/actions/creative-context-a2a.d.ts +10 -0
  12. package/dist/actions/creative-context-a2a.d.ts.map +1 -0
  13. package/dist/actions/creative-context-a2a.js +66 -0
  14. package/dist/actions/creative-context-a2a.js.map +1 -0
  15. package/dist/actions/get-brand-profile.d.ts +33 -0
  16. package/dist/actions/get-brand-profile.d.ts.map +1 -0
  17. package/dist/actions/get-brand-profile.js +13 -0
  18. package/dist/actions/get-brand-profile.js.map +1 -0
  19. package/dist/actions/get-context-import-status.d.ts +22 -0
  20. package/dist/actions/get-context-import-status.d.ts.map +1 -0
  21. package/dist/actions/get-context-import-status.js +12 -0
  22. package/dist/actions/get-context-import-status.js.map +1 -0
  23. package/dist/actions/get-context-item.d.ts +2 -0
  24. package/dist/actions/get-context-item.d.ts.map +1 -0
  25. package/dist/actions/get-context-item.js +2 -0
  26. package/dist/actions/get-context-item.js.map +1 -0
  27. package/dist/actions/get-context-pack.d.ts +7 -0
  28. package/dist/actions/get-context-pack.d.ts.map +1 -0
  29. package/dist/actions/get-context-pack.js +12 -0
  30. package/dist/actions/get-context-pack.js.map +1 -0
  31. package/dist/actions/get-creative-context.d.ts +119 -0
  32. package/dist/actions/get-creative-context.d.ts.map +1 -0
  33. package/dist/actions/get-creative-context.js +112 -0
  34. package/dist/actions/get-creative-context.js.map +1 -0
  35. package/dist/actions/index.d.ts +3 -0
  36. package/dist/actions/index.d.ts.map +1 -0
  37. package/dist/actions/index.js +63 -0
  38. package/dist/actions/index.js.map +1 -0
  39. package/dist/actions/infer-brand-dna.d.ts +52 -0
  40. package/dist/actions/infer-brand-dna.d.ts.map +1 -0
  41. package/dist/actions/infer-brand-dna.js +13 -0
  42. package/dist/actions/infer-brand-dna.js.map +1 -0
  43. package/dist/actions/list-canonical-logo-candidates.d.ts +9 -0
  44. package/dist/actions/list-canonical-logo-candidates.d.ts.map +1 -0
  45. package/dist/actions/list-canonical-logo-candidates.js +82 -0
  46. package/dist/actions/list-canonical-logo-candidates.js.map +1 -0
  47. package/dist/actions/list-context-connections.d.ts +22 -0
  48. package/dist/actions/list-context-connections.d.ts.map +1 -0
  49. package/dist/actions/list-context-connections.js +43 -0
  50. package/dist/actions/list-context-connections.js.map +1 -0
  51. package/dist/actions/list-context-packs.d.ts +10 -0
  52. package/dist/actions/list-context-packs.d.ts.map +1 -0
  53. package/dist/actions/list-context-packs.js +16 -0
  54. package/dist/actions/list-context-packs.js.map +1 -0
  55. package/dist/actions/list-context-sources.d.ts +12 -0
  56. package/dist/actions/list-context-sources.d.ts.map +1 -0
  57. package/dist/actions/list-context-sources.js +20 -0
  58. package/dist/actions/list-context-sources.js.map +1 -0
  59. package/dist/actions/list-context-suggestions.d.ts +13 -0
  60. package/dist/actions/list-context-suggestions.d.ts.map +1 -0
  61. package/dist/actions/list-context-suggestions.js +28 -0
  62. package/dist/actions/list-context-suggestions.js.map +1 -0
  63. package/dist/actions/manage-brand-profile.d.ts +28 -0
  64. package/dist/actions/manage-brand-profile.d.ts.map +1 -0
  65. package/dist/actions/manage-brand-profile.js +47 -0
  66. package/dist/actions/manage-brand-profile.js.map +1 -0
  67. package/dist/actions/manage-context-pack.d.ts +33 -0
  68. package/dist/actions/manage-context-pack.d.ts.map +1 -0
  69. package/dist/actions/manage-context-pack.js +65 -0
  70. package/dist/actions/manage-context-pack.js.map +1 -0
  71. package/dist/actions/manage-context-source.d.ts +51 -0
  72. package/dist/actions/manage-context-source.d.ts.map +1 -0
  73. package/dist/actions/manage-context-source.js +137 -0
  74. package/dist/actions/manage-context-source.js.map +1 -0
  75. package/dist/actions/manage-layout-template.d.ts +13 -0
  76. package/dist/actions/manage-layout-template.d.ts.map +1 -0
  77. package/dist/actions/manage-layout-template.js +43 -0
  78. package/dist/actions/manage-layout-template.js.map +1 -0
  79. package/dist/actions/preview-context-import.d.ts +23 -0
  80. package/dist/actions/preview-context-import.d.ts.map +1 -0
  81. package/dist/actions/preview-context-import.js +57 -0
  82. package/dist/actions/preview-context-import.js.map +1 -0
  83. package/dist/actions/process-context-purge.d.ts +22 -0
  84. package/dist/actions/process-context-purge.d.ts.map +1 -0
  85. package/dist/actions/process-context-purge.js +19 -0
  86. package/dist/actions/process-context-purge.js.map +1 -0
  87. package/dist/actions/propose-brand-dna.d.ts +15 -0
  88. package/dist/actions/propose-brand-dna.d.ts.map +1 -0
  89. package/dist/actions/propose-brand-dna.js +19 -0
  90. package/dist/actions/propose-brand-dna.js.map +1 -0
  91. package/dist/actions/propose-canonical-logo.d.ts +9 -0
  92. package/dist/actions/propose-canonical-logo.d.ts.map +1 -0
  93. package/dist/actions/propose-canonical-logo.js +16 -0
  94. package/dist/actions/propose-canonical-logo.js.map +1 -0
  95. package/dist/actions/publish-brand-dna.d.ts +13 -0
  96. package/dist/actions/publish-brand-dna.d.ts.map +1 -0
  97. package/dist/actions/publish-brand-dna.js +23 -0
  98. package/dist/actions/publish-brand-dna.js.map +1 -0
  99. package/dist/actions/recommend-context-roots.d.ts +14 -0
  100. package/dist/actions/recommend-context-roots.d.ts.map +1 -0
  101. package/dist/actions/recommend-context-roots.js +22 -0
  102. package/dist/actions/recommend-context-roots.js.map +1 -0
  103. package/dist/actions/record-context-feedback.d.ts +10 -0
  104. package/dist/actions/record-context-feedback.d.ts.map +1 -0
  105. package/dist/actions/record-context-feedback.js +14 -0
  106. package/dist/actions/record-context-feedback.js.map +1 -0
  107. package/dist/actions/review-context-items.d.ts +35 -0
  108. package/dist/actions/review-context-items.d.ts.map +1 -0
  109. package/dist/actions/review-context-items.js +65 -0
  110. package/dist/actions/review-context-items.js.map +1 -0
  111. package/dist/actions/run-embedding-bakeoff.d.ts +15 -0
  112. package/dist/actions/run-embedding-bakeoff.d.ts.map +1 -0
  113. package/dist/actions/run-embedding-bakeoff.js +72 -0
  114. package/dist/actions/run-embedding-bakeoff.js.map +1 -0
  115. package/dist/actions/search-creative-context.d.ts +75 -0
  116. package/dist/actions/search-creative-context.d.ts.map +1 -0
  117. package/dist/actions/search-creative-context.js +41 -0
  118. package/dist/actions/search-creative-context.js.map +1 -0
  119. package/dist/actions/set-brand-dna.d.ts +15 -0
  120. package/dist/actions/set-brand-dna.d.ts.map +1 -0
  121. package/dist/actions/set-brand-dna.js +19 -0
  122. package/dist/actions/set-brand-dna.js.map +1 -0
  123. package/dist/actions/start-context-enrichment.d.ts +10 -0
  124. package/dist/actions/start-context-enrichment.d.ts.map +1 -0
  125. package/dist/actions/start-context-enrichment.js +46 -0
  126. package/dist/actions/start-context-enrichment.js.map +1 -0
  127. package/dist/actions/start-context-import.d.ts +9 -0
  128. package/dist/actions/start-context-import.d.ts.map +1 -0
  129. package/dist/actions/start-context-import.js +36 -0
  130. package/dist/actions/start-context-import.js.map +1 -0
  131. package/dist/client/CreativeContextChip.d.ts +13 -0
  132. package/dist/client/CreativeContextChip.d.ts.map +1 -0
  133. package/dist/client/CreativeContextChip.js +45 -0
  134. package/dist/client/CreativeContextChip.js.map +1 -0
  135. package/dist/client/CreativeContextPanel.d.ts +131 -0
  136. package/dist/client/CreativeContextPanel.d.ts.map +1 -0
  137. package/dist/client/CreativeContextPanel.js +778 -0
  138. package/dist/client/CreativeContextPanel.js.map +1 -0
  139. package/dist/client/CreativeContextSettingsLink.d.ts +4 -0
  140. package/dist/client/CreativeContextSettingsLink.d.ts.map +1 -0
  141. package/dist/client/CreativeContextSettingsLink.js +8 -0
  142. package/dist/client/CreativeContextSettingsLink.js.map +1 -0
  143. package/dist/client/actions.d.ts +256 -0
  144. package/dist/client/actions.d.ts.map +1 -0
  145. package/dist/client/actions.js +89 -0
  146. package/dist/client/actions.js.map +1 -0
  147. package/dist/client/agent-tab.d.ts +9 -0
  148. package/dist/client/agent-tab.d.ts.map +1 -0
  149. package/dist/client/agent-tab.js +32 -0
  150. package/dist/client/agent-tab.js.map +1 -0
  151. package/dist/client/application-state.d.ts +22 -0
  152. package/dist/client/application-state.d.ts.map +1 -0
  153. package/dist/client/application-state.js +88 -0
  154. package/dist/client/application-state.js.map +1 -0
  155. package/dist/client/index.d.ts +8 -0
  156. package/dist/client/index.d.ts.map +1 -0
  157. package/dist/client/index.js +8 -0
  158. package/dist/client/index.js.map +1 -0
  159. package/dist/client/messages.d.ts +5 -0
  160. package/dist/client/messages.d.ts.map +1 -0
  161. package/dist/client/messages.js +1059 -0
  162. package/dist/client/messages.js.map +1 -0
  163. package/dist/connectors/context.d.ts +9 -0
  164. package/dist/connectors/context.d.ts.map +1 -0
  165. package/dist/connectors/context.js +59 -0
  166. package/dist/connectors/context.js.map +1 -0
  167. package/dist/connectors/document-parser.d.ts +2 -0
  168. package/dist/connectors/document-parser.d.ts.map +1 -0
  169. package/dist/connectors/document-parser.js +2 -0
  170. package/dist/connectors/document-parser.js.map +1 -0
  171. package/dist/connectors/figma-native.d.ts +18 -0
  172. package/dist/connectors/figma-native.d.ts.map +1 -0
  173. package/dist/connectors/figma-native.js +883 -0
  174. package/dist/connectors/figma-native.js.map +1 -0
  175. package/dist/connectors/figma.d.ts +10 -0
  176. package/dist/connectors/figma.d.ts.map +1 -0
  177. package/dist/connectors/figma.js +166 -0
  178. package/dist/connectors/figma.js.map +1 -0
  179. package/dist/connectors/google-slides-native.d.ts +58 -0
  180. package/dist/connectors/google-slides-native.d.ts.map +1 -0
  181. package/dist/connectors/google-slides-native.js +1118 -0
  182. package/dist/connectors/google-slides-native.js.map +1 -0
  183. package/dist/connectors/google-slides.d.ts +15 -0
  184. package/dist/connectors/google-slides.d.ts.map +1 -0
  185. package/dist/connectors/google-slides.js +896 -0
  186. package/dist/connectors/google-slides.js.map +1 -0
  187. package/dist/connectors/index.d.ts +20 -0
  188. package/dist/connectors/index.d.ts.map +1 -0
  189. package/dist/connectors/index.js +44 -0
  190. package/dist/connectors/index.js.map +1 -0
  191. package/dist/connectors/manual.d.ts +9 -0
  192. package/dist/connectors/manual.d.ts.map +1 -0
  193. package/dist/connectors/manual.js +91 -0
  194. package/dist/connectors/manual.js.map +1 -0
  195. package/dist/connectors/normalize.d.ts +35 -0
  196. package/dist/connectors/normalize.d.ts.map +1 -0
  197. package/dist/connectors/normalize.js +216 -0
  198. package/dist/connectors/normalize.js.map +1 -0
  199. package/dist/connectors/notion.d.ts +10 -0
  200. package/dist/connectors/notion.d.ts.map +1 -0
  201. package/dist/connectors/notion.js +393 -0
  202. package/dist/connectors/notion.js.map +1 -0
  203. package/dist/connectors/private-artifacts.d.ts +39 -0
  204. package/dist/connectors/private-artifacts.d.ts.map +1 -0
  205. package/dist/connectors/private-artifacts.js +175 -0
  206. package/dist/connectors/private-artifacts.js.map +1 -0
  207. package/dist/connectors/provider-response.d.ts +20 -0
  208. package/dist/connectors/provider-response.d.ts.map +1 -0
  209. package/dist/connectors/provider-response.js +98 -0
  210. package/dist/connectors/provider-response.js.map +1 -0
  211. package/dist/connectors/recommendations.d.ts +25 -0
  212. package/dist/connectors/recommendations.d.ts.map +1 -0
  213. package/dist/connectors/recommendations.js +172 -0
  214. package/dist/connectors/recommendations.js.map +1 -0
  215. package/dist/connectors/registry.d.ts +10 -0
  216. package/dist/connectors/registry.d.ts.map +1 -0
  217. package/dist/connectors/registry.js +34 -0
  218. package/dist/connectors/registry.js.map +1 -0
  219. package/dist/connectors/rendered-page.d.ts +101 -0
  220. package/dist/connectors/rendered-page.d.ts.map +1 -0
  221. package/dist/connectors/rendered-page.js +504 -0
  222. package/dist/connectors/rendered-page.js.map +1 -0
  223. package/dist/connectors/smart-defaults.d.ts +9 -0
  224. package/dist/connectors/smart-defaults.d.ts.map +1 -0
  225. package/dist/connectors/smart-defaults.js +72 -0
  226. package/dist/connectors/smart-defaults.js.map +1 -0
  227. package/dist/connectors/types.d.ts +79 -0
  228. package/dist/connectors/types.d.ts.map +1 -0
  229. package/dist/connectors/types.js +2 -0
  230. package/dist/connectors/types.js.map +1 -0
  231. package/dist/connectors/upload.d.ts +9 -0
  232. package/dist/connectors/upload.d.ts.map +1 -0
  233. package/dist/connectors/upload.js +440 -0
  234. package/dist/connectors/upload.js.map +1 -0
  235. package/dist/connectors/website.d.ts +20 -0
  236. package/dist/connectors/website.d.ts.map +1 -0
  237. package/dist/connectors/website.js +616 -0
  238. package/dist/connectors/website.js.map +1 -0
  239. package/dist/embeddings/index.d.ts +3 -0
  240. package/dist/embeddings/index.d.ts.map +1 -0
  241. package/dist/embeddings/index.js +3 -0
  242. package/dist/embeddings/index.js.map +1 -0
  243. package/dist/embeddings/providers.d.ts +7 -0
  244. package/dist/embeddings/providers.d.ts.map +1 -0
  245. package/dist/embeddings/providers.js +176 -0
  246. package/dist/embeddings/providers.js.map +1 -0
  247. package/dist/embeddings/types.d.ts +19 -0
  248. package/dist/embeddings/types.d.ts.map +1 -0
  249. package/dist/embeddings/types.js +2 -0
  250. package/dist/embeddings/types.js.map +1 -0
  251. package/dist/eval/acceptance-fixtures.d.ts +3 -0
  252. package/dist/eval/acceptance-fixtures.d.ts.map +1 -0
  253. package/dist/eval/acceptance-fixtures.js +84 -0
  254. package/dist/eval/acceptance-fixtures.js.map +1 -0
  255. package/dist/eval/acceptance.d.ts +114 -0
  256. package/dist/eval/acceptance.d.ts.map +1 -0
  257. package/dist/eval/acceptance.js +260 -0
  258. package/dist/eval/acceptance.js.map +1 -0
  259. package/dist/eval/fixtures.d.ts +16 -0
  260. package/dist/eval/fixtures.d.ts.map +1 -0
  261. package/dist/eval/fixtures.js +119 -0
  262. package/dist/eval/fixtures.js.map +1 -0
  263. package/dist/eval/index.d.ts +63 -0
  264. package/dist/eval/index.d.ts.map +1 -0
  265. package/dist/eval/index.js +174 -0
  266. package/dist/eval/index.js.map +1 -0
  267. package/dist/index.d.ts +5 -0
  268. package/dist/index.d.ts.map +1 -0
  269. package/dist/index.js +5 -0
  270. package/dist/index.js.map +1 -0
  271. package/dist/jobs/background-worker.d.ts +31 -0
  272. package/dist/jobs/background-worker.d.ts.map +1 -0
  273. package/dist/jobs/background-worker.js +315 -0
  274. package/dist/jobs/background-worker.js.map +1 -0
  275. package/dist/jobs/import-runner.d.ts +3 -0
  276. package/dist/jobs/import-runner.d.ts.map +1 -0
  277. package/dist/jobs/import-runner.js +613 -0
  278. package/dist/jobs/import-runner.js.map +1 -0
  279. package/dist/jobs/index.d.ts +9 -0
  280. package/dist/jobs/index.d.ts.map +1 -0
  281. package/dist/jobs/index.js +8 -0
  282. package/dist/jobs/index.js.map +1 -0
  283. package/dist/jobs/progress.d.ts +3 -0
  284. package/dist/jobs/progress.d.ts.map +1 -0
  285. package/dist/jobs/progress.js +30 -0
  286. package/dist/jobs/progress.js.map +1 -0
  287. package/dist/jobs/rebuild.d.ts +13 -0
  288. package/dist/jobs/rebuild.d.ts.map +1 -0
  289. package/dist/jobs/rebuild.js +178 -0
  290. package/dist/jobs/rebuild.js.map +1 -0
  291. package/dist/jobs/server-worker.d.ts +16 -0
  292. package/dist/jobs/server-worker.d.ts.map +1 -0
  293. package/dist/jobs/server-worker.js +252 -0
  294. package/dist/jobs/server-worker.js.map +1 -0
  295. package/dist/jobs/store-adapter.d.ts +3 -0
  296. package/dist/jobs/store-adapter.d.ts.map +1 -0
  297. package/dist/jobs/store-adapter.js +37 -0
  298. package/dist/jobs/store-adapter.js.map +1 -0
  299. package/dist/jobs/types.d.ts +138 -0
  300. package/dist/jobs/types.d.ts.map +1 -0
  301. package/dist/jobs/types.js +2 -0
  302. package/dist/jobs/types.js.map +1 -0
  303. package/dist/jobs/worker.d.ts +27 -0
  304. package/dist/jobs/worker.d.ts.map +1 -0
  305. package/dist/jobs/worker.js +90 -0
  306. package/dist/jobs/worker.js.map +1 -0
  307. package/dist/media-url.d.ts +7 -0
  308. package/dist/media-url.d.ts.map +1 -0
  309. package/dist/media-url.js +15 -0
  310. package/dist/media-url.js.map +1 -0
  311. package/dist/native-artifact-reassembly.d.ts +25 -0
  312. package/dist/native-artifact-reassembly.d.ts.map +1 -0
  313. package/dist/native-artifact-reassembly.js +262 -0
  314. package/dist/native-artifact-reassembly.js.map +1 -0
  315. package/dist/native-artifact.d.ts +88 -0
  316. package/dist/native-artifact.d.ts.map +1 -0
  317. package/dist/native-artifact.js +86 -0
  318. package/dist/native-artifact.js.map +1 -0
  319. package/dist/schema/index.d.ts +5519 -0
  320. package/dist/schema/index.d.ts.map +1 -0
  321. package/dist/schema/index.js +374 -0
  322. package/dist/schema/index.js.map +1 -0
  323. package/dist/schema/migrations.d.ts +10 -0
  324. package/dist/schema/migrations.d.ts.map +1 -0
  325. package/dist/schema/migrations.js +253 -0
  326. package/dist/schema/migrations.js.map +1 -0
  327. package/dist/search/fusion.d.ts +15 -0
  328. package/dist/search/fusion.d.ts.map +1 -0
  329. package/dist/search/fusion.js +25 -0
  330. package/dist/search/fusion.js.map +1 -0
  331. package/dist/search/mode.d.ts +4 -0
  332. package/dist/search/mode.d.ts.map +1 -0
  333. package/dist/search/mode.js +13 -0
  334. package/dist/search/mode.js.map +1 -0
  335. package/dist/search/postgres-fts.d.ts +22 -0
  336. package/dist/search/postgres-fts.d.ts.map +1 -0
  337. package/dist/search/postgres-fts.js +83 -0
  338. package/dist/search/postgres-fts.js.map +1 -0
  339. package/dist/server/brand-context.d.ts +15 -0
  340. package/dist/server/brand-context.d.ts.map +1 -0
  341. package/dist/server/brand-context.js +149 -0
  342. package/dist/server/brand-context.js.map +1 -0
  343. package/dist/server/context.d.ts +77 -0
  344. package/dist/server/context.d.ts.map +1 -0
  345. package/dist/server/context.js +63 -0
  346. package/dist/server/context.js.map +1 -0
  347. package/dist/server/enrichment.d.ts +23 -0
  348. package/dist/server/enrichment.d.ts.map +1 -0
  349. package/dist/server/enrichment.js +136 -0
  350. package/dist/server/enrichment.js.map +1 -0
  351. package/dist/server/generation-artifact-access.d.ts +21 -0
  352. package/dist/server/generation-artifact-access.d.ts.map +1 -0
  353. package/dist/server/generation-artifact-access.js +111 -0
  354. package/dist/server/generation-artifact-access.js.map +1 -0
  355. package/dist/server/generation-context.d.ts +204 -0
  356. package/dist/server/generation-context.d.ts.map +1 -0
  357. package/dist/server/generation-context.js +310 -0
  358. package/dist/server/generation-context.js.map +1 -0
  359. package/dist/server/index.d.ts +22 -0
  360. package/dist/server/index.d.ts.map +1 -0
  361. package/dist/server/index.js +96 -0
  362. package/dist/server/index.js.map +1 -0
  363. package/dist/server/isolated-a2a.d.ts +172 -0
  364. package/dist/server/isolated-a2a.d.ts.map +1 -0
  365. package/dist/server/isolated-a2a.js +310 -0
  366. package/dist/server/isolated-a2a.js.map +1 -0
  367. package/dist/server/media.d.ts +15 -0
  368. package/dist/server/media.d.ts.map +1 -0
  369. package/dist/server/media.js +93 -0
  370. package/dist/server/media.js.map +1 -0
  371. package/dist/server/prompt-provider.d.ts +5 -0
  372. package/dist/server/prompt-provider.d.ts.map +1 -0
  373. package/dist/server/prompt-provider.js +59 -0
  374. package/dist/server/prompt-provider.js.map +1 -0
  375. package/dist/server/public-serialization.d.ts +160 -0
  376. package/dist/server/public-serialization.d.ts.map +1 -0
  377. package/dist/server/public-serialization.js +108 -0
  378. package/dist/server/public-serialization.js.map +1 -0
  379. package/dist/server/resource-paths.d.ts +3 -0
  380. package/dist/server/resource-paths.d.ts.map +1 -0
  381. package/dist/server/resource-paths.js +5 -0
  382. package/dist/server/resource-paths.js.map +1 -0
  383. package/dist/server/retrieval.d.ts +78 -0
  384. package/dist/server/retrieval.d.ts.map +1 -0
  385. package/dist/server/retrieval.js +443 -0
  386. package/dist/server/retrieval.js.map +1 -0
  387. package/dist/server/untrusted-reference.d.ts +8 -0
  388. package/dist/server/untrusted-reference.d.ts.map +1 -0
  389. package/dist/server/untrusted-reference.js +19 -0
  390. package/dist/server/untrusted-reference.js.map +1 -0
  391. package/dist/store/brand.d.ts +47 -0
  392. package/dist/store/brand.d.ts.map +1 -0
  393. package/dist/store/brand.js +360 -0
  394. package/dist/store/brand.js.map +1 -0
  395. package/dist/store/content.d.ts +120 -0
  396. package/dist/store/content.d.ts.map +1 -0
  397. package/dist/store/content.js +1437 -0
  398. package/dist/store/content.js.map +1 -0
  399. package/dist/store/generation.d.ts +28 -0
  400. package/dist/store/generation.d.ts.map +1 -0
  401. package/dist/store/generation.js +140 -0
  402. package/dist/store/generation.js.map +1 -0
  403. package/dist/store/helpers.d.ts +12 -0
  404. package/dist/store/helpers.d.ts.map +1 -0
  405. package/dist/store/helpers.js +41 -0
  406. package/dist/store/helpers.js.map +1 -0
  407. package/dist/store/index.d.ts +10 -0
  408. package/dist/store/index.d.ts.map +1 -0
  409. package/dist/store/index.js +10 -0
  410. package/dist/store/index.js.map +1 -0
  411. package/dist/store/inference.d.ts +57 -0
  412. package/dist/store/inference.d.ts.map +1 -0
  413. package/dist/store/inference.js +279 -0
  414. package/dist/store/inference.js.map +1 -0
  415. package/dist/store/jobs.d.ts +80 -0
  416. package/dist/store/jobs.d.ts.map +1 -0
  417. package/dist/store/jobs.js +428 -0
  418. package/dist/store/jobs.js.map +1 -0
  419. package/dist/store/packs.d.ts +33 -0
  420. package/dist/store/packs.d.ts.map +1 -0
  421. package/dist/store/packs.js +321 -0
  422. package/dist/store/packs.js.map +1 -0
  423. package/dist/store/purge.d.ts +16 -0
  424. package/dist/store/purge.d.ts.map +1 -0
  425. package/dist/store/purge.js +256 -0
  426. package/dist/store/purge.js.map +1 -0
  427. package/dist/store/sources.d.ts +76 -0
  428. package/dist/store/sources.d.ts.map +1 -0
  429. package/dist/store/sources.js +538 -0
  430. package/dist/store/sources.js.map +1 -0
  431. package/dist/store/suggestions.d.ts +28 -0
  432. package/dist/store/suggestions.d.ts.map +1 -0
  433. package/dist/store/suggestions.js +250 -0
  434. package/dist/store/suggestions.js.map +1 -0
  435. package/dist/types.d.ts +493 -0
  436. package/dist/types.d.ts.map +1 -0
  437. package/dist/types.js +2 -0
  438. package/dist/types.js.map +1 -0
  439. package/dist/vector/index.d.ts +2 -0
  440. package/dist/vector/index.d.ts.map +1 -0
  441. package/dist/vector/index.js +2 -0
  442. package/dist/vector/index.js.map +1 -0
  443. package/dist/vector/pgvector.d.ts +28 -0
  444. package/dist/vector/pgvector.d.ts.map +1 -0
  445. package/dist/vector/pgvector.js +117 -0
  446. package/dist/vector/pgvector.js.map +1 -0
  447. package/docs/evaluation.md +92 -0
  448. package/docs/llms-full.txt +168 -0
  449. package/docs/llms.txt +76 -0
  450. package/docs/skills/creative-context/SKILL.md +74 -0
  451. package/package.json +91 -0
  452. package/src/actions/agent-input-schemas.spec.ts +46 -0
  453. package/src/actions/confirm-canonical-logo.ts +20 -0
  454. package/src/actions/continue-context-import.ts +12 -0
  455. package/src/actions/creative-context-a2a.spec.ts +64 -0
  456. package/src/actions/creative-context-a2a.ts +88 -0
  457. package/src/actions/get-brand-profile.ts +16 -0
  458. package/src/actions/get-context-import-status.ts +13 -0
  459. package/src/actions/get-context-item.ts +1 -0
  460. package/src/actions/get-context-pack.ts +14 -0
  461. package/src/actions/get-creative-context.spec.ts +315 -0
  462. package/src/actions/get-creative-context.ts +148 -0
  463. package/src/actions/index.ts +65 -0
  464. package/src/actions/infer-brand-dna.ts +15 -0
  465. package/src/actions/list-canonical-logo-candidates.ts +121 -0
  466. package/src/actions/list-context-connections.spec.ts +23 -0
  467. package/src/actions/list-context-connections.ts +51 -0
  468. package/src/actions/list-context-packs.ts +17 -0
  469. package/src/actions/list-context-sources.ts +21 -0
  470. package/src/actions/list-context-suggestions.ts +30 -0
  471. package/src/actions/manage-brand-profile.ts +58 -0
  472. package/src/actions/manage-context-pack.ts +75 -0
  473. package/src/actions/manage-context-source.ts +154 -0
  474. package/src/actions/manage-layout-template.ts +51 -0
  475. package/src/actions/preview-context-import.spec.ts +64 -0
  476. package/src/actions/preview-context-import.ts +61 -0
  477. package/src/actions/process-context-purge.ts +25 -0
  478. package/src/actions/propose-brand-dna.ts +22 -0
  479. package/src/actions/propose-canonical-logo.ts +19 -0
  480. package/src/actions/publish-brand-dna.ts +25 -0
  481. package/src/actions/recommend-context-roots.ts +24 -0
  482. package/src/actions/record-context-feedback.ts +16 -0
  483. package/src/actions/review-context-items.ts +69 -0
  484. package/src/actions/run-embedding-bakeoff.ts +90 -0
  485. package/src/actions/search-creative-context.ts +47 -0
  486. package/src/actions/set-brand-dna.ts +22 -0
  487. package/src/actions/start-context-enrichment.ts +50 -0
  488. package/src/actions/start-context-import.ts +37 -0
  489. package/src/client/CreativeContextChip.tsx +162 -0
  490. package/src/client/CreativeContextPanel.tsx +2442 -0
  491. package/src/client/CreativeContextSettingsLink.tsx +30 -0
  492. package/src/client/actions.ts +438 -0
  493. package/src/client/agent-tab.tsx +58 -0
  494. package/src/client/application-state.spec.ts +41 -0
  495. package/src/client/application-state.ts +130 -0
  496. package/src/client/index.ts +62 -0
  497. package/src/client/messages.ts +1240 -0
  498. package/src/client/source-config.spec.ts +152 -0
  499. package/src/connectors/connectors.spec.ts +778 -0
  500. package/src/connectors/context.ts +83 -0
  501. package/src/connectors/document-parser.ts +5 -0
  502. package/src/connectors/figma-native.ts +1192 -0
  503. package/src/connectors/figma-native.visual.spec.ts +221 -0
  504. package/src/connectors/figma.spec.ts +812 -0
  505. package/src/connectors/figma.ts +257 -0
  506. package/src/connectors/fixtures/google-slides-native.json +353 -0
  507. package/src/connectors/google-slides-native.spec.ts +804 -0
  508. package/src/connectors/google-slides-native.ts +1656 -0
  509. package/src/connectors/google-slides-native.visual.spec.ts +119 -0
  510. package/src/connectors/google-slides.ts +1172 -0
  511. package/src/connectors/index.ts +103 -0
  512. package/src/connectors/manual.ts +132 -0
  513. package/src/connectors/normalize.ts +277 -0
  514. package/src/connectors/notion.ts +529 -0
  515. package/src/connectors/private-artifacts.spec.ts +95 -0
  516. package/src/connectors/private-artifacts.ts +248 -0
  517. package/src/connectors/provider-response.ts +144 -0
  518. package/src/connectors/recommendations.ts +237 -0
  519. package/src/connectors/registry.ts +48 -0
  520. package/src/connectors/rendered-page.ts +705 -0
  521. package/src/connectors/smart-defaults.spec.ts +135 -0
  522. package/src/connectors/smart-defaults.ts +87 -0
  523. package/src/connectors/types.ts +115 -0
  524. package/src/connectors/upload.spec.ts +175 -0
  525. package/src/connectors/upload.ts +548 -0
  526. package/src/connectors/website.spec.ts +509 -0
  527. package/src/connectors/website.ts +777 -0
  528. package/src/embeddings/index.ts +2 -0
  529. package/src/embeddings/providers.spec.ts +73 -0
  530. package/src/embeddings/providers.ts +228 -0
  531. package/src/embeddings/types.ts +24 -0
  532. package/src/eval/acceptance-fixtures.ts +97 -0
  533. package/src/eval/acceptance.spec.ts +92 -0
  534. package/src/eval/acceptance.ts +371 -0
  535. package/src/eval/connectors-golden.spec.ts +421 -0
  536. package/src/eval/eval.spec.ts +166 -0
  537. package/src/eval/fixtures/README.md +17 -0
  538. package/src/eval/fixtures/figma-brand-system-v1.json +140 -0
  539. package/src/eval/fixtures/figma-brand-system-v2.json +127 -0
  540. package/src/eval/fixtures/launch-hero.png +0 -0
  541. package/src/eval/fixtures/launch-system-v1.pptx +0 -0
  542. package/src/eval/fixtures/launch-system-v2.pptx +0 -0
  543. package/src/eval/fixtures/notion-brand-root.json +218 -0
  544. package/src/eval/fixtures/pricing-hero.png +0 -0
  545. package/src/eval/fixtures/rendered-brand-page.html +76 -0
  546. package/src/eval/fixtures.spec.ts +43 -0
  547. package/src/eval/fixtures.ts +136 -0
  548. package/src/eval/index.ts +291 -0
  549. package/src/eval/library-flow.acceptance.spec.tsx +141 -0
  550. package/src/index.ts +4 -0
  551. package/src/jobs/background-worker.ts +423 -0
  552. package/src/jobs/import-runner.spec.ts +582 -0
  553. package/src/jobs/import-runner.ts +792 -0
  554. package/src/jobs/index.ts +40 -0
  555. package/src/jobs/progress.ts +37 -0
  556. package/src/jobs/rebuild.ts +245 -0
  557. package/src/jobs/server-worker.spec.ts +159 -0
  558. package/src/jobs/server-worker.ts +344 -0
  559. package/src/jobs/store-adapter.ts +52 -0
  560. package/src/jobs/types.ts +147 -0
  561. package/src/jobs/worker.ts +152 -0
  562. package/src/manifest.spec.ts +26 -0
  563. package/src/media-url.ts +17 -0
  564. package/src/native-artifact-reassembly.spec.ts +386 -0
  565. package/src/native-artifact-reassembly.ts +406 -0
  566. package/src/native-artifact.ts +102 -0
  567. package/src/schema/index.ts +411 -0
  568. package/src/schema/migrations.spec.ts +41 -0
  569. package/src/schema/migrations.ts +258 -0
  570. package/src/search/fusion.spec.ts +21 -0
  571. package/src/search/fusion.ts +39 -0
  572. package/src/search/mode.spec.ts +28 -0
  573. package/src/search/mode.ts +21 -0
  574. package/src/search/postgres-fts.spec.ts +71 -0
  575. package/src/search/postgres-fts.ts +112 -0
  576. package/src/server/brand-context.spec.ts +43 -0
  577. package/src/server/brand-context.ts +160 -0
  578. package/src/server/context.ts +162 -0
  579. package/src/server/enrichment.ts +163 -0
  580. package/src/server/generation-artifact-access.ts +216 -0
  581. package/src/server/generation-context-a2a.spec.ts +131 -0
  582. package/src/server/generation-context-off.spec.ts +213 -0
  583. package/src/server/generation-context.spec.ts +124 -0
  584. package/src/server/generation-context.ts +493 -0
  585. package/src/server/index.ts +135 -0
  586. package/src/server/isolated-a2a.spec.ts +164 -0
  587. package/src/server/isolated-a2a.ts +413 -0
  588. package/src/server/media.spec.ts +92 -0
  589. package/src/server/media.ts +117 -0
  590. package/src/server/prompt-provider.spec.ts +78 -0
  591. package/src/server/prompt-provider.ts +73 -0
  592. package/src/server/public-serialization.spec.ts +149 -0
  593. package/src/server/public-serialization.ts +130 -0
  594. package/src/server/resource-paths.spec.ts +9 -0
  595. package/src/server/resource-paths.ts +5 -0
  596. package/src/server/retrieval-vector.spec.ts +180 -0
  597. package/src/server/retrieval.ts +587 -0
  598. package/src/server/untrusted-reference.spec.ts +22 -0
  599. package/src/server/untrusted-reference.ts +24 -0
  600. package/src/store/access.integration.spec.ts +1922 -0
  601. package/src/store/brand.ts +477 -0
  602. package/src/store/content.ts +1900 -0
  603. package/src/store/generation.ts +226 -0
  604. package/src/store/helpers.ts +53 -0
  605. package/src/store/index.ts +9 -0
  606. package/src/store/inference.spec.ts +84 -0
  607. package/src/store/inference.ts +339 -0
  608. package/src/store/invariants.spec.ts +119 -0
  609. package/src/store/jobs.ts +672 -0
  610. package/src/store/packs.ts +466 -0
  611. package/src/store/purge.ts +321 -0
  612. package/src/store/sources.spec.ts +102 -0
  613. package/src/store/sources.ts +708 -0
  614. package/src/store/suggestions.ts +333 -0
  615. package/src/types.ts +571 -0
  616. package/src/vector/index.ts +1 -0
  617. package/src/vector/pgvector.spec.ts +98 -0
  618. package/src/vector/pgvector.ts +164 -0
@@ -0,0 +1,2442 @@
1
+ import {
2
+ useFormatters,
3
+ useUploadResource,
4
+ useT,
5
+ type AgentPageScope,
6
+ } from "@agent-native/core/client";
7
+ import { useOrg } from "@agent-native/core/client/org";
8
+ import {
9
+ AlertDialog,
10
+ AlertDialogAction,
11
+ AlertDialogCancel,
12
+ AlertDialogContent,
13
+ AlertDialogDescription,
14
+ AlertDialogFooter,
15
+ AlertDialogHeader,
16
+ AlertDialogTitle,
17
+ Badge,
18
+ Button,
19
+ Checkbox,
20
+ DropdownMenu,
21
+ DropdownMenuContent,
22
+ DropdownMenuItem,
23
+ DropdownMenuSeparator,
24
+ DropdownMenuTrigger,
25
+ Input,
26
+ Select,
27
+ SelectContent,
28
+ SelectItem,
29
+ SelectTrigger,
30
+ SelectValue,
31
+ Skeleton,
32
+ Textarea,
33
+ } from "@agent-native/toolkit/ui";
34
+ import {
35
+ IconAlertTriangle,
36
+ IconArrowUpRight,
37
+ IconBooks,
38
+ IconCheck,
39
+ IconDots,
40
+ IconFileImport,
41
+ IconFileText,
42
+ IconLayout,
43
+ IconPalette,
44
+ IconPhoto,
45
+ IconPin,
46
+ IconRefresh,
47
+ IconSearch,
48
+ IconSlideshow,
49
+ IconSparkles,
50
+ IconUpload,
51
+ IconWorld,
52
+ } from "@tabler/icons-react";
53
+ import {
54
+ useEffect,
55
+ useMemo,
56
+ useRef,
57
+ useState,
58
+ type ChangeEvent,
59
+ type DragEvent,
60
+ type FormEvent,
61
+ type ReactNode,
62
+ } from "react";
63
+
64
+ import { creativeContextMediaUrl } from "../media-url.js";
65
+ import type {
66
+ ContextImportJobResult,
67
+ ContextPackSummary,
68
+ ContextReviewItem,
69
+ ContextSourceSummary,
70
+ } from "../types.js";
71
+ import {
72
+ useCreativeContextBrandProfile,
73
+ useCanonicalLogoCandidates,
74
+ useCreativeContextConnections,
75
+ useCreativeContextRootRecommendations,
76
+ useCreativeContextImportStatus,
77
+ useCreativeContextPack,
78
+ useCreativeContextPacks,
79
+ useCreativeContextSearch,
80
+ useCreativeContextSuggestions,
81
+ useCreativeContextSources,
82
+ useManageCreativeContextSource,
83
+ useManageLayoutTemplate,
84
+ usePreviewCreativeContextImport,
85
+ usePublishCreativeContextBrandDna,
86
+ useConfirmCanonicalLogo,
87
+ useProposeCanonicalLogo,
88
+ useRefreshCreativeContextSource,
89
+ useReviewCreativeContextItems,
90
+ useStartCreativeContextImport,
91
+ type CreativeContextConnectionProvider,
92
+ type CreativeContextRootRecommendation,
93
+ type CreativeContextRecommendationProvider,
94
+ } from "./actions.js";
95
+ import {
96
+ useCreativeContextState,
97
+ type CreativeContextMode,
98
+ } from "./application-state.js";
99
+ import { CreativeContextChip } from "./CreativeContextChip.js";
100
+
101
+ type ConnectorKind =
102
+ | "google-slides"
103
+ | "figma"
104
+ | "notion"
105
+ | "website"
106
+ | "upload";
107
+
108
+ interface ConnectorDefinition {
109
+ kind: ConnectorKind;
110
+ label: string;
111
+ referencePlaceholder: string;
112
+ referenceRequired: boolean;
113
+ icon: typeof IconBooks;
114
+ }
115
+
116
+ export interface UploadedContextFile {
117
+ id: string;
118
+ title: string;
119
+ fileName: string;
120
+ mimeType: string;
121
+ url: string;
122
+ }
123
+
124
+ const CONNECTORS: readonly ConnectorDefinition[] = [
125
+ {
126
+ kind: "google-slides",
127
+ label: "Google Slides",
128
+ referencePlaceholder: "Folder or shared-drive URL / ID",
129
+ referenceRequired: false,
130
+ icon: IconSlideshow,
131
+ },
132
+ {
133
+ kind: "figma",
134
+ label: "Figma",
135
+ referencePlaceholder: "Team, project, or file URLs — one per line",
136
+ referenceRequired: false,
137
+ icon: IconPalette,
138
+ },
139
+ {
140
+ kind: "notion",
141
+ label: "Notion",
142
+ referencePlaceholder: "Page or teamspace root URLs / IDs — one per line",
143
+ referenceRequired: false,
144
+ icon: IconFileText,
145
+ },
146
+ {
147
+ kind: "website",
148
+ label: "Website",
149
+ referencePlaceholder: "https://example.com\nhttps://example.com/about",
150
+ referenceRequired: true,
151
+ icon: IconWorld,
152
+ },
153
+ {
154
+ kind: "upload",
155
+ label: "Uploaded files",
156
+ referencePlaceholder: "One hosted file URL per line",
157
+ referenceRequired: true,
158
+ icon: IconUpload,
159
+ },
160
+ ];
161
+
162
+ export interface CreativeContextPanelProps {
163
+ scope?: AgentPageScope;
164
+ canManageOrg?: boolean;
165
+ scopeControl?: ReactNode;
166
+ connectionsHref?: string;
167
+ }
168
+
169
+ function isVisibleInScope(
170
+ visibility: "private" | "org" | "public",
171
+ scope: AgentPageScope,
172
+ ) {
173
+ return scope === "org" ? visibility !== "private" : visibility === "private";
174
+ }
175
+
176
+ function splitReferences(value: string) {
177
+ return value
178
+ .split(/\r?\n|,/)
179
+ .map((entry) => entry.trim())
180
+ .filter(Boolean);
181
+ }
182
+
183
+ function connectionProviderForConnector(
184
+ kind: ConnectorKind | null,
185
+ ): CreativeContextConnectionProvider | null {
186
+ if (kind === "google-slides") return "google_drive";
187
+ if (kind === "figma" || kind === "notion") return kind;
188
+ return null;
189
+ }
190
+
191
+ function recommendationProviderForConnector(
192
+ kind: ConnectorKind | null,
193
+ ): CreativeContextRecommendationProvider | null {
194
+ if (kind === "google-slides" || kind === "figma" || kind === "notion") {
195
+ return kind;
196
+ }
197
+ return null;
198
+ }
199
+
200
+ export function parseFigmaRecommendationBoundary(reference: string): {
201
+ figmaProjectId?: string;
202
+ figmaTeamId?: string;
203
+ } {
204
+ for (const value of splitReferences(reference)) {
205
+ const teamId =
206
+ value.match(/\/team\/([^/?#]+)/)?.[1] ??
207
+ (value.startsWith("team:") ? value.slice("team:".length) : undefined);
208
+ if (teamId) return { figmaTeamId: teamId };
209
+ const projectId =
210
+ value.match(/\/project\/([^/?#]+)/)?.[1] ??
211
+ (value.startsWith("project:")
212
+ ? value.slice("project:".length)
213
+ : undefined);
214
+ if (projectId) return { figmaProjectId: projectId };
215
+ }
216
+ return {};
217
+ }
218
+
219
+ export function selectRenderableLayoutThumbnails<
220
+ T extends { hasThumbnail: boolean },
221
+ >(thumbnails: readonly T[]): T[] {
222
+ return thumbnails.filter((thumbnail) => thumbnail.hasThumbnail).slice(0, 3);
223
+ }
224
+
225
+ export function buildCreativeContextSourceConfig(
226
+ kind: ConnectorKind,
227
+ reference: string,
228
+ uploadedFiles: UploadedContextFile[],
229
+ recommendations: CreativeContextRootRecommendation[] = [],
230
+ ) {
231
+ const references = splitReferences(reference);
232
+ if (kind === "google-slides") {
233
+ if (!references.length && recommendations.length) {
234
+ return {
235
+ presentationIds: recommendations.map((item) => item.externalId),
236
+ };
237
+ }
238
+ const container = references[0];
239
+ const folderMatch = container?.match(/\/folders\/([^/?#]+)/);
240
+ if (container?.startsWith("shared-drive:")) {
241
+ return { sharedDriveId: container.slice("shared-drive:".length) };
242
+ }
243
+ if (container && /^https?:\/\//.test(container)) {
244
+ return { folderUrl: container };
245
+ }
246
+ return container ? { folderId: folderMatch?.[1] ?? container } : {};
247
+ }
248
+ if (kind === "figma") {
249
+ const fileUrls: string[] = [];
250
+ const projectUrls: string[] = [];
251
+ const teamUrls: string[] = [];
252
+ for (const url of references) {
253
+ if (/\/team\//.test(url)) teamUrls.push(url);
254
+ else if (/\/project\//.test(url)) projectUrls.push(url);
255
+ else fileUrls.push(url);
256
+ }
257
+ return {
258
+ fileUrls,
259
+ projectUrls,
260
+ teamUrls,
261
+ ...(recommendations.length
262
+ ? { fileKeys: recommendations.map((item) => item.externalId) }
263
+ : {}),
264
+ };
265
+ }
266
+ if (kind === "notion") {
267
+ const rootPageIds: string[] = [];
268
+ const rootPageUrls: string[] = [];
269
+ const teamspaceRootPageIds: string[] = [];
270
+ const teamspaceRootPageUrls: string[] = [];
271
+ for (const root of references) {
272
+ const isTeamspace = root.startsWith("teamspace:");
273
+ const value = isTeamspace ? root.slice("teamspace:".length) : root;
274
+ if (/^https?:\/\//.test(value)) {
275
+ (isTeamspace ? teamspaceRootPageUrls : rootPageUrls).push(value);
276
+ } else {
277
+ (isTeamspace ? teamspaceRootPageIds : rootPageIds).push(value);
278
+ }
279
+ }
280
+ return {
281
+ rootPageIds: [
282
+ ...rootPageIds,
283
+ ...recommendations.map((item) => item.externalId),
284
+ ],
285
+ rootPageUrls,
286
+ teamspaceRootPageIds,
287
+ teamspaceRootPageUrls,
288
+ };
289
+ }
290
+ if (kind === "website") return { urls: references };
291
+ return { items: uploadedFiles };
292
+ }
293
+
294
+ function ContextModeButton({
295
+ mode,
296
+ activeMode,
297
+ label,
298
+ description,
299
+ disabled,
300
+ onSelect,
301
+ }: {
302
+ mode: CreativeContextMode;
303
+ activeMode: CreativeContextMode;
304
+ label: string;
305
+ description: string;
306
+ disabled: boolean;
307
+ onSelect: (mode: CreativeContextMode) => void;
308
+ }) {
309
+ const selected = mode === activeMode;
310
+ return (
311
+ <button
312
+ type="button"
313
+ role="radio"
314
+ aria-checked={selected}
315
+ disabled={disabled}
316
+ onClick={() => onSelect(mode)}
317
+ 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 ${
318
+ selected
319
+ ? "border-foreground/25 bg-accent/70 text-foreground"
320
+ : "border-border bg-background text-muted-foreground hover:bg-accent/40 hover:text-foreground"
321
+ }`}
322
+ >
323
+ <span className="block text-sm font-medium">{label}</span>
324
+ <span className="mt-0.5 block text-xs leading-relaxed">
325
+ {description}
326
+ </span>
327
+ </button>
328
+ );
329
+ }
330
+
331
+ function ScopeControl({
332
+ scope,
333
+ onChange,
334
+ }: {
335
+ scope: AgentPageScope;
336
+ onChange: (scope: AgentPageScope) => void;
337
+ }) {
338
+ const t = useT();
339
+ return (
340
+ <div className="inline-flex rounded-md border border-border bg-muted/30 p-0.5">
341
+ {(["user", "org"] as const).map((value) => (
342
+ <button
343
+ key={value}
344
+ type="button"
345
+ aria-pressed={scope === value}
346
+ onClick={() => onChange(value)}
347
+ className={`rounded px-2.5 py-1 text-xs font-medium transition-colors ${
348
+ scope === value
349
+ ? "bg-background text-foreground shadow-sm"
350
+ : "text-muted-foreground hover:text-foreground"
351
+ }`}
352
+ >
353
+ {t(
354
+ value === "user"
355
+ ? "creativeContext.personal"
356
+ : "creativeContext.organization",
357
+ )}
358
+ </button>
359
+ ))}
360
+ </div>
361
+ );
362
+ }
363
+
364
+ function SourceRow({
365
+ source,
366
+ refreshing,
367
+ canReview,
368
+ onRefresh,
369
+ onReview,
370
+ onCurate,
371
+ canPromote,
372
+ onPromote,
373
+ onPause,
374
+ onRestore,
375
+ onDelete,
376
+ }: {
377
+ source: ContextSourceSummary;
378
+ refreshing: boolean;
379
+ canReview: boolean;
380
+ onRefresh: (sourceId: string) => void;
381
+ onReview: (source: ContextSourceSummary) => void;
382
+ onCurate: (source: ContextSourceSummary) => void;
383
+ canPromote: boolean;
384
+ onPromote: (source: ContextSourceSummary) => void;
385
+ onPause: (source: ContextSourceSummary) => void;
386
+ onRestore: (source: ContextSourceSummary) => void;
387
+ onDelete: (source: ContextSourceSummary) => void;
388
+ }) {
389
+ const t = useT();
390
+ const { formatDate, formatNumber } = useFormatters();
391
+ return (
392
+ <div className="flex items-start gap-3 border-t border-border/60 py-3 first:border-t-0">
393
+ <div className="mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground">
394
+ <IconBooks className="size-4" />
395
+ </div>
396
+ <div className="min-w-0 flex-1">
397
+ <div className="flex flex-wrap items-center gap-2">
398
+ <span className="truncate text-sm font-medium text-foreground">
399
+ {source.name}
400
+ </span>
401
+ <Badge variant="secondary" className="font-normal">
402
+ {source.kind}
403
+ </Badge>
404
+ {source.status === "error" ? (
405
+ <Badge variant="destructive" className="font-normal">
406
+ {t("creativeContext.sourceError")}
407
+ </Badge>
408
+ ) : source.status !== "active" ? (
409
+ <Badge variant="outline" className="font-normal">
410
+ {source.status}
411
+ </Badge>
412
+ ) : null}
413
+ </div>
414
+ <div className="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-xs text-muted-foreground">
415
+ <span>
416
+ {t("creativeContext.itemsLabel", {
417
+ count: formatNumber(source.itemCount),
418
+ })}
419
+ </span>
420
+ {source.lastSyncedAt ? (
421
+ <span>
422
+ {formatDate(source.lastSyncedAt, {
423
+ month: "short",
424
+ day: "numeric",
425
+ year: "numeric",
426
+ })}
427
+ </span>
428
+ ) : null}
429
+ </div>
430
+ {source.restrictedItemCount > 0 ? (
431
+ <div className="mt-2 flex flex-wrap items-center gap-2 text-xs text-amber-700 dark:text-amber-300">
432
+ <span>
433
+ {t("creativeContext.restrictedItems", {
434
+ count: formatNumber(source.restrictedItemCount),
435
+ })}
436
+ </span>
437
+ {canReview ? (
438
+ <Button
439
+ type="button"
440
+ variant="ghost"
441
+ size="sm"
442
+ className="h-6 px-2 text-xs"
443
+ onClick={() => onReview(source)}
444
+ >
445
+ {t("creativeContext.reviewRestricted")}
446
+ </Button>
447
+ ) : null}
448
+ </div>
449
+ ) : null}
450
+ {source.lastError ? (
451
+ <p className="mt-1 line-clamp-2 text-xs text-destructive">
452
+ {source.lastError}
453
+ </p>
454
+ ) : null}
455
+ </div>
456
+ {source.status !== "archived" ? (
457
+ <Button
458
+ type="button"
459
+ variant="ghost"
460
+ size="sm"
461
+ disabled={refreshing}
462
+ onClick={() => onRefresh(source.id)}
463
+ >
464
+ <IconRefresh className={refreshing ? "animate-spin" : undefined} />
465
+ {refreshing
466
+ ? t("creativeContext.refreshing")
467
+ : t("creativeContext.refresh")}
468
+ </Button>
469
+ ) : null}
470
+ <DropdownMenu>
471
+ <DropdownMenuTrigger asChild>
472
+ <Button
473
+ type="button"
474
+ variant="ghost"
475
+ size="icon"
476
+ aria-label={t("creativeContext.manage")}
477
+ >
478
+ <IconDots />
479
+ </Button>
480
+ </DropdownMenuTrigger>
481
+ <DropdownMenuContent align="end">
482
+ <DropdownMenuItem onSelect={() => onCurate(source)}>
483
+ {t("creativeContext.curateItems")}
484
+ </DropdownMenuItem>
485
+ {source.status === "paused" || source.status === "error" ? (
486
+ <DropdownMenuItem onSelect={() => onRestore(source)}>
487
+ {t("creativeContext.restore")}
488
+ </DropdownMenuItem>
489
+ ) : (
490
+ <DropdownMenuItem onSelect={() => onPause(source)}>
491
+ {t("creativeContext.pause")}
492
+ </DropdownMenuItem>
493
+ )}
494
+ {canPromote ? (
495
+ <DropdownMenuItem onSelect={() => onPromote(source)}>
496
+ {t("creativeContext.promoteToOrganization")}
497
+ </DropdownMenuItem>
498
+ ) : null}
499
+ <DropdownMenuSeparator />
500
+ <DropdownMenuItem
501
+ className="text-destructive focus:text-destructive"
502
+ onSelect={() => onDelete(source)}
503
+ >
504
+ {t("creativeContext.delete")}
505
+ </DropdownMenuItem>
506
+ </DropdownMenuContent>
507
+ </DropdownMenu>
508
+ </div>
509
+ );
510
+ }
511
+
512
+ function PackRow({
513
+ pack,
514
+ pinned,
515
+ disabled,
516
+ onPin,
517
+ onDetails,
518
+ }: {
519
+ pack: ContextPackSummary;
520
+ pinned: boolean;
521
+ disabled: boolean;
522
+ onPin: (packId: string | null) => void;
523
+ onDetails: (packId: string) => void;
524
+ }) {
525
+ const t = useT();
526
+ const { formatNumber } = useFormatters();
527
+ return (
528
+ <div className="flex items-center gap-3 border-t border-border/60 py-3 first:border-t-0">
529
+ <div className="min-w-0 flex-1">
530
+ <div className="flex items-center gap-2">
531
+ <span className="truncate text-sm font-medium">{pack.name}</span>
532
+ {pinned ? (
533
+ <Badge variant="secondary" className="font-normal">
534
+ <IconPin className="me-1 size-3" />
535
+ {t("creativeContext.pinned")}
536
+ </Badge>
537
+ ) : null}
538
+ </div>
539
+ {pack.description ? (
540
+ <p className="mt-0.5 line-clamp-2 text-xs text-muted-foreground">
541
+ {pack.description}
542
+ </p>
543
+ ) : null}
544
+ <p className="mt-1 text-xs text-muted-foreground">
545
+ {t("creativeContext.itemsLabel", {
546
+ count: formatNumber(pack.memberCount),
547
+ })}
548
+ </p>
549
+ </div>
550
+ <Button
551
+ type="button"
552
+ variant="ghost"
553
+ size="sm"
554
+ onClick={() => onDetails(pack.id)}
555
+ >
556
+ {t("creativeContext.packDetails")}
557
+ </Button>
558
+ <Button
559
+ type="button"
560
+ variant={pinned ? "secondary" : "ghost"}
561
+ size="sm"
562
+ disabled={disabled}
563
+ onClick={() => onPin(pinned ? null : pack.id)}
564
+ >
565
+ <IconPin />
566
+ {pinned ? t("creativeContext.unpin") : t("creativeContext.pin")}
567
+ </Button>
568
+ </div>
569
+ );
570
+ }
571
+
572
+ type ContextItemReviewOperation =
573
+ | "approve"
574
+ | "exclude"
575
+ | "exemplar"
576
+ | "normal"
577
+ | "ignore"
578
+ | "star"
579
+ | "unstar"
580
+ | "deprecate"
581
+ | "restore";
582
+
583
+ function AccessScopedThumbnail({
584
+ itemId,
585
+ itemVersionId,
586
+ className,
587
+ }: {
588
+ itemId: string;
589
+ itemVersionId: string;
590
+ className: string;
591
+ }) {
592
+ const [failed, setFailed] = useState(false);
593
+ if (failed) {
594
+ return (
595
+ <div
596
+ className={`flex items-center justify-center bg-muted text-muted-foreground ${className}`}
597
+ >
598
+ <IconFileText className="size-5" />
599
+ </div>
600
+ );
601
+ }
602
+ return (
603
+ <img
604
+ src={creativeContextMediaUrl({ itemId, itemVersionId })}
605
+ alt=""
606
+ className={className}
607
+ onError={() => setFailed(true)}
608
+ />
609
+ );
610
+ }
611
+
612
+ function ItemCuration({
613
+ source,
614
+ items,
615
+ busy,
616
+ onReview,
617
+ onClose,
618
+ }: {
619
+ source: ContextSourceSummary;
620
+ items: ContextReviewItem[];
621
+ busy: boolean;
622
+ onReview: (operation: ContextItemReviewOperation, itemId: string) => void;
623
+ onClose: () => void;
624
+ }) {
625
+ const t = useT();
626
+ return (
627
+ <div className="mt-4 rounded-md border border-border p-4">
628
+ <div className="flex items-start justify-between gap-3">
629
+ <div>
630
+ <h3 className="text-sm font-semibold">
631
+ {t("creativeContext.curateItems")}
632
+ </h3>
633
+ <p className="mt-0.5 text-xs text-muted-foreground">{source.name}</p>
634
+ </div>
635
+ <Button type="button" variant="ghost" size="sm" onClick={onClose}>
636
+ {t("creativeContext.cancel")}
637
+ </Button>
638
+ </div>
639
+ <div className="mt-3 grid gap-3 sm:grid-cols-2">
640
+ {items.map((item) => (
641
+ <article
642
+ key={item.id}
643
+ className="overflow-hidden rounded-md border border-border/70"
644
+ >
645
+ {item.thumbnailBlobRef ? (
646
+ <AccessScopedThumbnail
647
+ key={item.currentVersionId}
648
+ itemId={item.id}
649
+ itemVersionId={item.currentVersionId}
650
+ className="aspect-video w-full object-cover"
651
+ />
652
+ ) : (
653
+ <div className="flex aspect-video w-full items-center justify-center bg-muted text-muted-foreground">
654
+ <IconFileText className="size-5" />
655
+ </div>
656
+ )}
657
+ <div className="p-3">
658
+ <div className="flex items-start justify-between gap-2">
659
+ <div className="min-w-0">
660
+ <p className="truncate text-sm font-medium">{item.title}</p>
661
+ <p className="text-xs text-muted-foreground">{item.kind}</p>
662
+ </div>
663
+ <div className="flex flex-wrap justify-end gap-1">
664
+ {item.curationRank === "exemplar" ? (
665
+ <Badge variant="secondary">
666
+ {t("creativeContext.exemplar")}
667
+ </Badge>
668
+ ) : null}
669
+ {item.status === "deprecated" ? (
670
+ <Badge variant="outline">
671
+ {t("creativeContext.deprecated")}
672
+ </Badge>
673
+ ) : null}
674
+ {item.upstreamAccess === "unknown" ? (
675
+ <Badge variant="outline">
676
+ {t("creativeContext.unknownAccess")}
677
+ </Badge>
678
+ ) : null}
679
+ </div>
680
+ </div>
681
+ <div className="mt-3 flex flex-wrap gap-1.5">
682
+ <Button
683
+ type="button"
684
+ variant={item.starred ? "secondary" : "outline"}
685
+ size="sm"
686
+ disabled={busy}
687
+ onClick={() =>
688
+ onReview(item.starred ? "unstar" : "star", item.id)
689
+ }
690
+ >
691
+ {t(
692
+ item.starred
693
+ ? "creativeContext.unstar"
694
+ : "creativeContext.star",
695
+ )}
696
+ </Button>
697
+ <Button
698
+ type="button"
699
+ variant={
700
+ item.curationRank === "exemplar" ? "secondary" : "outline"
701
+ }
702
+ size="sm"
703
+ disabled={busy}
704
+ onClick={() =>
705
+ onReview(
706
+ item.curationRank === "exemplar" ? "normal" : "exemplar",
707
+ item.id,
708
+ )
709
+ }
710
+ >
711
+ {t("creativeContext.exemplar")}
712
+ </Button>
713
+ <Button
714
+ type="button"
715
+ variant={
716
+ item.curationRank === "ignored" ? "secondary" : "outline"
717
+ }
718
+ size="sm"
719
+ disabled={busy}
720
+ onClick={() =>
721
+ onReview(
722
+ item.curationRank === "ignored" ? "normal" : "ignore",
723
+ item.id,
724
+ )
725
+ }
726
+ >
727
+ {t("creativeContext.ignore")}
728
+ </Button>
729
+ <Button
730
+ type="button"
731
+ variant="ghost"
732
+ size="sm"
733
+ disabled={busy}
734
+ onClick={() =>
735
+ onReview(
736
+ item.status === "deprecated" ? "restore" : "deprecate",
737
+ item.id,
738
+ )
739
+ }
740
+ >
741
+ {t(
742
+ item.status === "deprecated"
743
+ ? "creativeContext.restore"
744
+ : "creativeContext.deprecate",
745
+ )}
746
+ </Button>
747
+ </div>
748
+ {item.curationStatus === "review" ? (
749
+ <div className="mt-2 flex gap-1.5 border-t border-border/60 pt-2">
750
+ <Button
751
+ type="button"
752
+ variant="outline"
753
+ size="sm"
754
+ disabled={busy}
755
+ onClick={() => onReview("exclude", item.id)}
756
+ >
757
+ {t("creativeContext.exclude")}
758
+ </Button>
759
+ <Button
760
+ type="button"
761
+ size="sm"
762
+ disabled={busy}
763
+ onClick={() => onReview("approve", item.id)}
764
+ >
765
+ {t("creativeContext.approve")}
766
+ </Button>
767
+ </div>
768
+ ) : null}
769
+ </div>
770
+ </article>
771
+ ))}
772
+ </div>
773
+ </div>
774
+ );
775
+ }
776
+
777
+ export function CreativeContextPanel({
778
+ scope = "user",
779
+ canManageOrg = false,
780
+ scopeControl,
781
+ connectionsHref = "/agent#connections",
782
+ }: CreativeContextPanelProps) {
783
+ const t = useT();
784
+ const { formatNumber } = useFormatters();
785
+ const { data: org } = useOrg();
786
+ const [libraryScope, setLibraryScope] = useState<AgentPageScope>(scope);
787
+ const sourcesQuery = useCreativeContextSources({ limit: 100 });
788
+ const packsQuery = useCreativeContextPacks();
789
+ const brandProfileQuery = useCreativeContextBrandProfile();
790
+ const suggestionsQuery = useCreativeContextSuggestions();
791
+ const logoCandidatesQuery = useCanonicalLogoCandidates(
792
+ brandProfileQuery.data?.profile?.id,
793
+ suggestionsQuery.data?.capabilities.canonicalLogo === true,
794
+ );
795
+ const refreshSource = useRefreshCreativeContextSource();
796
+ const manageSource = useManageCreativeContextSource();
797
+ const uploadResource = useUploadResource();
798
+ const startImport = useStartCreativeContextImport();
799
+ const searchContext = useCreativeContextSearch();
800
+ const reviewItems = useReviewCreativeContextItems();
801
+ const publishBrandDna = usePublishCreativeContextBrandDna();
802
+ const proposeCanonicalLogo = useProposeCanonicalLogo();
803
+ const confirmCanonicalLogo = useConfirmCanonicalLogo();
804
+ const manageLayoutTemplate = useManageLayoutTemplate();
805
+ const contextState = useCreativeContextState();
806
+ const [query, setQuery] = useState("");
807
+ const [savingState, setSavingState] = useState(false);
808
+ const [stateError, setStateError] = useState<string | null>(null);
809
+ const [refreshMessage, setRefreshMessage] = useState<string | null>(null);
810
+ const [searchError, setSearchError] = useState<string | null>(null);
811
+ const [connectorKind, setConnectorKind] = useState<ConnectorKind | null>(
812
+ null,
813
+ );
814
+ const connectionProvider = connectionProviderForConnector(connectorKind);
815
+ const connectionsQuery = useCreativeContextConnections(connectionProvider);
816
+ const [selectedConnectionId, setSelectedConnectionId] = useState("");
817
+ const [sourceName, setSourceName] = useState("");
818
+ const [sourceReference, setSourceReference] = useState("");
819
+ const recommendationProvider =
820
+ recommendationProviderForConnector(connectorKind);
821
+ const recommendationsQuery = useCreativeContextRootRecommendations(
822
+ recommendationProvider,
823
+ selectedConnectionId || null,
824
+ connectorKind === "figma"
825
+ ? parseFigmaRecommendationBoundary(sourceReference)
826
+ : {},
827
+ );
828
+ const [selectedRecommendationIds, setSelectedRecommendationIds] = useState<
829
+ Set<string>
830
+ >(() => new Set());
831
+ const [uploadedFiles, setUploadedFiles] = useState<UploadedContextFile[]>([]);
832
+ const fileInputRef = useRef<HTMLInputElement>(null);
833
+ const folderInputRef = useRef<HTMLInputElement>(null);
834
+ const [setupError, setSetupError] = useState<string | null>(null);
835
+ const [previewSourceId, setPreviewSourceId] = useState<string | null>(null);
836
+ const [previewSourceName, setPreviewSourceName] = useState("");
837
+ const [selectedPreviewItemIds, setSelectedPreviewItemIds] = useState<
838
+ Set<string>
839
+ >(() => new Set());
840
+ const initializedPreviewSelectionRef = useRef<string | null>(null);
841
+ const [importJobId, setImportJobId] = useState<string | null>(null);
842
+ const [importTargetScope, setImportTargetScope] =
843
+ useState<AgentPageScope>("user");
844
+ const [completedJobId, setCompletedJobId] = useState<string | null>(null);
845
+ const [promotionPreview, setPromotionPreview] = useState<NonNullable<
846
+ Awaited<ReturnType<typeof manageSource.mutateAsync>>["promotionPreview"]
847
+ > | null>(null);
848
+ const [promotionSourceId, setPromotionSourceId] = useState<string | null>(
849
+ null,
850
+ );
851
+ const [promotionMessage, setPromotionMessage] = useState<string | null>(null);
852
+ const [lifecycleMessage, setLifecycleMessage] = useState<string | null>(null);
853
+ const [deleteSource, setDeleteSource] = useState<ContextSourceSummary | null>(
854
+ null,
855
+ );
856
+ const [hiddenSourceIds, setHiddenSourceIds] = useState<Set<string>>(
857
+ () => new Set(),
858
+ );
859
+ const [publishedMessage, setPublishedMessage] = useState<string | null>(null);
860
+ const [reviewSource, setReviewSource] = useState<ContextSourceSummary | null>(
861
+ null,
862
+ );
863
+ const [reviewedItems, setReviewedItems] = useState<ContextReviewItem[]>([]);
864
+ const [reviewError, setReviewError] = useState<string | null>(null);
865
+ const [selectedPackId, setSelectedPackId] = useState<string | null>(null);
866
+ const [suggestionError, setSuggestionError] = useState<string | null>(null);
867
+ const previewQuery = usePreviewCreativeContextImport(previewSourceId);
868
+ const importStatusQuery = useCreativeContextImportStatus(importJobId);
869
+ const packQuery = useCreativeContextPack(selectedPackId);
870
+
871
+ useEffect(() => setLibraryScope(scope), [scope]);
872
+
873
+ useEffect(() => {
874
+ if (!connectionProvider) {
875
+ setSelectedConnectionId("");
876
+ return;
877
+ }
878
+ setSelectedConnectionId(
879
+ connectionsQuery.data?.autoSelectedConnectionId ?? "",
880
+ );
881
+ }, [connectionProvider, connectionsQuery.data?.autoSelectedConnectionId]);
882
+
883
+ useEffect(() => {
884
+ setSelectedRecommendationIds(
885
+ new Set(
886
+ (recommendationsQuery.data?.recommendations ?? []).map(
887
+ (recommendation) => recommendation.externalId,
888
+ ),
889
+ ),
890
+ );
891
+ }, [recommendationsQuery.data?.recommendations]);
892
+
893
+ const sources = useMemo(
894
+ () =>
895
+ (sourcesQuery.data?.sources ?? []).filter(
896
+ (source) =>
897
+ source.status !== "archived" &&
898
+ !hiddenSourceIds.has(source.id) &&
899
+ isVisibleInScope(source.visibility, libraryScope),
900
+ ),
901
+ [hiddenSourceIds, libraryScope, sourcesQuery.data?.sources],
902
+ );
903
+ const packs = useMemo(
904
+ () =>
905
+ (packsQuery.data?.packs ?? []).filter(
906
+ (pack) =>
907
+ !pack.archivedAt && isVisibleInScope(pack.visibility, libraryScope),
908
+ ),
909
+ [libraryScope, packsQuery.data?.packs],
910
+ );
911
+ const activePack = packs.find(
912
+ (pack) => pack.id === contextState.state.currentPackId,
913
+ );
914
+ const selectedConnector = CONNECTORS.find(
915
+ (connector) => connector.kind === connectorKind,
916
+ );
917
+ const importJob = importStatusQuery.data?.job;
918
+ const importResult = importJob?.result as ContextImportJobResult | null;
919
+ const brandProposal = importResult?.inference?.brandDnaProposal;
920
+ const brandLayoutThumbnails = selectRenderableLayoutThumbnails(
921
+ brandProposal?.layoutThumbnails ?? [],
922
+ );
923
+ const brandVoicePreview =
924
+ brandProposal?.voiceDescriptors?.join(" · ") ?? brandProposal?.voiceLine;
925
+ const canManageScope = libraryScope === "user" || canManageOrg;
926
+ const proposalCapabilities = suggestionsQuery.data?.capabilities;
927
+ const logoCandidates = proposalCapabilities?.canonicalLogo
928
+ ? (logoCandidatesQuery.data?.candidates ?? [])
929
+ : [];
930
+ const proposedLayouts = (suggestionsQuery.data?.suggestions ?? []).filter(
931
+ (suggestion) =>
932
+ suggestion.kind === "layout-template" &&
933
+ (suggestion.status === "proposed" || suggestion.status === "promoted"),
934
+ );
935
+
936
+ useEffect(() => {
937
+ if (!previewSourceId || !previewQuery.data) return;
938
+ const defaults = previewQuery.data.smartDefaultExternalIds ?? [];
939
+ const initializationKey = `${previewSourceId}:${defaults.join("\u0000")}`;
940
+ if (initializedPreviewSelectionRef.current === initializationKey) return;
941
+ const discoveredIds = new Set(
942
+ previewQuery.data.items.map((item) => item.externalId),
943
+ );
944
+ setSelectedPreviewItemIds(
945
+ new Set(defaults.filter((externalId) => discoveredIds.has(externalId))),
946
+ );
947
+ initializedPreviewSelectionRef.current = initializationKey;
948
+ }, [previewQuery.data, previewSourceId]);
949
+
950
+ useEffect(() => {
951
+ if (importJob?.status !== "completed" || completedJobId === importJob.id)
952
+ return;
953
+ setCompletedJobId(importJob.id);
954
+ void sourcesQuery.refetch();
955
+ void packsQuery.refetch();
956
+ void brandProfileQuery.refetch();
957
+ if (importTargetScope === "org" && previewSourceId) {
958
+ setPromotionSourceId(previewSourceId);
959
+ void manageSource
960
+ .mutateAsync({
961
+ operation: "preview-promotion",
962
+ sourceId: previewSourceId,
963
+ })
964
+ .then((result) => setPromotionPreview(result.promotionPreview ?? null))
965
+ .catch(() => setPromotionMessage(t("creativeContext.saveFailed")));
966
+ }
967
+ }, [
968
+ brandProfileQuery,
969
+ completedJobId,
970
+ importJob,
971
+ importTargetScope,
972
+ manageSource,
973
+ packsQuery,
974
+ previewSourceId,
975
+ sourcesQuery,
976
+ t,
977
+ ]);
978
+
979
+ async function changeMode(mode: CreativeContextMode) {
980
+ if (mode === contextState.state.contextMode) return;
981
+ setSavingState(true);
982
+ setStateError(null);
983
+ try {
984
+ await contextState.setState(
985
+ mode === "off"
986
+ ? { contextMode: "off", currentPackId: null, pinnedPackId: null }
987
+ : { ...contextState.state, contextMode: "auto" },
988
+ );
989
+ } catch {
990
+ setStateError(t("creativeContext.stateSaveFailed"));
991
+ } finally {
992
+ setSavingState(false);
993
+ }
994
+ }
995
+
996
+ async function changePinnedPack(packId: string | null) {
997
+ setSavingState(true);
998
+ setStateError(null);
999
+ try {
1000
+ await contextState.setState({
1001
+ ...contextState.state,
1002
+ contextMode: "auto",
1003
+ pinnedPackId: packId,
1004
+ });
1005
+ } catch {
1006
+ setStateError(t("creativeContext.stateSaveFailed"));
1007
+ } finally {
1008
+ setSavingState(false);
1009
+ }
1010
+ }
1011
+
1012
+ function refresh(sourceId: string) {
1013
+ setRefreshMessage(null);
1014
+ refreshSource.mutate(
1015
+ { sourceId, mode: "incremental" },
1016
+ {
1017
+ onSuccess: () => setRefreshMessage(t("creativeContext.refreshed")),
1018
+ onError: () => setRefreshMessage(t("creativeContext.refreshFailed")),
1019
+ },
1020
+ );
1021
+ }
1022
+
1023
+ async function uploadFiles(files: File[]) {
1024
+ if (!files.length) return;
1025
+ setSetupError(null);
1026
+ try {
1027
+ const uploaded = await Promise.all(
1028
+ files.map(async (file) => {
1029
+ const relativePath =
1030
+ (file as File & { webkitRelativePath?: string })
1031
+ .webkitRelativePath || file.name;
1032
+ const safePath = relativePath
1033
+ .replace(/\.\./g, "")
1034
+ .replace(/[^a-zA-Z0-9._/-]/g, "-");
1035
+ const formData = new FormData();
1036
+ formData.append("file", file, file.name);
1037
+ formData.append(
1038
+ "path",
1039
+ `/creative-context/${Date.now()}-${safePath}`,
1040
+ );
1041
+ const resource = (await uploadResource.mutateAsync(formData)) as {
1042
+ id: string;
1043
+ content: string;
1044
+ url?: string;
1045
+ };
1046
+ const url = resource.url ?? resource.content;
1047
+ if (!url) throw new Error("Upload returned no file handle");
1048
+ return {
1049
+ id: resource.id,
1050
+ title: file.name,
1051
+ fileName: file.name,
1052
+ mimeType: file.type || "application/octet-stream",
1053
+ url,
1054
+ };
1055
+ }),
1056
+ );
1057
+ setUploadedFiles((current) => [...current, ...uploaded]);
1058
+ } catch {
1059
+ setSetupError(t("creativeContext.saveFailed"));
1060
+ }
1061
+ }
1062
+
1063
+ function chooseFiles(event: ChangeEvent<HTMLInputElement>) {
1064
+ void uploadFiles(Array.from(event.target.files ?? []));
1065
+ event.target.value = "";
1066
+ }
1067
+
1068
+ function dropFiles(event: DragEvent<HTMLDivElement>) {
1069
+ event.preventDefault();
1070
+ void uploadFiles(Array.from(event.dataTransfer.files));
1071
+ }
1072
+
1073
+ async function previewImport(event: FormEvent<HTMLFormElement>) {
1074
+ event.preventDefault();
1075
+ if (!selectedConnector || !sourceName.trim()) return;
1076
+ setSetupError(null);
1077
+ setImportJobId(null);
1078
+ setCompletedJobId(null);
1079
+ setPublishedMessage(null);
1080
+ try {
1081
+ const confirmedRecommendations = (
1082
+ recommendationsQuery.data?.recommendations ?? []
1083
+ ).filter((recommendation) =>
1084
+ selectedRecommendationIds.has(recommendation.externalId),
1085
+ );
1086
+ const result = await manageSource.mutateAsync({
1087
+ operation: "create",
1088
+ name: sourceName.trim(),
1089
+ kind: selectedConnector.kind,
1090
+ connectionId: selectedConnectionId || undefined,
1091
+ externalRef:
1092
+ selectedConnector.kind === "upload"
1093
+ ? `${uploadedFiles.length} files`
1094
+ : sourceReference.trim() ||
1095
+ (confirmedRecommendations.length
1096
+ ? `${selectedConnector.kind}:${confirmedRecommendations
1097
+ .map((recommendation) => recommendation.externalId)
1098
+ .join(",")}`
1099
+ : undefined),
1100
+ config: buildCreativeContextSourceConfig(
1101
+ selectedConnector.kind,
1102
+ sourceReference,
1103
+ uploadedFiles,
1104
+ confirmedRecommendations,
1105
+ ),
1106
+ });
1107
+ if (!result.source) throw new Error("Source creation returned no source");
1108
+ setImportTargetScope(libraryScope);
1109
+ setPreviewSourceName(result.source.name);
1110
+ setPreviewSourceId(result.source.id);
1111
+ setSelectedPreviewItemIds(new Set());
1112
+ initializedPreviewSelectionRef.current = null;
1113
+ setPromotionSourceId(null);
1114
+ setPromotionPreview(null);
1115
+ setPromotionMessage(null);
1116
+ setConnectorKind(null);
1117
+ setSourceName("");
1118
+ setSourceReference("");
1119
+ setUploadedFiles([]);
1120
+ await sourcesQuery.refetch();
1121
+ } catch {
1122
+ setSetupError(t("creativeContext.saveFailed"));
1123
+ }
1124
+ }
1125
+
1126
+ async function confirmPromotion() {
1127
+ if (!promotionSourceId || !promotionPreview) return;
1128
+ setPromotionMessage(null);
1129
+ try {
1130
+ await manageSource.mutateAsync({
1131
+ operation: "promote",
1132
+ sourceId: promotionSourceId,
1133
+ confirmation: {
1134
+ containerRef: promotionPreview.containerRef,
1135
+ boundaryHash: promotionPreview.boundaryHash,
1136
+ itemCount: promotionPreview.itemCount,
1137
+ },
1138
+ });
1139
+ setPromotionMessage(t("creativeContext.promotionComplete"));
1140
+ setPromotionPreview(null);
1141
+ setPromotionSourceId(null);
1142
+ await sourcesQuery.refetch();
1143
+ } catch {
1144
+ setPromotionMessage(t("creativeContext.saveFailed"));
1145
+ }
1146
+ }
1147
+
1148
+ async function pauseSource(source: ContextSourceSummary) {
1149
+ setLifecycleMessage(null);
1150
+ try {
1151
+ await manageSource.mutateAsync({
1152
+ operation: "update",
1153
+ sourceId: source.id,
1154
+ patch: { status: "paused" },
1155
+ });
1156
+ setLifecycleMessage(t("creativeContext.sourcePaused"));
1157
+ await sourcesQuery.refetch();
1158
+ } catch {
1159
+ setLifecycleMessage(t("creativeContext.saveFailed"));
1160
+ }
1161
+ }
1162
+
1163
+ async function restoreSource(source: ContextSourceSummary) {
1164
+ setLifecycleMessage(null);
1165
+ try {
1166
+ await manageSource.mutateAsync({
1167
+ operation: "restore",
1168
+ sourceId: source.id,
1169
+ });
1170
+ setLifecycleMessage(t("creativeContext.sourceRestored"));
1171
+ await sourcesQuery.refetch();
1172
+ } catch {
1173
+ setLifecycleMessage(t("creativeContext.saveFailed"));
1174
+ }
1175
+ }
1176
+
1177
+ async function previewSourcePromotion(source: ContextSourceSummary) {
1178
+ setPromotionMessage(null);
1179
+ setPromotionPreview(null);
1180
+ setPromotionSourceId(source.id);
1181
+ try {
1182
+ const result = await manageSource.mutateAsync({
1183
+ operation: "preview-promotion",
1184
+ sourceId: source.id,
1185
+ });
1186
+ setPromotionPreview(result.promotionPreview ?? null);
1187
+ } catch {
1188
+ setPromotionSourceId(null);
1189
+ setPromotionMessage(t("creativeContext.saveFailed"));
1190
+ }
1191
+ }
1192
+
1193
+ async function confirmDeleteSource() {
1194
+ if (!deleteSource) return;
1195
+ const sourceId = deleteSource.id;
1196
+ setLifecycleMessage(null);
1197
+ setDeleteSource(null);
1198
+ setHiddenSourceIds((current) => new Set(current).add(sourceId));
1199
+ try {
1200
+ await manageSource.mutateAsync({ operation: "delete", sourceId });
1201
+ setLifecycleMessage(t("creativeContext.deletionQueued"));
1202
+ void sourcesQuery.refetch();
1203
+ } catch {
1204
+ setHiddenSourceIds((current) => {
1205
+ const next = new Set(current);
1206
+ next.delete(sourceId);
1207
+ return next;
1208
+ });
1209
+ setLifecycleMessage(t("creativeContext.saveFailed"));
1210
+ }
1211
+ }
1212
+
1213
+ async function beginImport() {
1214
+ if (!previewSourceId || !selectedPreviewItemIds.size) return;
1215
+ setSetupError(null);
1216
+ try {
1217
+ const result = await startImport.mutateAsync({
1218
+ sourceId: previewSourceId,
1219
+ mode: "incremental",
1220
+ itemExternalIds: (previewQuery.data?.items ?? [])
1221
+ .map((item) => item.externalId)
1222
+ .filter((externalId) => selectedPreviewItemIds.has(externalId)),
1223
+ });
1224
+ setImportJobId(result.job.id);
1225
+ } catch {
1226
+ setSetupError(t("creativeContext.importFailed"));
1227
+ }
1228
+ }
1229
+
1230
+ async function publishProposal() {
1231
+ const importResult = importJob?.result as ContextImportJobResult | null;
1232
+ const proposal = importResult?.inference?.brandDnaProposal;
1233
+ if (!proposal) return;
1234
+ setPublishedMessage(null);
1235
+ try {
1236
+ const result = await publishBrandDna.mutateAsync({
1237
+ profileId: proposal.profileId,
1238
+ proposalVersionId: proposal.dnaVersionId,
1239
+ confirmation: {
1240
+ proposalVersionId: proposal.dnaVersionId,
1241
+ contentHash: proposal.contentHash,
1242
+ },
1243
+ });
1244
+ setPublishedMessage(
1245
+ `${t("creativeContext.brandContextPublished")} · ${result.profile.name} · v${result.dna.versionNumber}`,
1246
+ );
1247
+ await brandProfileQuery.refetch();
1248
+ } catch {
1249
+ setPublishedMessage(t("creativeContext.saveFailed"));
1250
+ }
1251
+ }
1252
+
1253
+ async function openItemCuration(
1254
+ source: ContextSourceSummary,
1255
+ queue: "restricted" | "all" = "all",
1256
+ ) {
1257
+ setReviewSource(source);
1258
+ setReviewError(null);
1259
+ try {
1260
+ const result = await reviewItems.mutateAsync({
1261
+ sourceId: source.id,
1262
+ operation: "list",
1263
+ queue,
1264
+ limit: 100,
1265
+ });
1266
+ setReviewedItems(result.items);
1267
+ } catch {
1268
+ setReviewedItems([]);
1269
+ setReviewError(t("creativeContext.unavailable"));
1270
+ }
1271
+ }
1272
+
1273
+ async function reviewContextItem(
1274
+ operation: ContextItemReviewOperation,
1275
+ itemId: string,
1276
+ ) {
1277
+ if (!reviewSource) return;
1278
+ setReviewError(null);
1279
+ try {
1280
+ await reviewItems.mutateAsync({
1281
+ sourceId: reviewSource.id,
1282
+ operation,
1283
+ itemIds: [itemId],
1284
+ });
1285
+ const result = await reviewItems.mutateAsync({
1286
+ sourceId: reviewSource.id,
1287
+ operation: "list",
1288
+ queue: "all",
1289
+ limit: 100,
1290
+ });
1291
+ setReviewedItems(result.items);
1292
+ await sourcesQuery.refetch();
1293
+ } catch {
1294
+ setReviewError(t("creativeContext.saveFailed"));
1295
+ }
1296
+ }
1297
+
1298
+ async function chooseCanonicalLogo(
1299
+ candidate: NonNullable<
1300
+ typeof logoCandidatesQuery.data
1301
+ >["candidates"][number],
1302
+ ) {
1303
+ setSuggestionError(null);
1304
+ try {
1305
+ const suggestion = await proposeCanonicalLogo.mutateAsync({
1306
+ profileId: brandProfileQuery.data?.profile?.id,
1307
+ itemId: candidate.itemId,
1308
+ itemVersionId: candidate.itemVersionId,
1309
+ reason: "Selected from the ranked Library review card",
1310
+ payload: { mediaId: candidate.mediaId },
1311
+ });
1312
+ await confirmCanonicalLogo.mutateAsync({
1313
+ suggestionId: suggestion.id,
1314
+ decision: "confirm",
1315
+ });
1316
+ await suggestionsQuery.refetch();
1317
+ } catch {
1318
+ setSuggestionError(t("creativeContext.saveFailed"));
1319
+ }
1320
+ }
1321
+
1322
+ async function decideLayoutSuggestion(
1323
+ suggestionId: string,
1324
+ operation: "promote" | "demote" | "reject",
1325
+ ) {
1326
+ setSuggestionError(null);
1327
+ try {
1328
+ await manageLayoutTemplate.mutateAsync({ suggestionId, operation });
1329
+ await suggestionsQuery.refetch();
1330
+ } catch {
1331
+ setSuggestionError(t("creativeContext.saveFailed"));
1332
+ }
1333
+ }
1334
+
1335
+ async function search(event: FormEvent<HTMLFormElement>) {
1336
+ event.preventDefault();
1337
+ const searchText = query.trim();
1338
+ if (!searchText || !sources.length) return;
1339
+ setSearchError(null);
1340
+ try {
1341
+ const result = await searchContext.mutateAsync({
1342
+ query: searchText,
1343
+ sourceIds: sources.map((source) => source.id),
1344
+ packId: contextState.state.pinnedPackId ?? undefined,
1345
+ limit: 20,
1346
+ snapshot: true,
1347
+ });
1348
+ if (result.contextPackId && contextState.state.contextMode === "auto") {
1349
+ await contextState.setState({
1350
+ ...contextState.state,
1351
+ currentPackId: result.contextPackId,
1352
+ });
1353
+ }
1354
+ } catch {
1355
+ setSearchError(t("creativeContext.unavailable"));
1356
+ }
1357
+ }
1358
+
1359
+ const loading =
1360
+ sourcesQuery.isLoading || packsQuery.isLoading || contextState.isLoading;
1361
+ const unavailable = sourcesQuery.error || packsQuery.error;
1362
+
1363
+ return (
1364
+ <div className="mx-auto flex w-full max-w-5xl flex-col gap-7 p-6 lg:p-10">
1365
+ <header className="flex flex-wrap items-start justify-between gap-4 border-b border-border/70 pb-5">
1366
+ <div>
1367
+ <h1 className="text-2xl font-semibold tracking-tight">
1368
+ {t("creativeContext.title")}
1369
+ </h1>
1370
+ <p className="mt-1 max-w-2xl text-sm text-muted-foreground">
1371
+ {t("creativeContext.description")}
1372
+ </p>
1373
+ </div>
1374
+ <div className="flex flex-wrap items-center gap-2">
1375
+ {scopeControl ??
1376
+ (org?.orgId ? (
1377
+ <ScopeControl scope={libraryScope} onChange={setLibraryScope} />
1378
+ ) : null)}
1379
+ <CreativeContextChip state={contextState.state} packs={packs} />
1380
+ </div>
1381
+ </header>
1382
+
1383
+ {loading ? (
1384
+ <div className="space-y-3" aria-label={t("creativeContext.loading")}>
1385
+ <Skeleton className="h-24 w-full" />
1386
+ <Skeleton className="h-40 w-full" />
1387
+ <Skeleton className="h-32 w-full" />
1388
+ </div>
1389
+ ) : unavailable ? (
1390
+ <div className="flex items-center gap-3 rounded-md border border-dashed border-border p-6 text-sm text-muted-foreground">
1391
+ <IconAlertTriangle className="size-5" />
1392
+ {t("creativeContext.unavailable")}
1393
+ </div>
1394
+ ) : (
1395
+ <>
1396
+ <section className="space-y-3">
1397
+ <div>
1398
+ <h2 className="text-sm font-semibold">
1399
+ {t("creativeContext.modeLabel")}
1400
+ </h2>
1401
+ <p className="mt-1 text-xs text-muted-foreground">
1402
+ {activePack
1403
+ ? `${t("creativeContext.activePack")}: ${activePack.name}`
1404
+ : t("creativeContext.noActivePack")}
1405
+ </p>
1406
+ </div>
1407
+ <div className="flex flex-col gap-2 sm:flex-row" role="radiogroup">
1408
+ <ContextModeButton
1409
+ mode="auto"
1410
+ activeMode={contextState.state.contextMode}
1411
+ label={t("creativeContext.automatic")}
1412
+ description={t("creativeContext.automaticDescription")}
1413
+ disabled={savingState}
1414
+ onSelect={(mode) => void changeMode(mode)}
1415
+ />
1416
+ <ContextModeButton
1417
+ mode="off"
1418
+ activeMode={contextState.state.contextMode}
1419
+ label={t("creativeContext.off")}
1420
+ description={t("creativeContext.offDescription")}
1421
+ disabled={savingState}
1422
+ onSelect={(mode) => void changeMode(mode)}
1423
+ />
1424
+ </div>
1425
+ {stateError ? (
1426
+ <p className="text-xs text-destructive">{stateError}</p>
1427
+ ) : null}
1428
+ </section>
1429
+
1430
+ {brandProfileQuery.data?.profile && brandProfileQuery.data.dna ? (
1431
+ <section className="border-t border-border/70 pt-6">
1432
+ <div className="rounded-md border border-border p-4">
1433
+ <div className="flex flex-wrap items-center justify-between gap-2">
1434
+ <div>
1435
+ <h2 className="text-sm font-semibold">
1436
+ {t("creativeContext.publishedBrandContext")}
1437
+ </h2>
1438
+ <p className="mt-0.5 text-xs text-muted-foreground">
1439
+ {brandProfileQuery.data.profile.name}
1440
+ </p>
1441
+ </div>
1442
+ <Badge variant="secondary">
1443
+ v{brandProfileQuery.data.dna.versionNumber}
1444
+ </Badge>
1445
+ </div>
1446
+ <p className="mt-3 text-sm text-muted-foreground">
1447
+ {brandProfileQuery.data.dna.payload.summary}
1448
+ </p>
1449
+ </div>
1450
+ </section>
1451
+ ) : null}
1452
+
1453
+ {logoCandidates.length || proposedLayouts.length ? (
1454
+ <section className="border-t border-border/70 pt-6">
1455
+ <div className="flex items-center gap-2">
1456
+ <IconSparkles className="size-5 text-muted-foreground" />
1457
+ <h2 className="text-lg font-semibold">
1458
+ {t("creativeContext.suggestions")}
1459
+ </h2>
1460
+ </div>
1461
+ {logoCandidates.length ? (
1462
+ <div className="mt-4">
1463
+ <h3 className="flex items-center gap-2 text-sm font-semibold">
1464
+ <IconPhoto className="size-4 text-muted-foreground" />
1465
+ {t("creativeContext.logo")}
1466
+ </h3>
1467
+ <div className="mt-2 grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
1468
+ {logoCandidates.map((candidate) => (
1469
+ <article
1470
+ key={candidate.mediaId}
1471
+ className="overflow-hidden rounded-md border border-border"
1472
+ >
1473
+ <div className="flex h-28 items-center justify-center bg-muted/40 p-3">
1474
+ <img
1475
+ src={candidate.thumbnailUrl}
1476
+ alt={candidate.title}
1477
+ className="max-h-full max-w-full object-contain"
1478
+ />
1479
+ </div>
1480
+ <div className="flex items-center justify-between gap-2 p-3">
1481
+ <p className="min-w-0 truncate text-sm font-medium">
1482
+ {candidate.title}
1483
+ </p>
1484
+ <Button
1485
+ type="button"
1486
+ size="sm"
1487
+ variant="outline"
1488
+ disabled={
1489
+ proposeCanonicalLogo.isPending ||
1490
+ confirmCanonicalLogo.isPending
1491
+ }
1492
+ onClick={() => void chooseCanonicalLogo(candidate)}
1493
+ >
1494
+ <IconCheck />
1495
+ {t("creativeContext.approve")}
1496
+ </Button>
1497
+ </div>
1498
+ </article>
1499
+ ))}
1500
+ </div>
1501
+ </div>
1502
+ ) : null}
1503
+ {proposedLayouts.length &&
1504
+ proposalCapabilities?.layoutTemplate ? (
1505
+ <div className="mt-5">
1506
+ <h3 className="flex items-center gap-2 text-sm font-semibold">
1507
+ <IconLayout className="size-4 text-muted-foreground" />
1508
+ {t("creativeContext.layouts")}
1509
+ </h3>
1510
+ <div className="mt-2 divide-y divide-border/70 rounded-md border border-border">
1511
+ {proposedLayouts.map((suggestion) => (
1512
+ <article
1513
+ key={suggestion.id}
1514
+ className="flex flex-wrap items-center justify-between gap-3 p-3"
1515
+ >
1516
+ <div className="min-w-0">
1517
+ <p className="truncate text-sm font-medium">
1518
+ {suggestion.reason ?? suggestion.itemId}
1519
+ </p>
1520
+ <p className="mt-0.5 text-xs text-muted-foreground">
1521
+ {suggestion.status}
1522
+ </p>
1523
+ </div>
1524
+ <div className="flex items-center gap-2">
1525
+ {suggestion.status === "proposed" ? (
1526
+ <>
1527
+ <Button
1528
+ type="button"
1529
+ size="sm"
1530
+ disabled={manageLayoutTemplate.isPending}
1531
+ onClick={() =>
1532
+ void decideLayoutSuggestion(
1533
+ suggestion.id,
1534
+ "promote",
1535
+ )
1536
+ }
1537
+ >
1538
+ <IconCheck />
1539
+ {t("creativeContext.approve")}
1540
+ </Button>
1541
+ <Button
1542
+ type="button"
1543
+ size="sm"
1544
+ variant="outline"
1545
+ disabled={manageLayoutTemplate.isPending}
1546
+ onClick={() =>
1547
+ void decideLayoutSuggestion(
1548
+ suggestion.id,
1549
+ "reject",
1550
+ )
1551
+ }
1552
+ >
1553
+ {t("creativeContext.exclude")}
1554
+ </Button>
1555
+ </>
1556
+ ) : (
1557
+ <Button
1558
+ type="button"
1559
+ size="sm"
1560
+ variant="outline"
1561
+ disabled={manageLayoutTemplate.isPending}
1562
+ onClick={() =>
1563
+ void decideLayoutSuggestion(
1564
+ suggestion.id,
1565
+ "demote",
1566
+ )
1567
+ }
1568
+ >
1569
+ {t("creativeContext.deprecate")}
1570
+ </Button>
1571
+ )}
1572
+ </div>
1573
+ </article>
1574
+ ))}
1575
+ </div>
1576
+ </div>
1577
+ ) : null}
1578
+ {suggestionError ? (
1579
+ <p className="mt-3 text-sm text-destructive">
1580
+ {suggestionError}
1581
+ </p>
1582
+ ) : null}
1583
+ </section>
1584
+ ) : null}
1585
+
1586
+ <section className="border-t border-border/70 pt-6">
1587
+ <div>
1588
+ <h2 className="text-lg font-semibold">
1589
+ {t("creativeContext.addSource")}
1590
+ </h2>
1591
+ <p className="mt-1 text-sm text-muted-foreground">
1592
+ {t("creativeContext.sourcesDescription")}
1593
+ </p>
1594
+ </div>
1595
+ <div className="mt-4 grid gap-2 sm:grid-cols-2 lg:grid-cols-5">
1596
+ {CONNECTORS.map((connector) => {
1597
+ const Icon = connector.icon;
1598
+ return (
1599
+ <button
1600
+ key={connector.kind}
1601
+ type="button"
1602
+ disabled={!canManageScope}
1603
+ onClick={() => {
1604
+ setConnectorKind(connector.kind);
1605
+ setSourceName(connector.label);
1606
+ setSourceReference("");
1607
+ setUploadedFiles([]);
1608
+ setSelectedConnectionId("");
1609
+ setSelectedRecommendationIds(new Set());
1610
+ setSetupError(null);
1611
+ }}
1612
+ 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"
1613
+ >
1614
+ <Icon className="size-5 text-muted-foreground" />
1615
+ <span className="text-sm font-medium">
1616
+ {connector.label}
1617
+ </span>
1618
+ </button>
1619
+ );
1620
+ })}
1621
+ </div>
1622
+ {selectedConnector ? (
1623
+ <form
1624
+ className="mt-4 rounded-md border border-border p-4"
1625
+ onSubmit={(event) => void previewImport(event)}
1626
+ >
1627
+ {connectionProvider ? (
1628
+ <div className="mb-3">
1629
+ {connectionsQuery.isLoading ? (
1630
+ <Skeleton className="h-9 w-full" />
1631
+ ) : connectionsQuery.data?.needsSetup ? (
1632
+ <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">
1633
+ <span>{t("creativeContext.setupConnection")}</span>
1634
+ <Button
1635
+ asChild
1636
+ type="button"
1637
+ variant="outline"
1638
+ size="sm"
1639
+ >
1640
+ <a
1641
+ href={
1642
+ connectionsQuery.data.connectPath ||
1643
+ connectionsQuery.data.connectionsPath ||
1644
+ connectionsHref
1645
+ }
1646
+ >
1647
+ {t("creativeContext.connectProvider")}
1648
+ <IconArrowUpRight />
1649
+ </a>
1650
+ </Button>
1651
+ </div>
1652
+ ) : connectionsQuery.data?.needsPicker ? (
1653
+ <label className="block space-y-1.5 text-xs font-medium">
1654
+ <span>{t("creativeContext.chooseConnection")}</span>
1655
+ <Select
1656
+ value={selectedConnectionId}
1657
+ onValueChange={setSelectedConnectionId}
1658
+ >
1659
+ <SelectTrigger>
1660
+ <SelectValue
1661
+ placeholder={t(
1662
+ "creativeContext.chooseConnection",
1663
+ )}
1664
+ />
1665
+ </SelectTrigger>
1666
+ <SelectContent>
1667
+ {connectionsQuery.data.connections.map(
1668
+ (connection) => (
1669
+ <SelectItem
1670
+ key={connection.connectionId}
1671
+ value={connection.connectionId}
1672
+ >
1673
+ {connection.label}
1674
+ </SelectItem>
1675
+ ),
1676
+ )}
1677
+ </SelectContent>
1678
+ </Select>
1679
+ </label>
1680
+ ) : null}
1681
+ </div>
1682
+ ) : null}
1683
+ {recommendationProvider && selectedConnectionId ? (
1684
+ <div className="mb-3 rounded-md border border-border p-3">
1685
+ {recommendationsQuery.isLoading ? (
1686
+ <Skeleton className="h-16 w-full" />
1687
+ ) : recommendationsQuery.data?.recommendations.length ? (
1688
+ <div className="space-y-2">
1689
+ <div className="flex items-center justify-between gap-3 text-xs text-muted-foreground">
1690
+ <span>
1691
+ {t("creativeContext.discoveredItems", {
1692
+ count: formatNumber(
1693
+ recommendationsQuery.data.recommendations
1694
+ .length,
1695
+ ),
1696
+ })}
1697
+ </span>
1698
+ <span>
1699
+ {t("creativeContext.selectedItems", {
1700
+ count: formatNumber(
1701
+ selectedRecommendationIds.size,
1702
+ ),
1703
+ })}
1704
+ </span>
1705
+ </div>
1706
+ <div className="max-h-52 divide-y divide-border/60 overflow-y-auto">
1707
+ {recommendationsQuery.data.recommendations.map(
1708
+ (recommendation) => (
1709
+ <label
1710
+ key={recommendation.externalId}
1711
+ className="flex cursor-pointer items-start gap-3 py-2"
1712
+ >
1713
+ <Checkbox
1714
+ className="mt-0.5"
1715
+ checked={selectedRecommendationIds.has(
1716
+ recommendation.externalId,
1717
+ )}
1718
+ onCheckedChange={(checked) =>
1719
+ setSelectedRecommendationIds((current) => {
1720
+ const next = new Set(current);
1721
+ if (checked) {
1722
+ next.add(recommendation.externalId);
1723
+ } else {
1724
+ next.delete(recommendation.externalId);
1725
+ }
1726
+ return next;
1727
+ })
1728
+ }
1729
+ />
1730
+ <span className="min-w-0">
1731
+ <span className="block truncate text-sm">
1732
+ {recommendation.title}
1733
+ </span>
1734
+ <span className="block text-xs text-muted-foreground">
1735
+ {recommendation.containerRef ??
1736
+ recommendation.kind}
1737
+ </span>
1738
+ </span>
1739
+ </label>
1740
+ ),
1741
+ )}
1742
+ </div>
1743
+ </div>
1744
+ ) : (
1745
+ <p className="text-xs text-muted-foreground">
1746
+ {recommendationsQuery.data?.unavailableReason ??
1747
+ t("creativeContext.unavailable")}
1748
+ </p>
1749
+ )}
1750
+ </div>
1751
+ ) : null}
1752
+ <div className="grid gap-3 sm:grid-cols-2">
1753
+ <label className="space-y-1.5 text-xs font-medium">
1754
+ <span>{t("creativeContext.sourceName")}</span>
1755
+ <Input
1756
+ value={sourceName}
1757
+ onChange={(event) => setSourceName(event.target.value)}
1758
+ required
1759
+ />
1760
+ </label>
1761
+ {selectedConnector.kind === "upload" ? (
1762
+ <div className="space-y-1.5 text-xs font-medium">
1763
+ <span>{t("creativeContext.sourceReference")}</span>
1764
+ <input
1765
+ ref={fileInputRef}
1766
+ type="file"
1767
+ multiple
1768
+ 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"
1769
+ className="hidden"
1770
+ onChange={chooseFiles}
1771
+ />
1772
+ <input
1773
+ ref={folderInputRef}
1774
+ type="file"
1775
+ multiple
1776
+ className="hidden"
1777
+ onChange={chooseFiles}
1778
+ {...({ webkitdirectory: "", directory: "" } as {
1779
+ webkitdirectory: string;
1780
+ directory: string;
1781
+ })}
1782
+ />
1783
+ <div
1784
+ onDragOver={(event) => event.preventDefault()}
1785
+ onDrop={dropFiles}
1786
+ className="rounded-md border border-dashed border-border p-4"
1787
+ >
1788
+ <p className="text-xs text-muted-foreground">
1789
+ {t("creativeContext.dropFiles")}
1790
+ </p>
1791
+ <div className="mt-3 flex flex-wrap gap-2">
1792
+ <Button
1793
+ type="button"
1794
+ variant="outline"
1795
+ size="sm"
1796
+ onClick={() => fileInputRef.current?.click()}
1797
+ >
1798
+ {t("creativeContext.chooseFiles")}
1799
+ </Button>
1800
+ <Button
1801
+ type="button"
1802
+ variant="outline"
1803
+ size="sm"
1804
+ onClick={() => folderInputRef.current?.click()}
1805
+ >
1806
+ {t("creativeContext.chooseFolder")}
1807
+ </Button>
1808
+ </div>
1809
+ {uploadedFiles.length ? (
1810
+ <ul className="mt-3 space-y-1 text-xs text-muted-foreground">
1811
+ {uploadedFiles.map((file) => (
1812
+ <li key={file.id} className="truncate">
1813
+ {file.fileName}
1814
+ </li>
1815
+ ))}
1816
+ </ul>
1817
+ ) : null}
1818
+ </div>
1819
+ </div>
1820
+ ) : (
1821
+ <label className="space-y-1.5 text-xs font-medium">
1822
+ <span>{t("creativeContext.sourceReference")}</span>
1823
+ {selectedConnector.kind === "website" ||
1824
+ selectedConnector.kind === "figma" ||
1825
+ selectedConnector.kind === "notion" ? (
1826
+ <Textarea
1827
+ value={sourceReference}
1828
+ onChange={(event) =>
1829
+ setSourceReference(event.target.value)
1830
+ }
1831
+ placeholder={selectedConnector.referencePlaceholder}
1832
+ required={selectedConnector.referenceRequired}
1833
+ rows={3}
1834
+ />
1835
+ ) : (
1836
+ <Input
1837
+ value={sourceReference}
1838
+ onChange={(event) =>
1839
+ setSourceReference(event.target.value)
1840
+ }
1841
+ placeholder={selectedConnector.referencePlaceholder}
1842
+ required={selectedConnector.referenceRequired}
1843
+ />
1844
+ )}
1845
+ </label>
1846
+ )}
1847
+ </div>
1848
+ <div className="mt-3 flex justify-end gap-2">
1849
+ <Button
1850
+ type="button"
1851
+ variant="ghost"
1852
+ onClick={() => setConnectorKind(null)}
1853
+ >
1854
+ {t("creativeContext.cancel")}
1855
+ </Button>
1856
+ <Button
1857
+ type="submit"
1858
+ disabled={
1859
+ manageSource.isPending ||
1860
+ uploadResource.isPending ||
1861
+ (selectedConnector.kind === "upload" &&
1862
+ !uploadedFiles.length) ||
1863
+ (Boolean(connectionProvider) && !selectedConnectionId) ||
1864
+ (selectedConnector.referenceRequired &&
1865
+ !sourceReference.trim()) ||
1866
+ (Boolean(recommendationProvider) &&
1867
+ !sourceReference.trim() &&
1868
+ !selectedRecommendationIds.size)
1869
+ }
1870
+ >
1871
+ <IconFileImport />
1872
+ {t("creativeContext.preview")}
1873
+ </Button>
1874
+ </div>
1875
+ </form>
1876
+ ) : null}
1877
+
1878
+ {previewSourceId ? (
1879
+ <div className="mt-4 rounded-md border border-border p-4">
1880
+ <div className="flex flex-wrap items-start justify-between gap-3">
1881
+ <div>
1882
+ <h3 className="text-sm font-semibold">
1883
+ {previewSourceName}
1884
+ </h3>
1885
+ {previewQuery.isLoading ? (
1886
+ <p className="mt-0.5 text-xs text-muted-foreground">
1887
+ {t("creativeContext.loading")}
1888
+ </p>
1889
+ ) : (
1890
+ <div className="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-xs text-muted-foreground">
1891
+ <span>
1892
+ {t("creativeContext.discoveredItems", {
1893
+ count: formatNumber(
1894
+ previewQuery.data?.total ??
1895
+ previewQuery.data?.items.length ??
1896
+ 0,
1897
+ ),
1898
+ })}
1899
+ </span>
1900
+ <span>
1901
+ {t("creativeContext.selectedItems", {
1902
+ count: formatNumber(selectedPreviewItemIds.size),
1903
+ })}
1904
+ </span>
1905
+ </div>
1906
+ )}
1907
+ </div>
1908
+ <Button
1909
+ type="button"
1910
+ disabled={
1911
+ previewQuery.isLoading ||
1912
+ !previewQuery.data?.items.length ||
1913
+ !selectedPreviewItemIds.size ||
1914
+ startImport.isPending ||
1915
+ importJob?.status === "queued" ||
1916
+ importJob?.status === "running"
1917
+ }
1918
+ onClick={() => void beginImport()}
1919
+ >
1920
+ <IconFileImport />
1921
+ {startImport.isPending
1922
+ ? t("creativeContext.importing")
1923
+ : t("creativeContext.startImport")}
1924
+ </Button>
1925
+ </div>
1926
+ {previewQuery.data?.items.length ? (
1927
+ <div className="mt-3">
1928
+ <div className="flex items-center gap-2 border-y border-border/60 py-2">
1929
+ <Button
1930
+ type="button"
1931
+ variant="ghost"
1932
+ size="sm"
1933
+ onClick={() =>
1934
+ setSelectedPreviewItemIds(
1935
+ new Set(
1936
+ previewQuery.data?.items.map(
1937
+ (item) => item.externalId,
1938
+ ) ?? [],
1939
+ ),
1940
+ )
1941
+ }
1942
+ >
1943
+ {t("creativeContext.selectAll")}
1944
+ </Button>
1945
+ <Button
1946
+ type="button"
1947
+ variant="ghost"
1948
+ size="sm"
1949
+ onClick={() => setSelectedPreviewItemIds(new Set())}
1950
+ >
1951
+ {t("creativeContext.clearSelection")}
1952
+ </Button>
1953
+ </div>
1954
+ <div className="max-h-80 divide-y divide-border/60 overflow-y-auto">
1955
+ {previewQuery.data.items.map((item) => (
1956
+ <label
1957
+ key={item.externalId}
1958
+ className="flex cursor-pointer items-start gap-3 py-2"
1959
+ >
1960
+ <Checkbox
1961
+ className="mt-0.5"
1962
+ checked={selectedPreviewItemIds.has(
1963
+ item.externalId,
1964
+ )}
1965
+ onCheckedChange={(checked) =>
1966
+ setSelectedPreviewItemIds((current) => {
1967
+ const next = new Set(current);
1968
+ if (checked) next.add(item.externalId);
1969
+ else next.delete(item.externalId);
1970
+ return next;
1971
+ })
1972
+ }
1973
+ />
1974
+ <span className="min-w-0">
1975
+ <span className="block truncate text-sm">
1976
+ {item.title}
1977
+ </span>
1978
+ <span className="block text-xs text-muted-foreground">
1979
+ {item.kind}
1980
+ </span>
1981
+ </span>
1982
+ </label>
1983
+ ))}
1984
+ </div>
1985
+ </div>
1986
+ ) : null}
1987
+ </div>
1988
+ ) : null}
1989
+
1990
+ {importJob ? (
1991
+ <div className="mt-4 rounded-md border border-border p-4">
1992
+ <div className="flex items-center gap-2">
1993
+ {importJob.status === "completed" ? (
1994
+ <IconCheck className="size-5 text-emerald-600" />
1995
+ ) : importJob.status === "failed" ? (
1996
+ <IconAlertTriangle className="size-5 text-destructive" />
1997
+ ) : (
1998
+ <IconRefresh className="size-5 animate-spin text-muted-foreground" />
1999
+ )}
2000
+ <h3 className="text-sm font-semibold">
2001
+ {importJob.status === "completed"
2002
+ ? t("creativeContext.importComplete")
2003
+ : importJob.status === "failed"
2004
+ ? t("creativeContext.importFailed")
2005
+ : t("creativeContext.importing")}
2006
+ </h3>
2007
+ </div>
2008
+ {importJob.status === "completed" && brandProposal ? (
2009
+ <div className="mt-4 rounded-md bg-muted/50 p-4">
2010
+ <div className="flex items-start gap-3">
2011
+ <IconSparkles className="mt-0.5 size-5 text-muted-foreground" />
2012
+ <div className="min-w-0 flex-1">
2013
+ <h4 className="text-sm font-semibold">
2014
+ {t("creativeContext.brandDnaTitle")}
2015
+ </h4>
2016
+ <p className="mt-1 text-sm text-muted-foreground">
2017
+ {brandProposal.summary}
2018
+ </p>
2019
+ {brandProposal.colors.length ? (
2020
+ <div className="mt-4">
2021
+ <p className="text-xs font-medium">
2022
+ {t("creativeContext.colors")}
2023
+ </p>
2024
+ <div className="mt-2 flex flex-wrap gap-2">
2025
+ {brandProposal.colors.map((color) => (
2026
+ <div
2027
+ key={color}
2028
+ className="flex items-center gap-1.5 rounded border border-border bg-background px-2 py-1 text-xs"
2029
+ >
2030
+ <span
2031
+ className="size-4 rounded-sm border border-black/10"
2032
+ style={{ backgroundColor: color }}
2033
+ />
2034
+ {color}
2035
+ </div>
2036
+ ))}
2037
+ </div>
2038
+ </div>
2039
+ ) : null}
2040
+ {brandProposal.fonts.length ? (
2041
+ <div className="mt-4">
2042
+ <p className="text-xs font-medium">
2043
+ {t("creativeContext.fonts")}
2044
+ </p>
2045
+ <div className="mt-2 flex flex-wrap gap-2">
2046
+ {brandProposal.fonts.map((font) => (
2047
+ <Badge key={font} variant="outline">
2048
+ {font}
2049
+ </Badge>
2050
+ ))}
2051
+ </div>
2052
+ </div>
2053
+ ) : null}
2054
+ {brandLayoutThumbnails.length ? (
2055
+ <div className="mt-4">
2056
+ <p className="text-xs font-medium">
2057
+ {t("creativeContext.layouts")}
2058
+ </p>
2059
+ <div className="mt-2 grid grid-cols-3 gap-2">
2060
+ {brandLayoutThumbnails.map((thumbnail) => (
2061
+ <AccessScopedThumbnail
2062
+ key={thumbnail.itemVersionId}
2063
+ itemId={thumbnail.itemId}
2064
+ itemVersionId={thumbnail.itemVersionId}
2065
+ className="aspect-video w-full rounded border border-border object-cover"
2066
+ />
2067
+ ))}
2068
+ </div>
2069
+ </div>
2070
+ ) : null}
2071
+ {brandVoicePreview ? (
2072
+ <div className="mt-4">
2073
+ <p className="text-xs font-medium">
2074
+ {t("creativeContext.voice")}
2075
+ </p>
2076
+ <blockquote className="mt-1 border-s-2 border-border ps-3 text-sm text-muted-foreground">
2077
+ {brandVoicePreview}
2078
+ </blockquote>
2079
+ </div>
2080
+ ) : null}
2081
+ <div className="mt-4 flex flex-wrap gap-2">
2082
+ <Button
2083
+ type="button"
2084
+ disabled={publishBrandDna.isPending}
2085
+ onClick={() => void publishProposal()}
2086
+ >
2087
+ <IconSparkles />
2088
+ {publishBrandDna.isPending
2089
+ ? t("creativeContext.applyingBrandContext")
2090
+ : t("creativeContext.applyBrandContext")}
2091
+ </Button>
2092
+ <Button asChild type="button" variant="outline">
2093
+ <a href="/agent">
2094
+ {t("creativeContext.generateWithContext")}
2095
+ <IconArrowUpRight />
2096
+ </a>
2097
+ </Button>
2098
+ </div>
2099
+ {publishedMessage ? (
2100
+ <p className="mt-2 text-xs text-muted-foreground">
2101
+ {publishedMessage}
2102
+ </p>
2103
+ ) : null}
2104
+ </div>
2105
+ </div>
2106
+ </div>
2107
+ ) : importJob.error ? (
2108
+ <p className="mt-2 text-sm text-destructive">
2109
+ {importJob.error}
2110
+ </p>
2111
+ ) : (
2112
+ <p className="mt-2 text-xs text-muted-foreground">
2113
+ {formatNumber(importJob.progressCurrent)} /{" "}
2114
+ {formatNumber(importJob.progressTotal ?? 0)}
2115
+ </p>
2116
+ )}
2117
+ </div>
2118
+ ) : null}
2119
+ {promotionPreview ? (
2120
+ <div className="mt-4 rounded-md border border-border p-4">
2121
+ <h3 className="text-sm font-semibold">
2122
+ {t("creativeContext.promoteToOrganization")}
2123
+ </h3>
2124
+ <p className="mt-1 text-sm text-muted-foreground">
2125
+ {t("creativeContext.promotionDescription")}
2126
+ </p>
2127
+ <dl className="mt-3 grid gap-2 text-xs sm:grid-cols-3">
2128
+ <div>
2129
+ <dt className="text-muted-foreground">
2130
+ {t("creativeContext.sourceReference")}
2131
+ </dt>
2132
+ <dd className="mt-0.5 truncate font-medium">
2133
+ {promotionPreview.containerRef}
2134
+ </dd>
2135
+ </div>
2136
+ <div>
2137
+ <dt className="text-muted-foreground">
2138
+ {t("creativeContext.itemsLabel", { count: "" })}
2139
+ </dt>
2140
+ <dd className="mt-0.5 font-medium">
2141
+ {formatNumber(promotionPreview.itemCount)}
2142
+ </dd>
2143
+ </div>
2144
+ <div>
2145
+ <dt className="text-muted-foreground">
2146
+ {t("creativeContext.restrictedItems", { count: "" })}
2147
+ </dt>
2148
+ <dd className="mt-0.5 font-medium">
2149
+ {formatNumber(promotionPreview.restrictedItemCount)}
2150
+ </dd>
2151
+ </div>
2152
+ </dl>
2153
+ <Button
2154
+ type="button"
2155
+ className="mt-4"
2156
+ disabled={manageSource.isPending}
2157
+ onClick={() => void confirmPromotion()}
2158
+ >
2159
+ {t("creativeContext.promoteToOrganization")}
2160
+ </Button>
2161
+ </div>
2162
+ ) : null}
2163
+ {promotionMessage ? (
2164
+ <p className="mt-2 text-xs text-muted-foreground">
2165
+ {promotionMessage}
2166
+ </p>
2167
+ ) : null}
2168
+ {setupError ? (
2169
+ <p className="mt-2 text-sm text-destructive">{setupError}</p>
2170
+ ) : null}
2171
+ </section>
2172
+
2173
+ <section className="border-t border-border/70 pt-6">
2174
+ <h2 className="text-lg font-semibold">
2175
+ {t("creativeContext.sourcesTitle")}
2176
+ </h2>
2177
+ {sources.length ? (
2178
+ <div className="mt-3">
2179
+ {sources.map((source) => (
2180
+ <SourceRow
2181
+ key={source.id}
2182
+ source={source}
2183
+ refreshing={
2184
+ refreshSource.isPending &&
2185
+ refreshSource.variables?.sourceId === source.id
2186
+ }
2187
+ canReview={canManageScope}
2188
+ canPromote={
2189
+ libraryScope === "user" &&
2190
+ Boolean(org?.orgId) &&
2191
+ canManageOrg &&
2192
+ source.visibility === "private"
2193
+ }
2194
+ onRefresh={refresh}
2195
+ onReview={(selected) =>
2196
+ void openItemCuration(selected, "restricted")
2197
+ }
2198
+ onCurate={(selected) => void openItemCuration(selected)}
2199
+ onPromote={(selected) =>
2200
+ void previewSourcePromotion(selected)
2201
+ }
2202
+ onPause={(selected) => void pauseSource(selected)}
2203
+ onRestore={(selected) => void restoreSource(selected)}
2204
+ onDelete={setDeleteSource}
2205
+ />
2206
+ ))}
2207
+ </div>
2208
+ ) : (
2209
+ <div className="mt-4 rounded-md border border-dashed border-border p-6 text-center">
2210
+ <IconBooks className="mx-auto size-7 text-muted-foreground" />
2211
+ <h3 className="mt-3 text-sm font-semibold">
2212
+ {t("creativeContext.noSourcesTitle")}
2213
+ </h3>
2214
+ <p className="mx-auto mt-1 max-w-md text-sm text-muted-foreground">
2215
+ {t("creativeContext.noSourcesDescription")}
2216
+ </p>
2217
+ <Button asChild variant="outline" size="sm" className="mt-4">
2218
+ <a href={connectionsHref}>
2219
+ {t("creativeContext.connectSources")}
2220
+ <IconArrowUpRight />
2221
+ </a>
2222
+ </Button>
2223
+ </div>
2224
+ )}
2225
+ {reviewSource ? (
2226
+ <ItemCuration
2227
+ source={reviewSource}
2228
+ items={reviewedItems}
2229
+ busy={reviewItems.isPending}
2230
+ onReview={(operation, itemId) =>
2231
+ void reviewContextItem(operation, itemId)
2232
+ }
2233
+ onClose={() => {
2234
+ setReviewSource(null);
2235
+ setReviewedItems([]);
2236
+ }}
2237
+ />
2238
+ ) : null}
2239
+ {reviewError ? (
2240
+ <p className="mt-2 text-sm text-destructive">{reviewError}</p>
2241
+ ) : null}
2242
+ {refreshMessage ? (
2243
+ <p className="mt-2 text-xs text-muted-foreground">
2244
+ {refreshMessage}
2245
+ </p>
2246
+ ) : null}
2247
+ {lifecycleMessage ? (
2248
+ <p className="mt-2 text-xs text-muted-foreground">
2249
+ {lifecycleMessage}
2250
+ </p>
2251
+ ) : null}
2252
+ </section>
2253
+
2254
+ <section className="border-t border-border/70 pt-6">
2255
+ <h2 className="text-lg font-semibold">
2256
+ {t("creativeContext.packsTitle")}
2257
+ </h2>
2258
+ {packs.length ? (
2259
+ <div className="mt-3">
2260
+ {packs.map((pack) => (
2261
+ <PackRow
2262
+ key={pack.id}
2263
+ pack={pack}
2264
+ pinned={contextState.state.pinnedPackId === pack.id}
2265
+ disabled={savingState}
2266
+ onPin={(packId) => void changePinnedPack(packId)}
2267
+ onDetails={setSelectedPackId}
2268
+ />
2269
+ ))}
2270
+ </div>
2271
+ ) : (
2272
+ <p className="mt-3 rounded-md border border-dashed border-border p-5 text-sm text-muted-foreground">
2273
+ {t("creativeContext.noPacks")}
2274
+ </p>
2275
+ )}
2276
+ {selectedPackId ? (
2277
+ <div className="mt-4 rounded-md border border-border p-4">
2278
+ <div className="flex items-start justify-between gap-3">
2279
+ <div>
2280
+ <h3 className="text-sm font-semibold">
2281
+ {packQuery.data?.pack?.name ??
2282
+ t("creativeContext.packDetails")}
2283
+ </h3>
2284
+ {packQuery.data?.pack?.derivedFromPackId ? (
2285
+ <p className="mt-1 text-xs text-muted-foreground">
2286
+ {t("creativeContext.influence")}:{" "}
2287
+ {packQuery.data.pack.derivedFromPackId}
2288
+ </p>
2289
+ ) : null}
2290
+ </div>
2291
+ <Button
2292
+ type="button"
2293
+ variant="ghost"
2294
+ size="sm"
2295
+ onClick={() => setSelectedPackId(null)}
2296
+ >
2297
+ {t("creativeContext.cancel")}
2298
+ </Button>
2299
+ </div>
2300
+ {packQuery.data?.pack?.members.length ? (
2301
+ <div className="mt-3 divide-y divide-border/60">
2302
+ {packQuery.data.pack.members.map((member) => (
2303
+ <div key={member.id} className="py-2 text-xs">
2304
+ <p className="font-medium">{member.itemId}</p>
2305
+ {member.reason ? (
2306
+ <p className="mt-0.5 text-muted-foreground">
2307
+ {t("creativeContext.influence")}: {member.reason}
2308
+ </p>
2309
+ ) : null}
2310
+ </div>
2311
+ ))}
2312
+ </div>
2313
+ ) : null}
2314
+ </div>
2315
+ ) : null}
2316
+ </section>
2317
+
2318
+ <section className="border-t border-border/70 pt-6">
2319
+ <div>
2320
+ <h2 className="text-lg font-semibold">
2321
+ {t("creativeContext.searchTitle")}
2322
+ </h2>
2323
+ <p className="mt-1 text-sm text-muted-foreground">
2324
+ {t("creativeContext.searchDescription")}
2325
+ </p>
2326
+ </div>
2327
+ <form
2328
+ className="mt-4 flex gap-2"
2329
+ onSubmit={(event) => void search(event)}
2330
+ >
2331
+ <div className="relative min-w-0 flex-1">
2332
+ <IconSearch className="pointer-events-none absolute start-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
2333
+ <Input
2334
+ type="search"
2335
+ value={query}
2336
+ onChange={(event) => setQuery(event.target.value)}
2337
+ placeholder={t("creativeContext.searchPlaceholder")}
2338
+ aria-label={t("creativeContext.searchPlaceholder")}
2339
+ className="ps-9"
2340
+ />
2341
+ </div>
2342
+ <Button
2343
+ type="submit"
2344
+ variant="outline"
2345
+ disabled={
2346
+ !query.trim() || !sources.length || searchContext.isPending
2347
+ }
2348
+ >
2349
+ <IconSearch />
2350
+ {t("creativeContext.searchTitle")}
2351
+ </Button>
2352
+ </form>
2353
+ {searchError ? (
2354
+ <p className="mt-4 text-sm text-destructive">{searchError}</p>
2355
+ ) : !searchContext.data && !searchContext.isPending ? (
2356
+ <p className="mt-4 text-sm text-muted-foreground">
2357
+ {t("creativeContext.searchPrompt")}
2358
+ </p>
2359
+ ) : searchContext.isPending ? (
2360
+ <div className="mt-4 space-y-2">
2361
+ <Skeleton className="h-20 w-full" />
2362
+ <Skeleton className="h-20 w-full" />
2363
+ </div>
2364
+ ) : searchContext.data?.results.length ? (
2365
+ <div className="mt-4 space-y-2">
2366
+ <p className="text-xs text-muted-foreground">
2367
+ {t("creativeContext.resultsLabel", {
2368
+ count: formatNumber(searchContext.data.results.length),
2369
+ })}
2370
+ </p>
2371
+ {searchContext.data.results.map((result) => (
2372
+ <article
2373
+ key={`${result.itemVersionId}:${result.chunkId ?? "item"}`}
2374
+ className="rounded-md border border-border/70 p-3"
2375
+ >
2376
+ <div className="flex items-start justify-between gap-3">
2377
+ <div className="min-w-0">
2378
+ <h3 className="truncate text-sm font-medium">
2379
+ {result.title}
2380
+ </h3>
2381
+ <p className="mt-0.5 text-xs text-muted-foreground">
2382
+ {result.sourceName} · {result.kind}
2383
+ </p>
2384
+ </div>
2385
+ {result.canonicalUrl ? (
2386
+ <Button asChild variant="ghost" size="icon">
2387
+ <a
2388
+ href={result.canonicalUrl}
2389
+ target="_blank"
2390
+ rel="noreferrer"
2391
+ aria-label={result.title}
2392
+ >
2393
+ <IconArrowUpRight />
2394
+ </a>
2395
+ </Button>
2396
+ ) : null}
2397
+ </div>
2398
+ <p className="mt-2 line-clamp-3 text-sm leading-relaxed text-muted-foreground">
2399
+ {result.excerpt}
2400
+ </p>
2401
+ </article>
2402
+ ))}
2403
+ </div>
2404
+ ) : (
2405
+ <p className="mt-4 text-sm text-muted-foreground">
2406
+ {t("creativeContext.noResults")}
2407
+ </p>
2408
+ )}
2409
+ </section>
2410
+ </>
2411
+ )}
2412
+ <AlertDialog
2413
+ open={Boolean(deleteSource)}
2414
+ onOpenChange={(open) => {
2415
+ if (!open) setDeleteSource(null);
2416
+ }}
2417
+ >
2418
+ <AlertDialogContent>
2419
+ <AlertDialogHeader>
2420
+ <AlertDialogTitle>
2421
+ {t("creativeContext.deleteSourceTitle")}
2422
+ </AlertDialogTitle>
2423
+ <AlertDialogDescription>
2424
+ {t("creativeContext.deleteSourceDescription", {
2425
+ name: deleteSource?.name ?? "",
2426
+ })}
2427
+ </AlertDialogDescription>
2428
+ </AlertDialogHeader>
2429
+ <AlertDialogFooter>
2430
+ <AlertDialogCancel>{t("creativeContext.cancel")}</AlertDialogCancel>
2431
+ <AlertDialogAction
2432
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
2433
+ onClick={() => void confirmDeleteSource()}
2434
+ >
2435
+ {t("creativeContext.delete")}
2436
+ </AlertDialogAction>
2437
+ </AlertDialogFooter>
2438
+ </AlertDialogContent>
2439
+ </AlertDialog>
2440
+ </div>
2441
+ );
2442
+ }