iris-chatbot 5.2.0 → 5.3.1
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/package.json +1 -1
- package/template/package-lock.json +2 -2
- package/template/package.json +1 -1
- package/template/src/app/api/chat/route.ts +889 -92
- package/template/src/app/api/contacts/search/route.ts +71 -0
- package/template/src/app/api/tool-approval/route.ts +30 -2
- package/template/src/app/globals.css +223 -1
- package/template/src/components/ChatView.tsx +247 -27
- package/template/src/components/Composer.tsx +11 -8
- package/template/src/components/MessageCard.tsx +549 -29
- package/template/src/components/SettingsModal.tsx +7 -0
- package/template/src/lib/data.ts +5 -0
- package/template/src/lib/tooling/approvals.ts +24 -9
- package/template/src/lib/tooling/tools/communication.ts +178 -31
- package/template/src/lib/types.ts +12 -2
|
@@ -280,16 +280,79 @@ function resolveMusicTargetFromResult(result: unknown): string | null {
|
|
|
280
280
|
return artist ? `${title} by ${artist}` : title;
|
|
281
281
|
}
|
|
282
282
|
|
|
283
|
-
function toApprovalStatus(decision: "approve" | "deny" | "timeout") {
|
|
283
|
+
function toApprovalStatus(decision: "approve" | "deny" | "timeout" | "supersede") {
|
|
284
284
|
if (decision === "approve") {
|
|
285
285
|
return "approved" as const;
|
|
286
286
|
}
|
|
287
287
|
if (decision === "deny") {
|
|
288
288
|
return "denied" as const;
|
|
289
289
|
}
|
|
290
|
+
if (decision === "supersede") {
|
|
291
|
+
return "superseded" as const;
|
|
292
|
+
}
|
|
290
293
|
return "timeout" as const;
|
|
291
294
|
}
|
|
292
295
|
|
|
296
|
+
function isDraftSendApproval(
|
|
297
|
+
approval: ToolApproval,
|
|
298
|
+
): approval is ToolApproval & { toolName: "mail_send" | "messages_send" } {
|
|
299
|
+
return approval.toolName === "mail_send" || approval.toolName === "messages_send";
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
type PendingDraftContext = {
|
|
303
|
+
toolName: "mail_send" | "messages_send";
|
|
304
|
+
to: string[];
|
|
305
|
+
cc: string[];
|
|
306
|
+
subject: string;
|
|
307
|
+
body: string;
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
function parsePendingDraftContext(approval: ToolApproval): PendingDraftContext | null {
|
|
311
|
+
if (!isDraftSendApproval(approval)) {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
if (!approval.argsJson) {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
let payload: unknown;
|
|
319
|
+
try {
|
|
320
|
+
payload = JSON.parse(approval.argsJson);
|
|
321
|
+
} catch {
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
if (!payload || typeof payload !== "object") {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
const record = payload as Record<string, unknown>;
|
|
328
|
+
|
|
329
|
+
const to = Array.isArray(record.to) ? record.to.filter((value): value is string => typeof value === "string") : [];
|
|
330
|
+
const cc = Array.isArray(record.cc) ? record.cc.filter((value): value is string => typeof value === "string") : [];
|
|
331
|
+
const subject = typeof record.subject === "string" ? record.subject : "";
|
|
332
|
+
const body = typeof record.body === "string" ? record.body : "";
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
toolName: approval.toolName,
|
|
336
|
+
to,
|
|
337
|
+
cc,
|
|
338
|
+
subject,
|
|
339
|
+
body,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function isLikelyDraftEditFollowup(input: string): boolean {
|
|
344
|
+
const text = input.replace(/\s+/g, " ").trim();
|
|
345
|
+
if (!text) {
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
const editVerb = /\b(edit|rewrite|change|update|improve|polish|adjust|expand|shorten|lengthen|make)\b/i.test(text);
|
|
349
|
+
const draftNoun = /\b(email|mail|message|text|draft|subject|body)\b/i.test(text);
|
|
350
|
+
const toneOrLength =
|
|
351
|
+
/\b(longer|shorter|formal|casual|professional|friendly|polite|concise|detailed|brief)\b/i.test(text);
|
|
352
|
+
const pronounEdit = /\b(make|rewrite|edit|change|update)\s+(?:it|this)\b/i.test(text);
|
|
353
|
+
return (editVerb && draftNoun) || toneOrLength || pronounEdit;
|
|
354
|
+
}
|
|
355
|
+
|
|
293
356
|
type MicMode = "off" | "dictation";
|
|
294
357
|
type MicState = "idle" | "listening" | "processing";
|
|
295
358
|
|
|
@@ -341,7 +404,8 @@ export default function ChatView({
|
|
|
341
404
|
const lastFollowScrollAtRef = useRef(0);
|
|
342
405
|
const smoothFollowUntilRef = useRef(0);
|
|
343
406
|
const abortControllersRef = useRef<Map<string, AbortController>>(new Map());
|
|
344
|
-
const inflightThreadIdsRef = useRef<
|
|
407
|
+
const inflightThreadIdsRef = useRef<Map<string, number>>(new Map());
|
|
408
|
+
const streamingByThreadRef = useRef<Record<string, string>>({});
|
|
345
409
|
const micModeRef = useRef<MicMode>("off");
|
|
346
410
|
const micStateRef = useRef<MicState>("idle");
|
|
347
411
|
const currentInputRef = useRef("");
|
|
@@ -500,6 +564,18 @@ export default function ChatView({
|
|
|
500
564
|
}
|
|
501
565
|
return streamingByThread[thread.id] ?? null;
|
|
502
566
|
}, [streamingByThread, thread?.id]);
|
|
567
|
+
const pendingDraftApprovalMessageIds = useMemo(() => {
|
|
568
|
+
const set = new Set<string>();
|
|
569
|
+
for (const approval of toolApprovals) {
|
|
570
|
+
if (approval.status === "requested" && isDraftSendApproval(approval)) {
|
|
571
|
+
set.add(approval.assistantMessageId);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return set;
|
|
575
|
+
}, [toolApprovals]);
|
|
576
|
+
const isAwaitingDraftApproval = activeStreamingMessageId
|
|
577
|
+
? pendingDraftApprovalMessageIds.has(activeStreamingMessageId)
|
|
578
|
+
: false;
|
|
503
579
|
const micSupported = getSpeechRecognitionCtor() !== null;
|
|
504
580
|
|
|
505
581
|
const toolEventsByMessage = useMemo(() => {
|
|
@@ -563,6 +639,10 @@ export default function ChatView({
|
|
|
563
639
|
setInput(parts.join(" ").trim());
|
|
564
640
|
}, []);
|
|
565
641
|
|
|
642
|
+
useEffect(() => {
|
|
643
|
+
streamingByThreadRef.current = streamingByThread;
|
|
644
|
+
}, [streamingByThread]);
|
|
645
|
+
|
|
566
646
|
useEffect(() => {
|
|
567
647
|
const controllers = abortControllersRef.current;
|
|
568
648
|
const inflightThreadIds = inflightThreadIdsRef.current;
|
|
@@ -642,34 +722,81 @@ export default function ChatView({
|
|
|
642
722
|
});
|
|
643
723
|
}, [thread?.id, activeThreadId]);
|
|
644
724
|
|
|
645
|
-
const
|
|
646
|
-
async (
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
725
|
+
const resolveApprovalDecision = useCallback(
|
|
726
|
+
async (params: {
|
|
727
|
+
approvalId: string;
|
|
728
|
+
decision: "approve" | "deny" | "supersede";
|
|
729
|
+
argsOverride?: Record<string, unknown>;
|
|
730
|
+
source?: "user" | "system";
|
|
731
|
+
reasonCode?: "user_cancel" | "internal_replace" | "timeout" | "other";
|
|
732
|
+
busy?: boolean;
|
|
733
|
+
suppressErrors?: boolean;
|
|
734
|
+
}) => {
|
|
735
|
+
const {
|
|
736
|
+
approvalId,
|
|
737
|
+
decision,
|
|
738
|
+
argsOverride,
|
|
739
|
+
source = "user",
|
|
740
|
+
reasonCode,
|
|
741
|
+
busy = true,
|
|
742
|
+
suppressErrors = false,
|
|
743
|
+
} = params;
|
|
744
|
+
|
|
745
|
+
if (busy) {
|
|
746
|
+
setApprovalBusyIds((current) => ({
|
|
747
|
+
...current,
|
|
748
|
+
[approvalId]: true,
|
|
749
|
+
}));
|
|
750
|
+
}
|
|
651
751
|
|
|
652
752
|
try {
|
|
753
|
+
const payload: {
|
|
754
|
+
approvalId: string;
|
|
755
|
+
decision: "approve" | "deny" | "supersede";
|
|
756
|
+
source: "user" | "system";
|
|
757
|
+
reasonCode?: "user_cancel" | "internal_replace" | "timeout" | "other";
|
|
758
|
+
args?: Record<string, unknown>;
|
|
759
|
+
} = { approvalId, decision, source };
|
|
760
|
+
if (reasonCode) {
|
|
761
|
+
payload.reasonCode = reasonCode;
|
|
762
|
+
}
|
|
763
|
+
if (decision === "approve" && argsOverride) {
|
|
764
|
+
payload.args = argsOverride;
|
|
765
|
+
}
|
|
766
|
+
|
|
653
767
|
const response = await fetch("/api/tool-approval", {
|
|
654
768
|
method: "POST",
|
|
655
769
|
headers: { "Content-Type": "application/json" },
|
|
656
|
-
body: JSON.stringify(
|
|
770
|
+
body: JSON.stringify(payload),
|
|
657
771
|
});
|
|
658
772
|
|
|
659
773
|
if (!response.ok) {
|
|
660
|
-
const
|
|
661
|
-
|
|
774
|
+
const responsePayload = await response.json().catch(() => ({}));
|
|
775
|
+
const message = responsePayload.error || "Failed to resolve approval.";
|
|
776
|
+
if (!suppressErrors) {
|
|
777
|
+
throw new Error(message);
|
|
778
|
+
}
|
|
779
|
+
return;
|
|
662
780
|
}
|
|
663
781
|
|
|
664
782
|
await db.toolApprovals.update(approvalId, {
|
|
665
783
|
status: toApprovalStatus(decision),
|
|
666
784
|
resolvedAt: Date.now(),
|
|
785
|
+
...(decision === "approve" && argsOverride
|
|
786
|
+
? { argsJson: JSON.stringify(argsOverride) }
|
|
787
|
+
: {}),
|
|
667
788
|
});
|
|
668
789
|
} catch (err) {
|
|
790
|
+
if (suppressErrors) {
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
669
793
|
const message =
|
|
670
794
|
err instanceof Error ? err.message : "Unable to resolve approval.";
|
|
671
795
|
setError(message);
|
|
672
796
|
} finally {
|
|
797
|
+
if (!busy) {
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
673
800
|
setApprovalBusyIds((current) => {
|
|
674
801
|
if (!current[approvalId]) {
|
|
675
802
|
return current;
|
|
@@ -680,7 +807,38 @@ export default function ChatView({
|
|
|
680
807
|
});
|
|
681
808
|
}
|
|
682
809
|
},
|
|
683
|
-
[]
|
|
810
|
+
[],
|
|
811
|
+
);
|
|
812
|
+
|
|
813
|
+
const handleApprovalDecision = useCallback(
|
|
814
|
+
async (
|
|
815
|
+
approvalId: string,
|
|
816
|
+
decision: "approve" | "deny",
|
|
817
|
+
argsOverride?: Record<string, unknown>,
|
|
818
|
+
) => {
|
|
819
|
+
await resolveApprovalDecision({
|
|
820
|
+
approvalId,
|
|
821
|
+
decision,
|
|
822
|
+
argsOverride,
|
|
823
|
+
source: "user",
|
|
824
|
+
reasonCode: decision === "deny" ? "user_cancel" : undefined,
|
|
825
|
+
});
|
|
826
|
+
},
|
|
827
|
+
[resolveApprovalDecision],
|
|
828
|
+
);
|
|
829
|
+
|
|
830
|
+
const resolveApprovalSystemSupersede = useCallback(
|
|
831
|
+
async (approvalId: string) => {
|
|
832
|
+
await resolveApprovalDecision({
|
|
833
|
+
approvalId,
|
|
834
|
+
decision: "supersede",
|
|
835
|
+
source: "system",
|
|
836
|
+
reasonCode: "internal_replace",
|
|
837
|
+
busy: false,
|
|
838
|
+
suppressErrors: true,
|
|
839
|
+
});
|
|
840
|
+
},
|
|
841
|
+
[resolveApprovalDecision],
|
|
684
842
|
);
|
|
685
843
|
|
|
686
844
|
const startMicRecognitionSession = useCallback(
|
|
@@ -937,6 +1095,7 @@ export default function ChatView({
|
|
|
937
1095
|
const trimmed = (overrideInput ?? input).trim();
|
|
938
1096
|
const hasQuoted = Boolean(quotedContext);
|
|
939
1097
|
if (!trimmed && !hasQuoted) return;
|
|
1098
|
+
let draftEditContextForRequest: PendingDraftContext | null = null;
|
|
940
1099
|
const sendTriggeredByMic =
|
|
941
1100
|
typeof overrideInput === "string" || micModeRef.current !== "off";
|
|
942
1101
|
const stopMicAfterValidationFailure = () => {
|
|
@@ -987,15 +1146,51 @@ export default function ChatView({
|
|
|
987
1146
|
setMicState("processing");
|
|
988
1147
|
}
|
|
989
1148
|
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
inflightThreadIdsRef.current.
|
|
993
|
-
) {
|
|
1149
|
+
const isThreadLocked = () =>
|
|
1150
|
+
Boolean(streamingByThreadRef.current[resolvedThread.id]) ||
|
|
1151
|
+
(inflightThreadIdsRef.current.get(resolvedThread.id) ?? 0) > 0;
|
|
1152
|
+
if (isThreadLocked()) {
|
|
1153
|
+
const pendingDraftApprovals = toolApprovals.filter(
|
|
1154
|
+
(approval) =>
|
|
1155
|
+
approval.threadId === resolvedThread.id &&
|
|
1156
|
+
approval.status === "requested" &&
|
|
1157
|
+
isDraftSendApproval(approval),
|
|
1158
|
+
);
|
|
1159
|
+
|
|
1160
|
+
if (pendingDraftApprovals.length > 0) {
|
|
1161
|
+
if (isLikelyDraftEditFollowup(trimmed)) {
|
|
1162
|
+
const latestPendingApproval = [...pendingDraftApprovals].sort(
|
|
1163
|
+
(left, right) => right.requestedAt - left.requestedAt,
|
|
1164
|
+
)[0];
|
|
1165
|
+
const parsedDraft = parsePendingDraftContext(latestPendingApproval);
|
|
1166
|
+
if (parsedDraft) {
|
|
1167
|
+
draftEditContextForRequest = parsedDraft;
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
for (const approval of pendingDraftApprovals) {
|
|
1172
|
+
await resolveApprovalSystemSupersede(approval.id);
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
const controller = abortControllersRef.current.get(resolvedThread.id);
|
|
1176
|
+
controller?.abort();
|
|
1177
|
+
|
|
1178
|
+
const unlockDeadline = Date.now() + 1500;
|
|
1179
|
+
while (isThreadLocked() && Date.now() < unlockDeadline) {
|
|
1180
|
+
await new Promise((resolve) => window.setTimeout(resolve, 25));
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
if (isThreadLocked()) {
|
|
994
1186
|
setError("This thread is already responding.");
|
|
995
1187
|
return;
|
|
996
1188
|
}
|
|
997
1189
|
|
|
998
|
-
inflightThreadIdsRef.current.
|
|
1190
|
+
inflightThreadIdsRef.current.set(
|
|
1191
|
+
resolvedThread.id,
|
|
1192
|
+
(inflightThreadIdsRef.current.get(resolvedThread.id) ?? 0) + 1,
|
|
1193
|
+
);
|
|
999
1194
|
setError(null);
|
|
1000
1195
|
setInput("");
|
|
1001
1196
|
const capturedQuotedContext = quotedContext;
|
|
@@ -1136,7 +1331,6 @@ export default function ChatView({
|
|
|
1136
1331
|
siblingThreadsContext,
|
|
1137
1332
|
});
|
|
1138
1333
|
}
|
|
1139
|
-
|
|
1140
1334
|
payloadMessages.push(
|
|
1141
1335
|
...history
|
|
1142
1336
|
.filter((message) => message.role !== "system")
|
|
@@ -1209,6 +1403,18 @@ export default function ChatView({
|
|
|
1209
1403
|
threadId: resolvedThread.id,
|
|
1210
1404
|
conversationId: resolvedThread.conversationId,
|
|
1211
1405
|
assistantMessageId: assistantMessage.id,
|
|
1406
|
+
...(draftEditContextForRequest
|
|
1407
|
+
? {
|
|
1408
|
+
forceToolRouting: true,
|
|
1409
|
+
draftEditContext: {
|
|
1410
|
+
toolName: draftEditContextForRequest.toolName,
|
|
1411
|
+
to: draftEditContextForRequest.to,
|
|
1412
|
+
cc: draftEditContextForRequest.cc,
|
|
1413
|
+
subject: draftEditContextForRequest.subject,
|
|
1414
|
+
body: draftEditContextForRequest.body,
|
|
1415
|
+
},
|
|
1416
|
+
}
|
|
1417
|
+
: {}),
|
|
1212
1418
|
},
|
|
1213
1419
|
}),
|
|
1214
1420
|
signal: controller.signal,
|
|
@@ -1512,10 +1718,17 @@ export default function ChatView({
|
|
|
1512
1718
|
});
|
|
1513
1719
|
setError(message);
|
|
1514
1720
|
} finally {
|
|
1515
|
-
inflightThreadIdsRef.current.
|
|
1516
|
-
|
|
1721
|
+
const currentInflight = inflightThreadIdsRef.current.get(resolvedThread.id) ?? 0;
|
|
1722
|
+
if (currentInflight <= 1) {
|
|
1723
|
+
inflightThreadIdsRef.current.delete(resolvedThread.id);
|
|
1724
|
+
} else {
|
|
1725
|
+
inflightThreadIdsRef.current.set(resolvedThread.id, currentInflight - 1);
|
|
1726
|
+
}
|
|
1727
|
+
if (abortControllersRef.current.get(resolvedThread.id) === controller) {
|
|
1728
|
+
abortControllersRef.current.delete(resolvedThread.id);
|
|
1729
|
+
}
|
|
1517
1730
|
setStreamingByThread((current) => {
|
|
1518
|
-
if (
|
|
1731
|
+
if (current[resolvedThread.id] !== assistantMessage.id) {
|
|
1519
1732
|
return current;
|
|
1520
1733
|
}
|
|
1521
1734
|
const next = { ...current };
|
|
@@ -1553,7 +1766,8 @@ export default function ChatView({
|
|
|
1553
1766
|
threads,
|
|
1554
1767
|
activeThreadId,
|
|
1555
1768
|
onOpenSettings,
|
|
1556
|
-
|
|
1769
|
+
toolApprovals,
|
|
1770
|
+
resolveApprovalSystemSupersede,
|
|
1557
1771
|
scrollToBottom,
|
|
1558
1772
|
setFocusedMessageId,
|
|
1559
1773
|
maybeAutoScrollToBottom,
|
|
@@ -1641,7 +1855,10 @@ export default function ChatView({
|
|
|
1641
1855
|
activeThreadId={activeThreadId}
|
|
1642
1856
|
onSelectThread={onSelectThread}
|
|
1643
1857
|
onDeleteThread={handleDeleteBranch}
|
|
1644
|
-
isStreaming={
|
|
1858
|
+
isStreaming={
|
|
1859
|
+
activeStreamingMessageId === message.id &&
|
|
1860
|
+
!pendingDraftApprovalMessageIds.has(message.id)
|
|
1861
|
+
}
|
|
1645
1862
|
toolEvents={messageToolEvents}
|
|
1646
1863
|
approvals={messageApprovals}
|
|
1647
1864
|
onResolveApproval={handleApprovalDecision}
|
|
@@ -1659,6 +1876,7 @@ export default function ChatView({
|
|
|
1659
1876
|
onSelectThread,
|
|
1660
1877
|
handleDeleteBranch,
|
|
1661
1878
|
activeStreamingMessageId,
|
|
1879
|
+
pendingDraftApprovalMessageIds,
|
|
1662
1880
|
toolEventsByMessage,
|
|
1663
1881
|
toolApprovalsByMessage,
|
|
1664
1882
|
handleApprovalDecision,
|
|
@@ -1688,10 +1906,11 @@ export default function ChatView({
|
|
|
1688
1906
|
void handleSend();
|
|
1689
1907
|
}}
|
|
1690
1908
|
onStop={handleStop}
|
|
1691
|
-
isStreaming={Boolean(activeStreamingMessageId)}
|
|
1909
|
+
isStreaming={Boolean(activeStreamingMessageId) && !isAwaitingDraftApproval}
|
|
1910
|
+
showStopWhenStreaming={!isAwaitingDraftApproval}
|
|
1692
1911
|
onMicToggle={handleMicToggle}
|
|
1693
1912
|
micState={micState}
|
|
1694
|
-
micDisabled={Boolean(activeStreamingMessageId)}
|
|
1913
|
+
micDisabled={Boolean(activeStreamingMessageId) && !isAwaitingDraftApproval}
|
|
1695
1914
|
quotedContext={quotedContext}
|
|
1696
1915
|
onClearQuotedContext={handleClearQuotedContext}
|
|
1697
1916
|
/>
|
|
@@ -1753,10 +1972,11 @@ export default function ChatView({
|
|
|
1753
1972
|
void handleSend();
|
|
1754
1973
|
}}
|
|
1755
1974
|
onStop={handleStop}
|
|
1756
|
-
isStreaming={Boolean(activeStreamingMessageId)}
|
|
1975
|
+
isStreaming={Boolean(activeStreamingMessageId) && !isAwaitingDraftApproval}
|
|
1976
|
+
showStopWhenStreaming={!isAwaitingDraftApproval}
|
|
1757
1977
|
onMicToggle={handleMicToggle}
|
|
1758
1978
|
micState={micState}
|
|
1759
|
-
micDisabled={Boolean(activeStreamingMessageId)}
|
|
1979
|
+
micDisabled={Boolean(activeStreamingMessageId) && !isAwaitingDraftApproval}
|
|
1760
1980
|
quotedContext={quotedContext}
|
|
1761
1981
|
onClearQuotedContext={handleClearQuotedContext}
|
|
1762
1982
|
/>
|
|
@@ -18,6 +18,7 @@ export default function Composer({
|
|
|
18
18
|
onSend,
|
|
19
19
|
onStop,
|
|
20
20
|
isStreaming,
|
|
21
|
+
showStopWhenStreaming = true,
|
|
21
22
|
onMicToggle,
|
|
22
23
|
micState = "idle",
|
|
23
24
|
micDisabled = false,
|
|
@@ -29,6 +30,7 @@ export default function Composer({
|
|
|
29
30
|
onSend: () => void;
|
|
30
31
|
onStop: () => void;
|
|
31
32
|
isStreaming: boolean;
|
|
33
|
+
showStopWhenStreaming?: boolean;
|
|
32
34
|
onMicToggle?: () => void;
|
|
33
35
|
micState?: "idle" | "listening" | "processing";
|
|
34
36
|
micDisabled?: boolean;
|
|
@@ -177,13 +179,15 @@ export default function Composer({
|
|
|
177
179
|
</button>
|
|
178
180
|
) : null}
|
|
179
181
|
{isStreaming ? (
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
182
|
+
showStopWhenStreaming ? (
|
|
183
|
+
<button
|
|
184
|
+
onClick={onStop}
|
|
185
|
+
className="flex items-center gap-2 rounded-full border border-[var(--border)] bg-[var(--panel-2)] px-3 py-2 text-xs text-[var(--text-secondary)]"
|
|
186
|
+
>
|
|
187
|
+
<Square className="h-3 w-3" />
|
|
188
|
+
Stop
|
|
189
|
+
</button>
|
|
190
|
+
) : null
|
|
187
191
|
) : (
|
|
188
192
|
<button
|
|
189
193
|
onClick={onSend}
|
|
@@ -196,4 +200,3 @@ export default function Composer({
|
|
|
196
200
|
</div>
|
|
197
201
|
);
|
|
198
202
|
}
|
|
199
|
-
|