Files
microdao-daarion/apps/web/src/components/agent-dashboard/AgentAvatarUpload.tsx

185 lines
5.5 KiB
TypeScript

'use client';
import { useState, useRef } from 'react';
import { Upload, User, X, Loader2 } from 'lucide-react';
interface AgentAvatarUploadProps {
agentId: string;
currentAvatarUrl?: string | null;
displayName?: string;
canEdit?: boolean;
onUpdated?: () => void;
}
export function AgentAvatarUpload({
agentId,
currentAvatarUrl,
displayName = 'Agent',
canEdit = false,
onUpdated,
}: AgentAvatarUploadProps) {
const [uploading, setUploading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [previewUrl, setPreviewUrl] = useState<string | null>(currentAvatarUrl || null);
const inputRef = useRef<HTMLInputElement>(null);
const handleUpload = async (file: File) => {
setUploading(true);
setError(null);
try {
// Upload file
const formData = new FormData();
formData.append('file', file);
formData.append('type', 'agent_avatar');
const uploadRes = await fetch('/api/assets/upload', {
method: 'POST',
body: formData,
});
if (!uploadRes.ok) {
throw new Error('Failed to upload file');
}
const uploadData = await uploadRes.json();
// Convert relative URL to use our API proxy
let imageUrl = uploadData.processed_url || uploadData.original_url;
if (imageUrl && imageUrl.startsWith('/static/')) {
// Proxy through our API route
imageUrl = `/api${imageUrl}`;
}
// Update agent DAIS
const updateRes = await fetch(`/api/agents/${agentId}/dais`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
vis: { avatar_url: imageUrl },
}),
});
if (!updateRes.ok) {
throw new Error('Failed to update avatar');
}
setPreviewUrl(imageUrl);
onUpdated?.();
} catch (e) {
setError(e instanceof Error ? e.message : 'Upload failed');
} finally {
setUploading(false);
}
};
const handleRemove = async () => {
setUploading(true);
setError(null);
try {
const updateRes = await fetch(`/api/agents/${agentId}/dais`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
vis: { avatar_url: null },
}),
});
if (!updateRes.ok) {
throw new Error('Failed to remove avatar');
}
setPreviewUrl(null);
onUpdated?.();
} catch (e) {
setError(e instanceof Error ? e.message : 'Remove failed');
} finally {
setUploading(false);
}
};
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
handleUpload(file);
}
};
return (
<div className="bg-white/5 backdrop-blur-md rounded-2xl border border-white/10 p-6">
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
<User className="w-5 h-5 text-violet-400" />
Avatar
</h3>
{error && (
<div className="mb-4 p-3 bg-red-500/10 border border-red-500/20 rounded-lg text-red-400 text-sm">
{error}
</div>
)}
<div className="flex items-start gap-4">
{/* Avatar Preview */}
<div className="relative">
<div className="w-24 h-24 rounded-xl bg-gradient-to-br from-violet-500/30 to-purple-600/30 border border-white/10 overflow-hidden group">
{previewUrl ? (
<>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={previewUrl}
alt={displayName}
className="w-full h-full object-cover"
/>
{canEdit && (
<button
onClick={handleRemove}
disabled={uploading}
className="absolute top-1 right-1 p-1 bg-red-500/80 rounded-full opacity-0 group-hover:opacity-100 transition-opacity"
>
<X className="w-3 h-3 text-white" />
</button>
)}
</>
) : (
<div className="w-full h-full flex items-center justify-center">
<User className="w-10 h-10 text-violet-400/50" />
</div>
)}
{uploading && (
<div className="absolute inset-0 bg-black/50 flex items-center justify-center">
<Loader2 className="w-6 h-6 text-white animate-spin" />
</div>
)}
</div>
</div>
{/* Upload Controls */}
{canEdit && (
<div className="flex-1">
<p className="text-sm text-white/50 mb-3">
Upload a custom avatar for this agent. Recommended size: 256x256px.
</p>
<input
ref={inputRef}
type="file"
accept="image/*"
onChange={handleFileChange}
className="hidden"
/>
<button
onClick={() => inputRef.current?.click()}
disabled={uploading}
className="flex items-center gap-2 px-4 py-2 text-sm bg-violet-500/20 hover:bg-violet-500/30 border border-violet-500/30 rounded-lg text-violet-300 transition-colors disabled:opacity-50"
>
<Upload className="w-4 h-4" />
{previewUrl ? 'Change Avatar' : 'Upload Avatar'}
</button>
</div>
)}
</div>
</div>
);
}