perf: optimize app startup and background lifecycles

This commit is contained in:
inman
2026-08-19 00:56:15 +08:00
parent 31332f6d76
commit 927e13349b
121 changed files with 4359 additions and 1400 deletions

View File

@@ -1,7 +1,5 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { useGSAP } from '@gsap/react';
import { gsap } from 'gsap';
import { ArrowLeft, Check, FileText, FolderOpen, Plus, Trash2, Upload, Wrench, X } from 'lucide-react';
import { toast } from 'sonner';
import { Badge } from '@/components/ui/badge';
@@ -26,20 +24,6 @@ type SkillStructureEntry = { path: string; type: 'file' | 'directory' };
type RuntimeSkillInfo = { name: string; description?: string; location?: string; content?: string; entries?: SkillStructureEntry[] };
type SkillInfo = { id: string; name: string; description: string; location: string; content: string; entries: SkillStructureEntry[] };
gsap.registerPlugin(useGSAP);
function prefersReducedMotion() {
return typeof window === 'undefined'
|| typeof window.matchMedia !== 'function'
|| window.matchMedia('(prefers-reduced-motion: reduce)').matches;
}
function supportsHover() {
return typeof window !== 'undefined'
&& typeof window.matchMedia === 'function'
&& window.matchMedia('(hover: hover) and (pointer: fine)').matches;
}
function isMissingRuntimeSessionError(error: unknown): boolean {
const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
return message.includes('404') || message.includes('not found') || message.includes('不存在');
@@ -66,29 +50,14 @@ function createCustomAgent(index: number, model: string | null): ProjectAgentCon
}
function ResourceCard({ id, icon, title, subtitle, onClick }: { id: string; icon: React.ReactNode; title: string; subtitle: string; onClick: () => void }) {
const cardRef = useRef<HTMLButtonElement | null>(null);
const { contextSafe } = useGSAP({ scope: cardRef });
const animateHover = contextSafe((target: HTMLButtonElement, active: boolean) => {
if (prefersReducedMotion() || !supportsHover()) return;
gsap.to(target, { y: active ? -4 : 0, scale: active ? 1.015 : 1, duration: 0.22, ease: 'power2.out', overwrite: 'auto' });
});
return <button ref={cardRef} type="button" data-testid={`resource-card-${id}`} onClick={onClick} onPointerEnter={(event) => animateHover(event.currentTarget, true)} onPointerLeave={(event) => animateHover(event.currentTarget, false)} className="config-motion-resource surface-card motion-press rounded-2xl border border-border/70 bg-background p-3 text-left shadow-none hover:shadow-soft">
return <button type="button" data-testid={`resource-card-${id}`} onClick={onClick} className="config-motion-resource surface-card motion-press rounded-2xl border border-border/70 bg-background p-3 text-left shadow-none hover:shadow-soft">
<div className="flex items-center gap-2"><span className="text-2xl">{icon}</span><p className="text-sm font-semibold">{title}</p></div><p className="mt-1.5 truncate text-[11px] font-medium text-muted-foreground">{subtitle}</p>
</button>;
}
function DrawerFrame({ title, desc, onClose, children, closeLabel = '关闭', closeIcon = 'close' }: { title: string; desc: string; onClose: () => void; children: React.ReactNode; closeLabel?: string; closeIcon?: 'close' | 'back' }) {
const drawerRef = useRef<HTMLDivElement | null>(null);
useGSAP(() => {
if (prefersReducedMotion()) return;
gsap.timeline({ defaults: { ease: 'power3.out' } })
// Keep the drawer content render-safe: movement should never make the
// existing controls disappear if the entrance timeline is interrupted.
.from('.drawer-motion-header', { x: 8, duration: 0.22 })
.from('.drawer-motion-body > *', { y: 6, duration: 0.22, stagger: 0.035 }, '-=0.12');
}, { scope: drawerRef });
const closeButton = <Button type="button" variant="ghost" size="icon" onClick={onClose} aria-label={closeLabel} title={closeLabel} className="relative z-10 shrink-0">{closeIcon === 'back' ? <ArrowLeft className="h-4 w-4" /> : <X className="h-4 w-4" />}</Button>;
return <div ref={drawerRef} className="flex min-h-0 flex-1 flex-col"><div className="glass-surface drawer-motion-header flex items-center justify-between gap-3 border-b border-border/70 bg-background/80 px-5 py-4"><div className="min-w-0 flex-1"><SheetTitle className="text-lg font-semibold">{title}</SheetTitle><SheetDescription className="mt-1 text-sm font-medium text-muted-foreground">{desc}</SheetDescription></div>{closeIcon === 'back' ? closeButton : <SheetClose asChild>{closeButton}</SheetClose>}</div><div className="drawer-motion-body min-h-0 flex-1 overflow-auto p-5">{children}</div></div>;
return <div className="flex min-h-0 flex-1 flex-col"><div className="glass-surface drawer-motion-header flex items-center justify-between gap-3 border-b border-border/70 bg-background px-5 py-4"><div className="min-w-0 flex-1"><SheetTitle className="text-lg font-semibold">{title}</SheetTitle><SheetDescription className="mt-1 text-sm font-medium text-muted-foreground">{desc}</SheetDescription></div>{closeIcon === 'back' ? closeButton : <SheetClose asChild>{closeButton}</SheetClose>}</div><div className="drawer-motion-body min-h-0 flex-1 overflow-auto p-5">{children}</div></div>;
}
function SkillDetail({ skill }: { skill: SkillInfo }) {
@@ -114,14 +83,7 @@ function SkillDetail({ skill }: { skill: SkillInfo }) {
}
function AgentCard({ agent, onOpen }: { agent: ProjectAgentConfig; onOpen: () => void }) {
const cardRef = useRef<HTMLButtonElement | null>(null);
const { contextSafe } = useGSAP({ scope: cardRef });
const animateHover = contextSafe((target: HTMLButtonElement, active: boolean) => {
if (prefersReducedMotion() || !supportsHover()) return;
gsap.to(target, { y: active ? -7 : 0, rotation: active ? -0.4 : 0, zIndex: active ? 2 : 0, duration: 0.25, ease: 'power2.out', overwrite: 'auto' });
gsap.to(target.querySelector('.agent-motion-avatar'), { scale: active ? 1.07 : 1, duration: 0.28, ease: 'back.out(1.7)', overwrite: 'auto' });
});
return <button ref={cardRef} type="button" data-testid={`project-agent-${agent.id}`} onClick={onOpen} onPointerEnter={(event) => animateHover(event.currentTarget, true)} onPointerLeave={(event) => animateHover(event.currentTarget, false)} className="config-motion-agent surface-card motion-press h-[260px] w-[210px] flex-none rounded-2xl border border-border/70 bg-background p-4 text-left shadow-none hover:shadow-soft"><img src={getAgentAvatarSrc(agent.avatarId, agent.avatarDataUrl)} alt="" className="agent-motion-avatar mx-auto h-20 w-20 rounded-xl border border-border object-cover [image-rendering:pixelated]" /><div className="mt-4 flex items-center justify-between gap-2"><p className="truncate font-semibold">{agent.name || '未命名'}</p><Badge className="shrink-0 border border-brand/15 bg-brand-soft text-brand"></Badge></div><p className="mt-1 truncate text-xs font-semibold text-muted-foreground">{agent.roleName}</p><p className="mt-3 line-clamp-3 min-h-12 text-xs text-muted-foreground">{agent.responsibility.mission}</p><p className="mt-2 text-[11px] text-muted-foreground">{agent.skillIds.length} </p></button>;
return <button type="button" data-testid={`project-agent-${agent.id}`} onClick={onOpen} className="config-motion-agent surface-card motion-press h-[260px] w-[210px] flex-none rounded-2xl border border-border/70 bg-background p-4 text-left shadow-none hover:shadow-soft"><img src={getAgentAvatarSrc(agent.avatarId, agent.avatarDataUrl)} alt="" className="agent-motion-avatar mx-auto h-20 w-20 rounded-xl border border-border object-cover [image-rendering:pixelated]" /><div className="mt-4 flex items-center justify-between gap-2"><p className="truncate font-semibold">{agent.name || '未命名'}</p><Badge className="shrink-0 border border-brand/15 bg-brand-soft text-brand"></Badge></div><p className="mt-1 truncate text-xs font-semibold text-muted-foreground">{agent.roleName}</p><p className="mt-3 line-clamp-3 min-h-12 text-xs text-muted-foreground">{agent.responsibility.mission}</p><p className="mt-2 text-[11px] text-muted-foreground">{agent.skillIds.length} </p></button>;
}
function ModelCard({ model }: { model: ConfiguredModelOption }) {
@@ -183,7 +145,6 @@ export function ProjectConfiguration() {
const [partnerDialogOpen, setPartnerDialogOpen] = useState(false);
const [archiveAgentId, setArchiveAgentId] = useState<string | null>(null);
const [deleteAgentId, setDeleteAgentId] = useState<string | null>(null);
const pageRef = useRef<HTMLElement | null>(null);
const knowledgeInputRef = useRef<HTMLInputElement | null>(null);
const navigate = useNavigate();
const location = useLocation();
@@ -215,17 +176,7 @@ export function ProjectConfiguration() {
const modelOptions = useMemo(() => buildConfiguredModelOptions(providerAccounts, providerStatuses, providerDefaultAccountId), [providerAccounts, providerStatuses, providerDefaultAccountId]);
const activeAgent = draft?.agents.find((agent) => agent.id === activeAgentId) ?? null;
const activeSkill = skills.find((skill) => skill.id === activeSkillId) ?? null;
const { contextSafe } = useGSAP(() => {
if (!draft || prefersReducedMotion()) return;
gsap.timeline({ defaults: { ease: 'power3.out' } })
// Keep cards in their layout position on first paint. Their deliberate
// motion belongs to hover/open/save feedback, not page load.
.from('.config-motion-title', { y: 6, duration: 0.22 });
}, { scope: pageRef, dependencies: [Boolean(draft)], revertOnUpdate: true });
const playSaveConfirmation = contextSafe(() => {
if (prefersReducedMotion()) return;
gsap.fromTo('.project-save-button', { scale: 0.96 }, { scale: 1, duration: 0.42, ease: 'back.out(2.4)', overwrite: 'auto' });
});
const playSaveConfirmation = () => undefined;
const handleBack = () => navigate(-1);
if (!activeProject) return <main data-testid="project-configuration-empty-state" className="flex min-h-full items-start bg-background p-8 text-foreground"><p className="pt-4 text-xl font-semibold tracking-[-0.02em]">一念成光,万物可创。</p></main>;
@@ -337,7 +288,7 @@ export function ProjectConfiguration() {
toast.success(`${target.name || '未命名伙伴'}`);
};
return <main ref={pageRef} className="-m-5 flex min-h-[calc(100%+2.5rem)] min-h-0 flex-col overflow-auto bg-background p-4 text-foreground sm:-m-6 sm:min-h-[calc(100%+3rem)]" data-testid="project-configuration-page">
return <main className="-m-5 flex min-h-[calc(100%+2.5rem)] min-h-0 flex-col overflow-auto bg-background p-4 text-foreground sm:-m-6 sm:min-h-[calc(100%+3rem)]" data-testid="project-configuration-page">
<div className="mx-auto flex min-h-0 w-full max-w-7xl flex-1 flex-col gap-3">
<div className="config-motion-title flex items-center gap-2">
<Button type="button" variant="ghost" size="icon" data-testid="project-configuration-back-button" aria-label="返回" title="返回" onClick={handleBack} className="h-8 w-8 shrink-0 text-muted-foreground hover:bg-surface-subtle hover:text-foreground">