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