@elizaos/client 1.5.5-alpha.10
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/LICENSE +21 -0
- package/README.md +350 -0
- package/dist/assets/empty-module-CLMscLYw.js +1 -0
- package/dist/assets/main-BBZ_3lkn.css +5999 -0
- package/dist/assets/main-C5zNUkXH.js +7 -0
- package/dist/assets/main-Dz64ENQg.js +614 -0
- package/dist/assets/react-vendor-DM5m98rr.js +545 -0
- package/dist/assets/ui-vendor-BQCqNqg0.js +1 -0
- package/dist/elizaos-avatar.png +0 -0
- package/dist/elizaos-icon.png +0 -0
- package/dist/elizaos-logo-light.png +0 -0
- package/dist/elizaos.webp +0 -0
- package/dist/favicon.ico +0 -0
- package/dist/images/agents/agent1.png +0 -0
- package/dist/images/agents/agent2.png +0 -0
- package/dist/images/agents/agent3.png +0 -0
- package/dist/images/agents/agent4.png +0 -0
- package/dist/images/agents/agent5.png +0 -0
- package/dist/index.html +14 -0
- package/index.html +24 -0
- package/package.json +159 -0
- package/postcss.config.js +3 -0
- package/public/elizaos-avatar.png +0 -0
- package/public/elizaos-icon.png +0 -0
- package/public/elizaos-logo-light.png +0 -0
- package/public/elizaos.webp +0 -0
- package/public/favicon.ico +0 -0
- package/public/images/agents/agent1.png +0 -0
- package/public/images/agents/agent2.png +0 -0
- package/public/images/agents/agent3.png +0 -0
- package/public/images/agents/agent4.png +0 -0
- package/public/images/agents/agent5.png +0 -0
- package/src/App.tsx +222 -0
- package/src/components/AgentDetailsPanel.tsx +147 -0
- package/src/components/ChatInputArea.tsx +196 -0
- package/src/components/ChatMessageListComponent.tsx +139 -0
- package/src/components/actionTool.tsx +186 -0
- package/src/components/add-agent-card.tsx +77 -0
- package/src/components/agent-action-viewer.tsx +816 -0
- package/src/components/agent-avatar-stack.tsx +121 -0
- package/src/components/agent-card.cy.tsx +259 -0
- package/src/components/agent-card.tsx +177 -0
- package/src/components/agent-creator.tsx +142 -0
- package/src/components/agent-log-viewer.tsx +645 -0
- package/src/components/agent-memory-edit-overlay.tsx +461 -0
- package/src/components/agent-memory-viewer.tsx +504 -0
- package/src/components/agent-settings.tsx +270 -0
- package/src/components/agent-sidebar.tsx +178 -0
- package/src/components/api-key-dialog.tsx +113 -0
- package/src/components/app-sidebar.tsx +685 -0
- package/src/components/array-input.tsx +116 -0
- package/src/components/audio-recorder.tsx +292 -0
- package/src/components/avatar-panel.tsx +141 -0
- package/src/components/character-form.tsx +1138 -0
- package/src/components/chat.tsx +1813 -0
- package/src/components/combobox.tsx +187 -0
- package/src/components/confirmation-dialog.tsx +59 -0
- package/src/components/connection-error-banner.tsx +101 -0
- package/src/components/connection-status.cy.tsx +73 -0
- package/src/components/connection-status.tsx +155 -0
- package/src/components/copy-button.tsx +35 -0
- package/src/components/delete-button.tsx +24 -0
- package/src/components/env-settings.tsx +261 -0
- package/src/components/group-card.tsx +160 -0
- package/src/components/group-panel.tsx +543 -0
- package/src/components/input-copy.tsx +21 -0
- package/src/components/logs-page.tsx +41 -0
- package/src/components/media-content.tsx +385 -0
- package/src/components/memory-graph.tsx +170 -0
- package/src/components/missing-secrets-dialog.tsx +72 -0
- package/src/components/onboarding-tour.tsx +247 -0
- package/src/components/page-title.tsx +8 -0
- package/src/components/plugins-panel.tsx +383 -0
- package/src/components/profile-card.tsx +66 -0
- package/src/components/profile-overlay.tsx +283 -0
- package/src/components/retry-button.tsx +28 -0
- package/src/components/secret-panel.tsx +1505 -0
- package/src/components/server-management.tsx +264 -0
- package/src/components/split-button.tsx +148 -0
- package/src/components/stop-agent-button.tsx +99 -0
- package/src/components/ui/alert-dialog.cy.tsx +333 -0
- package/src/components/ui/alert-dialog.tsx +115 -0
- package/src/components/ui/alert.tsx +49 -0
- package/src/components/ui/avatar.cy.tsx +180 -0
- package/src/components/ui/avatar.tsx +57 -0
- package/src/components/ui/badge.cy.tsx +146 -0
- package/src/components/ui/badge.tsx +43 -0
- package/src/components/ui/button.cy.tsx +177 -0
- package/src/components/ui/button.tsx +56 -0
- package/src/components/ui/card.cy.tsx +160 -0
- package/src/components/ui/card.tsx +73 -0
- package/src/components/ui/chat/animated-markdown.tsx +59 -0
- package/src/components/ui/chat/chat-bubble.tsx +178 -0
- package/src/components/ui/chat/chat-container.tsx +51 -0
- package/src/components/ui/chat/chat-input.cy.tsx +169 -0
- package/src/components/ui/chat/chat-input.tsx +47 -0
- package/src/components/ui/chat/chat-message-list.tsx +61 -0
- package/src/components/ui/chat/chat-tts-button.tsx +199 -0
- package/src/components/ui/chat/code-block.tsx +79 -0
- package/src/components/ui/chat/expandable-chat.tsx +131 -0
- package/src/components/ui/chat/hooks/useAutoScroll.ts +86 -0
- package/src/components/ui/chat/markdown.tsx +209 -0
- package/src/components/ui/chat/message-loading.tsx +48 -0
- package/src/components/ui/checkbox.cy.tsx +170 -0
- package/src/components/ui/checkbox.tsx +30 -0
- package/src/components/ui/collapsible.cy.tsx +283 -0
- package/src/components/ui/collapsible.tsx +9 -0
- package/src/components/ui/command.cy.tsx +313 -0
- package/src/components/ui/command.tsx +143 -0
- package/src/components/ui/dialog.cy.tsx +279 -0
- package/src/components/ui/dialog.tsx +104 -0
- package/src/components/ui/dropdown-menu.cy.tsx +273 -0
- package/src/components/ui/dropdown-menu.tsx +281 -0
- package/src/components/ui/input.cy.tsx +82 -0
- package/src/components/ui/input.tsx +27 -0
- package/src/components/ui/label.cy.tsx +157 -0
- package/src/components/ui/label.tsx +19 -0
- package/src/components/ui/resizable.tsx +42 -0
- package/src/components/ui/scroll-area.cy.tsx +242 -0
- package/src/components/ui/scroll-area.tsx +46 -0
- package/src/components/ui/select.cy.tsx +277 -0
- package/src/components/ui/select.tsx +155 -0
- package/src/components/ui/separator.cy.tsx +145 -0
- package/src/components/ui/separator.tsx +29 -0
- package/src/components/ui/sheet.cy.tsx +324 -0
- package/src/components/ui/sheet.tsx +119 -0
- package/src/components/ui/sidebar.tsx +734 -0
- package/src/components/ui/skeleton.cy.tsx +149 -0
- package/src/components/ui/skeleton.tsx +17 -0
- package/src/components/ui/split-button.cy.tsx +274 -0
- package/src/components/ui/split-button.tsx +112 -0
- package/src/components/ui/switch.tsx +28 -0
- package/src/components/ui/tabs.cy.tsx +271 -0
- package/src/components/ui/tabs.tsx +53 -0
- package/src/components/ui/textarea.cy.tsx +136 -0
- package/src/components/ui/textarea.tsx +26 -0
- package/src/components/ui/toast.cy.tsx +209 -0
- package/src/components/ui/toast.tsx +126 -0
- package/src/components/ui/toaster.tsx +29 -0
- package/src/components/ui/tooltip.cy.tsx +244 -0
- package/src/components/ui/tooltip.tsx +30 -0
- package/src/config/agent-templates.ts +349 -0
- package/src/config/voice-models.ts +181 -0
- package/src/constants.ts +23 -0
- package/src/context/AuthContext.tsx +44 -0
- package/src/context/ConnectionContext.tsx +194 -0
- package/src/entry.tsx +9 -0
- package/src/hooks/__tests__/use-agent-tab-state.test.ts +137 -0
- package/src/hooks/__tests__/use-agent-update.test.tsx +250 -0
- package/src/hooks/__tests__/use-character-convert.test.ts +102 -0
- package/src/hooks/__tests__/use-panel-width-state.test.ts +243 -0
- package/src/hooks/__tests__/use-sidebar-state.test.ts +117 -0
- package/src/hooks/use-agent-management.ts +130 -0
- package/src/hooks/use-agent-tab-state.ts +74 -0
- package/src/hooks/use-agent-update.ts +469 -0
- package/src/hooks/use-character-convert.ts +138 -0
- package/src/hooks/use-confirmation.ts +55 -0
- package/src/hooks/use-delete-agent.ts +123 -0
- package/src/hooks/use-dm-channels.ts +198 -0
- package/src/hooks/use-elevenlabs-voices.ts +83 -0
- package/src/hooks/use-file-upload.ts +224 -0
- package/src/hooks/use-mobile.tsx +19 -0
- package/src/hooks/use-onboarding.tsx +49 -0
- package/src/hooks/use-panel-width-state.ts +147 -0
- package/src/hooks/use-partial-update.ts +288 -0
- package/src/hooks/use-plugin-details.ts +462 -0
- package/src/hooks/use-plugins.ts +119 -0
- package/src/hooks/use-query-hooks.ts +1263 -0
- package/src/hooks/use-server-agents.ts +62 -0
- package/src/hooks/use-server-version.tsx +47 -0
- package/src/hooks/use-sidebar-state.ts +50 -0
- package/src/hooks/use-socket-chat.ts +264 -0
- package/src/hooks/use-toast.ts +260 -0
- package/src/hooks/use-version.tsx +64 -0
- package/src/index.css +146 -0
- package/src/lib/api-client-config.ts +53 -0
- package/src/lib/api-type-mappers.ts +196 -0
- package/src/lib/export-utils.ts +123 -0
- package/src/lib/logger.ts +19 -0
- package/src/lib/media-utils.ts +170 -0
- package/src/lib/pca.test.ts +17 -0
- package/src/lib/pca.ts +52 -0
- package/src/lib/socketio-manager.ts +664 -0
- package/src/lib/utils.ts +168 -0
- package/src/main.tsx +16 -0
- package/src/mocks/empty-module.ts +12 -0
- package/src/mocks/node-module.ts +57 -0
- package/src/polyfills.ts +37 -0
- package/src/routes/agent-detail.tsx +30 -0
- package/src/routes/agent-list.tsx +27 -0
- package/src/routes/agent-settings.tsx +48 -0
- package/src/routes/character-detail.tsx +52 -0
- package/src/routes/character-form.tsx +79 -0
- package/src/routes/character-list.tsx +38 -0
- package/src/routes/chat.tsx +128 -0
- package/src/routes/createAgent.tsx +13 -0
- package/src/routes/group-new.tsx +50 -0
- package/src/routes/group.tsx +29 -0
- package/src/routes/home.tsx +218 -0
- package/src/routes/not-found.tsx +71 -0
- package/src/test/setup.ts +154 -0
- package/src/types/crypto-browserify.d.ts +4 -0
- package/src/types/index.ts +13 -0
- package/src/types/rooms.ts +8 -0
- package/src/types.ts +84 -0
- package/src/vite-env.d.ts +40 -0
- package/tailwind.config.ts +90 -0
- package/tsconfig.json +10 -0
- package/vite.config.ts +102 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
import { cn } from '@/lib/utils';
|
|
4
|
+
|
|
5
|
+
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
6
|
+
({ className, ...props }, ref) => (
|
|
7
|
+
<div
|
|
8
|
+
ref={ref}
|
|
9
|
+
className={cn('rounded-lg border bg-card text-card-foreground shadow-sm', className)}
|
|
10
|
+
data-slot="card"
|
|
11
|
+
{...props}
|
|
12
|
+
/>
|
|
13
|
+
)
|
|
14
|
+
);
|
|
15
|
+
Card.displayName = 'Card';
|
|
16
|
+
|
|
17
|
+
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
18
|
+
({ className, ...props }, ref) => (
|
|
19
|
+
<div
|
|
20
|
+
ref={ref}
|
|
21
|
+
className={cn('flex flex-col space-y-1.5 bg-black/50 rounded-t-lg p-2', className)}
|
|
22
|
+
data-slot="card-header"
|
|
23
|
+
{...props}
|
|
24
|
+
/>
|
|
25
|
+
)
|
|
26
|
+
);
|
|
27
|
+
CardHeader.displayName = 'CardHeader';
|
|
28
|
+
|
|
29
|
+
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
|
|
30
|
+
({ className, ...props }, ref) => (
|
|
31
|
+
<h3
|
|
32
|
+
ref={ref}
|
|
33
|
+
className={cn('text-2xl font-semibold leading-none tracking-tight', className)}
|
|
34
|
+
data-slot="card-title"
|
|
35
|
+
{...props}
|
|
36
|
+
/>
|
|
37
|
+
)
|
|
38
|
+
);
|
|
39
|
+
CardTitle.displayName = 'CardTitle';
|
|
40
|
+
|
|
41
|
+
const CardDescription = React.forwardRef<
|
|
42
|
+
HTMLParagraphElement,
|
|
43
|
+
React.HTMLAttributes<HTMLParagraphElement>
|
|
44
|
+
>(({ className, ...props }, ref) => (
|
|
45
|
+
<p
|
|
46
|
+
ref={ref}
|
|
47
|
+
className={cn('text-sm text-muted-foreground', className)}
|
|
48
|
+
data-slot="card-description"
|
|
49
|
+
{...props}
|
|
50
|
+
/>
|
|
51
|
+
));
|
|
52
|
+
CardDescription.displayName = 'CardDescription';
|
|
53
|
+
|
|
54
|
+
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
55
|
+
({ className, ...props }, ref) => (
|
|
56
|
+
<div ref={ref} className={cn('p-6 pt-0', className)} data-slot="card-content" {...props} />
|
|
57
|
+
)
|
|
58
|
+
);
|
|
59
|
+
CardContent.displayName = 'CardContent';
|
|
60
|
+
|
|
61
|
+
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
62
|
+
({ className, ...props }, ref) => (
|
|
63
|
+
<div
|
|
64
|
+
ref={ref}
|
|
65
|
+
className={cn('flex items-center p-6 pt-0', className)}
|
|
66
|
+
data-slot="card-footer"
|
|
67
|
+
{...props}
|
|
68
|
+
/>
|
|
69
|
+
)
|
|
70
|
+
);
|
|
71
|
+
CardFooter.displayName = 'CardFooter';
|
|
72
|
+
|
|
73
|
+
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { Markdown } from './markdown';
|
|
5
|
+
|
|
6
|
+
interface AnimatedMarkdownProps {
|
|
7
|
+
children: string;
|
|
8
|
+
className?: string;
|
|
9
|
+
variant?: 'user' | 'agent';
|
|
10
|
+
shouldAnimate?: boolean;
|
|
11
|
+
messageId?: string;
|
|
12
|
+
maxDurationMs?: number; // Optional prop to control cap duration
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const AnimatedMarkdown: React.FC<AnimatedMarkdownProps> = ({
|
|
16
|
+
children,
|
|
17
|
+
className,
|
|
18
|
+
variant = 'agent',
|
|
19
|
+
shouldAnimate = false,
|
|
20
|
+
messageId,
|
|
21
|
+
maxDurationMs = 10000,
|
|
22
|
+
}) => {
|
|
23
|
+
const [visibleText, setVisibleText] = React.useState(shouldAnimate ? '' : children);
|
|
24
|
+
|
|
25
|
+
React.useEffect(() => {
|
|
26
|
+
if (!shouldAnimate || !children.trim()) {
|
|
27
|
+
setVisibleText(children);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const safeDuration = Math.max(1000, maxDurationMs);
|
|
32
|
+
|
|
33
|
+
setVisibleText('');
|
|
34
|
+
|
|
35
|
+
const TYPING_INTERVAL = 20;
|
|
36
|
+
const totalChars = children.length;
|
|
37
|
+
const totalSteps = Math.ceil(safeDuration / TYPING_INTERVAL);
|
|
38
|
+
const charsPerStep = Math.max(1, Math.ceil(totalChars / totalSteps));
|
|
39
|
+
|
|
40
|
+
let visibleCharCount = 0;
|
|
41
|
+
const interval = setInterval(() => {
|
|
42
|
+
visibleCharCount += charsPerStep;
|
|
43
|
+
if (visibleCharCount >= totalChars) {
|
|
44
|
+
setVisibleText(children);
|
|
45
|
+
clearInterval(interval);
|
|
46
|
+
} else {
|
|
47
|
+
setVisibleText(children.slice(0, visibleCharCount));
|
|
48
|
+
}
|
|
49
|
+
}, TYPING_INTERVAL);
|
|
50
|
+
|
|
51
|
+
return () => clearInterval(interval);
|
|
52
|
+
}, [children, shouldAnimate, messageId, maxDurationMs]);
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<Markdown className={className} variant={variant}>
|
|
56
|
+
{visibleText}
|
|
57
|
+
</Markdown>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
|
2
|
+
import { cn } from '@/lib/utils';
|
|
3
|
+
import { type VariantProps, cva } from 'class-variance-authority';
|
|
4
|
+
import * as React from 'react';
|
|
5
|
+
import { Button } from '../button';
|
|
6
|
+
import MessageLoading from './message-loading';
|
|
7
|
+
|
|
8
|
+
// ChatBubble
|
|
9
|
+
const chatBubbleVariant = cva('flex gap-2 max-w-[60%] relative group', {
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
received: 'self-start',
|
|
13
|
+
sent: 'self-end flex-row-reverse',
|
|
14
|
+
},
|
|
15
|
+
layout: {
|
|
16
|
+
default: '',
|
|
17
|
+
ai: 'max-w-full w-full items-center',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
defaultVariants: {
|
|
21
|
+
variant: 'received',
|
|
22
|
+
layout: 'default',
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
interface ChatBubbleProps
|
|
27
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
28
|
+
VariantProps<typeof chatBubbleVariant> {}
|
|
29
|
+
|
|
30
|
+
const ChatBubble = React.forwardRef<HTMLDivElement, ChatBubbleProps>(
|
|
31
|
+
({ className, variant, layout, children, ...props }, ref) => (
|
|
32
|
+
<div
|
|
33
|
+
className={cn(chatBubbleVariant({ variant, layout, className }), 'relative group')}
|
|
34
|
+
ref={ref}
|
|
35
|
+
{...props}
|
|
36
|
+
>
|
|
37
|
+
{React.Children.map(children, (child) =>
|
|
38
|
+
React.isValidElement(child) &&
|
|
39
|
+
typeof child.type !== 'string' &&
|
|
40
|
+
child.type !== React.Fragment
|
|
41
|
+
? React.cloneElement(child, {
|
|
42
|
+
variant,
|
|
43
|
+
layout,
|
|
44
|
+
} as React.ComponentProps<typeof child.type>)
|
|
45
|
+
: child
|
|
46
|
+
)}
|
|
47
|
+
</div>
|
|
48
|
+
)
|
|
49
|
+
);
|
|
50
|
+
ChatBubble.displayName = 'ChatBubble';
|
|
51
|
+
|
|
52
|
+
// ChatBubbleAvatar
|
|
53
|
+
interface ChatBubbleAvatarProps {
|
|
54
|
+
src?: string;
|
|
55
|
+
fallback?: string;
|
|
56
|
+
className?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const ChatBubbleAvatar: React.FC<ChatBubbleAvatarProps> = ({ src, fallback, className }) => (
|
|
60
|
+
<Avatar className={className}>
|
|
61
|
+
<AvatarImage src={src} alt="Avatar" />
|
|
62
|
+
<AvatarFallback>{fallback}</AvatarFallback>
|
|
63
|
+
</Avatar>
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// ChatBubbleMessage
|
|
67
|
+
const chatBubbleMessageVariants = cva('', {
|
|
68
|
+
variants: {
|
|
69
|
+
variant: {
|
|
70
|
+
received: 'text-secondary-foreground',
|
|
71
|
+
sent: 'px-5 py-2.5 bg-secondary text-secondary-foreground rounded-lg',
|
|
72
|
+
},
|
|
73
|
+
layout: {
|
|
74
|
+
default: '',
|
|
75
|
+
ai: 'border-t w-full rounded-none bg-transparent',
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
defaultVariants: {
|
|
79
|
+
variant: 'received',
|
|
80
|
+
layout: 'default',
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
interface ChatBubbleMessageProps
|
|
85
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
86
|
+
VariantProps<typeof chatBubbleMessageVariants> {
|
|
87
|
+
isLoading?: boolean;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const ChatBubbleMessage = React.forwardRef<HTMLDivElement, ChatBubbleMessageProps>(
|
|
91
|
+
({ className, variant, layout, isLoading = false, children, ...props }, ref) => (
|
|
92
|
+
<div
|
|
93
|
+
className={cn(
|
|
94
|
+
chatBubbleMessageVariants({ variant, layout, className }),
|
|
95
|
+
'break-words max-w-full whitespace-pre-wrap'
|
|
96
|
+
)}
|
|
97
|
+
ref={ref}
|
|
98
|
+
{...props}
|
|
99
|
+
>
|
|
100
|
+
{isLoading ? (
|
|
101
|
+
<div className="flex items-center space-x-2">
|
|
102
|
+
<MessageLoading />
|
|
103
|
+
</div>
|
|
104
|
+
) : (
|
|
105
|
+
children
|
|
106
|
+
)}
|
|
107
|
+
</div>
|
|
108
|
+
)
|
|
109
|
+
);
|
|
110
|
+
ChatBubbleMessage.displayName = 'ChatBubbleMessage';
|
|
111
|
+
|
|
112
|
+
// ChatBubbleTimestamp
|
|
113
|
+
interface ChatBubbleTimestampProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
114
|
+
timestamp: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const ChatBubbleTimestamp: React.FC<ChatBubbleTimestampProps> = ({
|
|
118
|
+
timestamp,
|
|
119
|
+
className,
|
|
120
|
+
...props
|
|
121
|
+
}) => (
|
|
122
|
+
<div className={cn('text-xs text-right select-none', className)} {...props}>
|
|
123
|
+
{timestamp}
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
// ChatBubbleAction
|
|
128
|
+
type ChatBubbleActionProps = React.ComponentProps<typeof Button> & {
|
|
129
|
+
icon: React.ReactNode;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const ChatBubbleAction: React.FC<ChatBubbleActionProps> = ({
|
|
133
|
+
icon,
|
|
134
|
+
onClick,
|
|
135
|
+
className,
|
|
136
|
+
variant = 'ghost',
|
|
137
|
+
size = 'icon',
|
|
138
|
+
...props
|
|
139
|
+
}) => (
|
|
140
|
+
<Button variant={variant} size={size} className={className} onClick={onClick} {...props}>
|
|
141
|
+
{icon}
|
|
142
|
+
</Button>
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
interface ChatBubbleActionWrapperProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
146
|
+
variant?: 'sent' | 'received';
|
|
147
|
+
className?: string;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const ChatBubbleActionWrapper = React.forwardRef<HTMLDivElement, ChatBubbleActionWrapperProps>(
|
|
151
|
+
({ variant, className, children, ...props }, ref) => (
|
|
152
|
+
<div
|
|
153
|
+
ref={ref}
|
|
154
|
+
className={cn(
|
|
155
|
+
'absolute top-1/2 -translate-y-1/2 flex opacity-0 group-hover:opacity-100 transition-opacity duration-200',
|
|
156
|
+
variant === 'sent'
|
|
157
|
+
? '-left-1 -translate-x-full flex-row-reverse'
|
|
158
|
+
: '-right-1 translate-x-full',
|
|
159
|
+
className
|
|
160
|
+
)}
|
|
161
|
+
{...props}
|
|
162
|
+
>
|
|
163
|
+
{children}
|
|
164
|
+
</div>
|
|
165
|
+
)
|
|
166
|
+
);
|
|
167
|
+
ChatBubbleActionWrapper.displayName = 'ChatBubbleActionWrapper';
|
|
168
|
+
|
|
169
|
+
export {
|
|
170
|
+
ChatBubble,
|
|
171
|
+
ChatBubbleAvatar,
|
|
172
|
+
ChatBubbleMessage,
|
|
173
|
+
ChatBubbleTimestamp,
|
|
174
|
+
chatBubbleVariant,
|
|
175
|
+
chatBubbleMessageVariants,
|
|
176
|
+
ChatBubbleAction,
|
|
177
|
+
ChatBubbleActionWrapper,
|
|
178
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { cn } from '@/lib/utils';
|
|
2
|
+
import { StickToBottom } from 'use-stick-to-bottom';
|
|
3
|
+
|
|
4
|
+
export type ChatContainerRootProps = {
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
className?: string;
|
|
7
|
+
} & React.HTMLAttributes<HTMLDivElement>;
|
|
8
|
+
|
|
9
|
+
export type ChatContainerContentProps = {
|
|
10
|
+
children: React.ReactNode;
|
|
11
|
+
className?: string;
|
|
12
|
+
} & React.HTMLAttributes<HTMLDivElement>;
|
|
13
|
+
|
|
14
|
+
export type ChatContainerScrollAnchorProps = {
|
|
15
|
+
className?: string;
|
|
16
|
+
ref?: React.RefObject<HTMLDivElement>;
|
|
17
|
+
} & React.HTMLAttributes<HTMLDivElement>;
|
|
18
|
+
|
|
19
|
+
function ChatContainerRoot({ children, className, ...props }: ChatContainerRootProps) {
|
|
20
|
+
return (
|
|
21
|
+
<StickToBottom
|
|
22
|
+
className={cn('flex overflow-y-auto', className)}
|
|
23
|
+
resize="smooth"
|
|
24
|
+
initial="smooth"
|
|
25
|
+
role="log"
|
|
26
|
+
{...props}
|
|
27
|
+
>
|
|
28
|
+
{children}
|
|
29
|
+
</StickToBottom>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function ChatContainerContent({ children, className, ...props }: ChatContainerContentProps) {
|
|
34
|
+
return (
|
|
35
|
+
<StickToBottom.Content className={cn('flex w-full flex-col', className)} {...props}>
|
|
36
|
+
{children}
|
|
37
|
+
</StickToBottom.Content>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function ChatContainerScrollAnchor({ className, ...props }: ChatContainerScrollAnchorProps) {
|
|
42
|
+
return (
|
|
43
|
+
<div
|
|
44
|
+
className={cn('h-px w-full shrink-0 scroll-mt-4', className)}
|
|
45
|
+
aria-hidden="true"
|
|
46
|
+
{...props}
|
|
47
|
+
/>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export { ChatContainerRoot, ChatContainerContent, ChatContainerScrollAnchor };
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/// <reference types="cypress" />
|
|
2
|
+
/// <reference path="../../../../cypress/support/types.d.ts" />
|
|
3
|
+
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { ChatInput } from './chat-input';
|
|
6
|
+
|
|
7
|
+
// Test component for form submission
|
|
8
|
+
const TestForm = () => {
|
|
9
|
+
const [value, setValue] = React.useState('');
|
|
10
|
+
|
|
11
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
12
|
+
e.preventDefault();
|
|
13
|
+
setValue('');
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<form onSubmit={handleSubmit}>
|
|
18
|
+
<ChatInput value={value} onChange={(e) => setValue(e.target.value)} />
|
|
19
|
+
<button type="submit">Send</button>
|
|
20
|
+
</form>
|
|
21
|
+
);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Test component for chat form
|
|
25
|
+
const TestChatForm = () => {
|
|
26
|
+
const [messages, setMessages] = React.useState<string[]>([]);
|
|
27
|
+
const [input, setInput] = React.useState('');
|
|
28
|
+
|
|
29
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
30
|
+
e.preventDefault();
|
|
31
|
+
if (input.trim()) {
|
|
32
|
+
setMessages([...messages, input]);
|
|
33
|
+
setInput('');
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div>
|
|
39
|
+
<div className="messages">
|
|
40
|
+
{messages.map((msg, i) => (
|
|
41
|
+
<div key={i}>{msg}</div>
|
|
42
|
+
))}
|
|
43
|
+
</div>
|
|
44
|
+
<form onSubmit={handleSubmit}>
|
|
45
|
+
<ChatInput
|
|
46
|
+
value={input}
|
|
47
|
+
onChange={(e) => setInput(e.target.value)}
|
|
48
|
+
placeholder="Type a message..."
|
|
49
|
+
/>
|
|
50
|
+
<button type="submit">Send</button>
|
|
51
|
+
</form>
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
describe('ChatInput Component', () => {
|
|
57
|
+
it('renders correctly with default props', () => {
|
|
58
|
+
cy.mount(<ChatInput placeholder="Type a message..." />);
|
|
59
|
+
|
|
60
|
+
cy.get('textarea').should('exist');
|
|
61
|
+
cy.get('textarea').should('have.attr', 'placeholder', 'Type a message...');
|
|
62
|
+
cy.get('textarea').should('have.attr', 'name', 'message');
|
|
63
|
+
cy.get('textarea').should('have.class', 'resize-none');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('accepts user input', () => {
|
|
67
|
+
cy.mount(<ChatInput />);
|
|
68
|
+
|
|
69
|
+
cy.get('textarea').type('Hello, this is a test message');
|
|
70
|
+
cy.get('textarea').should('have.value', 'Hello, this is a test message');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('applies custom placeholder', () => {
|
|
74
|
+
cy.mount(<ChatInput placeholder="Write your message here..." />);
|
|
75
|
+
|
|
76
|
+
cy.get('textarea').should('have.attr', 'placeholder', 'Write your message here...');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('handles onChange events', () => {
|
|
80
|
+
const onChange = cy.stub();
|
|
81
|
+
cy.mount(<ChatInput onChange={onChange} />);
|
|
82
|
+
|
|
83
|
+
cy.get('textarea').type('Test');
|
|
84
|
+
cy.wrap(onChange).should('have.been.called');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('handles onKeyDown events', () => {
|
|
88
|
+
const onKeyDown = cy.stub();
|
|
89
|
+
cy.mount(<ChatInput onKeyDown={onKeyDown} />);
|
|
90
|
+
|
|
91
|
+
cy.get('textarea').type('{enter}');
|
|
92
|
+
cy.wrap(onKeyDown).should('have.been.called');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('can be disabled', () => {
|
|
96
|
+
cy.mount(<ChatInput disabled />);
|
|
97
|
+
|
|
98
|
+
cy.get('textarea').should('be.disabled');
|
|
99
|
+
cy.get('textarea').should('have.class', 'disabled:cursor-not-allowed');
|
|
100
|
+
cy.get('textarea').should('have.class', 'disabled:opacity-50');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('applies custom className', () => {
|
|
104
|
+
cy.mount(<ChatInput className="custom-class" />);
|
|
105
|
+
|
|
106
|
+
cy.get('textarea').should('have.class', 'custom-class');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('clears input on submit', () => {
|
|
110
|
+
cy.mount(<TestForm />);
|
|
111
|
+
|
|
112
|
+
cy.get('textarea').type('Test message');
|
|
113
|
+
cy.get('textarea').should('have.value', 'Test message');
|
|
114
|
+
cy.get('button').click();
|
|
115
|
+
cy.get('textarea').should('have.value', '');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('works with autoComplete', () => {
|
|
119
|
+
cy.mount(<ChatInput autoComplete="on" />);
|
|
120
|
+
|
|
121
|
+
cy.get('textarea').should('have.attr', 'autocomplete', 'on');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('supports maxLength', () => {
|
|
125
|
+
cy.mount(<ChatInput maxLength={100} />);
|
|
126
|
+
|
|
127
|
+
cy.get('textarea').should('have.attr', 'maxlength', '100');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('maintains focus after mount', () => {
|
|
131
|
+
cy.mount(<ChatInput autoFocus />);
|
|
132
|
+
|
|
133
|
+
cy.get('textarea').should('have.focus');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('handles paste events', () => {
|
|
137
|
+
const onPaste = cy.stub();
|
|
138
|
+
cy.mount(<ChatInput onPaste={onPaste} />);
|
|
139
|
+
|
|
140
|
+
cy.get('textarea').trigger('paste');
|
|
141
|
+
cy.wrap(onPaste).should('have.been.called');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('supports different input modes', () => {
|
|
145
|
+
cy.mount(<ChatInput inputMode="text" />);
|
|
146
|
+
|
|
147
|
+
cy.get('textarea').should('have.attr', 'inputmode', 'text');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('works in a chat form', () => {
|
|
151
|
+
cy.mount(<TestChatForm />);
|
|
152
|
+
|
|
153
|
+
cy.get('textarea').type('First message');
|
|
154
|
+
cy.get('button').click();
|
|
155
|
+
cy.get('.messages').should('contain', 'First message');
|
|
156
|
+
cy.get('textarea').should('have.value', '');
|
|
157
|
+
|
|
158
|
+
cy.get('textarea').type('Second message');
|
|
159
|
+
cy.get('button').click();
|
|
160
|
+
cy.get('.messages').should('contain', 'Second message');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('supports data attributes', () => {
|
|
164
|
+
cy.mount(<ChatInput data-testid="chat-input" data-cy="chat-input" />);
|
|
165
|
+
|
|
166
|
+
cy.get('[data-testid="chat-input"]').should('exist');
|
|
167
|
+
cy.get('[data-cy="chat-input"]').should('exist');
|
|
168
|
+
});
|
|
169
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Textarea } from '@/components/ui/textarea';
|
|
2
|
+
import { cn } from '@/lib/utils';
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
|
|
5
|
+
interface ChatInputProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
|
6
|
+
|
|
7
|
+
const MAX_HEIGHT = 160;
|
|
8
|
+
|
|
9
|
+
const ChatInput = React.forwardRef<HTMLTextAreaElement, ChatInputProps>(
|
|
10
|
+
({ className, ...props }, ref) => {
|
|
11
|
+
const internalRef = React.useRef<HTMLTextAreaElement | null>(null);
|
|
12
|
+
|
|
13
|
+
const combinedRef = (node: HTMLTextAreaElement) => {
|
|
14
|
+
if (typeof ref === 'function') ref(node);
|
|
15
|
+
else if (ref) ref.current = node;
|
|
16
|
+
internalRef.current = node;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const resizeTextarea = () => {
|
|
20
|
+
const textarea = internalRef.current;
|
|
21
|
+
if (textarea) {
|
|
22
|
+
textarea.style.height = 'auto';
|
|
23
|
+
textarea.style.height = Math.min(textarea.scrollHeight, MAX_HEIGHT) + 'px';
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
React.useEffect(() => {
|
|
28
|
+
resizeTextarea();
|
|
29
|
+
}, [props.value]);
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<Textarea
|
|
33
|
+
autoComplete="off"
|
|
34
|
+
ref={combinedRef}
|
|
35
|
+
name="message"
|
|
36
|
+
className={cn(
|
|
37
|
+
'px-4 py-3 bg-background text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring disabled:cursor-not-allowed disabled:opacity-50 w-full rounded-md flex items-center h-16 resize-none',
|
|
38
|
+
className
|
|
39
|
+
)}
|
|
40
|
+
{...props}
|
|
41
|
+
/>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
ChatInput.displayName = 'ChatInput';
|
|
46
|
+
|
|
47
|
+
export { ChatInput };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Button } from '@/components/ui/button';
|
|
2
|
+
import { ArrowDown } from 'lucide-react';
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
|
|
5
|
+
interface ChatMessageListProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
6
|
+
scrollRef: React.RefObject<HTMLDivElement | null>;
|
|
7
|
+
contentRef?: React.RefObject<HTMLDivElement | null>; // Optional content ref for StickToBottom
|
|
8
|
+
isAtBottom: boolean;
|
|
9
|
+
scrollToBottom: () => void;
|
|
10
|
+
disableAutoScroll: () => void;
|
|
11
|
+
smooth?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const ChatMessageList = React.forwardRef<HTMLDivElement, ChatMessageListProps>(
|
|
15
|
+
(
|
|
16
|
+
{
|
|
17
|
+
className,
|
|
18
|
+
children,
|
|
19
|
+
scrollRef,
|
|
20
|
+
contentRef,
|
|
21
|
+
isAtBottom,
|
|
22
|
+
scrollToBottom,
|
|
23
|
+
disableAutoScroll,
|
|
24
|
+
...props
|
|
25
|
+
},
|
|
26
|
+
_ref
|
|
27
|
+
) => {
|
|
28
|
+
return (
|
|
29
|
+
<div className="relative w-full h-full overflow-hidden">
|
|
30
|
+
<div
|
|
31
|
+
className={`flex flex-col w-full h-full p-2 overflow-y-auto ${className}`}
|
|
32
|
+
ref={scrollRef}
|
|
33
|
+
onWheel={disableAutoScroll}
|
|
34
|
+
onTouchMove={disableAutoScroll}
|
|
35
|
+
{...props}
|
|
36
|
+
>
|
|
37
|
+
<div className="flex flex-col gap-10" ref={contentRef}>
|
|
38
|
+
{children}
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
{!isAtBottom && (
|
|
43
|
+
<Button
|
|
44
|
+
onClick={() => {
|
|
45
|
+
scrollToBottom();
|
|
46
|
+
}}
|
|
47
|
+
size="icon"
|
|
48
|
+
variant="outline"
|
|
49
|
+
className="absolute bottom-2 left-1/2 transform -translate-x-1/2 inline-flex rounded-full shadow-md"
|
|
50
|
+
>
|
|
51
|
+
<ArrowDown className="h-4 w-4" />
|
|
52
|
+
</Button>
|
|
53
|
+
)}
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
ChatMessageList.displayName = 'ChatMessageList';
|
|
60
|
+
|
|
61
|
+
export { ChatMessageList };
|