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
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file audio-recorder.js
|
|
3
|
+
* @description Microphone capture for voice messages and the transcription test. Plain JS, no
|
|
4
|
+
* framework: one function starts a recording and hands back the controls to stop or cancel it.
|
|
5
|
+
*
|
|
6
|
+
* FORMAT: whatever the browser actually produces, negotiated with isTypeSupported — webm/opus in
|
|
7
|
+
* Chrome and Firefox, mp4/AAC in Safari. Not WAV. MediaRecorder cannot produce WAV without hand
|
|
8
|
+
* muxing PCM, and the result is roughly ten times the bytes for the same speech (a minute of 16 kHz
|
|
9
|
+
* mono WAV is ~2 MB against ~180 kB of Opus) with nothing gained: every browser plays these back,
|
|
10
|
+
* and OpenRouter's transcription endpoint lists webm, ogg, m4a and aac among its accepted formats.
|
|
11
|
+
* The extra megabytes would be paid for in the storage quota, the transfer and the federation copy.
|
|
12
|
+
*
|
|
13
|
+
* The microphone track is stopped on every exit path. A forgotten track leaves the browser's
|
|
14
|
+
* recording indicator lit, which reads to a user as an application still listening to them.
|
|
15
|
+
* @structure isRecordingSupported() · pickMimeType() · startRecording() · fmtDuration()
|
|
16
|
+
* @usage const rec = await startRecording({ onLevel, maxSeconds }); const file = await rec.stop();
|
|
17
|
+
* @version-history
|
|
18
|
+
* v1.0.0 — 2026-08-01 — Initial version for inbox voice messages + the settings STT test.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** Preference order: Opus where available (smallest for speech), then whatever the browser offers. */
|
|
22
|
+
const CANDIDATES = [
|
|
23
|
+
'audio/webm;codecs=opus',
|
|
24
|
+
'audio/webm',
|
|
25
|
+
'audio/ogg;codecs=opus',
|
|
26
|
+
'audio/mp4', // Safari
|
|
27
|
+
'audio/aac',
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
export function isRecordingSupported() {
|
|
31
|
+
return typeof window !== 'undefined'
|
|
32
|
+
&& typeof window.MediaRecorder !== 'undefined'
|
|
33
|
+
&& !!navigator?.mediaDevices?.getUserMedia;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The first candidate this browser admits to supporting, or '' to let it choose its own default. */
|
|
37
|
+
export function pickMimeType() {
|
|
38
|
+
if (typeof window === 'undefined' || !window.MediaRecorder?.isTypeSupported) return '';
|
|
39
|
+
for (const type of CANDIDATES) {
|
|
40
|
+
try { if (window.MediaRecorder.isTypeSupported(type)) return type; } catch { /* keep looking */ } // eslint-disable-line aimeat/no-silent-catch -- a browser refusing to answer about a type IS the answer: try the next one
|
|
41
|
+
}
|
|
42
|
+
return '';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** File extension for a recorded blob, from its mime type. */
|
|
46
|
+
function extFor(mime) {
|
|
47
|
+
if (!mime) return 'webm';
|
|
48
|
+
if (mime.includes('mp4')) return 'm4a';
|
|
49
|
+
if (mime.includes('aac')) return 'aac';
|
|
50
|
+
if (mime.includes('ogg')) return 'ogg';
|
|
51
|
+
return 'webm';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Seconds → "0:07" / "12:03". */
|
|
55
|
+
export function fmtDuration(seconds) {
|
|
56
|
+
const s = Math.max(0, Math.round(Number(seconds) || 0));
|
|
57
|
+
return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Start recording from the default microphone.
|
|
62
|
+
*
|
|
63
|
+
* @param {object} [opts]
|
|
64
|
+
* @param {(level: number) => void} [opts.onLevel] 0..1 input level, ~20/s while recording
|
|
65
|
+
* @param {(seconds: number) => void} [opts.onTick] elapsed seconds, once a second
|
|
66
|
+
* @param {number} [opts.maxSeconds] hard stop; the recording is kept, not discarded
|
|
67
|
+
* @param {() => void} [opts.onAutoStop] fired when maxSeconds ended the recording
|
|
68
|
+
* @returns {Promise<{ stop: () => Promise<{file: File, durationSeconds: number}>, cancel: () => void, mimeType: string }>}
|
|
69
|
+
*/
|
|
70
|
+
export async function startRecording(opts = {}) {
|
|
71
|
+
if (!isRecordingSupported()) {
|
|
72
|
+
throw new Error('unsupported');
|
|
73
|
+
}
|
|
74
|
+
const stream = await navigator.mediaDevices.getUserMedia({
|
|
75
|
+
// Speech, not music: the browser's own cleanup is worth more than the fidelity it costs.
|
|
76
|
+
audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true },
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const mimeType = pickMimeType();
|
|
80
|
+
const recorder = new MediaRecorder(stream, mimeType ? { mimeType } : undefined);
|
|
81
|
+
const chunks = [];
|
|
82
|
+
const startedAt = Date.now();
|
|
83
|
+
let stopped = false;
|
|
84
|
+
|
|
85
|
+
// Level metering. Deliberately minimal: a number, not a waveform. Its whole job is to answer
|
|
86
|
+
// "is the microphone hearing me", which a silent countdown cannot.
|
|
87
|
+
let audioCtx = null;
|
|
88
|
+
let rafId = 0;
|
|
89
|
+
if (opts.onLevel && typeof window.AudioContext !== 'undefined') {
|
|
90
|
+
try {
|
|
91
|
+
audioCtx = new AudioContext();
|
|
92
|
+
const source = audioCtx.createMediaStreamSource(stream);
|
|
93
|
+
const analyser = audioCtx.createAnalyser();
|
|
94
|
+
analyser.fftSize = 512;
|
|
95
|
+
source.connect(analyser);
|
|
96
|
+
const buf = new Uint8Array(analyser.frequencyBinCount);
|
|
97
|
+
const tick = () => {
|
|
98
|
+
analyser.getByteTimeDomainData(buf);
|
|
99
|
+
let peak = 0;
|
|
100
|
+
for (let i = 0; i < buf.length; i++) peak = Math.max(peak, Math.abs(buf[i] - 128) / 128);
|
|
101
|
+
opts.onLevel(peak);
|
|
102
|
+
rafId = requestAnimationFrame(tick);
|
|
103
|
+
};
|
|
104
|
+
rafId = requestAnimationFrame(tick);
|
|
105
|
+
} catch (err) {
|
|
106
|
+
// Metering is decoration; a browser that refuses an AudioContext still records fine.
|
|
107
|
+
audioCtx = null;
|
|
108
|
+
if (typeof console !== 'undefined') console.warn('[audio-recorder] level meter unavailable:', err?.message || err);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let tickTimer = 0;
|
|
113
|
+
if (opts.onTick) {
|
|
114
|
+
tickTimer = setInterval(() => opts.onTick(Math.floor((Date.now() - startedAt) / 1000)), 1000);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let autoStopTimer = 0;
|
|
118
|
+
if (opts.maxSeconds > 0) {
|
|
119
|
+
autoStopTimer = setTimeout(() => {
|
|
120
|
+
if (!stopped && recorder.state === 'recording') {
|
|
121
|
+
recorder.stop(); // the blob is KEPT — hitting the cap is not losing the take
|
|
122
|
+
opts.onAutoStop?.();
|
|
123
|
+
}
|
|
124
|
+
}, opts.maxSeconds * 1000);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
recorder.ondataavailable = (e) => { if (e.data && e.data.size > 0) chunks.push(e.data); };
|
|
128
|
+
|
|
129
|
+
const teardown = () => {
|
|
130
|
+
stopped = true;
|
|
131
|
+
if (tickTimer) clearInterval(tickTimer);
|
|
132
|
+
if (autoStopTimer) clearTimeout(autoStopTimer);
|
|
133
|
+
if (rafId) cancelAnimationFrame(rafId);
|
|
134
|
+
if (audioCtx) audioCtx.close().catch(() => { /* already closing */ }); // eslint-disable-line aimeat/no-silent-catch -- closing a closed context is not an error worth reporting
|
|
135
|
+
// The one line that must never be skipped: leave this out and the browser keeps showing the
|
|
136
|
+
// user that something is recording them.
|
|
137
|
+
stream.getTracks().forEach((tr) => tr.stop());
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
recorder.start();
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
mimeType: recorder.mimeType || mimeType || 'audio/webm',
|
|
144
|
+
/** Stop and resolve with the recorded File plus its measured length. */
|
|
145
|
+
stop() {
|
|
146
|
+
return new Promise((resolve, reject) => {
|
|
147
|
+
if (stopped) { reject(new Error('already stopped')); return; }
|
|
148
|
+
recorder.onstop = () => {
|
|
149
|
+
const durationSeconds = Math.max(0.1, (Date.now() - startedAt) / 1000);
|
|
150
|
+
teardown();
|
|
151
|
+
const type = recorder.mimeType || mimeType || 'audio/webm';
|
|
152
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
153
|
+
const file = new File(chunks, `voice-${stamp}.${extFor(type)}`, { type });
|
|
154
|
+
resolve({ file, durationSeconds });
|
|
155
|
+
};
|
|
156
|
+
recorder.onerror = (e) => { teardown(); reject(e?.error || new Error('recording failed')); };
|
|
157
|
+
// Already inactive (an auto-stop at maxSeconds got there first): resolve from the chunks we
|
|
158
|
+
// have instead of waiting for an onstop that will never fire.
|
|
159
|
+
if (recorder.state === 'recording') recorder.stop();
|
|
160
|
+
else recorder.onstop?.(new Event('stop'));
|
|
161
|
+
});
|
|
162
|
+
},
|
|
163
|
+
/** Throw the take away and release the microphone. */
|
|
164
|
+
cancel() {
|
|
165
|
+
if (stopped) return;
|
|
166
|
+
recorder.onstop = () => {};
|
|
167
|
+
try { if (recorder.state === 'recording') recorder.stop(); } catch { /* already stopping */ } // eslint-disable-line aimeat/no-silent-catch -- the goal is releasing the microphone below, which happens either way
|
|
168
|
+
teardown();
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
@@ -5,9 +5,12 @@
|
|
|
5
5
|
* block), plus small storage helpers used to upload and resolve message attachments.
|
|
6
6
|
* @structure send / listInbox / listConversations / getConversation / markConversationRead /
|
|
7
7
|
* markRead / deleteMessage / listRequests / acceptRequest / blockContact / listContacts /
|
|
8
|
-
* uploadAttachment / attachmentUrl
|
|
8
|
+
* uploadAttachment / attachmentUrl / transcribeAttachment
|
|
9
9
|
* @usage import * as messages from '/js/services/messages.js';
|
|
10
10
|
* @version-history
|
|
11
|
+
* v1.4.0 -- 2026-08-01 -- Voice messages: uploadAttachment carries a recording's measured
|
|
12
|
+
* `duration_seconds` into the descriptor (so a thread can show "0:14" before fetching the audio),
|
|
13
|
+
* and transcribeAttachment() turns one voice attachment into text on the caller's own copy.
|
|
11
14
|
* v1.0.0 -- 2026-06-16 -- Initial creation for user-to-user messaging (layer 5: Inbox tab).
|
|
12
15
|
* v1.0.1 -- 2026-06-19 -- JSDoc type annotations for frontend type-checking
|
|
13
16
|
* v1.1.0 -- 2026-06-23 -- send() carries the optional `interactive` payload (federated AskUserQuestion).
|
|
@@ -177,7 +180,23 @@ export async function uploadAttachment(file, kindOverride) {
|
|
|
177
180
|
const kind = kindOverride || (mime.startsWith('image/') ? 'image'
|
|
178
181
|
: mime.startsWith('audio/') ? 'audio'
|
|
179
182
|
: mime.startsWith('video/') ? 'video' : 'file');
|
|
180
|
-
|
|
183
|
+
const desc = { storage_key: key, mime, size: file.size, kind, name: file.name };
|
|
184
|
+
// A voice recording arrives with its measured length attached (composer addRecording). Carrying it
|
|
185
|
+
// in the descriptor lets the recipient's thread show "0:14" without downloading the audio first.
|
|
186
|
+
if (Number(file.durationSeconds) > 0) desc.duration_seconds = Number(file.durationSeconds);
|
|
187
|
+
return desc;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Transcribe a voice attachment on the caller's own copy of a message, with the caller's own AI key.
|
|
191
|
+
* Idempotent server-side: a second call returns the stored transcript instead of paying again. */
|
|
192
|
+
export async function transcribeAttachment(messageId, attachmentId, opts = {}) {
|
|
193
|
+
return api(`/v1/messages/${enc(messageId)}/attachments/${enc(attachmentId)}/transcribe`, {
|
|
194
|
+
method: 'POST',
|
|
195
|
+
body: JSON.stringify({ force: !!opts.force, model: opts.model, language: opts.language }),
|
|
196
|
+
// Speech-to-text is a provider round trip over an audio file; the default 30 s client timeout
|
|
197
|
+
// cuts a perfectly good transcription off at the knees.
|
|
198
|
+
timeoutMs: 180_000,
|
|
199
|
+
});
|
|
181
200
|
}
|
|
182
201
|
|
|
183
202
|
/** Resolve a presigned, no-auth download URL for one of the caller's own storage keys (for <img>). */
|
package/dist/public/spa.html
CHANGED
|
@@ -97,6 +97,7 @@
|
|
|
97
97
|
<link rel="stylesheet" href="/css/components/markdown.css">
|
|
98
98
|
<link rel="stylesheet" href="/css/components/contact-picker.css">
|
|
99
99
|
<link rel="stylesheet" href="/css/components/app-sandbox.css">
|
|
100
|
+
<link rel="stylesheet" href="/css/components/voice-recorder.css">
|
|
100
101
|
<link rel="stylesheet" href="/css/views/app-grant.css">
|
|
101
102
|
<link rel="stylesheet" href="/css/views/invite-accept.css">
|
|
102
103
|
<!-- View CSS preloaded so they are ready before any view renders (prevents FOUC) -->
|
|
@@ -148,6 +149,7 @@
|
|
|
148
149
|
"/js/recents.js": "/js/recents.js",
|
|
149
150
|
"/js/services/admin.js": "/js/services/admin.js",
|
|
150
151
|
"/js/services/agents.js": "/js/services/agents.js",
|
|
152
|
+
"/js/services/audio-recorder.js": "/js/services/audio-recorder.js",
|
|
151
153
|
"/js/services/ecosystem.js": "/js/services/ecosystem.js",
|
|
152
154
|
"/js/services/apps.js": "/js/services/apps.js",
|
|
153
155
|
"/js/services/auth.js": "/js/services/auth.js",
|
|
@@ -189,6 +191,7 @@
|
|
|
189
191
|
"/js/components/tag-cloud.js": "/js/components/tag-cloud.js",
|
|
190
192
|
"/js/components/tag-editor.js": "/js/components/tag-editor.js",
|
|
191
193
|
"/components/Modal.js": "/components/Modal.js",
|
|
194
|
+
"/components/VoiceRecorder.js": "/components/VoiceRecorder.js",
|
|
192
195
|
"/components/useViewCSS.js": "/components/useViewCSS.js",
|
|
193
196
|
"/components/Markdown.js": "/components/Markdown.js",
|
|
194
197
|
"/components/LinkPreview.js": "/components/LinkPreview.js",
|
|
@@ -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}
|