polymorph-sdk 0.2.0 → 0.2.3

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.
@@ -1,71 +1,90 @@
1
- import { LiveKitRoom, RoomAudioRenderer, useRoomContext } from "@livekit/components-react";
2
- import { useCallback, useEffect, useState } from "react";
3
1
  import type { Room } from "livekit-client";
4
- import { type DataPacket_Kind, type Participant, ParticipantKind, RoomEvent, type TranscriptionSegment } from "livekit-client";
2
+ import { useCallback, useRef, useState } from "react";
5
3
  import { ChatThread } from "./ChatThread";
6
4
  import styles from "./styles.module.css";
7
5
  import type { ChatMessage, SessionStatus, WidgetConfig } from "./types";
8
6
  import { VoiceOverlay } from "./VoiceOverlay";
9
7
 
10
- // Internal SVG icons (no @tabler dependency)
11
- function MicIcon() { return <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" x2="12" y1="19" y2="22"/></svg>; }
12
- function MicOffIcon() { return <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="2" x2="22" y1="2" y2="22"/><path d="M18.89 13.23A7.12 7.12 0 0 0 19 12v-2"/><path d="M5 10v2a7 7 0 0 0 12 5"/><path d="M15 9.34V5a3 3 0 0 0-5.68-1.33"/><path d="M9 9v3a3 3 0 0 0 5.12 2.12"/><line x1="12" x2="12" y1="19" y2="22"/></svg>; }
13
- function SendIcon() { return <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m22 2-7 20-4-9-9-4Z"/><path d="M22 2 11 13"/></svg>; }
14
- function CloseIcon() { return <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>; }
15
-
16
- function RoomHandler({
17
- setRoom,
18
- addMessage,
19
- }: {
20
- setRoom: (room: Room | null) => void;
21
- addMessage: (role: "user" | "agent", text: string, source: "chat" | "voice") => void;
22
- }) {
23
- const room = useRoomContext();
24
-
25
- useEffect(() => {
26
- setRoom(room);
27
- return () => { setRoom(null); };
28
- }, [room, setRoom]);
29
-
30
- useEffect(() => {
31
- const handleDataReceived = (
32
- payload: Uint8Array,
33
- _participant?: Participant,
34
- _kind?: DataPacket_Kind,
35
- topic?: string,
36
- ) => {
37
- if (topic === "chat_message") {
38
- try {
39
- const data = JSON.parse(new TextDecoder().decode(payload));
40
- if (data.type === "chat_response") {
41
- addMessage("agent", data.text, "chat");
42
- }
43
- } catch { /* ignore malformed */ }
44
- }
45
- };
46
-
47
- const handleTranscription = (
48
- segments: TranscriptionSegment[],
49
- participant?: Participant,
50
- ) => {
51
- const finalSegments = segments.filter(s => s.final);
52
- if (finalSegments.length > 0) {
53
- const text = finalSegments.map(s => s.text).join(" ");
54
- const isAgent = participant?.kind === ParticipantKind.AGENT ||
55
- participant?.identity?.includes("agent");
56
- addMessage(isAgent ? "agent" : "user", text, "voice");
57
- }
58
- };
59
-
60
- room.on(RoomEvent.DataReceived, handleDataReceived);
61
- room.on(RoomEvent.TranscriptionReceived, handleTranscription);
62
- return () => {
63
- room.off(RoomEvent.DataReceived, handleDataReceived);
64
- room.off(RoomEvent.TranscriptionReceived, handleTranscription);
65
- };
66
- }, [room, addMessage]);
67
-
68
- return <RoomAudioRenderer />;
8
+ function MicIcon() {
9
+ return (
10
+ <svg
11
+ xmlns="http://www.w3.org/2000/svg"
12
+ width="18"
13
+ height="18"
14
+ viewBox="0 0 24 24"
15
+ fill="none"
16
+ stroke="currentColor"
17
+ strokeWidth="2"
18
+ strokeLinecap="round"
19
+ strokeLinejoin="round"
20
+ >
21
+ <title>Microphone on</title>
22
+ <path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" />
23
+ <path d="M19 10v2a7 7 0 0 1-14 0v-2" />
24
+ <line x1="12" x2="12" y1="19" y2="22" />
25
+ </svg>
26
+ );
27
+ }
28
+ function MicOffIcon() {
29
+ return (
30
+ <svg
31
+ xmlns="http://www.w3.org/2000/svg"
32
+ width="18"
33
+ height="18"
34
+ viewBox="0 0 24 24"
35
+ fill="none"
36
+ stroke="currentColor"
37
+ strokeWidth="2"
38
+ strokeLinecap="round"
39
+ strokeLinejoin="round"
40
+ >
41
+ <title>Microphone off</title>
42
+ <line x1="2" x2="22" y1="2" y2="22" />
43
+ <path d="M18.89 13.23A7.12 7.12 0 0 0 19 12v-2" />
44
+ <path d="M5 10v2a7 7 0 0 0 12 5" />
45
+ <path d="M15 9.34V5a3 3 0 0 0-5.68-1.33" />
46
+ <path d="M9 9v3a3 3 0 0 0 5.12 2.12" />
47
+ <line x1="12" x2="12" y1="19" y2="22" />
48
+ </svg>
49
+ );
50
+ }
51
+ function SendIcon() {
52
+ return (
53
+ <svg
54
+ xmlns="http://www.w3.org/2000/svg"
55
+ width="18"
56
+ height="18"
57
+ viewBox="0 0 24 24"
58
+ fill="none"
59
+ stroke="currentColor"
60
+ strokeWidth="2"
61
+ strokeLinecap="round"
62
+ strokeLinejoin="round"
63
+ >
64
+ <title>Send</title>
65
+ <path d="m22 2-7 20-4-9-9-4Z" />
66
+ <path d="M22 2 11 13" />
67
+ </svg>
68
+ );
69
+ }
70
+ function CloseIcon() {
71
+ return (
72
+ <svg
73
+ xmlns="http://www.w3.org/2000/svg"
74
+ width="18"
75
+ height="18"
76
+ viewBox="0 0 24 24"
77
+ fill="none"
78
+ stroke="currentColor"
79
+ strokeWidth="2"
80
+ strokeLinecap="round"
81
+ strokeLinejoin="round"
82
+ >
83
+ <title>Close</title>
84
+ <path d="M18 6 6 18" />
85
+ <path d="m6 6 12 12" />
86
+ </svg>
87
+ );
69
88
  }
70
89
 
71
90
  interface WidgetPanelProps {
@@ -79,91 +98,125 @@ interface WidgetPanelProps {
79
98
  error: string | null;
80
99
  connect: () => Promise<void>;
81
100
  disconnect: () => void;
82
- addMessage: (role: "user" | "agent", text: string, source: "chat" | "voice") => void;
101
+ addMessage: (
102
+ role: "user" | "agent",
103
+ text: string,
104
+ source: "chat" | "voice",
105
+ ) => void;
83
106
  sendMessage: (text: string) => void;
84
107
  toggleMic: () => void;
85
108
  toggleVoice: () => void;
86
109
  setRoom: (room: Room | null) => void;
87
110
  };
88
111
  onClose: () => void;
112
+ hidden?: boolean;
89
113
  }
90
114
 
91
- export function WidgetPanel({ config, session, onClose }: WidgetPanelProps) {
115
+ export function WidgetPanel({
116
+ config,
117
+ session,
118
+ onClose,
119
+ hidden,
120
+ }: WidgetPanelProps) {
92
121
  const [inputValue, setInputValue] = useState("");
122
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
93
123
  const primaryColor = config.branding?.primaryColor || "#171717";
124
+ const isChatOnlyAgent = (config.agentName || "")
125
+ .toLowerCase()
126
+ .includes("chat-agent");
94
127
 
95
128
  const handleSend = useCallback(() => {
96
129
  if (!inputValue.trim()) return;
97
130
  session.sendMessage(inputValue);
98
131
  setInputValue("");
132
+ const el = textareaRef.current;
133
+ if (el) el.style.height = "auto";
99
134
  }, [inputValue, session]);
100
135
 
101
- const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
102
- if (e.key === "Enter" && !e.shiftKey) {
103
- e.preventDefault();
104
- handleSend();
105
- }
106
- }, [handleSend]);
136
+ const handleKeyDown = useCallback(
137
+ (e: React.KeyboardEvent) => {
138
+ if (e.key === "Enter" && !e.shiftKey) {
139
+ e.preventDefault();
140
+ handleSend();
141
+ }
142
+ },
143
+ [handleSend],
144
+ );
145
+
146
+ const handleInput = useCallback(() => {
147
+ const el = textareaRef.current;
148
+ if (!el) return;
149
+ el.style.height = "auto";
150
+ el.style.height = `${el.scrollHeight}px`;
151
+ }, []);
107
152
 
108
153
  return (
109
- <div className={styles.panel}>
110
- {/* Header */}
154
+ <div
155
+ className={`${styles.panel} ${hidden ? styles.panelHidden : ""}`}
156
+ aria-hidden={hidden}
157
+ >
111
158
  <div className={styles.header}>
112
159
  <div>
113
- <div style={{ fontWeight: 500, fontSize: 16 }}>{config.branding?.title || "Hi there"}</div>
160
+ <div style={{ fontWeight: 500, fontSize: 16 }}>
161
+ {config.branding?.title || "Hi there"}
162
+ </div>
114
163
  {config.branding?.subtitle && (
115
- <div style={{ fontSize: 13, color: "#737373", marginTop: 2 }}>{config.branding.subtitle}</div>
164
+ <div
165
+ style={{
166
+ fontSize: 13,
167
+ color: "var(--mantine-color-dimmed)",
168
+ marginTop: 2,
169
+ }}
170
+ >
171
+ {config.branding.subtitle}
172
+ </div>
116
173
  )}
117
174
  </div>
118
- <button type="button" className={styles.iconButton} onClick={onClose}><CloseIcon /></button>
175
+ <button type="button" className={styles.iconButton} onClick={onClose}>
176
+ <CloseIcon />
177
+ </button>
119
178
  </div>
120
-
121
- {/* Chat Thread */}
122
- <ChatThread messages={session.messages} primaryColor={primaryColor} />
123
-
124
- {/* Voice Overlay */}
125
- <VoiceOverlay
126
- isVoiceEnabled={session.isVoiceEnabled}
127
- isMicActive={session.isMicActive}
179
+ <ChatThread
180
+ messages={session.messages}
181
+ primaryColor={primaryColor}
128
182
  status={session.status}
129
- onToggleVoice={session.toggleVoice}
130
183
  />
131
-
132
- {/* Error */}
184
+ {!isChatOnlyAgent && (
185
+ <VoiceOverlay
186
+ isVoiceEnabled={session.isVoiceEnabled}
187
+ isMicActive={session.isMicActive}
188
+ status={session.status}
189
+ onToggleVoice={session.toggleVoice}
190
+ />
191
+ )}
133
192
  {session.error && <div className={styles.errorText}>{session.error}</div>}
134
-
135
- {/* Connect button or Input bar */}
136
- {session.status === "idle" || session.status === "error" ? (
193
+ <div className={styles.inputBar}>
194
+ <textarea
195
+ ref={textareaRef}
196
+ className={styles.inputField}
197
+ placeholder={
198
+ session.status === "connecting"
199
+ ? "Connecting..."
200
+ : "Type a message..."
201
+ }
202
+ value={inputValue}
203
+ onChange={(e) => setInputValue(e.target.value)}
204
+ onInput={handleInput}
205
+ onKeyDown={handleKeyDown}
206
+ disabled={session.status === "connecting"}
207
+ rows={1}
208
+ />
137
209
  <button
138
210
  type="button"
139
- className={styles.connectButton}
140
- style={{ backgroundColor: primaryColor }}
141
- onClick={session.connect}
211
+ className={styles.iconButton}
212
+ onClick={handleSend}
213
+ disabled={!inputValue.trim() || session.status === "connecting"}
142
214
  >
143
- Start conversation
144
- </button>
145
- ) : session.status === "connecting" ? (
146
- <button type="button" className={styles.connectButton} style={{ backgroundColor: primaryColor }} disabled>
147
- Connecting...
215
+ <SendIcon />
148
216
  </button>
149
- ) : (
150
- <div className={styles.inputBar}>
151
- <input
152
- className={styles.inputField}
153
- placeholder="Type a message..."
154
- value={inputValue}
155
- onChange={(e) => setInputValue(e.target.value)}
156
- onKeyDown={handleKeyDown}
157
- />
158
- <button
159
- type="button"
160
- className={styles.iconButton}
161
- onClick={handleSend}
162
- disabled={!inputValue.trim()}
163
- >
164
- <SendIcon />
165
- </button>
166
- {session.isVoiceEnabled && (
217
+ {session.isVoiceEnabled &&
218
+ !isChatOnlyAgent &&
219
+ session.status === "connected" && (
167
220
  <button
168
221
  type="button"
169
222
  className={`${styles.iconButton} ${session.isMicActive ? styles.iconButtonActive : ""}`}
@@ -172,23 +225,7 @@ export function WidgetPanel({ config, session, onClose }: WidgetPanelProps) {
172
225
  {session.isMicActive ? <MicIcon /> : <MicOffIcon />}
173
226
  </button>
174
227
  )}
175
- </div>
176
- )}
177
-
178
- {/* Hidden LiveKit Room */}
179
- {session.roomConnection && (
180
- <LiveKitRoom
181
- token={session.roomConnection.token}
182
- serverUrl={session.roomConnection.livekitUrl}
183
- connect={true}
184
- audio={session.isVoiceEnabled ? { echoCancellation: true, noiseSuppression: true, autoGainControl: true } : false}
185
- video={false}
186
- style={{ display: "none" }}
187
- onDisconnected={session.disconnect}
188
- >
189
- <RoomHandler setRoom={session.setRoom} addMessage={session.addMessage} />
190
- </LiveKitRoom>
191
- )}
228
+ </div>
192
229
  </div>
193
230
  );
194
231
  }
@@ -0,0 +1,13 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ describe("polymorph-sdk exports", () => {
4
+ it("exports PolymorphWidget component", async () => {
5
+ const mod = await import("../index");
6
+ expect(mod.PolymorphWidget).toBeDefined();
7
+ });
8
+
9
+ it("exports usePolymorphSession hook", async () => {
10
+ const mod = await import("../index");
11
+ expect(mod.usePolymorphSession).toBeDefined();
12
+ });
13
+ });
package/src/index.ts CHANGED
@@ -1,3 +1,8 @@
1
1
  export { PolymorphWidget } from "./PolymorphWidget";
2
+ export type {
3
+ ChatMessage,
4
+ SessionStatus,
5
+ WidgetBranding,
6
+ WidgetConfig,
7
+ } from "./types";
2
8
  export { usePolymorphSession } from "./usePolymorphSession";
3
- export type { ChatMessage, SessionStatus, WidgetBranding, WidgetConfig } from "./types";
@@ -1,240 +1,41 @@
1
- .widgetRoot {
2
- position: fixed;
3
- z-index: 9999;
4
- display: flex;
5
- flex-direction: column;
6
- align-items: flex-end;
7
- gap: 12px;
8
- }
9
-
10
- .bottomRight {
11
- bottom: 24px;
12
- right: 24px;
13
- }
14
-
15
- .bottomLeft {
16
- bottom: 24px;
17
- left: 24px;
18
- align-items: flex-start;
19
- }
20
-
21
- .fab {
22
- width: 56px;
23
- height: 56px;
24
- border-radius: 28px;
25
- border: none;
26
- cursor: pointer;
27
- display: flex;
28
- align-items: center;
29
- justify-content: center;
30
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
31
- transition: transform 150ms ease, box-shadow 150ms ease;
32
- color: white;
33
- }
34
-
35
- .fab:hover {
36
- transform: scale(1.05);
37
- box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
38
- }
39
-
40
- .panel {
41
- width: 380px;
42
- max-height: 600px;
43
- display: flex;
44
- flex-direction: column;
45
- border-radius: 16px;
46
- box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
47
- overflow: hidden;
48
- background: white;
49
- border: 1px solid #e5e5e5;
50
- }
51
-
52
- .header {
53
- padding: 16px 16px 12px;
54
- border-bottom: 1px solid #f0f0f0;
55
- display: flex;
56
- justify-content: space-between;
57
- align-items: flex-start;
58
- }
59
-
60
- .chatThread {
61
- flex: 1;
62
- overflow-y: auto;
63
- padding: 16px;
64
- display: flex;
65
- flex-direction: column;
66
- gap: 8px;
67
- min-height: 200px;
68
- max-height: 400px;
69
- }
70
-
71
- .messageBubble {
72
- max-width: 80%;
73
- padding: 8px 12px;
74
- border-radius: 12px;
75
- font-size: 14px;
76
- line-height: 1.4;
77
- word-wrap: break-word;
78
- }
79
-
80
- .agentMessage {
81
- align-self: flex-start;
82
- background: #f5f5f5;
83
- color: #171717;
84
- }
85
-
86
- .userMessage {
87
- align-self: flex-end;
88
- color: white;
89
- }
90
-
91
- .voiceLabel {
92
- font-size: 10px;
93
- opacity: 0.6;
94
- margin-top: 2px;
95
- }
96
-
97
- .voiceOverlay {
98
- padding: 8px 16px;
99
- display: flex;
100
- align-items: center;
101
- gap: 8px;
102
- font-size: 13px;
103
- color: #737373;
104
- border-top: 1px solid #f0f0f0;
105
- }
106
-
107
- .voiceBars {
108
- display: flex;
109
- align-items: center;
110
- gap: 2px;
111
- height: 16px;
112
- }
113
-
114
- .voiceBar {
115
- width: 3px;
116
- border-radius: 2px;
117
- background: #22c55e;
118
- animation: voiceBar 1.2s ease-in-out infinite;
119
- }
120
-
1
+ .widgetRoot { position: fixed; z-index: 9999; display: flex; flex-direction: column; align-items: flex-end; justify-content: flex-end; gap: 12px; pointer-events: none; }
2
+ .bottomRight { top: 24px; bottom: 24px; right: 24px; }
3
+ .bottomLeft { top: 24px; bottom: 24px; left: 24px; align-items: flex-start; }
4
+ .fab { width: 56px; height: 56px; border-radius: 28px; border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); transition: transform 150ms ease, box-shadow 150ms ease; color: white; pointer-events: auto; }
5
+ .fab svg { transition: transform 200ms ease; }
6
+ .fabOpen svg { transform: rotate(90deg); }
7
+ .fab:hover { transform: scale(1.05); box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2); }
8
+ .panel { width: 380px; max-height: 600px; flex-shrink: 1; min-height: 0; display: flex; flex-direction: column; border-radius: 16px; box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08), 0 12px 48px rgba(0, 0, 0, 0.12); overflow: hidden; background: var(--mantine-color-body); color: var(--mantine-color-text); pointer-events: auto; transition: opacity 200ms ease, transform 200ms ease; transform-origin: bottom right; }
9
+ .panelHidden { opacity: 0; transform: scale(0.95) translateY(8px); pointer-events: none; }
10
+ .header { padding: 16px 16px 12px; border-bottom: 1px solid var(--mantine-color-default-border); display: flex; justify-content: space-between; align-items: flex-start; }
11
+ .chatThread { flex: 1; overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 8px; min-height: 0; }
12
+ .messageBubble { max-width: 80%; padding: 8px 12px; border-radius: 12px; font-size: 14px; line-height: 1.4; word-wrap: break-word; animation: messageAppear 200ms ease; }
13
+ @keyframes messageAppear { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
14
+ .agentMessage { align-self: flex-start; background: var(--mantine-color-gray-light); color: var(--mantine-color-text); }
15
+ .userMessage { align-self: flex-end; color: white; }
16
+ .voiceLabel { font-size: 10px; opacity: 0.6; margin-top: 2px; }
17
+ .voiceOverlay { padding: 8px 16px; display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--mantine-color-dimmed); border-top: 1px solid var(--mantine-color-default-border); }
18
+ .voiceBars { display: flex; align-items: center; gap: 2px; height: 16px; }
19
+ .voiceBar { width: 3px; border-radius: 2px; background: #22c55e; animation: voiceBar 1.2s ease-in-out infinite; }
121
20
  .voiceBar:nth-child(1) { height: 6px; animation-delay: 0s; }
122
21
  .voiceBar:nth-child(2) { height: 12px; animation-delay: 0.2s; }
123
22
  .voiceBar:nth-child(3) { height: 8px; animation-delay: 0.4s; }
124
-
125
- @keyframes voiceBar {
126
- 0%, 100% { transform: scaleY(1); }
127
- 50% { transform: scaleY(0.4); }
128
- }
129
-
130
- .voiceToggle {
131
- margin-left: auto;
132
- width: 28px;
133
- height: 28px;
134
- border-radius: 14px;
135
- border: 1px solid #e5e5e5;
136
- cursor: pointer;
137
- display: flex;
138
- align-items: center;
139
- justify-content: center;
140
- background: #f5f5f5;
141
- color: #737373;
142
- transition: all 150ms ease;
143
- }
144
-
145
- .voiceToggle:hover {
146
- background: #e5e5e5;
147
- }
148
-
149
- .voiceToggleActive {
150
- background: #dcfce7;
151
- border-color: #bbf7d0;
152
- color: #16a34a;
153
- }
154
-
155
- .voiceToggleActive:hover {
156
- background: #bbf7d0;
157
- }
158
-
159
- .inputBar {
160
- padding: 12px 16px;
161
- border-top: 1px solid #f0f0f0;
162
- display: flex;
163
- gap: 8px;
164
- align-items: center;
165
- }
166
-
167
- .inputField {
168
- flex: 1;
169
- border: 1px solid #e5e5e5;
170
- border-radius: 8px;
171
- padding: 8px 12px;
172
- font-size: 14px;
173
- outline: none;
174
- font-family: inherit;
175
- }
176
-
177
- .inputField:focus {
178
- border-color: #a3a3a3;
179
- }
180
-
181
- .iconButton {
182
- width: 36px;
183
- height: 36px;
184
- border-radius: 8px;
185
- border: none;
186
- cursor: pointer;
187
- display: flex;
188
- align-items: center;
189
- justify-content: center;
190
- background: transparent;
191
- color: #737373;
192
- transition: background 150ms ease, color 150ms ease;
193
- }
194
-
195
- .iconButton:hover {
196
- background: #f5f5f5;
197
- }
198
-
199
- .iconButton:disabled {
200
- opacity: 0.4;
201
- cursor: default;
202
- }
203
-
204
- .iconButtonActive {
205
- color: #22c55e;
206
- }
207
-
208
- .connectButton {
209
- margin: 16px;
210
- padding: 10px 20px;
211
- border-radius: 8px;
212
- border: none;
213
- cursor: pointer;
214
- font-size: 14px;
215
- font-weight: 500;
216
- color: white;
217
- transition: opacity 150ms ease;
218
- }
219
-
220
- .connectButton:hover {
221
- opacity: 0.9;
222
- }
223
-
224
- .connectButton:disabled {
225
- opacity: 0.5;
226
- cursor: default;
227
- }
228
-
229
- .statusBadge {
230
- font-size: 11px;
231
- padding: 2px 8px;
232
- border-radius: 10px;
233
- font-weight: 500;
234
- }
235
-
236
- .errorText {
237
- color: #dc2626;
238
- font-size: 13px;
239
- padding: 0 16px;
240
- }
23
+ @keyframes voiceBar { 0%, 100% { transform: scaleY(1); } 50% { transform: scaleY(0.4); } }
24
+ .voiceToggle { margin-left: auto; width: 28px; height: 28px; border-radius: 14px; border: 1px solid var(--mantine-color-default-border); cursor: pointer; display: flex; align-items: center; justify-content: center; background: var(--mantine-color-gray-light); color: var(--mantine-color-dimmed); transition: all 150ms ease; }
25
+ .voiceToggle:hover { background: var(--mantine-color-gray-light-hover); }
26
+ .voiceToggleActive { background: light-dark(#dcfce7, #1a3a2a); border-color: light-dark(#bbf7d0, #2a5a3a); color: light-dark(#16a34a, #4ade80); }
27
+ .voiceToggleActive:hover { background: light-dark(#bbf7d0, #2a5a3a); }
28
+ .inputBar { padding: 12px 16px; border-top: 1px solid var(--mantine-color-default-border); display: flex; gap: 8px; align-items: flex-end; }
29
+ .inputField { flex: 1; border: 1px solid var(--mantine-color-default-border); border-radius: 8px; padding: 8px 12px; font-size: 14px; line-height: 1.4; outline: none; font-family: inherit; background: var(--mantine-color-body); color: var(--mantine-color-text); resize: none; overflow-y: auto; max-height: 120px; }
30
+ .inputField:focus { border-color: var(--mantine-color-dimmed); }
31
+ .iconButton { width: 36px; height: 36px; border-radius: 8px; border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; background: transparent; color: var(--mantine-color-dimmed); transition: background 150ms ease, color 150ms ease; flex-shrink: 0; }
32
+ .iconButton:hover { background: var(--mantine-color-gray-light); }
33
+ .iconButton:disabled { opacity: 0.4; cursor: default; }
34
+ .iconButtonActive { color: #22c55e; }
35
+ .statusBadge { font-size: 11px; padding: 2px 8px; border-radius: 10px; font-weight: 500; }
36
+ .errorText { color: #dc2626; font-size: 13px; padding: 0 16px; }
37
+ .thinkingDots { display: flex; gap: 4px; align-items: center; height: 20px; }
38
+ .thinkingDots span { width: 6px; height: 6px; border-radius: 50%; background: var(--mantine-color-dimmed); animation: thinkingDot 1.4s ease-in-out infinite; }
39
+ .thinkingDots span:nth-child(2) { animation-delay: 0.2s; }
40
+ .thinkingDots span:nth-child(3) { animation-delay: 0.4s; }
41
+ @keyframes thinkingDot { 0%, 80%, 100% { opacity: 0.3; transform: scale(0.8); } 40% { opacity: 1; transform: scale(1); } }