185 lines
5.5 KiB
TypeScript
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>
|
|
);
|
|
}
|
|
|