@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,128 @@
|
|
|
1
|
+
import ChatComponent from '@/components/chat';
|
|
2
|
+
import { Button } from '@/components/ui/button';
|
|
3
|
+
import { useAgentManagement } from '@/hooks/use-agent-management';
|
|
4
|
+
import { useAgent } from '@/hooks/use-query-hooks';
|
|
5
|
+
import clientLogger from '@/lib/logger';
|
|
6
|
+
import {
|
|
7
|
+
type Agent,
|
|
8
|
+
ChannelType,
|
|
9
|
+
AgentStatus as CoreAgentStatusEnum,
|
|
10
|
+
type UUID,
|
|
11
|
+
} from '@elizaos/core';
|
|
12
|
+
import { Loader2, Play, Settings } from 'lucide-react';
|
|
13
|
+
import { useEffect } from 'react';
|
|
14
|
+
import { useParams, useNavigate } from 'react-router';
|
|
15
|
+
import type { AgentWithStatus } from '../types';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Displays the agent chat interface with an optional details sidebar in a resizable layout.
|
|
19
|
+
*
|
|
20
|
+
* Renders the chat panel for a specific agent, and conditionally shows a sidebar with agent details based on user interaction. If no agent ID is present in the URL, displays a "No data." message.
|
|
21
|
+
*/
|
|
22
|
+
export default function AgentRoute() {
|
|
23
|
+
// useParams will include agentId and optionally channelId for /chat/:agentId/:channelId routes
|
|
24
|
+
const { agentId, channelId } = useParams<{ agentId: UUID; channelId?: UUID }>();
|
|
25
|
+
const navigate = useNavigate();
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
clientLogger.info('[AgentRoute] Component mounted/updated', { agentId, channelId });
|
|
29
|
+
return () => {
|
|
30
|
+
clientLogger.info('[AgentRoute] Component unmounted', { agentId, channelId });
|
|
31
|
+
};
|
|
32
|
+
}, [agentId, channelId]);
|
|
33
|
+
|
|
34
|
+
const { data: agentDataResponse, isLoading: isLoadingAgent } = useAgent(agentId);
|
|
35
|
+
const { startAgent, isAgentStarting } = useAgentManagement();
|
|
36
|
+
|
|
37
|
+
const agentFromHook: Agent | undefined = agentDataResponse?.data
|
|
38
|
+
? ({
|
|
39
|
+
...(agentDataResponse.data as AgentWithStatus),
|
|
40
|
+
status:
|
|
41
|
+
agentDataResponse.data.status === 'active'
|
|
42
|
+
? CoreAgentStatusEnum.ACTIVE
|
|
43
|
+
: agentDataResponse.data.status === 'inactive'
|
|
44
|
+
? CoreAgentStatusEnum.INACTIVE
|
|
45
|
+
: CoreAgentStatusEnum.INACTIVE,
|
|
46
|
+
username: agentDataResponse.data.username || agentDataResponse.data.name || 'Unknown',
|
|
47
|
+
bio: agentDataResponse.data.bio || '',
|
|
48
|
+
messageExamples: agentDataResponse.data.messageExamples || [],
|
|
49
|
+
postExamples: agentDataResponse.data.postExamples || [],
|
|
50
|
+
topics: agentDataResponse.data.topics || [],
|
|
51
|
+
adjectives: agentDataResponse.data.adjectives || [],
|
|
52
|
+
knowledge: agentDataResponse.data.knowledge || [],
|
|
53
|
+
plugins: agentDataResponse.data.plugins || [],
|
|
54
|
+
settings: agentDataResponse.data.settings || {},
|
|
55
|
+
secrets: (agentDataResponse.data as any).secrets || {},
|
|
56
|
+
style: agentDataResponse.data.style || {},
|
|
57
|
+
templates: agentDataResponse.data.templates || {},
|
|
58
|
+
enabled:
|
|
59
|
+
typeof agentDataResponse.data.enabled === 'boolean'
|
|
60
|
+
? agentDataResponse.data.enabled
|
|
61
|
+
: true,
|
|
62
|
+
createdAt:
|
|
63
|
+
typeof agentDataResponse.data.createdAt === 'number'
|
|
64
|
+
? agentDataResponse.data.createdAt
|
|
65
|
+
: Date.now(),
|
|
66
|
+
updatedAt:
|
|
67
|
+
typeof agentDataResponse.data.updatedAt === 'number'
|
|
68
|
+
? agentDataResponse.data.updatedAt
|
|
69
|
+
: Date.now(),
|
|
70
|
+
} as Agent)
|
|
71
|
+
: undefined;
|
|
72
|
+
|
|
73
|
+
if (!agentId) return <div className="p-4">Agent ID not provided.</div>;
|
|
74
|
+
if (isLoadingAgent || !agentFromHook)
|
|
75
|
+
return (
|
|
76
|
+
<div className="p-4 flex items-center justify-center h-full">
|
|
77
|
+
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const isActive = agentFromHook.status === CoreAgentStatusEnum.ACTIVE;
|
|
82
|
+
const isStarting = isAgentStarting(agentFromHook.id);
|
|
83
|
+
|
|
84
|
+
const handleStartAgent = () => {
|
|
85
|
+
if (agentFromHook) {
|
|
86
|
+
startAgent(agentFromHook);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
if (!isActive) {
|
|
91
|
+
clientLogger.info('[AgentRoute] Agent is not active, rendering inactive state UI', {
|
|
92
|
+
agentName: agentFromHook?.name,
|
|
93
|
+
});
|
|
94
|
+
return (
|
|
95
|
+
<div className="flex flex-col items-center justify-center h-full w-full p-8 text-center">
|
|
96
|
+
<h2 className="text-2xl font-semibold mb-4">{agentFromHook.name} is not active.</h2>
|
|
97
|
+
<p className="text-muted-foreground mb-6">Press the button below to start this agent.</p>
|
|
98
|
+
<div className="flex gap-3">
|
|
99
|
+
<Button onClick={() => navigate(`/settings/${agentId}`)} variant="outline" size="lg">
|
|
100
|
+
<Settings className="h-5 w-5" />
|
|
101
|
+
</Button>
|
|
102
|
+
<Button onClick={handleStartAgent} disabled={isStarting} size="lg">
|
|
103
|
+
{isStarting ? (
|
|
104
|
+
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
|
|
105
|
+
) : (
|
|
106
|
+
<Play className="mr-2 h-5 w-5" />
|
|
107
|
+
)}
|
|
108
|
+
{isStarting ? 'Starting Agent...' : 'Start Agent'}
|
|
109
|
+
</Button>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
clientLogger.info('[AgentRoute] Agent is active, rendering chat for DM', {
|
|
116
|
+
agentName: agentFromHook?.name,
|
|
117
|
+
dmChannelIdFromRoute: channelId,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<ChatComponent
|
|
122
|
+
key={`${agentId}-${channelId || 'no-dm-channel'}`}
|
|
123
|
+
chatType={ChannelType.DM}
|
|
124
|
+
contextId={agentId}
|
|
125
|
+
initialDmChannelId={channelId}
|
|
126
|
+
/>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import AgentCreator from '@/components/agent-creator';
|
|
2
|
+
|
|
3
|
+
export default function AgentCreatorRoute() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="h-full w-full overflow-y-auto">
|
|
6
|
+
<div className="min-h-full flex w-full justify-center px-4 sm:px-6 py-4">
|
|
7
|
+
<div className="w-full max-w-4xl min-w-0">
|
|
8
|
+
<AgentCreator />
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { useNavigate } from 'react-router-dom';
|
|
3
|
+
// import { CreateGroupDialog } from '@/components/create-group-dialog'; // To be removed
|
|
4
|
+
import GroupPanel from '@/components/group-panel'; // Import GroupPanel
|
|
5
|
+
// import { useAgentsWithDetails, useServers } from '@/hooks/use-query-hooks'; // No longer needed if GroupPanel fetches its own agents
|
|
6
|
+
import type { UUID } from '@elizaos/core';
|
|
7
|
+
|
|
8
|
+
export default function GroupNew() {
|
|
9
|
+
const navigate = useNavigate();
|
|
10
|
+
// const [open, setOpen] = useState(true); // GroupPanel typically manages its own visibility or is used as a page component
|
|
11
|
+
// const { data: serversData } = useServers();
|
|
12
|
+
// const { data: agentsData, isLoading: isLoadingAgents } = useAgentsWithDetails(); // GroupPanel fetches its own agents
|
|
13
|
+
// const [selectedServerId, setSelectedServerId] = useState<UUID | null>(null); // GroupPanel will use DEFAULT_SERVER_ID
|
|
14
|
+
|
|
15
|
+
// useEffect(() => {
|
|
16
|
+
// // Use the first available server or create one if needed
|
|
17
|
+
// if (serversData?.data?.servers && serversData.data.servers.length > 0) {
|
|
18
|
+
// setSelectedServerId(serversData.data.servers[0].id);
|
|
19
|
+
// }
|
|
20
|
+
// }, [serversData]);
|
|
21
|
+
|
|
22
|
+
// const handleOpenChange = (open: boolean) => {
|
|
23
|
+
// setOpen(open);
|
|
24
|
+
// if (!open) {
|
|
25
|
+
// // Navigate back to home when dialog is closed
|
|
26
|
+
// navigate('/');
|
|
27
|
+
// }
|
|
28
|
+
// };
|
|
29
|
+
|
|
30
|
+
// if (!selectedServerId) { // GroupPanel handles server ID internally or gets it via props if needed for specific server contexts
|
|
31
|
+
// return (
|
|
32
|
+
// <div className="flex items-center justify-center h-screen">
|
|
33
|
+
// <p>Loading servers...</p>
|
|
34
|
+
// </div>
|
|
35
|
+
// );
|
|
36
|
+
// }
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
// <CreateGroupDialog open={open} onOpenChange={handleOpenChange} serverId={selectedServerId} />
|
|
40
|
+
// Render GroupPanel directly as the route's content
|
|
41
|
+
// GroupPanel will handle its own logic for fetching serverId (default) or if it were to be passed.
|
|
42
|
+
<div className="pt-4 md:pt-8">
|
|
43
|
+
<GroupPanel
|
|
44
|
+
// agents={agents} // Removed prop
|
|
45
|
+
onClose={() => navigate(-1)} // Navigate back on close
|
|
46
|
+
// channelId is undefined, so it's in "create" mode
|
|
47
|
+
/>
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import ChatComponent from '@/components/chat';
|
|
2
|
+
import { ChannelType, validateUuid, type UUID } from '@elizaos/core';
|
|
3
|
+
import { useParams, useSearchParams } from 'react-router-dom';
|
|
4
|
+
|
|
5
|
+
export default function GroupRoute() {
|
|
6
|
+
const { channelId: channelIdFromPath } = useParams<{ channelId: string }>();
|
|
7
|
+
const [searchParams] = useSearchParams();
|
|
8
|
+
const serverIdFromQuery = searchParams.get('serverId');
|
|
9
|
+
|
|
10
|
+
const channelId = validateUuid(channelIdFromPath);
|
|
11
|
+
const serverId = validateUuid(serverIdFromQuery || '');
|
|
12
|
+
|
|
13
|
+
if (!channelId || !serverId) {
|
|
14
|
+
return (
|
|
15
|
+
<div className="flex flex-1 justify-center items-center">
|
|
16
|
+
<p>Missing channel or server information.</p>
|
|
17
|
+
</div>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<ChatComponent
|
|
23
|
+
key={channelId}
|
|
24
|
+
chatType={ChannelType.GROUP}
|
|
25
|
+
contextId={channelId as UUID}
|
|
26
|
+
serverId={serverId as UUID}
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import AgentCard from '@/components/agent-card';
|
|
2
|
+
import GroupCard from '@/components/group-card';
|
|
3
|
+
import GroupPanel from '@/components/group-panel';
|
|
4
|
+
import ProfileOverlay from '@/components/profile-overlay';
|
|
5
|
+
import { useAgentsWithDetails, useChannels, useServers } from '@/hooks/use-query-hooks';
|
|
6
|
+
import clientLogger from '@/lib/logger';
|
|
7
|
+
import { type Agent, type UUID, ChannelType as CoreChannelType, AgentStatus } from '@elizaos/core';
|
|
8
|
+
import type { MessageChannel, MessageServer } from '@/types';
|
|
9
|
+
import { Plus } from 'lucide-react';
|
|
10
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
11
|
+
import { useNavigate } from 'react-router-dom';
|
|
12
|
+
import { Button } from '../components/ui/button';
|
|
13
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../components/ui/tabs';
|
|
14
|
+
import { Separator } from '@/components/ui/separator';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Renders the main dashboard for managing agents and groups, providing interactive controls for viewing, starting, messaging, and configuring agents, as well as creating and editing groups.
|
|
18
|
+
*
|
|
19
|
+
* Displays lists of agents and groups with status indicators, action buttons, and overlays for detailed views and settings. Handles loading and error states, and supports navigation to chat and settings pages.
|
|
20
|
+
*/
|
|
21
|
+
export default function Home() {
|
|
22
|
+
const { data: agentsData, isLoading, isError, error } = useAgentsWithDetails();
|
|
23
|
+
const navigate = useNavigate();
|
|
24
|
+
|
|
25
|
+
// Extract agents properly from the response
|
|
26
|
+
const agents = useMemo(() => agentsData?.agents || [], [agentsData]);
|
|
27
|
+
const activeAgentsCount = agents.filter((a) => a.status === AgentStatus.ACTIVE).length;
|
|
28
|
+
|
|
29
|
+
const { data: serversData } = useServers() as {
|
|
30
|
+
data: { data: { servers: MessageServer[] } } | undefined;
|
|
31
|
+
};
|
|
32
|
+
const servers = serversData?.data?.servers || [];
|
|
33
|
+
|
|
34
|
+
const [isOverlayOpen, setOverlayOpen] = useState(false);
|
|
35
|
+
const [isGroupPanelOpen, setIsGroupPanelOpen] = useState(false);
|
|
36
|
+
const [selectedAgent, setSelectedAgent] = useState<Partial<Agent> | null>(null);
|
|
37
|
+
const [selectedGroupId, setSelectedGroupId] = useState<UUID | null>(null);
|
|
38
|
+
const [activeTab, setActiveTab] = useState('agents');
|
|
39
|
+
|
|
40
|
+
const closeOverlay = () => {
|
|
41
|
+
setSelectedAgent(null);
|
|
42
|
+
setOverlayOpen(false);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const handleNavigateToDm = async (agent: Partial<Agent>, forceNew: boolean) => {
|
|
46
|
+
if (!agent.id) return;
|
|
47
|
+
// Navigate directly to agent chat - DM channel will be created automatically with default server
|
|
48
|
+
navigate(`/chat/${agent.id}`, { state: { forceNew } });
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const handleCreateGroup = () => {
|
|
52
|
+
navigate('/group/new');
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
clientLogger.info('[Home] Component mounted/re-rendered. Key might have changed.');
|
|
57
|
+
// You might want to trigger data re-fetching here if it's not automatic
|
|
58
|
+
// e.g., queryClient.invalidateQueries(['agents']);
|
|
59
|
+
}, []); // Empty dependency array means this runs on mount and when key changes
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<>
|
|
63
|
+
<div className="flex-1 w-full overflow-y-auto bg-background">
|
|
64
|
+
<div className="flex flex-col w-full h-full">
|
|
65
|
+
<Tabs
|
|
66
|
+
value={activeTab}
|
|
67
|
+
onValueChange={setActiveTab}
|
|
68
|
+
className="w-full h-full flex flex-col"
|
|
69
|
+
>
|
|
70
|
+
<div className="w-full">
|
|
71
|
+
<div className="w-full md:max-w-4xl mx-auto px-6 pt-6 pb-2">
|
|
72
|
+
<div className="flex justify-between items-center mb-3">
|
|
73
|
+
<TabsList className="h-auto p-0 bg-transparent border-0 border-b-0 gap-2 w-auto">
|
|
74
|
+
<TabsTrigger
|
|
75
|
+
value="agents"
|
|
76
|
+
className="relative rounded-full data-[state=active]:border-b-0 data-[state=active]:bg-white data-[state=active]:text-black data-[state=active]:font-bold cursor-pointer text-lg py-1"
|
|
77
|
+
>
|
|
78
|
+
Agents
|
|
79
|
+
<span
|
|
80
|
+
className={`
|
|
81
|
+
absolute -top-2.5 right-0 inline-flex items-center justify-center h-5 w-5 rounded-full bg-blue-600 text-white text-[8px] font-semibold border border-black
|
|
82
|
+
transition-all duration-300 ease-in-out
|
|
83
|
+
${activeTab === 'agents' && activeAgentsCount > 0 ? 'opacity-100 scale-100' : 'opacity-0 scale-75 pointer-events-none'}
|
|
84
|
+
`}
|
|
85
|
+
>
|
|
86
|
+
{activeAgentsCount}
|
|
87
|
+
</span>
|
|
88
|
+
</TabsTrigger>
|
|
89
|
+
<TabsTrigger
|
|
90
|
+
value="groups"
|
|
91
|
+
className="rounded-full data-[state=active]:border-b-0 data-[state=active]:bg-white data-[state=active]:text-black data-[state=active]:font-bold cursor-pointer text-lg py-1"
|
|
92
|
+
>
|
|
93
|
+
Groups
|
|
94
|
+
</TabsTrigger>
|
|
95
|
+
</TabsList>
|
|
96
|
+
<Button
|
|
97
|
+
variant="ghost"
|
|
98
|
+
onClick={() => {
|
|
99
|
+
if (activeTab === 'agents') {
|
|
100
|
+
navigate('/create');
|
|
101
|
+
} else {
|
|
102
|
+
handleCreateGroup();
|
|
103
|
+
}
|
|
104
|
+
}}
|
|
105
|
+
className="create-agent-button cursor-pointer gap-1"
|
|
106
|
+
>
|
|
107
|
+
<Plus className="w-4 h-4" />
|
|
108
|
+
{activeTab === 'agents' ? 'Create New Agent' : 'Create New Group'}
|
|
109
|
+
</Button>
|
|
110
|
+
</div>
|
|
111
|
+
<Separator />
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<TabsContent value="agents" className="flex-1 mt-0 bg-background">
|
|
116
|
+
<div className="flex flex-col gap-6 w-full md:max-w-4xl mx-auto px-6 py-2">
|
|
117
|
+
{isLoading && <div className="text-center py-8">Loading agents...</div>}
|
|
118
|
+
|
|
119
|
+
{isError && (
|
|
120
|
+
<div className="text-center py-8">
|
|
121
|
+
Error loading agents: {error instanceof Error ? error.message : 'Unknown error'}
|
|
122
|
+
</div>
|
|
123
|
+
)}
|
|
124
|
+
|
|
125
|
+
{agents.length === 0 && !isLoading && (
|
|
126
|
+
<div className="text-center py-8 flex flex-col items-center gap-4">
|
|
127
|
+
<p className="text-muted-foreground">
|
|
128
|
+
No agents currently running. Start a character to begin.
|
|
129
|
+
</p>
|
|
130
|
+
</div>
|
|
131
|
+
)}
|
|
132
|
+
|
|
133
|
+
{!isLoading && !isError && (
|
|
134
|
+
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-2 gap-3 agents-section">
|
|
135
|
+
{agents
|
|
136
|
+
.sort((a, b) => {
|
|
137
|
+
// Sort by status - ACTIVE agents first
|
|
138
|
+
const aActive = a.status === AgentStatus.ACTIVE ? 1 : 0;
|
|
139
|
+
const bActive = b.status === AgentStatus.ACTIVE ? 1 : 0;
|
|
140
|
+
return bActive - aActive;
|
|
141
|
+
})
|
|
142
|
+
.map((agent) => {
|
|
143
|
+
return (
|
|
144
|
+
<AgentCard
|
|
145
|
+
key={agent.id}
|
|
146
|
+
agent={agent}
|
|
147
|
+
onChat={(forceNew) => handleNavigateToDm(agent as Agent, forceNew)}
|
|
148
|
+
/>
|
|
149
|
+
);
|
|
150
|
+
})}
|
|
151
|
+
</div>
|
|
152
|
+
)}
|
|
153
|
+
</div>
|
|
154
|
+
</TabsContent>
|
|
155
|
+
|
|
156
|
+
<TabsContent value="groups" className="flex-1 mt-0 bg-background">
|
|
157
|
+
<div className="flex flex-col gap-6 w-full md:max-w-4xl mx-auto px-6 py-2">
|
|
158
|
+
{!isLoading && !isError && (
|
|
159
|
+
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-2 gap-3 groups-section">
|
|
160
|
+
{servers.map((server: MessageServer) => (
|
|
161
|
+
<ServerChannels key={server.id} serverId={server.id} />
|
|
162
|
+
))}
|
|
163
|
+
</div>
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
166
|
+
</TabsContent>
|
|
167
|
+
</Tabs>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
{selectedAgent?.id && (
|
|
172
|
+
<ProfileOverlay isOpen={isOverlayOpen} onClose={closeOverlay} agentId={selectedAgent.id} />
|
|
173
|
+
)}
|
|
174
|
+
|
|
175
|
+
{isGroupPanelOpen && (
|
|
176
|
+
<GroupPanel
|
|
177
|
+
onClose={() => {
|
|
178
|
+
setSelectedGroupId(null);
|
|
179
|
+
setIsGroupPanelOpen(false);
|
|
180
|
+
}}
|
|
181
|
+
channelId={selectedGroupId ?? undefined}
|
|
182
|
+
/>
|
|
183
|
+
)}
|
|
184
|
+
</>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Sub-component to fetch and display channels for a given server
|
|
189
|
+
const ServerChannels = React.memo(({ serverId }: { serverId: UUID }) => {
|
|
190
|
+
const { data: channelsData, isLoading: isLoadingChannels } = useChannels(serverId) as {
|
|
191
|
+
data: { data: { channels: MessageChannel[] } } | undefined;
|
|
192
|
+
isLoading: boolean;
|
|
193
|
+
};
|
|
194
|
+
const groupChannels = useMemo(
|
|
195
|
+
() =>
|
|
196
|
+
channelsData?.data?.channels?.filter(
|
|
197
|
+
(ch: MessageChannel) => ch.type === CoreChannelType.GROUP
|
|
198
|
+
) || [],
|
|
199
|
+
[channelsData]
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
if (isLoadingChannels) return <p>Loading channels for server...</p>;
|
|
203
|
+
if (!groupChannels || groupChannels.length === 0)
|
|
204
|
+
return <p className="text-sm text-muted-foreground">No group channels in this server.</p>;
|
|
205
|
+
|
|
206
|
+
return (
|
|
207
|
+
<>
|
|
208
|
+
{groupChannels.map((channel: MessageChannel) => (
|
|
209
|
+
<GroupCard
|
|
210
|
+
key={channel.id}
|
|
211
|
+
group={{ ...channel, server_id: serverId } as MessageChannel & { server_id: UUID }} // Pass server_id for navigation context
|
|
212
|
+
/>
|
|
213
|
+
))}
|
|
214
|
+
</>
|
|
215
|
+
);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
ServerChannels.displayName = 'ServerChannels';
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { AlertCircle, Home, ArrowLeft } from 'lucide-react';
|
|
2
|
+
import { Link, useLocation } from 'react-router-dom';
|
|
3
|
+
import { Button } from '@/components/ui/button';
|
|
4
|
+
|
|
5
|
+
export default function NotFound() {
|
|
6
|
+
const location = useLocation();
|
|
7
|
+
const path = location.pathname;
|
|
8
|
+
|
|
9
|
+
// Determine if this is likely an API endpoint that doesn't exist
|
|
10
|
+
const isLikelyApiEndpoint =
|
|
11
|
+
path.startsWith('/api/') ||
|
|
12
|
+
path.includes('/agents/') ||
|
|
13
|
+
path.includes('/memory/') ||
|
|
14
|
+
path.includes('/speech/');
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className="flex flex-col items-center justify-center min-h-[70vh] px-4 text-center">
|
|
18
|
+
<div className="flex items-center justify-center mb-8">
|
|
19
|
+
<div className="bg-red-900/20 h-24 w-24 rounded-full flex items-center justify-center">
|
|
20
|
+
<AlertCircle className="h-14 w-14 text-red-500" />
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<h1 className="text-3xl font-bold mb-2">Page Not Found</h1>
|
|
25
|
+
|
|
26
|
+
<div className="max-w-lg">
|
|
27
|
+
{isLikelyApiEndpoint ? (
|
|
28
|
+
<>
|
|
29
|
+
<p className="text-lg mb-6 text-muted-foreground">
|
|
30
|
+
The endpoint <span className="font-mono text-red-400">{path}</span> does not exist.
|
|
31
|
+
</p>
|
|
32
|
+
<div className="bg-red-900/20 border border-red-800/30 rounded-md p-4 mb-8 text-left">
|
|
33
|
+
<h3 className="text-red-400 font-medium mb-2 flex items-center">
|
|
34
|
+
<AlertCircle className="h-4 w-4 mr-2" />
|
|
35
|
+
Endpoint Not Found
|
|
36
|
+
</h3>
|
|
37
|
+
<p className="text-sm text-red-300/80 mb-2">
|
|
38
|
+
The requested API endpoint does not exist on this server. Please check that:
|
|
39
|
+
</p>
|
|
40
|
+
<ul className="text-sm text-red-300/80 list-disc pl-5 space-y-1">
|
|
41
|
+
<li>The URL is spelled correctly</li>
|
|
42
|
+
<li>You're using the correct version of the API</li>
|
|
43
|
+
<li>The endpoint is available in this version of Eliza</li>
|
|
44
|
+
</ul>
|
|
45
|
+
</div>
|
|
46
|
+
</>
|
|
47
|
+
) : (
|
|
48
|
+
<p className="text-lg mb-6 text-muted-foreground">
|
|
49
|
+
Sorry, the page you're looking for doesn't exist or has been moved.
|
|
50
|
+
</p>
|
|
51
|
+
)}
|
|
52
|
+
|
|
53
|
+
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
|
54
|
+
<Button asChild variant="default">
|
|
55
|
+
<Link to="/">
|
|
56
|
+
<Home className="h-4 w-4 mr-2" />
|
|
57
|
+
Go to Home
|
|
58
|
+
</Link>
|
|
59
|
+
</Button>
|
|
60
|
+
|
|
61
|
+
<Button asChild variant="outline">
|
|
62
|
+
<a onClick={() => window.history.back()}>
|
|
63
|
+
<ArrowLeft className="h-4 w-4 mr-2" />
|
|
64
|
+
Go Back
|
|
65
|
+
</a>
|
|
66
|
+
</Button>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// Test setup file for Bun:test
|
|
2
|
+
import { GlobalRegistrator } from '@happy-dom/global-registrator';
|
|
3
|
+
import { afterEach, beforeEach, expect, mock } from 'bun:test';
|
|
4
|
+
import { cleanup } from '@testing-library/react';
|
|
5
|
+
import * as matchers from '@testing-library/jest-dom/matchers';
|
|
6
|
+
|
|
7
|
+
// Import React to access internal state
|
|
8
|
+
import React from 'react';
|
|
9
|
+
|
|
10
|
+
// Set up DOM environment with Happy DOM (recommended by Bun)
|
|
11
|
+
GlobalRegistrator.register();
|
|
12
|
+
|
|
13
|
+
// Extend expect with jest-dom matchers
|
|
14
|
+
expect.extend(matchers);
|
|
15
|
+
|
|
16
|
+
// Create a comprehensive localStorage mock
|
|
17
|
+
const createLocalStorageMock = () => {
|
|
18
|
+
const store: Record<string, string> = {};
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
getItem: (key: string): string | null => {
|
|
22
|
+
return store[key] || null;
|
|
23
|
+
},
|
|
24
|
+
setItem: (key: string, value: string): void => {
|
|
25
|
+
store[key] = value;
|
|
26
|
+
},
|
|
27
|
+
removeItem: (key: string): void => {
|
|
28
|
+
delete store[key];
|
|
29
|
+
},
|
|
30
|
+
clear: (): void => {
|
|
31
|
+
Object.keys(store).forEach((key) => delete store[key]);
|
|
32
|
+
},
|
|
33
|
+
get length(): number {
|
|
34
|
+
return Object.keys(store).length;
|
|
35
|
+
},
|
|
36
|
+
key: (index: number): string | null => {
|
|
37
|
+
const keys = Object.keys(store);
|
|
38
|
+
return keys[index] || null;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Set up fresh localStorage and sessionStorage for each test
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
// Create fresh storage instances for each test
|
|
46
|
+
Object.defineProperty(window, 'localStorage', {
|
|
47
|
+
value: createLocalStorageMock(),
|
|
48
|
+
writable: true,
|
|
49
|
+
configurable: true,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
Object.defineProperty(window, 'sessionStorage', {
|
|
53
|
+
value: createLocalStorageMock(),
|
|
54
|
+
writable: true,
|
|
55
|
+
configurable: true,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Reset window dimensions to default
|
|
59
|
+
Object.defineProperty(window, 'innerWidth', {
|
|
60
|
+
writable: true,
|
|
61
|
+
configurable: true,
|
|
62
|
+
value: 1500,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
Object.defineProperty(window, 'innerHeight', {
|
|
66
|
+
writable: true,
|
|
67
|
+
configurable: true,
|
|
68
|
+
value: 900,
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Clean up after each test
|
|
73
|
+
afterEach(() => {
|
|
74
|
+
cleanup();
|
|
75
|
+
|
|
76
|
+
// Clear storage but don't reset the implementation
|
|
77
|
+
if (window.localStorage) {
|
|
78
|
+
window.localStorage.clear();
|
|
79
|
+
}
|
|
80
|
+
if (window.sessionStorage) {
|
|
81
|
+
window.sessionStorage.clear();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Force React to clear any cached hook state
|
|
85
|
+
// This helps prevent cross-test contamination when running multiple test files
|
|
86
|
+
if ((React as any).__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED) {
|
|
87
|
+
const internals = (React as any).__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
|
|
88
|
+
if (internals.ReactCurrentDispatcher) {
|
|
89
|
+
internals.ReactCurrentDispatcher.current = null;
|
|
90
|
+
}
|
|
91
|
+
if (internals.ReactCurrentBatchConfig) {
|
|
92
|
+
internals.ReactCurrentBatchConfig.transition = null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// React 19 specific setup - add missing APIs for testing-library compatibility
|
|
98
|
+
try {
|
|
99
|
+
const React = require('react');
|
|
100
|
+
|
|
101
|
+
// Add React.createRef polyfill for React 19 compatibility with testing-library
|
|
102
|
+
if (!React.createRef) {
|
|
103
|
+
React.createRef = function createRef() {
|
|
104
|
+
return { current: null };
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
} catch (e) {
|
|
108
|
+
console.warn('Failed to set up React internals:', e instanceof Error ? e.message : String(e));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Mock window.matchMedia
|
|
112
|
+
Object.defineProperty(window, 'matchMedia', {
|
|
113
|
+
writable: true,
|
|
114
|
+
value: (query: string) => ({
|
|
115
|
+
matches: false,
|
|
116
|
+
media: query,
|
|
117
|
+
onchange: null,
|
|
118
|
+
addListener: () => {},
|
|
119
|
+
removeListener: () => {},
|
|
120
|
+
addEventListener: () => {},
|
|
121
|
+
removeEventListener: () => {},
|
|
122
|
+
dispatchEvent: () => {},
|
|
123
|
+
}),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Mock IntersectionObserver
|
|
127
|
+
global.IntersectionObserver = class MockIntersectionObserver {
|
|
128
|
+
root = null;
|
|
129
|
+
rootMargin = '';
|
|
130
|
+
thresholds = [];
|
|
131
|
+
|
|
132
|
+
constructor() {}
|
|
133
|
+
observe() {}
|
|
134
|
+
disconnect() {}
|
|
135
|
+
unobserve() {}
|
|
136
|
+
takeRecords() {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
} as any;
|
|
140
|
+
|
|
141
|
+
// Mock ResizeObserver
|
|
142
|
+
global.ResizeObserver = class MockResizeObserver {
|
|
143
|
+
constructor() {}
|
|
144
|
+
observe() {}
|
|
145
|
+
disconnect() {}
|
|
146
|
+
unobserve() {}
|
|
147
|
+
} as any;
|
|
148
|
+
|
|
149
|
+
// Mock scrollTo for window and elements
|
|
150
|
+
window.scrollTo = mock();
|
|
151
|
+
Element.prototype.scrollTo = mock();
|
|
152
|
+
Element.prototype.scrollIntoView = mock();
|
|
153
|
+
|
|
154
|
+
// Add any other global test setup here
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interface representing an attachment.
|
|
3
|
+
*
|
|
4
|
+
* @interface
|
|
5
|
+
* @property {string} url - The URL of the attachment.
|
|
6
|
+
* @property {string} contentType - The content type of the attachment.
|
|
7
|
+
* @property {string} title - The title of the attachment.
|
|
8
|
+
*/
|
|
9
|
+
export interface IAttachment {
|
|
10
|
+
url: string;
|
|
11
|
+
contentType: string;
|
|
12
|
+
title: string;
|
|
13
|
+
}
|