@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,187 @@
|
|
|
1
|
+
import { useState, useRef, useEffect } from 'react';
|
|
2
|
+
import { Card } from '@/components/ui/card';
|
|
3
|
+
import { Badge } from '@/components/ui/badge';
|
|
4
|
+
import { ChevronDown, X } from 'lucide-react';
|
|
5
|
+
import { formatAgentName } from '@/lib/utils';
|
|
6
|
+
|
|
7
|
+
interface Option {
|
|
8
|
+
icon: string;
|
|
9
|
+
label: string;
|
|
10
|
+
id?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface MultiSelectComboboxProps {
|
|
14
|
+
options: Option[];
|
|
15
|
+
className?: string;
|
|
16
|
+
onSelect?: (selected: Option[]) => void;
|
|
17
|
+
initialSelected?: Option[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default function MultiSelectCombobox({
|
|
21
|
+
options = [],
|
|
22
|
+
className = '',
|
|
23
|
+
onSelect,
|
|
24
|
+
initialSelected = [],
|
|
25
|
+
}: MultiSelectComboboxProps) {
|
|
26
|
+
const [selected, setSelected] = useState<Option[]>(initialSelected);
|
|
27
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
28
|
+
const comboboxRef = useRef<HTMLDivElement>(null);
|
|
29
|
+
|
|
30
|
+
// Apply initialSelected when it changes - improved to handle both initial load and updates
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
setSelected(initialSelected);
|
|
33
|
+
}, [initialSelected]);
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
37
|
+
if (comboboxRef.current && !comboboxRef.current.contains(event.target as Node)) {
|
|
38
|
+
setIsOpen(false);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
43
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
44
|
+
}, []);
|
|
45
|
+
|
|
46
|
+
// Helper function to compare options using id if available, fallback to label
|
|
47
|
+
const isOptionSelected = (option: Option): boolean => {
|
|
48
|
+
return selected.some((item) => {
|
|
49
|
+
if (option.id && item.id) {
|
|
50
|
+
return item.id === option.id;
|
|
51
|
+
}
|
|
52
|
+
return item.label === option.label;
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Helper function to find option in selected array
|
|
57
|
+
const findSelectedOption = (option: Option): Option | undefined => {
|
|
58
|
+
return selected.find((item) => {
|
|
59
|
+
if (option.id && item.id) {
|
|
60
|
+
return item.id === option.id;
|
|
61
|
+
}
|
|
62
|
+
return item.label === option.label;
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const toggleSelection = (option: Option) => {
|
|
67
|
+
console.log('[MultiSelectCombobox] toggleSelection called with:', option);
|
|
68
|
+
setSelected((prev) => {
|
|
69
|
+
const isCurrentlySelected = isOptionSelected(option);
|
|
70
|
+
let newSelection: Option[];
|
|
71
|
+
|
|
72
|
+
if (isCurrentlySelected) {
|
|
73
|
+
// Remove the option
|
|
74
|
+
newSelection = prev.filter((item) => {
|
|
75
|
+
if (option.id && item.id) {
|
|
76
|
+
return item.id !== option.id;
|
|
77
|
+
}
|
|
78
|
+
return item.label !== option.label;
|
|
79
|
+
});
|
|
80
|
+
} else {
|
|
81
|
+
// Add the option
|
|
82
|
+
newSelection = [...prev, option];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
console.log('[MultiSelectCombobox] New selection:', newSelection);
|
|
86
|
+
if (onSelect) onSelect(newSelection);
|
|
87
|
+
return newSelection;
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const removeSelection = (option: Option) => {
|
|
92
|
+
setSelected((prev) => {
|
|
93
|
+
const newSelection = prev.filter((item) => {
|
|
94
|
+
if (option.id && item.id) {
|
|
95
|
+
return item.id !== option.id;
|
|
96
|
+
}
|
|
97
|
+
return item.label !== option.label;
|
|
98
|
+
});
|
|
99
|
+
if (onSelect) onSelect(newSelection);
|
|
100
|
+
return newSelection;
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const removeExtraSelections = () => {
|
|
105
|
+
setSelected((prev) => {
|
|
106
|
+
const newSelection = prev.slice(0, 3); // Keep only the first 3
|
|
107
|
+
if (onSelect) onSelect(newSelection);
|
|
108
|
+
return newSelection;
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<div className={`relative w-80 ${className}`} ref={comboboxRef}>
|
|
114
|
+
<div
|
|
115
|
+
className={`flex items-center gap-2 border p-2 bg-background rounded cursor-pointer ${isOpen ? 'border-primary' : 'border-input'}`}
|
|
116
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
117
|
+
>
|
|
118
|
+
<div className="flex flex-wrap gap-1 w-full">
|
|
119
|
+
{selected.length > 0 ? (
|
|
120
|
+
<>
|
|
121
|
+
{selected.slice(0, 3).map((item, index) => (
|
|
122
|
+
<Badge
|
|
123
|
+
key={item.id || item.label || index}
|
|
124
|
+
className="flex items-center gap-1 px-2"
|
|
125
|
+
>
|
|
126
|
+
{item.label}
|
|
127
|
+
<X
|
|
128
|
+
size={12}
|
|
129
|
+
className="cursor-pointer"
|
|
130
|
+
onClick={(e) => {
|
|
131
|
+
e.stopPropagation();
|
|
132
|
+
removeSelection(item);
|
|
133
|
+
}}
|
|
134
|
+
/>
|
|
135
|
+
</Badge>
|
|
136
|
+
))}
|
|
137
|
+
{selected.length > 3 && (
|
|
138
|
+
<Badge
|
|
139
|
+
className="px-2 cursor-pointer"
|
|
140
|
+
onClick={(e) => {
|
|
141
|
+
e.stopPropagation();
|
|
142
|
+
removeExtraSelections();
|
|
143
|
+
}}
|
|
144
|
+
>
|
|
145
|
+
+{selected.length - 3} more
|
|
146
|
+
</Badge>
|
|
147
|
+
)}
|
|
148
|
+
</>
|
|
149
|
+
) : (
|
|
150
|
+
<span className="text-muted-foreground">Select agents...</span>
|
|
151
|
+
)}
|
|
152
|
+
</div>
|
|
153
|
+
<ChevronDown size={16} />
|
|
154
|
+
</div>
|
|
155
|
+
{isOpen && (
|
|
156
|
+
<Card className="absolute left-0 mt-2 w-full shadow-md border border-border rounded z-40 max-h-60 overflow-y-auto">
|
|
157
|
+
{options.length === 0 ? (
|
|
158
|
+
<div className="p-2 text-muted-foreground text-sm">No agents available</div>
|
|
159
|
+
) : (
|
|
160
|
+
options.map((option, index) => (
|
|
161
|
+
<div
|
|
162
|
+
key={option.id || option.label || index}
|
|
163
|
+
className={`flex items-center gap-2 p-2 cursor-pointer rounded hover:bg-muted ${
|
|
164
|
+
isOptionSelected(option) ? 'bg-muted' : 'bg-card'
|
|
165
|
+
}`}
|
|
166
|
+
onClick={() => toggleSelection(option)}
|
|
167
|
+
>
|
|
168
|
+
<div className="bg-gray-500 rounded-full w-4 h-4 flex justify-center items-center overflow-hidden text-xs">
|
|
169
|
+
{option.icon ? (
|
|
170
|
+
<img
|
|
171
|
+
src={option.icon}
|
|
172
|
+
alt={option.label}
|
|
173
|
+
className="w-full h-full object-cover"
|
|
174
|
+
/>
|
|
175
|
+
) : (
|
|
176
|
+
formatAgentName(option.label)
|
|
177
|
+
)}
|
|
178
|
+
</div>
|
|
179
|
+
{option.label}
|
|
180
|
+
</div>
|
|
181
|
+
))
|
|
182
|
+
)}
|
|
183
|
+
</Card>
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AlertDialog,
|
|
3
|
+
AlertDialogAction,
|
|
4
|
+
AlertDialogCancel,
|
|
5
|
+
AlertDialogContent,
|
|
6
|
+
AlertDialogDescription,
|
|
7
|
+
AlertDialogFooter,
|
|
8
|
+
AlertDialogHeader,
|
|
9
|
+
AlertDialogTitle,
|
|
10
|
+
} from '@/components/ui/alert-dialog';
|
|
11
|
+
|
|
12
|
+
interface ConfirmationDialogProps {
|
|
13
|
+
open: boolean;
|
|
14
|
+
onOpenChange: (open: boolean) => void;
|
|
15
|
+
title: string;
|
|
16
|
+
description: string;
|
|
17
|
+
confirmText?: string;
|
|
18
|
+
cancelText?: string;
|
|
19
|
+
onConfirm: () => void;
|
|
20
|
+
variant?: 'default' | 'destructive';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default function ConfirmationDialog({
|
|
24
|
+
open,
|
|
25
|
+
onOpenChange,
|
|
26
|
+
title,
|
|
27
|
+
description,
|
|
28
|
+
confirmText = 'Confirm',
|
|
29
|
+
cancelText = 'Cancel',
|
|
30
|
+
onConfirm,
|
|
31
|
+
variant = 'destructive',
|
|
32
|
+
}: ConfirmationDialogProps) {
|
|
33
|
+
const handleConfirm = () => {
|
|
34
|
+
onConfirm();
|
|
35
|
+
onOpenChange(false);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
|
40
|
+
<AlertDialogContent>
|
|
41
|
+
<AlertDialogHeader>
|
|
42
|
+
<AlertDialogTitle>{title}</AlertDialogTitle>
|
|
43
|
+
<AlertDialogDescription>{description}</AlertDialogDescription>
|
|
44
|
+
</AlertDialogHeader>
|
|
45
|
+
<AlertDialogFooter>
|
|
46
|
+
<AlertDialogCancel>{cancelText}</AlertDialogCancel>
|
|
47
|
+
<AlertDialogAction
|
|
48
|
+
onClick={handleConfirm}
|
|
49
|
+
className={
|
|
50
|
+
variant === 'destructive' ? 'bg-destructive text-destructive-foreground' : undefined
|
|
51
|
+
}
|
|
52
|
+
>
|
|
53
|
+
{confirmText}
|
|
54
|
+
</AlertDialogAction>
|
|
55
|
+
</AlertDialogFooter>
|
|
56
|
+
</AlertDialogContent>
|
|
57
|
+
</AlertDialog>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { AlertCircle, ExternalLink } from 'lucide-react';
|
|
2
|
+
import { cn } from '@/lib/utils';
|
|
3
|
+
import { useConnection } from '../context/ConnectionContext';
|
|
4
|
+
|
|
5
|
+
export interface ConnectionErrorBannerProps {
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function ConnectionErrorBanner({ className }: ConnectionErrorBannerProps) {
|
|
10
|
+
const { status, error } = useConnection();
|
|
11
|
+
|
|
12
|
+
const shouldShowBanner = status === 'error' || status === 'unauthorized';
|
|
13
|
+
|
|
14
|
+
if (!shouldShowBanner) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let errorTitle = 'Connection Failed';
|
|
19
|
+
let errorDescription = 'Please ensure the Eliza server is running and accessible.';
|
|
20
|
+
const isUnauthorized = status === 'unauthorized';
|
|
21
|
+
|
|
22
|
+
if (error) {
|
|
23
|
+
const errorMsg = error;
|
|
24
|
+
|
|
25
|
+
if (isUnauthorized) {
|
|
26
|
+
errorTitle = 'Unauthorized';
|
|
27
|
+
errorDescription = 'Invalid or missing API Key provided.';
|
|
28
|
+
} else if (errorMsg.includes('NetworkError') || errorMsg.includes('Failed to fetch')) {
|
|
29
|
+
errorTitle = 'Network Error';
|
|
30
|
+
errorDescription = 'Cannot reach the server. Please check your network connection.';
|
|
31
|
+
} else if (errorMsg.includes('ECONNREFUSED')) {
|
|
32
|
+
errorTitle = 'Connection Refused';
|
|
33
|
+
errorDescription = 'The server refused the connection. Please ensure it is running.';
|
|
34
|
+
} else if (errorMsg.includes('timeout')) {
|
|
35
|
+
errorTitle = 'Connection Timeout';
|
|
36
|
+
errorDescription = 'The server took too long to respond.';
|
|
37
|
+
} else if (
|
|
38
|
+
errorMsg.includes('404') ||
|
|
39
|
+
errorMsg.includes('not found') ||
|
|
40
|
+
errorMsg.includes('API endpoint not found') ||
|
|
41
|
+
errorMsg.includes('Endpoint not found')
|
|
42
|
+
) {
|
|
43
|
+
errorTitle = 'Endpoint Not Found';
|
|
44
|
+
errorDescription = 'The server API endpoint could not be found.';
|
|
45
|
+
} else {
|
|
46
|
+
// Use the provided error message directly for other cases
|
|
47
|
+
errorDescription = errorMsg;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div
|
|
53
|
+
className={cn(
|
|
54
|
+
'bg-opacity-10 border rounded-md p-3 mb-4 w-full md:max-w-4xl',
|
|
55
|
+
'flex items-center justify-between w-full mt-4',
|
|
56
|
+
isUnauthorized
|
|
57
|
+
? 'bg-yellow-900/20 border-yellow-700 text-yellow-100'
|
|
58
|
+
: 'bg-red-900/20 border-red-700 text-red-100',
|
|
59
|
+
className
|
|
60
|
+
)}
|
|
61
|
+
>
|
|
62
|
+
<div className="flex items-center space-x-3">
|
|
63
|
+
<AlertCircle
|
|
64
|
+
className={cn(
|
|
65
|
+
'h-5 w-5 flex-shrink-0',
|
|
66
|
+
isUnauthorized ? 'text-yellow-400' : 'text-red-400'
|
|
67
|
+
)}
|
|
68
|
+
/>
|
|
69
|
+
<div>
|
|
70
|
+
<h4
|
|
71
|
+
className={cn(
|
|
72
|
+
'font-medium text-sm',
|
|
73
|
+
isUnauthorized ? 'text-yellow-200' : 'text-red-200'
|
|
74
|
+
)}
|
|
75
|
+
>
|
|
76
|
+
{errorTitle}
|
|
77
|
+
</h4>
|
|
78
|
+
<p className={cn('text-xs mt-1', isUnauthorized ? 'text-yellow-300' : 'text-red-300')}>
|
|
79
|
+
{errorDescription}
|
|
80
|
+
</p>
|
|
81
|
+
<div className="mt-2 flex space-x-4">
|
|
82
|
+
<a
|
|
83
|
+
href="https://eliza.how"
|
|
84
|
+
target="_blank"
|
|
85
|
+
rel="noopener noreferrer"
|
|
86
|
+
className={cn(
|
|
87
|
+
'text-xs flex items-center',
|
|
88
|
+
isUnauthorized
|
|
89
|
+
? 'hover:text-yellow-200 text-yellow-300'
|
|
90
|
+
: 'hover:text-red-200 text-red-300'
|
|
91
|
+
)}
|
|
92
|
+
>
|
|
93
|
+
<ExternalLink className="h-3 w-3 mr-1" />
|
|
94
|
+
Troubleshooting Guide
|
|
95
|
+
</a>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
// Mock the context modules before importing the component
|
|
4
|
+
beforeEach(() => {
|
|
5
|
+
cy.stub(React, 'useContext').callsFake((context: any) => {
|
|
6
|
+
if (context.displayName === 'AuthContext' || context._currentValue === undefined) {
|
|
7
|
+
return {
|
|
8
|
+
openApiKeyDialog: cy.stub(),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
return context._currentValue;
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
import ConnectionStatus from './connection-status';
|
|
16
|
+
|
|
17
|
+
describe('ConnectionStatus Component', () => {
|
|
18
|
+
it('renders connection status component', () => {
|
|
19
|
+
cy.mount(<ConnectionStatus />);
|
|
20
|
+
|
|
21
|
+
// Component should render without errors
|
|
22
|
+
cy.get('*').should('exist');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('renders with default state', () => {
|
|
26
|
+
cy.mount(<ConnectionStatus />);
|
|
27
|
+
|
|
28
|
+
// Component should render
|
|
29
|
+
cy.get('*').should('exist');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('has accessible elements', () => {
|
|
33
|
+
cy.mount(<ConnectionStatus />);
|
|
34
|
+
|
|
35
|
+
// Should render successfully with mocked context
|
|
36
|
+
cy.get('*').should('exist');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('handles click interactions', () => {
|
|
40
|
+
cy.mount(<ConnectionStatus />);
|
|
41
|
+
|
|
42
|
+
// Component should render successfully
|
|
43
|
+
cy.get('*').should('exist');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('displays status indicator', () => {
|
|
47
|
+
cy.mount(<ConnectionStatus />);
|
|
48
|
+
|
|
49
|
+
// Should render with mock contexts
|
|
50
|
+
cy.get('*').should('exist');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('supports hover interactions', () => {
|
|
54
|
+
cy.mount(<ConnectionStatus />);
|
|
55
|
+
|
|
56
|
+
// Component should render without errors
|
|
57
|
+
cy.get('*').should('exist');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('maintains consistent styling', () => {
|
|
61
|
+
cy.mount(<ConnectionStatus />);
|
|
62
|
+
|
|
63
|
+
// Should render successfully
|
|
64
|
+
cy.get('*').should('exist');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('renders without console errors', () => {
|
|
68
|
+
cy.mount(<ConnectionStatus />);
|
|
69
|
+
|
|
70
|
+
// Basic smoke test - component renders
|
|
71
|
+
cy.get('*').should('exist');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { cn } from '@/lib/utils';
|
|
2
|
+
import { AlertCircle, CheckCircle } from 'lucide-react';
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import { SidebarMenuButton, SidebarMenuItem } from './ui/sidebar';
|
|
5
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
|
|
6
|
+
import { useToast } from '@/hooks/use-toast';
|
|
7
|
+
import { useAuth } from '../context/AuthContext';
|
|
8
|
+
import { useConnection } from '../context/ConnectionContext';
|
|
9
|
+
|
|
10
|
+
export interface ConnectionStatusProps {
|
|
11
|
+
// Allow standard HTML attributes like className
|
|
12
|
+
[key: string]: any;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default function ConnectionStatus() {
|
|
16
|
+
const { toast } = useToast();
|
|
17
|
+
const [prevStatus, setPrevStatus] = useState<string | null>(null);
|
|
18
|
+
const { openApiKeyDialog } = useAuth();
|
|
19
|
+
const { status, error } = useConnection();
|
|
20
|
+
|
|
21
|
+
// Derive states from context
|
|
22
|
+
const isLoading = status === 'loading';
|
|
23
|
+
const isConnected = status === 'connected';
|
|
24
|
+
const isError = status === 'error';
|
|
25
|
+
const isUnauthorized = status === 'unauthorized'; // Context handles this now
|
|
26
|
+
const showingError = isError || isUnauthorized;
|
|
27
|
+
|
|
28
|
+
// Track connection state changes and show appropriate toast notifications
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
// Transition from disconnected/error/unauthorized to connected
|
|
31
|
+
if (
|
|
32
|
+
(prevStatus === 'error' || prevStatus === 'unauthorized' || prevStatus === 'disconnected') &&
|
|
33
|
+
isConnected
|
|
34
|
+
) {
|
|
35
|
+
toast({
|
|
36
|
+
title: 'Connection Restored',
|
|
37
|
+
description: (
|
|
38
|
+
<div className="flex items-center gap-2">
|
|
39
|
+
<CheckCircle className="h-4 w-4 text-green-500" />
|
|
40
|
+
<span>Successfully reconnected to the Eliza server.</span>
|
|
41
|
+
</div>
|
|
42
|
+
),
|
|
43
|
+
});
|
|
44
|
+
} else if (prevStatus === 'connected' && (isError || isUnauthorized)) {
|
|
45
|
+
// Transition from connected to error/unauthorized
|
|
46
|
+
toast({
|
|
47
|
+
title: 'Connection Lost',
|
|
48
|
+
description: 'Attempting to reconnect to the Eliza server...',
|
|
49
|
+
variant: 'destructive',
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Update the connection state tracking for the next render
|
|
54
|
+
setPrevStatus(status);
|
|
55
|
+
}, [status, prevStatus, isConnected, isError, isUnauthorized, toast]);
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
// If the status becomes unauthorized, trigger the API key dialog
|
|
59
|
+
if (isUnauthorized && prevStatus !== 'unauthorized') {
|
|
60
|
+
openApiKeyDialog(); // Trigger the global dialog
|
|
61
|
+
}
|
|
62
|
+
}, [isUnauthorized, prevStatus, openApiKeyDialog]);
|
|
63
|
+
|
|
64
|
+
const getStatusColor = () => {
|
|
65
|
+
if (isUnauthorized) return 'bg-yellow-500';
|
|
66
|
+
if (isLoading) return 'bg-muted-foreground';
|
|
67
|
+
return isConnected ? 'bg-green-600' : 'bg-red-600'; // Treat error/disconnected as red
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const getStatusText = () => {
|
|
71
|
+
if (isUnauthorized) return 'Unauthorized';
|
|
72
|
+
if (isLoading) return 'Connecting...';
|
|
73
|
+
return isConnected ? 'Connected' : 'Disconnected';
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const getTextColor = () => {
|
|
77
|
+
if (isUnauthorized) return 'text-yellow-500';
|
|
78
|
+
if (isLoading) return 'text-muted-foreground';
|
|
79
|
+
return isConnected ? 'text-green-600' : 'text-red-600';
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// Get a specific error message based on the error
|
|
83
|
+
const getErrorMessage = () => {
|
|
84
|
+
if (!error) return 'Connection failed'; // Use error from context
|
|
85
|
+
|
|
86
|
+
// Specific check for Unauthorized first
|
|
87
|
+
if (isUnauthorized) {
|
|
88
|
+
return 'Unauthorized: Invalid or missing API Key. Check client configuration or server logs.';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// The context already provides the error message string
|
|
92
|
+
if (error.includes('NetworkError') || error.includes('Failed to fetch')) {
|
|
93
|
+
return 'Cannot reach server';
|
|
94
|
+
} else if (error.includes('ECONNREFUSED')) {
|
|
95
|
+
return 'Connection refused';
|
|
96
|
+
} else if (error.includes('timeout')) {
|
|
97
|
+
return 'Connection timeout';
|
|
98
|
+
} else if (
|
|
99
|
+
error.includes('404') ||
|
|
100
|
+
error.includes('not found') ||
|
|
101
|
+
error.includes('API endpoint not found')
|
|
102
|
+
) {
|
|
103
|
+
return 'Endpoint not found';
|
|
104
|
+
}
|
|
105
|
+
return error; // Return the error message directly
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<SidebarMenuItem data-testid="connection-status">
|
|
110
|
+
<Tooltip>
|
|
111
|
+
<TooltipTrigger asChild>
|
|
112
|
+
<SidebarMenuButton className="rounded">
|
|
113
|
+
<div className="flex flex-col gap-1 select-none">
|
|
114
|
+
<div className="flex items-center gap-1">
|
|
115
|
+
{showingError || isUnauthorized ? (
|
|
116
|
+
// Use AlertCircle for both general errors and unauthorized, but color differently
|
|
117
|
+
<AlertCircle
|
|
118
|
+
className={cn(
|
|
119
|
+
'h-3.5 w-3.5',
|
|
120
|
+
isUnauthorized ? 'text-yellow-500' : 'text-red-600'
|
|
121
|
+
)}
|
|
122
|
+
/>
|
|
123
|
+
) : (
|
|
124
|
+
<div className={cn(['h-2.5 w-2.5 rounded-full', getStatusColor()])} />
|
|
125
|
+
)}
|
|
126
|
+
<span className={cn('text-xs', getTextColor())}>{getStatusText()}</span>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
</SidebarMenuButton>
|
|
130
|
+
</TooltipTrigger>
|
|
131
|
+
{showingError && (
|
|
132
|
+
<TooltipContent side="top" align="center" className="max-w-xs">
|
|
133
|
+
<div className="flex flex-col gap-2">
|
|
134
|
+
<div
|
|
135
|
+
className={cn('font-semibold', isUnauthorized ? 'text-yellow-500' : 'text-red-500')}
|
|
136
|
+
>
|
|
137
|
+
{getErrorMessage()}
|
|
138
|
+
</div>
|
|
139
|
+
<p className="text-xs">Please ensure the Eliza server is running and accessible.</p>
|
|
140
|
+
{!isUnauthorized && (
|
|
141
|
+
<p className="text-xs">Try refreshing the connection or check server logs.</p>
|
|
142
|
+
)}
|
|
143
|
+
{isUnauthorized && (
|
|
144
|
+
<p className="text-xs">
|
|
145
|
+
Check the X-API-KEY configured in your client or the ELIZA_SERVER_AUTH_TOKEN on
|
|
146
|
+
the server.
|
|
147
|
+
</p>
|
|
148
|
+
)}
|
|
149
|
+
</div>
|
|
150
|
+
</TooltipContent>
|
|
151
|
+
)}
|
|
152
|
+
</Tooltip>
|
|
153
|
+
</SidebarMenuItem>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Button } from '@/components/ui/button';
|
|
2
|
+
import { Check, Copy } from 'lucide-react';
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
|
|
5
|
+
|
|
6
|
+
const CopyButton = ({ text }: { text: string }) => {
|
|
7
|
+
const [copied, setCopied] = useState(false);
|
|
8
|
+
|
|
9
|
+
const handleCopy = () => {
|
|
10
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
11
|
+
setCopied(true);
|
|
12
|
+
setTimeout(() => setCopied(false), 2000); // Reset after 2 seconds
|
|
13
|
+
});
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<Tooltip>
|
|
18
|
+
<TooltipTrigger asChild>
|
|
19
|
+
<Button
|
|
20
|
+
onClick={handleCopy}
|
|
21
|
+
variant="ghost"
|
|
22
|
+
size="icon"
|
|
23
|
+
className="flex items-center space-x-2 text-muted-foreground"
|
|
24
|
+
>
|
|
25
|
+
{copied ? <Check className="size-3" /> : <Copy className="size-3" />}
|
|
26
|
+
</Button>
|
|
27
|
+
</TooltipTrigger>
|
|
28
|
+
<TooltipContent side="bottom">
|
|
29
|
+
<p>Copy</p>
|
|
30
|
+
</TooltipContent>
|
|
31
|
+
</Tooltip>
|
|
32
|
+
);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default CopyButton;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Button } from '@/components/ui/button';
|
|
2
|
+
import { Trash2 } from 'lucide-react';
|
|
3
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
|
|
4
|
+
|
|
5
|
+
interface DeleteButtonProps {
|
|
6
|
+
onClick: () => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const DeleteButton = ({ onClick }: DeleteButtonProps) => {
|
|
10
|
+
return (
|
|
11
|
+
<Tooltip>
|
|
12
|
+
<TooltipTrigger asChild>
|
|
13
|
+
<Button onClick={onClick} variant="ghost" size="icon" className="text-muted-foreground">
|
|
14
|
+
<Trash2 className="size-3" />
|
|
15
|
+
</Button>
|
|
16
|
+
</TooltipTrigger>
|
|
17
|
+
<TooltipContent side="bottom">
|
|
18
|
+
<p>Delete</p>
|
|
19
|
+
</TooltipContent>
|
|
20
|
+
</Tooltip>
|
|
21
|
+
);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default DeleteButton;
|