fix: restore microdao/agent logos and banner asset urls
- Enhanced normalizeAssetUrl to handle all edge cases - Added normalizeAssetUrl to all avatar/logo/banner usages: - agents/page.tsx - agents/[agentId]/page.tsx - citizens/page.tsx - citizens/[slug]/page.tsx - MicrodaoAgentsSection.tsx - MicrodaoBrandingCard.tsx - AgentSummaryCard.tsx - AgentChatWidget.tsx Task: TASK_PHASE_ASSET_BRANDING_HOTFIX_v1
This commit is contained in:
@@ -25,6 +25,7 @@ import { CityChatWidget } from '@/components/city/CityChatWidget';
|
||||
import { AgentChatWidget } from '@/components/chat/AgentChatWidget';
|
||||
import { AgentPresenceBadge } from '@/components/ui/AgentPresenceBadge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { normalizeAssetUrl } from '@/lib/utils/assetUrl';
|
||||
|
||||
// Tab types
|
||||
type TabId = 'dashboard' | 'prompts' | 'microdao' | 'identity' | 'models' | 'chat';
|
||||
@@ -232,8 +233,8 @@ export default function AgentConsolePage() {
|
||||
{/* Agent Avatar & Name */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-cyan-500/20 to-purple-500/20 flex items-center justify-center border border-white/10">
|
||||
{profile?.avatar_url ? (
|
||||
<img src={profile.avatar_url} alt="" className="w-full h-full rounded-xl object-cover" />
|
||||
{normalizeAssetUrl(profile?.avatar_url) ? (
|
||||
<img src={normalizeAssetUrl(profile?.avatar_url)!} alt="" className="w-full h-full rounded-xl object-cover" />
|
||||
) : (
|
||||
<Bot className="w-6 h-6 text-cyan-400" />
|
||||
)}
|
||||
@@ -466,7 +467,7 @@ export default function AgentConsolePage() {
|
||||
{/* Avatar Upload */}
|
||||
<AgentAvatarUpload
|
||||
agentId={dashboard.profile.agent_id}
|
||||
currentAvatarUrl={profile?.avatar_url || dashboard.profile.dais?.vis?.avatar_url}
|
||||
currentAvatarUrl={normalizeAssetUrl(profile?.avatar_url) || normalizeAssetUrl(dashboard.profile.dais?.vis?.avatar_url)}
|
||||
displayName={profile?.display_name || dashboard.profile.display_name}
|
||||
canEdit={true}
|
||||
onUpdated={refresh}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Bot, Users, Building2, Server, ExternalLink, Shield, Plus } from 'lucid
|
||||
import { useAgentList } from '@/hooks/useAgents';
|
||||
import { AgentSummary, getGovLevelBadge } from '@/lib/types/agents';
|
||||
import { AgentPresenceBadge } from '@/components/ui/AgentPresenceBadge';
|
||||
import { normalizeAssetUrl } from '@/lib/utils/assetUrl';
|
||||
|
||||
// Kind emoji mapping
|
||||
const kindEmoji: Record<string, string> = {
|
||||
@@ -45,10 +46,10 @@ function AgentCard({ agent }: { agent: AgentSummary }) {
|
||||
<div className="flex items-start gap-4 mb-4">
|
||||
<div className="relative">
|
||||
<div className="w-14 h-14 rounded-xl bg-gradient-to-br from-violet-500/30 to-purple-600/30 flex items-center justify-center flex-shrink-0 overflow-hidden">
|
||||
{agent.avatar_url ? (
|
||||
{normalizeAssetUrl(agent.avatar_url) ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={agent.avatar_url}
|
||||
src={normalizeAssetUrl(agent.avatar_url)!}
|
||||
alt={agent.display_name}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useCitizenProfile, useCitizenInteraction } from '@/hooks/useCitizens';
|
||||
import { askCitizen } from '@/lib/api/citizens';
|
||||
import { CityChatWidget } from '@/components/city/CityChatWidget';
|
||||
import { ChevronLeft, Building2, MapPin, MessageSquare, HelpCircle, Loader2, Users } from 'lucide-react';
|
||||
import { normalizeAssetUrl } from '@/lib/utils/assetUrl';
|
||||
|
||||
type LooseRecord = Record<string, unknown>;
|
||||
|
||||
@@ -77,9 +78,9 @@ export default function CitizenProfilePage() {
|
||||
<div className="flex flex-col gap-6 md:flex-row md:items-start">
|
||||
{/* Avatar */}
|
||||
<div className="w-24 h-24 flex-shrink-0 rounded-2xl bg-gradient-to-br from-cyan-500/40 to-purple-500/40 flex items-center justify-center text-5xl shadow-xl overflow-hidden">
|
||||
{citizen.avatar_url ? (
|
||||
{normalizeAssetUrl(citizen.avatar_url) ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img src={citizen.avatar_url} alt="" className="w-full h-full object-cover" />
|
||||
<img src={normalizeAssetUrl(citizen.avatar_url)!} alt="" className="w-full h-full object-cover" />
|
||||
) : (
|
||||
getAgentKindIcon(citizen.kind || '')
|
||||
)}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { DISTRICTS } from '@/lib/microdao';
|
||||
import { useCitizensList } from '@/hooks/useCitizens';
|
||||
import type { PublicCitizenSummary } from '@/lib/types/citizens';
|
||||
import { Users, Search, MapPin, Building2 } from 'lucide-react';
|
||||
import { normalizeAssetUrl } from '@/lib/utils/assetUrl';
|
||||
|
||||
const CITIZEN_KINDS = [
|
||||
'orchestrator',
|
||||
@@ -157,10 +158,10 @@ function CitizenCard({ citizen }: { citizen: PublicCitizenSummary }) {
|
||||
{/* Header */}
|
||||
<div className="flex items-start gap-4 mb-4">
|
||||
<div className="w-16 h-16 rounded-xl bg-gradient-to-br from-cyan-500/30 to-purple-500/30 flex items-center justify-center text-3xl flex-shrink-0 overflow-hidden">
|
||||
{citizen.avatar_url ? (
|
||||
{normalizeAssetUrl(citizen.avatar_url) ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={citizen.avatar_url}
|
||||
src={normalizeAssetUrl(citizen.avatar_url)!}
|
||||
alt=""
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { AgentProfile, AgentRuntime, getAgentKindIcon, getAgentStatusColor } fro
|
||||
import { StatusBadge } from '@/components/node-dashboard';
|
||||
import { getGovLevelBadge } from '@/lib/types/agents';
|
||||
import { Shield, Fingerprint, Building2 } from 'lucide-react';
|
||||
import { normalizeAssetUrl } from '@/lib/utils/assetUrl';
|
||||
|
||||
interface AgentSummaryCardProps {
|
||||
profile: AgentProfile;
|
||||
@@ -20,9 +21,9 @@ export function AgentSummaryCard({ profile, runtime }: AgentSummaryCardProps) {
|
||||
<div className="flex items-start gap-4">
|
||||
{/* Avatar */}
|
||||
<div className="relative">
|
||||
{profile.dais.vis?.avatar_url ? (
|
||||
{normalizeAssetUrl(profile.dais.vis?.avatar_url) ? (
|
||||
<img
|
||||
src={profile.dais.vis.avatar_url}
|
||||
src={normalizeAssetUrl(profile.dais.vis?.avatar_url)!}
|
||||
alt={profile.display_name}
|
||||
className="w-20 h-20 rounded-xl object-cover"
|
||||
/>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState, useEffect, useCallback } from 'react';
|
||||
import { MessageCircle, X, Bot, Server, Building2, Loader2, AlertCircle } from 'lucide-react';
|
||||
import { CityChatWidget } from '@/components/city/CityChatWidget';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { normalizeAssetUrl } from '@/lib/utils/assetUrl';
|
||||
|
||||
export type ChatContextType = 'agent' | 'node' | 'microdao';
|
||||
|
||||
@@ -111,7 +112,7 @@ export function AgentChatWidget({ contextType, contextId, className }: AgentChat
|
||||
name: chatInfo.agent_display_name || 'Agent Chat',
|
||||
icon: Bot,
|
||||
status: chatInfo.agent_status || 'offline',
|
||||
avatarUrl: chatInfo.agent_avatar_url
|
||||
avatarUrl: normalizeAssetUrl(chatInfo.agent_avatar_url)
|
||||
};
|
||||
case 'node':
|
||||
return {
|
||||
@@ -125,7 +126,7 @@ export function AgentChatWidget({ contextType, contextId, className }: AgentChat
|
||||
name: chatInfo.microdao_name || 'MicroDAO Chat',
|
||||
icon: Building2,
|
||||
status: chatInfo.orchestrator?.status || 'offline',
|
||||
avatarUrl: chatInfo.orchestrator?.avatar_url
|
||||
avatarUrl: normalizeAssetUrl(chatInfo.orchestrator?.avatar_url)
|
||||
};
|
||||
default:
|
||||
return { name: 'Chat', icon: MessageCircle, status: 'offline' };
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import Link from "next/link";
|
||||
import { Bot, Crown, Users, Shield, Sparkles } from "lucide-react";
|
||||
import { MicrodaoAgent } from "@/hooks/useMicrodao";
|
||||
import { normalizeAssetUrl } from "@/lib/utils/assetUrl";
|
||||
|
||||
interface MicrodaoAgentsSectionProps {
|
||||
agents: MicrodaoAgent[];
|
||||
@@ -140,8 +141,8 @@ function AgentCard({ agent, featured = false, compact = false }: AgentCardProps)
|
||||
<div className="flex items-center gap-3 p-3 bg-slate-900/50 border border-slate-700/30 rounded-lg hover:border-slate-600/50 transition-colors">
|
||||
<div className="relative">
|
||||
<div className="w-8 h-8 rounded-full bg-violet-500/20 flex items-center justify-center">
|
||||
{agent.avatar_url ? (
|
||||
<img src={agent.avatar_url} alt={agent.name} className="w-8 h-8 rounded-full" />
|
||||
{normalizeAssetUrl(agent.avatar_url) ? (
|
||||
<img src={normalizeAssetUrl(agent.avatar_url)!} alt={agent.name} className="w-8 h-8 rounded-full" />
|
||||
) : (
|
||||
<Bot className="w-4 h-4 text-violet-400" />
|
||||
)}
|
||||
@@ -169,8 +170,8 @@ function AgentCard({ agent, featured = false, compact = false }: AgentCardProps)
|
||||
<div className={`rounded-full bg-violet-500/20 flex items-center justify-center ${
|
||||
featured ? "w-14 h-14" : "w-10 h-10"
|
||||
}`}>
|
||||
{agent.avatar_url ? (
|
||||
<img src={agent.avatar_url} alt={agent.name} className={`rounded-full ${featured ? "w-14 h-14" : "w-10 h-10"}`} />
|
||||
{normalizeAssetUrl(agent.avatar_url) ? (
|
||||
<img src={normalizeAssetUrl(agent.avatar_url)!} alt={agent.name} className={`rounded-full ${featured ? "w-14 h-14" : "w-10 h-10"}`} />
|
||||
) : (
|
||||
<Bot className={`text-violet-400 ${featured ? "w-7 h-7" : "w-5 h-5"}`} />
|
||||
)}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState, useRef } from 'react';
|
||||
import { Upload, Image, X, Building2, Loader2 } from 'lucide-react';
|
||||
import { normalizeAssetUrl } from '@/lib/utils/assetUrl';
|
||||
|
||||
interface MicrodaoBrandingCardProps {
|
||||
slug: string;
|
||||
@@ -20,8 +21,8 @@ export function MicrodaoBrandingCard({
|
||||
}: MicrodaoBrandingCardProps) {
|
||||
const [uploading, setUploading] = useState<'logo' | 'banner' | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [previewLogo, setPreviewLogo] = useState<string | null>(logoUrl || null);
|
||||
const [previewBanner, setPreviewBanner] = useState<string | null>(bannerUrl || null);
|
||||
const [previewLogo, setPreviewLogo] = useState<string | null>(normalizeAssetUrl(logoUrl));
|
||||
const [previewBanner, setPreviewBanner] = useState<string | null>(normalizeAssetUrl(bannerUrl));
|
||||
|
||||
const logoInputRef = useRef<HTMLInputElement>(null);
|
||||
const bannerInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
@@ -1,35 +1,59 @@
|
||||
/**
|
||||
* Normalize asset URL for display
|
||||
* Normalize asset URL for display.
|
||||
* This is the SINGLE source of truth for building static asset URLs.
|
||||
*
|
||||
* Handles various URL formats from the backend:
|
||||
* - /api/static/uploads/... - already correct
|
||||
* - /assets/... - static assets, use as-is
|
||||
* - /api/static/uploads/... - already correct, return as-is
|
||||
* - /assets/... - static assets in public folder, return as-is
|
||||
* - /static/uploads/... - needs /api prefix
|
||||
* - https://... - external URL, use as-is
|
||||
* - https://... or http://... - external URL, return as-is
|
||||
* - uploads/... or static/uploads/... - relative path, needs /api/static prefix
|
||||
*
|
||||
* IMPORTANT: Do NOT manually concatenate /api/static anywhere in the codebase.
|
||||
* Always use this function instead.
|
||||
*/
|
||||
export function normalizeAssetUrl(url: string | null | undefined): string | null {
|
||||
if (!url) return null;
|
||||
|
||||
// Already correct format
|
||||
if (url.startsWith('/api/static')) {
|
||||
return url;
|
||||
}
|
||||
|
||||
// Static assets in public folder
|
||||
if (url.startsWith('/assets/')) {
|
||||
return url;
|
||||
}
|
||||
|
||||
// External URLs
|
||||
// External URLs - return as-is
|
||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||
return url;
|
||||
}
|
||||
|
||||
// Old format - needs /api prefix
|
||||
// Already correct format with /api/static
|
||||
if (url.startsWith('/api/static')) {
|
||||
return url;
|
||||
}
|
||||
|
||||
// Static assets in public folder (/assets/...)
|
||||
if (url.startsWith('/assets/')) {
|
||||
return url;
|
||||
}
|
||||
|
||||
// Old format with /static/ prefix - add /api
|
||||
if (url.startsWith('/static/')) {
|
||||
return `/api${url}`;
|
||||
}
|
||||
|
||||
// Unknown format - return as-is
|
||||
// Relative path without leading slash (uploads/..., static/...)
|
||||
// Remove any duplicate prefixes and normalize
|
||||
let cleaned = url;
|
||||
|
||||
// Remove leading /api/static if somehow duplicated
|
||||
cleaned = cleaned.replace(/^\/api\/static\//, '');
|
||||
// Remove leading /static/
|
||||
cleaned = cleaned.replace(/^\/static\//, '');
|
||||
// Remove leading static/ (no slash)
|
||||
cleaned = cleaned.replace(/^static\//, '');
|
||||
// Remove leading slash
|
||||
cleaned = cleaned.replace(/^\/+/, '');
|
||||
|
||||
// If it looks like a relative upload path, prefix with /api/static/
|
||||
if (cleaned.startsWith('uploads/') || cleaned.includes('/')) {
|
||||
return `/api/static/${cleaned}`;
|
||||
}
|
||||
|
||||
// Unknown format - return as-is (might be a simple filename)
|
||||
return url;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user