@happyvertical/smrt-chat 0.55.6 → 0.55.7
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/README.md +2 -0
- package/dist/chunks/{chat-stream-Dq-tb26i.js → chat-stream-BM3-maAM.js} +2 -2
- package/dist/chunks/chat-stream-BM3-maAM.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/manifest.json +9 -1
- package/dist/smrt-knowledge.json +11 -3
- package/dist/svelte/components/assistant/AssistantComposer.svelte +183 -27
- package/dist/svelte/components/assistant/AssistantComposer.svelte.d.ts +41 -2
- package/dist/svelte/components/assistant/AssistantComposer.svelte.d.ts.map +1 -1
- package/dist/svelte/components/assistant/AssistantDock.svelte +231 -12
- package/dist/svelte/components/assistant/AssistantDock.svelte.d.ts +26 -1
- package/dist/svelte/components/assistant/AssistantDock.svelte.d.ts.map +1 -1
- package/dist/svelte/i18n.d.ts +3 -0
- package/dist/svelte/i18n.d.ts.map +1 -1
- package/dist/svelte/i18n.js +4 -0
- package/dist/sveltekit.js +1 -1
- package/package.json +15 -15
- package/dist/chunks/chat-stream-Dq-tb26i.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { c as ChatThread, d as ChatParticipant, f as ChatMessage, i as clientRequestMessageId, l as ChatRoom, n as ChatService, o as VoiceSessionCollection, p as AgentSession, s as VoiceSession, t as ChatClientRequestConflictError, u as ChatReaction } from "./chunks/ChatService-C9UxuJaw.js";
|
|
2
|
-
import { A as MAX_CLIENT_TOOLS, C as SESSION_CONTINUATIONS_FIELD, D as runAssistantTurn, E as createSessionContinuationStore, F as classifyToolError, I as invokeManifestTool, L as manifestToolToAITool, M as MAX_CLIENT_TOOL_SCHEMA_BYTES, N as appendClientToolResults, O as CLIENT_TOOL_RESULT_GUIDANCE, P as buildManifestToolCatalog, R as runToolLoop, S as DEFAULT_CONTINUATION_TTL_MS, T as createMemoryContinuationStore, _ as runPersonaConversationTurn, a as MAX_CHAT_STREAM_CONTENT_LENGTH, b as CONTINUATION_CLAIM_TTL_MS, c as encodeChatStreamEvent, d as conversationPersonaFromAgentPersona, f as conversationPersonaFromResolved, g as resolveConversationInstructions, h as recallPersonaMemory, i as DEFAULT_CHAT_STREAM_HEARTBEAT_MS, j as MAX_CLIENT_TOOL_RESULT_CHARS, k as DEFAULT_MAX_STEPS, l as runChatConversationStream, m as principalBindingFor, n as ChatStreamError, o as MAX_CHAT_STREAM_MESSAGES, p as formatRecalledMemory, r as ChatStreamUnauthorizedError, s as createChatStreamHandler, t as ChatStreamBadRequestError, u as bindPersonaToSession, v as ASSISTANT_TURN_GENERIC_ERROR, w as createAssistantTurnResponse, x as DEFAULT_ASSISTANT_TURN_HEARTBEAT_MS, y as AssistantTurnUserError, z as sanitizeClientToolDeclarations } from "./chunks/chat-stream-
|
|
2
|
+
import { A as MAX_CLIENT_TOOLS, C as SESSION_CONTINUATIONS_FIELD, D as runAssistantTurn, E as createSessionContinuationStore, F as classifyToolError, I as invokeManifestTool, L as manifestToolToAITool, M as MAX_CLIENT_TOOL_SCHEMA_BYTES, N as appendClientToolResults, O as CLIENT_TOOL_RESULT_GUIDANCE, P as buildManifestToolCatalog, R as runToolLoop, S as DEFAULT_CONTINUATION_TTL_MS, T as createMemoryContinuationStore, _ as runPersonaConversationTurn, a as MAX_CHAT_STREAM_CONTENT_LENGTH, b as CONTINUATION_CLAIM_TTL_MS, c as encodeChatStreamEvent, d as conversationPersonaFromAgentPersona, f as conversationPersonaFromResolved, g as resolveConversationInstructions, h as recallPersonaMemory, i as DEFAULT_CHAT_STREAM_HEARTBEAT_MS, j as MAX_CLIENT_TOOL_RESULT_CHARS, k as DEFAULT_MAX_STEPS, l as runChatConversationStream, m as principalBindingFor, n as ChatStreamError, o as MAX_CHAT_STREAM_MESSAGES, p as formatRecalledMemory, r as ChatStreamUnauthorizedError, s as createChatStreamHandler, t as ChatStreamBadRequestError, u as bindPersonaToSession, v as ASSISTANT_TURN_GENERIC_ERROR, w as createAssistantTurnResponse, x as DEFAULT_ASSISTANT_TURN_HEARTBEAT_MS, y as AssistantTurnUserError, z as sanitizeClientToolDeclarations } from "./chunks/chat-stream-BM3-maAM.js";
|
|
3
3
|
import { i as matchesToolAllowList, n as encodeAssistantTurnEvent, r as readAssistantTurnStream, t as AssistantTurnStreamError } from "./chunks/assistant-turn-events-BbZUf-Ju.js";
|
|
4
4
|
import { a as createServerDataSurfaceBridge, i as createDataSurfaceCommandBridge, n as DEFAULT_DATA_SURFACE_BRIDGE_TTL_MS, r as createDataSurfaceBridge, t as DATA_SURFACE_BRIDGE_VERSION } from "./chunks/data-surface-bridge-BJomMxjS.js";
|
|
5
5
|
import { CHAT_MODULE_META, CHAT_UI_SLOTS } from "./ui.js";
|
package/dist/manifest.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"version": "1.0.0",
|
|
3
3
|
"timestamp": 0,
|
|
4
4
|
"packageName": "@happyvertical/smrt-chat",
|
|
5
|
-
"packageVersion": "0.55.
|
|
5
|
+
"packageVersion": "0.55.7",
|
|
6
6
|
"objects": {
|
|
7
7
|
"@happyvertical/smrt-chat:AgentSessionCollection": {
|
|
8
8
|
"name": "agentsessioncollection",
|
|
@@ -1948,6 +1948,7 @@
|
|
|
1948
1948
|
"className": "AgentSession",
|
|
1949
1949
|
"qualifiedName": "@happyvertical/smrt-chat:AgentSession",
|
|
1950
1950
|
"collection": "agentsessions",
|
|
1951
|
+
"description": "Agent conversation session.",
|
|
1951
1952
|
"filePath": "/home/runner/work/smrt/smrt/packages/chat/src/models/AgentSession.ts",
|
|
1952
1953
|
"packageName": "@happyvertical/smrt-chat",
|
|
1953
1954
|
"fields": {
|
|
@@ -2406,6 +2407,7 @@
|
|
|
2406
2407
|
"className": "ChatMessage",
|
|
2407
2408
|
"qualifiedName": "@happyvertical/smrt-chat:ChatMessage",
|
|
2408
2409
|
"collection": "chatmessages",
|
|
2410
|
+
"description": "Chat message. Internal model — all mutations must go through the membership-checked ChatService (S5 #1392).",
|
|
2409
2411
|
"filePath": "/home/runner/work/smrt/smrt/packages/chat/src/models/ChatMessage.ts",
|
|
2410
2412
|
"packageName": "@happyvertical/smrt-chat",
|
|
2411
2413
|
"fields": {
|
|
@@ -2924,6 +2926,7 @@
|
|
|
2924
2926
|
"className": "ChatParticipant",
|
|
2925
2927
|
"qualifiedName": "@happyvertical/smrt-chat:ChatParticipant",
|
|
2926
2928
|
"collection": "chatparticipants",
|
|
2929
|
+
"description": "Room membership row. Internal model — membership must be established and mutated only through the membership/owner-checked ChatService (S5 #1392).",
|
|
2927
2930
|
"filePath": "/home/runner/work/smrt/smrt/packages/chat/src/models/ChatParticipant.ts",
|
|
2928
2931
|
"packageName": "@happyvertical/smrt-chat",
|
|
2929
2932
|
"fields": {
|
|
@@ -3303,6 +3306,7 @@
|
|
|
3303
3306
|
"className": "ChatReaction",
|
|
3304
3307
|
"qualifiedName": "@happyvertical/smrt-chat:ChatReaction",
|
|
3305
3308
|
"collection": "chatreactions",
|
|
3309
|
+
"description": "Message reaction. Internal model — reactions must be added/removed only through the membership-checked ChatService (S5 #1392).",
|
|
3306
3310
|
"filePath": "/home/runner/work/smrt/smrt/packages/chat/src/models/ChatReaction.ts",
|
|
3307
3311
|
"packageName": "@happyvertical/smrt-chat",
|
|
3308
3312
|
"fields": {
|
|
@@ -3487,6 +3491,7 @@
|
|
|
3487
3491
|
"className": "ChatRoom",
|
|
3488
3492
|
"qualifiedName": "@happyvertical/smrt-chat:ChatRoom",
|
|
3489
3493
|
"collection": "chatrooms",
|
|
3494
|
+
"description": "Chat room supporting public channels, private groups, DMs, and agent conversations.",
|
|
3490
3495
|
"filePath": "/home/runner/work/smrt/smrt/packages/chat/src/models/ChatRoom.ts",
|
|
3491
3496
|
"packageName": "@happyvertical/smrt-chat",
|
|
3492
3497
|
"fields": {
|
|
@@ -3841,6 +3846,7 @@
|
|
|
3841
3846
|
"className": "ChatThread",
|
|
3842
3847
|
"qualifiedName": "@happyvertical/smrt-chat:ChatThread",
|
|
3843
3848
|
"collection": "chatthreads",
|
|
3849
|
+
"description": "Conversation thread. Internal model — threads must be created/mutated only through the membership-checked ChatService (S5 #1392).",
|
|
3844
3850
|
"filePath": "/home/runner/work/smrt/smrt/packages/chat/src/models/ChatThread.ts",
|
|
3845
3851
|
"packageName": "@happyvertical/smrt-chat",
|
|
3846
3852
|
"fields": {
|
|
@@ -4085,6 +4091,7 @@
|
|
|
4085
4091
|
"className": "VoiceGatewayTurn",
|
|
4086
4092
|
"qualifiedName": "@happyvertical/smrt-chat:VoiceGatewayTurn",
|
|
4087
4093
|
"collection": "voicegatewayturns",
|
|
4094
|
+
"description": "Durable reservation for a gateway turn. The natural key makes turn_id replay checks concurrency-safe before transcript messages are written.",
|
|
4088
4095
|
"filePath": "/home/runner/work/smrt/smrt/packages/chat/src/models/VoiceGatewayTurn.ts",
|
|
4089
4096
|
"packageName": "@happyvertical/smrt-chat",
|
|
4090
4097
|
"fields": {
|
|
@@ -4357,6 +4364,7 @@
|
|
|
4357
4364
|
"className": "VoiceSession",
|
|
4358
4365
|
"qualifiedName": "@happyvertical/smrt-chat:VoiceSession",
|
|
4359
4366
|
"collection": "voicesessions",
|
|
4367
|
+
"description": "Short-lived binding proving a voice gateway turn belongs to an authenticated SMRT actor, persona, and chat session.",
|
|
4360
4368
|
"filePath": "/home/runner/work/smrt/smrt/packages/chat/src/models/VoiceSession.ts",
|
|
4361
4369
|
"packageName": "@happyvertical/smrt-chat",
|
|
4362
4370
|
"fields": {
|
package/dist/smrt-knowledge.json
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
"sensitiveFieldsExcluded": true,
|
|
4
4
|
"generatedAt": "1970-01-01T00:00:00.000Z",
|
|
5
5
|
"packageName": "@happyvertical/smrt-chat",
|
|
6
|
-
"packageVersion": "0.55.
|
|
6
|
+
"packageVersion": "0.55.7",
|
|
7
7
|
"sourceManifestPath": "dist/manifest.json",
|
|
8
8
|
"agentDocPath": "AGENTS.md",
|
|
9
9
|
"sourceHashes": {
|
|
10
|
-
"manifest": "
|
|
11
|
-
"packageJson": "
|
|
10
|
+
"manifest": "f855574fffcad9f6344572790fef65561ef4cf1c2e05e7d4ca720f2548c188ed",
|
|
11
|
+
"packageJson": "3fcd03b006478ce2d74ceed792325386f344095904d71501ac770628d661535d",
|
|
12
12
|
"agents": "0d11ad3c134fc4de4c3fd61fb3a5e7d760ecf8ef6b2b16679b66358e6877dd74",
|
|
13
13
|
"moduleDoc:agents/dev-workbench.md": "69b6f827d25c8aeb0e7516c90303af23948803213b5d34bc334c1e3376c83a16",
|
|
14
14
|
"agentSurface:src/routes/chat-dev.intents.ts": "ff8f7c19c7eb6e841a08f03c97773c53092ed208004666bfed372bd1ad456b35"
|
|
@@ -1237,6 +1237,7 @@
|
|
|
1237
1237
|
"name": "AgentSession",
|
|
1238
1238
|
"qualifiedName": "@happyvertical/smrt-chat:AgentSession",
|
|
1239
1239
|
"collection": "agentsessions",
|
|
1240
|
+
"description": "Agent conversation session.",
|
|
1240
1241
|
"tableName": "agent_sessions",
|
|
1241
1242
|
"packageName": "@happyvertical/smrt-chat",
|
|
1242
1243
|
"extends": "SmrtObject",
|
|
@@ -1689,6 +1690,7 @@
|
|
|
1689
1690
|
"name": "ChatMessage",
|
|
1690
1691
|
"qualifiedName": "@happyvertical/smrt-chat:ChatMessage",
|
|
1691
1692
|
"collection": "chatmessages",
|
|
1693
|
+
"description": "Chat message. Internal model — all mutations must go through the membership-checked ChatService (S5 #1392).",
|
|
1692
1694
|
"tableName": "chat_messages",
|
|
1693
1695
|
"packageName": "@happyvertical/smrt-chat",
|
|
1694
1696
|
"extends": "SmrtObject",
|
|
@@ -2190,6 +2192,7 @@
|
|
|
2190
2192
|
"name": "ChatParticipant",
|
|
2191
2193
|
"qualifiedName": "@happyvertical/smrt-chat:ChatParticipant",
|
|
2192
2194
|
"collection": "chatparticipants",
|
|
2195
|
+
"description": "Room membership row. Internal model — membership must be established and mutated only through the membership/owner-checked ChatService (S5 #1392).",
|
|
2193
2196
|
"tableName": "chat_participants",
|
|
2194
2197
|
"packageName": "@happyvertical/smrt-chat",
|
|
2195
2198
|
"extends": "SmrtObject",
|
|
@@ -2504,6 +2507,7 @@
|
|
|
2504
2507
|
"name": "ChatReaction",
|
|
2505
2508
|
"qualifiedName": "@happyvertical/smrt-chat:ChatReaction",
|
|
2506
2509
|
"collection": "chatreactions",
|
|
2510
|
+
"description": "Message reaction. Internal model — reactions must be added/removed only through the membership-checked ChatService (S5 #1392).",
|
|
2507
2511
|
"tableName": "chat_reactions",
|
|
2508
2512
|
"packageName": "@happyvertical/smrt-chat",
|
|
2509
2513
|
"extends": "SmrtObject",
|
|
@@ -2592,6 +2596,7 @@
|
|
|
2592
2596
|
"qualifiedName": "@happyvertical/smrt-chat:ChatRoom",
|
|
2593
2597
|
"displayLabelField": "name",
|
|
2594
2598
|
"collection": "chatrooms",
|
|
2599
|
+
"description": "Chat room supporting public channels, private groups, DMs, and agent conversations.",
|
|
2595
2600
|
"tableName": "chat_rooms",
|
|
2596
2601
|
"packageName": "@happyvertical/smrt-chat",
|
|
2597
2602
|
"extends": "SmrtObject",
|
|
@@ -2946,6 +2951,7 @@
|
|
|
2946
2951
|
"qualifiedName": "@happyvertical/smrt-chat:ChatThread",
|
|
2947
2952
|
"displayLabelField": "title",
|
|
2948
2953
|
"collection": "chatthreads",
|
|
2954
|
+
"description": "Conversation thread. Internal model — threads must be created/mutated only through the membership-checked ChatService (S5 #1392).",
|
|
2949
2955
|
"tableName": "chat_threads",
|
|
2950
2956
|
"packageName": "@happyvertical/smrt-chat",
|
|
2951
2957
|
"extends": "SmrtObject",
|
|
@@ -3146,6 +3152,7 @@
|
|
|
3146
3152
|
"name": "VoiceGatewayTurn",
|
|
3147
3153
|
"qualifiedName": "@happyvertical/smrt-chat:VoiceGatewayTurn",
|
|
3148
3154
|
"collection": "voicegatewayturns",
|
|
3155
|
+
"description": "Durable reservation for a gateway turn. The natural key makes turn_id replay checks concurrency-safe before transcript messages are written.",
|
|
3149
3156
|
"tableName": "voice_gateway_turns",
|
|
3150
3157
|
"packageName": "@happyvertical/smrt-chat",
|
|
3151
3158
|
"extends": "SmrtObject",
|
|
@@ -3301,6 +3308,7 @@
|
|
|
3301
3308
|
"name": "VoiceSession",
|
|
3302
3309
|
"qualifiedName": "@happyvertical/smrt-chat:VoiceSession",
|
|
3303
3310
|
"collection": "voicesessions",
|
|
3311
|
+
"description": "Short-lived binding proving a voice gateway turn belongs to an authenticated SMRT actor, persona, and chat session.",
|
|
3304
3312
|
"tableName": "voice_sessions",
|
|
3305
3313
|
"packageName": "@happyvertical/smrt-chat",
|
|
3306
3314
|
"extends": "SmrtObject",
|
|
@@ -15,6 +15,8 @@ import {
|
|
|
15
15
|
type DictationSourceProvider,
|
|
16
16
|
DictationStatus,
|
|
17
17
|
type DictationTranscribe,
|
|
18
|
+
type HandsFreeCaptureFactory,
|
|
19
|
+
type HandsFreeVadOptions,
|
|
18
20
|
insertTextAtCursor,
|
|
19
21
|
longPress,
|
|
20
22
|
primeReadyBeep,
|
|
@@ -22,7 +24,7 @@ import {
|
|
|
22
24
|
} from '@happyvertical/smrt-ui/forms';
|
|
23
25
|
import { useI18n } from '@happyvertical/smrt-ui/i18n';
|
|
24
26
|
import { Button } from '@happyvertical/smrt-ui/ui';
|
|
25
|
-
import { onDestroy } from 'svelte';
|
|
27
|
+
import { onDestroy, untrack } from 'svelte';
|
|
26
28
|
import { M } from '../../i18n.js';
|
|
27
29
|
import type { AssistantAttachmentRef } from './assistant-transport.js';
|
|
28
30
|
|
|
@@ -66,6 +68,43 @@ export interface Props {
|
|
|
66
68
|
* own, without `dictation`.
|
|
67
69
|
*/
|
|
68
70
|
transcribe?: DictationTranscribe | null;
|
|
71
|
+
/**
|
|
72
|
+
* `'hands-free'`: the microphone stays on and each sentence is written
|
|
73
|
+
* down when the speaker pauses, with no tap per phrase; tapping the
|
|
74
|
+
* microphone (or sending, or Escape) ends it. Needs `handsFreeCapture` and
|
|
75
|
+
* an on-device speech source (smrt-svelte's `whisper-local` / `moonshine`);
|
|
76
|
+
* otherwise it is ordinary press-to-talk. Default `'push'`.
|
|
77
|
+
*/
|
|
78
|
+
dictationMode?: 'push' | 'hands-free';
|
|
79
|
+
/**
|
|
80
|
+
* The hands-free microphone, `createHandsFreeCapture` from
|
|
81
|
+
* `@happyvertical/smrt-ui/forms/hands-free`.
|
|
82
|
+
*/
|
|
83
|
+
handsFreeCapture?: HandsFreeCaptureFactory | null;
|
|
84
|
+
/** Pause length and sensitivity for hands-free (see `HandsFreeVadOptions`). */
|
|
85
|
+
handsFreeVad?: HandsFreeVadOptions;
|
|
86
|
+
/**
|
|
87
|
+
* Hands-free only: send the message once the speaker has been quiet for
|
|
88
|
+
* `sendOnPauseMs` after the last sentence was written down (the same path
|
|
89
|
+
* as pressing Send). A new sentence, a key press, Escape or the microphone
|
|
90
|
+
* button cancels the pending send. Never sends an empty message. While a
|
|
91
|
+
* send is in flight, or the composer is disabled, the send waits and the
|
|
92
|
+
* text stays in the box until the composer can accept it again. Default
|
|
93
|
+
* `false`.
|
|
94
|
+
*/
|
|
95
|
+
sendOnPause?: boolean;
|
|
96
|
+
/** Quiet time before `sendOnPause` sends, in ms. Default 1200. */
|
|
97
|
+
sendOnPauseMs?: number;
|
|
98
|
+
/**
|
|
99
|
+
* The assistant's reply is being played aloud (text to speech). While
|
|
100
|
+
* `true`, hands-free listening is suspended (half-duplex: the microphone
|
|
101
|
+
* stays open but nothing is heard, so the assistant is not transcribed into
|
|
102
|
+
* the box) and resumes after a short guard when it turns `false`. The
|
|
103
|
+
* microphone shows "Paused while the assistant speaks". A pending send on
|
|
104
|
+
* pause is unaffected; tapping the microphone still ends hands-free.
|
|
105
|
+
* Default `false`.
|
|
106
|
+
*/
|
|
107
|
+
speaking?: boolean;
|
|
69
108
|
}
|
|
70
109
|
|
|
71
110
|
let {
|
|
@@ -76,6 +115,12 @@ let {
|
|
|
76
115
|
value: content = $bindable(''),
|
|
77
116
|
dictation: dictationSource = null,
|
|
78
117
|
transcribe = null,
|
|
118
|
+
dictationMode = 'push',
|
|
119
|
+
handsFreeCapture = null,
|
|
120
|
+
handsFreeVad,
|
|
121
|
+
sendOnPause = false,
|
|
122
|
+
sendOnPauseMs = 1200,
|
|
123
|
+
speaking = false,
|
|
79
124
|
}: Props = $props();
|
|
80
125
|
const canDictate = $derived(Boolean(dictationSource || transcribe));
|
|
81
126
|
let stagedAttachments = $state<AssistantAttachmentRef[]>([]);
|
|
@@ -105,6 +150,26 @@ function messageField(): HTMLTextAreaElement | null {
|
|
|
105
150
|
return textareaEl ?? null;
|
|
106
151
|
}
|
|
107
152
|
|
|
153
|
+
/** Moves focus to the message field (a host that opens the composer
|
|
154
|
+
* directly, e.g. AssistantDock `conversations="single"`). A no-op while the
|
|
155
|
+
* composer is disabled. */
|
|
156
|
+
export function focus(): void {
|
|
157
|
+
messageField()?.focus();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Send on pause: true once a hands-free sentence has been written into the
|
|
161
|
+
// box and not yet sent, `autoSendCountdown` while the grace period runs, and
|
|
162
|
+
// `autoSendWaiting` when the grace period is over but a send is in flight.
|
|
163
|
+
let sendDictated = $state(false);
|
|
164
|
+
let autoSendCountdown = $state(false);
|
|
165
|
+
let autoSendWaiting = $state(false);
|
|
166
|
+
|
|
167
|
+
function cancelAutoSend() {
|
|
168
|
+
sendDictated = false;
|
|
169
|
+
autoSendCountdown = false;
|
|
170
|
+
autoSendWaiting = false;
|
|
171
|
+
}
|
|
172
|
+
|
|
108
173
|
// One dictation per composer; the source is read when listening starts.
|
|
109
174
|
const dictation = new Dictation({
|
|
110
175
|
source: () => {
|
|
@@ -112,6 +177,8 @@ const dictation = new Dictation({
|
|
|
112
177
|
return dictationSource();
|
|
113
178
|
},
|
|
114
179
|
onText: (text) => {
|
|
180
|
+
// A new sentence (re)arms the send-on-pause countdown.
|
|
181
|
+
sendDictated = true;
|
|
115
182
|
const field = messageField();
|
|
116
183
|
if (field) insertTextAtCursor(field, text);
|
|
117
184
|
else content = content ? `${content} ${text}` : text;
|
|
@@ -119,11 +186,74 @@ const dictation = new Dictation({
|
|
|
119
186
|
});
|
|
120
187
|
// The recording fallback follows the prop (a host may set it later).
|
|
121
188
|
$effect.pre(() => {
|
|
122
|
-
dictation.setOptions({
|
|
189
|
+
dictation.setOptions({
|
|
190
|
+
transcribe,
|
|
191
|
+
mode: dictationMode,
|
|
192
|
+
handsFreeCapture,
|
|
193
|
+
vad: handsFreeVad,
|
|
194
|
+
});
|
|
123
195
|
});
|
|
124
196
|
|
|
125
197
|
onDestroy(() => dictation.dispose());
|
|
126
198
|
|
|
199
|
+
// Half-duplex: do not listen while the assistant's reply is played aloud.
|
|
200
|
+
$effect(() => {
|
|
201
|
+
const on = speaking;
|
|
202
|
+
untrack(() => (on ? dictation.suspend() : dictation.resume()));
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// The grace period: quiet, nothing being written down, something to send.
|
|
206
|
+
// Speech starting (or a sentence still queued) clears it; the next written
|
|
207
|
+
// sentence starts it again.
|
|
208
|
+
$effect(() => {
|
|
209
|
+
const armed =
|
|
210
|
+
sendOnPause &&
|
|
211
|
+
sendDictated &&
|
|
212
|
+
dictation.handsFree &&
|
|
213
|
+
dictation.state === 'listening' &&
|
|
214
|
+
!dictation.speaking &&
|
|
215
|
+
dictation.queued === 0 &&
|
|
216
|
+
content.trim().length > 0;
|
|
217
|
+
if (!armed) {
|
|
218
|
+
autoSendCountdown = false;
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
autoSendCountdown = true;
|
|
222
|
+
const timer = setTimeout(
|
|
223
|
+
() => {
|
|
224
|
+
autoSendCountdown = false;
|
|
225
|
+
sendDictated = false;
|
|
226
|
+
autoSendWaiting = true;
|
|
227
|
+
},
|
|
228
|
+
Math.max(0, sendOnPauseMs),
|
|
229
|
+
);
|
|
230
|
+
return () => clearTimeout(timer);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// Ends of the wait: send as soon as the composer can take input, but only
|
|
234
|
+
// while still hands-free (stopping the microphone cancels a pending send).
|
|
235
|
+
$effect(() => {
|
|
236
|
+
if (!autoSendWaiting) return;
|
|
237
|
+
if (!sendOnPause || !dictation.handsFree || !content.trim()) {
|
|
238
|
+
autoSendWaiting = false;
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (dictation.speaking || dictation.queued > 0) {
|
|
242
|
+
// More was said while waiting: back to the grace period.
|
|
243
|
+
autoSendWaiting = false;
|
|
244
|
+
sendDictated = true;
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (sending || disabled || uploading) return;
|
|
248
|
+
autoSendWaiting = false;
|
|
249
|
+
void handleSend(true);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// Hands-free ended (microphone tapped, Escape, an error): nothing is sent.
|
|
253
|
+
$effect(() => {
|
|
254
|
+
if (!dictation.handsFree) cancelAutoSend();
|
|
255
|
+
});
|
|
256
|
+
|
|
127
257
|
function startDictationFromHold() {
|
|
128
258
|
if (disabled || uploading || dictation.active) return;
|
|
129
259
|
void dictation.start();
|
|
@@ -192,25 +322,34 @@ async function handleDrop(event: DragEvent) {
|
|
|
192
322
|
}
|
|
193
323
|
}
|
|
194
324
|
|
|
195
|
-
|
|
325
|
+
/** `keepListening`: a hands-free send on pause keeps the microphone on so the
|
|
326
|
+
* conversation can go on; pressing Send or Enter ends dictation. */
|
|
327
|
+
async function handleSend(keepListening = false) {
|
|
328
|
+
if (disabled || sending) return;
|
|
329
|
+
cancelAutoSend();
|
|
330
|
+
// End dictation and wait for its last words before the draft is read.
|
|
331
|
+
if (dictation.active && !keepListening) {
|
|
332
|
+
await dictation.stop();
|
|
333
|
+
await dictation.whenSettled();
|
|
334
|
+
}
|
|
196
335
|
const trimmed = content.trim();
|
|
197
336
|
if (!trimmed || disabled || sending) return;
|
|
198
|
-
if (dictation.active) void dictation.stop();
|
|
199
337
|
sendError = null;
|
|
200
338
|
sending = true;
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
//
|
|
339
|
+
// The sent text leaves the box as soon as the send starts, so a manual or
|
|
340
|
+
// automatic send never leaves it sitting there for the whole reply, and
|
|
341
|
+
// anything said or typed meanwhile lands in a fresh box. A failed send puts
|
|
342
|
+
// it back (#2904 review finding 4: never lose the message on failure).
|
|
343
|
+
const sentAttachments = stagedAttachments;
|
|
344
|
+
content = '';
|
|
345
|
+
stagedAttachments = [];
|
|
346
|
+
if (textareaEl) textareaEl.style.height = 'auto';
|
|
204
347
|
try {
|
|
205
|
-
await onsend(trimmed,
|
|
206
|
-
// #2991: text typed while the send was in flight is newer than what was
|
|
207
|
-
// sent; keep it.
|
|
208
|
-
if (content.trim() === trimmed) content = '';
|
|
209
|
-
stagedAttachments = [];
|
|
210
|
-
if (textareaEl) {
|
|
211
|
-
textareaEl.style.height = 'auto';
|
|
212
|
-
}
|
|
348
|
+
await onsend(trimmed, sentAttachments);
|
|
213
349
|
} catch (error) {
|
|
350
|
+
const newer = content.trim();
|
|
351
|
+
content = newer ? `${trimmed}\n${newer}` : trimmed;
|
|
352
|
+
stagedAttachments = [...sentAttachments, ...stagedAttachments];
|
|
214
353
|
sendError =
|
|
215
354
|
error instanceof Error
|
|
216
355
|
? error.message
|
|
@@ -221,17 +360,20 @@ async function handleSend() {
|
|
|
221
360
|
}
|
|
222
361
|
|
|
223
362
|
function handleKeydown(event: KeyboardEvent) {
|
|
224
|
-
if (event.key === 'Escape' && dictation.active) {
|
|
363
|
+
if (event.key === 'Escape' && (dictation.active || sendDictated)) {
|
|
225
364
|
event.preventDefault();
|
|
226
|
-
|
|
365
|
+
cancelAutoSend();
|
|
366
|
+
if (dictation.active) void dictation.stop();
|
|
227
367
|
return;
|
|
228
368
|
}
|
|
369
|
+
// Typing means the person has taken over: no automatic send.
|
|
370
|
+
if (event.key.length === 1 || event.key === 'Backspace') cancelAutoSend();
|
|
229
371
|
// Matches ../messages/MessageInput.svelte's handleKeydown convention:
|
|
230
372
|
// plain Enter sends, Shift+Enter inserts a newline. `isComposing` guards
|
|
231
373
|
// against an IME's confirmation Enter being treated as a send.
|
|
232
374
|
if (event.key === 'Enter' && !event.shiftKey && !event.isComposing) {
|
|
233
375
|
event.preventDefault();
|
|
234
|
-
handleSend();
|
|
376
|
+
void handleSend();
|
|
235
377
|
}
|
|
236
378
|
}
|
|
237
379
|
|
|
@@ -284,7 +426,7 @@ function removeAttachment(id: string) {
|
|
|
284
426
|
</ul>
|
|
285
427
|
{/if}
|
|
286
428
|
{#if canDictate}
|
|
287
|
-
<DictationStatus {dictation} />
|
|
429
|
+
<DictationStatus {dictation} sending={autoSendCountdown || autoSendWaiting} />
|
|
288
430
|
{/if}
|
|
289
431
|
<div class="assistant-composer-row">
|
|
290
432
|
{#if onupload}
|
|
@@ -349,7 +491,7 @@ function removeAttachment(id: string) {
|
|
|
349
491
|
<Button
|
|
350
492
|
type="button"
|
|
351
493
|
class="assistant-composer-send"
|
|
352
|
-
onclick={handleSend}
|
|
494
|
+
onclick={() => handleSend()}
|
|
353
495
|
disabled={disabled || uploading || sending || !content.trim()}
|
|
354
496
|
>
|
|
355
497
|
{t(M['chat.assistant_composer.send'])}
|
|
@@ -363,8 +505,10 @@ function removeAttachment(id: string) {
|
|
|
363
505
|
background: var(--smrt-color-surface, #ffffff);
|
|
364
506
|
padding: var(--smrt-spacing-2, 8px);
|
|
365
507
|
flex-shrink: 0;
|
|
508
|
+
container-type: inline-size;
|
|
366
509
|
}
|
|
367
510
|
|
|
511
|
+
|
|
368
512
|
.assistant-composer-row {
|
|
369
513
|
display: flex;
|
|
370
514
|
align-items: flex-end;
|
|
@@ -419,13 +563,7 @@ function removeAttachment(id: string) {
|
|
|
419
563
|
/* Phones: 16px text so iOS Safari doesn't zoom the page when the
|
|
420
564
|
message field gets focus. */
|
|
421
565
|
@media (max-width: 48rem) {
|
|
422
|
-
.assistant-composer-
|
|
423
|
-
flex: 1;
|
|
424
|
-
display: flex;
|
|
425
|
-
min-inline-size: 0;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
:global(.assistant-composer-textarea) {
|
|
566
|
+
:global(.assistant-composer-textarea) {
|
|
429
567
|
font-size: max(1rem, 16px);
|
|
430
568
|
}
|
|
431
569
|
}
|
|
@@ -491,4 +629,22 @@ function removeAttachment(id: string) {
|
|
|
491
629
|
color: var(--smrt-color-on-error-container, #410002);
|
|
492
630
|
font: var(--smrt-typography-body-small-font, 0.8125rem/1.4 sans-serif);
|
|
493
631
|
}
|
|
632
|
+
|
|
633
|
+
/* A narrow dock (a sidebar a couple of hundred pixels wide): the message
|
|
634
|
+
field takes the whole row and the attach, dictate and send controls sit
|
|
635
|
+
under it, so the placeholder never wraps a syllable per line. */
|
|
636
|
+
@container (max-width: 22rem) {
|
|
637
|
+
.assistant-composer-row {
|
|
638
|
+
flex-wrap: wrap;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
.assistant-composer-field {
|
|
642
|
+
flex: 1 1 100%;
|
|
643
|
+
order: -1;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
:global(.assistant-composer-send) {
|
|
647
|
+
margin-inline-start: auto;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
494
650
|
</style>
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* flow, which is sized for a dedicated upload panel, not a one-line composer
|
|
9
9
|
* (#2904 review fix).
|
|
10
10
|
*/
|
|
11
|
-
import { type DictationSourceProvider, type DictationTranscribe } from '@happyvertical/smrt-ui/forms';
|
|
11
|
+
import { type DictationSourceProvider, type DictationTranscribe, type HandsFreeCaptureFactory, type HandsFreeVadOptions } from '@happyvertical/smrt-ui/forms';
|
|
12
12
|
import type { AssistantAttachmentRef } from './assistant-transport.js';
|
|
13
13
|
export interface Props {
|
|
14
14
|
/** Called with the trimmed message text and any staged attachments when
|
|
@@ -45,8 +45,47 @@ export interface Props {
|
|
|
45
45
|
* own, without `dictation`.
|
|
46
46
|
*/
|
|
47
47
|
transcribe?: DictationTranscribe | null;
|
|
48
|
+
/**
|
|
49
|
+
* `'hands-free'`: the microphone stays on and each sentence is written
|
|
50
|
+
* down when the speaker pauses, with no tap per phrase; tapping the
|
|
51
|
+
* microphone (or sending, or Escape) ends it. Needs `handsFreeCapture` and
|
|
52
|
+
* an on-device speech source (smrt-svelte's `whisper-local` / `moonshine`);
|
|
53
|
+
* otherwise it is ordinary press-to-talk. Default `'push'`.
|
|
54
|
+
*/
|
|
55
|
+
dictationMode?: 'push' | 'hands-free';
|
|
56
|
+
/**
|
|
57
|
+
* The hands-free microphone, `createHandsFreeCapture` from
|
|
58
|
+
* `@happyvertical/smrt-ui/forms/hands-free`.
|
|
59
|
+
*/
|
|
60
|
+
handsFreeCapture?: HandsFreeCaptureFactory | null;
|
|
61
|
+
/** Pause length and sensitivity for hands-free (see `HandsFreeVadOptions`). */
|
|
62
|
+
handsFreeVad?: HandsFreeVadOptions;
|
|
63
|
+
/**
|
|
64
|
+
* Hands-free only: send the message once the speaker has been quiet for
|
|
65
|
+
* `sendOnPauseMs` after the last sentence was written down (the same path
|
|
66
|
+
* as pressing Send). A new sentence, a key press, Escape or the microphone
|
|
67
|
+
* button cancels the pending send. Never sends an empty message. While a
|
|
68
|
+
* send is in flight, or the composer is disabled, the send waits and the
|
|
69
|
+
* text stays in the box until the composer can accept it again. Default
|
|
70
|
+
* `false`.
|
|
71
|
+
*/
|
|
72
|
+
sendOnPause?: boolean;
|
|
73
|
+
/** Quiet time before `sendOnPause` sends, in ms. Default 1200. */
|
|
74
|
+
sendOnPauseMs?: number;
|
|
75
|
+
/**
|
|
76
|
+
* The assistant's reply is being played aloud (text to speech). While
|
|
77
|
+
* `true`, hands-free listening is suspended (half-duplex: the microphone
|
|
78
|
+
* stays open but nothing is heard, so the assistant is not transcribed into
|
|
79
|
+
* the box) and resumes after a short guard when it turns `false`. The
|
|
80
|
+
* microphone shows "Paused while the assistant speaks". A pending send on
|
|
81
|
+
* pause is unaffected; tapping the microphone still ends hands-free.
|
|
82
|
+
* Default `false`.
|
|
83
|
+
*/
|
|
84
|
+
speaking?: boolean;
|
|
48
85
|
}
|
|
49
|
-
declare const AssistantComposer: import("svelte").Component<Props, {
|
|
86
|
+
declare const AssistantComposer: import("svelte").Component<Props, {
|
|
87
|
+
focus: () => void;
|
|
88
|
+
}, "value">;
|
|
50
89
|
type AssistantComposer = ReturnType<typeof AssistantComposer>;
|
|
51
90
|
export default AssistantComposer;
|
|
52
91
|
//# sourceMappingURL=AssistantComposer.svelte.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AssistantComposer.svelte.d.ts","sourceRoot":"","sources":["../../../../src/svelte/components/assistant/AssistantComposer.svelte.ts"],"names":[],"mappings":"AAGA;;;;;;;;;GASG;AACH,OAAO,EAGL,KAAK,uBAAuB,EAE5B,KAAK,mBAAmB,EAKzB,MAAM,8BAA8B,CAAC;AAKtC,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAGvE,MAAM,WAAW,KAAK;IACpB;;;;4DAIwD;IACxD,MAAM,EAAE,CACN,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,sBAAsB,EAAE,KAClC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B;sEACkE;IAClE,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,OAAO,CAAC,sBAAsB,EAAE,CAAC,CAAC;IAClE,yDAAyD;IACzD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,+CAA+C;IAC/C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;0CAEsC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,uBAAuB,GAAG,IAAI,CAAC;IAC3C;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,mBAAmB,GAAG,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"AssistantComposer.svelte.d.ts","sourceRoot":"","sources":["../../../../src/svelte/components/assistant/AssistantComposer.svelte.ts"],"names":[],"mappings":"AAGA;;;;;;;;;GASG;AACH,OAAO,EAGL,KAAK,uBAAuB,EAE5B,KAAK,mBAAmB,EACxB,KAAK,uBAAuB,EAC5B,KAAK,mBAAmB,EAKzB,MAAM,8BAA8B,CAAC;AAKtC,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAGvE,MAAM,WAAW,KAAK;IACpB;;;;4DAIwD;IACxD,MAAM,EAAE,CACN,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,sBAAsB,EAAE,KAClC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B;sEACkE;IAClE,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,OAAO,CAAC,sBAAsB,EAAE,CAAC,CAAC;IAClE,yDAAyD;IACzD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,+CAA+C;IAC/C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;0CAEsC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,uBAAuB,GAAG,IAAI,CAAC;IAC3C;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,mBAAmB,GAAG,IAAI,CAAC;IACxC;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,YAAY,CAAC;IACtC;;;OAGG;IACH,gBAAgB,CAAC,EAAE,uBAAuB,GAAG,IAAI,CAAC;IAClD,+EAA+E;IAC/E,YAAY,CAAC,EAAE,mBAAmB,CAAC;IACnC;;;;;;;;OAQG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,kEAAkE;IAClE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAmWD,QAAA,MAAM,iBAAiB;iBAxSJ,IAAI;WAwSwC,CAAC;AAChE,KAAK,iBAAiB,GAAG,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC9D,eAAe,iBAAiB,CAAC"}
|