polymorph-sdk 0.2.2 → 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.
- package/dist/index.css +1 -1
- package/dist/index.d.ts +13 -0
- package/dist/index.js +202 -148
- package/package.json +1 -1
- package/src/ChatThread.tsx +28 -8
- package/src/PolymorphWidget.tsx +30 -20
- package/src/WidgetPanel.tsx +50 -47
- package/src/styles.module.css +41 -304
- package/src/types.ts +13 -0
- package/src/usePolymorphSession.ts +57 -18
package/src/WidgetPanel.tsx
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import type { Room } from "livekit-client";
|
|
2
|
-
import { useCallback, useState } from "react";
|
|
2
|
+
import { useCallback, useRef, useState } from "react";
|
|
3
3
|
import { ChatThread } from "./ChatThread";
|
|
4
4
|
import styles from "./styles.module.css";
|
|
5
5
|
import type { ChatMessage, SessionStatus, WidgetConfig } from "./types";
|
|
6
6
|
import { VoiceOverlay } from "./VoiceOverlay";
|
|
7
7
|
|
|
8
|
-
// Internal SVG icons (no @tabler dependency)
|
|
9
8
|
function MicIcon() {
|
|
10
9
|
return (
|
|
11
10
|
<svg
|
|
@@ -19,6 +18,7 @@ function MicIcon() {
|
|
|
19
18
|
strokeLinecap="round"
|
|
20
19
|
strokeLinejoin="round"
|
|
21
20
|
>
|
|
21
|
+
<title>Microphone on</title>
|
|
22
22
|
<path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" />
|
|
23
23
|
<path d="M19 10v2a7 7 0 0 1-14 0v-2" />
|
|
24
24
|
<line x1="12" x2="12" y1="19" y2="22" />
|
|
@@ -38,6 +38,7 @@ function MicOffIcon() {
|
|
|
38
38
|
strokeLinecap="round"
|
|
39
39
|
strokeLinejoin="round"
|
|
40
40
|
>
|
|
41
|
+
<title>Microphone off</title>
|
|
41
42
|
<line x1="2" x2="22" y1="2" y2="22" />
|
|
42
43
|
<path d="M18.89 13.23A7.12 7.12 0 0 0 19 12v-2" />
|
|
43
44
|
<path d="M5 10v2a7 7 0 0 0 12 5" />
|
|
@@ -60,6 +61,7 @@ function SendIcon() {
|
|
|
60
61
|
strokeLinecap="round"
|
|
61
62
|
strokeLinejoin="round"
|
|
62
63
|
>
|
|
64
|
+
<title>Send</title>
|
|
63
65
|
<path d="m22 2-7 20-4-9-9-4Z" />
|
|
64
66
|
<path d="M22 2 11 13" />
|
|
65
67
|
</svg>
|
|
@@ -78,6 +80,7 @@ function CloseIcon() {
|
|
|
78
80
|
strokeLinecap="round"
|
|
79
81
|
strokeLinejoin="round"
|
|
80
82
|
>
|
|
83
|
+
<title>Close</title>
|
|
81
84
|
<path d="M18 6 6 18" />
|
|
82
85
|
<path d="m6 6 12 12" />
|
|
83
86
|
</svg>
|
|
@@ -106,10 +109,17 @@ interface WidgetPanelProps {
|
|
|
106
109
|
setRoom: (room: Room | null) => void;
|
|
107
110
|
};
|
|
108
111
|
onClose: () => void;
|
|
112
|
+
hidden?: boolean;
|
|
109
113
|
}
|
|
110
114
|
|
|
111
|
-
export function WidgetPanel({
|
|
115
|
+
export function WidgetPanel({
|
|
116
|
+
config,
|
|
117
|
+
session,
|
|
118
|
+
onClose,
|
|
119
|
+
hidden,
|
|
120
|
+
}: WidgetPanelProps) {
|
|
112
121
|
const [inputValue, setInputValue] = useState("");
|
|
122
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
113
123
|
const primaryColor = config.branding?.primaryColor || "#171717";
|
|
114
124
|
const isChatOnlyAgent = (config.agentName || "")
|
|
115
125
|
.toLowerCase()
|
|
@@ -119,6 +129,8 @@ export function WidgetPanel({ config, session, onClose }: WidgetPanelProps) {
|
|
|
119
129
|
if (!inputValue.trim()) return;
|
|
120
130
|
session.sendMessage(inputValue);
|
|
121
131
|
setInputValue("");
|
|
132
|
+
const el = textareaRef.current;
|
|
133
|
+
if (el) el.style.height = "auto";
|
|
122
134
|
}, [inputValue, session]);
|
|
123
135
|
|
|
124
136
|
const handleKeyDown = useCallback(
|
|
@@ -131,9 +143,18 @@ export function WidgetPanel({ config, session, onClose }: WidgetPanelProps) {
|
|
|
131
143
|
[handleSend],
|
|
132
144
|
);
|
|
133
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
|
+
}, []);
|
|
152
|
+
|
|
134
153
|
return (
|
|
135
|
-
<div
|
|
136
|
-
{
|
|
154
|
+
<div
|
|
155
|
+
className={`${styles.panel} ${hidden ? styles.panelHidden : ""}`}
|
|
156
|
+
aria-hidden={hidden}
|
|
157
|
+
>
|
|
137
158
|
<div className={styles.header}>
|
|
138
159
|
<div>
|
|
139
160
|
<div style={{ fontWeight: 500, fontSize: 16 }}>
|
|
@@ -155,15 +176,11 @@ export function WidgetPanel({ config, session, onClose }: WidgetPanelProps) {
|
|
|
155
176
|
<CloseIcon />
|
|
156
177
|
</button>
|
|
157
178
|
</div>
|
|
158
|
-
|
|
159
|
-
{/* Chat Thread */}
|
|
160
179
|
<ChatThread
|
|
161
180
|
messages={session.messages}
|
|
162
181
|
primaryColor={primaryColor}
|
|
163
182
|
status={session.status}
|
|
164
183
|
/>
|
|
165
|
-
|
|
166
|
-
{/* Voice Overlay */}
|
|
167
184
|
{!isChatOnlyAgent && (
|
|
168
185
|
<VoiceOverlay
|
|
169
186
|
isVoiceEnabled={session.isVoiceEnabled}
|
|
@@ -172,47 +189,34 @@ export function WidgetPanel({ config, session, onClose }: WidgetPanelProps) {
|
|
|
172
189
|
onToggleVoice={session.toggleVoice}
|
|
173
190
|
/>
|
|
174
191
|
)}
|
|
175
|
-
|
|
176
|
-
{/* Error */}
|
|
177
192
|
{session.error && <div className={styles.errorText}>{session.error}</div>}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
+
/>
|
|
190
209
|
<button
|
|
191
210
|
type="button"
|
|
192
|
-
className={styles.
|
|
193
|
-
|
|
194
|
-
disabled
|
|
211
|
+
className={styles.iconButton}
|
|
212
|
+
onClick={handleSend}
|
|
213
|
+
disabled={!inputValue.trim() || session.status === "connecting"}
|
|
195
214
|
>
|
|
196
|
-
|
|
215
|
+
<SendIcon />
|
|
197
216
|
</button>
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
className={styles.inputField}
|
|
202
|
-
placeholder="Type a message..."
|
|
203
|
-
value={inputValue}
|
|
204
|
-
onChange={(e) => setInputValue(e.target.value)}
|
|
205
|
-
onKeyDown={handleKeyDown}
|
|
206
|
-
/>
|
|
207
|
-
<button
|
|
208
|
-
type="button"
|
|
209
|
-
className={styles.iconButton}
|
|
210
|
-
onClick={handleSend}
|
|
211
|
-
disabled={!inputValue.trim()}
|
|
212
|
-
>
|
|
213
|
-
<SendIcon />
|
|
214
|
-
</button>
|
|
215
|
-
{session.isVoiceEnabled && !isChatOnlyAgent && (
|
|
217
|
+
{session.isVoiceEnabled &&
|
|
218
|
+
!isChatOnlyAgent &&
|
|
219
|
+
session.status === "connected" && (
|
|
216
220
|
<button
|
|
217
221
|
type="button"
|
|
218
222
|
className={`${styles.iconButton} ${session.isMicActive ? styles.iconButtonActive : ""}`}
|
|
@@ -221,8 +225,7 @@ export function WidgetPanel({ config, session, onClose }: WidgetPanelProps) {
|
|
|
221
225
|
{session.isMicActive ? <MicIcon /> : <MicOffIcon />}
|
|
222
226
|
</button>
|
|
223
227
|
)}
|
|
224
|
-
|
|
225
|
-
)}
|
|
228
|
+
</div>
|
|
226
229
|
</div>
|
|
227
230
|
);
|
|
228
231
|
}
|
package/src/styles.module.css
CHANGED
|
@@ -1,304 +1,41 @@
|
|
|
1
|
-
.widgetRoot {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
.fab:hover {
|
|
43
|
-
transform: scale(1.05);
|
|
44
|
-
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
.panel {
|
|
48
|
-
width: 380px;
|
|
49
|
-
max-height: 600px;
|
|
50
|
-
display: flex;
|
|
51
|
-
flex-direction: column;
|
|
52
|
-
border-radius: 16px;
|
|
53
|
-
box-shadow:
|
|
54
|
-
0 4px 24px rgba(0, 0, 0, 0.08),
|
|
55
|
-
0 12px 48px rgba(0, 0, 0, 0.12);
|
|
56
|
-
overflow: hidden;
|
|
57
|
-
background: var(--mantine-color-body);
|
|
58
|
-
color: var(--mantine-color-text);
|
|
59
|
-
pointer-events: auto;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
.header {
|
|
63
|
-
padding: 16px 16px 12px;
|
|
64
|
-
border-bottom: 1px solid var(--mantine-color-default-border);
|
|
65
|
-
display: flex;
|
|
66
|
-
justify-content: space-between;
|
|
67
|
-
align-items: flex-start;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
.chatThread {
|
|
71
|
-
flex: 1;
|
|
72
|
-
overflow-y: auto;
|
|
73
|
-
padding: 16px;
|
|
74
|
-
display: flex;
|
|
75
|
-
flex-direction: column;
|
|
76
|
-
gap: 8px;
|
|
77
|
-
min-height: 200px;
|
|
78
|
-
max-height: 400px;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
.messageBubble {
|
|
82
|
-
max-width: 80%;
|
|
83
|
-
padding: 8px 12px;
|
|
84
|
-
border-radius: 12px;
|
|
85
|
-
font-size: 14px;
|
|
86
|
-
line-height: 1.4;
|
|
87
|
-
word-wrap: break-word;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
.agentMessage {
|
|
91
|
-
align-self: flex-start;
|
|
92
|
-
background: var(--mantine-color-gray-light);
|
|
93
|
-
color: var(--mantine-color-text);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
.userMessage {
|
|
97
|
-
align-self: flex-end;
|
|
98
|
-
color: white;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
.voiceLabel {
|
|
102
|
-
font-size: 10px;
|
|
103
|
-
opacity: 0.6;
|
|
104
|
-
margin-top: 2px;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
.voiceOverlay {
|
|
108
|
-
padding: 8px 16px;
|
|
109
|
-
display: flex;
|
|
110
|
-
align-items: center;
|
|
111
|
-
gap: 8px;
|
|
112
|
-
font-size: 13px;
|
|
113
|
-
color: var(--mantine-color-dimmed);
|
|
114
|
-
border-top: 1px solid var(--mantine-color-default-border);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
.voiceBars {
|
|
118
|
-
display: flex;
|
|
119
|
-
align-items: center;
|
|
120
|
-
gap: 2px;
|
|
121
|
-
height: 16px;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
.voiceBar {
|
|
125
|
-
width: 3px;
|
|
126
|
-
border-radius: 2px;
|
|
127
|
-
background: #22c55e;
|
|
128
|
-
animation: voiceBar 1.2s ease-in-out infinite;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
.voiceBar:nth-child(1) {
|
|
132
|
-
height: 6px;
|
|
133
|
-
animation-delay: 0s;
|
|
134
|
-
}
|
|
135
|
-
.voiceBar:nth-child(2) {
|
|
136
|
-
height: 12px;
|
|
137
|
-
animation-delay: 0.2s;
|
|
138
|
-
}
|
|
139
|
-
.voiceBar:nth-child(3) {
|
|
140
|
-
height: 8px;
|
|
141
|
-
animation-delay: 0.4s;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
@keyframes voiceBar {
|
|
145
|
-
0%,
|
|
146
|
-
100% {
|
|
147
|
-
transform: scaleY(1);
|
|
148
|
-
}
|
|
149
|
-
50% {
|
|
150
|
-
transform: scaleY(0.4);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
.voiceToggle {
|
|
155
|
-
margin-left: auto;
|
|
156
|
-
width: 28px;
|
|
157
|
-
height: 28px;
|
|
158
|
-
border-radius: 14px;
|
|
159
|
-
border: 1px solid var(--mantine-color-default-border);
|
|
160
|
-
cursor: pointer;
|
|
161
|
-
display: flex;
|
|
162
|
-
align-items: center;
|
|
163
|
-
justify-content: center;
|
|
164
|
-
background: var(--mantine-color-gray-light);
|
|
165
|
-
color: var(--mantine-color-dimmed);
|
|
166
|
-
transition: all 150ms ease;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
.voiceToggle:hover {
|
|
170
|
-
background: var(--mantine-color-gray-light-hover);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
.voiceToggleActive {
|
|
174
|
-
background: light-dark(#dcfce7, #1a3a2a);
|
|
175
|
-
border-color: light-dark(#bbf7d0, #2a5a3a);
|
|
176
|
-
color: light-dark(#16a34a, #4ade80);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
.voiceToggleActive:hover {
|
|
180
|
-
background: light-dark(#bbf7d0, #2a5a3a);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
.inputBar {
|
|
184
|
-
padding: 12px 16px;
|
|
185
|
-
border-top: 1px solid var(--mantine-color-default-border);
|
|
186
|
-
display: flex;
|
|
187
|
-
gap: 8px;
|
|
188
|
-
align-items: center;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
.inputField {
|
|
192
|
-
flex: 1;
|
|
193
|
-
border: 1px solid var(--mantine-color-default-border);
|
|
194
|
-
border-radius: 8px;
|
|
195
|
-
padding: 8px 12px;
|
|
196
|
-
font-size: 14px;
|
|
197
|
-
outline: none;
|
|
198
|
-
font-family: inherit;
|
|
199
|
-
background: var(--mantine-color-body);
|
|
200
|
-
color: var(--mantine-color-text);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
.inputField:focus {
|
|
204
|
-
border-color: var(--mantine-color-dimmed);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
.iconButton {
|
|
208
|
-
width: 36px;
|
|
209
|
-
height: 36px;
|
|
210
|
-
border-radius: 8px;
|
|
211
|
-
border: none;
|
|
212
|
-
cursor: pointer;
|
|
213
|
-
display: flex;
|
|
214
|
-
align-items: center;
|
|
215
|
-
justify-content: center;
|
|
216
|
-
background: transparent;
|
|
217
|
-
color: var(--mantine-color-dimmed);
|
|
218
|
-
transition:
|
|
219
|
-
background 150ms ease,
|
|
220
|
-
color 150ms ease;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
.iconButton:hover {
|
|
224
|
-
background: var(--mantine-color-gray-light);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
.iconButton:disabled {
|
|
228
|
-
opacity: 0.4;
|
|
229
|
-
cursor: default;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
.iconButtonActive {
|
|
233
|
-
color: #22c55e;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
.connectButton {
|
|
237
|
-
margin: 16px;
|
|
238
|
-
padding: 10px 20px;
|
|
239
|
-
border-radius: 8px;
|
|
240
|
-
border: none;
|
|
241
|
-
cursor: pointer;
|
|
242
|
-
font-size: 14px;
|
|
243
|
-
font-weight: 500;
|
|
244
|
-
color: white;
|
|
245
|
-
transition: opacity 150ms ease;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
.connectButton:hover {
|
|
249
|
-
opacity: 0.9;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
.connectButton:disabled {
|
|
253
|
-
opacity: 0.5;
|
|
254
|
-
cursor: default;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
.statusBadge {
|
|
258
|
-
font-size: 11px;
|
|
259
|
-
padding: 2px 8px;
|
|
260
|
-
border-radius: 10px;
|
|
261
|
-
font-weight: 500;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
.errorText {
|
|
265
|
-
color: #dc2626;
|
|
266
|
-
font-size: 13px;
|
|
267
|
-
padding: 0 16px;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
.thinkingDots {
|
|
271
|
-
display: flex;
|
|
272
|
-
gap: 4px;
|
|
273
|
-
align-items: center;
|
|
274
|
-
height: 20px;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
.thinkingDots span {
|
|
278
|
-
width: 6px;
|
|
279
|
-
height: 6px;
|
|
280
|
-
border-radius: 50%;
|
|
281
|
-
background: var(--mantine-color-dimmed);
|
|
282
|
-
animation: thinkingDot 1.4s ease-in-out infinite;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
.thinkingDots span:nth-child(2) {
|
|
286
|
-
animation-delay: 0.2s;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
.thinkingDots span:nth-child(3) {
|
|
290
|
-
animation-delay: 0.4s;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
@keyframes thinkingDot {
|
|
294
|
-
0%,
|
|
295
|
-
80%,
|
|
296
|
-
100% {
|
|
297
|
-
opacity: 0.3;
|
|
298
|
-
transform: scale(0.8);
|
|
299
|
-
}
|
|
300
|
-
40% {
|
|
301
|
-
opacity: 1;
|
|
302
|
-
transform: scale(1);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
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; }
|
|
20
|
+
.voiceBar:nth-child(1) { height: 6px; animation-delay: 0s; }
|
|
21
|
+
.voiceBar:nth-child(2) { height: 12px; animation-delay: 0.2s; }
|
|
22
|
+
.voiceBar:nth-child(3) { height: 8px; animation-delay: 0.4s; }
|
|
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); } }
|
package/src/types.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
export interface WidgetConfig {
|
|
2
2
|
apiBaseUrl: string;
|
|
3
3
|
apiKey?: string;
|
|
4
|
+
/** Server-side widget config ID. When set, metadata is ignored and config is resolved server-side. */
|
|
5
|
+
configId?: string;
|
|
4
6
|
/** LiveKit dispatched agent name (default: "custom-voice-agent"). */
|
|
5
7
|
agentName?: string;
|
|
6
8
|
metadata?: Record<string, string | string[]>;
|
|
9
|
+
/** Pre-filled user identity. When provided, the agent skips asking for these fields. */
|
|
10
|
+
user?: WidgetUser;
|
|
7
11
|
branding?: WidgetBranding;
|
|
8
12
|
position?: "bottom-right" | "bottom-left";
|
|
9
13
|
/** Enable voice call (default: true). When false, widget is chat-only. */
|
|
@@ -14,6 +18,15 @@ export interface WidgetConfig {
|
|
|
14
18
|
darkMode?: boolean;
|
|
15
19
|
}
|
|
16
20
|
|
|
21
|
+
export interface WidgetUser {
|
|
22
|
+
/** Display name (e.g. "Jane Smith") */
|
|
23
|
+
name?: string;
|
|
24
|
+
/** Email address */
|
|
25
|
+
email?: string;
|
|
26
|
+
/** Phone number */
|
|
27
|
+
phone?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
17
30
|
export interface WidgetBranding {
|
|
18
31
|
/** FAB and accent color (default: "#171717") */
|
|
19
32
|
primaryColor?: string;
|
|
@@ -42,11 +42,25 @@ export function usePolymorphSession(config: WidgetConfig) {
|
|
|
42
42
|
const [roomConnection, setRoomConnection] = useState<RoomConnection | null>(
|
|
43
43
|
null,
|
|
44
44
|
);
|
|
45
|
-
const
|
|
45
|
+
const greeting = config.branding?.greeting;
|
|
46
|
+
const [messages, setMessages] = useState<ChatMessage[]>(() =>
|
|
47
|
+
greeting
|
|
48
|
+
? [
|
|
49
|
+
{
|
|
50
|
+
id: "greeting",
|
|
51
|
+
role: "agent" as const,
|
|
52
|
+
text: greeting,
|
|
53
|
+
source: "chat" as const,
|
|
54
|
+
timestamp: Date.now(),
|
|
55
|
+
},
|
|
56
|
+
]
|
|
57
|
+
: [],
|
|
58
|
+
);
|
|
46
59
|
const [isVoiceEnabled, setIsVoiceEnabled] = useState(defaultVoiceEnabled);
|
|
47
60
|
const [isMicActive, setIsMicActive] = useState(defaultVoiceEnabled);
|
|
48
61
|
const [error, setError] = useState<string | null>(null);
|
|
49
62
|
const roomRef = useRef<Room | null>(null);
|
|
63
|
+
const pendingMessagesRef = useRef<string[]>([]);
|
|
50
64
|
|
|
51
65
|
useEffect(() => {
|
|
52
66
|
setIsVoiceEnabled(defaultVoiceEnabled);
|
|
@@ -109,12 +123,19 @@ export function usePolymorphSession(config: WidgetConfig) {
|
|
|
109
123
|
},
|
|
110
124
|
body: JSON.stringify({
|
|
111
125
|
agent_name: config.agentName || "custom-voice-agent",
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
126
|
+
config_id: config.configId ?? undefined,
|
|
127
|
+
metadata: config.configId
|
|
128
|
+
? undefined
|
|
129
|
+
: {
|
|
130
|
+
...config.metadata,
|
|
131
|
+
...(config.branding?.greeting && {
|
|
132
|
+
greeting: config.branding.greeting,
|
|
133
|
+
}),
|
|
134
|
+
...(config.user?.name && { user_name: config.user.name }),
|
|
135
|
+
...(config.user?.email && { user_email: config.user.email }),
|
|
136
|
+
...(config.user?.phone && { user_phone: config.user.phone }),
|
|
137
|
+
},
|
|
138
|
+
user_name: config.user?.name,
|
|
118
139
|
external_user_id: externalUserId,
|
|
119
140
|
}),
|
|
120
141
|
...restFetchOptions,
|
|
@@ -142,18 +163,25 @@ export function usePolymorphSession(config: WidgetConfig) {
|
|
|
142
163
|
|
|
143
164
|
const sendMessage = useCallback(
|
|
144
165
|
(text: string) => {
|
|
166
|
+
if (!text.trim()) return;
|
|
167
|
+
const trimmed = text.trim();
|
|
168
|
+
addMessage("user", trimmed, "chat");
|
|
169
|
+
|
|
145
170
|
const room = roomRef.current;
|
|
146
|
-
if (
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
171
|
+
if (room) {
|
|
172
|
+
const payload = new TextEncoder().encode(
|
|
173
|
+
JSON.stringify({ text: trimmed }),
|
|
174
|
+
);
|
|
175
|
+
room.localParticipant.publishData(payload, {
|
|
176
|
+
reliable: true,
|
|
177
|
+
topic: "chat_message",
|
|
178
|
+
});
|
|
179
|
+
} else {
|
|
180
|
+
pendingMessagesRef.current.push(trimmed);
|
|
181
|
+
void connect();
|
|
182
|
+
}
|
|
155
183
|
},
|
|
156
|
-
[addMessage],
|
|
184
|
+
[addMessage, connect],
|
|
157
185
|
);
|
|
158
186
|
|
|
159
187
|
const toggleMic = useCallback(async () => {
|
|
@@ -182,7 +210,18 @@ export function usePolymorphSession(config: WidgetConfig) {
|
|
|
182
210
|
|
|
183
211
|
const setRoom = useCallback((room: Room | null) => {
|
|
184
212
|
roomRef.current = room;
|
|
185
|
-
if (room)
|
|
213
|
+
if (room) {
|
|
214
|
+
setStatus("connected");
|
|
215
|
+
// Flush any messages queued before the room was ready
|
|
216
|
+
const pending = pendingMessagesRef.current.splice(0);
|
|
217
|
+
for (const text of pending) {
|
|
218
|
+
const payload = new TextEncoder().encode(JSON.stringify({ text }));
|
|
219
|
+
room.localParticipant.publishData(payload, {
|
|
220
|
+
reliable: true,
|
|
221
|
+
topic: "chat_message",
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
186
225
|
}, []);
|
|
187
226
|
|
|
188
227
|
return {
|