@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,155 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { CheckCircle2, XCircle } from "lucide-react";
|
|
5
|
+
import { Button } from "@usereq/ui/components/button";
|
|
6
|
+
import {
|
|
7
|
+
Alert,
|
|
8
|
+
AlertDescription,
|
|
9
|
+
AlertTitle,
|
|
10
|
+
} from "@usereq/ui/components/alert";
|
|
11
|
+
import { cn } from "@usereq/ui/lib/utils";
|
|
12
|
+
import type {
|
|
13
|
+
StopConfirmationDecision,
|
|
14
|
+
StopConfirmationState,
|
|
15
|
+
} from "../stop-confirmation";
|
|
16
|
+
|
|
17
|
+
type ConfirmationContextValue = {
|
|
18
|
+
state: StopConfirmationState;
|
|
19
|
+
approvalId?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const ConfirmationContext =
|
|
23
|
+
React.createContext<ConfirmationContextValue | null>(null);
|
|
24
|
+
|
|
25
|
+
function useConfirmationContext() {
|
|
26
|
+
const context = React.useContext(ConfirmationContext);
|
|
27
|
+
if (!context) {
|
|
28
|
+
throw new Error("Confirmation components must be used within <Confirmation />");
|
|
29
|
+
}
|
|
30
|
+
return context;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type ConfirmationProps = {
|
|
34
|
+
approval?: { id: string };
|
|
35
|
+
state?: StopConfirmationState;
|
|
36
|
+
className?: string;
|
|
37
|
+
children?: React.ReactNode;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export function Confirmation({
|
|
41
|
+
approval,
|
|
42
|
+
state = "pending",
|
|
43
|
+
className,
|
|
44
|
+
children,
|
|
45
|
+
}: ConfirmationProps): React.ReactElement {
|
|
46
|
+
return (
|
|
47
|
+
<Alert className={cn("gap-2 rounded-2xl border-border/70", className)}>
|
|
48
|
+
<ConfirmationContext.Provider
|
|
49
|
+
value={{ state, approvalId: approval?.id }}
|
|
50
|
+
>
|
|
51
|
+
{children}
|
|
52
|
+
</ConfirmationContext.Provider>
|
|
53
|
+
</Alert>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function ConfirmationTitle({
|
|
58
|
+
className,
|
|
59
|
+
...props
|
|
60
|
+
}: React.ComponentProps<typeof AlertTitle>): React.ReactElement {
|
|
61
|
+
return (
|
|
62
|
+
<AlertTitle className={cn("text-sm font-semibold", className)} {...props} />
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function ConfirmationRequest({
|
|
67
|
+
className,
|
|
68
|
+
...props
|
|
69
|
+
}: React.ComponentProps<typeof AlertDescription>): React.ReactElement | null {
|
|
70
|
+
const { state } = useConfirmationContext();
|
|
71
|
+
if (state !== "pending") {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<AlertDescription
|
|
77
|
+
className={cn("text-sm leading-relaxed", className)}
|
|
78
|
+
{...props}
|
|
79
|
+
/>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function ConfirmationAccepted({
|
|
84
|
+
className,
|
|
85
|
+
children,
|
|
86
|
+
}: React.ComponentProps<"div">): React.ReactElement | null {
|
|
87
|
+
const { state } = useConfirmationContext();
|
|
88
|
+
if (state !== "accepted") {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<div
|
|
94
|
+
className={cn(
|
|
95
|
+
"flex items-center gap-2 text-sm font-medium text-emerald-600 dark:text-emerald-400",
|
|
96
|
+
className,
|
|
97
|
+
)}
|
|
98
|
+
>
|
|
99
|
+
<CheckCircle2 className="size-4 shrink-0" />
|
|
100
|
+
<span>{children ?? "Conversation stopped."}</span>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function ConfirmationRejected({
|
|
106
|
+
className,
|
|
107
|
+
children,
|
|
108
|
+
}: React.ComponentProps<"div">): React.ReactElement | null {
|
|
109
|
+
const { state } = useConfirmationContext();
|
|
110
|
+
if (state !== "rejected") {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<div
|
|
116
|
+
className={cn(
|
|
117
|
+
"flex items-center gap-2 text-sm font-medium text-rose-600 dark:text-rose-400",
|
|
118
|
+
className,
|
|
119
|
+
)}
|
|
120
|
+
>
|
|
121
|
+
<XCircle className="size-4 shrink-0" />
|
|
122
|
+
<span>{children ?? "Conversation continues."}</span>
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function ConfirmationActions({
|
|
128
|
+
className,
|
|
129
|
+
...props
|
|
130
|
+
}: React.ComponentProps<"div">): React.ReactElement | null {
|
|
131
|
+
const { state } = useConfirmationContext();
|
|
132
|
+
if (state !== "pending") {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<div
|
|
138
|
+
className={cn("flex w-full flex-wrap justify-end gap-2 pt-1", className)}
|
|
139
|
+
{...props}
|
|
140
|
+
/>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function ConfirmationAction({
|
|
145
|
+
children,
|
|
146
|
+
...props
|
|
147
|
+
}: React.ComponentProps<typeof Button>): React.ReactElement {
|
|
148
|
+
return (
|
|
149
|
+
<Button {...props}>
|
|
150
|
+
{typeof children === "string" ? children : children}
|
|
151
|
+
</Button>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export type ConfirmationDecision = StopConfirmationDecision;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Button } from "@usereq/ui/components/button";
|
|
5
|
+
import { cn } from "@usereq/ui/lib/utils";
|
|
6
|
+
import { ArrowDown } from "lucide-react";
|
|
7
|
+
import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom";
|
|
8
|
+
|
|
9
|
+
export type ConversationProps = React.ComponentProps<typeof StickToBottom>;
|
|
10
|
+
|
|
11
|
+
export function Conversation({
|
|
12
|
+
className,
|
|
13
|
+
...props
|
|
14
|
+
}: ConversationProps): React.ReactElement {
|
|
15
|
+
return (
|
|
16
|
+
<StickToBottom
|
|
17
|
+
className={cn("relative size-full overflow-y-hidden", className)}
|
|
18
|
+
initial="instant"
|
|
19
|
+
resize="smooth"
|
|
20
|
+
role="log"
|
|
21
|
+
{...props}
|
|
22
|
+
/>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type ConversationContentProps = React.ComponentProps<
|
|
27
|
+
typeof StickToBottom.Content
|
|
28
|
+
>;
|
|
29
|
+
|
|
30
|
+
export function ConversationContent({
|
|
31
|
+
className,
|
|
32
|
+
children,
|
|
33
|
+
...props
|
|
34
|
+
}: ConversationContentProps): React.ReactElement {
|
|
35
|
+
return (
|
|
36
|
+
<StickToBottom.Content
|
|
37
|
+
className={cn("flex min-h-full flex-1 flex-col gap-2 p-0", className)}
|
|
38
|
+
{...props}
|
|
39
|
+
>
|
|
40
|
+
{children}
|
|
41
|
+
</StickToBottom.Content>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type ConversationEmptyStateProps = React.ComponentProps<"div"> & {
|
|
46
|
+
icon?: React.ReactNode;
|
|
47
|
+
title?: string;
|
|
48
|
+
description?: string;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export function ConversationEmptyState({
|
|
52
|
+
icon,
|
|
53
|
+
title,
|
|
54
|
+
description,
|
|
55
|
+
className,
|
|
56
|
+
children,
|
|
57
|
+
...props
|
|
58
|
+
}: ConversationEmptyStateProps): React.ReactElement {
|
|
59
|
+
return (
|
|
60
|
+
<div
|
|
61
|
+
className={cn(
|
|
62
|
+
"flex flex-col items-center justify-center gap-3 text-center",
|
|
63
|
+
className,
|
|
64
|
+
)}
|
|
65
|
+
{...props}
|
|
66
|
+
>
|
|
67
|
+
{icon ? <div className="text-muted-foreground">{icon}</div> : null}
|
|
68
|
+
{title ? (
|
|
69
|
+
<p className="text-lg font-semibold leading-tight text-foreground">
|
|
70
|
+
{title}
|
|
71
|
+
</p>
|
|
72
|
+
) : null}
|
|
73
|
+
{description ? (
|
|
74
|
+
<p className="text-sm text-muted-foreground">{description}</p>
|
|
75
|
+
) : null}
|
|
76
|
+
{children}
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export type ConversationScrollButtonProps = React.ComponentProps<typeof Button>;
|
|
82
|
+
|
|
83
|
+
export function ConversationScrollButton({
|
|
84
|
+
className,
|
|
85
|
+
children,
|
|
86
|
+
...props
|
|
87
|
+
}: ConversationScrollButtonProps): React.ReactElement | null {
|
|
88
|
+
const { isAtBottom, scrollToBottom } = useStickToBottomContext();
|
|
89
|
+
|
|
90
|
+
if (isAtBottom) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<Button
|
|
96
|
+
type="button"
|
|
97
|
+
size="icon"
|
|
98
|
+
variant="outline"
|
|
99
|
+
className={cn(
|
|
100
|
+
"absolute bottom-3 left-1/2 z-10 -translate-x-1/2 rounded-full shadow-lg",
|
|
101
|
+
className,
|
|
102
|
+
)}
|
|
103
|
+
onClick={() => scrollToBottom()}
|
|
104
|
+
aria-label="Scroll to bottom"
|
|
105
|
+
{...props}
|
|
106
|
+
>
|
|
107
|
+
{children ?? <ArrowDown className="size-4" />}
|
|
108
|
+
</Button>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
@@ -0,0 +1,325 @@
|
|
|
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 { mermaid } from "@streamdown/mermaid";
|
|
7
|
+
import { Streamdown } from "streamdown";
|
|
8
|
+
import "streamdown/styles.css";
|
|
9
|
+
import {
|
|
10
|
+
Confirmation,
|
|
11
|
+
ConfirmationAccepted,
|
|
12
|
+
ConfirmationAction,
|
|
13
|
+
ConfirmationActions,
|
|
14
|
+
ConfirmationRejected,
|
|
15
|
+
ConfirmationRequest,
|
|
16
|
+
ConfirmationTitle,
|
|
17
|
+
} from "./confirmation";
|
|
18
|
+
import {
|
|
19
|
+
DEFAULT_STOP_CONVERSATION_PROMPT,
|
|
20
|
+
parseStopConfirmationEnvelope,
|
|
21
|
+
STOP_CONFIRMATION_KIND,
|
|
22
|
+
type StopConfirmationEnvelope,
|
|
23
|
+
} from "../stop-confirmation";
|
|
24
|
+
|
|
25
|
+
export type MessageRole = "assistant" | "user" | "system";
|
|
26
|
+
|
|
27
|
+
export interface MessageProps {
|
|
28
|
+
children?: React.ReactNode;
|
|
29
|
+
from?: MessageRole;
|
|
30
|
+
role?: MessageRole;
|
|
31
|
+
className?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function Message({
|
|
35
|
+
children,
|
|
36
|
+
from,
|
|
37
|
+
role,
|
|
38
|
+
className,
|
|
39
|
+
}: MessageProps): React.ReactElement {
|
|
40
|
+
const resolvedRole = from ?? role ?? "assistant";
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<motion.div
|
|
44
|
+
initial={{ opacity: 0, y: 8 }}
|
|
45
|
+
animate={{ opacity: 1, y: 0 }}
|
|
46
|
+
transition={{ duration: 0.2, ease: "easeOut" }}
|
|
47
|
+
className={cn(
|
|
48
|
+
"w-full flex",
|
|
49
|
+
resolvedRole === "user" ? "justify-end" : "justify-start",
|
|
50
|
+
className,
|
|
51
|
+
)}
|
|
52
|
+
>
|
|
53
|
+
{children}
|
|
54
|
+
</motion.div>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function MessageContent({
|
|
59
|
+
children,
|
|
60
|
+
className,
|
|
61
|
+
...props
|
|
62
|
+
}: React.HTMLAttributes<HTMLDivElement>): React.ReactElement {
|
|
63
|
+
return (
|
|
64
|
+
<div
|
|
65
|
+
className={cn(
|
|
66
|
+
"rounded-2xl px-3 py-2 text-sm leading-relaxed whitespace-pre-wrap",
|
|
67
|
+
className,
|
|
68
|
+
)}
|
|
69
|
+
{...props}
|
|
70
|
+
>
|
|
71
|
+
{children}
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
type StreamdownProps = React.ComponentProps<typeof Streamdown>;
|
|
77
|
+
|
|
78
|
+
export type MessageResponseProps = Omit<StreamdownProps, "children"> & {
|
|
79
|
+
className?: string;
|
|
80
|
+
children?: string;
|
|
81
|
+
onLinkClick?: (linkUrl: string) => void;
|
|
82
|
+
confirmation?: StopConfirmationEnvelope | null;
|
|
83
|
+
confirmationPrompt?: string;
|
|
84
|
+
confirmationActionsDisabled?: boolean;
|
|
85
|
+
onConfirmationDecision?: (
|
|
86
|
+
confirmationId: string,
|
|
87
|
+
accepted: boolean,
|
|
88
|
+
) => void | Promise<void>;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export function MessageResponse({
|
|
92
|
+
children = "",
|
|
93
|
+
className,
|
|
94
|
+
onLinkClick,
|
|
95
|
+
plugins,
|
|
96
|
+
mode = "static",
|
|
97
|
+
linkSafety: linkSafetyProp,
|
|
98
|
+
confirmation,
|
|
99
|
+
confirmationPrompt,
|
|
100
|
+
confirmationActionsDisabled,
|
|
101
|
+
onConfirmationDecision,
|
|
102
|
+
...props
|
|
103
|
+
}: MessageResponseProps): React.ReactElement {
|
|
104
|
+
const parsedConfirmation =
|
|
105
|
+
confirmation ?? parseStopConfirmationEnvelope(children);
|
|
106
|
+
|
|
107
|
+
if (parsedConfirmation) {
|
|
108
|
+
const confirmationId = parsedConfirmation.confirmationId;
|
|
109
|
+
const state =
|
|
110
|
+
parsedConfirmation.kind === STOP_CONFIRMATION_KIND
|
|
111
|
+
? parsedConfirmation.state
|
|
112
|
+
: parsedConfirmation.decision === "accepted"
|
|
113
|
+
? "accepted"
|
|
114
|
+
: "rejected";
|
|
115
|
+
const prompt =
|
|
116
|
+
confirmationPrompt ??
|
|
117
|
+
(parsedConfirmation.kind === STOP_CONFIRMATION_KIND
|
|
118
|
+
? parsedConfirmation.prompt
|
|
119
|
+
: DEFAULT_STOP_CONVERSATION_PROMPT);
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<Confirmation
|
|
123
|
+
approval={{ id: confirmationId }}
|
|
124
|
+
state={state}
|
|
125
|
+
className={cn("w-full", className)}
|
|
126
|
+
>
|
|
127
|
+
<ConfirmationTitle>Stop conversation</ConfirmationTitle>
|
|
128
|
+
{state === "pending" && <ConfirmationRequest>{prompt}</ConfirmationRequest>}
|
|
129
|
+
{state === "accepted" && (
|
|
130
|
+
<ConfirmationAccepted>Conversation stopped.</ConfirmationAccepted>
|
|
131
|
+
)}
|
|
132
|
+
{state === "rejected" && (
|
|
133
|
+
<ConfirmationRejected>Conversation continues.</ConfirmationRejected>
|
|
134
|
+
)}
|
|
135
|
+
{state === "pending" && onConfirmationDecision && (
|
|
136
|
+
<ConfirmationActions className="pt-2">
|
|
137
|
+
<ConfirmationAction
|
|
138
|
+
variant="outline"
|
|
139
|
+
disabled={confirmationActionsDisabled}
|
|
140
|
+
onClick={() => void onConfirmationDecision(confirmationId, false)}
|
|
141
|
+
>
|
|
142
|
+
No
|
|
143
|
+
</ConfirmationAction>
|
|
144
|
+
<ConfirmationAction
|
|
145
|
+
variant="default"
|
|
146
|
+
disabled={confirmationActionsDisabled}
|
|
147
|
+
onClick={() => void onConfirmationDecision(confirmationId, true)}
|
|
148
|
+
>
|
|
149
|
+
Yes
|
|
150
|
+
</ConfirmationAction>
|
|
151
|
+
</ConfirmationActions>
|
|
152
|
+
)}
|
|
153
|
+
</Confirmation>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const lastTrackedLinkRef = React.useRef<{
|
|
158
|
+
href: string;
|
|
159
|
+
at: number;
|
|
160
|
+
} | null>(null);
|
|
161
|
+
|
|
162
|
+
const emitLinkClick = React.useCallback(
|
|
163
|
+
(href: string | null | undefined) => {
|
|
164
|
+
const normalizedHref = href?.trim();
|
|
165
|
+
if (!normalizedHref) return;
|
|
166
|
+
|
|
167
|
+
const now = Date.now();
|
|
168
|
+
const lastTrackedLink = lastTrackedLinkRef.current;
|
|
169
|
+
if (
|
|
170
|
+
lastTrackedLink &&
|
|
171
|
+
lastTrackedLink.href === normalizedHref &&
|
|
172
|
+
now - lastTrackedLink.at < 250
|
|
173
|
+
) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
lastTrackedLinkRef.current = {
|
|
178
|
+
href: normalizedHref,
|
|
179
|
+
at: now,
|
|
180
|
+
};
|
|
181
|
+
onLinkClick?.(normalizedHref);
|
|
182
|
+
},
|
|
183
|
+
[onLinkClick],
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
const resolveAnchorFromTarget = React.useCallback((target: EventTarget | null) => {
|
|
187
|
+
if (target instanceof HTMLAnchorElement) {
|
|
188
|
+
return target;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (target instanceof Element) {
|
|
192
|
+
const anchor = target.closest("a[href]");
|
|
193
|
+
return anchor instanceof HTMLAnchorElement ? anchor : null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (target instanceof Node) {
|
|
197
|
+
const anchor = target.parentElement?.closest("a[href]");
|
|
198
|
+
return anchor instanceof HTMLAnchorElement ? anchor : null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return null;
|
|
202
|
+
}, []);
|
|
203
|
+
|
|
204
|
+
const handleClickCapture = React.useCallback(
|
|
205
|
+
(event: React.MouseEvent<HTMLDivElement>) => {
|
|
206
|
+
if (!onLinkClick) return;
|
|
207
|
+
const composedPath = event.nativeEvent.composedPath?.() ?? [];
|
|
208
|
+
const pathAnchor = composedPath.find(
|
|
209
|
+
(entry) =>
|
|
210
|
+
entry instanceof HTMLAnchorElement && entry.href?.trim().length > 0,
|
|
211
|
+
);
|
|
212
|
+
const anchor =
|
|
213
|
+
(pathAnchor instanceof HTMLAnchorElement ? pathAnchor : null) ??
|
|
214
|
+
resolveAnchorFromTarget(event.target);
|
|
215
|
+
if (!anchor) return;
|
|
216
|
+
emitLinkClick(anchor.href);
|
|
217
|
+
},
|
|
218
|
+
[emitLinkClick, onLinkClick, resolveAnchorFromTarget],
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
const handleLinkSafetyCheck = React.useCallback(
|
|
222
|
+
async (url: string): Promise<boolean> => {
|
|
223
|
+
const href = url?.trim();
|
|
224
|
+
if (linkSafetyProp?.onLinkCheck) {
|
|
225
|
+
const shouldOpenDirectly = await linkSafetyProp.onLinkCheck(url);
|
|
226
|
+
if (!shouldOpenDirectly && href) {
|
|
227
|
+
emitLinkClick(href);
|
|
228
|
+
}
|
|
229
|
+
return shouldOpenDirectly;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (href) {
|
|
233
|
+
emitLinkClick(href);
|
|
234
|
+
}
|
|
235
|
+
return false;
|
|
236
|
+
},
|
|
237
|
+
[emitLinkClick, linkSafetyProp],
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
const resolvedLinkSafety = React.useMemo(() => {
|
|
241
|
+
if (!onLinkClick) {
|
|
242
|
+
return linkSafetyProp;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (linkSafetyProp?.enabled === false) {
|
|
246
|
+
return linkSafetyProp;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
...(linkSafetyProp ?? {}),
|
|
251
|
+
enabled: linkSafetyProp?.enabled ?? true,
|
|
252
|
+
onLinkCheck: handleLinkSafetyCheck,
|
|
253
|
+
};
|
|
254
|
+
}, [handleLinkSafetyCheck, linkSafetyProp, onLinkClick]);
|
|
255
|
+
|
|
256
|
+
return (
|
|
257
|
+
<div onClickCapture={handleClickCapture}>
|
|
258
|
+
<Streamdown
|
|
259
|
+
{...props}
|
|
260
|
+
mode={mode}
|
|
261
|
+
linkSafety={resolvedLinkSafety}
|
|
262
|
+
plugins={{ mermaid, ...plugins }}
|
|
263
|
+
className={cn(
|
|
264
|
+
"text-sm leading-relaxed whitespace-normal",
|
|
265
|
+
// "[&_[data-streamdown='table-wrapper']]:my-0",
|
|
266
|
+
// "[&_[data-streamdown='table-wrapper']]:w-fit",
|
|
267
|
+
// "[&_[data-streamdown='table-wrapper']]:max-w-full",
|
|
268
|
+
// "[&_[data-streamdown='table-wrapper']]:gap-1",
|
|
269
|
+
// "[&_[data-streamdown='table-wrapper']]:p-1",
|
|
270
|
+
// "[&_[data-streamdown='table-wrapper']]:rounded-xl",
|
|
271
|
+
className,
|
|
272
|
+
)}
|
|
273
|
+
>
|
|
274
|
+
{children}
|
|
275
|
+
</Streamdown>
|
|
276
|
+
</div>
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export type AssistantMessageRevealSource = {
|
|
281
|
+
id: string;
|
|
282
|
+
content: string;
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
export type AssistantMessageRevealStep<
|
|
286
|
+
T extends AssistantMessageRevealSource = AssistantMessageRevealSource,
|
|
287
|
+
> = {
|
|
288
|
+
message: T;
|
|
289
|
+
delayMs: number;
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
export function buildAssistantMessageRevealSteps<
|
|
293
|
+
T extends AssistantMessageRevealSource,
|
|
294
|
+
>(
|
|
295
|
+
messages: T[],
|
|
296
|
+
options?: {
|
|
297
|
+
initialDelayMs?: number;
|
|
298
|
+
typingHoldMs?: number;
|
|
299
|
+
interMessageDelayMs?: number;
|
|
300
|
+
interMessageDelayJitterMs?: number;
|
|
301
|
+
},
|
|
302
|
+
): AssistantMessageRevealStep<T>[] {
|
|
303
|
+
if (messages.length === 0) {
|
|
304
|
+
return [];
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const initialDelayMs = options?.initialDelayMs ?? 0;
|
|
308
|
+
const typingHoldMs = options?.typingHoldMs ?? 1260;
|
|
309
|
+
const interMessageDelayMs = options?.interMessageDelayMs ?? 1520;
|
|
310
|
+
const jitterMs = Math.max(0, options?.interMessageDelayJitterMs ?? 100);
|
|
311
|
+
|
|
312
|
+
return messages.map((message, index) => {
|
|
313
|
+
const baseDelay =
|
|
314
|
+
index === 0 ? initialDelayMs + typingHoldMs : interMessageDelayMs;
|
|
315
|
+
const jitter =
|
|
316
|
+
index === 0 || jitterMs === 0
|
|
317
|
+
? 0
|
|
318
|
+
: Math.round((Math.random() * 2 - 1) * jitterMs);
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
message,
|
|
322
|
+
delayMs: Math.max(0, baseDelay + jitter),
|
|
323
|
+
};
|
|
324
|
+
});
|
|
325
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from "./components/chat-widget-frame";
|
|
2
|
+
export * from "./components/chat-widget-primitives";
|
|
3
|
+
export * from "./components/confirmation";
|
|
4
|
+
export * from "./components/conversation";
|
|
5
|
+
export * from "./components/message";
|
|
6
|
+
export * from "./stop-confirmation";
|
|
7
|
+
export * from "./styles/chat-widget-bubble";
|
|
8
|
+
export * from "./styles/chat-widget-box";
|
|
9
|
+
export * from "./styles/chat-widget-chatbar";
|