@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.
Files changed (209) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +350 -0
  3. package/dist/assets/empty-module-CLMscLYw.js +1 -0
  4. package/dist/assets/main-BBZ_3lkn.css +5999 -0
  5. package/dist/assets/main-C5zNUkXH.js +7 -0
  6. package/dist/assets/main-Dz64ENQg.js +614 -0
  7. package/dist/assets/react-vendor-DM5m98rr.js +545 -0
  8. package/dist/assets/ui-vendor-BQCqNqg0.js +1 -0
  9. package/dist/elizaos-avatar.png +0 -0
  10. package/dist/elizaos-icon.png +0 -0
  11. package/dist/elizaos-logo-light.png +0 -0
  12. package/dist/elizaos.webp +0 -0
  13. package/dist/favicon.ico +0 -0
  14. package/dist/images/agents/agent1.png +0 -0
  15. package/dist/images/agents/agent2.png +0 -0
  16. package/dist/images/agents/agent3.png +0 -0
  17. package/dist/images/agents/agent4.png +0 -0
  18. package/dist/images/agents/agent5.png +0 -0
  19. package/dist/index.html +14 -0
  20. package/index.html +24 -0
  21. package/package.json +159 -0
  22. package/postcss.config.js +3 -0
  23. package/public/elizaos-avatar.png +0 -0
  24. package/public/elizaos-icon.png +0 -0
  25. package/public/elizaos-logo-light.png +0 -0
  26. package/public/elizaos.webp +0 -0
  27. package/public/favicon.ico +0 -0
  28. package/public/images/agents/agent1.png +0 -0
  29. package/public/images/agents/agent2.png +0 -0
  30. package/public/images/agents/agent3.png +0 -0
  31. package/public/images/agents/agent4.png +0 -0
  32. package/public/images/agents/agent5.png +0 -0
  33. package/src/App.tsx +222 -0
  34. package/src/components/AgentDetailsPanel.tsx +147 -0
  35. package/src/components/ChatInputArea.tsx +196 -0
  36. package/src/components/ChatMessageListComponent.tsx +139 -0
  37. package/src/components/actionTool.tsx +186 -0
  38. package/src/components/add-agent-card.tsx +77 -0
  39. package/src/components/agent-action-viewer.tsx +816 -0
  40. package/src/components/agent-avatar-stack.tsx +121 -0
  41. package/src/components/agent-card.cy.tsx +259 -0
  42. package/src/components/agent-card.tsx +177 -0
  43. package/src/components/agent-creator.tsx +142 -0
  44. package/src/components/agent-log-viewer.tsx +645 -0
  45. package/src/components/agent-memory-edit-overlay.tsx +461 -0
  46. package/src/components/agent-memory-viewer.tsx +504 -0
  47. package/src/components/agent-settings.tsx +270 -0
  48. package/src/components/agent-sidebar.tsx +178 -0
  49. package/src/components/api-key-dialog.tsx +113 -0
  50. package/src/components/app-sidebar.tsx +685 -0
  51. package/src/components/array-input.tsx +116 -0
  52. package/src/components/audio-recorder.tsx +292 -0
  53. package/src/components/avatar-panel.tsx +141 -0
  54. package/src/components/character-form.tsx +1138 -0
  55. package/src/components/chat.tsx +1813 -0
  56. package/src/components/combobox.tsx +187 -0
  57. package/src/components/confirmation-dialog.tsx +59 -0
  58. package/src/components/connection-error-banner.tsx +101 -0
  59. package/src/components/connection-status.cy.tsx +73 -0
  60. package/src/components/connection-status.tsx +155 -0
  61. package/src/components/copy-button.tsx +35 -0
  62. package/src/components/delete-button.tsx +24 -0
  63. package/src/components/env-settings.tsx +261 -0
  64. package/src/components/group-card.tsx +160 -0
  65. package/src/components/group-panel.tsx +543 -0
  66. package/src/components/input-copy.tsx +21 -0
  67. package/src/components/logs-page.tsx +41 -0
  68. package/src/components/media-content.tsx +385 -0
  69. package/src/components/memory-graph.tsx +170 -0
  70. package/src/components/missing-secrets-dialog.tsx +72 -0
  71. package/src/components/onboarding-tour.tsx +247 -0
  72. package/src/components/page-title.tsx +8 -0
  73. package/src/components/plugins-panel.tsx +383 -0
  74. package/src/components/profile-card.tsx +66 -0
  75. package/src/components/profile-overlay.tsx +283 -0
  76. package/src/components/retry-button.tsx +28 -0
  77. package/src/components/secret-panel.tsx +1505 -0
  78. package/src/components/server-management.tsx +264 -0
  79. package/src/components/split-button.tsx +148 -0
  80. package/src/components/stop-agent-button.tsx +99 -0
  81. package/src/components/ui/alert-dialog.cy.tsx +333 -0
  82. package/src/components/ui/alert-dialog.tsx +115 -0
  83. package/src/components/ui/alert.tsx +49 -0
  84. package/src/components/ui/avatar.cy.tsx +180 -0
  85. package/src/components/ui/avatar.tsx +57 -0
  86. package/src/components/ui/badge.cy.tsx +146 -0
  87. package/src/components/ui/badge.tsx +43 -0
  88. package/src/components/ui/button.cy.tsx +177 -0
  89. package/src/components/ui/button.tsx +56 -0
  90. package/src/components/ui/card.cy.tsx +160 -0
  91. package/src/components/ui/card.tsx +73 -0
  92. package/src/components/ui/chat/animated-markdown.tsx +59 -0
  93. package/src/components/ui/chat/chat-bubble.tsx +178 -0
  94. package/src/components/ui/chat/chat-container.tsx +51 -0
  95. package/src/components/ui/chat/chat-input.cy.tsx +169 -0
  96. package/src/components/ui/chat/chat-input.tsx +47 -0
  97. package/src/components/ui/chat/chat-message-list.tsx +61 -0
  98. package/src/components/ui/chat/chat-tts-button.tsx +199 -0
  99. package/src/components/ui/chat/code-block.tsx +79 -0
  100. package/src/components/ui/chat/expandable-chat.tsx +131 -0
  101. package/src/components/ui/chat/hooks/useAutoScroll.ts +86 -0
  102. package/src/components/ui/chat/markdown.tsx +209 -0
  103. package/src/components/ui/chat/message-loading.tsx +48 -0
  104. package/src/components/ui/checkbox.cy.tsx +170 -0
  105. package/src/components/ui/checkbox.tsx +30 -0
  106. package/src/components/ui/collapsible.cy.tsx +283 -0
  107. package/src/components/ui/collapsible.tsx +9 -0
  108. package/src/components/ui/command.cy.tsx +313 -0
  109. package/src/components/ui/command.tsx +143 -0
  110. package/src/components/ui/dialog.cy.tsx +279 -0
  111. package/src/components/ui/dialog.tsx +104 -0
  112. package/src/components/ui/dropdown-menu.cy.tsx +273 -0
  113. package/src/components/ui/dropdown-menu.tsx +281 -0
  114. package/src/components/ui/input.cy.tsx +82 -0
  115. package/src/components/ui/input.tsx +27 -0
  116. package/src/components/ui/label.cy.tsx +157 -0
  117. package/src/components/ui/label.tsx +19 -0
  118. package/src/components/ui/resizable.tsx +42 -0
  119. package/src/components/ui/scroll-area.cy.tsx +242 -0
  120. package/src/components/ui/scroll-area.tsx +46 -0
  121. package/src/components/ui/select.cy.tsx +277 -0
  122. package/src/components/ui/select.tsx +155 -0
  123. package/src/components/ui/separator.cy.tsx +145 -0
  124. package/src/components/ui/separator.tsx +29 -0
  125. package/src/components/ui/sheet.cy.tsx +324 -0
  126. package/src/components/ui/sheet.tsx +119 -0
  127. package/src/components/ui/sidebar.tsx +734 -0
  128. package/src/components/ui/skeleton.cy.tsx +149 -0
  129. package/src/components/ui/skeleton.tsx +17 -0
  130. package/src/components/ui/split-button.cy.tsx +274 -0
  131. package/src/components/ui/split-button.tsx +112 -0
  132. package/src/components/ui/switch.tsx +28 -0
  133. package/src/components/ui/tabs.cy.tsx +271 -0
  134. package/src/components/ui/tabs.tsx +53 -0
  135. package/src/components/ui/textarea.cy.tsx +136 -0
  136. package/src/components/ui/textarea.tsx +26 -0
  137. package/src/components/ui/toast.cy.tsx +209 -0
  138. package/src/components/ui/toast.tsx +126 -0
  139. package/src/components/ui/toaster.tsx +29 -0
  140. package/src/components/ui/tooltip.cy.tsx +244 -0
  141. package/src/components/ui/tooltip.tsx +30 -0
  142. package/src/config/agent-templates.ts +349 -0
  143. package/src/config/voice-models.ts +181 -0
  144. package/src/constants.ts +23 -0
  145. package/src/context/AuthContext.tsx +44 -0
  146. package/src/context/ConnectionContext.tsx +194 -0
  147. package/src/entry.tsx +9 -0
  148. package/src/hooks/__tests__/use-agent-tab-state.test.ts +137 -0
  149. package/src/hooks/__tests__/use-agent-update.test.tsx +250 -0
  150. package/src/hooks/__tests__/use-character-convert.test.ts +102 -0
  151. package/src/hooks/__tests__/use-panel-width-state.test.ts +243 -0
  152. package/src/hooks/__tests__/use-sidebar-state.test.ts +117 -0
  153. package/src/hooks/use-agent-management.ts +130 -0
  154. package/src/hooks/use-agent-tab-state.ts +74 -0
  155. package/src/hooks/use-agent-update.ts +469 -0
  156. package/src/hooks/use-character-convert.ts +138 -0
  157. package/src/hooks/use-confirmation.ts +55 -0
  158. package/src/hooks/use-delete-agent.ts +123 -0
  159. package/src/hooks/use-dm-channels.ts +198 -0
  160. package/src/hooks/use-elevenlabs-voices.ts +83 -0
  161. package/src/hooks/use-file-upload.ts +224 -0
  162. package/src/hooks/use-mobile.tsx +19 -0
  163. package/src/hooks/use-onboarding.tsx +49 -0
  164. package/src/hooks/use-panel-width-state.ts +147 -0
  165. package/src/hooks/use-partial-update.ts +288 -0
  166. package/src/hooks/use-plugin-details.ts +462 -0
  167. package/src/hooks/use-plugins.ts +119 -0
  168. package/src/hooks/use-query-hooks.ts +1263 -0
  169. package/src/hooks/use-server-agents.ts +62 -0
  170. package/src/hooks/use-server-version.tsx +47 -0
  171. package/src/hooks/use-sidebar-state.ts +50 -0
  172. package/src/hooks/use-socket-chat.ts +264 -0
  173. package/src/hooks/use-toast.ts +260 -0
  174. package/src/hooks/use-version.tsx +64 -0
  175. package/src/index.css +146 -0
  176. package/src/lib/api-client-config.ts +53 -0
  177. package/src/lib/api-type-mappers.ts +196 -0
  178. package/src/lib/export-utils.ts +123 -0
  179. package/src/lib/logger.ts +19 -0
  180. package/src/lib/media-utils.ts +170 -0
  181. package/src/lib/pca.test.ts +17 -0
  182. package/src/lib/pca.ts +52 -0
  183. package/src/lib/socketio-manager.ts +664 -0
  184. package/src/lib/utils.ts +168 -0
  185. package/src/main.tsx +16 -0
  186. package/src/mocks/empty-module.ts +12 -0
  187. package/src/mocks/node-module.ts +57 -0
  188. package/src/polyfills.ts +37 -0
  189. package/src/routes/agent-detail.tsx +30 -0
  190. package/src/routes/agent-list.tsx +27 -0
  191. package/src/routes/agent-settings.tsx +48 -0
  192. package/src/routes/character-detail.tsx +52 -0
  193. package/src/routes/character-form.tsx +79 -0
  194. package/src/routes/character-list.tsx +38 -0
  195. package/src/routes/chat.tsx +128 -0
  196. package/src/routes/createAgent.tsx +13 -0
  197. package/src/routes/group-new.tsx +50 -0
  198. package/src/routes/group.tsx +29 -0
  199. package/src/routes/home.tsx +218 -0
  200. package/src/routes/not-found.tsx +71 -0
  201. package/src/test/setup.ts +154 -0
  202. package/src/types/crypto-browserify.d.ts +4 -0
  203. package/src/types/index.ts +13 -0
  204. package/src/types/rooms.ts +8 -0
  205. package/src/types.ts +84 -0
  206. package/src/vite-env.d.ts +40 -0
  207. package/tailwind.config.ts +90 -0
  208. package/tsconfig.json +10 -0
  209. package/vite.config.ts +102 -0
@@ -0,0 +1,199 @@
1
+ import { useToast } from '@/hooks/use-toast';
2
+ import { createElizaClient } from '@/lib/api-client-config';
3
+ import { UUID } from '@elizaos/core';
4
+ import { useMutation } from '@tanstack/react-query';
5
+ import { Ellipsis, StopCircle, Volume2 } from 'lucide-react';
6
+ import { useEffect, useRef, useState } from 'react';
7
+ import { Button } from '../button';
8
+ import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip';
9
+
10
+ // Global ref to track currently playing audio
11
+ let currentlyPlayingAudio: HTMLAudioElement | null = null;
12
+
13
+ export default function ChatTtsButton({ agentId, text }: { agentId: string; text: string }) {
14
+ const { toast } = useToast();
15
+ const [playing, setPlaying] = useState<boolean>(false);
16
+ const [audioBlob, setAudioBlob] = useState<Blob | null>(null);
17
+ const [audioUrl, setAudioUrl] = useState<string | null>(null);
18
+ const audioRef = useRef<HTMLAudioElement | null>(null);
19
+ const audioBlobRef = useRef<Blob | null>(null);
20
+
21
+ const elizaClient = createElizaClient();
22
+
23
+ // Cleanup blob URL when component unmounts or audioBlob changes
24
+ useEffect(() => {
25
+ return () => {
26
+ if (audioUrl) {
27
+ URL.revokeObjectURL(audioUrl);
28
+ }
29
+ };
30
+ }, [audioUrl]);
31
+
32
+ const mutation = useMutation({
33
+ mutationKey: ['tts', text],
34
+ mutationFn: async () => {
35
+ console.log('🎵 Starting TTS API call...');
36
+ console.log('🎵 agentId:', agentId);
37
+ console.log('🎵 text:', text);
38
+
39
+ const response = await elizaClient.audio.generateSpeech(agentId as UUID, { text });
40
+ console.log('🎵 TTS API response:', response);
41
+
42
+ // Convert base64 audio string to Blob
43
+ const { audio, format } = response;
44
+
45
+ // Handle data URL format (data:audio/mp3;base64,...)
46
+ let audioData: string;
47
+ let mimeType: string;
48
+
49
+ if (audio.startsWith('data:')) {
50
+ const [header, base64Data] = audio.split(',');
51
+ const mimeMatch = header.match(/data:([^;]+)/);
52
+ mimeType = mimeMatch ? mimeMatch[1] : `audio/${format || 'mpeg'}`;
53
+ audioData = base64Data;
54
+ } else {
55
+ // Plain base64 string
56
+ audioData = audio;
57
+ mimeType = `audio/${format || 'mpeg'}`;
58
+ }
59
+
60
+ // Convert base64 to Blob
61
+ const binaryString = atob(audioData);
62
+ const bytes = new Uint8Array(binaryString.length);
63
+ for (let i = 0; i < binaryString.length; i++) {
64
+ bytes[i] = binaryString.charCodeAt(i);
65
+ }
66
+
67
+ return new Blob([bytes], { type: mimeType });
68
+ },
69
+ onSuccess: (data: Blob) => {
70
+ setAudioBlob(data);
71
+ audioBlobRef.current = data;
72
+ const url = URL.createObjectURL(data);
73
+ setAudioUrl(url);
74
+
75
+ // Auto-play after TTS generation
76
+ setTimeout(() => {
77
+ play();
78
+ }, 100);
79
+ },
80
+ onError: (e) => {
81
+ toast({
82
+ variant: 'destructive',
83
+ title: 'Unable to read message aloud',
84
+ description: e.message,
85
+ });
86
+ },
87
+ });
88
+
89
+ const play = async () => {
90
+ if (audioRef.current) {
91
+ try {
92
+ // Stop any currently playing audio
93
+ if (currentlyPlayingAudio && currentlyPlayingAudio !== audioRef.current) {
94
+ currentlyPlayingAudio.pause();
95
+ currentlyPlayingAudio.currentTime = 0;
96
+ }
97
+
98
+ // Set this as the currently playing audio
99
+ currentlyPlayingAudio = audioRef.current;
100
+
101
+ audioRef.current.volume = 1.0;
102
+ audioRef.current.muted = false;
103
+
104
+ await audioRef.current.play();
105
+ setPlaying(true);
106
+ } catch (err) {
107
+ toast({
108
+ variant: 'destructive',
109
+ title: 'Audio playback failed',
110
+ description: (err as Error).message,
111
+ });
112
+ setPlaying(false);
113
+ }
114
+ }
115
+ };
116
+
117
+ const stop = () => {
118
+ if (audioRef.current) {
119
+ audioRef.current.pause();
120
+ audioRef.current.currentTime = 0;
121
+
122
+ // Clear global reference if this was the currently playing audio
123
+ if (currentlyPlayingAudio === audioRef.current) {
124
+ currentlyPlayingAudio = null;
125
+ }
126
+ }
127
+ setPlaying(false);
128
+ };
129
+
130
+ const execute = async () => {
131
+ if (mutation?.isPending) {
132
+ return;
133
+ }
134
+
135
+ if (playing) {
136
+ stop();
137
+ return;
138
+ }
139
+
140
+ // Check if audio is already available
141
+ const hasAudioBlob =
142
+ audioBlob ||
143
+ audioBlobRef.current ||
144
+ (audioRef.current?.src && audioRef.current.src.startsWith('blob:'));
145
+ if (hasAudioBlob) {
146
+ await play();
147
+ return;
148
+ }
149
+
150
+ // Generate TTS
151
+ mutation.mutate();
152
+ };
153
+
154
+ const iconClass = 'text-muted-foreground size-3';
155
+
156
+ return (
157
+ <>
158
+ {audioBlob ? (
159
+ <audio
160
+ crossOrigin="anonymous"
161
+ playsInline
162
+ ref={audioRef}
163
+ src={audioUrl || ''}
164
+ onEnded={() => {
165
+ setPlaying(false);
166
+ // Clear global reference when audio ends
167
+ if (currentlyPlayingAudio === audioRef.current) {
168
+ currentlyPlayingAudio = null;
169
+ }
170
+ }}
171
+ >
172
+ Your browser does not support the audio element.
173
+ </audio>
174
+ ) : null}
175
+ <Tooltip>
176
+ <TooltipTrigger asChild>
177
+ <Button
178
+ size="icon"
179
+ variant="ghost"
180
+ type="button"
181
+ onClick={() => execute()}
182
+ disabled={mutation?.isPending}
183
+ >
184
+ {mutation?.isPending ? (
185
+ <Ellipsis className={iconClass} />
186
+ ) : playing ? (
187
+ <StopCircle className={iconClass} />
188
+ ) : (
189
+ <Volume2 className={iconClass} />
190
+ )}
191
+ </Button>
192
+ </TooltipTrigger>
193
+ <TooltipContent side="bottom">
194
+ <p>{playing ? 'Stop' : 'Read aloud'}</p>
195
+ </TooltipContent>
196
+ </Tooltip>
197
+ </>
198
+ );
199
+ }
@@ -0,0 +1,79 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { cn } from '@/lib/utils';
5
+
6
+ interface CodeBlockProps extends React.HTMLAttributes<HTMLDivElement> {
7
+ children: React.ReactNode;
8
+ variant?: 'user' | 'agent';
9
+ }
10
+
11
+ interface CodeBlockCodeProps {
12
+ code: string;
13
+ language?: string;
14
+ className?: string;
15
+ variant?: 'user' | 'agent';
16
+ }
17
+
18
+ interface CodeBlockGroupProps extends React.HTMLAttributes<HTMLDivElement> {
19
+ children: React.ReactNode;
20
+ }
21
+
22
+ const CodeBlock = React.forwardRef<HTMLDivElement, CodeBlockProps>(
23
+ ({ className, children, variant = 'agent', ...props }, ref) => {
24
+ return (
25
+ <div
26
+ ref={ref}
27
+ className={cn(
28
+ 'not-prose relative overflow-hidden rounded-lg border border-border',
29
+ className
30
+ )}
31
+ style={{ backgroundColor: '#0f172a', color: 'white' }}
32
+ {...props}
33
+ >
34
+ {children}
35
+ </div>
36
+ );
37
+ }
38
+ );
39
+ CodeBlock.displayName = 'CodeBlock';
40
+
41
+ const CodeBlockGroup = React.forwardRef<HTMLDivElement, CodeBlockGroupProps>(
42
+ ({ className, children, ...props }, ref) => {
43
+ return (
44
+ <div
45
+ ref={ref}
46
+ className={cn(
47
+ 'flex items-center justify-between border-b border-border px-4 py-2 text-sm text-muted-foreground bg-muted/30 dark:bg-slate-800/50',
48
+ className
49
+ )}
50
+ {...props}
51
+ >
52
+ {children}
53
+ </div>
54
+ );
55
+ }
56
+ );
57
+ CodeBlockGroup.displayName = 'CodeBlockGroup';
58
+
59
+ const CodeBlockCode = React.forwardRef<HTMLDivElement, CodeBlockCodeProps>(
60
+ ({ code, language = 'text', className, variant = 'agent', ...props }, ref) => {
61
+ return (
62
+ <div
63
+ ref={ref}
64
+ className={cn('overflow-x-auto p-4', className)}
65
+ style={{ backgroundColor: 'transparent' }}
66
+ {...props}
67
+ >
68
+ <pre className="text-sm whitespace-pre-wrap" style={{ color: 'white' }}>
69
+ <code className="font-mono" style={{ color: 'white' }}>
70
+ {code}
71
+ </code>
72
+ </pre>
73
+ </div>
74
+ );
75
+ }
76
+ );
77
+ CodeBlockCode.displayName = 'CodeBlockCode';
78
+
79
+ export { CodeBlock, CodeBlockCode, CodeBlockGroup };
@@ -0,0 +1,131 @@
1
+ import { Button } from '@/components/ui/button';
2
+ import { cn } from '@/lib/utils';
3
+ import { MessageCircle, X } from 'lucide-react';
4
+ import type React from 'react';
5
+ import { useRef, useState } from 'react';
6
+
7
+ export type ChatPosition = 'bottom-right' | 'bottom-left';
8
+ export type ChatSize = 'sm' | 'md' | 'lg' | 'xl' | 'full';
9
+
10
+ const chatConfig = {
11
+ dimensions: {
12
+ sm: 'sm:max-w-sm sm:max-h-[500px]',
13
+ md: 'sm:max-w-md sm:max-h-[600px]',
14
+ lg: 'sm:max-w-lg sm:max-h-[700px]',
15
+ xl: 'sm:max-w-xl sm:max-h-[800px]',
16
+ full: 'sm:w-full sm:h-full',
17
+ },
18
+ positions: {
19
+ 'bottom-right': 'bottom-5 right-5',
20
+ 'bottom-left': 'bottom-5 left-5',
21
+ },
22
+ chatPositions: {
23
+ 'bottom-right': 'sm:bottom-[calc(100%+10px)] sm:right-0',
24
+ 'bottom-left': 'sm:bottom-[calc(100%+10px)] sm:left-0',
25
+ },
26
+ states: {
27
+ open: 'pointer-events-auto opacity-100 visible scale-100 translate-y-0',
28
+ closed: 'pointer-events-none opacity-0 invisible scale-100 sm:translate-y-5',
29
+ },
30
+ };
31
+
32
+ interface ExpandableChatProps extends React.HTMLAttributes<HTMLDivElement> {
33
+ position?: ChatPosition;
34
+ size?: ChatSize;
35
+ icon?: React.ReactNode;
36
+ }
37
+
38
+ const ExpandableChat: React.FC<ExpandableChatProps> = ({
39
+ className,
40
+ position = 'bottom-right',
41
+ size = 'md',
42
+ icon,
43
+ children,
44
+ ...props
45
+ }) => {
46
+ const [isOpen, setIsOpen] = useState(false);
47
+ const chatRef = useRef<HTMLDivElement>(null);
48
+
49
+ const toggleChat = () => setIsOpen(!isOpen);
50
+
51
+ return (
52
+ <div className={cn(`fixed ${chatConfig.positions[position]} z-50`, className)} {...props}>
53
+ <div
54
+ ref={chatRef}
55
+ className={cn(
56
+ 'flex flex-col bg-background border sm:rounded-md shadow-md overflow-hidden transition-all duration-250 ease-out sm:absolute sm:w-[90vw] sm:h-[80vh] fixed inset-0 w-full h-full sm:inset-auto',
57
+ chatConfig.chatPositions[position],
58
+ chatConfig.dimensions[size],
59
+ isOpen ? chatConfig.states.open : chatConfig.states.closed,
60
+ className
61
+ )}
62
+ >
63
+ {children}
64
+ <Button
65
+ variant="ghost"
66
+ size="icon"
67
+ className="absolute top-2 right-2 sm:hidden"
68
+ onClick={toggleChat}
69
+ >
70
+ <X className="h-4 w-4" />
71
+ </Button>
72
+ </div>
73
+ <ExpandableChatToggle icon={icon} isOpen={isOpen} toggleChat={toggleChat} />
74
+ </div>
75
+ );
76
+ };
77
+
78
+ ExpandableChat.displayName = 'ExpandableChat';
79
+
80
+ const ExpandableChatHeader: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({
81
+ className,
82
+ ...props
83
+ }) => (
84
+ <div className={cn('flex items-center justify-between p-4 border-b', className)} {...props} />
85
+ );
86
+
87
+ ExpandableChatHeader.displayName = 'ExpandableChatHeader';
88
+
89
+ const ExpandableChatBody: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({
90
+ className,
91
+ ...props
92
+ }) => <div className={cn('flex-grow overflow-y-auto', className)} {...props} />;
93
+
94
+ ExpandableChatBody.displayName = 'ExpandableChatBody';
95
+
96
+ const ExpandableChatFooter: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({
97
+ className,
98
+ ...props
99
+ }) => <div className={cn('border-t p-4', className)} {...props} />;
100
+
101
+ ExpandableChatFooter.displayName = 'ExpandableChatFooter';
102
+
103
+ interface ExpandableChatToggleProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
104
+ icon?: React.ReactNode;
105
+ isOpen: boolean;
106
+ toggleChat: () => void;
107
+ }
108
+
109
+ const ExpandableChatToggle: React.FC<ExpandableChatToggleProps> = ({
110
+ className,
111
+ icon,
112
+ isOpen,
113
+ toggleChat,
114
+ ...props
115
+ }) => (
116
+ <Button
117
+ variant="default"
118
+ onClick={toggleChat}
119
+ className={cn(
120
+ 'w-14 h-14 rounded-full shadow-md flex items-center justify-center hover:shadow-lg hover:shadow-black/30 transition-all duration-300',
121
+ className
122
+ )}
123
+ {...props}
124
+ >
125
+ {isOpen ? <X className="h-6 w-6" /> : icon || <MessageCircle className="h-6 w-6" />}
126
+ </Button>
127
+ );
128
+
129
+ ExpandableChatToggle.displayName = 'ExpandableChatToggle';
130
+
131
+ export { ExpandableChat, ExpandableChatHeader, ExpandableChatBody, ExpandableChatFooter };
@@ -0,0 +1,86 @@
1
+ // Enhanced with StickToBottom for better UX
2
+ import { useCallback, useRef, useState, useEffect } from 'react';
3
+ import { useStickToBottom } from 'use-stick-to-bottom';
4
+
5
+ interface ScrollState {
6
+ isAtBottom: boolean;
7
+ autoScrollEnabled: boolean;
8
+ }
9
+
10
+ interface UseAutoScrollOptions {
11
+ offset?: number;
12
+ smooth?: boolean;
13
+ }
14
+
15
+ export function useAutoScroll(options: UseAutoScrollOptions = {}) {
16
+ const { smooth = false } = options;
17
+ const userHasScrolled = useRef(false); // To track if user manually scrolled up
18
+
19
+ // Use StickToBottom for enhanced scroll behavior
20
+ const stickToBottom = useStickToBottom({
21
+ initial: smooth ? 'smooth' : 'instant',
22
+ resize: smooth ? 'smooth' : 'instant',
23
+ });
24
+
25
+ const [scrollState, setScrollState] = useState<ScrollState>({
26
+ isAtBottom: true,
27
+ autoScrollEnabled: true,
28
+ });
29
+
30
+ // Sync our state with StickToBottom's state
31
+ useEffect(() => {
32
+ const isAtBottom = stickToBottom.isAtBottom;
33
+
34
+ if (isAtBottom) {
35
+ // User is at bottom - reset user control and enable auto-scroll
36
+ userHasScrolled.current = false;
37
+ setScrollState({ isAtBottom: true, autoScrollEnabled: true });
38
+ } else {
39
+ // User is not at bottom
40
+ setScrollState((prev) => ({
41
+ ...prev,
42
+ isAtBottom: false,
43
+ autoScrollEnabled: userHasScrolled.current ? false : prev.autoScrollEnabled,
44
+ }));
45
+ }
46
+ }, [stickToBottom.isAtBottom]);
47
+
48
+ // Enhanced scroll to bottom using StickToBottom
49
+ const scrollToBottom = useCallback(
50
+ (instant?: boolean) => {
51
+ const animation = instant ? 'instant' : smooth ? 'smooth' : 'instant';
52
+ stickToBottom.scrollToBottom({
53
+ animation,
54
+ preserveScrollPosition: false, // Always scroll to bottom when called
55
+ });
56
+
57
+ // Update our state
58
+ setScrollState({ isAtBottom: true, autoScrollEnabled: true });
59
+ userHasScrolled.current = false;
60
+ },
61
+ [stickToBottom.scrollToBottom, smooth]
62
+ );
63
+
64
+ // Enhanced disable auto-scroll using StickToBottom
65
+ const disableAutoScroll = useCallback(() => {
66
+ if (!stickToBottom.isAtBottom) {
67
+ userHasScrolled.current = true; // User has taken control by scrolling up
68
+ stickToBottom.stopScroll(); // Stop any ongoing scroll animations
69
+ setScrollState((prev) => ({
70
+ ...prev,
71
+ autoScrollEnabled: false, // Disable auto-scroll
72
+ }));
73
+ }
74
+ }, [stickToBottom.isAtBottom, stickToBottom.stopScroll]);
75
+
76
+ return {
77
+ scrollRef: stickToBottom.scrollRef,
78
+ contentRef: stickToBottom.contentRef, // Expose content ref for proper StickToBottom usage
79
+ isAtBottom: scrollState.isAtBottom,
80
+ autoScrollEnabled: scrollState.autoScrollEnabled,
81
+ scrollToBottom: () => scrollToBottom(false), // Expose a non-instant scroll by default
82
+ disableAutoScroll,
83
+ // Expose StickToBottom instance for advanced usage if needed
84
+ _stickToBottom: stickToBottom,
85
+ };
86
+ }
@@ -0,0 +1,209 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import ReactMarkdown from 'react-markdown';
5
+ import remarkGfm from 'remark-gfm';
6
+ import { cn } from '@/lib/utils';
7
+ import { CodeBlock, CodeBlockCode } from './code-block';
8
+
9
+ interface MarkdownProps {
10
+ children: string;
11
+ className?: string;
12
+ components?: Record<string, React.ComponentType<any>>;
13
+ variant?: 'user' | 'agent';
14
+ }
15
+
16
+ const createComponents = (variant: 'user' | 'agent' = 'agent') => ({
17
+ // Code blocks
18
+ pre: ({ children, className, ...props }: any) => {
19
+ // Check if this contains a code element with a language class
20
+ const hasCodeWithLanguage = React.Children.toArray(children).some((child: any) => {
21
+ return (
22
+ React.isValidElement(child) &&
23
+ (child.props as any)?.className &&
24
+ /language-\w+/.test((child.props as any).className)
25
+ );
26
+ });
27
+
28
+ if (hasCodeWithLanguage) {
29
+ // Find the code element
30
+ const codeChild = React.Children.toArray(children).find((child: any) => {
31
+ return (
32
+ React.isValidElement(child) &&
33
+ (child.props as any)?.className &&
34
+ /language-\w+/.test((child.props as any).className)
35
+ );
36
+ }) as React.ReactElement;
37
+
38
+ const codeClassName = (codeChild.props as any).className || '';
39
+ const languageMatch = codeClassName.match(/language-(\w+)/);
40
+ const language = languageMatch ? languageMatch[1] : 'text';
41
+ const code = String((codeChild.props as any).children || '').trim();
42
+
43
+ return (
44
+ <CodeBlock>
45
+ {language && language !== 'text' && (
46
+ <div className="flex items-center justify-between px-4 py-2 text-xs bg-black/30 text-white border-b border-border">
47
+ <span className="font-mono uppercase tracking-wide">{language}</span>
48
+ </div>
49
+ )}
50
+ <CodeBlockCode code={code} language={language} />
51
+ </CodeBlock>
52
+ );
53
+ }
54
+
55
+ // Regular pre block (not code)
56
+ return (
57
+ <pre
58
+ className={cn(
59
+ 'overflow-x-auto rounded-md bg-black dark:bg-black/50 border border-border p-4 text-sm font-mono text-white dark:text-white',
60
+ className
61
+ )}
62
+ {...props}
63
+ >
64
+ {children}
65
+ </pre>
66
+ );
67
+ },
68
+
69
+ // Inline code
70
+ code: ({ className, children, ...props }: any) => {
71
+ // If this has a language class, it's likely part of a code block handled by pre above
72
+ const hasLanguage = className && /language-\w+/.test(className);
73
+
74
+ if (hasLanguage) {
75
+ // Let the pre component handle this
76
+ return (
77
+ <code className={className} {...props}>
78
+ {children}
79
+ </code>
80
+ );
81
+ }
82
+
83
+ // Regular inline code
84
+ return (
85
+ <code
86
+ className={cn(
87
+ 'relative rounded bg-muted/60 dark:bg-slate-800/60 px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold text-foreground dark:text-slate-100 border border-border/50',
88
+ className
89
+ )}
90
+ {...props}
91
+ >
92
+ {children}
93
+ </code>
94
+ );
95
+ },
96
+
97
+ // Headers
98
+ h1: ({ className, ...props }: any) => (
99
+ <h1 className={cn('mt-2 scroll-m-20 text-xl font-bold tracking-tight', className)} {...props} />
100
+ ),
101
+ h2: ({ className, ...props }: any) => (
102
+ <h2
103
+ className={cn('mt-2 scroll-m-20 text-lg font-semibold tracking-tight', className)}
104
+ {...props}
105
+ />
106
+ ),
107
+ h3: ({ className, ...props }: any) => (
108
+ <h3
109
+ className={cn('mt-2 scroll-m-20 text-base font-semibold tracking-tight', className)}
110
+ {...props}
111
+ />
112
+ ),
113
+ h4: ({ className, ...props }: any) => (
114
+ <h4
115
+ className={cn('mt-2 scroll-m-20 text-sm font-semibold tracking-tight', className)}
116
+ {...props}
117
+ />
118
+ ),
119
+
120
+ // Paragraphs
121
+ p: ({ className, ...props }: any) => <p className={cn('leading-7', className)} {...props} />,
122
+
123
+ // Lists
124
+ ul: ({ className, ...props }: any) => (
125
+ <ul className={cn('my-2 ml-6 list-disc [&>li]:mt-1', className)} {...props} />
126
+ ),
127
+ ol: ({ className, ...props }: any) => (
128
+ <ol className={cn('my-2 ml-6 list-decimal [&>li]:mt-1', className)} {...props} />
129
+ ),
130
+
131
+ // Links
132
+ a: ({ className, ...props }: any) => (
133
+ <a
134
+ className={cn('font-medium text-primary underline underline-offset-4', className)}
135
+ {...props}
136
+ />
137
+ ),
138
+
139
+ // Blockquotes
140
+ blockquote: ({ className, ...props }: any) => (
141
+ <blockquote
142
+ className={cn('mt-2 border-l-2 border-muted-foreground/20 pl-6 italic', className)}
143
+ {...props}
144
+ />
145
+ ),
146
+
147
+ // Tables
148
+ table: ({ className, ...props }: any) => (
149
+ <div className="my-2 w-full overflow-y-auto">
150
+ <table className={cn('w-full', className)} {...props} />
151
+ </div>
152
+ ),
153
+ th: ({ className, ...props }: any) => (
154
+ <th
155
+ className={cn(
156
+ 'border border-muted-foreground/20 px-4 py-2 text-left font-bold [&[align=center]]:text-center [&[align=right]]:text-right',
157
+ className
158
+ )}
159
+ {...props}
160
+ />
161
+ ),
162
+ td: ({ className, ...props }: any) => (
163
+ <td
164
+ className={cn(
165
+ 'border border-muted-foreground/20 px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right',
166
+ className
167
+ )}
168
+ {...props}
169
+ />
170
+ ),
171
+
172
+ // Images
173
+ img: ({ className, alt, ...props }: any) => (
174
+ <img className={cn('rounded-md', className)} alt={alt} {...props} />
175
+ ),
176
+
177
+ // Horizontal rule
178
+ hr: ({ ...props }: any) => <hr className="my-4 border-muted-foreground/20" {...props} />,
179
+
180
+ // Strong/Bold
181
+ strong: ({ className, ...props }: any) => (
182
+ <strong className={cn('font-semibold', className)} {...props} />
183
+ ),
184
+
185
+ // Emphasis/Italic
186
+ em: ({ className, ...props }: any) => <em className={cn('italic', className)} {...props} />,
187
+ });
188
+
189
+ const Markdown = React.memo<MarkdownProps>(
190
+ ({ children, className, components, variant = 'agent', ...props }) => {
191
+ const defaultComponents = React.useMemo(() => createComponents(variant), [variant]);
192
+ const mergedComponents = React.useMemo(
193
+ () => ({ ...defaultComponents, ...components }),
194
+ [defaultComponents, components]
195
+ );
196
+
197
+ return (
198
+ <div className={cn('markdown-content', className)} {...props}>
199
+ <ReactMarkdown remarkPlugins={[remarkGfm]} components={mergedComponents}>
200
+ {children}
201
+ </ReactMarkdown>
202
+ </div>
203
+ );
204
+ }
205
+ );
206
+
207
+ Markdown.displayName = 'Markdown';
208
+
209
+ export { Markdown, type MarkdownProps };