@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,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 };
|