@bytexbyte/nxtlinq-ai-agent-ui-react-development 0.1.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.
Files changed (142) hide show
  1. package/dist/NxtlinqAgentChat.d.ts +26 -0
  2. package/dist/NxtlinqAgentChat.d.ts.map +1 -0
  3. package/dist/NxtlinqAgentChat.js +28 -0
  4. package/dist/components/AgentAssistantShell.d.ts +5 -0
  5. package/dist/components/AgentAssistantShell.d.ts.map +1 -0
  6. package/dist/components/AgentAssistantShell.js +52 -0
  7. package/dist/components/AgentComposer.d.ts +3 -0
  8. package/dist/components/AgentComposer.d.ts.map +1 -0
  9. package/dist/components/AgentComposer.js +60 -0
  10. package/dist/components/AgentMessageList.d.ts +3 -0
  11. package/dist/components/AgentMessageList.d.ts.map +1 -0
  12. package/dist/components/AgentMessageList.js +37 -0
  13. package/dist/components/AgentRemoteAudio.d.ts +4 -0
  14. package/dist/components/AgentRemoteAudio.d.ts.map +1 -0
  15. package/dist/components/AgentRemoteAudio.js +34 -0
  16. package/dist/components/AgentVoiceBar.d.ts +3 -0
  17. package/dist/components/AgentVoiceBar.d.ts.map +1 -0
  18. package/dist/components/AgentVoiceBar.js +91 -0
  19. package/dist/components/PresetMessageChips.d.ts +3 -0
  20. package/dist/components/PresetMessageChips.d.ts.map +1 -0
  21. package/dist/components/PresetMessageChips.js +23 -0
  22. package/dist/context/AgentAssistantContext.d.ts +32 -0
  23. package/dist/context/AgentAssistantContext.d.ts.map +1 -0
  24. package/dist/context/AgentAssistantContext.js +159 -0
  25. package/dist/index.d.ts +16 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +12 -0
  28. package/dist/legacy/assets/images/adiSideItalicDataUri.d.ts +2 -0
  29. package/dist/legacy/assets/images/adiSideItalicDataUri.d.ts.map +1 -0
  30. package/dist/legacy/assets/images/adiSideItalicDataUri.js +1 -0
  31. package/dist/legacy/chatbot/ChatBot.d.ts +5 -0
  32. package/dist/legacy/chatbot/ChatBot.d.ts.map +1 -0
  33. package/dist/legacy/chatbot/ChatBot.js +35 -0
  34. package/dist/legacy/chatbot/context/ChatBotContext.d.ts +5 -0
  35. package/dist/legacy/chatbot/context/ChatBotContext.d.ts.map +1 -0
  36. package/dist/legacy/chatbot/context/ChatBotContext.js +2908 -0
  37. package/dist/legacy/chatbot/types/ChatBotTypes.d.ts +166 -0
  38. package/dist/legacy/chatbot/types/ChatBotTypes.d.ts.map +1 -0
  39. package/dist/legacy/chatbot/types/ChatBotTypes.js +1 -0
  40. package/dist/legacy/chatbot/ui/BerifyMeModal.d.ts +17 -0
  41. package/dist/legacy/chatbot/ui/BerifyMeModal.d.ts.map +1 -0
  42. package/dist/legacy/chatbot/ui/BerifyMeModal.js +110 -0
  43. package/dist/legacy/chatbot/ui/ChatBotUI.d.ts +3 -0
  44. package/dist/legacy/chatbot/ui/ChatBotUI.d.ts.map +1 -0
  45. package/dist/legacy/chatbot/ui/ChatBotUI.js +625 -0
  46. package/dist/legacy/chatbot/ui/MessageInput.d.ts +3 -0
  47. package/dist/legacy/chatbot/ui/MessageInput.d.ts.map +1 -0
  48. package/dist/legacy/chatbot/ui/MessageInput.js +321 -0
  49. package/dist/legacy/chatbot/ui/MessageList.d.ts +4 -0
  50. package/dist/legacy/chatbot/ui/MessageList.d.ts.map +1 -0
  51. package/dist/legacy/chatbot/ui/MessageList.js +455 -0
  52. package/dist/legacy/chatbot/ui/ModelSelector.d.ts +4 -0
  53. package/dist/legacy/chatbot/ui/ModelSelector.d.ts.map +1 -0
  54. package/dist/legacy/chatbot/ui/ModelSelector.js +122 -0
  55. package/dist/legacy/chatbot/ui/NotificationModal.d.ts +15 -0
  56. package/dist/legacy/chatbot/ui/NotificationModal.d.ts.map +1 -0
  57. package/dist/legacy/chatbot/ui/NotificationModal.js +53 -0
  58. package/dist/legacy/chatbot/ui/PermissionForm.d.ts +8 -0
  59. package/dist/legacy/chatbot/ui/PermissionForm.d.ts.map +1 -0
  60. package/dist/legacy/chatbot/ui/PermissionForm.js +465 -0
  61. package/dist/legacy/chatbot/ui/PresetMessages.d.ts +4 -0
  62. package/dist/legacy/chatbot/ui/PresetMessages.d.ts.map +1 -0
  63. package/dist/legacy/chatbot/ui/PresetMessages.js +33 -0
  64. package/dist/legacy/chatbot/ui/VoiceModePanel.d.ts +3 -0
  65. package/dist/legacy/chatbot/ui/VoiceModePanel.d.ts.map +1 -0
  66. package/dist/legacy/chatbot/ui/VoiceModePanel.js +95 -0
  67. package/dist/legacy/chatbot/ui/styles/isolatedStyles.d.ts +73 -0
  68. package/dist/legacy/chatbot/ui/styles/isolatedStyles.d.ts.map +1 -0
  69. package/dist/legacy/chatbot/ui/styles/isolatedStyles.js +985 -0
  70. package/dist/legacy/index.d.ts +14 -0
  71. package/dist/legacy/index.d.ts.map +1 -0
  72. package/dist/legacy/index.js +12 -0
  73. package/dist/theme/defaultTheme.d.ts +3 -0
  74. package/dist/theme/defaultTheme.d.ts.map +1 -0
  75. package/dist/theme/defaultTheme.js +20 -0
  76. package/dist/types.d.ts +62 -0
  77. package/dist/types.d.ts.map +1 -0
  78. package/dist/types.js +1 -0
  79. package/dist/voice/useVoiceConnectOrchestration.d.ts +21 -0
  80. package/dist/voice/useVoiceConnectOrchestration.d.ts.map +1 -0
  81. package/dist/voice/useVoiceConnectOrchestration.js +86 -0
  82. package/dist/voice/useVoiceMicState.d.ts +15 -0
  83. package/dist/voice/useVoiceMicState.d.ts.map +1 -0
  84. package/dist/voice/useVoiceMicState.js +94 -0
  85. package/dist/voice/useVoiceSilenceCommit.d.ts +10 -0
  86. package/dist/voice/useVoiceSilenceCommit.d.ts.map +1 -0
  87. package/dist/voice/useVoiceSilenceCommit.js +67 -0
  88. package/dist/voice/useVoiceTranscriptMessages.d.ts +16 -0
  89. package/dist/voice/useVoiceTranscriptMessages.d.ts.map +1 -0
  90. package/dist/voice/useVoiceTranscriptMessages.js +129 -0
  91. package/dist/voice/useWsRealtimeAudio.d.ts +18 -0
  92. package/dist/voice/useWsRealtimeAudio.d.ts.map +1 -0
  93. package/dist/voice/useWsRealtimeAudio.js +102 -0
  94. package/dist/voice/voiceMicConstants.d.ts +4 -0
  95. package/dist/voice/voiceMicConstants.d.ts.map +1 -0
  96. package/dist/voice/voiceMicConstants.js +10 -0
  97. package/dist/voice/ws/BrowserWsPcmPlayer.d.ts +23 -0
  98. package/dist/voice/ws/BrowserWsPcmPlayer.d.ts.map +1 -0
  99. package/dist/voice/ws/BrowserWsPcmPlayer.js +137 -0
  100. package/dist/voice/ws/BrowserWsPcmRecorder.d.ts +17 -0
  101. package/dist/voice/ws/BrowserWsPcmRecorder.d.ts.map +1 -0
  102. package/dist/voice/ws/BrowserWsPcmRecorder.js +71 -0
  103. package/dist/voice/ws/float32ToPcm16.d.ts +2 -0
  104. package/dist/voice/ws/float32ToPcm16.d.ts.map +1 -0
  105. package/dist/voice/ws/float32ToPcm16.js +8 -0
  106. package/dist/voice/ws/voiceSilenceConstants.d.ts +5 -0
  107. package/dist/voice/ws/voiceSilenceConstants.d.ts.map +1 -0
  108. package/dist/voice/ws/voiceSilenceConstants.js +4 -0
  109. package/dist/voice/ws/wsRealtimeConstants.d.ts +2 -0
  110. package/dist/voice/ws/wsRealtimeConstants.d.ts.map +1 -0
  111. package/dist/voice/ws/wsRealtimeConstants.js +1 -0
  112. package/package.json +60 -0
  113. package/src/NxtlinqAgentChat.tsx +79 -0
  114. package/src/components/AgentAssistantShell.tsx +104 -0
  115. package/src/components/AgentComposer.tsx +134 -0
  116. package/src/components/AgentMessageList.tsx +78 -0
  117. package/src/components/AgentRemoteAudio.tsx +34 -0
  118. package/src/components/AgentVoiceBar.tsx +173 -0
  119. package/src/components/PresetMessageChips.tsx +41 -0
  120. package/src/context/AgentAssistantContext.tsx +276 -0
  121. package/src/index.ts +78 -0
  122. package/src/legacy/assets/images/adiSideItalicDataUri.ts +1 -0
  123. package/src/legacy/chatbot/ChatBot.tsx +61 -0
  124. package/src/legacy/chatbot/context/ChatBotContext.tsx +3227 -0
  125. package/src/legacy/chatbot/types/ChatBotTypes.ts +195 -0
  126. package/src/legacy/chatbot/ui/BerifyMeModal.tsx +145 -0
  127. package/src/legacy/chatbot/ui/ChatBotUI.tsx +949 -0
  128. package/src/legacy/chatbot/ui/MessageInput.tsx +517 -0
  129. package/src/legacy/chatbot/ui/MessageList.tsx +764 -0
  130. package/src/legacy/chatbot/ui/ModelSelector.tsx +190 -0
  131. package/src/legacy/chatbot/ui/NotificationModal.tsx +110 -0
  132. package/src/legacy/chatbot/ui/PermissionForm.tsx +632 -0
  133. package/src/legacy/chatbot/ui/PresetMessages.tsx +50 -0
  134. package/src/legacy/chatbot/ui/VoiceModePanel.tsx +168 -0
  135. package/src/legacy/chatbot/ui/styles/isolatedStyles.ts +1058 -0
  136. package/src/legacy/index.ts +26 -0
  137. package/src/theme/defaultTheme.ts +22 -0
  138. package/src/types.ts +65 -0
  139. package/src/voice/useVoiceConnectOrchestration.ts +117 -0
  140. package/src/voice/useVoiceMicState.ts +117 -0
  141. package/src/voice/useVoiceTranscriptMessages.ts +173 -0
  142. package/src/voice/voiceMicConstants.ts +13 -0
@@ -0,0 +1,134 @@
1
+ import React from 'react';
2
+ import { useAgentAssistant } from '../context/AgentAssistantContext';
3
+
4
+ export function AgentComposer(): React.ReactElement | null {
5
+ const {
6
+ theme,
7
+ placeholder,
8
+ inputText,
9
+ setInputText,
10
+ sendText,
11
+ isLoading,
12
+ isVoiceAvailable,
13
+ interactionMode,
14
+ startVoice,
15
+ isVoiceConnecting,
16
+ enableFileUpload,
17
+ sendMessage,
18
+ } = useAgentAssistant();
19
+ const fileInputRef = React.useRef<HTMLInputElement | null>(null);
20
+
21
+ if (interactionMode === 'voice') {
22
+ return null;
23
+ }
24
+
25
+ const handleFiles = async (files: FileList | null) => {
26
+ if (!files?.length) return;
27
+ const { fileToAttachment } = await import('@bytexbyte/nxtlinq-ai-agent-web-development');
28
+ const attachments = await Promise.all(
29
+ Array.from(files).map((file) => fileToAttachment(file)),
30
+ );
31
+ await sendMessage(
32
+ attachments.length ? `Uploaded ${attachments.length} file(s)` : '',
33
+ { attachments },
34
+ );
35
+ if (fileInputRef.current) fileInputRef.current.value = '';
36
+ };
37
+
38
+ return (
39
+ <div
40
+ style={{
41
+ borderTop: `1px solid ${theme.colors.border}`,
42
+ backgroundColor: theme.colors.surface,
43
+ padding: theme.spacing.md,
44
+ }}
45
+ >
46
+ <div style={{ display: 'flex', gap: theme.spacing.sm, alignItems: 'flex-end' }}>
47
+ {enableFileUpload ? (
48
+ <>
49
+ <button
50
+ type="button"
51
+ onClick={() => fileInputRef.current?.click()}
52
+ disabled={isLoading}
53
+ style={{
54
+ width: 36,
55
+ height: 36,
56
+ borderRadius: theme.radius.button,
57
+ border: `1px solid ${theme.colors.border}`,
58
+ backgroundColor: theme.colors.surface,
59
+ cursor: 'pointer',
60
+ }}
61
+ aria-label="Upload file"
62
+ >
63
+ +
64
+ </button>
65
+ <input
66
+ ref={fileInputRef}
67
+ type="file"
68
+ multiple
69
+ accept="image/*,.pdf,.doc,.docx,.txt"
70
+ style={{ display: 'none' }}
71
+ onChange={(e) => void handleFiles(e.target.files)}
72
+ />
73
+ </>
74
+ ) : null}
75
+ <textarea
76
+ value={inputText}
77
+ onChange={(e) => setInputText(e.target.value)}
78
+ placeholder={placeholder}
79
+ rows={2}
80
+ disabled={isLoading}
81
+ maxLength={4000}
82
+ style={{
83
+ flex: 1,
84
+ resize: 'none',
85
+ border: `1px solid ${theme.colors.border}`,
86
+ borderRadius: theme.radius.panel,
87
+ padding: theme.spacing.sm,
88
+ fontSize: theme.typography.bodySize,
89
+ color: theme.colors.assistantText,
90
+ }}
91
+ />
92
+ <button
93
+ type="button"
94
+ onClick={() => void sendText()}
95
+ disabled={isLoading || !inputText.trim()}
96
+ style={{
97
+ backgroundColor: theme.colors.primary,
98
+ color: theme.colors.primaryText,
99
+ border: 'none',
100
+ borderRadius: theme.radius.button,
101
+ padding: `${theme.spacing.sm}px ${theme.spacing.md}px`,
102
+ fontWeight: 600,
103
+ opacity: isLoading || !inputText.trim() ? 0.5 : 1,
104
+ cursor: 'pointer',
105
+ }}
106
+ >
107
+ Send
108
+ </button>
109
+ </div>
110
+ {isVoiceAvailable ? (
111
+ <button
112
+ type="button"
113
+ onClick={() => {
114
+ startVoice().catch((err: unknown) => {
115
+ console.warn('[nxtlinq] startVoice failed:', err);
116
+ });
117
+ }}
118
+ disabled={isVoiceConnecting}
119
+ style={{
120
+ display: 'block',
121
+ margin: `${theme.spacing.sm}px auto 0`,
122
+ border: 'none',
123
+ background: 'none',
124
+ color: theme.colors.primary,
125
+ fontSize: theme.typography.captionSize,
126
+ cursor: 'pointer',
127
+ }}
128
+ >
129
+ {isVoiceConnecting ? 'Connecting voice…' : 'Switch to voice mode'}
130
+ </button>
131
+ ) : null}
132
+ </div>
133
+ );
134
+ }
@@ -0,0 +1,78 @@
1
+ import type { Message } from '@bytexbyte/nxtlinq-ai-agent-core-development';
2
+ import React, { useEffect, useRef } from 'react';
3
+ import { useAgentAssistant } from '../context/AgentAssistantContext';
4
+
5
+ function MessageBubble({ message }: { message: Message }): React.ReactElement {
6
+ const { theme } = useAgentAssistant();
7
+ const isUser = message.role === 'user';
8
+ const displayText =
9
+ message.partialContent && message.isStreaming
10
+ ? message.partialContent
11
+ : message.content;
12
+
13
+ return (
14
+ <div
15
+ style={{
16
+ display: 'flex',
17
+ justifyContent: isUser ? 'flex-end' : 'flex-start',
18
+ marginBottom: theme.spacing.sm,
19
+ }}
20
+ >
21
+ <div
22
+ style={{
23
+ maxWidth: '85%',
24
+ padding: `${theme.spacing.sm}px ${theme.spacing.md}px`,
25
+ borderRadius: theme.radius.bubble,
26
+ backgroundColor: isUser ? theme.colors.userBubble : theme.colors.assistantBubble,
27
+ color: isUser ? theme.colors.userText : theme.colors.assistantText,
28
+ fontSize: theme.typography.bodySize,
29
+ whiteSpace: 'pre-wrap',
30
+ }}
31
+ >
32
+ {displayText || ' '}
33
+ {message.error ? (
34
+ <div style={{ color: theme.colors.error, fontSize: theme.typography.captionSize }}>
35
+ {message.error}
36
+ </div>
37
+ ) : null}
38
+ </div>
39
+ </div>
40
+ );
41
+ }
42
+
43
+ export function AgentMessageList(): React.ReactElement {
44
+ const { messages, isLoading, theme } = useAgentAssistant();
45
+ const endRef = useRef<HTMLDivElement | null>(null);
46
+
47
+ useEffect(() => {
48
+ endRef.current?.scrollIntoView({ behavior: 'smooth' });
49
+ }, [messages.length, messages[messages.length - 1]?.content, messages[messages.length - 1]?.partialContent]);
50
+
51
+ return (
52
+ <div
53
+ style={{
54
+ flex: 1,
55
+ minHeight: 0,
56
+ overflowY: 'auto',
57
+ padding: theme.spacing.md,
58
+ backgroundColor: theme.colors.background,
59
+ }}
60
+ >
61
+ {messages.length === 0 ? (
62
+ <p style={{ textAlign: 'center', color: theme.colors.mutedText, marginTop: 40 }}>
63
+ Send a message to start the conversation.
64
+ </p>
65
+ ) : (
66
+ messages.map((message: Message) => (
67
+ <MessageBubble key={message.id} message={message} />
68
+ ))
69
+ )}
70
+ {isLoading ? (
71
+ <p style={{ color: theme.colors.mutedText, fontSize: theme.typography.captionSize }}>
72
+ Thinking…
73
+ </p>
74
+ ) : null}
75
+ <div ref={endRef} />
76
+ </div>
77
+ );
78
+ }
@@ -0,0 +1,34 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { useAgentAssistant } from '../context/AgentAssistantContext';
3
+
4
+ /** Plays assistant WebRTC remote audio with retry until stream is ready. */
5
+ export function AgentRemoteAudio(): React.ReactElement | null {
6
+ const { getRemoteAudioStream, isVoiceActive, isVoiceChannelReady } = useAgentAssistant();
7
+ const audioRef = useRef<HTMLAudioElement | null>(null);
8
+
9
+ useEffect(() => {
10
+ const audio = audioRef.current;
11
+ if (!audio || !isVoiceActive) return;
12
+
13
+ let cancelled = false;
14
+ const attach = () => {
15
+ if (cancelled) return;
16
+ const stream = getRemoteAudioStream();
17
+ if (!stream) return;
18
+ audio.srcObject = stream;
19
+ void audio.play().catch(() => undefined);
20
+ };
21
+
22
+ attach();
23
+ const timer = window.setInterval(attach, 120);
24
+ return () => {
25
+ cancelled = true;
26
+ window.clearInterval(timer);
27
+ audio.pause();
28
+ audio.srcObject = null;
29
+ };
30
+ }, [getRemoteAudioStream, isVoiceActive, isVoiceChannelReady]);
31
+
32
+ if (!isVoiceActive) return null;
33
+ return <audio ref={audioRef} autoPlay playsInline style={{ display: 'none' }} />;
34
+ }
@@ -0,0 +1,173 @@
1
+ import React, { useCallback } from 'react';
2
+ import {
3
+ useAgentAssistant,
4
+ voiceStatusLabel,
5
+ } from '../context/AgentAssistantContext';
6
+
7
+ function statusDotColor(
8
+ status: ReturnType<typeof useAgentAssistant>['voiceStatus'],
9
+ theme: ReturnType<typeof useAgentAssistant>['theme'],
10
+ ): string {
11
+ switch (status) {
12
+ case 'listening':
13
+ return theme.colors.voiceActive;
14
+ case 'speaking':
15
+ return theme.colors.voiceSpeaking;
16
+ case 'idle':
17
+ return theme.colors.mutedText;
18
+ default:
19
+ return theme.colors.primary;
20
+ }
21
+ }
22
+
23
+ function MicIcon({ color }: { color: string }): React.ReactElement {
24
+ return (
25
+ <svg width="24" height="24" viewBox="0 0 24 24" fill={color} aria-hidden>
26
+ <path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm5-3c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z" />
27
+ </svg>
28
+ );
29
+ }
30
+
31
+ function MicOffIcon(): React.ReactElement {
32
+ return (
33
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="#ef4444" aria-hidden>
34
+ <path d="M19 11h-1.7c0 .74-.16 1.43-.43 2.05l1.23 1.23c.56-.98.9-2.09.9-3.28zm-4.02.17c0-.06.02-.11.02-.17V5c0-1.66-1.34-3-3-3s-3 1.34-3 3v.18l5.98 5.99zM4.27 3L3 4.27l6.01 6.01V11c0 1.66 1.33 3 2.99 3 .22 0 .44-.03.65-.08l1.66 1.66c-.71.33-1.5.52-2.31.52-2.76 0-5.3-2.1-5.3-5.1H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c.91-.13 1.77-.45 2.54-.9L19.73 21 21 19.73 4.27 3z" />
35
+ </svg>
36
+ );
37
+ }
38
+
39
+ export function AgentVoiceBar(): React.ReactElement | null {
40
+ const {
41
+ theme,
42
+ interactionMode,
43
+ voiceStatus,
44
+ voiceSessionId,
45
+ isVoiceConnecting,
46
+ isVoiceChannelReady,
47
+ stopVoice,
48
+ isVoiceAvailable,
49
+ isMicMuted,
50
+ isMicHeldForAssistant,
51
+ toggleVoiceMicMute,
52
+ } = useAgentAssistant();
53
+
54
+ const returnToTextMode = useCallback(() => {
55
+ void stopVoice();
56
+ }, [stopVoice]);
57
+
58
+ if (!isVoiceAvailable) return null;
59
+ if (interactionMode !== 'voice' && !isVoiceConnecting) return null;
60
+
61
+ const showConnecting = isVoiceConnecting;
62
+ const awaitingChannel =
63
+ Boolean(voiceSessionId) &&
64
+ !isVoiceConnecting &&
65
+ !isVoiceChannelReady &&
66
+ voiceStatus === 'idle';
67
+
68
+ const statusHint = showConnecting
69
+ ? 'Tap Back to text mode below to cancel'
70
+ : awaitingChannel
71
+ ? 'Waiting for voice channel…'
72
+ : isMicHeldForAssistant
73
+ ? 'Assistant is responding — tap mic to interrupt and speak'
74
+ : isMicMuted
75
+ ? 'Mic is off — tap the mic when ready to speak'
76
+ : voiceStatus === 'listening'
77
+ ? 'Start speaking'
78
+ : voiceStatus === 'speaking'
79
+ ? 'Assistant is speaking…'
80
+ : voiceStatus === 'thinking'
81
+ ? 'Thinking…'
82
+ : '';
83
+
84
+ return (
85
+ <div
86
+ style={{
87
+ borderTop: `1px solid ${theme.colors.border}`,
88
+ backgroundColor: theme.colors.surface,
89
+ padding: theme.spacing.md,
90
+ }}
91
+ >
92
+ <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
93
+ <div
94
+ style={{
95
+ flex: 1,
96
+ minWidth: 0,
97
+ border: `1px solid ${theme.colors.border}`,
98
+ borderRadius: 12,
99
+ backgroundColor: theme.colors.background,
100
+ padding: '8px 10px',
101
+ }}
102
+ >
103
+ <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
104
+ <span
105
+ style={{
106
+ width: 8,
107
+ height: 8,
108
+ borderRadius: 4,
109
+ backgroundColor: statusDotColor(voiceStatus, theme),
110
+ flexShrink: 0,
111
+ }}
112
+ />
113
+ <span
114
+ style={{
115
+ color: theme.colors.assistantText,
116
+ fontSize: theme.typography.captionSize,
117
+ fontWeight: 600,
118
+ }}
119
+ >
120
+ {showConnecting || awaitingChannel ? 'Connecting' : voiceStatusLabel(voiceStatus)}
121
+ </span>
122
+ {showConnecting ? (
123
+ <span style={{ color: theme.colors.mutedText, fontSize: theme.typography.captionSize }}>
124
+
125
+ </span>
126
+ ) : null}
127
+ </div>
128
+ {statusHint ? (
129
+ <p
130
+ style={{
131
+ margin: '4px 0 0',
132
+ color: theme.colors.mutedText,
133
+ fontSize: theme.typography.captionSize - 1,
134
+ }}
135
+ >
136
+ {statusHint}
137
+ </p>
138
+ ) : null}
139
+ </div>
140
+ <button
141
+ type="button"
142
+ onClick={toggleVoiceMicMute}
143
+ disabled={showConnecting || awaitingChannel}
144
+ style={{
145
+ border: 'none',
146
+ background: 'none',
147
+ padding: 8,
148
+ cursor: showConnecting || awaitingChannel ? 'not-allowed' : 'pointer',
149
+ opacity: showConnecting || awaitingChannel ? 0.45 : 1,
150
+ }}
151
+ aria-label={isMicMuted ? 'Unmute microphone' : 'Mute microphone'}
152
+ >
153
+ {isMicMuted ? <MicOffIcon /> : <MicIcon color={theme.colors.assistantText} />}
154
+ </button>
155
+ </div>
156
+ <button
157
+ type="button"
158
+ onClick={returnToTextMode}
159
+ style={{
160
+ display: 'block',
161
+ margin: `${theme.spacing.sm}px auto 0`,
162
+ border: 'none',
163
+ background: 'none',
164
+ color: theme.colors.primary,
165
+ fontSize: theme.typography.captionSize,
166
+ cursor: 'pointer',
167
+ }}
168
+ >
169
+ Back to text mode
170
+ </button>
171
+ </div>
172
+ );
173
+ }
@@ -0,0 +1,41 @@
1
+ import React from 'react';
2
+ import { useAgentAssistant } from '../context/AgentAssistantContext';
3
+ import type { PresetMessage } from '../types';
4
+
5
+ export function PresetMessageChips(): React.ReactElement | null {
6
+ const { presetMessages, selectPreset, theme, interactionMode } = useAgentAssistant();
7
+ if (!presetMessages.length || interactionMode === 'voice') {
8
+ return null;
9
+ }
10
+
11
+ return (
12
+ <div
13
+ style={{
14
+ display: 'flex',
15
+ flexWrap: 'wrap',
16
+ gap: theme.spacing.sm,
17
+ padding: `${theme.spacing.sm}px ${theme.spacing.md}px`,
18
+ borderBottom: `1px solid ${theme.colors.border}`,
19
+ }}
20
+ >
21
+ {presetMessages.map((preset: PresetMessage, index: number) => (
22
+ <button
23
+ key={`${preset.text}-${index}`}
24
+ type="button"
25
+ onClick={() => void selectPreset(preset)}
26
+ style={{
27
+ border: `1px solid ${theme.colors.border}`,
28
+ borderRadius: theme.radius.button,
29
+ padding: `${theme.spacing.xs}px ${theme.spacing.sm}px`,
30
+ backgroundColor: theme.colors.surface,
31
+ color: theme.colors.assistantText,
32
+ fontSize: theme.typography.captionSize,
33
+ cursor: 'pointer',
34
+ }}
35
+ >
36
+ {preset.text}
37
+ </button>
38
+ ))}
39
+ </div>
40
+ );
41
+ }