mirror of https://github.com/kortix-ai/suna.git
profile pic for agents
This commit is contained in:
parent
123117e61d
commit
b8c42f98e3
|
@ -702,6 +702,7 @@ async def get_thread_agent(thread_id: str, user_id: str = Depends(get_current_us
|
||||||
tags=agent_data.get('tags', []),
|
tags=agent_data.get('tags', []),
|
||||||
avatar=agent_config.get('avatar'),
|
avatar=agent_config.get('avatar'),
|
||||||
avatar_color=agent_config.get('avatar_color'),
|
avatar_color=agent_config.get('avatar_color'),
|
||||||
|
profile_image_url=agent_config.get('profile_image_url'),
|
||||||
created_at=agent_data['created_at'],
|
created_at=agent_data['created_at'],
|
||||||
updated_at=agent_data.get('updated_at', agent_data['created_at']),
|
updated_at=agent_data.get('updated_at', agent_data['created_at']),
|
||||||
current_version_id=agent_data.get('current_version_id'),
|
current_version_id=agent_data.get('current_version_id'),
|
||||||
|
@ -1579,6 +1580,7 @@ async def get_agents(
|
||||||
tags=agent.get('tags', []),
|
tags=agent.get('tags', []),
|
||||||
avatar=agent_config.get('avatar'),
|
avatar=agent_config.get('avatar'),
|
||||||
avatar_color=agent_config.get('avatar_color'),
|
avatar_color=agent_config.get('avatar_color'),
|
||||||
|
profile_image_url=agent_config.get('profile_image_url'),
|
||||||
created_at=agent['created_at'],
|
created_at=agent['created_at'],
|
||||||
updated_at=agent['updated_at'],
|
updated_at=agent['updated_at'],
|
||||||
current_version_id=agent.get('current_version_id'),
|
current_version_id=agent.get('current_version_id'),
|
||||||
|
@ -1704,6 +1706,7 @@ async def get_agent(agent_id: str, user_id: str = Depends(get_current_user_id_fr
|
||||||
tags=agent_data.get('tags', []),
|
tags=agent_data.get('tags', []),
|
||||||
avatar=agent_config.get('avatar'),
|
avatar=agent_config.get('avatar'),
|
||||||
avatar_color=agent_config.get('avatar_color'),
|
avatar_color=agent_config.get('avatar_color'),
|
||||||
|
profile_image_url=agent_config.get('profile_image_url'),
|
||||||
created_at=agent_data['created_at'],
|
created_at=agent_data['created_at'],
|
||||||
updated_at=agent_data.get('updated_at', agent_data['created_at']),
|
updated_at=agent_data.get('updated_at', agent_data['created_at']),
|
||||||
current_version_id=agent_data.get('current_version_id'),
|
current_version_id=agent_data.get('current_version_id'),
|
||||||
|
@ -2018,6 +2021,7 @@ async def create_agent(
|
||||||
tags=agent.get('tags', []),
|
tags=agent.get('tags', []),
|
||||||
avatar=agent.get('avatar'),
|
avatar=agent.get('avatar'),
|
||||||
avatar_color=agent.get('avatar_color'),
|
avatar_color=agent.get('avatar_color'),
|
||||||
|
profile_image_url=agent.get('profile_image_url'),
|
||||||
created_at=agent['created_at'],
|
created_at=agent['created_at'],
|
||||||
updated_at=agent.get('updated_at', agent['created_at']),
|
updated_at=agent.get('updated_at', agent['created_at']),
|
||||||
current_version_id=agent.get('current_version_id'),
|
current_version_id=agent.get('current_version_id'),
|
||||||
|
@ -2372,6 +2376,7 @@ async def update_agent(
|
||||||
tags=agent.get('tags', []),
|
tags=agent.get('tags', []),
|
||||||
avatar=agent_config.get('avatar'),
|
avatar=agent_config.get('avatar'),
|
||||||
avatar_color=agent_config.get('avatar_color'),
|
avatar_color=agent_config.get('avatar_color'),
|
||||||
|
profile_image_url=agent_config.get('profile_image_url'),
|
||||||
created_at=agent['created_at'],
|
created_at=agent['created_at'],
|
||||||
updated_at=agent.get('updated_at', agent['created_at']),
|
updated_at=agent.get('updated_at', agent['created_at']),
|
||||||
current_version_id=agent.get('current_version_id'),
|
current_version_id=agent.get('current_version_id'),
|
||||||
|
|
|
@ -73,7 +73,7 @@ export default function AgentConfigurationPage() {
|
||||||
|
|
||||||
const [originalData, setOriginalData] = useState<FormData>(formData);
|
const [originalData, setOriginalData] = useState<FormData>(formData);
|
||||||
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
|
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
|
||||||
const initialTab = tabParam === 'configuration' ? 'configuration' : 'agent-builder';
|
const initialTab = tabParam === 'agent-builder' ? 'agent-builder' : 'configuration';
|
||||||
const [activeTab, setActiveTab] = useState(initialTab);
|
const [activeTab, setActiveTab] = useState(initialTab);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -531,12 +531,12 @@ export default function AgentConfigurationPage() {
|
||||||
>
|
>
|
||||||
{isSaving ? (
|
{isSaving ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="h-3 w-3 animate-spin mr-2" />
|
<Loader2 className="h-3 w-3 animate-spin" />
|
||||||
Saving...
|
Saving...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Save className="h-3 w-3 mr-2" />
|
<Save className="h-3 w-3" />
|
||||||
Save
|
Save
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -162,13 +162,13 @@ export default function AgentsPage() {
|
||||||
marketplace_published_at: template.marketplace_published_at,
|
marketplace_published_at: template.marketplace_published_at,
|
||||||
avatar: template.avatar,
|
avatar: template.avatar,
|
||||||
avatar_color: template.avatar_color,
|
avatar_color: template.avatar_color,
|
||||||
|
profile_image_url: template.profile_image_url,
|
||||||
template_id: template.template_id,
|
template_id: template.template_id,
|
||||||
is_kortix_team: template.is_kortix_team,
|
is_kortix_team: template.is_kortix_team,
|
||||||
mcp_requirements: template.mcp_requirements,
|
mcp_requirements: template.mcp_requirements,
|
||||||
metadata: template.metadata,
|
metadata: template.metadata,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Apply search filtering to each item
|
|
||||||
const matchesSearch = !marketplaceSearchQuery.trim() || (() => {
|
const matchesSearch = !marketplaceSearchQuery.trim() || (() => {
|
||||||
const searchLower = marketplaceSearchQuery.toLowerCase();
|
const searchLower = marketplaceSearchQuery.toLowerCase();
|
||||||
return item.name.toLowerCase().includes(searchLower) ||
|
return item.name.toLowerCase().includes(searchLower) ||
|
||||||
|
|
|
@ -689,6 +689,7 @@ export default function ThreadPage({
|
||||||
agentName={agent && agent.name}
|
agentName={agent && agent.name}
|
||||||
agentAvatar={agent && agent.avatar}
|
agentAvatar={agent && agent.avatar}
|
||||||
agentMetadata={agent?.metadata}
|
agentMetadata={agent?.metadata}
|
||||||
|
agentData={agent}
|
||||||
scrollContainerRef={scrollContainerRef}
|
scrollContainerRef={scrollContainerRef}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ interface Agent {
|
||||||
is_default: boolean;
|
is_default: boolean;
|
||||||
created_at?: string;
|
created_at?: string;
|
||||||
updated_at?: string;
|
updated_at?: string;
|
||||||
|
profile_image_url?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AgentPreviewProps {
|
interface AgentPreviewProps {
|
||||||
|
@ -63,6 +64,25 @@ export const AgentPreview = ({ agent, agentMetadata }: AgentPreviewProps) => {
|
||||||
|
|
||||||
const { avatar, color } = getAgentStyling();
|
const { avatar, color } = getAgentStyling();
|
||||||
|
|
||||||
|
const agentAvatarComponent = React.useMemo(() => {
|
||||||
|
if (isSunaAgent) {
|
||||||
|
return <KortixLogo size={16} />;
|
||||||
|
}
|
||||||
|
if (agent.profile_image_url) {
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
src={agent.profile_image_url}
|
||||||
|
alt={agent.name}
|
||||||
|
className="h-4 w-4 rounded-sm object-cover"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (avatar) {
|
||||||
|
return <div className="text-base leading-none">{avatar}</div>;
|
||||||
|
}
|
||||||
|
return <KortixLogo size={16} />;
|
||||||
|
}, [agent.profile_image_url, agent.name, avatar, isSunaAgent]);
|
||||||
|
|
||||||
const initiateAgentMutation = useInitiateAgentWithInvalidation();
|
const initiateAgentMutation = useInitiateAgentWithInvalidation();
|
||||||
const addUserMessageMutation = useAddUserMessageMutation();
|
const addUserMessageMutation = useAddUserMessageMutation();
|
||||||
const startAgentMutation = useStartAgentMutation();
|
const startAgentMutation = useStartAgentMutation();
|
||||||
|
@ -328,13 +348,20 @@ export const AgentPreview = ({ agent, agentMetadata }: AgentPreviewProps) => {
|
||||||
streamHookStatus={streamHookStatus}
|
streamHookStatus={streamHookStatus}
|
||||||
isPreviewMode={true}
|
isPreviewMode={true}
|
||||||
agentName={agent.name}
|
agentName={agent.name}
|
||||||
agentAvatar={avatar}
|
agentAvatar={agentAvatarComponent}
|
||||||
agentMetadata={agentMetadata}
|
agentMetadata={agentMetadata}
|
||||||
|
agentData={agent}
|
||||||
emptyStateComponent={
|
emptyStateComponent={
|
||||||
<div className="flex flex-col items-center text-center text-muted-foreground/80">
|
<div className="flex flex-col items-center text-center text-muted-foreground/80">
|
||||||
<div className="flex w-20 aspect-square items-center justify-center rounded-2xl bg-muted-foreground/10 p-4 mb-4">
|
<div className="flex w-20 aspect-square items-center justify-center rounded-2xl bg-muted-foreground/10 p-4 mb-4">
|
||||||
{isSunaAgent ? (
|
{isSunaAgent ? (
|
||||||
<KortixLogo size={36} />
|
<KortixLogo size={36} />
|
||||||
|
) : agent.profile_image_url ? (
|
||||||
|
<img
|
||||||
|
src={agent.profile_image_url}
|
||||||
|
alt={agent.name}
|
||||||
|
className="w-12 h-12 rounded-xl object-cover"
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-4xl">{avatar}</div>
|
<div className="text-4xl">{avatar}</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -107,21 +107,21 @@ const AgentModal: React.FC<AgentModalProps> = ({
|
||||||
<DialogContent className="max-w-md p-0 overflow-hidden border-none">
|
<DialogContent className="max-w-md p-0 overflow-hidden border-none">
|
||||||
<DialogTitle className="sr-only">Agent actions</DialogTitle>
|
<DialogTitle className="sr-only">Agent actions</DialogTitle>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className={`h-32 flex items-center justify-center relative bg-gradient-to-br from-opacity-90 to-opacity-100`} style={{ backgroundColor: isSunaAgent ? '' : color }}>
|
<div className={`p-4 h-24 flex items-start justify-start relative`}>
|
||||||
{isSunaAgent ? (
|
{isSunaAgent ? (
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<KortixLogo size={48} />
|
<KortixLogo size={24} />
|
||||||
</div>
|
</div>
|
||||||
) : agent.profile_image_url ? (
|
) : agent.profile_image_url ? (
|
||||||
<img src={agent.profile_image_url} alt={agent.name} className="h-24 w-24 rounded-xl object-cover shadow" />
|
<img src={agent.profile_image_url} alt={agent.name} className="h-16 w-16 rounded-xl object-cover" />
|
||||||
) : (
|
) : (
|
||||||
<div className="text-6xl drop-shadow-sm">
|
<div className="text-6xl">
|
||||||
{avatar}
|
{avatar}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-4 space-y-4">
|
<div className="p-4 space-y-2">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<h2 className="text-xl font-semibold text-foreground">
|
<h2 className="text-xl font-semibold text-foreground">
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Sparkles, Settings, MoreHorizontal, Download, Image as ImageIcon } from 'lucide-react';
|
import { Sparkles, Settings, MoreHorizontal, Download, Image as ImageIcon } from 'lucide-react';
|
||||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import { EditableText } from '@/components/ui/editable';
|
import { EditableText } from '@/components/ui/editable';
|
||||||
// import { StylePicker } from '../style-picker';
|
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { KortixLogo } from '@/components/sidebar/kortix-logo';
|
import { KortixLogo } from '@/components/sidebar/kortix-logo';
|
||||||
|
@ -15,7 +14,7 @@ import {
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
|
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
|
||||||
import { createClient } from '@/lib/supabase/client';
|
import { ProfilePictureDialog } from './profile-picture-dialog';
|
||||||
|
|
||||||
interface AgentHeaderProps {
|
interface AgentHeaderProps {
|
||||||
agentId: string;
|
agentId: string;
|
||||||
|
@ -37,6 +36,7 @@ interface AgentHeaderProps {
|
||||||
isExporting?: boolean;
|
isExporting?: boolean;
|
||||||
agentMetadata?: {
|
agentMetadata?: {
|
||||||
is_suna_default?: boolean;
|
is_suna_default?: boolean;
|
||||||
|
centrally_managed?: boolean;
|
||||||
restrictions?: {
|
restrictions?: {
|
||||||
name_editable?: boolean;
|
name_editable?: boolean;
|
||||||
};
|
};
|
||||||
|
@ -56,6 +56,7 @@ export function AgentHeader({
|
||||||
isExporting = false,
|
isExporting = false,
|
||||||
agentMetadata,
|
agentMetadata,
|
||||||
}: AgentHeaderProps) {
|
}: AgentHeaderProps) {
|
||||||
|
const [isProfileDialogOpen, setIsProfileDialogOpen] = useState(false);
|
||||||
const isSunaAgent = agentMetadata?.is_suna_default || false;
|
const isSunaAgent = agentMetadata?.is_suna_default || false;
|
||||||
const restrictions = agentMetadata?.restrictions || {};
|
const restrictions = agentMetadata?.restrictions || {};
|
||||||
const isNameEditable = !isViewingOldVersion && (restrictions.name_editable !== false);
|
const isNameEditable = !isViewingOldVersion && (restrictions.name_editable !== false);
|
||||||
|
@ -70,42 +71,8 @@ export function AgentHeader({
|
||||||
onFieldChange('name', value);
|
onFieldChange('name', value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleImageUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleImageUpdate = (url: string | null) => {
|
||||||
try {
|
onFieldChange('profile_image_url', url);
|
||||||
const file = e.target.files?.[0];
|
|
||||||
if (!file) return;
|
|
||||||
|
|
||||||
const supabase = createClient();
|
|
||||||
const { data: { session } } = await supabase.auth.getSession();
|
|
||||||
|
|
||||||
if (!session) {
|
|
||||||
toast.error('You must be logged in to upload images');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const form = new FormData();
|
|
||||||
form.append('file', file);
|
|
||||||
|
|
||||||
const res = await fetch(`${process.env.NEXT_PUBLIC_BACKEND_URL}/agents/profile-image/upload`, {
|
|
||||||
method: 'POST',
|
|
||||||
body: form,
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${session.access_token}`,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) throw new Error('Upload failed');
|
|
||||||
const data = await res.json();
|
|
||||||
if (data?.url) {
|
|
||||||
onFieldChange('profile_image_url', data.url);
|
|
||||||
toast.success('Profile image updated');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
toast.error('Failed to upload image');
|
|
||||||
} finally {
|
|
||||||
e.target.value = '';
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -118,18 +85,21 @@ export function AgentHeader({
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<label className="cursor-pointer">
|
<button
|
||||||
<input type="file" accept="image/*" className="hidden" onChange={handleImageUpload} />
|
className="cursor-pointer transition-opacity hover:opacity-80"
|
||||||
<Avatar className="h-9 w-9 rounded-lg ring-1 ring-black/5 hover:ring-black/10">
|
onClick={() => setIsProfileDialogOpen(true)}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<Avatar className="h-9 w-9 rounded-lg ring-1 ring-black/5 hover:ring-black/10 transition-colors">
|
||||||
{displayData.profile_image_url ? (
|
{displayData.profile_image_url ? (
|
||||||
<AvatarImage src={displayData.profile_image_url} alt={displayData.name} />
|
<AvatarImage src={displayData.profile_image_url} alt={displayData.name} />
|
||||||
) : (
|
) : (
|
||||||
<AvatarFallback className="rounded-lg text-xs">
|
<AvatarFallback className="rounded-lg text-xs hover:bg-muted">
|
||||||
<ImageIcon className="h-4 w-4" />
|
<ImageIcon className="h-4 w-4" />
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
)}
|
)}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</label>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -148,52 +118,46 @@ export function AgentHeader({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{onExport && (
|
<Tabs value={activeTab} onValueChange={onTabChange}>
|
||||||
<TooltipProvider>
|
<TabsList className="grid w-full grid-cols-2">
|
||||||
<Tooltip>
|
<TabsTrigger value="agent-builder" className="flex items-center gap-1.5">
|
||||||
<TooltipTrigger asChild>
|
<Sparkles className="h-3.5 w-3.5" />
|
||||||
<Button
|
<span className="hidden sm:inline">Prompt to Build</span>
|
||||||
variant="outline"
|
</TabsTrigger>
|
||||||
size="sm"
|
<TabsTrigger value="configuration" className="flex items-center gap-1.5">
|
||||||
onClick={onExport}
|
<Settings className="h-3.5 w-3.5" />
|
||||||
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
|
<span className="hidden sm:inline">Manual Config</span>
|
||||||
disabled={isExporting}
|
</TabsTrigger>
|
||||||
>
|
</TabsList>
|
||||||
<Download className="h-4 w-4" />
|
</Tabs>
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
Export agent
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isSunaAgent && (
|
<DropdownMenu>
|
||||||
<Tabs value={activeTab} onValueChange={onTabChange}>
|
<DropdownMenuTrigger asChild>
|
||||||
<TabsList className="grid grid-cols-2 bg-muted/50 h-9">
|
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
||||||
<TabsTrigger
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
value="agent-builder"
|
</Button>
|
||||||
disabled={isViewingOldVersion}
|
</DropdownMenuTrigger>
|
||||||
className={cn(
|
<DropdownMenuContent align="end">
|
||||||
"flex items-center gap-2 text-sm data-[state=active]:bg-background data-[state=active]:shadow-sm",
|
{onExport && (
|
||||||
isViewingOldVersion && "opacity-50 cursor-not-allowed"
|
<DropdownMenuItem
|
||||||
)}
|
onClick={onExport}
|
||||||
|
disabled={isExporting}
|
||||||
|
className="flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<Sparkles className="h-3 w-3" />
|
<Download className="h-4 w-4" />
|
||||||
Prompt to Build
|
{isExporting ? 'Exporting...' : 'Export Agent'}
|
||||||
</TabsTrigger>
|
</DropdownMenuItem>
|
||||||
<TabsTrigger
|
)}
|
||||||
value="configuration"
|
</DropdownMenuContent>
|
||||||
className="flex items-center gap-2 text-sm data-[state=active]:bg-background data-[state=active]:shadow-sm"
|
</DropdownMenu>
|
||||||
>
|
|
||||||
<Settings className="h-3 w-3" />
|
|
||||||
Manual Config
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
</Tabs>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
<ProfilePictureDialog
|
||||||
|
isOpen={isProfileDialogOpen}
|
||||||
|
onClose={() => setIsProfileDialogOpen(false)}
|
||||||
|
currentImageUrl={displayData.profile_image_url}
|
||||||
|
agentName={displayData.name}
|
||||||
|
onImageUpdate={handleImageUpdate}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
export { AgentHeader } from './agent-header';
|
export { AgentHeader } from './agent-header';
|
||||||
export { VersionAlert } from './version-alert';
|
export { VersionAlert } from './version-alert';
|
||||||
export { AgentBuilderTab } from './agent-builder-tab';
|
export { AgentBuilderTab } from './agent-builder-tab';
|
||||||
export { ConfigurationTab } from './configuration-tab';
|
export { ConfigurationTab } from './configuration-tab';
|
||||||
|
export { ProfilePictureDialog } from './profile-picture-dialog';
|
|
@ -0,0 +1,297 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Upload, Link2, X, Image as ImageIcon } from 'lucide-react';
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
|
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { createClient } from '@/lib/supabase/client';
|
||||||
|
|
||||||
|
interface ProfilePictureDialogProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
currentImageUrl?: string;
|
||||||
|
agentName: string;
|
||||||
|
onImageUpdate: (url: string | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProfilePictureDialog({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
currentImageUrl,
|
||||||
|
agentName,
|
||||||
|
onImageUpdate,
|
||||||
|
}: ProfilePictureDialogProps) {
|
||||||
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
|
const [isUrlSubmitting, setIsUrlSubmitting] = useState(false);
|
||||||
|
const [customUrl, setCustomUrl] = useState('');
|
||||||
|
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
||||||
|
const [dragActive, setDragActive] = useState(false);
|
||||||
|
|
||||||
|
const handleFileUpload = async (file: File) => {
|
||||||
|
if (!file.type.startsWith('image/')) {
|
||||||
|
toast.error('Please select an image file');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.size > 5 * 1024 * 1024) { // 5MB limit
|
||||||
|
toast.error('File size must be less than 5MB');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsUploading(true);
|
||||||
|
try {
|
||||||
|
const supabase = createClient();
|
||||||
|
const { data: { session } } = await supabase.auth.getSession();
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
toast.error('You must be logged in to upload images');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const form = new FormData();
|
||||||
|
form.append('file', file);
|
||||||
|
|
||||||
|
const res = await fetch(`${process.env.NEXT_PUBLIC_BACKEND_URL}/agents/profile-image/upload`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: form,
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${session.access_token}`,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const error = await res.json().catch(() => ({ message: 'Upload failed' }));
|
||||||
|
throw new Error(error.message || 'Upload failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
if (data?.url) {
|
||||||
|
onImageUpdate(data.url);
|
||||||
|
toast.success('Profile image uploaded successfully!');
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Upload error:', err);
|
||||||
|
toast.error(err instanceof Error ? err.message : 'Failed to upload image');
|
||||||
|
} finally {
|
||||||
|
setIsUploading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
handleFileUpload(file);
|
||||||
|
}
|
||||||
|
// Reset input
|
||||||
|
e.target.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrop = (e: React.DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setDragActive(false);
|
||||||
|
|
||||||
|
const file = e.dataTransfer.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
handleFileUpload(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragOver = (e: React.DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setDragActive(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragLeave = (e: React.DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setDragActive(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUrlSubmit = async () => {
|
||||||
|
if (!customUrl.trim()) {
|
||||||
|
toast.error('Please enter a valid URL');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic URL validation
|
||||||
|
try {
|
||||||
|
new URL(customUrl);
|
||||||
|
} catch {
|
||||||
|
toast.error('Please enter a valid URL');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsUrlSubmitting(true);
|
||||||
|
try {
|
||||||
|
onImageUpdate(customUrl);
|
||||||
|
toast.success('Profile image URL updated successfully!');
|
||||||
|
onClose();
|
||||||
|
} catch (err) {
|
||||||
|
toast.error('Failed to update profile image URL');
|
||||||
|
} finally {
|
||||||
|
setIsUrlSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUrlPreview = () => {
|
||||||
|
if (customUrl) {
|
||||||
|
try {
|
||||||
|
new URL(customUrl);
|
||||||
|
setPreviewUrl(customUrl);
|
||||||
|
} catch {
|
||||||
|
toast.error('Please enter a valid URL');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveImage = () => {
|
||||||
|
onImageUpdate(null);
|
||||||
|
toast.success('Profile image removed');
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setCustomUrl('');
|
||||||
|
setPreviewUrl(null);
|
||||||
|
setDragActive(false);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||||
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Update Profile Picture</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4 mt-4">
|
||||||
|
<div className="flex flex-col items-center space-y-3">
|
||||||
|
<Avatar className="h-20 w-20 rounded-xl ring-1 ring-border">
|
||||||
|
{currentImageUrl ? (
|
||||||
|
<AvatarImage src={currentImageUrl} alt={agentName} />
|
||||||
|
) : (
|
||||||
|
<AvatarFallback className="rounded-xl">
|
||||||
|
<ImageIcon className="h-8 w-8 text-muted-foreground" />
|
||||||
|
</AvatarFallback>
|
||||||
|
)}
|
||||||
|
</Avatar>
|
||||||
|
<p className="text-sm text-muted-foreground text-center">
|
||||||
|
Current profile picture for <span className="font-medium">{agentName}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tabs defaultValue="upload" className="w-full">
|
||||||
|
<TabsList className="grid w-full grid-cols-2">
|
||||||
|
<TabsTrigger value="upload" className="flex items-center gap-2">
|
||||||
|
<Upload className="h-4 w-4" />
|
||||||
|
Upload File
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="url" className="flex items-center gap-2">
|
||||||
|
<Link2 className="h-4 w-4" />
|
||||||
|
Custom URL
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="upload" className="space-y-4 mt-4">
|
||||||
|
<div
|
||||||
|
className={`border-2 border-dashed rounded-lg p-8 text-center transition-colors ${
|
||||||
|
dragActive
|
||||||
|
? 'border-primary bg-primary/5'
|
||||||
|
: 'border-muted-foreground/25 hover:border-muted-foreground/50'
|
||||||
|
}`}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
>
|
||||||
|
<Upload className="mx-auto h-12 w-12 text-muted-foreground mb-4" />
|
||||||
|
<p className="text-sm font-medium mb-2">
|
||||||
|
Drop an image here, or click to select
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground mb-4">
|
||||||
|
PNG, JPG, GIF up to 5MB
|
||||||
|
</p>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={handleFileInputChange}
|
||||||
|
className="hidden"
|
||||||
|
id="file-upload"
|
||||||
|
disabled={isUploading}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
asChild
|
||||||
|
variant="outline"
|
||||||
|
disabled={isUploading}
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
|
<label htmlFor="file-upload">
|
||||||
|
{isUploading ? 'Uploading...' : 'Select File'}
|
||||||
|
</label>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="url" className="space-y-4 mt-4">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Label htmlFor="image-url">Image URL</Label>
|
||||||
|
<Input
|
||||||
|
id="image-url"
|
||||||
|
type="url"
|
||||||
|
placeholder="https://example.com/image.png"
|
||||||
|
value={customUrl}
|
||||||
|
onChange={(e) => setCustomUrl(e.target.value)}
|
||||||
|
onBlur={handleUrlPreview}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{previewUrl && (
|
||||||
|
<div className="flex items-center justify-center p-4 border rounded-lg">
|
||||||
|
<Avatar className="h-16 w-16 rounded-xl">
|
||||||
|
<AvatarImage
|
||||||
|
src={previewUrl}
|
||||||
|
alt="Preview"
|
||||||
|
onError={() => {
|
||||||
|
setPreviewUrl(null);
|
||||||
|
toast.error('Unable to load image from URL');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<AvatarFallback className="rounded-xl">
|
||||||
|
<ImageIcon className="h-6 w-6 text-muted-foreground" />
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={handleUrlSubmit}
|
||||||
|
disabled={!customUrl.trim() || isUrlSubmitting}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{isUrlSubmitting ? 'Updating...' : 'Update Image URL'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
<div className="flex justify-end pt-4 gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleRemoveImage}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
disabled={!currentImageUrl}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
Remove Image
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" onClick={handleClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
|
@ -316,15 +316,23 @@ export const StreamlinedInstallDialog: React.FC<StreamlinedInstallDialogProps> =
|
||||||
<DialogContent className="sm:max-w-lg max-h-[90vh] overflow-y-auto">
|
<DialogContent className="sm:max-w-lg max-h-[90vh] overflow-y-auto">
|
||||||
<DialogHeader className="space-y-4">
|
<DialogHeader className="space-y-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div
|
{item.profile_image_url ? (
|
||||||
className="h-12 w-12 flex-shrink-0 rounded-lg flex items-center justify-center"
|
<img
|
||||||
style={{
|
src={item.profile_image_url}
|
||||||
backgroundColor: color,
|
alt={item.name}
|
||||||
boxShadow: `0 16px 48px -8px ${color}70, 0 8px 24px -4px ${color}50`
|
className="h-12 w-12 flex-shrink-0 rounded-lg object-cover shadow-lg"
|
||||||
}}
|
/>
|
||||||
>
|
) : (
|
||||||
<span className="text-lg">{avatar}</span>
|
<div
|
||||||
</div>
|
className="h-12 w-12 flex-shrink-0 rounded-lg flex items-center justify-center"
|
||||||
|
style={{
|
||||||
|
backgroundColor: color,
|
||||||
|
boxShadow: `0 16px 48px -8px ${color}70, 0 8px 24px -4px ${color}50`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="text-lg">{avatar}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<DialogTitle className="text-left flex items-center gap-2">
|
<DialogTitle className="text-left flex items-center gap-2">
|
||||||
Install {item.name}
|
Install {item.name}
|
||||||
|
|
|
@ -10,6 +10,7 @@ export interface MarketplaceTemplate {
|
||||||
marketplace_published_at?: string;
|
marketplace_published_at?: string;
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
avatar_color?: string;
|
avatar_color?: string;
|
||||||
|
profile_image_url?: string;
|
||||||
template_id: string;
|
template_id: string;
|
||||||
is_kortix_team?: boolean;
|
is_kortix_team?: boolean;
|
||||||
model?: string;
|
model?: string;
|
||||||
|
|
|
@ -128,17 +128,25 @@ export const MarketplaceAgentPreviewDialog: React.FC<MarketplaceAgentPreviewDial
|
||||||
<DialogContent className="max-w-2xl max-h-[90vh] p-0 overflow-hidden">
|
<DialogContent className="max-w-2xl max-h-[90vh] p-0 overflow-hidden">
|
||||||
<DialogHeader className='p-6'>
|
<DialogHeader className='p-6'>
|
||||||
<DialogTitle className='sr-only'>Agent Preview</DialogTitle>
|
<DialogTitle className='sr-only'>Agent Preview</DialogTitle>
|
||||||
<div className='relative h-20 w-20 aspect-square bg-muted rounded-2xl flex items-center justify-center' style={{ backgroundColor: avatar_color }}>
|
{agent.profile_image_url ? (
|
||||||
<div className="text-4xl drop-shadow-lg">
|
<img
|
||||||
{avatar || '🤖'}
|
src={agent.profile_image_url}
|
||||||
</div>
|
alt={agent.name}
|
||||||
<div
|
className='h-20 w-20 aspect-square rounded-2xl object-cover shadow-lg'
|
||||||
className="absolute inset-0 rounded-2xl pointer-events-none opacity-0 dark:opacity-100 transition-opacity"
|
|
||||||
style={{
|
|
||||||
boxShadow: `0 16px 48px -8px ${avatar_color}70, 0 8px 24px -4px ${avatar_color}50`
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
) : (
|
||||||
|
<div className='relative h-20 w-20 aspect-square bg-muted rounded-2xl flex items-center justify-center' style={{ backgroundColor: avatar_color }}>
|
||||||
|
<div className="text-4xl drop-shadow-lg">
|
||||||
|
{avatar || '🤖'}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 rounded-2xl pointer-events-none opacity-0 dark:opacity-100 transition-opacity"
|
||||||
|
style={{
|
||||||
|
boxShadow: `0 16px 48px -8px ${avatar_color}70, 0 8px 24px -4px ${avatar_color}50`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="-mt-4 flex flex-col max-h-[calc(90vh-8rem)] overflow-hidden">
|
<div className="-mt-4 flex flex-col max-h-[calc(90vh-8rem)] overflow-hidden">
|
||||||
<div className="p-6 py-0 pb-4">
|
<div className="p-6 py-0 pb-4">
|
||||||
|
|
|
@ -16,7 +16,6 @@ import {
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { isLocalMode, isYearlyCommitmentDowngrade, isPlanChangeAllowed, getPlanInfo } from '@/lib/config';
|
import { isLocalMode, isYearlyCommitmentDowngrade, isPlanChangeAllowed, getPlanInfo } from '@/lib/config';
|
||||||
import { useSubscription, useSubscriptionCommitment } from '@/hooks/react-query';
|
import { useSubscription, useSubscriptionCommitment } from '@/hooks/react-query';
|
||||||
import { useAuth } from '@/components/AuthProvider';
|
|
||||||
import posthog from 'posthog-js';
|
import posthog from 'posthog-js';
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
|
@ -547,19 +546,10 @@ export function PricingSection({
|
||||||
noPadding = false,
|
noPadding = false,
|
||||||
}: PricingSectionProps) {
|
}: PricingSectionProps) {
|
||||||
|
|
||||||
const { user, isLoading: authLoading } = useAuth();
|
const { data: subscriptionData, isLoading: isFetchingPlan, error: subscriptionQueryError, refetch: refetchSubscription } = useSubscription();
|
||||||
const {
|
|
||||||
data: subscriptionData,
|
|
||||||
isLoading: isFetchingPlan,
|
|
||||||
error: subscriptionQueryError,
|
|
||||||
refetch: refetchSubscription
|
|
||||||
} = useSubscription({
|
|
||||||
enabled: !!user && !authLoading,
|
|
||||||
});
|
|
||||||
|
|
||||||
const subCommitmentQuery = useSubscriptionCommitment(subscriptionData?.subscription_id);
|
const subCommitmentQuery = useSubscriptionCommitment(subscriptionData?.subscription_id);
|
||||||
|
|
||||||
const isAuthenticated = !!user && !!subscriptionData && subscriptionQueryError === null;
|
const isAuthenticated = !!subscriptionData && subscriptionQueryError === null;
|
||||||
const currentSubscription = subscriptionData || null;
|
const currentSubscription = subscriptionData || null;
|
||||||
|
|
||||||
const getDefaultBillingPeriod = useCallback((): 'monthly' | 'yearly' | 'yearly_commitment' => {
|
const getDefaultBillingPeriod = useCallback((): 'monthly' | 'yearly' | 'yearly_commitment' => {
|
||||||
|
|
|
@ -29,6 +29,7 @@ import { Skeleton } from '@/components/ui/skeleton';
|
||||||
import { NewAgentDialog } from '@/components/agents/new-agent-dialog';
|
import { NewAgentDialog } from '@/components/agents/new-agent-dialog';
|
||||||
import { useAgentWorkflows } from '@/hooks/react-query/agents/use-agent-workflows';
|
import { useAgentWorkflows } from '@/hooks/react-query/agents/use-agent-workflows';
|
||||||
import { PlaybookExecuteDialog } from '@/components/playbooks/playbook-execute-dialog';
|
import { PlaybookExecuteDialog } from '@/components/playbooks/playbook-execute-dialog';
|
||||||
|
import { AgentAvatar } from '@/components/thread/content/agent-avatar';
|
||||||
|
|
||||||
type UnifiedConfigMenuProps = {
|
type UnifiedConfigMenuProps = {
|
||||||
isLoggedIn?: boolean;
|
isLoggedIn?: boolean;
|
||||||
|
@ -209,18 +210,7 @@ const LoggedInMenu: React.FC<UnifiedConfigMenuProps> = ({
|
||||||
// combinedModels defined earlier
|
// combinedModels defined earlier
|
||||||
|
|
||||||
const renderAgentIcon = (agent: any) => {
|
const renderAgentIcon = (agent: any) => {
|
||||||
const isSuna = agent?.metadata?.is_suna_default;
|
return <AgentAvatar agentId={agent?.agent_id} size={16} className="h-4 w-4" fallbackName={agent?.name} />;
|
||||||
if (isSuna) return <KortixLogo size={16} />;
|
|
||||||
if (agent?.avatar) return (
|
|
||||||
// avatar can be URL or emoji string – handle both without extra chrome
|
|
||||||
typeof agent.avatar === 'string' && agent.avatar.startsWith('http') ? (
|
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
|
||||||
<img src={agent.avatar} alt={agent.name} className="h-4 w-4 rounded-sm object-cover" />
|
|
||||||
) : (
|
|
||||||
<span className="text-base leading-none">{agent.avatar}</span>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
return <KortixLogo size={16} />;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const displayAgent = useMemo(() => {
|
const displayAgent = useMemo(() => {
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {
|
||||||
} from '@/components/thread/utils';
|
} from '@/components/thread/utils';
|
||||||
import { KortixLogo } from '@/components/sidebar/kortix-logo';
|
import { KortixLogo } from '@/components/sidebar/kortix-logo';
|
||||||
import { AgentLoader } from './loader';
|
import { AgentLoader } from './loader';
|
||||||
|
import { AgentAvatar, AgentName } from './agent-avatar';
|
||||||
import { parseXmlToolCalls, isNewXmlFormat } from '@/components/thread/tool-views/xml-parser';
|
import { parseXmlToolCalls, isNewXmlFormat } from '@/components/thread/tool-views/xml-parser';
|
||||||
import { ShowToolStream } from './ShowToolStream';
|
import { ShowToolStream } from './ShowToolStream';
|
||||||
import { ComposioUrlDetector } from './composio-url-detector';
|
import { ComposioUrlDetector } from './composio-url-detector';
|
||||||
|
@ -282,6 +283,7 @@ export interface ThreadContentProps {
|
||||||
threadMetadata?: any; // Add thread metadata prop
|
threadMetadata?: any; // Add thread metadata prop
|
||||||
scrollContainerRef?: React.RefObject<HTMLDivElement>; // Add scroll container ref prop
|
scrollContainerRef?: React.RefObject<HTMLDivElement>; // Add scroll container ref prop
|
||||||
agentMetadata?: any; // Add agent metadata prop
|
agentMetadata?: any; // Add agent metadata prop
|
||||||
|
agentData?: any; // Add full agent data prop
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ThreadContent: React.FC<ThreadContentProps> = ({
|
export const ThreadContent: React.FC<ThreadContentProps> = ({
|
||||||
|
@ -307,6 +309,7 @@ export const ThreadContent: React.FC<ThreadContentProps> = ({
|
||||||
threadMetadata,
|
threadMetadata,
|
||||||
scrollContainerRef,
|
scrollContainerRef,
|
||||||
agentMetadata,
|
agentMetadata,
|
||||||
|
agentData,
|
||||||
}) => {
|
}) => {
|
||||||
const messagesContainerRef = useRef<HTMLDivElement>(null);
|
const messagesContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const latestMessageRef = useRef<HTMLDivElement>(null);
|
const latestMessageRef = useRef<HTMLDivElement>(null);
|
||||||
|
@ -357,6 +360,25 @@ export const ThreadContent: React.FC<ThreadContentProps> = ({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (agentData && !isSunaDefaultAgent) {
|
||||||
|
const profileUrl = agentData.profile_image_url;
|
||||||
|
const avatar = profileUrl ? (
|
||||||
|
<img src={profileUrl} alt={agentData.name || agentName} className="h-5 w-5 rounded object-cover" />
|
||||||
|
) : agentData.avatar ? (
|
||||||
|
<div className="h-5 w-5 flex items-center justify-center rounded text-xs">
|
||||||
|
<span className="text-lg">{agentData.avatar}</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="h-5 w-5 flex items-center justify-center rounded text-xs">
|
||||||
|
<KortixLogo size={16} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
name: agentData.name || agentName,
|
||||||
|
avatar
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (recentAssistantWithAgent?.agents?.name) {
|
if (recentAssistantWithAgent?.agents?.name) {
|
||||||
const isSunaAgent = recentAssistantWithAgent.agents.name === 'Suna' || isSunaDefaultAgent;
|
const isSunaAgent = recentAssistantWithAgent.agents.name === 'Suna' || isSunaDefaultAgent;
|
||||||
// Prefer profile image if available on the agent payload
|
// Prefer profile image if available on the agent payload
|
||||||
|
@ -402,7 +424,7 @@ export const ThreadContent: React.FC<ThreadContentProps> = ({
|
||||||
name: agentName || 'Suna',
|
name: agentName || 'Suna',
|
||||||
avatar: agentAvatar
|
avatar: agentAvatar
|
||||||
};
|
};
|
||||||
}, [threadMetadata, displayMessages, agentName, agentAvatar, agentMetadata]);
|
}, [threadMetadata, displayMessages, agentName, agentAvatar, agentMetadata, agentData]);
|
||||||
|
|
||||||
// Simplified scroll handler - flex-column-reverse handles positioning
|
// Simplified scroll handler - flex-column-reverse handles positioning
|
||||||
const handleScroll = useCallback(() => {
|
const handleScroll = useCallback(() => {
|
||||||
|
@ -705,15 +727,27 @@ export const ThreadContent: React.FC<ThreadContentProps> = ({
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (group.type === 'assistant_group') {
|
} else if (group.type === 'assistant_group') {
|
||||||
|
// Get agent_id from the first assistant message in this group
|
||||||
|
const firstAssistantMsg = group.messages.find(m => m.type === 'assistant');
|
||||||
|
const groupAgentId = firstAssistantMsg?.agent_id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={group.key} ref={groupIndex === groupedMessages.length - 1 ? latestMessageRef : null}>
|
<div key={group.key} ref={groupIndex === groupedMessages.length - 1 ? latestMessageRef : null}>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className="rounded-md flex items-center justify-center relative">
|
<div className="rounded-md flex items-center justify-center relative">
|
||||||
{getAgentInfo().avatar}
|
{groupAgentId ? (
|
||||||
|
<AgentAvatar agentId={groupAgentId} size={20} className="h-5 w-5" />
|
||||||
|
) : (
|
||||||
|
getAgentInfo().avatar
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className='ml-2 text-sm text-muted-foreground'>
|
<p className='ml-2 text-sm text-muted-foreground'>
|
||||||
{getAgentInfo().name}
|
{groupAgentId ? (
|
||||||
|
<AgentName agentId={groupAgentId} fallback={getAgentInfo().name} />
|
||||||
|
) : (
|
||||||
|
getAgentInfo().name
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { useAgent } from '@/hooks/react-query/agents/use-agents';
|
||||||
|
import { KortixLogo } from '@/components/sidebar/kortix-logo';
|
||||||
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
|
|
||||||
|
interface AgentAvatarProps {
|
||||||
|
agentId?: string;
|
||||||
|
size?: number;
|
||||||
|
className?: string;
|
||||||
|
fallbackName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AgentAvatar: React.FC<AgentAvatarProps> = ({
|
||||||
|
agentId,
|
||||||
|
size = 16,
|
||||||
|
className = "",
|
||||||
|
fallbackName = "Suna"
|
||||||
|
}) => {
|
||||||
|
const { data: agent, isLoading } = useAgent(agentId || '');
|
||||||
|
|
||||||
|
if (isLoading && agentId) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`bg-muted animate-pulse rounded ${className}`}
|
||||||
|
style={{ width: size, height: size }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!agent && !agentId) {
|
||||||
|
return <KortixLogo size={size} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSuna = agent?.metadata?.is_suna_default;
|
||||||
|
if (isSuna) {
|
||||||
|
return <KortixLogo size={size} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (agent?.profile_image_url) {
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
src={agent.profile_image_url}
|
||||||
|
alt={agent.name || fallbackName}
|
||||||
|
className={`rounded object-cover ${className}`}
|
||||||
|
style={{ width: size, height: size }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (agent?.avatar) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`flex items-center justify-center text-xs ${className}`}
|
||||||
|
style={{ width: size, height: size, fontSize: size * 0.75 }}
|
||||||
|
>
|
||||||
|
{agent.avatar}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <KortixLogo size={size} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface AgentNameProps {
|
||||||
|
agentId?: string;
|
||||||
|
fallback?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AgentName: React.FC<AgentNameProps> = ({
|
||||||
|
agentId,
|
||||||
|
fallback = "Suna"
|
||||||
|
}) => {
|
||||||
|
const { data: agent, isLoading } = useAgent(agentId || '');
|
||||||
|
|
||||||
|
if (isLoading && agentId) {
|
||||||
|
return <span className="text-muted-foreground">Loading...</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <span>{agent?.name || fallback}</span>;
|
||||||
|
};
|
|
@ -24,6 +24,7 @@ export interface UnifiedMessage {
|
||||||
name: string;
|
name: string;
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
avatar_color?: string;
|
avatar_color?: string;
|
||||||
|
profile_image_url?: string;
|
||||||
}; // Agent information from join
|
}; // Agent information from join
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -45,6 +45,7 @@ export interface AgentTemplate {
|
||||||
creator_name?: string;
|
creator_name?: string;
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
avatar_color?: string;
|
avatar_color?: string;
|
||||||
|
profile_image_url?: string;
|
||||||
is_kortix_team?: boolean;
|
is_kortix_team?: boolean;
|
||||||
metadata?: {
|
metadata?: {
|
||||||
source_agent_id?: string;
|
source_agent_id?: string;
|
||||||
|
|
|
@ -6,10 +6,7 @@ export const useAccounts = (options?: SWRConfiguration) => {
|
||||||
const supabaseClient = createClient();
|
const supabaseClient = createClient();
|
||||||
|
|
||||||
return useSWR<GetAccountsResponse>(
|
return useSWR<GetAccountsResponse>(
|
||||||
async () => {
|
!!supabaseClient && ['accounts'],
|
||||||
const { data: { user } } = await supabaseClient.auth.getUser();
|
|
||||||
return user ? ['accounts', user.id] : null;
|
|
||||||
},
|
|
||||||
async () => {
|
async () => {
|
||||||
const { data, error } = await supabaseClient.rpc('get_accounts');
|
const { data, error } = await supabaseClient.rpc('get_accounts');
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue