aimeat 2.5.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 +166 -3
- package/dist/locales/fi.json +166 -3
- 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/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/profile/access-tab/connections.js +264 -0
- package/dist/public/views/profile/access-tab.js +2 -0
- 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/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 +65 -0
- package/dist/scripts/check-ai-disclosure.js.map +1 -1
- package/dist/src/cli/connect/ai-provenance-carry.d.ts +54 -3
- package/dist/src/cli/connect/ai-provenance-carry.d.ts.map +1 -1
- package/dist/src/cli/connect/ai-provenance-carry.js +84 -12
- package/dist/src/cli/connect/ai-provenance-carry.js.map +1 -1
- 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/apps.d.ts +1 -0
- package/dist/src/cli/connect/mcp/tools/apps.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/apps.js +3 -2
- package/dist/src/cli/connect/mcp/tools/apps.js.map +1 -1
- package/dist/src/cli/connect/mcp/tools/core.d.ts +2 -0
- package/dist/src/cli/connect/mcp/tools/core.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/core.js +5 -2
- package/dist/src/cli/connect/mcp/tools/core.js.map +1 -1
- package/dist/src/cli/connect/mcp/tools/knowledge.d.ts +1 -0
- package/dist/src/cli/connect/mcp/tools/knowledge.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/knowledge.js +3 -2
- package/dist/src/cli/connect/mcp/tools/knowledge.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/generated/api-types.d.ts +969 -4
- package/dist/src/generated/api-types.d.ts.map +1 -1
- package/dist/src/mcp/ai-provenance-input.d.ts +33 -0
- package/dist/src/mcp/ai-provenance-input.d.ts.map +1 -1
- package/dist/src/mcp/ai-provenance-input.js +36 -0
- 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.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/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
|
@@ -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
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file public/views/profile/inbox-tab/use-voice.js
|
|
3
|
+
* @description The Inbox tab's voice-message state: whether this owner can transcribe at all, how
|
|
4
|
+
* long the node lets a recording run, and the call that turns one voice attachment into text.
|
|
5
|
+
* Extracted from inbox-tab.js to keep it under the max-file-lines rule.
|
|
6
|
+
* @structure useVoiceMessages(loadThread, activeConvRef) → { canTranscribe, voiceMaxSeconds, transcribeVoice }
|
|
7
|
+
* @usage const { canTranscribe, voiceMaxSeconds, transcribeVoice } = useVoiceMessages(loadThread, activeConvRef);
|
|
8
|
+
* @version-history
|
|
9
|
+
* v1.0.0 — 2026-08-01 — Extracted from inbox-tab.js (voice messages).
|
|
10
|
+
*/
|
|
11
|
+
import { useState, useEffect, useCallback } from 'preact/hooks';
|
|
12
|
+
import { t } from '/js/i18n.js';
|
|
13
|
+
import { apiGet } from '/js/api.js';
|
|
14
|
+
import * as messages from '/js/services/messages.js';
|
|
15
|
+
import { swallowed } from '/js/swallowed.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {(conv: any, markRead?: boolean) => Promise<void>} loadThread
|
|
19
|
+
* @param {{ current: any }} activeConvRef the OPEN conversation, read at call time
|
|
20
|
+
*/
|
|
21
|
+
export function useVoiceMessages(loadThread, activeConvRef) {
|
|
22
|
+
// canTranscribe answers "has this owner chosen a transcription model", which decides whether a
|
|
23
|
+
// voice bubble offers a Transcribe button or explains why it cannot. Read once per mount: an owner
|
|
24
|
+
// does not change it mid-thread, and polling it would be a request per render for one boolean.
|
|
25
|
+
const [canTranscribe, setCanTranscribe] = useState(null);
|
|
26
|
+
// Served by the node so the recorder stops at the operator's number rather than one compiled in.
|
|
27
|
+
const [voiceMaxSeconds, setVoiceMaxSeconds] = useState(300);
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
apiGet('/v1/openrouter/settings')
|
|
31
|
+
.then((r) => {
|
|
32
|
+
setCanTranscribe(!!(r?.data?.sttModel));
|
|
33
|
+
const n = Number(r?.data?.limits?.voice_msg_max_seconds);
|
|
34
|
+
if (n > 0) setVoiceMaxSeconds(n);
|
|
35
|
+
})
|
|
36
|
+
.catch((err) => { swallowed('inbox-tab: stt settings', err); setCanTranscribe(false); });
|
|
37
|
+
}, []);
|
|
38
|
+
|
|
39
|
+
/** Transcribe one voice attachment on the caller's OWN copy, then reload the thread so the text
|
|
40
|
+
* appears where the audio is. Errors are rethrown so the bubble can show them in place. */
|
|
41
|
+
const transcribeVoice = useCallback(async (msgId, attId) => {
|
|
42
|
+
const r = await messages.transcribeAttachment(msgId, attId);
|
|
43
|
+
if (r?.ok === false) throw new Error(r?.error?.message || t('inbox.transcribeFailed'));
|
|
44
|
+
const conv = activeConvRef.current;
|
|
45
|
+
if (conv) await loadThread(conv);
|
|
46
|
+
return r?.data;
|
|
47
|
+
}, [loadThread, activeConvRef]);
|
|
48
|
+
|
|
49
|
+
return { canTranscribe, voiceMaxSeconds, transcribeVoice };
|
|
50
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file public/views/profile/inbox-tab/voice-parts.js
|
|
3
|
+
* @description The voice-message pieces of a message bubble: the in-place audio player and the
|
|
4
|
+
* transcript row under it. Extracted from components.js purely to keep that file under the
|
|
5
|
+
* max-file-lines rule; behaviour is unchanged.
|
|
6
|
+
*
|
|
7
|
+
* Two rules live here. Only one player runs at a time (and starting one silences the read-aloud
|
|
8
|
+
* engine, so a synthetic voice never talks over a human one). And transcription is never automatic:
|
|
9
|
+
* it spends the owner's own AI budget, so it is always a click, and the button says what it will do.
|
|
10
|
+
* @structure fmtClock · stopOtherAudio · AudioAttachment · TranscriptPanel
|
|
11
|
+
* @usage import { AudioAttachment } from './voice-parts.js';
|
|
12
|
+
* @version-history
|
|
13
|
+
* v1.0.0 — 2026-08-01 — Extracted from components.js (voice messages).
|
|
14
|
+
*/
|
|
15
|
+
import { h } from 'preact';
|
|
16
|
+
import { useState } from 'preact/hooks';
|
|
17
|
+
import htm from 'htm';
|
|
18
|
+
const html = htm.bind(h);
|
|
19
|
+
import { t } from '/js/i18n.js';
|
|
20
|
+
import { escHtml } from '/js/utils.js';
|
|
21
|
+
import { stop as stopSpeech } from '/js/services/speech-reader.js';
|
|
22
|
+
|
|
23
|
+
/** Seconds → "0:14". */
|
|
24
|
+
export function fmtClock(seconds) {
|
|
25
|
+
const s = Math.max(0, Math.round(Number(seconds) || 0));
|
|
26
|
+
return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** One voice message at a time. Two playing at once is noise, and a screen reader talking over a
|
|
30
|
+
* recording is worse — so starting one stops every other player AND the read-aloud engine. */
|
|
31
|
+
export function stopOtherAudio(current) {
|
|
32
|
+
document.querySelectorAll('audio.inbox-audio').forEach((el) => {
|
|
33
|
+
const player = /** @type {HTMLAudioElement} */ (el);
|
|
34
|
+
if (player !== current && !player.paused) player.pause();
|
|
35
|
+
});
|
|
36
|
+
stopSpeech();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The text of a voice message, and the button that produces it.
|
|
41
|
+
*
|
|
42
|
+
* Three states, none of them hidden: a transcript that came WITH the message (the sender wrote it,
|
|
43
|
+
* so reading it costs nothing), a transcript this reader produced, or an offer to produce one. With
|
|
44
|
+
* no transcription model configured the row says so and points at the settings instead of quietly
|
|
45
|
+
* rendering nothing — an empty space reads as broken.
|
|
46
|
+
*/
|
|
47
|
+
export function TranscriptPanel({ att, msgId, onTranscribe, canTranscribe }) {
|
|
48
|
+
const [busy, setBusy] = useState(false);
|
|
49
|
+
const [error, setError] = useState(null);
|
|
50
|
+
const [open, setOpen] = useState(false);
|
|
51
|
+
const tr = att.transcript;
|
|
52
|
+
|
|
53
|
+
async function run() {
|
|
54
|
+
setBusy(true); setError(null);
|
|
55
|
+
try {
|
|
56
|
+
await onTranscribe(msgId, att.id);
|
|
57
|
+
setOpen(true);
|
|
58
|
+
} catch (e) {
|
|
59
|
+
// Shown in place, not as a toast: "no model configured" and "daily budget spent" are both
|
|
60
|
+
// things the reader can act on, and they belong next to the button that produced them.
|
|
61
|
+
setError(e?.message || t('inbox.transcribeFailed'));
|
|
62
|
+
}
|
|
63
|
+
setBusy(false);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (tr?.text) {
|
|
67
|
+
return html`
|
|
68
|
+
<div class="inbox-transcript">
|
|
69
|
+
<button class="inbox-transcript-toggle" onClick=${() => setOpen((v) => !v)} aria-expanded=${open}>
|
|
70
|
+
${open ? '▾' : '▸'} ${t('inbox.transcript')}
|
|
71
|
+
<span class="inbox-transcript-src">
|
|
72
|
+
${tr.by === 'sender' ? t('inbox.transcriptBySender') : t('inbox.transcriptByYou', { model: tr.model || '' })}
|
|
73
|
+
</span>
|
|
74
|
+
</button>
|
|
75
|
+
${open ? html`<div class="inbox-transcript-text">${escHtml(tr.text)}</div>` : null}
|
|
76
|
+
</div>`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!onTranscribe) return null;
|
|
80
|
+
|
|
81
|
+
return html`
|
|
82
|
+
<div class="inbox-transcript">
|
|
83
|
+
${canTranscribe === false
|
|
84
|
+
? html`<span class="inbox-transcript-hint">${t('inbox.transcribeNoModel')}</span>`
|
|
85
|
+
: html`
|
|
86
|
+
<button class="inbox-transcript-btn" onClick=${run} disabled=${busy}>
|
|
87
|
+
${busy ? t('inbox.transcribing') : t('inbox.transcribe')}
|
|
88
|
+
</button>`}
|
|
89
|
+
${error ? html`<span class="inbox-transcript-err">${error}</span>` : null}
|
|
90
|
+
</div>`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* A voice message in a bubble: it plays here rather than opening in a browser tab, which is a
|
|
95
|
+
* download and not a conversation.
|
|
96
|
+
*
|
|
97
|
+
* preload="metadata" is deliberate — a thread with ten voice messages must not fetch all ten on
|
|
98
|
+
* open. The duration is all that is needed until someone presses play.
|
|
99
|
+
*/
|
|
100
|
+
export function AudioAttachment({ a, url, name, download, msgId, onTranscribe, canTranscribe }) {
|
|
101
|
+
return html`<div class="inbox-attach-audio">
|
|
102
|
+
<audio class="inbox-audio" controls preload="metadata" src=${url}
|
|
103
|
+
onPlay=${(e) => stopOtherAudio(e.currentTarget)}></audio>
|
|
104
|
+
<div class="inbox-attach-cap">
|
|
105
|
+
<span class="inbox-attach-name">${escHtml(name)}</span>
|
|
106
|
+
${a.durationSeconds ? html`<span class="inbox-audio-dur">${fmtClock(a.durationSeconds)}</span>` : null}
|
|
107
|
+
${download}
|
|
108
|
+
</div>
|
|
109
|
+
<${TranscriptPanel} att=${a} msgId=${msgId} onTranscribe=${onTranscribe} canTranscribe=${canTranscribe} />
|
|
110
|
+
</div>`;
|
|
111
|
+
}
|
|
@@ -14,6 +14,11 @@
|
|
|
14
14
|
* (./inbox-tab/use-thread-ux.js)
|
|
15
15
|
* @usage Lazy-loaded profile tab; registered in profile.js TABS as id `messages`.
|
|
16
16
|
* @version-history
|
|
17
|
+
* v1.27.0 -- 2026-08-01 -- Voice messages: 🎤 in the composer records a clip that travels the normal
|
|
18
|
+
* attachment path, an audio bubble plays in place, and a voice attachment can be turned into text
|
|
19
|
+
* with the owner's own key (useVoiceMessages → POST /v1/messages/:id/attachments/:attId/transcribe).
|
|
20
|
+
* Never automatic — transcription spends the owner's AI budget, so it is always a click. parkMessage
|
|
21
|
+
* and openTrackedRecord moved to helpers.js to stay under max-file-lines.
|
|
17
22
|
* v1.26.0 -- 2026-07-27 -- Failed sends toast the thrown REASON (helpers.sendFailure); the flat generic line hid every actionable attachment error.
|
|
18
23
|
* v1.25.0 -- 2026-07-21 -- Thread history is no longer capped at the newest 50: threads open showing
|
|
19
24
|
* their FULL history by default (getConversation(all)); a header "Show last 50 / all" toggle
|
|
@@ -130,17 +135,16 @@ import { escHtml } from '/js/utils.js';
|
|
|
130
135
|
import * as messages from '/js/services/messages.js';
|
|
131
136
|
import * as tracked from '/js/services/tracked-responses.js';
|
|
132
137
|
import * as agentsSvc from '/js/services/agents.js';
|
|
133
|
-
import { parkMessageToNotebook } from '/js/services/notebook.js';
|
|
134
|
-
import { firstLine } from './notebook-helpers.js';
|
|
135
138
|
import { apiGet } from '/js/api.js';
|
|
136
139
|
import { getSession } from '/js/services/auth.js';
|
|
137
140
|
import { TrackResponseModal } from './track-response-modal.js';
|
|
138
141
|
import { peerLabel } from '/js/services/messages-ai-prompts.js';
|
|
139
|
-
import { peerName, ownerKeyOf, isAgentPeer, buildAnswerSummary, resolveThreadAttachmentUrls, sendFailure } from './inbox-tab/helpers.js';
|
|
142
|
+
import { peerName, ownerKeyOf, isAgentPeer, buildAnswerSummary, resolveThreadAttachmentUrls, sendFailure, parkMessage, openTrackedRecord } from './inbox-tab/helpers.js';
|
|
140
143
|
import { Composer, PollBuilder, MarkdownViewer, ReplyWithAiPopover, ConversationToNotebookPopover } from './inbox-tab/components.js';
|
|
141
144
|
import { buildConversationReplyProps, buildMessageReplyProps, buildConversationNotebookProps } from './inbox-tab/ai-actions.js';
|
|
142
145
|
import { ListPanel, ThreadPanel, TrackedPanel, ResultsPanel } from './inbox-tab/panels.js';
|
|
143
146
|
import { useThreadAutoScroll, useMobileComposerKeyboard, useLinkPreviewToggle } from './inbox-tab/use-thread-ux.js';
|
|
147
|
+
import { useVoiceMessages } from './inbox-tab/use-voice.js';
|
|
144
148
|
import { ContactPicker } from '/components/ContactPicker.js';
|
|
145
149
|
import { swallowed } from '/js/swallowed.js';
|
|
146
150
|
|
|
@@ -148,6 +152,9 @@ export default function InboxTab({ showToast }) {
|
|
|
148
152
|
const [requests, setRequests] = useState([]);
|
|
149
153
|
const [conversations, setConversations] = useState([]);
|
|
150
154
|
const [activeConv, setActiveConv] = useState(null); // { conversationId, peerGhii }
|
|
155
|
+
// Mirrored into a ref so []-memoized callbacks (transcribeVoice) can reload the OPEN thread
|
|
156
|
+
// without taking activeConv as a dependency and being rebuilt on every thread switch.
|
|
157
|
+
const activeConvRef = useRef(null); activeConvRef.current = activeConv;
|
|
151
158
|
const [thread, setThread] = useState([]);
|
|
152
159
|
const [urlMap, setUrlMap] = useState({});
|
|
153
160
|
const { showLinkPreviews, toggleLinkPreviews } = useLinkPreviewToggle(); const [mdViewer, setMdViewer] = useState(null); // link-preview toggle (persisted) + markdown viewer state
|
|
@@ -266,12 +273,7 @@ export default function InboxTab({ showToast }) {
|
|
|
266
273
|
|
|
267
274
|
// Clicking 📓: copy the message straight into the notebook for later processing (no AI step) — keeps the
|
|
268
275
|
// source link + reply intent so it can be replied to or enriched/filed from the notebook later.
|
|
269
|
-
const onParkMsg =
|
|
270
|
-
try {
|
|
271
|
-
await parkMessageToNotebook(msg, { title: firstLine(msg.body) });
|
|
272
|
-
showToast?.(t('inbox.parkedToNotebook'));
|
|
273
|
-
} catch (e) { showToast?.(e?.message || t('inbox.trackFailed'), true); }
|
|
274
|
-
};
|
|
276
|
+
const onParkMsg = (msg) => parkMessage(msg, showToast);
|
|
275
277
|
|
|
276
278
|
const cancelTracked = async (tr) => {
|
|
277
279
|
let resp;
|
|
@@ -299,20 +301,8 @@ export default function InboxTab({ showToast }) {
|
|
|
299
301
|
setReplyingTrId(tr.id);
|
|
300
302
|
};
|
|
301
303
|
|
|
302
|
-
//
|
|
303
|
-
|
|
304
|
-
// (so the Organisms tab opens that exact workspace), then hard-navigate.
|
|
305
|
-
const openRecord = (tr) => {
|
|
306
|
-
const r = tr.references || {};
|
|
307
|
-
if (!r.organismId || !r.workspaceId) { showToast?.(t('inbox.trackNoRecord'), true); return; }
|
|
308
|
-
try {
|
|
309
|
-
sessionStorage.setItem('aimeat-profile-tab', 'organisms');
|
|
310
|
-
sessionStorage.setItem('aimeat.ws.openId', r.organismId);
|
|
311
|
-
sessionStorage.setItem('aimeat.ws.openWs', r.workspaceId);
|
|
312
|
-
// eslint-disable-next-line aimeat/no-silent-catch -- noop
|
|
313
|
-
} catch { /* noop */ }
|
|
314
|
-
window.location.assign('/v1/profile?tab=organisms');
|
|
315
|
-
};
|
|
304
|
+
// Jump to the workspace record this tracked response watches (straight to the bug it is about).
|
|
305
|
+
const openRecord = (tr) => openTrackedRecord(tr, showToast);
|
|
316
306
|
|
|
317
307
|
// Fetch the suggested-reply body for each awaiting contract in the open conversation (for the bubble).
|
|
318
308
|
const awaitingIds = awaitingForConv.map(t => t.id).join(',');
|
|
@@ -356,6 +346,9 @@ export default function InboxTab({ showToast }) {
|
|
|
356
346
|
}, []);
|
|
357
347
|
const toggleThreadAll = () => setThreadAll(v => { const nv = !v; threadAllRef.current = nv; if (activeConv) loadThread(activeConv); return nv; });
|
|
358
348
|
|
|
349
|
+
// Voice messages: can this owner transcribe, how long may a recording run, and the call that does it.
|
|
350
|
+
const { canTranscribe, voiceMaxSeconds, transcribeVoice } = useVoiceMessages(loadThread, activeConvRef);
|
|
351
|
+
|
|
359
352
|
// Mount: one composite call seeds all six sections (requests/conversations/important/tracked/agents/groups).
|
|
360
353
|
useEffect(() => { loadOverview(); }, [loadOverview]);
|
|
361
354
|
|
|
@@ -774,7 +767,8 @@ export default function InboxTab({ showToast }) {
|
|
|
774
767
|
onTrackMsg=${onTrackMsg} onParkMsg=${onParkMsg} openMessageAi=${openMessageAi} submitInteractiveAnswers=${submitInteractiveAnswers}
|
|
775
768
|
setMdViewer=${setMdViewer} openConversationAi=${openConversationAi} openConversationNotebook=${openConversationNotebook} insertCommand=${insertCommand} setCmdFill=${setCmdFill}
|
|
776
769
|
cancelTracked=${cancelTracked} openRecord=${openRecord} startSuggestedReply=${startSuggestedReply} doSend=${doSend} showLinkPreviews=${showLinkPreviews} toggleLinkPreviews=${toggleLinkPreviews}
|
|
777
|
-
threadAll=${threadAll} toggleThreadAll=${toggleThreadAll}
|
|
770
|
+
threadAll=${threadAll} toggleThreadAll=${toggleThreadAll}
|
|
771
|
+
onTranscribe=${transcribeVoice} canTranscribe=${canTranscribe} voiceMaxSeconds=${voiceMaxSeconds} />` : null}
|
|
778
772
|
|
|
779
773
|
${mode === 'tracked' ? html`<${TrackedPanel} activeTracked=${activeTracked} doneCount=${doneCount}
|
|
780
774
|
openRecord=${openRecord} openTracked=${openTracked} cancelTracked=${cancelTracked} />` : null}
|