aimeat 2.4.0 → 2.6.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 (308) hide show
  1. package/dist/.env.example +33 -0
  2. package/dist/build-stamp.json +13 -0
  3. package/dist/locales/en.json +186 -20
  4. package/dist/locales/fi.json +186 -20
  5. package/dist/public/components/VoiceRecorder.js +116 -0
  6. package/dist/public/connection-done.html +53 -0
  7. package/dist/public/css/components/voice-recorder.css +66 -0
  8. package/dist/public/css/views/inbox.css +41 -0
  9. package/dist/public/css/views/profile.css +171 -95
  10. package/dist/public/css/views/transparency.css +5 -0
  11. package/dist/public/js/services/audio-recorder.js +171 -0
  12. package/dist/public/js/services/messages.js +21 -2
  13. package/dist/public/spa.html +3 -0
  14. package/dist/public/views/landing.js +4 -0
  15. package/dist/public/views/profile/access-tab/connections.js +264 -0
  16. package/dist/public/views/profile/access-tab.js +2 -0
  17. package/dist/public/views/profile/ai-transparency-card.js +22 -17
  18. package/dist/public/views/profile/inbox-tab/components.js +60 -10
  19. package/dist/public/views/profile/inbox-tab/helpers.js +33 -0
  20. package/dist/public/views/profile/inbox-tab/panels.js +6 -1
  21. package/dist/public/views/profile/inbox-tab/use-voice.js +50 -0
  22. package/dist/public/views/profile/inbox-tab/voice-parts.js +111 -0
  23. package/dist/public/views/profile/inbox-tab.js +18 -24
  24. package/dist/public/views/profile/openrouter/budget-panel.js +224 -0
  25. package/dist/public/views/profile/openrouter/model-picker.js +150 -0
  26. package/dist/public/views/profile/openrouter/pricing.js +139 -0
  27. package/dist/public/views/profile/openrouter-settings.js +425 -488
  28. package/dist/public/views/transparency.js +50 -0
  29. package/dist/scripts/build-sdk-libs.d.ts.map +1 -1
  30. package/dist/scripts/build-sdk-libs.js +1 -0
  31. package/dist/scripts/build-sdk-libs.js.map +1 -1
  32. package/dist/scripts/check-ai-disclosure.d.ts.map +1 -1
  33. package/dist/scripts/check-ai-disclosure.js +182 -0
  34. package/dist/scripts/check-ai-disclosure.js.map +1 -1
  35. package/dist/src/cli/connect/ai-provenance-carry.d.ts +247 -0
  36. package/dist/src/cli/connect/ai-provenance-carry.d.ts.map +1 -0
  37. package/dist/src/cli/connect/ai-provenance-carry.js +334 -0
  38. package/dist/src/cli/connect/ai-provenance-carry.js.map +1 -0
  39. package/dist/src/cli/connect/api-client.d.ts +11 -0
  40. package/dist/src/cli/connect/api-client.d.ts.map +1 -1
  41. package/dist/src/cli/connect/api-client.js +2 -0
  42. package/dist/src/cli/connect/api-client.js.map +1 -1
  43. package/dist/src/cli/connect/mcp/local-server.d.ts.map +1 -1
  44. package/dist/src/cli/connect/mcp/local-server.js +11 -0
  45. package/dist/src/cli/connect/mcp/local-server.js.map +1 -1
  46. package/dist/src/cli/connect/mcp/tools/agent-messages.d.ts +2 -0
  47. package/dist/src/cli/connect/mcp/tools/agent-messages.d.ts.map +1 -1
  48. package/dist/src/cli/connect/mcp/tools/agent-messages.js +5 -2
  49. package/dist/src/cli/connect/mcp/tools/agent-messages.js.map +1 -1
  50. package/dist/src/cli/connect/mcp/tools/agent-tasks.d.ts +2 -0
  51. package/dist/src/cli/connect/mcp/tools/agent-tasks.d.ts.map +1 -1
  52. package/dist/src/cli/connect/mcp/tools/agent-tasks.js +5 -2
  53. package/dist/src/cli/connect/mcp/tools/agent-tasks.js.map +1 -1
  54. package/dist/src/cli/connect/mcp/tools/apps.d.ts +3 -0
  55. package/dist/src/cli/connect/mcp/tools/apps.d.ts.map +1 -1
  56. package/dist/src/cli/connect/mcp/tools/apps.js +13 -5
  57. package/dist/src/cli/connect/mcp/tools/apps.js.map +1 -1
  58. package/dist/src/cli/connect/mcp/tools/boards.d.ts +3 -0
  59. package/dist/src/cli/connect/mcp/tools/boards.d.ts.map +1 -1
  60. package/dist/src/cli/connect/mcp/tools/boards.js +5 -2
  61. package/dist/src/cli/connect/mcp/tools/boards.js.map +1 -1
  62. package/dist/src/cli/connect/mcp/tools/core.d.ts +5 -0
  63. package/dist/src/cli/connect/mcp/tools/core.d.ts.map +1 -1
  64. package/dist/src/cli/connect/mcp/tools/core.js +25 -5
  65. package/dist/src/cli/connect/mcp/tools/core.js.map +1 -1
  66. package/dist/src/cli/connect/mcp/tools/dm-messages.d.ts +2 -0
  67. package/dist/src/cli/connect/mcp/tools/dm-messages.d.ts.map +1 -1
  68. package/dist/src/cli/connect/mcp/tools/dm-messages.js +11 -6
  69. package/dist/src/cli/connect/mcp/tools/dm-messages.js.map +1 -1
  70. package/dist/src/cli/connect/mcp/tools/exchange.d.ts +2 -0
  71. package/dist/src/cli/connect/mcp/tools/exchange.d.ts.map +1 -1
  72. package/dist/src/cli/connect/mcp/tools/exchange.js +8 -2
  73. package/dist/src/cli/connect/mcp/tools/exchange.js.map +1 -1
  74. package/dist/src/cli/connect/mcp/tools/knowledge.d.ts +3 -0
  75. package/dist/src/cli/connect/mcp/tools/knowledge.d.ts.map +1 -1
  76. package/dist/src/cli/connect/mcp/tools/knowledge.js +7 -3
  77. package/dist/src/cli/connect/mcp/tools/knowledge.js.map +1 -1
  78. package/dist/src/cli/connect/mcp/tools/organisms.d.ts +2 -0
  79. package/dist/src/cli/connect/mcp/tools/organisms.d.ts.map +1 -1
  80. package/dist/src/cli/connect/mcp/tools/organisms.js +7 -2
  81. package/dist/src/cli/connect/mcp/tools/organisms.js.map +1 -1
  82. package/dist/src/cli/connect/mcp/tools/workspaces.d.ts +3 -0
  83. package/dist/src/cli/connect/mcp/tools/workspaces.d.ts.map +1 -1
  84. package/dist/src/cli/connect/mcp/tools/workspaces.js +8 -2
  85. package/dist/src/cli/connect/mcp/tools/workspaces.js.map +1 -1
  86. package/dist/src/cli/connect/tool-call-defs-core.d.ts +3 -0
  87. package/dist/src/cli/connect/tool-call-defs-core.d.ts.map +1 -1
  88. package/dist/src/cli/connect/tool-call-defs-core.js +7 -0
  89. package/dist/src/cli/connect/tool-call-defs-core.js.map +1 -1
  90. package/dist/src/cli/connect/tool-call.d.ts +3 -0
  91. package/dist/src/cli/connect/tool-call.d.ts.map +1 -1
  92. package/dist/src/cli/connect/tool-call.js +11 -1
  93. package/dist/src/cli/connect/tool-call.js.map +1 -1
  94. package/dist/src/config-types.d.ts +14 -0
  95. package/dist/src/config-types.d.ts.map +1 -1
  96. package/dist/src/config.d.ts.map +1 -1
  97. package/dist/src/config.js +10 -0
  98. package/dist/src/config.js.map +1 -1
  99. package/dist/src/data/public-pages.d.ts +12 -0
  100. package/dist/src/data/public-pages.d.ts.map +1 -1
  101. package/dist/src/data/public-pages.js +12 -0
  102. package/dist/src/data/public-pages.js.map +1 -1
  103. package/dist/src/generated/api-types.d.ts +969 -85
  104. package/dist/src/generated/api-types.d.ts.map +1 -1
  105. package/dist/src/mcp/ai-provenance-input.d.ts +45 -2
  106. package/dist/src/mcp/ai-provenance-input.d.ts.map +1 -1
  107. package/dist/src/mcp/ai-provenance-input.js +48 -2
  108. package/dist/src/mcp/ai-provenance-input.js.map +1 -1
  109. package/dist/src/mcp/apps.d.ts +7 -0
  110. package/dist/src/mcp/apps.d.ts.map +1 -1
  111. package/dist/src/mcp/apps.js +20 -2
  112. package/dist/src/mcp/apps.js.map +1 -1
  113. package/dist/src/models/ai-provenance-schemas.d.ts +7 -1
  114. package/dist/src/models/ai-provenance-schemas.d.ts.map +1 -1
  115. package/dist/src/models/ai-provenance-schemas.js +11 -1
  116. package/dist/src/models/ai-provenance-schemas.js.map +1 -1
  117. package/dist/src/models/connection-schemas.d.ts +203 -0
  118. package/dist/src/models/connection-schemas.d.ts.map +1 -0
  119. package/dist/src/models/connection-schemas.js +33 -0
  120. package/dist/src/models/connection-schemas.js.map +1 -0
  121. package/dist/src/models/message-schemas.d.ts +40 -0
  122. package/dist/src/models/message-schemas.d.ts.map +1 -1
  123. package/dist/src/models/message-schemas.js +23 -0
  124. package/dist/src/models/message-schemas.js.map +1 -1
  125. package/dist/src/routes/ai-provenance.d.ts +6 -0
  126. package/dist/src/routes/ai-provenance.d.ts.map +1 -1
  127. package/dist/src/routes/ai-provenance.js +34 -1
  128. package/dist/src/routes/ai-provenance.js.map +1 -1
  129. package/dist/src/routes/ai-transparency.d.ts +6 -1
  130. package/dist/src/routes/ai-transparency.d.ts.map +1 -1
  131. package/dist/src/routes/ai-transparency.js +17 -3
  132. package/dist/src/routes/ai-transparency.js.map +1 -1
  133. package/dist/src/routes/ai.d.ts +5 -0
  134. package/dist/src/routes/ai.d.ts.map +1 -1
  135. package/dist/src/routes/ai.js +83 -0
  136. package/dist/src/routes/ai.js.map +1 -1
  137. package/dist/src/routes/app-grants.d.ts.map +1 -1
  138. package/dist/src/routes/app-grants.js +4 -0
  139. package/dist/src/routes/app-grants.js.map +1 -1
  140. package/dist/src/routes/apps/drafts.d.ts +3 -0
  141. package/dist/src/routes/apps/drafts.d.ts.map +1 -1
  142. package/dist/src/routes/apps/drafts.js +13 -0
  143. package/dist/src/routes/apps/drafts.js.map +1 -1
  144. package/dist/src/routes/apps/publish.d.ts +5 -0
  145. package/dist/src/routes/apps/publish.d.ts.map +1 -1
  146. package/dist/src/routes/apps/publish.js +21 -5
  147. package/dist/src/routes/apps/publish.js.map +1 -1
  148. package/dist/src/routes/connections.d.ts +35 -0
  149. package/dist/src/routes/connections.d.ts.map +1 -0
  150. package/dist/src/routes/connections.js +406 -0
  151. package/dist/src/routes/connections.js.map +1 -0
  152. package/dist/src/routes/libs.js +1 -1
  153. package/dist/src/routes/libs.js.map +1 -1
  154. package/dist/src/routes/messages.d.ts +5 -0
  155. package/dist/src/routes/messages.d.ts.map +1 -1
  156. package/dist/src/routes/messages.js +92 -0
  157. package/dist/src/routes/messages.js.map +1 -1
  158. package/dist/src/routes/openrouter.d.ts +6 -1
  159. package/dist/src/routes/openrouter.d.ts.map +1 -1
  160. package/dist/src/routes/openrouter.js +37 -7
  161. package/dist/src/routes/openrouter.js.map +1 -1
  162. package/dist/src/routes/upload.d.ts +5 -0
  163. package/dist/src/routes/upload.d.ts.map +1 -1
  164. package/dist/src/routes/upload.js +32 -0
  165. package/dist/src/routes/upload.js.map +1 -1
  166. package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
  167. package/dist/src/server-bootstrap/routes-loader.js +2 -0
  168. package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
  169. package/dist/src/services/ai-completion.d.ts +48 -0
  170. package/dist/src/services/ai-completion.d.ts.map +1 -1
  171. package/dist/src/services/ai-completion.js +112 -72
  172. package/dist/src/services/ai-completion.js.map +1 -1
  173. package/dist/src/services/ai-disclosure.d.ts +6 -0
  174. package/dist/src/services/ai-disclosure.d.ts.map +1 -1
  175. package/dist/src/services/ai-disclosure.js +20 -2
  176. package/dist/src/services/ai-disclosure.js.map +1 -1
  177. package/dist/src/services/ai-provenance-page.d.ts +59 -0
  178. package/dist/src/services/ai-provenance-page.d.ts.map +1 -0
  179. package/dist/src/services/ai-provenance-page.js +185 -0
  180. package/dist/src/services/ai-provenance-page.js.map +1 -0
  181. package/dist/src/services/ai-provenance.d.ts +6 -0
  182. package/dist/src/services/ai-provenance.d.ts.map +1 -1
  183. package/dist/src/services/ai-provenance.js +5 -0
  184. package/dist/src/services/ai-provenance.js.map +1 -1
  185. package/dist/src/services/ai-transcription.d.ts +66 -0
  186. package/dist/src/services/ai-transcription.d.ts.map +1 -0
  187. package/dist/src/services/ai-transcription.js +90 -0
  188. package/dist/src/services/ai-transcription.js.map +1 -0
  189. package/dist/src/services/app-publish.d.ts.map +1 -1
  190. package/dist/src/services/app-publish.js +16 -1
  191. package/dist/src/services/app-publish.js.map +1 -1
  192. package/dist/src/services/build-app-prompt.d.ts.map +1 -1
  193. package/dist/src/services/build-app-prompt.js +2 -1
  194. package/dist/src/services/build-app-prompt.js.map +1 -1
  195. package/dist/src/services/connections/attach.d.ts +62 -0
  196. package/dist/src/services/connections/attach.d.ts.map +1 -0
  197. package/dist/src/services/connections/attach.js +168 -0
  198. package/dist/src/services/connections/attach.js.map +1 -0
  199. package/dist/src/services/connections/credential.d.ts +39 -0
  200. package/dist/src/services/connections/credential.d.ts.map +1 -0
  201. package/dist/src/services/connections/credential.js +66 -0
  202. package/dist/src/services/connections/credential.js.map +1 -0
  203. package/dist/src/services/connections/instance.d.ts +65 -0
  204. package/dist/src/services/connections/instance.d.ts.map +1 -0
  205. package/dist/src/services/connections/instance.js +144 -0
  206. package/dist/src/services/connections/instance.js.map +1 -0
  207. package/dist/src/services/connections/oauth.d.ts +77 -0
  208. package/dist/src/services/connections/oauth.d.ts.map +1 -0
  209. package/dist/src/services/connections/oauth.js +290 -0
  210. package/dist/src/services/connections/oauth.js.map +1 -0
  211. package/dist/src/services/connections/providers.d.ts +118 -0
  212. package/dist/src/services/connections/providers.d.ts.map +1 -0
  213. package/dist/src/services/connections/providers.js +260 -0
  214. package/dist/src/services/connections/providers.js.map +1 -0
  215. package/dist/src/services/connections/publish-gate.d.ts +103 -0
  216. package/dist/src/services/connections/publish-gate.d.ts.map +1 -0
  217. package/dist/src/services/connections/publish-gate.js +0 -0
  218. package/dist/src/services/connections/publish-gate.js.map +1 -0
  219. package/dist/src/services/connections/publish.d.ts +60 -0
  220. package/dist/src/services/connections/publish.d.ts.map +1 -0
  221. package/dist/src/services/connections/publish.js +340 -0
  222. package/dist/src/services/connections/publish.js.map +1 -0
  223. package/dist/src/services/connections/refresh.d.ts +55 -0
  224. package/dist/src/services/connections/refresh.d.ts.map +1 -0
  225. package/dist/src/services/connections/refresh.js +307 -0
  226. package/dist/src/services/connections/refresh.js.map +1 -0
  227. package/dist/src/services/markdown-negotiation.d.ts +17 -0
  228. package/dist/src/services/markdown-negotiation.d.ts.map +1 -1
  229. package/dist/src/services/markdown-negotiation.js +15 -0
  230. package/dist/src/services/markdown-negotiation.js.map +1 -1
  231. package/dist/src/services/message-send.d.ts.map +1 -1
  232. package/dist/src/services/message-send.js +16 -2
  233. package/dist/src/services/message-send.js.map +1 -1
  234. package/dist/src/services/openrouter.d.ts +67 -1
  235. package/dist/src/services/openrouter.d.ts.map +1 -1
  236. package/dist/src/services/openrouter.js +102 -11
  237. package/dist/src/services/openrouter.js.map +1 -1
  238. package/dist/src/services/upload-token.d.ts +5 -1
  239. package/dist/src/services/upload-token.d.ts.map +1 -1
  240. package/dist/src/services/upload-token.js +13 -1
  241. package/dist/src/services/upload-token.js.map +1 -1
  242. package/dist/src/services/workflow/engine-util.d.ts +18 -0
  243. package/dist/src/services/workflow/engine-util.d.ts.map +1 -1
  244. package/dist/src/services/workflow/engine-util.js +27 -0
  245. package/dist/src/services/workflow/engine-util.js.map +1 -1
  246. package/dist/src/services/workflow/engine.d.ts.map +1 -1
  247. package/dist/src/services/workflow/engine.js +4 -2
  248. package/dist/src/services/workflow/engine.js.map +1 -1
  249. package/dist/src/storage/interface.d.ts +2 -1
  250. package/dist/src/storage/interface.d.ts.map +1 -1
  251. package/dist/src/storage/providers/postgres-kysely/db-types.d.ts +65 -0
  252. package/dist/src/storage/providers/postgres-kysely/db-types.d.ts.map +1 -1
  253. package/dist/src/storage/providers/postgres-kysely/index.d.ts.map +1 -1
  254. package/dist/src/storage/providers/postgres-kysely/index.js +2 -1
  255. package/dist/src/storage/providers/postgres-kysely/index.js.map +1 -1
  256. package/dist/src/storage/providers/postgres-kysely/methods/connections.d.ts +44 -0
  257. package/dist/src/storage/providers/postgres-kysely/methods/connections.d.ts.map +1 -0
  258. package/dist/src/storage/providers/postgres-kysely/methods/connections.js +308 -0
  259. package/dist/src/storage/providers/postgres-kysely/methods/connections.js.map +1 -0
  260. package/dist/src/storage/providers/postgres-kysely/methods/node-infra.d.ts.map +1 -1
  261. package/dist/src/storage/providers/postgres-kysely/methods/node-infra.js +4 -2
  262. package/dist/src/storage/providers/postgres-kysely/methods/node-infra.js.map +1 -1
  263. package/dist/src/storage/providers/postgres-kysely/migrations/0021_connections.sql +116 -0
  264. package/dist/src/storage/providers/postgres-kysely/migrations/0022_provider_clients.sql +36 -0
  265. package/dist/src/storage/providers/postgres-kysely/migrations/0023_nonce_payload.sql +13 -0
  266. package/dist/src/storage/providers/sqlite/index.d.ts.map +1 -1
  267. package/dist/src/storage/providers/sqlite/index.js +2 -1
  268. package/dist/src/storage/providers/sqlite/index.js.map +1 -1
  269. package/dist/src/storage/providers/sqlite/methods/community.d.ts.map +1 -1
  270. package/dist/src/storage/providers/sqlite/methods/community.js +2 -1
  271. package/dist/src/storage/providers/sqlite/methods/community.js.map +1 -1
  272. package/dist/src/storage/providers/sqlite/methods/connections.d.ts +60 -0
  273. package/dist/src/storage/providers/sqlite/methods/connections.d.ts.map +1 -0
  274. package/dist/src/storage/providers/sqlite/methods/connections.js +287 -0
  275. package/dist/src/storage/providers/sqlite/methods/connections.js.map +1 -0
  276. package/dist/src/storage/providers/sqlite/schema-tables-1.d.ts.map +1 -1
  277. package/dist/src/storage/providers/sqlite/schema-tables-1.js +4 -0
  278. package/dist/src/storage/providers/sqlite/schema-tables-1.js.map +1 -1
  279. package/dist/src/storage/providers/sqlite/schema-tables-3.d.ts.map +1 -1
  280. package/dist/src/storage/providers/sqlite/schema-tables-3.js +106 -0
  281. package/dist/src/storage/providers/sqlite/schema-tables-3.js.map +1 -1
  282. package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
  283. package/dist/src/storage/providers/sqlite/schema.js +2 -0
  284. package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
  285. package/dist/src/storage/repositories/connection.repository.d.ts +124 -0
  286. package/dist/src/storage/repositories/connection.repository.d.ts.map +1 -0
  287. package/dist/src/storage/repositories/connection.repository.js +2 -0
  288. package/dist/src/storage/repositories/connection.repository.js.map +1 -0
  289. package/dist/src/storage/types/agents-messaging.d.ts +19 -0
  290. package/dist/src/storage/types/agents-messaging.d.ts.map +1 -1
  291. package/dist/src/storage/types/agents-messaging.js.map +1 -1
  292. package/dist/src/storage/types/auth.d.ts +8 -1
  293. package/dist/src/storage/types/auth.d.ts.map +1 -1
  294. package/dist/src/utils/build-stamp.d.ts +56 -0
  295. package/dist/src/utils/build-stamp.d.ts.map +1 -0
  296. package/dist/src/utils/build-stamp.js +142 -0
  297. package/dist/src/utils/build-stamp.js.map +1 -0
  298. package/dist/src/utils/messaging.d.ts +14 -0
  299. package/dist/src/utils/messaging.d.ts.map +1 -1
  300. package/dist/src/utils/messaging.js +0 -0
  301. package/dist/src/utils/messaging.js.map +1 -1
  302. package/dist/static/sdk-libs/connect/index.js +199 -0
  303. package/dist/static/sdk-libs/connect/notes.js +47 -0
  304. package/dist/static/sdk-libs/connect/panel.js +238 -0
  305. package/dist/static/sdk-libs/data/index.js +63 -11
  306. package/dist/static/sdk-libs/dist/aimeat-connect.js +357 -0
  307. package/dist/static/sdk-libs/dist/aimeat-data.js +40 -9
  308. package/package.json +2 -2
@@ -1114,7 +1114,7 @@
1114
1114
  textareas a comfortable touch height and let the prompt box scroll. */
1115
1115
  .pf-gen-ws-pane .pf-gen-prompt-box { max-height: 220px; }
1116
1116
  .pf-gen-ws-pane .pf-gen-result-area { min-height: 200px; }
1117
- .pf-gen-or-run-all-bar { flex-direction: column; align-items: stretch; gap: 8px; }
1117
+ .pf-or-run-all-bar { flex-direction: column; align-items: stretch; gap: 8px; }
1118
1118
  }
1119
1119
 
1120
1120
  /* ══ Shared Profile Utilities (2026-03-17) ══════════════ */
@@ -1249,15 +1249,15 @@
1249
1249
  .pf-gen-agent-actions { display: flex; gap: 8px; }
1250
1250
  .pf-gen-stop-btn { margin-left: auto; }
1251
1251
 
1252
- /* ── OpenRouter Settings (pf-gen-or-*) ──────────────── */
1253
- .pf-gen-or-wrapper {
1252
+ /* ── OpenRouter Settings (pf-or-*) ──────────────── */
1253
+ .pf-or-wrapper {
1254
1254
  margin-bottom: 1.25rem;
1255
1255
  background: var(--card, #FFFFFF);
1256
1256
  border: 1px solid var(--border, #E5E7EB);
1257
1257
  border-radius: 8px;
1258
1258
  overflow: hidden;
1259
1259
  }
1260
- .pf-gen-or-toggle {
1260
+ .pf-or-toggle {
1261
1261
  display: flex;
1262
1262
  align-items: center;
1263
1263
  gap: 8px;
@@ -1271,22 +1271,22 @@
1271
1271
  color: var(--text, #1A1A2E);
1272
1272
  text-align: left;
1273
1273
  }
1274
- .pf-gen-or-toggle:hover { background: var(--bg-dim, #F3F4F6); }
1275
- .pf-gen-or-toggle-icon { font-size: 0.65em; color: var(--text-dim, #6B7280); width: 1rem; flex-shrink: 0; }
1276
- .pf-gen-or-status-dot {
1274
+ .pf-or-toggle:hover { background: var(--bg-dim, #F3F4F6); }
1275
+ .pf-or-toggle-icon { font-size: 0.65em; color: var(--text-dim, #6B7280); width: 1rem; flex-shrink: 0; }
1276
+ .pf-or-status-dot {
1277
1277
  width: 8px; height: 8px; border-radius: 50%;
1278
1278
  background: var(--success, #22c55e);
1279
1279
  box-shadow: 0 0 6px rgba(34, 197, 94, 0.4);
1280
1280
  flex-shrink: 0;
1281
1281
  margin-left: auto;
1282
1282
  }
1283
- .pf-gen-or-panel {
1283
+ .pf-or-panel {
1284
1284
  padding: 0 14px 14px;
1285
1285
  }
1286
- .pf-gen-or-field {
1286
+ .pf-or-field {
1287
1287
  margin-bottom: 12px;
1288
1288
  }
1289
- .pf-gen-or-label {
1289
+ .pf-or-label {
1290
1290
  display: block;
1291
1291
  font-size: 0.8em;
1292
1292
  color: var(--text-dim, #6B7280);
@@ -1294,7 +1294,7 @@
1294
1294
  margin-bottom: 4px;
1295
1295
  font-weight: 600;
1296
1296
  }
1297
- .pf-gen-or-input {
1297
+ .pf-or-input {
1298
1298
  width: 100%;
1299
1299
  padding: 8px 12px;
1300
1300
  background: var(--surface, #F9FAFB);
@@ -1305,15 +1305,15 @@
1305
1305
  font-family: inherit;
1306
1306
  box-sizing: border-box;
1307
1307
  }
1308
- .pf-gen-or-input:focus { outline: none; border-color: var(--accent, #E8564A); box-shadow: 0 0 0 3px rgba(232, 86, 74, 0.1); }
1309
- .pf-gen-or-input-sm { max-width: 100px; }
1310
- .pf-gen-or-param-row { display: flex; align-items: center; gap: 10px; }
1311
- .pf-gen-or-param-hint { font-size: 0.8em; color: var(--text-dim, #888); white-space: nowrap; }
1308
+ .pf-or-input:focus { outline: none; border-color: var(--accent, #E8564A); box-shadow: 0 0 0 3px rgba(232, 86, 74, 0.1); }
1309
+ .pf-or-input-sm { max-width: 100px; }
1310
+ .pf-or-param-row { display: flex; align-items: center; gap: 10px; }
1311
+ .pf-or-param-hint { font-size: 0.8em; color: var(--text-dim, #888); white-space: nowrap; }
1312
1312
  /* Model refresh row: sits below the model <select>; let the hint/error wrap on narrow widths. */
1313
- .pf-gen-or-model-row { margin-top: 8px; flex-wrap: wrap; }
1314
- .pf-gen-or-model-row .pf-gen-or-param-hint { white-space: normal; }
1315
- .pf-gen-or-model-error { color: var(--danger, #ef4444); }
1316
- .pf-gen-or-select {
1313
+ .pf-or-model-row { margin-top: 8px; flex-wrap: wrap; }
1314
+ .pf-or-model-row .pf-or-param-hint { white-space: normal; }
1315
+ .pf-or-model-error { color: var(--danger, #ef4444); }
1316
+ .pf-or-select {
1317
1317
  width: 100%;
1318
1318
  padding: 8px 12px;
1319
1319
  background: var(--surface, #F9FAFB);
@@ -1324,9 +1324,9 @@
1324
1324
  font-family: inherit;
1325
1325
  cursor: pointer;
1326
1326
  }
1327
- .pf-gen-or-select:disabled { opacity: 0.5; cursor: not-allowed; }
1328
- .pf-gen-or-select:focus { outline: none; border-color: var(--accent, #E8564A); }
1329
- .pf-gen-or-checkbox {
1327
+ .pf-or-select:disabled { opacity: 0.5; cursor: not-allowed; }
1328
+ .pf-or-select:focus { outline: none; border-color: var(--accent, #E8564A); }
1329
+ .pf-or-checkbox {
1330
1330
  display: flex;
1331
1331
  align-items: center;
1332
1332
  gap: 8px;
@@ -1334,13 +1334,13 @@
1334
1334
  color: var(--text, #1A1A2E);
1335
1335
  cursor: pointer;
1336
1336
  }
1337
- .pf-gen-or-checkbox input[type="checkbox"] { accent-color: var(--accent, #E8564A); width: 16px; height: 16px; cursor: pointer; }
1338
- .pf-gen-or-radio-group {
1337
+ .pf-or-checkbox input[type="checkbox"] { accent-color: var(--accent, #E8564A); width: 16px; height: 16px; cursor: pointer; }
1338
+ .pf-or-radio-group {
1339
1339
  display: flex;
1340
1340
  flex-direction: column;
1341
1341
  gap: 6px;
1342
1342
  }
1343
- .pf-gen-or-radio-label {
1343
+ .pf-or-radio-label {
1344
1344
  display: flex;
1345
1345
  align-items: center;
1346
1346
  gap: 8px;
@@ -1348,34 +1348,34 @@
1348
1348
  color: var(--text, #1A1A2E);
1349
1349
  cursor: pointer;
1350
1350
  }
1351
- .pf-gen-or-radio-label input[type="radio"] { accent-color: var(--accent, #E8564A); width: 16px; height: 16px; cursor: pointer; flex-shrink: 0; }
1352
- .pf-gen-or-loading { font-size: 0.85em; color: var(--text-dim, #6B7280); font-style: italic; }
1353
- .pf-gen-or-actions {
1351
+ .pf-or-radio-label input[type="radio"] { accent-color: var(--accent, #E8564A); width: 16px; height: 16px; cursor: pointer; flex-shrink: 0; }
1352
+ .pf-or-loading { font-size: 0.85em; color: var(--text-dim, #6B7280); font-style: italic; }
1353
+ .pf-or-actions {
1354
1354
  display: flex;
1355
1355
  gap: 8px;
1356
1356
  margin-top: 14px;
1357
1357
  flex-wrap: wrap;
1358
1358
  }
1359
- .pf-gen-or-delete {
1359
+ .pf-or-delete {
1360
1360
  color: var(--danger, #ef4444);
1361
1361
  border-color: var(--danger, #ef4444);
1362
1362
  }
1363
- .pf-gen-or-delete:hover {
1363
+ .pf-or-delete:hover {
1364
1364
  background: var(--danger, #ef4444);
1365
1365
  color: #fff;
1366
1366
  }
1367
- .pf-gen-or-message {
1367
+ .pf-or-message {
1368
1368
  padding: 8px 12px;
1369
1369
  border-radius: 6px;
1370
1370
  font-size: 0.85em;
1371
1371
  margin-top: 8px;
1372
1372
  }
1373
- .pf-gen-or-message-success {
1373
+ .pf-or-message-success {
1374
1374
  background: rgba(34, 197, 94, 0.1);
1375
1375
  color: var(--success, #22c55e);
1376
1376
  border: 1px solid rgba(34, 197, 94, 0.25);
1377
1377
  }
1378
- .pf-gen-or-message-error {
1378
+ .pf-or-message-error {
1379
1379
  background: rgba(239, 68, 68, 0.1);
1380
1380
  color: var(--danger, #ef4444);
1381
1381
  border: 1px solid rgba(239, 68, 68, 0.25);
@@ -1383,57 +1383,133 @@
1383
1383
 
1384
1384
  /* ── AI-apps daily-budget / spend section (generator-settings OpenRouterSettings) ──
1385
1385
  Lifted out of ~28 inline styles + hardcoded hex; all tokenized so it flips in dark mode. */
1386
- .pf-gen-spend-section { border-top: 2px solid var(--border); padding-top: 14px; margin-top: 14px; }
1387
- .pf-gen-spend-desc { font-size: 12px; color: var(--text-dim); margin-bottom: 8px; }
1388
- .pf-gen-spend-bar-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
1389
- .pf-gen-spend-bar { flex: 1; min-width: 200px; background: var(--bg-surface); border-radius: 4px; height: 20px; position: relative; overflow: hidden; }
1390
- .pf-gen-spend-bar-fill { position: absolute; top: 0; left: 0; bottom: 0; transition: width .3s; background: var(--success); }
1391
- .pf-gen-spend-bar-fill.warn { background: var(--warn); }
1392
- .pf-gen-spend-bar-fill.crit { background: var(--danger); }
1393
- .pf-gen-spend-bar-label { position: relative; text-align: center; font-size: 11px; line-height: 20px; font-weight: 700; }
1394
- .pf-gen-spend-budget-input { width: 90px; padding: 4px 6px; font-size: 13px; }
1395
- .pf-gen-spend-details { margin-top: 10px; }
1396
- .pf-gen-spend-summary { cursor: pointer; font-size: 12px; color: var(--text-dim); }
1397
- .pf-gen-spend-hint { font-size: 11px; color: var(--text-muted); margin: 6px 0; }
1398
- .pf-gen-spend-table { width: 100%; margin-top: 4px; font-size: 12px; border-collapse: collapse; }
1399
- .pf-gen-spend-table th, .pf-gen-spend-table td { padding: 4px; text-align: left; }
1400
- .pf-gen-spend-table th.num, .pf-gen-spend-table td.num { text-align: right; }
1401
- .pf-gen-spend-table thead tr { border-bottom: 1px solid var(--border); }
1402
- .pf-gen-spend-cap-input { width: 74px; padding: 3px 5px; font-size: 12px; text-align: right; }
1403
- .pf-gen-spend-actions { display: flex; align-items: center; gap: 8px; margin-top: 8px; }
1404
- .pf-gen-spend-msg { margin-top: 8px; }
1405
- .pf-gen-spend-chart-wrap { margin-top: 14px; }
1406
- .pf-gen-spend-chart-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 6px; }
1407
- .pf-gen-spend-chart-title { font-size: 12px; font-weight: 600; color: var(--text-dim); }
1408
- .pf-gen-spend-metric-toggle { display: inline-flex; gap: 4px; }
1409
- .pf-gen-spend-footnote { font-size: 11px; color: var(--text-muted); margin-top: 8px; }
1410
-
1411
- /* ── OpenRouter Autopilot (pf-gen-or-*) ──────────────── */
1412
- .pf-gen-or-btn-row {
1386
+ .pf-or-spend-section { border-top: 2px solid var(--border); padding-top: 14px; margin-top: 14px; }
1387
+ .pf-or-spend-desc { font-size: 12px; color: var(--text-dim); margin-bottom: 8px; }
1388
+ .pf-or-spend-bar-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
1389
+ .pf-or-spend-bar { flex: 1; min-width: 200px; background: var(--bg-surface); border-radius: 4px; height: 20px; position: relative; overflow: hidden; }
1390
+ .pf-or-spend-bar-fill { position: absolute; top: 0; left: 0; bottom: 0; transition: width .3s; background: var(--success); }
1391
+ .pf-or-spend-bar-fill.warn { background: var(--warn); }
1392
+ .pf-or-spend-bar-fill.crit { background: var(--danger); }
1393
+ .pf-or-spend-bar-label { position: relative; text-align: center; font-size: 11px; line-height: 20px; font-weight: 700; }
1394
+ .pf-or-spend-budget-input { width: 90px; padding: 4px 6px; font-size: 13px; }
1395
+ .pf-or-spend-details { margin-top: 10px; }
1396
+ .pf-or-spend-summary { cursor: pointer; font-size: 12px; color: var(--text-dim); }
1397
+ .pf-or-spend-hint { font-size: 11px; color: var(--text-muted); margin: 6px 0; }
1398
+ .pf-or-spend-table { width: 100%; margin-top: 4px; font-size: 12px; border-collapse: collapse; }
1399
+ .pf-or-spend-table th, .pf-or-spend-table td { padding: 4px; text-align: left; }
1400
+ .pf-or-spend-table th.num, .pf-or-spend-table td.num { text-align: right; }
1401
+ .pf-or-spend-table thead tr { border-bottom: 1px solid var(--border); }
1402
+ .pf-or-spend-cap-input { width: 74px; padding: 3px 5px; font-size: 12px; text-align: right; }
1403
+ .pf-or-spend-actions { display: flex; align-items: center; gap: 8px; margin-top: 8px; }
1404
+ .pf-or-spend-msg { margin-top: 8px; }
1405
+ .pf-or-spend-chart-wrap { margin-top: 14px; }
1406
+ .pf-or-spend-chart-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 6px; }
1407
+ .pf-or-spend-chart-title { font-size: 12px; font-weight: 600; color: var(--text-dim); }
1408
+ .pf-or-spend-metric-toggle { display: inline-flex; gap: 4px; }
1409
+ .pf-or-spend-footnote { font-size: 11px; color: var(--text-muted); margin-top: 8px; }
1410
+
1411
+ /* ── AI provider settings v3: sections, model picker, STT (2026-08-01) ──
1412
+ The panel is four sections that each save their own fields, so the answer to "did that save?"
1413
+ appears where the question is asked instead of at the bottom of one long form. */
1414
+ .pf-or-section { padding: 14px 0; border-top: 1px solid var(--border); }
1415
+ .pf-or-section:first-child { border-top: none; padding-top: 4px; }
1416
+ .pf-or-section-title { font-size: 13px; font-weight: 700; margin: 0 0 10px; color: var(--text); }
1417
+
1418
+ .pf-or-status { margin-left: auto; font-size: 11px; color: var(--text-muted); font-weight: 500; }
1419
+ .pf-or-status--on { color: var(--success); }
1420
+
1421
+ /* Key row: the save button sits next to the field it saves. */
1422
+ .pf-or-key-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
1423
+ .pf-or-key-input { flex: 1; min-width: 220px; }
1424
+ .pf-or-links { display: flex; gap: 14px; margin-top: 8px; font-size: 12px; flex-wrap: wrap; }
1425
+
1426
+ .pf-or-models-head { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-bottom: 10px; }
1427
+ .pf-or-hint { font-size: 11px; color: var(--text-muted); display: block; margin-top: 4px; }
1428
+ .pf-or-hint--error { color: var(--danger); }
1429
+ .pf-or-loading { font-size: 12px; color: var(--text-dim); padding: 8px 0; }
1430
+ .pf-or-actions { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-top: 12px; }
1431
+
1432
+ .pf-or-roles-summary,
1433
+ .pf-or-params-summary { cursor: pointer; font-size: 12px; color: var(--text-dim); margin-bottom: 8px; }
1434
+ .pf-or-params-summary { font-size: 13px; font-weight: 700; color: var(--text); }
1435
+
1436
+ .pf-or-inline-field { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-top: 10px; }
1437
+ .pf-or-label--inline { margin: 0; }
1438
+ .pf-or-select--sm { width: auto; min-width: 150px; }
1439
+
1440
+ /* ── Model picker ──
1441
+ Replaces a <select> holding the provider's whole catalogue (336 options on OpenRouter, unlabelled
1442
+ and unsearchable). Prices are tabular so a column of them can actually be compared. */
1443
+ .pf-or-picker { border: 1px solid var(--border); border-radius: var(--radius, 8px); padding: 8px; background: var(--bg-surface); }
1444
+ .pf-or-picker--off { opacity: .55; }
1445
+ .pf-or-picker-head { display: flex; gap: 8px; align-items: center; justify-content: space-between; margin-bottom: 6px; }
1446
+ .pf-or-picked-id { font-size: 12px; word-break: break-all; }
1447
+ .pf-or-picked-none { font-size: 12px; color: var(--text-muted); }
1448
+ .pf-or-picked-warn { font-size: 11px; color: var(--warn); margin-left: 6px; }
1449
+ .pf-or-msearch { width: 100%; }
1450
+
1451
+ .pf-or-mlist { list-style: none; margin: 8px 0 0; padding: 0; max-height: 260px; overflow-y: auto; }
1452
+ .pf-or-mgroup { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: var(--text-muted); padding: 4px 2px; }
1453
+ .pf-or-mrow { display: flex; align-items: center; gap: 4px; border-radius: 6px; }
1454
+ .pf-or-mrow:hover { background: var(--bg-dim); }
1455
+ .pf-or-mrow--on { background: var(--bg-dim); box-shadow: inset 2px 0 0 var(--accent); }
1456
+ .pf-or-mpick {
1457
+ flex: 1; min-width: 0; text-align: left; background: none; border: none; cursor: pointer;
1458
+ padding: 6px 6px; color: var(--text); display: flex; flex-direction: column; gap: 2px;
1459
+ }
1460
+ .pf-or-mname { font-size: 13px; font-weight: 600; }
1461
+ .pf-or-mid { font-size: 11px; color: var(--text-muted); word-break: break-all; }
1462
+ .pf-or-mmeta { display: flex; gap: 10px; flex-wrap: wrap; font-size: 11px; color: var(--text-dim); }
1463
+ .pf-or-mprice { font-variant-numeric: tabular-nums; }
1464
+ .pf-or-mlink { padding: 0 6px; color: var(--text-dim); text-decoration: none; font-size: 13px; }
1465
+ .pf-or-mlink:hover { color: var(--accent); }
1466
+ .pf-or-mempty { font-size: 12px; color: var(--text-muted); padding: 10px 4px; }
1467
+ .pf-or-mall { margin-top: 6px; }
1468
+ .pf-or-mcustom { display: flex; gap: 8px; margin-top: 8px; flex-wrap: wrap; }
1469
+ .pf-or-mcustom .pf-or-input { flex: 1; min-width: 180px; }
1470
+
1471
+ /* ── Speech-to-text ──
1472
+ The measured result is the point of the test: an audio model's listed price carries no unit, so
1473
+ one real transcription says more than any number derived from the catalogue. */
1474
+ .pf-or-stt { border-top: 1px dashed var(--border); padding-top: 12px; margin-top: 12px; }
1475
+ .pf-or-stt-test { margin-top: 10px; }
1476
+ .pf-or-stt-test-head { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
1477
+ .pf-or-stt-result { margin-top: 8px; padding: 8px; border-radius: var(--radius, 8px); background: var(--bg-dim); border: 1px solid var(--border); }
1478
+ .pf-or-stt-text { font-size: 13px; white-space: pre-wrap; word-break: break-word; }
1479
+ .pf-or-stt-meta { font-size: 11px; color: var(--text-muted); margin-top: 6px; font-variant-numeric: tabular-nums; }
1480
+
1481
+ @media (max-width: 520px) {
1482
+ .pf-or-key-row { flex-direction: column; align-items: stretch; }
1483
+ .pf-or-key-input { min-width: 0; }
1484
+ .pf-or-mmeta { flex-direction: column; gap: 2px; }
1485
+ }
1486
+
1487
+ /* ── OpenRouter Autopilot (pf-or-*) ──────────────── */
1488
+ .pf-or-btn-row {
1413
1489
  display: flex;
1414
1490
  gap: 8px;
1415
1491
  align-items: center;
1416
1492
  flex-wrap: wrap;
1417
1493
  }
1418
- .pf-gen-or-run-btn {
1494
+ .pf-or-run-btn {
1419
1495
  display: inline-flex;
1420
1496
  align-items: center;
1421
1497
  gap: 6px;
1422
1498
  }
1423
- .pf-gen-or-run-btn.pf-gen-or-running {
1499
+ .pf-or-run-btn.pf-or-running {
1424
1500
  opacity: 0.8;
1425
1501
  cursor: wait;
1426
1502
  }
1427
- .pf-gen-or-spinner {
1503
+ .pf-or-spinner {
1428
1504
  display: inline-block;
1429
1505
  width: 14px;
1430
1506
  height: 14px;
1431
1507
  border: 2px solid var(--border, #E5E7EB);
1432
1508
  border-top-color: var(--accent, #E8564A);
1433
1509
  border-radius: 50%;
1434
- animation: pf-gen-or-spin 0.7s linear infinite;
1510
+ animation: pf-or-spin 0.7s linear infinite;
1435
1511
  }
1436
- @keyframes pf-gen-or-spin {
1512
+ @keyframes pf-or-spin {
1437
1513
  to { transform: rotate(360deg); }
1438
1514
  }
1439
1515
  .pf-gen-data-viewer {
@@ -1458,7 +1534,7 @@
1458
1534
  white-space: pre-wrap;
1459
1535
  word-break: break-word;
1460
1536
  }
1461
- .pf-gen-or-run-all-bar {
1537
+ .pf-or-run-all-bar {
1462
1538
  display: flex;
1463
1539
  align-items: center;
1464
1540
  gap: 12px;
@@ -1468,7 +1544,7 @@
1468
1544
  border: 1px solid var(--border, #E5E7EB);
1469
1545
  border-radius: 8px;
1470
1546
  }
1471
- .pf-gen-or-run-all-status {
1547
+ .pf-or-run-all-status {
1472
1548
  display: flex;
1473
1549
  align-items: center;
1474
1550
  gap: 8px;
@@ -2058,8 +2134,8 @@
2058
2134
  .pf-gen-test-scope-compact {
2059
2135
  display: flex; gap: 8px; align-items: center; margin: 0; font-size: 0.85em;
2060
2136
  }
2061
- .pf-gen-test-scope-compact .pf-gen-or-radio-label { margin: 0; white-space: nowrap; }
2062
- .pf-gen-or-run-all-controls {
2137
+ .pf-gen-test-scope-compact .pf-or-radio-label { margin: 0; white-space: nowrap; }
2138
+ .pf-or-run-all-controls {
2063
2139
  display: flex; align-items: center; gap: 12px; width: 100%;
2064
2140
  }
2065
2141
 
@@ -3449,29 +3525,29 @@
3449
3525
  .pf-adk-sev--warn { background: color-mix(in srgb, var(--warn) 18%, transparent); color: var(--warn); }
3450
3526
  .pf-adk-sev--info { background: var(--bg-dim); color: var(--muted); }
3451
3527
  .pf-adk-empty { color: var(--muted); font-size: 0.9rem; padding: 0.9rem; margin-bottom: 0.7rem; }
3452
-
3453
- /* ── AI transparency card (pf-aitr-*) — TARGET-058 Phase 8 ──
3454
- The owner's own view of what they published with a model in it. The one visual decision here is
3455
- that the "public and unlabelled" tile changes colour only when the number is ABOVE zero: a
3456
- compliance surface that is always red teaches people to stop looking at it. */
3457
- .pf-aitr-head { display: flex; align-items: center; justify-content: space-between; gap: 0.8rem; width: 100%; background: none; border: 0; padding: 0; cursor: pointer; text-align: left; color: inherit; font: inherit; }
3458
- .pf-aitr-chevron { color: var(--muted); font-size: 1.1rem; line-height: 1; flex-shrink: 0; }
3459
- .pf-aitr-body { margin-top: 0.9rem; border-top: 1px solid var(--border); padding-top: 0.9rem; }
3460
- /* auto-fit, so four tiles become two rows on a phone instead of overflowing the card. */
3461
- .pf-aitr-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(7.5rem, 1fr)); gap: 0.6rem; }
3462
- .pf-aitr-stat { display: flex; flex-direction: column; gap: 0.15rem; padding: 0.7rem 0.8rem; border: 1px solid var(--border); border-radius: 10px; background: var(--bg-dim); min-width: 0; }
3463
- .pf-aitr-stat-warn { border-color: var(--warn); }
3464
- .pf-aitr-stat-warn .pf-aitr-num { color: var(--warn); }
3465
- .pf-aitr-num { font-size: 1.5rem; font-weight: 700; line-height: 1.1; }
3466
- .pf-aitr-lbl { font-size: 0.75rem; color: var(--muted); overflow-wrap: anywhere; }
3467
- .pf-aitr-note { margin: 0.8rem 0 0; font-size: 0.82rem; color: var(--muted); }
3468
- .pf-aitr-warn-line { margin: 0.9rem 0 0.4rem; font-size: 0.85rem; color: var(--fg); }
3469
- .pf-aitr-error { margin: 0; font-size: 0.85rem; color: var(--danger); }
3470
- .pf-aitr-muted { margin: 0.5rem 0 0; font-size: 0.8rem; color: var(--muted); }
3471
- .pf-aitr-sub { margin: 1.2rem 0 0.4rem; font-size: 0.9rem; font-weight: 600; }
3472
- .pf-aitr-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.4rem; }
3473
- .pf-aitr-row, .pf-aitr-policy { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.3rem 0.7rem; padding: 0.55rem 0.7rem; border: 1px solid var(--border); border-radius: 8px; background: var(--bg-dim); min-width: 0; }
3474
- .pf-aitr-policy { flex-direction: column; }
3475
- .pf-aitr-row-main { font-size: 0.85rem; font-weight: 600; overflow-wrap: anywhere; }
3476
- .pf-aitr-row-meta { font-size: 0.78rem; color: var(--muted); overflow-wrap: anywhere; }
3477
- .pf-aitr-row-link { font-size: 0.78rem; color: var(--accent); }
3528
+
3529
+ /* ── AI transparency card (pf-aitr-*) — TARGET-058 Phase 8 ──
3530
+ The owner's own view of what they published with a model in it. The one visual decision here is
3531
+ that the "public and unlabelled" tile changes colour only when the number is ABOVE zero: a
3532
+ compliance surface that is always red teaches people to stop looking at it. */
3533
+ .pf-aitr-head { display: flex; align-items: center; justify-content: space-between; gap: 0.8rem; width: 100%; background: none; border: 0; padding: 0; cursor: pointer; text-align: left; color: inherit; font: inherit; }
3534
+ .pf-aitr-chevron { color: var(--muted); font-size: 1.1rem; line-height: 1; flex-shrink: 0; }
3535
+ .pf-aitr-body { margin-top: 0.9rem; border-top: 1px solid var(--border); padding-top: 0.9rem; }
3536
+ /* auto-fit, so four tiles become two rows on a phone instead of overflowing the card. */
3537
+ .pf-aitr-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(7.5rem, 1fr)); gap: 0.6rem; }
3538
+ .pf-aitr-stat { display: flex; flex-direction: column; gap: 0.15rem; padding: 0.7rem 0.8rem; border: 1px solid var(--border); border-radius: 10px; background: var(--bg-dim); min-width: 0; }
3539
+ .pf-aitr-stat-warn { border-color: var(--warn); }
3540
+ .pf-aitr-stat-warn .pf-aitr-num { color: var(--warn); }
3541
+ .pf-aitr-num { font-size: 1.5rem; font-weight: 700; line-height: 1.1; }
3542
+ .pf-aitr-lbl { font-size: 0.75rem; color: var(--muted); overflow-wrap: anywhere; }
3543
+ .pf-aitr-note { margin: 0.8rem 0 0; font-size: 0.82rem; color: var(--muted); }
3544
+ .pf-aitr-warn-line { margin: 0.9rem 0 0.4rem; font-size: 0.85rem; color: var(--fg); }
3545
+ .pf-aitr-error { margin: 0; font-size: 0.85rem; color: var(--danger); }
3546
+ .pf-aitr-muted { margin: 0.5rem 0 0; font-size: 0.8rem; color: var(--muted); }
3547
+ .pf-aitr-sub { margin: 1.2rem 0 0.4rem; font-size: 0.9rem; font-weight: 600; }
3548
+ .pf-aitr-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.4rem; }
3549
+ .pf-aitr-row, .pf-aitr-policy { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.3rem 0.7rem; padding: 0.55rem 0.7rem; border: 1px solid var(--border); border-radius: 8px; background: var(--bg-dim); min-width: 0; }
3550
+ .pf-aitr-policy { flex-direction: column; }
3551
+ .pf-aitr-row-main { font-size: 0.85rem; font-weight: 600; overflow-wrap: anywhere; }
3552
+ .pf-aitr-row-meta { font-size: 0.78rem; color: var(--muted); overflow-wrap: anywhere; }
3553
+ .pf-aitr-row-link { font-size: 0.78rem; color: var(--accent); }
@@ -29,6 +29,11 @@
29
29
  centred H1 it sat inset from the headings and looked like a caption. */
30
30
  .tp-hero .ld-hero-sub { max-width: 62ch; margin: 0; text-align: left; }
31
31
 
32
+ /* "Somewhere to see one" — one sentence under the limits, pointing at something on this node that
33
+ actually carries a label. Quiet: it is evidence, not a call to action. */
34
+ .tp-seeit { margin: var(--sp-4, 16px) 0 0; max-width: 62ch; line-height: 1.6; color: var(--text-dim); font-size: var(--text-sm); }
35
+ .tp-seeit a { color: var(--accent); font-weight: 600; }
36
+
32
37
  .tp-section { margin: var(--sp-8, 32px) 0; }
33
38
  .tp-h2 { font-size: var(--text-xl); font-weight: 700; margin: 0 0 var(--sp-3, 12px); color: var(--text); }
34
39
  .tp-p { margin: 0 0 var(--sp-3, 12px); max-width: 62ch; line-height: 1.6; color: var(--text); }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * @file audio-recorder.js
3
+ * @description Microphone capture for voice messages and the transcription test. Plain JS, no
4
+ * framework: one function starts a recording and hands back the controls to stop or cancel it.
5
+ *
6
+ * FORMAT: whatever the browser actually produces, negotiated with isTypeSupported — webm/opus in
7
+ * Chrome and Firefox, mp4/AAC in Safari. Not WAV. MediaRecorder cannot produce WAV without hand
8
+ * muxing PCM, and the result is roughly ten times the bytes for the same speech (a minute of 16 kHz
9
+ * mono WAV is ~2 MB against ~180 kB of Opus) with nothing gained: every browser plays these back,
10
+ * and OpenRouter's transcription endpoint lists webm, ogg, m4a and aac among its accepted formats.
11
+ * The extra megabytes would be paid for in the storage quota, the transfer and the federation copy.
12
+ *
13
+ * The microphone track is stopped on every exit path. A forgotten track leaves the browser's
14
+ * recording indicator lit, which reads to a user as an application still listening to them.
15
+ * @structure isRecordingSupported() · pickMimeType() · startRecording() · fmtDuration()
16
+ * @usage const rec = await startRecording({ onLevel, maxSeconds }); const file = await rec.stop();
17
+ * @version-history
18
+ * v1.0.0 — 2026-08-01 — Initial version for inbox voice messages + the settings STT test.
19
+ */
20
+
21
+ /** Preference order: Opus where available (smallest for speech), then whatever the browser offers. */
22
+ const CANDIDATES = [
23
+ 'audio/webm;codecs=opus',
24
+ 'audio/webm',
25
+ 'audio/ogg;codecs=opus',
26
+ 'audio/mp4', // Safari
27
+ 'audio/aac',
28
+ ];
29
+
30
+ export function isRecordingSupported() {
31
+ return typeof window !== 'undefined'
32
+ && typeof window.MediaRecorder !== 'undefined'
33
+ && !!navigator?.mediaDevices?.getUserMedia;
34
+ }
35
+
36
+ /** The first candidate this browser admits to supporting, or '' to let it choose its own default. */
37
+ export function pickMimeType() {
38
+ if (typeof window === 'undefined' || !window.MediaRecorder?.isTypeSupported) return '';
39
+ for (const type of CANDIDATES) {
40
+ try { if (window.MediaRecorder.isTypeSupported(type)) return type; } catch { /* keep looking */ } // eslint-disable-line aimeat/no-silent-catch -- a browser refusing to answer about a type IS the answer: try the next one
41
+ }
42
+ return '';
43
+ }
44
+
45
+ /** File extension for a recorded blob, from its mime type. */
46
+ function extFor(mime) {
47
+ if (!mime) return 'webm';
48
+ if (mime.includes('mp4')) return 'm4a';
49
+ if (mime.includes('aac')) return 'aac';
50
+ if (mime.includes('ogg')) return 'ogg';
51
+ return 'webm';
52
+ }
53
+
54
+ /** Seconds → "0:07" / "12:03". */
55
+ export function fmtDuration(seconds) {
56
+ const s = Math.max(0, Math.round(Number(seconds) || 0));
57
+ return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`;
58
+ }
59
+
60
+ /**
61
+ * Start recording from the default microphone.
62
+ *
63
+ * @param {object} [opts]
64
+ * @param {(level: number) => void} [opts.onLevel] 0..1 input level, ~20/s while recording
65
+ * @param {(seconds: number) => void} [opts.onTick] elapsed seconds, once a second
66
+ * @param {number} [opts.maxSeconds] hard stop; the recording is kept, not discarded
67
+ * @param {() => void} [opts.onAutoStop] fired when maxSeconds ended the recording
68
+ * @returns {Promise<{ stop: () => Promise<{file: File, durationSeconds: number}>, cancel: () => void, mimeType: string }>}
69
+ */
70
+ export async function startRecording(opts = {}) {
71
+ if (!isRecordingSupported()) {
72
+ throw new Error('unsupported');
73
+ }
74
+ const stream = await navigator.mediaDevices.getUserMedia({
75
+ // Speech, not music: the browser's own cleanup is worth more than the fidelity it costs.
76
+ audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true },
77
+ });
78
+
79
+ const mimeType = pickMimeType();
80
+ const recorder = new MediaRecorder(stream, mimeType ? { mimeType } : undefined);
81
+ const chunks = [];
82
+ const startedAt = Date.now();
83
+ let stopped = false;
84
+
85
+ // Level metering. Deliberately minimal: a number, not a waveform. Its whole job is to answer
86
+ // "is the microphone hearing me", which a silent countdown cannot.
87
+ let audioCtx = null;
88
+ let rafId = 0;
89
+ if (opts.onLevel && typeof window.AudioContext !== 'undefined') {
90
+ try {
91
+ audioCtx = new AudioContext();
92
+ const source = audioCtx.createMediaStreamSource(stream);
93
+ const analyser = audioCtx.createAnalyser();
94
+ analyser.fftSize = 512;
95
+ source.connect(analyser);
96
+ const buf = new Uint8Array(analyser.frequencyBinCount);
97
+ const tick = () => {
98
+ analyser.getByteTimeDomainData(buf);
99
+ let peak = 0;
100
+ for (let i = 0; i < buf.length; i++) peak = Math.max(peak, Math.abs(buf[i] - 128) / 128);
101
+ opts.onLevel(peak);
102
+ rafId = requestAnimationFrame(tick);
103
+ };
104
+ rafId = requestAnimationFrame(tick);
105
+ } catch (err) {
106
+ // Metering is decoration; a browser that refuses an AudioContext still records fine.
107
+ audioCtx = null;
108
+ if (typeof console !== 'undefined') console.warn('[audio-recorder] level meter unavailable:', err?.message || err);
109
+ }
110
+ }
111
+
112
+ let tickTimer = 0;
113
+ if (opts.onTick) {
114
+ tickTimer = setInterval(() => opts.onTick(Math.floor((Date.now() - startedAt) / 1000)), 1000);
115
+ }
116
+
117
+ let autoStopTimer = 0;
118
+ if (opts.maxSeconds > 0) {
119
+ autoStopTimer = setTimeout(() => {
120
+ if (!stopped && recorder.state === 'recording') {
121
+ recorder.stop(); // the blob is KEPT — hitting the cap is not losing the take
122
+ opts.onAutoStop?.();
123
+ }
124
+ }, opts.maxSeconds * 1000);
125
+ }
126
+
127
+ recorder.ondataavailable = (e) => { if (e.data && e.data.size > 0) chunks.push(e.data); };
128
+
129
+ const teardown = () => {
130
+ stopped = true;
131
+ if (tickTimer) clearInterval(tickTimer);
132
+ if (autoStopTimer) clearTimeout(autoStopTimer);
133
+ if (rafId) cancelAnimationFrame(rafId);
134
+ if (audioCtx) audioCtx.close().catch(() => { /* already closing */ }); // eslint-disable-line aimeat/no-silent-catch -- closing a closed context is not an error worth reporting
135
+ // The one line that must never be skipped: leave this out and the browser keeps showing the
136
+ // user that something is recording them.
137
+ stream.getTracks().forEach((tr) => tr.stop());
138
+ };
139
+
140
+ recorder.start();
141
+
142
+ return {
143
+ mimeType: recorder.mimeType || mimeType || 'audio/webm',
144
+ /** Stop and resolve with the recorded File plus its measured length. */
145
+ stop() {
146
+ return new Promise((resolve, reject) => {
147
+ if (stopped) { reject(new Error('already stopped')); return; }
148
+ recorder.onstop = () => {
149
+ const durationSeconds = Math.max(0.1, (Date.now() - startedAt) / 1000);
150
+ teardown();
151
+ const type = recorder.mimeType || mimeType || 'audio/webm';
152
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
153
+ const file = new File(chunks, `voice-${stamp}.${extFor(type)}`, { type });
154
+ resolve({ file, durationSeconds });
155
+ };
156
+ recorder.onerror = (e) => { teardown(); reject(e?.error || new Error('recording failed')); };
157
+ // Already inactive (an auto-stop at maxSeconds got there first): resolve from the chunks we
158
+ // have instead of waiting for an onstop that will never fire.
159
+ if (recorder.state === 'recording') recorder.stop();
160
+ else recorder.onstop?.(new Event('stop'));
161
+ });
162
+ },
163
+ /** Throw the take away and release the microphone. */
164
+ cancel() {
165
+ if (stopped) return;
166
+ recorder.onstop = () => {};
167
+ try { if (recorder.state === 'recording') recorder.stop(); } catch { /* already stopping */ } // eslint-disable-line aimeat/no-silent-catch -- the goal is releasing the microphone below, which happens either way
168
+ teardown();
169
+ },
170
+ };
171
+ }
@@ -5,9 +5,12 @@
5
5
  * block), plus small storage helpers used to upload and resolve message attachments.
6
6
  * @structure send / listInbox / listConversations / getConversation / markConversationRead /
7
7
  * markRead / deleteMessage / listRequests / acceptRequest / blockContact / listContacts /
8
- * uploadAttachment / attachmentUrl
8
+ * uploadAttachment / attachmentUrl / transcribeAttachment
9
9
  * @usage import * as messages from '/js/services/messages.js';
10
10
  * @version-history
11
+ * v1.4.0 -- 2026-08-01 -- Voice messages: uploadAttachment carries a recording's measured
12
+ * `duration_seconds` into the descriptor (so a thread can show "0:14" before fetching the audio),
13
+ * and transcribeAttachment() turns one voice attachment into text on the caller's own copy.
11
14
  * v1.0.0 -- 2026-06-16 -- Initial creation for user-to-user messaging (layer 5: Inbox tab).
12
15
  * v1.0.1 -- 2026-06-19 -- JSDoc type annotations for frontend type-checking
13
16
  * v1.1.0 -- 2026-06-23 -- send() carries the optional `interactive` payload (federated AskUserQuestion).
@@ -177,7 +180,23 @@ export async function uploadAttachment(file, kindOverride) {
177
180
  const kind = kindOverride || (mime.startsWith('image/') ? 'image'
178
181
  : mime.startsWith('audio/') ? 'audio'
179
182
  : mime.startsWith('video/') ? 'video' : 'file');
180
- return { storage_key: key, mime, size: file.size, kind, name: file.name };
183
+ const desc = { storage_key: key, mime, size: file.size, kind, name: file.name };
184
+ // A voice recording arrives with its measured length attached (composer addRecording). Carrying it
185
+ // in the descriptor lets the recipient's thread show "0:14" without downloading the audio first.
186
+ if (Number(file.durationSeconds) > 0) desc.duration_seconds = Number(file.durationSeconds);
187
+ return desc;
188
+ }
189
+
190
+ /** Transcribe a voice attachment on the caller's own copy of a message, with the caller's own AI key.
191
+ * Idempotent server-side: a second call returns the stored transcript instead of paying again. */
192
+ export async function transcribeAttachment(messageId, attachmentId, opts = {}) {
193
+ return api(`/v1/messages/${enc(messageId)}/attachments/${enc(attachmentId)}/transcribe`, {
194
+ method: 'POST',
195
+ body: JSON.stringify({ force: !!opts.force, model: opts.model, language: opts.language }),
196
+ // Speech-to-text is a provider round trip over an audio file; the default 30 s client timeout
197
+ // cuts a perfectly good transcription off at the knees.
198
+ timeoutMs: 180_000,
199
+ });
181
200
  }
182
201
 
183
202
  /** Resolve a presigned, no-auth download URL for one of the caller's own storage keys (for <img>). */
@@ -97,6 +97,7 @@
97
97
  <link rel="stylesheet" href="/css/components/markdown.css">
98
98
  <link rel="stylesheet" href="/css/components/contact-picker.css">
99
99
  <link rel="stylesheet" href="/css/components/app-sandbox.css">
100
+ <link rel="stylesheet" href="/css/components/voice-recorder.css">
100
101
  <link rel="stylesheet" href="/css/views/app-grant.css">
101
102
  <link rel="stylesheet" href="/css/views/invite-accept.css">
102
103
  <!-- View CSS preloaded so they are ready before any view renders (prevents FOUC) -->
@@ -148,6 +149,7 @@
148
149
  "/js/recents.js": "/js/recents.js",
149
150
  "/js/services/admin.js": "/js/services/admin.js",
150
151
  "/js/services/agents.js": "/js/services/agents.js",
152
+ "/js/services/audio-recorder.js": "/js/services/audio-recorder.js",
151
153
  "/js/services/ecosystem.js": "/js/services/ecosystem.js",
152
154
  "/js/services/apps.js": "/js/services/apps.js",
153
155
  "/js/services/auth.js": "/js/services/auth.js",
@@ -189,6 +191,7 @@
189
191
  "/js/components/tag-cloud.js": "/js/components/tag-cloud.js",
190
192
  "/js/components/tag-editor.js": "/js/components/tag-editor.js",
191
193
  "/components/Modal.js": "/components/Modal.js",
194
+ "/components/VoiceRecorder.js": "/components/VoiceRecorder.js",
192
195
  "/components/useViewCSS.js": "/components/useViewCSS.js",
193
196
  "/components/Markdown.js": "/components/Markdown.js",
194
197
  "/components/LinkPreview.js": "/components/LinkPreview.js",