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.
- package/dist/.env.example +33 -0
- package/dist/build-stamp.json +13 -0
- package/dist/locales/en.json +186 -20
- package/dist/locales/fi.json +186 -20
- package/dist/public/components/VoiceRecorder.js +116 -0
- package/dist/public/connection-done.html +53 -0
- package/dist/public/css/components/voice-recorder.css +66 -0
- package/dist/public/css/views/inbox.css +41 -0
- package/dist/public/css/views/profile.css +171 -95
- package/dist/public/css/views/transparency.css +5 -0
- package/dist/public/js/services/audio-recorder.js +171 -0
- package/dist/public/js/services/messages.js +21 -2
- package/dist/public/spa.html +3 -0
- package/dist/public/views/landing.js +4 -0
- package/dist/public/views/profile/access-tab/connections.js +264 -0
- package/dist/public/views/profile/access-tab.js +2 -0
- package/dist/public/views/profile/ai-transparency-card.js +22 -17
- package/dist/public/views/profile/inbox-tab/components.js +60 -10
- package/dist/public/views/profile/inbox-tab/helpers.js +33 -0
- package/dist/public/views/profile/inbox-tab/panels.js +6 -1
- package/dist/public/views/profile/inbox-tab/use-voice.js +50 -0
- package/dist/public/views/profile/inbox-tab/voice-parts.js +111 -0
- package/dist/public/views/profile/inbox-tab.js +18 -24
- package/dist/public/views/profile/openrouter/budget-panel.js +224 -0
- package/dist/public/views/profile/openrouter/model-picker.js +150 -0
- package/dist/public/views/profile/openrouter/pricing.js +139 -0
- package/dist/public/views/profile/openrouter-settings.js +425 -488
- package/dist/public/views/transparency.js +50 -0
- package/dist/scripts/build-sdk-libs.d.ts.map +1 -1
- package/dist/scripts/build-sdk-libs.js +1 -0
- package/dist/scripts/build-sdk-libs.js.map +1 -1
- package/dist/scripts/check-ai-disclosure.d.ts.map +1 -1
- package/dist/scripts/check-ai-disclosure.js +182 -0
- package/dist/scripts/check-ai-disclosure.js.map +1 -1
- package/dist/src/cli/connect/ai-provenance-carry.d.ts +247 -0
- package/dist/src/cli/connect/ai-provenance-carry.d.ts.map +1 -0
- package/dist/src/cli/connect/ai-provenance-carry.js +334 -0
- package/dist/src/cli/connect/ai-provenance-carry.js.map +1 -0
- package/dist/src/cli/connect/api-client.d.ts +11 -0
- package/dist/src/cli/connect/api-client.d.ts.map +1 -1
- package/dist/src/cli/connect/api-client.js +2 -0
- package/dist/src/cli/connect/api-client.js.map +1 -1
- package/dist/src/cli/connect/mcp/local-server.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/local-server.js +11 -0
- package/dist/src/cli/connect/mcp/local-server.js.map +1 -1
- package/dist/src/cli/connect/mcp/tools/agent-messages.d.ts +2 -0
- package/dist/src/cli/connect/mcp/tools/agent-messages.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/agent-messages.js +5 -2
- package/dist/src/cli/connect/mcp/tools/agent-messages.js.map +1 -1
- package/dist/src/cli/connect/mcp/tools/agent-tasks.d.ts +2 -0
- package/dist/src/cli/connect/mcp/tools/agent-tasks.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/agent-tasks.js +5 -2
- package/dist/src/cli/connect/mcp/tools/agent-tasks.js.map +1 -1
- package/dist/src/cli/connect/mcp/tools/apps.d.ts +3 -0
- package/dist/src/cli/connect/mcp/tools/apps.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/apps.js +13 -5
- package/dist/src/cli/connect/mcp/tools/apps.js.map +1 -1
- package/dist/src/cli/connect/mcp/tools/boards.d.ts +3 -0
- package/dist/src/cli/connect/mcp/tools/boards.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/boards.js +5 -2
- package/dist/src/cli/connect/mcp/tools/boards.js.map +1 -1
- package/dist/src/cli/connect/mcp/tools/core.d.ts +5 -0
- package/dist/src/cli/connect/mcp/tools/core.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/core.js +25 -5
- package/dist/src/cli/connect/mcp/tools/core.js.map +1 -1
- package/dist/src/cli/connect/mcp/tools/dm-messages.d.ts +2 -0
- package/dist/src/cli/connect/mcp/tools/dm-messages.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/dm-messages.js +11 -6
- package/dist/src/cli/connect/mcp/tools/dm-messages.js.map +1 -1
- package/dist/src/cli/connect/mcp/tools/exchange.d.ts +2 -0
- package/dist/src/cli/connect/mcp/tools/exchange.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/exchange.js +8 -2
- package/dist/src/cli/connect/mcp/tools/exchange.js.map +1 -1
- package/dist/src/cli/connect/mcp/tools/knowledge.d.ts +3 -0
- package/dist/src/cli/connect/mcp/tools/knowledge.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/knowledge.js +7 -3
- package/dist/src/cli/connect/mcp/tools/knowledge.js.map +1 -1
- package/dist/src/cli/connect/mcp/tools/organisms.d.ts +2 -0
- package/dist/src/cli/connect/mcp/tools/organisms.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/organisms.js +7 -2
- package/dist/src/cli/connect/mcp/tools/organisms.js.map +1 -1
- package/dist/src/cli/connect/mcp/tools/workspaces.d.ts +3 -0
- package/dist/src/cli/connect/mcp/tools/workspaces.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/workspaces.js +8 -2
- package/dist/src/cli/connect/mcp/tools/workspaces.js.map +1 -1
- package/dist/src/cli/connect/tool-call-defs-core.d.ts +3 -0
- package/dist/src/cli/connect/tool-call-defs-core.d.ts.map +1 -1
- package/dist/src/cli/connect/tool-call-defs-core.js +7 -0
- package/dist/src/cli/connect/tool-call-defs-core.js.map +1 -1
- package/dist/src/cli/connect/tool-call.d.ts +3 -0
- package/dist/src/cli/connect/tool-call.d.ts.map +1 -1
- package/dist/src/cli/connect/tool-call.js +11 -1
- package/dist/src/cli/connect/tool-call.js.map +1 -1
- package/dist/src/config-types.d.ts +14 -0
- package/dist/src/config-types.d.ts.map +1 -1
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/config.js +10 -0
- package/dist/src/config.js.map +1 -1
- package/dist/src/data/public-pages.d.ts +12 -0
- package/dist/src/data/public-pages.d.ts.map +1 -1
- package/dist/src/data/public-pages.js +12 -0
- package/dist/src/data/public-pages.js.map +1 -1
- package/dist/src/generated/api-types.d.ts +969 -85
- package/dist/src/generated/api-types.d.ts.map +1 -1
- package/dist/src/mcp/ai-provenance-input.d.ts +45 -2
- package/dist/src/mcp/ai-provenance-input.d.ts.map +1 -1
- package/dist/src/mcp/ai-provenance-input.js +48 -2
- package/dist/src/mcp/ai-provenance-input.js.map +1 -1
- package/dist/src/mcp/apps.d.ts +7 -0
- package/dist/src/mcp/apps.d.ts.map +1 -1
- package/dist/src/mcp/apps.js +20 -2
- package/dist/src/mcp/apps.js.map +1 -1
- package/dist/src/models/ai-provenance-schemas.d.ts +7 -1
- package/dist/src/models/ai-provenance-schemas.d.ts.map +1 -1
- package/dist/src/models/ai-provenance-schemas.js +11 -1
- package/dist/src/models/ai-provenance-schemas.js.map +1 -1
- package/dist/src/models/connection-schemas.d.ts +203 -0
- package/dist/src/models/connection-schemas.d.ts.map +1 -0
- package/dist/src/models/connection-schemas.js +33 -0
- package/dist/src/models/connection-schemas.js.map +1 -0
- package/dist/src/models/message-schemas.d.ts +40 -0
- package/dist/src/models/message-schemas.d.ts.map +1 -1
- package/dist/src/models/message-schemas.js +23 -0
- package/dist/src/models/message-schemas.js.map +1 -1
- package/dist/src/routes/ai-provenance.d.ts +6 -0
- package/dist/src/routes/ai-provenance.d.ts.map +1 -1
- package/dist/src/routes/ai-provenance.js +34 -1
- package/dist/src/routes/ai-provenance.js.map +1 -1
- package/dist/src/routes/ai-transparency.d.ts +6 -1
- package/dist/src/routes/ai-transparency.d.ts.map +1 -1
- package/dist/src/routes/ai-transparency.js +17 -3
- package/dist/src/routes/ai-transparency.js.map +1 -1
- package/dist/src/routes/ai.d.ts +5 -0
- package/dist/src/routes/ai.d.ts.map +1 -1
- package/dist/src/routes/ai.js +83 -0
- package/dist/src/routes/ai.js.map +1 -1
- package/dist/src/routes/app-grants.d.ts.map +1 -1
- package/dist/src/routes/app-grants.js +4 -0
- package/dist/src/routes/app-grants.js.map +1 -1
- package/dist/src/routes/apps/drafts.d.ts +3 -0
- package/dist/src/routes/apps/drafts.d.ts.map +1 -1
- package/dist/src/routes/apps/drafts.js +13 -0
- package/dist/src/routes/apps/drafts.js.map +1 -1
- package/dist/src/routes/apps/publish.d.ts +5 -0
- package/dist/src/routes/apps/publish.d.ts.map +1 -1
- package/dist/src/routes/apps/publish.js +21 -5
- package/dist/src/routes/apps/publish.js.map +1 -1
- package/dist/src/routes/connections.d.ts +35 -0
- package/dist/src/routes/connections.d.ts.map +1 -0
- package/dist/src/routes/connections.js +406 -0
- package/dist/src/routes/connections.js.map +1 -0
- package/dist/src/routes/libs.js +1 -1
- package/dist/src/routes/libs.js.map +1 -1
- package/dist/src/routes/messages.d.ts +5 -0
- package/dist/src/routes/messages.d.ts.map +1 -1
- package/dist/src/routes/messages.js +92 -0
- package/dist/src/routes/messages.js.map +1 -1
- package/dist/src/routes/openrouter.d.ts +6 -1
- package/dist/src/routes/openrouter.d.ts.map +1 -1
- package/dist/src/routes/openrouter.js +37 -7
- package/dist/src/routes/openrouter.js.map +1 -1
- package/dist/src/routes/upload.d.ts +5 -0
- package/dist/src/routes/upload.d.ts.map +1 -1
- package/dist/src/routes/upload.js +32 -0
- package/dist/src/routes/upload.js.map +1 -1
- package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
- package/dist/src/server-bootstrap/routes-loader.js +2 -0
- package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
- package/dist/src/services/ai-completion.d.ts +48 -0
- package/dist/src/services/ai-completion.d.ts.map +1 -1
- package/dist/src/services/ai-completion.js +112 -72
- package/dist/src/services/ai-completion.js.map +1 -1
- package/dist/src/services/ai-disclosure.d.ts +6 -0
- package/dist/src/services/ai-disclosure.d.ts.map +1 -1
- package/dist/src/services/ai-disclosure.js +20 -2
- package/dist/src/services/ai-disclosure.js.map +1 -1
- package/dist/src/services/ai-provenance-page.d.ts +59 -0
- package/dist/src/services/ai-provenance-page.d.ts.map +1 -0
- package/dist/src/services/ai-provenance-page.js +185 -0
- package/dist/src/services/ai-provenance-page.js.map +1 -0
- package/dist/src/services/ai-provenance.d.ts +6 -0
- package/dist/src/services/ai-provenance.d.ts.map +1 -1
- package/dist/src/services/ai-provenance.js +5 -0
- package/dist/src/services/ai-provenance.js.map +1 -1
- package/dist/src/services/ai-transcription.d.ts +66 -0
- package/dist/src/services/ai-transcription.d.ts.map +1 -0
- package/dist/src/services/ai-transcription.js +90 -0
- package/dist/src/services/ai-transcription.js.map +1 -0
- package/dist/src/services/app-publish.d.ts.map +1 -1
- package/dist/src/services/app-publish.js +16 -1
- package/dist/src/services/app-publish.js.map +1 -1
- package/dist/src/services/build-app-prompt.d.ts.map +1 -1
- package/dist/src/services/build-app-prompt.js +2 -1
- package/dist/src/services/build-app-prompt.js.map +1 -1
- package/dist/src/services/connections/attach.d.ts +62 -0
- package/dist/src/services/connections/attach.d.ts.map +1 -0
- package/dist/src/services/connections/attach.js +168 -0
- package/dist/src/services/connections/attach.js.map +1 -0
- package/dist/src/services/connections/credential.d.ts +39 -0
- package/dist/src/services/connections/credential.d.ts.map +1 -0
- package/dist/src/services/connections/credential.js +66 -0
- package/dist/src/services/connections/credential.js.map +1 -0
- package/dist/src/services/connections/instance.d.ts +65 -0
- package/dist/src/services/connections/instance.d.ts.map +1 -0
- package/dist/src/services/connections/instance.js +144 -0
- package/dist/src/services/connections/instance.js.map +1 -0
- package/dist/src/services/connections/oauth.d.ts +77 -0
- package/dist/src/services/connections/oauth.d.ts.map +1 -0
- package/dist/src/services/connections/oauth.js +290 -0
- package/dist/src/services/connections/oauth.js.map +1 -0
- package/dist/src/services/connections/providers.d.ts +118 -0
- package/dist/src/services/connections/providers.d.ts.map +1 -0
- package/dist/src/services/connections/providers.js +260 -0
- package/dist/src/services/connections/providers.js.map +1 -0
- package/dist/src/services/connections/publish-gate.d.ts +103 -0
- package/dist/src/services/connections/publish-gate.d.ts.map +1 -0
- package/dist/src/services/connections/publish-gate.js +0 -0
- package/dist/src/services/connections/publish-gate.js.map +1 -0
- package/dist/src/services/connections/publish.d.ts +60 -0
- package/dist/src/services/connections/publish.d.ts.map +1 -0
- package/dist/src/services/connections/publish.js +340 -0
- package/dist/src/services/connections/publish.js.map +1 -0
- package/dist/src/services/connections/refresh.d.ts +55 -0
- package/dist/src/services/connections/refresh.d.ts.map +1 -0
- package/dist/src/services/connections/refresh.js +307 -0
- package/dist/src/services/connections/refresh.js.map +1 -0
- package/dist/src/services/markdown-negotiation.d.ts +17 -0
- package/dist/src/services/markdown-negotiation.d.ts.map +1 -1
- package/dist/src/services/markdown-negotiation.js +15 -0
- package/dist/src/services/markdown-negotiation.js.map +1 -1
- package/dist/src/services/message-send.d.ts.map +1 -1
- package/dist/src/services/message-send.js +16 -2
- package/dist/src/services/message-send.js.map +1 -1
- package/dist/src/services/openrouter.d.ts +67 -1
- package/dist/src/services/openrouter.d.ts.map +1 -1
- package/dist/src/services/openrouter.js +102 -11
- package/dist/src/services/openrouter.js.map +1 -1
- package/dist/src/services/upload-token.d.ts +5 -1
- package/dist/src/services/upload-token.d.ts.map +1 -1
- package/dist/src/services/upload-token.js +13 -1
- package/dist/src/services/upload-token.js.map +1 -1
- package/dist/src/services/workflow/engine-util.d.ts +18 -0
- package/dist/src/services/workflow/engine-util.d.ts.map +1 -1
- package/dist/src/services/workflow/engine-util.js +27 -0
- package/dist/src/services/workflow/engine-util.js.map +1 -1
- package/dist/src/services/workflow/engine.d.ts.map +1 -1
- package/dist/src/services/workflow/engine.js +4 -2
- package/dist/src/services/workflow/engine.js.map +1 -1
- package/dist/src/storage/interface.d.ts +2 -1
- package/dist/src/storage/interface.d.ts.map +1 -1
- package/dist/src/storage/providers/postgres-kysely/db-types.d.ts +65 -0
- package/dist/src/storage/providers/postgres-kysely/db-types.d.ts.map +1 -1
- package/dist/src/storage/providers/postgres-kysely/index.d.ts.map +1 -1
- package/dist/src/storage/providers/postgres-kysely/index.js +2 -1
- package/dist/src/storage/providers/postgres-kysely/index.js.map +1 -1
- package/dist/src/storage/providers/postgres-kysely/methods/connections.d.ts +44 -0
- package/dist/src/storage/providers/postgres-kysely/methods/connections.d.ts.map +1 -0
- package/dist/src/storage/providers/postgres-kysely/methods/connections.js +308 -0
- package/dist/src/storage/providers/postgres-kysely/methods/connections.js.map +1 -0
- package/dist/src/storage/providers/postgres-kysely/methods/node-infra.d.ts.map +1 -1
- package/dist/src/storage/providers/postgres-kysely/methods/node-infra.js +4 -2
- package/dist/src/storage/providers/postgres-kysely/methods/node-infra.js.map +1 -1
- package/dist/src/storage/providers/postgres-kysely/migrations/0021_connections.sql +116 -0
- package/dist/src/storage/providers/postgres-kysely/migrations/0022_provider_clients.sql +36 -0
- package/dist/src/storage/providers/postgres-kysely/migrations/0023_nonce_payload.sql +13 -0
- package/dist/src/storage/providers/sqlite/index.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/index.js +2 -1
- package/dist/src/storage/providers/sqlite/index.js.map +1 -1
- package/dist/src/storage/providers/sqlite/methods/community.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/methods/community.js +2 -1
- package/dist/src/storage/providers/sqlite/methods/community.js.map +1 -1
- package/dist/src/storage/providers/sqlite/methods/connections.d.ts +60 -0
- package/dist/src/storage/providers/sqlite/methods/connections.d.ts.map +1 -0
- package/dist/src/storage/providers/sqlite/methods/connections.js +287 -0
- package/dist/src/storage/providers/sqlite/methods/connections.js.map +1 -0
- package/dist/src/storage/providers/sqlite/schema-tables-1.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/schema-tables-1.js +4 -0
- package/dist/src/storage/providers/sqlite/schema-tables-1.js.map +1 -1
- package/dist/src/storage/providers/sqlite/schema-tables-3.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/schema-tables-3.js +106 -0
- package/dist/src/storage/providers/sqlite/schema-tables-3.js.map +1 -1
- package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/schema.js +2 -0
- package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
- package/dist/src/storage/repositories/connection.repository.d.ts +124 -0
- package/dist/src/storage/repositories/connection.repository.d.ts.map +1 -0
- package/dist/src/storage/repositories/connection.repository.js +2 -0
- package/dist/src/storage/repositories/connection.repository.js.map +1 -0
- package/dist/src/storage/types/agents-messaging.d.ts +19 -0
- package/dist/src/storage/types/agents-messaging.d.ts.map +1 -1
- package/dist/src/storage/types/agents-messaging.js.map +1 -1
- package/dist/src/storage/types/auth.d.ts +8 -1
- package/dist/src/storage/types/auth.d.ts.map +1 -1
- package/dist/src/utils/build-stamp.d.ts +56 -0
- package/dist/src/utils/build-stamp.d.ts.map +1 -0
- package/dist/src/utils/build-stamp.js +142 -0
- package/dist/src/utils/build-stamp.js.map +1 -0
- package/dist/src/utils/messaging.d.ts +14 -0
- package/dist/src/utils/messaging.d.ts.map +1 -1
- package/dist/src/utils/messaging.js +0 -0
- package/dist/src/utils/messaging.js.map +1 -1
- package/dist/static/sdk-libs/connect/index.js +199 -0
- package/dist/static/sdk-libs/connect/notes.js +47 -0
- package/dist/static/sdk-libs/connect/panel.js +238 -0
- package/dist/static/sdk-libs/data/index.js +63 -11
- package/dist/static/sdk-libs/dist/aimeat-connect.js +357 -0
- package/dist/static/sdk-libs/dist/aimeat-data.js +40 -9
- package/package.json +2 -2
|
@@ -787,6 +787,10 @@ export default function Landing({ navigate }) {
|
|
|
787
787
|
<a href="/v1/pricing#own-node" onClick=${(e) => { e.preventDefault(); navigate('/v1/pricing'); }}>${tr('landing.footOwnNode', 'Run your own node')}</a>
|
|
788
788
|
<a class="ld-gh" href="https://github.com/miikkij/aimeat-protocol" target="_blank" rel="noopener">${GhMark}GitHub</a>
|
|
789
789
|
<a href="/v1/portal?view=dev">${tr('landing.footDev', 'For developers')}</a>
|
|
790
|
+
${/* The transparency page in the footer as well as the one line above it. A page a
|
|
791
|
+
visitor cannot find is close to not having one, and the footer is where someone
|
|
792
|
+
looks for this kind of statement. Header nav stays out of it. */''}
|
|
793
|
+
<a href="/v1/transparency" onClick=${(e) => { e.preventDefault(); navigate('/v1/transparency'); }}>${tr('landing.footTransparency', 'AI transparency')}</a>
|
|
790
794
|
</footer>
|
|
791
795
|
</div>
|
|
792
796
|
`;
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file views/profile/access-tab/connections.js
|
|
3
|
+
* @description Connected Accounts section (TARGET-057) โ the accounts the owner holds at external
|
|
4
|
+
* services, and the one place they can all be seen and taken away.
|
|
5
|
+
*
|
|
6
|
+
* IT LIVES HERE AND NOT IN AN APP because a person must be able to find everything they have
|
|
7
|
+
* given a third party in ONE place. An app-by-app panel means that revoking an account requires
|
|
8
|
+
* remembering which app created it, which is the same as not being able to revoke it.
|
|
9
|
+
*
|
|
10
|
+
* `needs_reauth` is rendered as a BUTTON THAT FIXES IT, not as an error. It is the expected end of
|
|
11
|
+
* a token's life, it happens to every long-lived connection eventually, and showing it in red
|
|
12
|
+
* turns a two-click repair into a support question.
|
|
13
|
+
* @structure ConnectionsSection โ GET /v1/connections + /providers, connect via a pop-up, revoke.
|
|
14
|
+
* @version-history
|
|
15
|
+
* v1.0.0 โ 2026-08-02 โ Initial (TARGET-057 phase 3).
|
|
16
|
+
*/
|
|
17
|
+
import { h } from 'preact';
|
|
18
|
+
import { useState, useEffect, useCallback } from 'preact/hooks';
|
|
19
|
+
import htm from 'htm';
|
|
20
|
+
const html = htm.bind(h);
|
|
21
|
+
import { t } from '/js/i18n.js';
|
|
22
|
+
import { escHtml } from '/js/utils.js';
|
|
23
|
+
import { useConfirm } from '/components/Modal.js';
|
|
24
|
+
import { apiGet, apiPost, apiDelete } from '/js/api.js';
|
|
25
|
+
import { swallowed } from '/js/swallowed.js';
|
|
26
|
+
|
|
27
|
+
/** Things a user must be told BEFORE they try, because the failure they prevent is unreadable. */
|
|
28
|
+
const NOTES = {
|
|
29
|
+
mastodon: ['cxNoteMastodonNeeds', 'cxNoteMastodonBefore'],
|
|
30
|
+
youtube: ['cxNoteYoutubeBefore'],
|
|
31
|
+
bluesky: ['cxNoteBlueskyNeeds', 'cxNoteBlueskyWhere', 'cxNoteBlueskyBefore'],
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export function ConnectionsSection({ showToast }) {
|
|
35
|
+
const [connections, setConnections] = useState([]);
|
|
36
|
+
const [providers, setProviders] = useState([]);
|
|
37
|
+
const [instances, setInstances] = useState({});
|
|
38
|
+
// Supplied credentials, per provider per field. Held only until the POST; nothing here is
|
|
39
|
+
// persisted client-side, and the server never sends any of it back.
|
|
40
|
+
const [fields, setFields] = useState({});
|
|
41
|
+
const [busy, setBusy] = useState('');
|
|
42
|
+
const [unavailable, setUnavailable] = useState(false);
|
|
43
|
+
const { confirm, ConfirmUI } = useConfirm();
|
|
44
|
+
|
|
45
|
+
const load = useCallback(async () => {
|
|
46
|
+
try {
|
|
47
|
+
const [list, provs] = await Promise.all([
|
|
48
|
+
apiGet('/v1/connections'),
|
|
49
|
+
apiGet('/v1/connections/providers'),
|
|
50
|
+
]);
|
|
51
|
+
// apiGet returns the WHOLE envelope, so the payload is under .data. Reading it a level
|
|
52
|
+
// too high yields undefined -> [], which renders as a believable empty state rather than an
|
|
53
|
+
// error, and that is exactly how this stayed wrong through a typecheck and a lint.
|
|
54
|
+
setConnections(list?.data?.connections || []);
|
|
55
|
+
setProviders(provs?.data?.providers || []);
|
|
56
|
+
setUnavailable(false);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
// A node with the capability switched off answers 503. Saying so beats an empty list that
|
|
59
|
+
// reads as "you have no accounts".
|
|
60
|
+
swallowed('connections-load', err);
|
|
61
|
+
setUnavailable(true);
|
|
62
|
+
}
|
|
63
|
+
}, []);
|
|
64
|
+
|
|
65
|
+
useEffect(() => { void load(); }, [load]);
|
|
66
|
+
// Every tab showing server data re-reads on a live update.
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
const handler = () => void load();
|
|
69
|
+
window.addEventListener('aimeat-live-update', handler);
|
|
70
|
+
return () => window.removeEventListener('aimeat-live-update', handler);
|
|
71
|
+
}, [load]);
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Wait for the round to finish, WITHOUT touching the pop-up window.
|
|
75
|
+
*
|
|
76
|
+
* This node sets a Cross-Origin-Opener-Policy, which severs the opener relationship as soon as the
|
|
77
|
+
* pop-up navigates to the provider. `win.closed` is then unreadable โ the first version of this
|
|
78
|
+
* polled it and logged ten "COOP would block the window.closed call" errors per connection while
|
|
79
|
+
* appearing to work, because the flow completed for other reasons.
|
|
80
|
+
*
|
|
81
|
+
* Two signals instead, and neither goes near the window:
|
|
82
|
+
* - the done page announces on a same-origin BroadcastChannel, which COOP does not affect
|
|
83
|
+
* - the connection list is polled, which is what ACTUALLY decides success and also covers the
|
|
84
|
+
* user who closes the pop-up before it ever reaches the done page
|
|
85
|
+
*
|
|
86
|
+
* Resolves either way; gives up after the timeout rather than waiting forever on a cancelled round.
|
|
87
|
+
*/
|
|
88
|
+
const waitForConnection = useCallback(async (countBefore) => {
|
|
89
|
+
const DEADLINE = Date.now() + 180_000;
|
|
90
|
+
let channel = null;
|
|
91
|
+
let announced = false;
|
|
92
|
+
try {
|
|
93
|
+
channel = new BroadcastChannel('aimeat-connect');
|
|
94
|
+
channel.onmessage = () => { announced = true; };
|
|
95
|
+
} catch (err) {
|
|
96
|
+
// Not fatal: the poll below is the one that decides. Logged so the two paths stay
|
|
97
|
+
// distinguishable when someone is looking at why a connection took four seconds.
|
|
98
|
+
swallowed('connections-broadcast', err);
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
while (Date.now() < DEADLINE) {
|
|
102
|
+
await new Promise(r => setTimeout(r, 1200));
|
|
103
|
+
if (announced) return true;
|
|
104
|
+
const now = (await apiGet('/v1/connections'))?.data?.connections?.length ?? 0;
|
|
105
|
+
// A re-authorisation repairs a row rather than adding one, so a count that did not grow is
|
|
106
|
+
// not proof of failure -- but it is the cheap signal, and the announce covers the rest.
|
|
107
|
+
if (now > countBefore) return true;
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
} finally {
|
|
111
|
+
if (channel) channel.close();
|
|
112
|
+
}
|
|
113
|
+
}, []);
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Consent happens at the provider, in a pop-up. Completion is detected by that window closing and
|
|
117
|
+
* a re-read of the list, because a cross-origin child window cannot be asked what happened to it
|
|
118
|
+
* โ and treating "the window opened" as success would report a cancelled connection as connected.
|
|
119
|
+
*/
|
|
120
|
+
const connect = useCallback(async (provider) => {
|
|
121
|
+
setBusy(provider.id);
|
|
122
|
+
try {
|
|
123
|
+
const res = await apiPost('/v1/connections/start', {
|
|
124
|
+
provider: provider.id,
|
|
125
|
+
instance: instances[provider.id] || undefined,
|
|
126
|
+
mode: 'personal',
|
|
127
|
+
return_url: '/connection-done.html',
|
|
128
|
+
});
|
|
129
|
+
const url = res?.data?.authorize_url;
|
|
130
|
+
if (!url) throw new Error('no authorize url');
|
|
131
|
+
const before = (await apiGet('/v1/connections'))?.data?.connections?.length ?? 0;
|
|
132
|
+
const win = window.open(url, 'aimeat-connect', 'width=620,height=760');
|
|
133
|
+
if (!win) throw new Error('popup blocked');
|
|
134
|
+
await waitForConnection(before);
|
|
135
|
+
await load();
|
|
136
|
+
} catch (err) {
|
|
137
|
+
swallowed('connections-start', err);
|
|
138
|
+
showToast(t('profile.access.cxConnectFailed') || 'Could not start connecting that account');
|
|
139
|
+
}
|
|
140
|
+
setBusy('');
|
|
141
|
+
}, [instances, load, showToast, waitForConnection]);
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The other way to connect: the user supplies a credential and there is no round trip to the
|
|
145
|
+
* provider's consent screen. Kept separate from connect() because the two must never overlap โ
|
|
146
|
+
* attaching an OAuth provider would be a way to skip its consent screen, and the server refuses
|
|
147
|
+
* that in both directions.
|
|
148
|
+
*/
|
|
149
|
+
const attach = useCallback(async (provider) => {
|
|
150
|
+
setBusy(provider.id);
|
|
151
|
+
try {
|
|
152
|
+
await apiPost('/v1/connections/attach', {
|
|
153
|
+
provider: provider.id,
|
|
154
|
+
mode: 'personal',
|
|
155
|
+
fields: fields[provider.id] || {},
|
|
156
|
+
});
|
|
157
|
+
// Cleared on success so a secret does not sit in a form field after it has been stored.
|
|
158
|
+
setFields(f => ({ ...f, [provider.id]: {} }));
|
|
159
|
+
await load();
|
|
160
|
+
} catch (err) {
|
|
161
|
+
swallowed('connections-attach', err);
|
|
162
|
+
// The server's reason is the useful half here: "that is your account password, not an app
|
|
163
|
+
// password" is actionable in a way that "could not connect" is not.
|
|
164
|
+
showToast(err?.message || t('profile.access.cxConnectFailed') || 'Could not connect that account');
|
|
165
|
+
}
|
|
166
|
+
setBusy('');
|
|
167
|
+
}, [fields, load, showToast]);
|
|
168
|
+
|
|
169
|
+
// useConfirm is (message, onConfirm, opts) โ positional, with a callback. Passing it an options
|
|
170
|
+
// object and awaiting the result renders an EMPTY dialog and never runs the action: the owner is
|
|
171
|
+
// asked to confirm losing an account without being told which one, and pressing Confirm does
|
|
172
|
+
// nothing. Both halves of that were live until the browser pass caught them.
|
|
173
|
+
const revoke = useCallback((conn) => {
|
|
174
|
+
const message = (t('profile.access.cxConfirm')
|
|
175
|
+
|| 'Disconnect {account}? Apps that publish to it will stop being able to.')
|
|
176
|
+
.replace('{account}', conn.accountLabel);
|
|
177
|
+
confirm(message, async () => {
|
|
178
|
+
try {
|
|
179
|
+
const res = await apiDelete('/v1/connections/' + encodeURIComponent(conn.id));
|
|
180
|
+
// Honest about which half happened: a token still live at the provider is something the
|
|
181
|
+
// owner may want to go and remove there themselves.
|
|
182
|
+
showToast(res?.data?.told_provider
|
|
183
|
+
? (t('profile.access.cxRevokedTold') || 'Disconnected, and the service was told.')
|
|
184
|
+
: (t('profile.access.cxRevokedLocal') || 'Disconnected here. The service could not be reached, so check it there too.'));
|
|
185
|
+
await load();
|
|
186
|
+
} catch (err) {
|
|
187
|
+
swallowed('connections-revoke', err);
|
|
188
|
+
showToast(t('profile.access.cxRevokeFailed') || 'Could not disconnect that account');
|
|
189
|
+
}
|
|
190
|
+
}, {
|
|
191
|
+
title: t('profile.access.cxDisconnect') || 'Disconnect',
|
|
192
|
+
confirmLabel: t('profile.access.cxDisconnect') || 'Disconnect',
|
|
193
|
+
danger: true,
|
|
194
|
+
});
|
|
195
|
+
}, [confirm, load, showToast]);
|
|
196
|
+
|
|
197
|
+
if (unavailable) return null;
|
|
198
|
+
|
|
199
|
+
return html`
|
|
200
|
+
<div class="access-section">
|
|
201
|
+
<${ConfirmUI} />
|
|
202
|
+
<h3 class="access-h3">${t('profile.access.cxTitle') || 'Connected accounts'}</h3>
|
|
203
|
+
<p class="text-meta-sm">${t('profile.access.cxIntro')
|
|
204
|
+
|| 'Accounts you have connected at other services. The credential stays on this node โ an app is only ever told which account it may use, never the account itself.'}</p>
|
|
205
|
+
|
|
206
|
+
${connections.length === 0 && html`
|
|
207
|
+
<div class="mem-item"><span class="adm-text-dim">${t('profile.access.cxEmpty') || 'No connected accounts yet.'}</span></div>
|
|
208
|
+
`}
|
|
209
|
+
|
|
210
|
+
${connections.map(c => html`
|
|
211
|
+
<div class="mem-item" key=${c.id}>
|
|
212
|
+
<span class="mem-key">${escHtml(c.accountLabel)}</span>
|
|
213
|
+
<span class="text-meta-sm">
|
|
214
|
+
${escHtml(c.provider)}${c.status === 'needs_reauth'
|
|
215
|
+
? ' ยท ' + (t('profile.access.cxNeedsReauth') || 'needs reconnecting')
|
|
216
|
+
: ''}
|
|
217
|
+
</span>
|
|
218
|
+
${c.status === 'needs_reauth' && html`
|
|
219
|
+
<button class="btn-outline" onClick=${() => connect({ id: c.provider })}>
|
|
220
|
+
${t('profile.access.cxReconnect') || 'Reconnect'}
|
|
221
|
+
</button>
|
|
222
|
+
`}
|
|
223
|
+
<button class="btn-ghost btn-danger" onClick=${() => revoke(c)}>
|
|
224
|
+
${t('profile.access.cxDisconnect') || 'Disconnect'}
|
|
225
|
+
</button>
|
|
226
|
+
</div>
|
|
227
|
+
`)}
|
|
228
|
+
|
|
229
|
+
${providers.map(p => html`
|
|
230
|
+
<div class="access-cx-add" key=${p.id}>
|
|
231
|
+
<div class="mem-item access-fed-add">
|
|
232
|
+
${p.instanceScoped && html`
|
|
233
|
+
<input type="text" class="input-field input-sm"
|
|
234
|
+
placeholder=${t('profile.access.cxInstance') || 'instance address, e.g. mastodon.social'}
|
|
235
|
+
value=${instances[p.id] || ''}
|
|
236
|
+
onInput=${e => setInstances({ ...instances, [p.id]: e.target.value })} />
|
|
237
|
+
`}
|
|
238
|
+
${(p.attachFields || []).map(f => html`
|
|
239
|
+
<input key=${f.name}
|
|
240
|
+
type=${f.secret ? 'password' : 'text'}
|
|
241
|
+
class="input-field input-sm"
|
|
242
|
+
autocomplete=${f.secret ? 'new-password' : 'off'}
|
|
243
|
+
placeholder=${f.placeholder || f.label}
|
|
244
|
+
aria-label=${f.label}
|
|
245
|
+
value=${(fields[p.id] || {})[f.name] || ''}
|
|
246
|
+
onInput=${e => setFields(prev => ({
|
|
247
|
+
...prev, [p.id]: { ...(prev[p.id] || {}), [f.name]: e.target.value },
|
|
248
|
+
}))} />
|
|
249
|
+
`)}
|
|
250
|
+
<button class="btn-outline" disabled=${busy === p.id}
|
|
251
|
+
onClick=${() => (p.attachFields ? attach(p) : connect(p))}>
|
|
252
|
+
${busy === p.id
|
|
253
|
+
? (t('profile.access.cxConnecting') || 'Connectingโฆ')
|
|
254
|
+
: `${t('profile.access.cxConnect') || 'Connect'} ${escHtml(p.label)}`}
|
|
255
|
+
</button>
|
|
256
|
+
</div>
|
|
257
|
+
${(NOTES[p.id] || []).map(key => html`
|
|
258
|
+
<p class="text-meta-sm" key=${key}>${t('profile.access.' + key)}</p>
|
|
259
|
+
`)}
|
|
260
|
+
</div>
|
|
261
|
+
`)}
|
|
262
|
+
</div>
|
|
263
|
+
`;
|
|
264
|
+
}
|
|
@@ -40,6 +40,7 @@ import { SharingGroupsSection } from './access-tab/sharing-groups.js';
|
|
|
40
40
|
import { AgentDefaultsSection } from './access-tab/agent-defaults.js';
|
|
41
41
|
import { ConnectedAppsSection } from './access-tab/connected-apps.js';
|
|
42
42
|
import { AccessTokensSection } from './access-tab/access-tokens.js';
|
|
43
|
+
import { ConnectionsSection } from './access-tab/connections.js';
|
|
43
44
|
import { swallowed } from '/js/swallowed.js';
|
|
44
45
|
|
|
45
46
|
/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
@@ -252,6 +253,7 @@ export default function AccessTab({ session, showToast }) {
|
|
|
252
253
|
${overview !== undefined ? html`
|
|
253
254
|
<${ConnectedAppsSection} showToast=${showToast} initial=${overview?.appGrants} />
|
|
254
255
|
<${AccessTokensSection} session=${session} showToast=${showToast} initial=${overview?.accessTokens} />
|
|
256
|
+
<${ConnectionsSection} showToast=${showToast} />
|
|
255
257
|
<${SharingGroupsSection} showToast=${showToast} initial=${overview?.groups} />
|
|
256
258
|
<${AgentDefaultsSection} showToast=${showToast} initial=${overview?.agentDefaults} />
|
|
257
259
|
` : null}
|
|
@@ -21,6 +21,11 @@
|
|
|
21
21
|
* import { AiTransparencyCard } from './ai-transparency-card.js';
|
|
22
22
|
* html`<${AiTransparencyCard} />`
|
|
23
23
|
* @version-history
|
|
24
|
+
* v1.1.0 โ 2026-08-01 โ i18n namespace renamed `aiTransparency.*` โ `aiTransparencyMine.*`
|
|
25
|
+
* (TARGET-058 Phase 10b). It sat one character away from `transparency.*`, the PUBLIC page's
|
|
26
|
+
* namespace, with `title` and `loading` meaning different things in each โ a pair an
|
|
27
|
+
* autocomplete picks wrong and nothing catches, because a missing key renders as itself.
|
|
28
|
+
* Strings and behaviour unchanged.
|
|
24
29
|
* v1.0.0 โ 2026-08-01 โ TARGET-058 Phase 8.
|
|
25
30
|
*/
|
|
26
31
|
import { h } from 'preact';
|
|
@@ -59,7 +64,7 @@ export function AiTransparencyCard() {
|
|
|
59
64
|
setReport(prev => (JSON.stringify(prev) === JSON.stringify(mine?.data ?? null) ? prev : (mine?.data ?? null)));
|
|
60
65
|
setPolicy(prev => (JSON.stringify(prev) === JSON.stringify(pol?.data ?? null) ? prev : (pol?.data ?? null)));
|
|
61
66
|
} catch (err) {
|
|
62
|
-
setError(err?.message || t('
|
|
67
|
+
setError(err?.message || t('aiTransparencyMine.loadFailed'));
|
|
63
68
|
} finally {
|
|
64
69
|
if (!opts.quiet) setLoading(false);
|
|
65
70
|
}
|
|
@@ -85,55 +90,55 @@ export function AiTransparencyCard() {
|
|
|
85
90
|
<div class="pf-card pf-aitr">
|
|
86
91
|
<button type="button" class="pf-aitr-head" onClick=${() => setCollapsed(c => !c)}
|
|
87
92
|
aria-expanded=${!collapsed}>
|
|
88
|
-
<span class="section-title">${t('
|
|
93
|
+
<span class="section-title">${t('aiTransparencyMine.title')}</span>
|
|
89
94
|
<span class="pf-aitr-chevron">${collapsed ? '+' : 'โ'}</span>
|
|
90
95
|
</button>
|
|
91
|
-
<p class="section-desc">${t('
|
|
96
|
+
<p class="section-desc">${t('aiTransparencyMine.desc')}</p>
|
|
92
97
|
|
|
93
98
|
${!collapsed && html`
|
|
94
99
|
<div class="pf-aitr-body">
|
|
95
|
-
${loading && html`<p class="pf-aitr-muted">${t('
|
|
100
|
+
${loading && html`<p class="pf-aitr-muted">${t('aiTransparencyMine.loading')}</p>`}
|
|
96
101
|
${error && html`<p class="pf-aitr-error">${error}</p>`}
|
|
97
102
|
|
|
98
103
|
${report && html`
|
|
99
104
|
<div class="pf-aitr-stats">
|
|
100
105
|
<div class=${'pf-aitr-stat' + (unlabelled > 0 ? ' pf-aitr-stat-warn' : '')}>
|
|
101
106
|
<span class="pf-aitr-num">${unlabelled}</span>
|
|
102
|
-
<span class="pf-aitr-lbl">${t('
|
|
107
|
+
<span class="pf-aitr-lbl">${t('aiTransparencyMine.unlabelled')}</span>
|
|
103
108
|
</div>
|
|
104
109
|
<div class="pf-aitr-stat">
|
|
105
110
|
<span class="pf-aitr-num">${report.labelled ?? 0}</span>
|
|
106
|
-
<span class="pf-aitr-lbl">${t('
|
|
111
|
+
<span class="pf-aitr-lbl">${t('aiTransparencyMine.labelled')}</span>
|
|
107
112
|
</div>
|
|
108
113
|
<div class="pf-aitr-stat">
|
|
109
114
|
<span class="pf-aitr-num">${report.public_total ?? 0}</span>
|
|
110
|
-
<span class="pf-aitr-lbl">${t('
|
|
115
|
+
<span class="pf-aitr-lbl">${t('aiTransparencyMine.publicTotal')}</span>
|
|
111
116
|
</div>
|
|
112
117
|
<div class="pf-aitr-stat">
|
|
113
118
|
<span class="pf-aitr-num">${report.total ?? 0}</span>
|
|
114
|
-
<span class="pf-aitr-lbl">${t('
|
|
119
|
+
<span class="pf-aitr-lbl">${t('aiTransparencyMine.total')}</span>
|
|
115
120
|
</div>
|
|
116
121
|
</div>
|
|
117
122
|
|
|
118
|
-
<p class="pf-aitr-note">${t('
|
|
123
|
+
<p class="pf-aitr-note">${t('aiTransparencyMine.scopeNote')}</p>
|
|
119
124
|
|
|
120
125
|
${unlabelled > 0 && html`
|
|
121
|
-
<p class="pf-aitr-warn-line">${t('
|
|
126
|
+
<p class="pf-aitr-warn-line">${t('aiTransparencyMine.unlabelledHelp')}</p>
|
|
122
127
|
<ul class="pf-aitr-list">
|
|
123
128
|
${(report.unlabelled_detail?.items ?? []).map(item => html`
|
|
124
129
|
<li key=${item.id} class="pf-aitr-row">
|
|
125
|
-
<span class="pf-aitr-row-main">${item.pipeline || t('
|
|
130
|
+
<span class="pf-aitr-row-main">${item.pipeline || t('aiTransparencyMine.unknownSource')}</span>
|
|
126
131
|
<span class="pf-aitr-row-meta">${shortTime(item.generated_at)}</span>
|
|
127
132
|
${item.record_url && html`
|
|
128
133
|
<a class="pf-aitr-row-link" href=${item.record_url} target="_blank" rel="noopener noreferrer">
|
|
129
|
-
${t('
|
|
134
|
+
${t('aiTransparencyMine.openRecord')}
|
|
130
135
|
</a>`}
|
|
131
136
|
</li>`)}
|
|
132
137
|
</ul>
|
|
133
138
|
${report.unlabelled_detail
|
|
134
139
|
&& report.unlabelled_detail.shown < report.unlabelled_detail.total
|
|
135
140
|
&& html`<p class="pf-aitr-muted">
|
|
136
|
-
${t('
|
|
141
|
+
${t('aiTransparencyMine.showingOf', {
|
|
137
142
|
shown: String(report.unlabelled_detail.shown),
|
|
138
143
|
total: String(report.unlabelled_detail.total),
|
|
139
144
|
})}
|
|
@@ -141,7 +146,7 @@ export function AiTransparencyCard() {
|
|
|
141
146
|
`}
|
|
142
147
|
|
|
143
148
|
${(report.apps_declaring_generation_with_gap ?? []).length > 0 && html`
|
|
144
|
-
<h4 class="pf-aitr-sub">${t('
|
|
149
|
+
<h4 class="pf-aitr-sub">${t('aiTransparencyMine.appsWithGap')}</h4>
|
|
145
150
|
<ul class="pf-aitr-list">
|
|
146
151
|
${report.apps_declaring_generation_with_gap.map(a => html`
|
|
147
152
|
<li key=${a.owner + '/' + a.filename} class="pf-aitr-row">
|
|
@@ -152,14 +157,14 @@ export function AiTransparencyCard() {
|
|
|
152
157
|
`}
|
|
153
158
|
|
|
154
159
|
${policy && html`
|
|
155
|
-
<h4 class="pf-aitr-sub">${t('
|
|
160
|
+
<h4 class="pf-aitr-sub">${t('aiTransparencyMine.policyTitle')}</h4>
|
|
156
161
|
<p class="pf-aitr-note">${policy.why}</p>
|
|
157
162
|
<ul class="pf-aitr-list">
|
|
158
163
|
${(policy.records ?? []).map(r => html`
|
|
159
164
|
<li key=${r.what} class="pf-aitr-policy">
|
|
160
165
|
<span class="pf-aitr-row-main">${r.what}</span>
|
|
161
|
-
<span class="pf-aitr-row-meta">${t('
|
|
162
|
-
<span class="pf-aitr-row-meta">${t('
|
|
166
|
+
<span class="pf-aitr-row-meta">${t('aiTransparencyMine.retention')}: ${r.retention}</span>
|
|
167
|
+
<span class="pf-aitr-row-meta">${t('aiTransparencyMine.neverContains')}: ${r.never_contains}</span>
|
|
163
168
|
</li>`)}
|
|
164
169
|
</ul>
|
|
165
170
|
<p class="pf-aitr-note">${policy.note}</p>`}
|
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
* chat.commands), SchedulePanel (own-agent scheduler), and ReplyWithAiPopover (TARGET-031). Each is
|
|
7
7
|
* self-contained (owns its own hooks). Extracted from inbox-tab.js to satisfy max-file-lines.
|
|
8
8
|
* @version-history
|
|
9
|
+
* v1.11.0 โ 2026-08-01 โ Voice messages. AttachmentItem hands an audio attachment to AudioAttachment
|
|
10
|
+
* (./voice-parts.js) so it plays in the bubble instead of opening in a browser tab, which is a
|
|
11
|
+
* download and not a conversation. The Composer gains a ๐ค recorder feeding the SAME attachment
|
|
12
|
+
* queue as ๐ (one upload path), and a recording's chip carries a player + its length so a bad
|
|
13
|
+
* take is caught here rather than in the other person's mailbox.
|
|
9
14
|
* v1.10.0 โ 2026-07-31 โ MessageBubble gets a ๐ read-aloud action (BubbleSpeakButton from
|
|
10
15
|
* ./read-aloud.js): speaks that one message via the Web Speech API, like the Sanomat app's per-article
|
|
11
16
|
* "Puhu". Hidden when the browser can't speak or the message has no speakable body.
|
|
@@ -51,6 +56,8 @@ import * as schedules from '/js/services/schedules.js';
|
|
|
51
56
|
import { MODES } from '/js/services/messages-ai-prompts.js';
|
|
52
57
|
import { loadToastUI, prepareBody, quoteSnippet, statusTick, timeShort, trackStateLabel, ATTACH_ICO, attachKind, IFORM_OTHER } from './helpers.js';
|
|
53
58
|
import { BubbleSpeakButton } from './read-aloud.js';
|
|
59
|
+
import { AudioAttachment, fmtClock, stopOtherAudio } from './voice-parts.js';
|
|
60
|
+
import { VoiceRecorder } from '/components/VoiceRecorder.js';
|
|
54
61
|
import { swallowed } from '/js/swallowed.js';
|
|
55
62
|
|
|
56
63
|
export function Avatar({ seed, size = 36 }) {
|
|
@@ -59,10 +66,12 @@ export function Avatar({ seed, size = 36 }) {
|
|
|
59
66
|
dangerouslySetInnerHTML=${{ __html: svg }}></span>`;
|
|
60
67
|
}
|
|
61
68
|
|
|
69
|
+
|
|
62
70
|
/** One received/sent attachment. Images render as a thumbnail (click โ full-size in a new tab);
|
|
63
|
-
* PDF/
|
|
64
|
-
* ready attachment gets a download button. Not-yet-duplicated / expired
|
|
65
|
-
|
|
71
|
+
* audio plays in place; PDF/video/file open natively in a new tab; markdown opens the in-app
|
|
72
|
+
* rendered viewer. Every ready attachment gets a download button. Not-yet-duplicated / expired
|
|
73
|
+
* attachments show their state. */
|
|
74
|
+
export function AttachmentItem({ a, url, onOpenMarkdown, msgId, onTranscribe, canTranscribe }) {
|
|
66
75
|
const kind = attachKind(a);
|
|
67
76
|
const name = a.name || a.storageKey;
|
|
68
77
|
const ready = !!url && !a.expired;
|
|
@@ -93,7 +102,11 @@ export function AttachmentItem({ a, url, onOpenMarkdown }) {
|
|
|
93
102
|
</button>${download}
|
|
94
103
|
</div>`;
|
|
95
104
|
}
|
|
96
|
-
|
|
105
|
+
if (kind === 'audio') {
|
|
106
|
+
return html`<${AudioAttachment} a=${a} url=${url} name=${name} download=${download}
|
|
107
|
+
msgId=${msgId} onTranscribe=${onTranscribe} canTranscribe=${canTranscribe} />`;
|
|
108
|
+
}
|
|
109
|
+
// pdf / video / file โ let the browser open it in a new tab.
|
|
97
110
|
return html`<div class="inbox-attach-chip">
|
|
98
111
|
<a class="inbox-attach-open" href=${url} target="_blank" rel="noopener" title=${t('inbox.attachmentOpen')}>
|
|
99
112
|
<span class="inbox-attach-ico">${ATTACH_ICO[kind]}</span><span class="inbox-attach-name">${escHtml(name)}</span>
|
|
@@ -261,7 +274,7 @@ export function PollBuilder({ questions, setQuestions }) {
|
|
|
261
274
|
</div>`;
|
|
262
275
|
}
|
|
263
276
|
|
|
264
|
-
export function MessageBubble({ msg, mine, urlMap, starred, onStar, onTrack, onPark, onReplyAi, onQuote, quoted, quotedName, onJumpTo, domId, tracked, onOpenMarkdown, answeredWith, onAnswer, submitting, showLinkPreviews }) {
|
|
277
|
+
export function MessageBubble({ msg, mine, urlMap, starred, onStar, onTrack, onPark, onReplyAi, onQuote, quoted, quotedName, onJumpTo, domId, tracked, onOpenMarkdown, answeredWith, onAnswer, submitting, showLinkPreviews, onTranscribe, canTranscribe }) {
|
|
265
278
|
// Copy the message text to the clipboard (the raw markdown the sender wrote โ that's what pastes
|
|
266
279
|
// usefully into an AI chat or a document; the rendered body's presigned image URLs are transient).
|
|
267
280
|
const [copied, setCopied] = useState(false);
|
|
@@ -319,7 +332,9 @@ export function MessageBubble({ msg, mine, urlMap, starred, onStar, onTrack, onP
|
|
|
319
332
|
) : null}
|
|
320
333
|
${nonInline.length > 0 && html`
|
|
321
334
|
<div class="inbox-attach-row">
|
|
322
|
-
${nonInline.map(a => html`<${AttachmentItem} key=${a.id} a=${a} url=${urls[a.id]}
|
|
335
|
+
${nonInline.map(a => html`<${AttachmentItem} key=${a.id} a=${a} url=${urls[a.id]}
|
|
336
|
+
onOpenMarkdown=${onOpenMarkdown} msgId=${msg.id}
|
|
337
|
+
onTranscribe=${onTranscribe} canTranscribe=${canTranscribe} />`)}
|
|
323
338
|
</div>`}
|
|
324
339
|
<div class="inbox-bubble-meta">
|
|
325
340
|
${trk ? html`<span class=${`inbox-track-badge inbox-track-badge--${trk.tone}`} title=${t('inbox.trackResponse')}>๐ ${trk.text}</span>` : null}
|
|
@@ -334,7 +349,10 @@ export function MessageBubble({ msg, mine, urlMap, starred, onStar, onTrack, onP
|
|
|
334
349
|
* markdown-textarea + live-preview fallback if the editor can't load. Owns its own draft + file
|
|
335
350
|
* state; calls onSend(recipient, markdown, files, reset). Remount it (via key) per conversation so
|
|
336
351
|
* the draft doesn't leak between threads. */
|
|
337
|
-
export function Composer({
|
|
352
|
+
export function Composer({
|
|
353
|
+
recipient, sendLabel, sending, onSend, initialText = '', draftKey = '', focusNonce = 0,
|
|
354
|
+
voiceMaxSeconds = 300,
|
|
355
|
+
}) {
|
|
338
356
|
// Restore an in-progress draft for this conversation/compose (localStorage), or the passed initialText.
|
|
339
357
|
const readDraft = () => { try { return draftKey ? (localStorage.getItem(draftKey) || '') : ''; } catch { return ''; } }; // eslint-disable-line aimeat/no-silent-catch -- a browser API refusing here IS the answer
|
|
340
358
|
const seeded = initialText || readDraft(); // an explicit suggested reply wins; else restore a draft
|
|
@@ -379,6 +397,21 @@ export function Composer({ recipient, sendLabel, sending, onSend, initialText =
|
|
|
379
397
|
const file = (blob instanceof File && !generic) ? blob : new File([blob], name, { type: blob.type || 'image/png' });
|
|
380
398
|
setFiles((prev) => [...prev, file]);
|
|
381
399
|
};
|
|
400
|
+
// A finished recording joins the same attachment queue as a picked file, so it travels the one
|
|
401
|
+
// upload path (presigned PUT) everything else uses. The measured length rides ON the File: the
|
|
402
|
+
// send path reads it back as `duration_seconds`, which lets the recipient's thread show "0:14"
|
|
403
|
+
// before a single byte of audio has been fetched.
|
|
404
|
+
const addRecording = (file, durationSeconds) => {
|
|
405
|
+
file.durationSeconds = durationSeconds;
|
|
406
|
+
// Minted ONCE here, not in render: createObjectURL in a render body allocates a new blob URL on
|
|
407
|
+
// every re-render and never releases the old ones.
|
|
408
|
+
file.previewUrl = URL.createObjectURL(file);
|
|
409
|
+
setFiles((prev) => [...prev, file]);
|
|
410
|
+
};
|
|
411
|
+
const releasePreview = (file) => {
|
|
412
|
+
if (file?.previewUrl) { URL.revokeObjectURL(file.previewUrl); file.previewUrl = null; }
|
|
413
|
+
};
|
|
414
|
+
|
|
382
415
|
// Pull image files out of a clipboard/drop event; returns true if any were handled (caller preventDefaults).
|
|
383
416
|
const handleImagePaste = (e) => {
|
|
384
417
|
const items = Array.from(e.clipboardData?.items || e.dataTransfer?.items || []);
|
|
@@ -479,7 +512,9 @@ export function Composer({ recipient, sendLabel, sending, onSend, initialText =
|
|
|
479
512
|
const getText = () => (mode === 'rich' && editorRef.current) ? editorRef.current.getMarkdown() : md;
|
|
480
513
|
const reset = () => {
|
|
481
514
|
try { editorRef.current?.setMarkdown(''); } catch (err) { swallowed('components: reset', err); }
|
|
482
|
-
setMd('');
|
|
515
|
+
setMd('');
|
|
516
|
+
setFiles((prev) => { prev.forEach(releasePreview); return []; });
|
|
517
|
+
if (fileRef.current) fileRef.current.value = '';
|
|
483
518
|
if (mode === 'simple' && taRef.current) taRef.current.style.height = 'auto';
|
|
484
519
|
clearDraft(); // a sent message is no longer a draft
|
|
485
520
|
};
|
|
@@ -487,6 +522,7 @@ export function Composer({ recipient, sendLabel, sending, onSend, initialText =
|
|
|
487
522
|
// Remove one queued attachment before sending (a mis-paste shouldn't force starting the message over).
|
|
488
523
|
// Also clear the hidden file input when the last chip goes, so re-picking the same file fires onChange.
|
|
489
524
|
const removeFile = (idx) => setFiles((prev) => {
|
|
525
|
+
releasePreview(prev[idx]);
|
|
490
526
|
const next = prev.filter((_, j) => j !== idx);
|
|
491
527
|
if (next.length === 0 && fileRef.current) fileRef.current.value = '';
|
|
492
528
|
return next;
|
|
@@ -495,8 +531,20 @@ export function Composer({ recipient, sendLabel, sending, onSend, initialText =
|
|
|
495
531
|
return html`
|
|
496
532
|
<div class="inbox-composer ${expanded ? 'inbox-composer--tall' : ''}">
|
|
497
533
|
${files.length > 0 ? html`<div class="inbox-file-chips">
|
|
498
|
-
${files.map((f, i) =>
|
|
499
|
-
|
|
534
|
+
${files.map((f, i) => {
|
|
535
|
+
// A recording gets its own chip with a player: a bad take should be caught here, not in
|
|
536
|
+
// the other person's mailbox.
|
|
537
|
+
const isVoice = (f.type || '').startsWith('audio/') && f.durationSeconds;
|
|
538
|
+
return html`<span class=${`inbox-file-chip${isVoice ? ' inbox-file-chip--voice' : ''}`} key=${f.name + i}>
|
|
539
|
+
${isVoice
|
|
540
|
+
? html`<span class="inbox-chip-voice">๐ค ${fmtClock(f.durationSeconds)}
|
|
541
|
+
<audio class="inbox-audio inbox-audio--chip" controls preload="metadata"
|
|
542
|
+
src=${f.previewUrl}
|
|
543
|
+
onPlay=${(e) => stopOtherAudio(e.currentTarget)}></audio></span>`
|
|
544
|
+
: html`<span>๐ ${escHtml(f.name)}</span>`}
|
|
545
|
+
<button class="inbox-bc-chip-x" title=${t('inbox.attachmentRemove')} onClick=${() => removeFile(i)}>โ</button>
|
|
546
|
+
</span>`;
|
|
547
|
+
})}
|
|
500
548
|
</div>` : null}
|
|
501
549
|
${mode === 'rich'
|
|
502
550
|
? html`<div class="inbox-editor" ref=${containerRef}></div>`
|
|
@@ -515,6 +563,8 @@ export function Composer({ recipient, sendLabel, sending, onSend, initialText =
|
|
|
515
563
|
<label class="inbox-attach-btn" title=${t('inbox.attach')}>
|
|
516
564
|
๐<input ref=${fileRef} type="file" multiple hidden onChange=${(e) => setFiles(Array.from(e.target.files || []))} />
|
|
517
565
|
</label>
|
|
566
|
+
<${VoiceRecorder} maxSeconds=${voiceMaxSeconds} className="inbox-attach-btn"
|
|
567
|
+
onRecorded=${addRecording} />
|
|
518
568
|
<button type="button" class="inbox-attach-btn" title=${expanded ? t('inbox.collapse') : t('inbox.expand')}
|
|
519
569
|
aria-pressed=${expanded} onClick=${() => setExpanded((v) => !v)}>${expanded ? 'โคก' : 'โคข'}</button>
|
|
520
570
|
</div>
|
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
* pixel defense), tracked-state labels, attachment classification, the interactive-answer summary +
|
|
6
6
|
* poll tally, and the lazy Toast UI editor loader. Extracted from inbox-tab.js to satisfy max-file-lines.
|
|
7
7
|
* @version-history
|
|
8
|
+
* v1.4.0 โ 2026-08-01 โ parkMessage() + openTrackedRecord() moved here from inbox-tab.js: two
|
|
9
|
+
* self-contained actions that needed nothing from the tab's state beyond a toast, pulled out to
|
|
10
|
+
* keep the tab under max-file-lines while voice messages were added to it.
|
|
8
11
|
* v1.3.0 โ 2026-07-27 โ sendFailure(): pick the toast text for a failed send, preferring the thrown
|
|
9
12
|
* reason (attachment too large, out of quota) over the generic "could not send".
|
|
10
13
|
* v1.2.0 โ 2026-07-21 โ resolveThreadAttachmentUrls(): resolve non-inline attachment URLs for a loaded
|
|
@@ -14,6 +17,36 @@
|
|
|
14
17
|
*/
|
|
15
18
|
import { t, getLocale } from '/js/i18n.js';
|
|
16
19
|
import { swallowed } from '/js/swallowed.js';
|
|
20
|
+
import { parkMessageToNotebook } from '/js/services/notebook.js';
|
|
21
|
+
import { firstLine } from '../notebook-helpers.js';
|
|
22
|
+
|
|
23
|
+
/** ๐ on a message: copy it straight into the notebook for later processing (no AI step), keeping the
|
|
24
|
+
* source link + reply intent so it can be replied to or filed from there. */
|
|
25
|
+
export async function parkMessage(msg, showToast) {
|
|
26
|
+
try {
|
|
27
|
+
await parkMessageToNotebook(msg, { title: firstLine(msg.body) });
|
|
28
|
+
showToast?.(t('inbox.parkedToNotebook'));
|
|
29
|
+
} catch (e) {
|
|
30
|
+
showToast?.(e?.message || t('inbox.trackFailed'), true);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Open the workspace record a tracked response watches. Sets BOTH the saved tab (so the profile
|
|
35
|
+
* loads onto Organisms) and the workspace deep-link (so that exact workspace opens), then navigates. */
|
|
36
|
+
export function openTrackedRecord(tr, showToast) {
|
|
37
|
+
const r = tr.references || {};
|
|
38
|
+
if (!r.organismId || !r.workspaceId) { showToast?.(t('inbox.trackNoRecord'), true); return; }
|
|
39
|
+
try {
|
|
40
|
+
sessionStorage.setItem('aimeat-profile-tab', 'organisms');
|
|
41
|
+
sessionStorage.setItem('aimeat.ws.openId', r.organismId);
|
|
42
|
+
sessionStorage.setItem('aimeat.ws.openWs', r.workspaceId);
|
|
43
|
+
} catch (err) {
|
|
44
|
+
// Storage refused (private mode, quota): the navigation below still happens, it just lands on
|
|
45
|
+
// the Organisms tab without pre-opening the workspace.
|
|
46
|
+
swallowed('inbox helpers: openTrackedRecord deep-link', err);
|
|
47
|
+
}
|
|
48
|
+
window.location.assign('/v1/profile?tab=organisms');
|
|
49
|
+
}
|
|
17
50
|
|
|
18
51
|
/**
|
|
19
52
|
* Toast text for a failed send. An attachment upload throws for reasons the sender can act on: the file
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
* (broadcast/poll results). Each is a presentational component driven entirely by props from InboxTab;
|
|
7
7
|
* the stateful container keeps all hooks. Extracted from inbox-tab.js to satisfy max-file-lines.
|
|
8
8
|
* @version-history
|
|
9
|
+
* v1.6.0 โ 2026-08-01 โ Voice messages threaded through: ThreadPanel passes onTranscribe /
|
|
10
|
+
* canTranscribe to each bubble and voiceMaxSeconds to the Composer. An agent-owned ("via
|
|
11
|
+
* <agent>") thread is read-only for the owner, so it gets no transcribe action.
|
|
9
12
|
* v1.5.0 โ 2026-07-31 โ ThreadPanel head hosts ThreadReadAloud (./read-aloud.js): reads the whole open
|
|
10
13
|
* conversation aloud (Listen / Pause / Continue + โ), the thread-level twin of the per-bubble ๐.
|
|
11
14
|
* v1.4.0 โ 2026-07-21 โ ThreadPanel head: "Show all messages / Last 50" toggle (threadAll/
|
|
@@ -109,7 +112,7 @@ export function ThreadPanel({
|
|
|
109
112
|
peerDisplay, showToast, toggleImportant, onTrackMsg, onParkMsg, openMessageAi, submitInteractiveAnswers,
|
|
110
113
|
setMdViewer, openConversationAi, openConversationNotebook, insertCommand, setCmdFill, cancelTracked, openRecord, startSuggestedReply, doSend,
|
|
111
114
|
replyQuote, setReplyQuote, onQuoteReply, composerFocus, showLinkPreviews, toggleLinkPreviews,
|
|
112
|
-
threadAll, toggleThreadAll,
|
|
115
|
+
threadAll, toggleThreadAll, onTranscribe, canTranscribe, voiceMaxSeconds,
|
|
113
116
|
}) {
|
|
114
117
|
let lastDay = '';
|
|
115
118
|
// Reply-to quotes: resolve a message's `replyToId` to the original within the loaded page (a parent
|
|
@@ -179,6 +182,7 @@ export function ThreadPanel({
|
|
|
179
182
|
starred=${important.has(m.id)} onStar=${toggleImportant} onTrack=${onTrackMsg} onPark=${onParkMsg} onReplyAi=${openMessageAi} tracked=${trackedByMsg[m.id]}
|
|
180
183
|
answeredWith=${m.interactive?.role === 'questions' ? answersByQ[m.id] : null}
|
|
181
184
|
onAnswer=${submitInteractiveAnswers} submitting=${sending} showLinkPreviews=${showLinkPreviews}
|
|
185
|
+
onTranscribe=${activeConv.viaAgent ? null : onTranscribe} canTranscribe=${canTranscribe}
|
|
182
186
|
onOpenMarkdown=${(url, name) => setMdViewer({ url, name })} />`;
|
|
183
187
|
})}
|
|
184
188
|
</div>
|
|
@@ -214,6 +218,7 @@ export function ThreadPanel({
|
|
|
214
218
|
</div>` : null}
|
|
215
219
|
<${Composer} key=${'c-' + activeConv.conversationId + (draftPrefill ? '-d' + prefillNonce : '')} recipient=${activeConv.peerGhii}
|
|
216
220
|
sendLabel=${t('inbox.reply')} sending=${sending} onSend=${doSend} initialText=${draftPrefill}
|
|
221
|
+
voiceMaxSeconds=${voiceMaxSeconds}
|
|
217
222
|
focusNonce=${composerFocus} draftKey=${'aimeat.inbox.draft.' + activeConv.conversationId} />`}
|
|
218
223
|
</div>`;
|
|
219
224
|
}
|