@usereq/widget 0.1.0
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 +166 -0
- package/dist/widget.css +2 -0
- package/dist/widget.js +3569 -0
- package/package.json +98 -0
- package/src/chat-widget/chat-widget-appearance.ts +55 -0
- package/src/chat-widget/chat-widget-defaults.tsx +45 -0
- package/src/chat-widget/components/chat-widget-frame.tsx +56 -0
- package/src/chat-widget/components/chat-widget-layout.ts +6 -0
- package/src/chat-widget/components/chat-widget-primitives.tsx +720 -0
- package/src/chat-widget/components/confirmation.tsx +155 -0
- package/src/chat-widget/components/conversation.tsx +110 -0
- package/src/chat-widget/components/message.tsx +325 -0
- package/src/chat-widget/index.ts +9 -0
- package/src/chat-widget/stop-confirmation.ts +288 -0
- package/src/chat-widget/styles/chat-widget-box.tsx +258 -0
- package/src/chat-widget/styles/chat-widget-bubble.tsx +238 -0
- package/src/chat-widget/styles/chat-widget-chatbar.tsx +248 -0
- package/src/custom-element/agent-widget-element.tsx +584 -0
- package/src/embed.ts +8 -0
- package/src/index.ts +10 -0
- package/src/register.ts +15 -0
- package/src/renderer/index.ts +6 -0
- package/src/renderer/widget-runtime.tsx +205 -0
- package/src/runtime/analytics.ts +327 -0
- package/src/runtime/api-origin.ts +29 -0
- package/src/runtime/api.ts +151 -0
- package/src/runtime/bootstrap.ts +309 -0
- package/src/runtime/messages.ts +12 -0
- package/src/runtime/session-storage.ts +32 -0
- package/src/runtime/token-renewal.ts +13 -0
- package/src/runtime/trigger-rule.ts +182 -0
- package/src/shared/analytics.ts +54 -0
- package/src/shared/shadow-css.ts +15 -0
- package/src/shared/shadow-theme.ts +85 -0
- package/src/shared/stop-confirmation.ts +20 -0
- package/src/shared/widget-config.ts +104 -0
- package/src/styles/widget.css.ts +27 -0
- package/src/types.ts +85 -0
- package/src/vite-env.d.ts +4 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
export const STOP_CONFIRMATION_KIND = "stop_confirmation" as const;
|
|
2
|
+
export const STOP_CONFIRMATION_RESULT_KIND =
|
|
3
|
+
"stop_confirmation_result" as const;
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_STOP_CONVERSATION_PROMPT =
|
|
6
|
+
"Do you want to stop this conversation?";
|
|
7
|
+
|
|
8
|
+
export type StopConfirmationState = "pending" | "accepted" | "rejected";
|
|
9
|
+
export type StopConfirmationDecision = "accepted" | "rejected";
|
|
10
|
+
|
|
11
|
+
export type StopConfirmationPromptEnvelope = {
|
|
12
|
+
kind: typeof STOP_CONFIRMATION_KIND;
|
|
13
|
+
confirmationId: string;
|
|
14
|
+
state: StopConfirmationState;
|
|
15
|
+
prompt: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type StopConfirmationResultEnvelope = {
|
|
19
|
+
kind: typeof STOP_CONFIRMATION_RESULT_KIND;
|
|
20
|
+
confirmationId: string;
|
|
21
|
+
decision: StopConfirmationDecision;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type StopConfirmationEnvelope =
|
|
25
|
+
| StopConfirmationPromptEnvelope
|
|
26
|
+
| StopConfirmationResultEnvelope;
|
|
27
|
+
|
|
28
|
+
export type StopConfirmationMessageLike = {
|
|
29
|
+
id: string;
|
|
30
|
+
content: string;
|
|
31
|
+
createdAt?: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type StopConfirmationRenderItem<T extends StopConfirmationMessageLike> =
|
|
35
|
+
| {
|
|
36
|
+
type: "message";
|
|
37
|
+
message: T;
|
|
38
|
+
}
|
|
39
|
+
| {
|
|
40
|
+
type: "confirmation";
|
|
41
|
+
message: T;
|
|
42
|
+
envelope: StopConfirmationPromptEnvelope;
|
|
43
|
+
latestEnvelope: StopConfirmationEnvelope;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
47
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function normalizeState(value: unknown): StopConfirmationState {
|
|
51
|
+
return value === "accepted" || value === "rejected" ? value : "pending";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeDecision(value: unknown): StopConfirmationDecision | null {
|
|
55
|
+
return value === "accepted" || value === "rejected" ? value : null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function normalizePrompt(value: unknown): string {
|
|
59
|
+
if (typeof value !== "string") {
|
|
60
|
+
return DEFAULT_STOP_CONVERSATION_PROMPT;
|
|
61
|
+
}
|
|
62
|
+
const trimmed = value.trim();
|
|
63
|
+
return trimmed.length > 0 ? trimmed : DEFAULT_STOP_CONVERSATION_PROMPT;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function createStopConfirmationPromptEnvelope(input: {
|
|
67
|
+
confirmationId: string;
|
|
68
|
+
prompt?: string;
|
|
69
|
+
state?: StopConfirmationState;
|
|
70
|
+
}): StopConfirmationPromptEnvelope {
|
|
71
|
+
return {
|
|
72
|
+
kind: STOP_CONFIRMATION_KIND,
|
|
73
|
+
confirmationId: input.confirmationId,
|
|
74
|
+
state: normalizeState(input.state),
|
|
75
|
+
prompt: normalizePrompt(input.prompt),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function createStopConfirmationResultEnvelope(input: {
|
|
80
|
+
confirmationId: string;
|
|
81
|
+
decision: StopConfirmationDecision;
|
|
82
|
+
}): StopConfirmationResultEnvelope {
|
|
83
|
+
return {
|
|
84
|
+
kind: STOP_CONFIRMATION_RESULT_KIND,
|
|
85
|
+
confirmationId: input.confirmationId,
|
|
86
|
+
decision: input.decision,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function encodeStopConfirmationEnvelope(
|
|
91
|
+
envelope: StopConfirmationEnvelope,
|
|
92
|
+
): string {
|
|
93
|
+
return JSON.stringify(envelope);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function encodeStopConfirmationPrompt(input: {
|
|
97
|
+
confirmationId: string;
|
|
98
|
+
prompt?: string;
|
|
99
|
+
state?: StopConfirmationState;
|
|
100
|
+
}): string {
|
|
101
|
+
return encodeStopConfirmationEnvelope(
|
|
102
|
+
createStopConfirmationPromptEnvelope(input),
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function encodeStopConfirmationResult(input: {
|
|
107
|
+
confirmationId: string;
|
|
108
|
+
decision: StopConfirmationDecision;
|
|
109
|
+
}): string {
|
|
110
|
+
return encodeStopConfirmationEnvelope(
|
|
111
|
+
createStopConfirmationResultEnvelope(input),
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function parseStopConfirmationEnvelope(
|
|
116
|
+
content: string | null | undefined,
|
|
117
|
+
): StopConfirmationEnvelope | null {
|
|
118
|
+
if (typeof content !== "string" || content.trim() === "") {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let parsed: unknown;
|
|
123
|
+
try {
|
|
124
|
+
parsed = JSON.parse(content);
|
|
125
|
+
} catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!isRecord(parsed) || typeof parsed.kind !== "string") {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (parsed.kind === STOP_CONFIRMATION_KIND) {
|
|
134
|
+
if (typeof parsed.confirmationId !== "string" || !parsed.confirmationId.trim()) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
kind: STOP_CONFIRMATION_KIND,
|
|
140
|
+
confirmationId: parsed.confirmationId,
|
|
141
|
+
state: normalizeState(parsed.state),
|
|
142
|
+
prompt: normalizePrompt(parsed.prompt),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (parsed.kind === STOP_CONFIRMATION_RESULT_KIND) {
|
|
147
|
+
const decision = normalizeDecision(parsed.decision);
|
|
148
|
+
if (
|
|
149
|
+
typeof parsed.confirmationId !== "string" ||
|
|
150
|
+
!parsed.confirmationId.trim() ||
|
|
151
|
+
decision == null
|
|
152
|
+
) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
kind: STOP_CONFIRMATION_RESULT_KIND,
|
|
158
|
+
confirmationId: parsed.confirmationId,
|
|
159
|
+
decision,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function foldStopConfirmationMessages<T extends StopConfirmationMessageLike>(
|
|
167
|
+
messages: T[],
|
|
168
|
+
): StopConfirmationRenderItem<T>[] {
|
|
169
|
+
const byConfirmationId = new Map<
|
|
170
|
+
string,
|
|
171
|
+
{
|
|
172
|
+
firstIndex: number;
|
|
173
|
+
latestIndex: number;
|
|
174
|
+
latestEnvelope: StopConfirmationEnvelope;
|
|
175
|
+
}
|
|
176
|
+
>();
|
|
177
|
+
|
|
178
|
+
messages.forEach((message, index) => {
|
|
179
|
+
const envelope = parseStopConfirmationEnvelope(message.content);
|
|
180
|
+
if (!envelope) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const entry = byConfirmationId.get(envelope.confirmationId);
|
|
185
|
+
const sortValue = safeCreatedAtValue(message.createdAt, index);
|
|
186
|
+
if (!entry) {
|
|
187
|
+
byConfirmationId.set(envelope.confirmationId, {
|
|
188
|
+
firstIndex: index,
|
|
189
|
+
latestIndex: index,
|
|
190
|
+
latestEnvelope: envelope,
|
|
191
|
+
});
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const currentSortValue = safeCreatedAtValue(
|
|
196
|
+
messages[entry.latestIndex]?.createdAt,
|
|
197
|
+
entry.latestIndex,
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
if (sortValue >= currentSortValue) {
|
|
201
|
+
byConfirmationId.set(envelope.confirmationId, {
|
|
202
|
+
firstIndex: entry.firstIndex,
|
|
203
|
+
latestIndex: index,
|
|
204
|
+
latestEnvelope: envelope,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const renderItems: StopConfirmationRenderItem<T>[] = [];
|
|
210
|
+
|
|
211
|
+
messages.forEach((message, index) => {
|
|
212
|
+
const envelope = parseStopConfirmationEnvelope(message.content);
|
|
213
|
+
if (!envelope) {
|
|
214
|
+
renderItems.push({ type: "message", message });
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const entry = byConfirmationId.get(envelope.confirmationId);
|
|
219
|
+
if (!entry || entry.firstIndex !== index) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const promptEnvelope =
|
|
224
|
+
entry.latestEnvelope.kind === STOP_CONFIRMATION_KIND
|
|
225
|
+
? entry.latestEnvelope
|
|
226
|
+
: createStopConfirmationPromptEnvelope({
|
|
227
|
+
confirmationId: entry.latestEnvelope.confirmationId,
|
|
228
|
+
prompt: DEFAULT_STOP_CONVERSATION_PROMPT,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
renderItems.push({
|
|
232
|
+
type: "confirmation",
|
|
233
|
+
message,
|
|
234
|
+
envelope: promptEnvelope,
|
|
235
|
+
latestEnvelope: entry.latestEnvelope,
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
return renderItems;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function getPendingStopConfirmationId<T extends StopConfirmationMessageLike>(
|
|
243
|
+
messages: T[],
|
|
244
|
+
): string | null {
|
|
245
|
+
const latestByConfirmationId = new Map<
|
|
246
|
+
string,
|
|
247
|
+
{
|
|
248
|
+
sortValue: number;
|
|
249
|
+
envelope: StopConfirmationEnvelope;
|
|
250
|
+
}
|
|
251
|
+
>();
|
|
252
|
+
|
|
253
|
+
messages.forEach((message, index) => {
|
|
254
|
+
const envelope = parseStopConfirmationEnvelope(message.content);
|
|
255
|
+
if (!envelope) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const sortValue = safeCreatedAtValue(message.createdAt, index);
|
|
260
|
+
const current = latestByConfirmationId.get(envelope.confirmationId);
|
|
261
|
+
if (!current || sortValue >= current.sortValue) {
|
|
262
|
+
latestByConfirmationId.set(envelope.confirmationId, {
|
|
263
|
+
sortValue,
|
|
264
|
+
envelope,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
for (const [confirmationId, entry] of latestByConfirmationId.entries()) {
|
|
270
|
+
if (
|
|
271
|
+
entry.envelope.kind === STOP_CONFIRMATION_KIND &&
|
|
272
|
+
entry.envelope.state === "pending"
|
|
273
|
+
) {
|
|
274
|
+
return confirmationId;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function safeCreatedAtValue(value: string | undefined, fallbackIndex: number): number {
|
|
282
|
+
if (typeof value !== "string") {
|
|
283
|
+
return fallbackIndex;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const timestamp = Date.parse(value);
|
|
287
|
+
return Number.isNaN(timestamp) ? fallbackIndex : timestamp;
|
|
288
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { motion } from "motion/react";
|
|
5
|
+
import { cn } from "@usereq/ui/lib/utils";
|
|
6
|
+
import {
|
|
7
|
+
Avatar,
|
|
8
|
+
AvatarFallback,
|
|
9
|
+
AvatarImage,
|
|
10
|
+
} from "@usereq/ui/components/avatar";
|
|
11
|
+
import { Button } from "@usereq/ui/components/button";
|
|
12
|
+
import { ChevronDown } from "lucide-react";
|
|
13
|
+
import {
|
|
14
|
+
getChatWidgetAvatarStyle,
|
|
15
|
+
getChatWidgetGradientStyle,
|
|
16
|
+
} from "../chat-widget-appearance";
|
|
17
|
+
import {
|
|
18
|
+
DEFAULT_AGENT_NAME_BOX,
|
|
19
|
+
DEFAULT_POWERED_BY,
|
|
20
|
+
DEFAULT_SPOTLIGHT_MESSAGES,
|
|
21
|
+
DEFAULT_START_CHAT_LABEL,
|
|
22
|
+
} from "../chat-widget-defaults";
|
|
23
|
+
import {
|
|
24
|
+
ChatWidgetComposer,
|
|
25
|
+
ChatWidgetHeader,
|
|
26
|
+
ChatWidgetMessages,
|
|
27
|
+
ChatWidgetPanel,
|
|
28
|
+
ChatWidgetPoweredBy,
|
|
29
|
+
useIsChatWidgetFullscreenViewport,
|
|
30
|
+
useSpotlightTyping,
|
|
31
|
+
useStableSpotlightMessages,
|
|
32
|
+
type ChatWidgetDataProps,
|
|
33
|
+
} from "../components/chat-widget-primitives";
|
|
34
|
+
|
|
35
|
+
const PANEL_WIDTH = 360;
|
|
36
|
+
const PANEL_GAP = 12;
|
|
37
|
+
const LAUNCHER_CARD_WIDTH = 280;
|
|
38
|
+
const LAUNCHER_CARD_HEIGHT = 120;
|
|
39
|
+
/** When panel is open, launcher is just the close button (square). */
|
|
40
|
+
const LAUNCHER_BUTTON_SIZE = 56;
|
|
41
|
+
|
|
42
|
+
const panelTransition = {
|
|
43
|
+
type: "spring" as const,
|
|
44
|
+
stiffness: 400,
|
|
45
|
+
damping: 35,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* ChatWidgetBox — closed: intro message + Start chat button; open: same panel as Bubble + chevron to close.
|
|
50
|
+
*/
|
|
51
|
+
export function ChatWidgetBox({
|
|
52
|
+
agentName = DEFAULT_AGENT_NAME_BOX,
|
|
53
|
+
agentAvatarUrl,
|
|
54
|
+
appearance,
|
|
55
|
+
spotlightMessages = [],
|
|
56
|
+
messages = [],
|
|
57
|
+
poweredBy = DEFAULT_POWERED_BY,
|
|
58
|
+
headerStatus = "Online",
|
|
59
|
+
errorMessage,
|
|
60
|
+
status = "idle",
|
|
61
|
+
sendDisabled = false,
|
|
62
|
+
startChatLabel = DEFAULT_START_CHAT_LABEL,
|
|
63
|
+
onSendMessage,
|
|
64
|
+
isTyping,
|
|
65
|
+
onSuggestion,
|
|
66
|
+
onLauncherClick,
|
|
67
|
+
className,
|
|
68
|
+
open,
|
|
69
|
+
defaultOpen = false,
|
|
70
|
+
onOpenChange,
|
|
71
|
+
previewOnly = false,
|
|
72
|
+
onStopConversation,
|
|
73
|
+
stopDisabled,
|
|
74
|
+
onConfirmationDecision,
|
|
75
|
+
onLinkClick,
|
|
76
|
+
emptyAction,
|
|
77
|
+
portalContainer,
|
|
78
|
+
}: ChatWidgetDataProps & {
|
|
79
|
+
/** Button label on closed card */
|
|
80
|
+
startChatLabel?: string;
|
|
81
|
+
open?: boolean;
|
|
82
|
+
className?: string;
|
|
83
|
+
defaultOpen?: boolean;
|
|
84
|
+
onOpenChange?: (open: boolean) => void;
|
|
85
|
+
/** When true, launcher only (no open state); for layout editor. */
|
|
86
|
+
previewOnly?: boolean;
|
|
87
|
+
portalContainer?: React.ComponentProps<
|
|
88
|
+
typeof ChatWidgetPanel
|
|
89
|
+
>["portalContainer"];
|
|
90
|
+
}) {
|
|
91
|
+
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(
|
|
92
|
+
previewOnly ? false : defaultOpen,
|
|
93
|
+
);
|
|
94
|
+
const [draftValue, setDraftValue] = React.useState("");
|
|
95
|
+
const effectiveOpen = previewOnly ? false : (open ?? uncontrolledOpen);
|
|
96
|
+
const handleOpenChange = React.useCallback(
|
|
97
|
+
(nextOpen: boolean) => {
|
|
98
|
+
if (onOpenChange) {
|
|
99
|
+
onOpenChange(nextOpen);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
setUncontrolledOpen(nextOpen);
|
|
103
|
+
},
|
|
104
|
+
[onOpenChange],
|
|
105
|
+
);
|
|
106
|
+
const stableSpotlightMessages = useStableSpotlightMessages(spotlightMessages);
|
|
107
|
+
const spotlightForTyping =
|
|
108
|
+
stableSpotlightMessages.length > 0
|
|
109
|
+
? stableSpotlightMessages
|
|
110
|
+
: [...DEFAULT_SPOTLIGHT_MESSAGES];
|
|
111
|
+
const introTextTyping = useSpotlightTyping(spotlightForTyping);
|
|
112
|
+
const introText = introTextTyping || spotlightForTyping[0] || DEFAULT_SPOTLIGHT_MESSAGES[0];
|
|
113
|
+
|
|
114
|
+
const launcherHeight = effectiveOpen
|
|
115
|
+
? LAUNCHER_BUTTON_SIZE
|
|
116
|
+
: LAUNCHER_CARD_HEIGHT;
|
|
117
|
+
const rootWidth = effectiveOpen ? LAUNCHER_BUTTON_SIZE : LAUNCHER_CARD_WIDTH;
|
|
118
|
+
const isFullscreenViewport = useIsChatWidgetFullscreenViewport();
|
|
119
|
+
const hideLauncher = isFullscreenViewport && effectiveOpen;
|
|
120
|
+
const closeButtonStyle: React.CSSProperties = {
|
|
121
|
+
width: LAUNCHER_BUTTON_SIZE,
|
|
122
|
+
height: LAUNCHER_BUTTON_SIZE,
|
|
123
|
+
...(appearance ? getChatWidgetGradientStyle(appearance) : {}),
|
|
124
|
+
pointerEvents: effectiveOpen ? "auto" : "none",
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<div
|
|
129
|
+
className={cn("relative", className)}
|
|
130
|
+
style={{ width: rootWidth, height: launcherHeight }}
|
|
131
|
+
>
|
|
132
|
+
{/* Chat panel: full-screen on mobile, floating on desktop (hidden when previewOnly) */}
|
|
133
|
+
{!previewOnly && (
|
|
134
|
+
<ChatWidgetPanel
|
|
135
|
+
open={effectiveOpen}
|
|
136
|
+
onOpenChange={handleOpenChange}
|
|
137
|
+
portalContainer={portalContainer}
|
|
138
|
+
desktopClassName="absolute right-0 overflow-hidden rounded-2xl border bg-background shadow-xl"
|
|
139
|
+
desktopStyle={{
|
|
140
|
+
width: PANEL_WIDTH,
|
|
141
|
+
bottom: launcherHeight + PANEL_GAP,
|
|
142
|
+
}}
|
|
143
|
+
>
|
|
144
|
+
<div className="flex shrink-0 border-b p-4">
|
|
145
|
+
<ChatWidgetHeader
|
|
146
|
+
agentName={agentName}
|
|
147
|
+
agentAvatarUrl={agentAvatarUrl}
|
|
148
|
+
appearance={appearance}
|
|
149
|
+
statusText={headerStatus}
|
|
150
|
+
onStopConversation={onStopConversation ?? undefined}
|
|
151
|
+
stopDisabled={stopDisabled}
|
|
152
|
+
onClose={() => handleOpenChange(false)}
|
|
153
|
+
/>
|
|
154
|
+
</div>
|
|
155
|
+
<div className="flex min-h-0 flex-1 flex-col overflow-hidden p-4 md:max-h-90">
|
|
156
|
+
<ChatWidgetMessages
|
|
157
|
+
messages={messages}
|
|
158
|
+
isTyping={isTyping ?? status === "sending"}
|
|
159
|
+
status={status}
|
|
160
|
+
errorMessage={errorMessage}
|
|
161
|
+
appearance={appearance}
|
|
162
|
+
onConfirmationDecision={onConfirmationDecision}
|
|
163
|
+
onLinkClick={onLinkClick}
|
|
164
|
+
onSuggestion={(value: string) => {
|
|
165
|
+
setDraftValue(value);
|
|
166
|
+
onSuggestion?.(value);
|
|
167
|
+
}}
|
|
168
|
+
emptyAction={emptyAction}
|
|
169
|
+
/>
|
|
170
|
+
</div>
|
|
171
|
+
{!(emptyAction && messages.length === 0 && !isTyping) && (
|
|
172
|
+
<div className="border-t p-3 shrink-0">
|
|
173
|
+
<ChatWidgetComposer
|
|
174
|
+
value={draftValue}
|
|
175
|
+
onChange={setDraftValue}
|
|
176
|
+
onSubmit={onSendMessage}
|
|
177
|
+
appearance={appearance}
|
|
178
|
+
disabled={sendDisabled || status === "booting"}
|
|
179
|
+
/>
|
|
180
|
+
<ChatWidgetPoweredBy text={poweredBy} className="mt-2" />
|
|
181
|
+
</div>
|
|
182
|
+
)}
|
|
183
|
+
</ChatWidgetPanel>
|
|
184
|
+
)}
|
|
185
|
+
|
|
186
|
+
{/* Closed: intro message + Start chat button */}
|
|
187
|
+
{!effectiveOpen && (
|
|
188
|
+
<motion.div
|
|
189
|
+
className="absolute bottom-0 right-0 flex flex-col gap-4 rounded-2xl border bg-background p-4 shadow-xl"
|
|
190
|
+
style={{ width: LAUNCHER_CARD_WIDTH, height: LAUNCHER_CARD_HEIGHT }}
|
|
191
|
+
initial={false}
|
|
192
|
+
animate={{ opacity: 1 }}
|
|
193
|
+
>
|
|
194
|
+
<div className="flex flex-1 items-start gap-2">
|
|
195
|
+
<Avatar
|
|
196
|
+
className="size-9 shrink-0"
|
|
197
|
+
style={
|
|
198
|
+
appearance ? getChatWidgetAvatarStyle(appearance) : undefined
|
|
199
|
+
}
|
|
200
|
+
>
|
|
201
|
+
{agentAvatarUrl ? (
|
|
202
|
+
<AvatarImage src={agentAvatarUrl} alt={agentName} />
|
|
203
|
+
) : null}
|
|
204
|
+
<AvatarFallback className="bg-transparent text-white text-xs">
|
|
205
|
+
{agentName.slice(0, 2).toUpperCase()}
|
|
206
|
+
</AvatarFallback>
|
|
207
|
+
</Avatar>
|
|
208
|
+
{introText ? (
|
|
209
|
+
<p className="min-w-0 flex-1 text-sm leading-snug text-foreground">
|
|
210
|
+
{introText}
|
|
211
|
+
</p>
|
|
212
|
+
) : null}
|
|
213
|
+
</div>
|
|
214
|
+
<Button
|
|
215
|
+
className={cn(
|
|
216
|
+
"w-full rounded-full",
|
|
217
|
+
previewOnly && "pointer-events-none",
|
|
218
|
+
)}
|
|
219
|
+
onClick={
|
|
220
|
+
previewOnly
|
|
221
|
+
? undefined
|
|
222
|
+
: () => {
|
|
223
|
+
onLauncherClick?.();
|
|
224
|
+
handleOpenChange(true);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
style={
|
|
228
|
+
appearance ? getChatWidgetGradientStyle(appearance) : undefined
|
|
229
|
+
}
|
|
230
|
+
>
|
|
231
|
+
{appearance?.actionText?.trim() || startChatLabel}
|
|
232
|
+
</Button>
|
|
233
|
+
</motion.div>
|
|
234
|
+
)}
|
|
235
|
+
|
|
236
|
+
{/* Open: square close button (matches launcher width/height when open) */}
|
|
237
|
+
{!previewOnly && !hideLauncher && (
|
|
238
|
+
<motion.button
|
|
239
|
+
type="button"
|
|
240
|
+
onClick={() => handleOpenChange(false)}
|
|
241
|
+
className="absolute bottom-0 right-0 flex items-center justify-center rounded-full text-primary-foreground shadow-lg"
|
|
242
|
+
style={closeButtonStyle as any}
|
|
243
|
+
aria-label="Close chat"
|
|
244
|
+
initial={false}
|
|
245
|
+
animate={{
|
|
246
|
+
opacity: effectiveOpen ? 1 : 0,
|
|
247
|
+
visibility: effectiveOpen ? "visible" : "hidden",
|
|
248
|
+
}}
|
|
249
|
+
transition={panelTransition}
|
|
250
|
+
whileHover={effectiveOpen ? { scale: 1.02 } : undefined}
|
|
251
|
+
whileTap={effectiveOpen ? { scale: 0.98 } : undefined}
|
|
252
|
+
>
|
|
253
|
+
<ChevronDown className="size-6" />
|
|
254
|
+
</motion.button>
|
|
255
|
+
)}
|
|
256
|
+
</div>
|
|
257
|
+
);
|
|
258
|
+
}
|