New core product layout: resizable chat + artifacts panel replaces the old wizard-only flow. Front door (create/grow), onboarding exits to chat, CEO discusses strategy before planning. Approval actions live in the artifacts pane, not inline in chat. Chat history drawer, animated paperclip thinking indicator, optimistic typing, faster polling. Rename Issue → Task across all frontend UI labels (16 files). Add global pause/resume all agents on dashboard with sidebar badge. Move toasts to bottom-right. Add Artifacts page and sidebar nav item. Reorder wizard: Mission → CEO config → Launch (exits to chat). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
99 lines
3.2 KiB
TypeScript
99 lines
3.2 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { Link } from "@/lib/router";
|
|
import { X } from "lucide-react";
|
|
import { useToast, type ToastItem, type ToastTone } from "../context/ToastContext";
|
|
import { cn } from "../lib/utils";
|
|
|
|
const toneClasses: Record<ToastTone, string> = {
|
|
info: "border-sky-300 bg-sky-50 text-sky-900 dark:border-sky-500/25 dark:bg-sky-950/60 dark:text-sky-100",
|
|
success: "border-emerald-300 bg-emerald-50 text-emerald-900 dark:border-emerald-500/25 dark:bg-emerald-950/60 dark:text-emerald-100",
|
|
warn: "border-amber-300 bg-amber-50 text-amber-900 dark:border-amber-500/25 dark:bg-amber-950/60 dark:text-amber-100",
|
|
error: "border-red-300 bg-red-50 text-red-900 dark:border-red-500/30 dark:bg-red-950/60 dark:text-red-100",
|
|
};
|
|
|
|
const toneDotClasses: Record<ToastTone, string> = {
|
|
info: "bg-sky-500 dark:bg-sky-400",
|
|
success: "bg-emerald-500 dark:bg-emerald-400",
|
|
warn: "bg-amber-500 dark:bg-amber-400",
|
|
error: "bg-red-500 dark:bg-red-400",
|
|
};
|
|
|
|
function AnimatedToast({
|
|
toast,
|
|
onDismiss,
|
|
}: {
|
|
toast: ToastItem;
|
|
onDismiss: (id: string) => void;
|
|
}) {
|
|
const [visible, setVisible] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const frame = requestAnimationFrame(() => setVisible(true));
|
|
return () => cancelAnimationFrame(frame);
|
|
}, []);
|
|
|
|
return (
|
|
<li
|
|
className={cn(
|
|
"pointer-events-auto rounded-sm border shadow-lg backdrop-blur-xl transition-[transform,opacity] duration-200 ease-out",
|
|
visible
|
|
? "translate-y-0 opacity-100"
|
|
: "translate-y-3 opacity-0",
|
|
toneClasses[toast.tone],
|
|
)}
|
|
>
|
|
<div className="flex items-start gap-3 px-3 py-2.5">
|
|
<span className={cn("mt-1 h-2 w-2 shrink-0 rounded-full", toneDotClasses[toast.tone])} />
|
|
<div className="min-w-0 flex-1">
|
|
<p className="text-sm font-semibold leading-5">{toast.title}</p>
|
|
{toast.body && (
|
|
<p className="mt-1 text-xs leading-4 opacity-70">
|
|
{toast.body}
|
|
</p>
|
|
)}
|
|
{toast.action && (
|
|
<Link
|
|
to={toast.action.href}
|
|
onClick={() => onDismiss(toast.id)}
|
|
className="mt-2 inline-flex text-xs font-medium underline underline-offset-4 hover:opacity-90"
|
|
>
|
|
{toast.action.label}
|
|
</Link>
|
|
)}
|
|
</div>
|
|
<button
|
|
type="button"
|
|
aria-label="Dismiss notification"
|
|
onClick={() => onDismiss(toast.id)}
|
|
className="mt-0.5 shrink-0 rounded p-1 opacity-50 hover:bg-black/10 hover:opacity-100 dark:hover:bg-white/10"
|
|
>
|
|
<X className="h-3.5 w-3.5" />
|
|
</button>
|
|
</div>
|
|
</li>
|
|
);
|
|
}
|
|
|
|
export function ToastViewport() {
|
|
const { toasts, dismissToast } = useToast();
|
|
|
|
if (toasts.length === 0) return null;
|
|
|
|
return (
|
|
<aside
|
|
aria-live="polite"
|
|
aria-atomic="false"
|
|
className="pointer-events-none fixed bottom-3 right-3 z-[120] w-full max-w-sm px-1"
|
|
>
|
|
<ol className="flex w-full flex-col-reverse gap-2">
|
|
{toasts.map((toast) => (
|
|
<AnimatedToast
|
|
key={toast.id}
|
|
toast={toast}
|
|
onDismiss={dismissToast}
|
|
/>
|
|
))}
|
|
</ol>
|
|
</aside>
|
|
);
|
|
}
|