Revise design and UI components for AI video admin interface

Updated DESIGN.md to reflect a new editorial design approach with a navy palette and refined typography. Enhanced global styles in globals.css, including new color tokens and layout adjustments. Refactored components in AppShell, Sidebar, and Topbar for improved consistency and theming. Introduced a ThemeToggle component for user theme preferences and updated various pages to utilize new styles and components, ensuring a cohesive user experience across the application.
This commit is contained in:
Xin Wang
2026-06-05 13:48:42 +08:00
parent 9952e08e49
commit 571c67526f
17 changed files with 809 additions and 583 deletions

View File

@@ -9,7 +9,8 @@
--color-foreground: var(--foreground);
--font-sans: var(--font-sans);
--font-mono: var(--font-geist-mono);
--font-heading: var(--font-sans);
--font-display: var(--font-display);
--font-heading: var(--font-display);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
@@ -39,6 +40,24 @@
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
/* Editorial (ElevenLabs-derived) tokens — navy palette */
--color-canvas-soft: var(--canvas-soft);
--color-surface-strong: var(--surface-strong);
--color-ink: var(--ink);
--color-body: var(--body-text);
--color-muted-soft: var(--muted-soft);
--color-hairline: var(--hairline);
--color-hairline-soft: var(--hairline-soft);
--color-hairline-strong: var(--hairline-strong);
--color-on-primary: var(--on-primary);
--color-success: var(--success);
--color-gradient-mint: var(--gradient-mint);
--color-gradient-peach: var(--gradient-peach);
--color-gradient-lavender: var(--gradient-lavender);
--color-gradient-sky: var(--gradient-sky);
--color-gradient-rose: var(--gradient-rose);
--radius-sm: calc(var(--radius) * 0.6);
--radius-md: calc(var(--radius) * 0.8);
--radius-lg: var(--radius);
@@ -48,73 +67,114 @@
--radius-4xl: calc(var(--radius) * 2.6);
}
/* ---------- Light navy (editorial off-white, navy ink) ---------- */
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.55 0.22 255);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
--background: #f3f5fb;
--foreground: #0f1b33;
--card: #ffffff;
--card-foreground: #0f1b33;
--popover: #ffffff;
--popover-foreground: #0f1b33;
--primary: #1b2741;
--primary-foreground: #ffffff;
--secondary: #e9edf7;
--secondary-foreground: #1b2741;
--muted: #eef1f8;
--muted-foreground: #5d6b86;
--accent: #e9edf7;
--accent-foreground: #1b2741;
--destructive: #dc2626;
--border: #dfe4f0;
--input: #cbd3e4;
--ring: #94a0bd;
--chart-1: #1b2741;
--chart-2: #3a4a6b;
--chart-3: #5d6b86;
--chart-4: #94a0bd;
--chart-5: #c8d0e2;
--radius: 0.75rem;
/* Editorial tokens */
--canvas-soft: #f9fafd;
--surface-strong: #e9edf7;
--ink: #0c1426;
--body-text: #44516c;
--muted-soft: #94a0bd;
--hairline: #e3e7f1;
--hairline-soft: #eef1f8;
--hairline-strong: #cbd3e4;
--on-primary: #ffffff;
--success: #16a34a;
/* Atmospheric pastel orbs (cool-leaning for navy) */
--gradient-mint: #a7e5d3;
--gradient-peach: #f4c5a8;
--gradient-lavender: #c8b8e0;
--gradient-sky: #a8c8e8;
--gradient-rose: #e8b8c4;
--sidebar: #ffffff;
--sidebar-foreground: #0f1b33;
--sidebar-primary: #1b2741;
--sidebar-primary-foreground: #ffffff;
--sidebar-accent: #eef1f8;
--sidebar-accent-foreground: #1b2741;
--sidebar-border: #e3e7f1;
--sidebar-ring: #94a0bd;
}
/* ---------- Dark navy (deep navy canvas, off-white ink) ---------- */
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.6 0.22 255);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
--background: #070b16;
--foreground: #e8edf9;
--card: #0e1626;
--card-foreground: #e8edf9;
--popover: #0e1626;
--popover-foreground: #e8edf9;
--primary: #e8edf9;
--primary-foreground: #0c1426;
--secondary: #18233a;
--secondary-foreground: #e8edf9;
--muted: #141d30;
--muted-foreground: #93a0bb;
--accent: #18233a;
--accent-foreground: #e8edf9;
--destructive: #f87171;
--border: #1b2740;
--input: #283450;
--ring: #4a5876;
--chart-1: #e8edf9;
--chart-2: #c8d0e2;
--chart-3: #93a0bb;
--chart-4: #5d6b86;
--chart-5: #2a3654;
/* Editorial tokens */
--canvas-soft: #0b1322;
--surface-strong: #18233a;
--ink: #f1f5ff;
--body-text: #a6b2cb;
--muted-soft: #6c7a96;
--hairline: #1b2740;
--hairline-soft: #141d30;
--hairline-strong: #283450;
--on-primary: #0c1426;
--success: #4ade80;
--gradient-mint: #5fae9b;
--gradient-peach: #c08a6b;
--gradient-lavender: #8a78ad;
--gradient-sky: #5f86b8;
--gradient-rose: #b07d8c;
--sidebar: #0a111e;
--sidebar-foreground: #e8edf9;
--sidebar-primary: #e8edf9;
--sidebar-primary-foreground: #0c1426;
--sidebar-accent: #18233a;
--sidebar-accent-foreground: #e8edf9;
--sidebar-border: #1b2740;
--sidebar-ring: #4a5876;
}
@layer base {
@@ -123,8 +183,52 @@
}
body {
@apply bg-background text-foreground;
letter-spacing: 0.01em;
}
html {
@apply font-sans;
}
}
}
@layer components {
/* Waldenburg Light substitute — EB Garamond at weight 300. The editorial
display signature: serif, light, tightly tracked. Never bold. */
.font-display {
font-family: var(--font-display), "Times New Roman", serif;
font-weight: 300;
letter-spacing: -0.01em;
}
.display-mega {
font-size: clamp(2.5rem, 5vw, 4rem);
line-height: 1.05;
letter-spacing: -0.03em;
}
.display-xl {
font-size: clamp(2rem, 4vw, 3rem);
line-height: 1.08;
letter-spacing: -0.02em;
}
.display-lg {
font-size: 2.25rem;
line-height: 1.17;
letter-spacing: -0.01em;
}
.display-md {
font-size: 2rem;
line-height: 1.13;
letter-spacing: -0.01em;
}
.display-sm {
font-size: 1.5rem;
line-height: 1.2;
}
/* Caption label — uppercase, tracked, the editorial section marker */
.caption-label {
font-size: 0.75rem;
font-weight: 600;
line-height: 1.4;
letter-spacing: 0.08em;
text-transform: uppercase;
}
}

View File

@@ -1,13 +1,16 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono, Inter } from "next/font/google";
import { Geist_Mono, Inter, Cormorant_Garamond } from "next/font/google";
import "./globals.css";
import { cn } from "@/lib/utils";
const inter = Inter({subsets:['latin'],variable:'--font-sans'});
const inter = Inter({ subsets: ["latin"], variable: "--font-sans" });
const geistSans = Geist({
variable: "--font-geist-sans",
// Waldenburg Light is licensed; Cormorant Garamond at weight 300 is a close
// open-source substitute for the light editorial display voice.
const display = Cormorant_Garamond({
subsets: ["latin"],
weight: ["300", "400", "500"],
variable: "--font-display",
});
const geistMono = Geist_Mono({
@@ -16,10 +19,13 @@ const geistMono = Geist_Mono({
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "AI 视频助手 · 管理台",
description: "创建、配置、测试并发布 AI 视频助手",
};
// Apply the persisted theme before paint to avoid a flash. Defaults to dark navy.
const themeScript = `(function(){try{var t=localStorage.getItem('theme');if(t==='light'){document.documentElement.classList.remove('dark')}else{document.documentElement.classList.add('dark')}}catch(e){document.documentElement.classList.add('dark')}})();`;
export default function RootLayout({
children,
}: Readonly<{
@@ -27,9 +33,21 @@ export default function RootLayout({
}>) {
return (
<html
lang="en"
className={cn("h-full", "antialiased", geistSans.variable, geistMono.variable, "font-sans", inter.variable)}
lang="zh-CN"
className={cn(
"h-full",
"antialiased",
"dark",
geistMono.variable,
"font-sans",
inter.variable,
display.variable,
)}
suppressHydrationWarning
>
<head>
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
</head>
<body className="min-h-full flex flex-col">{children}</body>
</html>
);

View File

@@ -28,7 +28,7 @@ export function AppShell() {
const [collapsed, setCollapsed] = useState(false);
return (
<div className="flex h-screen overflow-hidden bg-[#080b13] text-[#e9eef7]">
<div className="flex h-screen overflow-hidden bg-background text-foreground">
<Sidebar
active={active}
collapsed={collapsed}
@@ -39,7 +39,7 @@ export function AppShell() {
<main className="flex min-w-0 flex-1 flex-col overflow-hidden">
<Topbar />
<div className="flex-1 overflow-y-auto px-8 py-7">
<div className="flex-1 overflow-y-auto px-8 py-10">
{active === "home" && <HomePage onNavigate={setActive} />}
{active === "assistant-prompt" && <AssistantPage />}

View File

@@ -55,27 +55,33 @@ export function Sidebar({
return (
<aside
className={[
"flex shrink-0 flex-col border-r border-[#161d2c] bg-[#0a0e17] transition-all",
"flex shrink-0 flex-col border-r border-sidebar-border bg-sidebar transition-all",
collapsed ? "w-[76px]" : "w-[252px]",
].join(" ")}
>
<div className="flex h-[81px] items-center gap-3 border-b border-[#161d2c] px-5">
<div className="relative flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl bg-gradient-to-br from-cyan-400 to-blue-500 text-white shadow-lg shadow-cyan-500/30">
<Video size={22} />
<div className="absolute inset-0 rounded-2xl ring-1 ring-inset ring-white/20" />
<div className="flex h-[81px] items-center gap-3 border-b border-sidebar-border px-5">
<div
className="relative flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl text-on-primary shadow-sm"
style={{
backgroundColor: "var(--primary)",
backgroundImage:
"radial-gradient(circle at 30% 20%, color-mix(in srgb, var(--gradient-sky) 70%, transparent), transparent 60%), radial-gradient(circle at 80% 90%, color-mix(in srgb, var(--gradient-lavender) 65%, transparent), transparent 55%)",
}}
>
<Video size={22} style={{ color: "var(--primary-foreground)" }} />
</div>
{!collapsed && (
<div>
<div className="bg-gradient-to-r from-cyan-300 to-blue-400 bg-clip-text text-sm font-bold text-transparent">
<div className="font-display text-base text-foreground">
AI
</div>
<div className="text-xs text-[#5d6880]"></div>
<div className="caption-label text-muted-soft"></div>
</div>
)}
</div>
<nav className="flex-1 space-y-2 px-3 py-4">
<nav className="flex-1 space-y-1 px-3 py-5">
<NavButton
active={active === "home"}
collapsed={collapsed}
@@ -84,55 +90,51 @@ export function Sidebar({
onClick={() => onNavigate("home")}
/>
<div>
<div className="pt-2">
<div
className={[
"flex h-11 w-full items-center gap-3 rounded-xl px-3 text-sm",
assistantActive
? "bg-blue-500/10 text-blue-300"
: "text-[#9aa6bd]",
"flex h-11 w-full items-center gap-3 rounded-full px-3 text-sm",
assistantActive ? "text-foreground" : "text-muted-foreground",
collapsed ? "justify-center" : "",
].join(" ")}
>
<Bot size={18} />
{!collapsed && <span></span>}
{!collapsed && <span className="font-medium"></span>}
</div>
<div
className={[
"mt-1 space-y-1",
collapsed ? "pl-0" : "pl-6",
].join(" ")}
>
<div className={["mt-1 space-y-1", collapsed ? "pl-0" : "pl-5"].join(" ")}>
{assistantSubItems.map((item) => (
<SubNavButton
<NavButton
key={item.key}
active={active === item.key}
collapsed={collapsed}
icon={item.icon}
label={item.label}
onClick={() => onNavigate(item.key)}
small
/>
))}
</div>
</div>
{mainItems.slice(1).map((item) => (
<NavButton
key={item.key}
active={active === item.key}
collapsed={collapsed}
icon={item.icon}
label={item.label}
onClick={() => onNavigate(item.key)}
/>
))}
<div className="pt-2">
{mainItems.slice(1).map((item) => (
<NavButton
key={item.key}
active={active === item.key}
collapsed={collapsed}
icon={item.icon}
label={item.label}
onClick={() => onNavigate(item.key)}
/>
))}
</div>
</nav>
<div className="border-t border-[#161d2c] p-3">
<div className="border-t border-sidebar-border p-3">
<button
onClick={onToggle}
className="flex h-10 w-full items-center justify-center rounded-xl border border-[#1b2233] bg-[#0f1521] text-[#9aa6bd] hover:text-white"
className="flex h-10 w-full items-center justify-center rounded-full border border-hairline-strong text-muted-foreground transition-colors hover:bg-surface-strong hover:text-foreground"
>
<ChevronLeft
size={18}
@@ -150,58 +152,30 @@ function NavButton({
icon: Icon,
label,
onClick,
small = false,
}: {
active: boolean;
collapsed: boolean;
icon: React.ComponentType<{ size?: number }>;
label: string;
onClick: () => void;
small?: boolean;
}) {
return (
<button
onClick={onClick}
title={collapsed ? label : undefined}
className={[
"flex h-11 w-full items-center gap-3 rounded-xl px-3 text-sm transition",
"mt-1 flex w-full items-center gap-3 rounded-full px-3 text-sm transition-colors",
small ? "h-10" : "h-11",
active
? "bg-blue-500/15 text-blue-400"
: "text-[#9aa6bd] hover:bg-white/5 hover:text-white",
? "bg-sidebar-accent text-sidebar-accent-foreground font-medium"
: "text-muted-foreground hover:bg-sidebar-accent/60 hover:text-foreground",
collapsed ? "justify-center" : "",
].join(" ")}
>
<Icon size={18} />
<Icon size={small ? 16 : 18} />
{!collapsed && <span>{label}</span>}
</button>
);
}
function SubNavButton({
active,
collapsed,
icon: Icon,
label,
onClick,
}: {
active: boolean;
collapsed: boolean;
icon: React.ComponentType<{ size?: number }>;
label: string;
onClick: () => void;
}) {
return (
<button
onClick={onClick}
title={collapsed ? label : undefined}
className={[
"flex h-10 w-full items-center gap-3 rounded-xl px-3 text-sm transition",
active
? "bg-blue-500/15 text-blue-400"
: "text-[#7f8aa3] hover:bg-white/5 hover:text-white",
collapsed ? "justify-center" : "",
].join(" ")}
>
<Icon size={16} />
{!collapsed && <span>{label}</span>}
</button>
);
}

View File

@@ -0,0 +1,34 @@
"use client";
import { useEffect, useState } from "react";
import { Moon, Sun } from "lucide-react";
export function ThemeToggle() {
const [dark, setDark] = useState(true);
useEffect(() => {
setDark(document.documentElement.classList.contains("dark"));
}, []);
function toggle() {
const next = !dark;
setDark(next);
document.documentElement.classList.toggle("dark", next);
try {
localStorage.setItem("theme", next ? "dark" : "light");
} catch {
/* ignore */
}
}
return (
<button
onClick={toggle}
title={dark ? "切换到浅色" : "切换到深色"}
aria-label="切换主题"
className="flex size-10 items-center justify-center rounded-full border border-hairline-strong text-muted-foreground transition-colors hover:bg-surface-strong hover:text-foreground"
>
{dark ? <Sun size={17} /> : <Moon size={17} />}
</button>
);
}

View File

@@ -1,5 +1,6 @@
import { Bell, HelpCircle, ChevronDown } from "lucide-react";
import { Button } from "@/components/ui/button";
import { ThemeToggle } from "./ThemeToggle";
import {
Tooltip,
TooltipContent,
@@ -9,43 +10,51 @@ import {
export function Topbar() {
return (
<header className="flex h-[81px] shrink-0 items-center justify-end border-b border-[#161d2c] bg-[#080b13] px-8">
<header className="flex h-[81px] shrink-0 items-center justify-end gap-2 border-b border-border bg-background px-8">
<TooltipProvider>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="lg"
className="rounded-xl border-[#1b2233] bg-[#0f1521] text-[#9aa6bd] hover:bg-[#151e30] hover:text-white hover:border-[#2a3550] transition-colors"
<Button
variant="outline"
size="lg"
className="border-hairline-strong text-muted-foreground hover:bg-surface-strong hover:text-foreground"
>
<HelpCircle />
</Button>
<ThemeToggle />
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="icon-lg"
className="border-hairline-strong text-muted-foreground hover:bg-surface-strong hover:text-foreground"
>
<Bell />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom"></TooltipContent>
</Tooltip>
<button className="flex items-center gap-2.5 rounded-full border border-hairline-strong px-3 py-1.5 transition-colors hover:bg-surface-strong">
<div
className="flex h-8 w-8 items-center justify-center rounded-full text-sm font-medium text-on-primary"
style={{
backgroundColor: "var(--primary)",
backgroundImage:
"radial-gradient(circle at 30% 20%, color-mix(in srgb, var(--gradient-sky) 70%, transparent), transparent 60%)",
color: "var(--primary-foreground)",
}}
>
<HelpCircle />
</Button>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="icon-lg"
className="rounded-xl border-[#1b2233] bg-[#0f1521] text-[#9aa6bd] hover:bg-[#151e30] hover:text-white hover:border-[#2a3550] transition-colors"
>
<Bell />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom"></TooltipContent>
</Tooltip>
<button className="flex items-center gap-2.5 rounded-xl border border-[#1b2233] bg-[#0f1521] px-3 py-2 transition-colors hover:bg-[#151e30] hover:border-[#2a3550]">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-gradient-to-br from-cyan-400 to-blue-500 text-sm font-bold text-white shadow-lg shadow-cyan-500/25">
A
</div>
<div className="hidden text-sm md:block text-left">
<div className="font-semibold text-[#e9eef7]"></div>
<div className="text-xs text-[#5d6880]">admin</div>
</div>
<ChevronDown size={14} className="hidden md:block text-[#5d6880]" />
</button>
</div>
A
</div>
<div className="hidden text-sm md:block text-left">
<div className="font-medium text-foreground"></div>
<div className="text-xs text-muted-soft">admin</div>
</div>
<ChevronDown size={14} className="hidden md:block text-muted-soft" />
</button>
</TooltipProvider>
</header>
);
}
}

View File

@@ -66,28 +66,31 @@ export function AssistantPage() {
return (
<div className="mx-auto flex w-full max-w-[1180px] flex-col gap-6">
<div className="flex items-start justify-between">
<div className="flex items-start justify-between gap-6">
<div>
<div className="flex items-center gap-3">
<span className="h-3 w-3 rounded-full bg-blue-400 shadow-[0_0_0_4px_rgba(46,161,255,.16),0_0_14px_rgba(46,161,255,.35)]" />
<h1 className="text-3xl font-bold"> · </h1>
</div>
<p className="mt-2 text-sm text-[#5d6880]">
<div className="caption-label text-muted-soft"></div>
<h1 className="font-display display-lg mt-3 text-ink">
</h1>
<p className="mt-3 text-[15px] text-muted-foreground">
AI
</p>
</div>
<div className="flex gap-3">
<Button variant="outline" className="gap-2 border-[#1b2233] bg-[#0f1521] text-[#9aa6bd] hover:bg-[#1b2233] hover:text-[#e9eef7]">
<div className="flex shrink-0 gap-3">
<Button
variant="outline"
size="lg"
className="gap-2 border-hairline-strong text-foreground hover:bg-surface-strong"
>
<Save size={16} />
稿
</Button>
<Button className="gap-2">
<Button size="lg" className="gap-2">
<Rocket size={16} />
</Button>
</div>
</div>
<div className="space-y-5">
@@ -192,16 +195,16 @@ function SectionCard({
children: React.ReactNode;
}) {
return (
<Card className="border-[#1b2233] bg-[#0f1521] text-[#e9eef7]">
<Card className="rounded-2xl border-hairline bg-card text-card-foreground shadow-sm">
<CardHeader>
<div className="flex items-start gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-blue-500/10 text-blue-400">
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-surface-strong text-foreground">
{icon}
</div>
<div>
<CardTitle className="text-base">{title}</CardTitle>
<CardDescription className="mt-1 text-[#5d6880]">
<CardTitle className="text-base font-medium">{title}</CardTitle>
<CardDescription className="mt-1 text-muted-foreground">
{description}
</CardDescription>
</div>
@@ -226,12 +229,12 @@ function TextField({
}) {
return (
<label className="block">
<div className="mb-2 text-sm font-medium text-[#9aa6bd]">{label}</div>
<div className="mb-2 text-sm font-medium text-foreground">{label}</div>
<Input
value={value}
placeholder={placeholder}
onChange={(event) => onChange(event.target.value)}
className="border-[#1b2233] bg-[#0d121d] text-white placeholder:text-[#5d6880]"
className="border-hairline-strong bg-background text-foreground placeholder:text-muted-soft"
/>
</label>
);
@@ -252,13 +255,13 @@ function TextAreaField({
}) {
return (
<label className="block">
<div className="mb-2 text-sm font-medium text-[#9aa6bd]">{label}</div>
<div className="mb-2 text-sm font-medium text-foreground">{label}</div>
<Textarea
value={value}
placeholder={placeholder}
onChange={(event) => onChange(event.target.value)}
rows={rows}
className="resize-none border-[#1b2233] bg-[#0d121d] text-white placeholder:text-[#5d6880]"
className="resize-none border-hairline-strong bg-background text-foreground placeholder:text-muted-soft"
/>
</label>
);
@@ -277,20 +280,16 @@ function SelectField({
}) {
return (
<div className="block">
<div className="mb-2 text-sm font-medium text-[#9aa6bd]">{label}</div>
<div className="mb-2 text-sm font-medium text-foreground">{label}</div>
<Select value={value} onValueChange={onChange}>
<SelectTrigger className="w-full border-[#1b2233] bg-[#0d121d] text-white">
<SelectTrigger className="w-full border-hairline-strong bg-background text-foreground">
<SelectValue placeholder={`请选择${label}`} />
</SelectTrigger>
<SelectContent className="border-[#1b2233] bg-[#0f1521] text-white">
<SelectContent className="border-hairline bg-popover text-popover-foreground">
{options.map((item) => (
<SelectItem
key={item}
value={item}
className="focus:bg-blue-500/15 focus:text-blue-300"
>
<SelectItem key={item} value={item}>
{item}
</SelectItem>
))}
@@ -312,10 +311,10 @@ function ToggleRow({
onChange: (checked: boolean) => void;
}) {
return (
<div className="flex items-center justify-between rounded-xl border border-[#1b2233] bg-[#0d121d] p-4">
<div className="flex items-center justify-between rounded-xl border border-hairline bg-canvas-soft p-4">
<div>
<div className="font-semibold">{title}</div>
<div className="mt-1 text-sm text-[#5d6880]">{description}</div>
<div className="font-medium text-foreground">{title}</div>
<div className="mt-1 text-sm text-muted-foreground">{description}</div>
</div>
<Switch checked={checked} onCheckedChange={onChange} />
</div>

View File

@@ -1,47 +1,61 @@
import { ArrowRight, Boxes, GitBranch, Plus, Workflow } from "lucide-react";
import { Button } from "@/components/ui/button";
export function AssistantWorkflowPage() {
return (
<div className="mx-auto flex w-full max-w-[1180px] flex-col gap-6">
<div className="mx-auto flex w-full max-w-[1180px] flex-col gap-8">
<div>
<div className="flex items-center gap-3">
<span className="h-3 w-3 rounded-full bg-cyan-400 shadow-[0_0_0_4px_rgba(34,211,238,.16),0_0_14px_rgba(34,211,238,.35)]" />
<h1 className="text-3xl font-bold"> · </h1>
</div>
<p className="mt-2 text-sm text-[#5d6880]">
AI
<div className="caption-label text-muted-soft"></div>
<h1 className="font-display display-lg mt-3 text-ink"></h1>
<p className="mt-3 max-w-2xl text-[15px] leading-7 text-muted-foreground">
AI
</p>
</div>
<section className="rounded-[28px] border border-cyan-500/25 bg-[radial-gradient(circle_at_top_left,rgba(34,211,238,.16),transparent_34%),#0f1521] p-8">
<div className="flex items-start gap-5">
<div className="flex h-16 w-16 items-center justify-center rounded-3xl bg-cyan-400 text-[#04121a] shadow-[0_0_32px_rgba(34,211,238,.22)]">
<Workflow size={32} />
<section className="relative overflow-hidden rounded-3xl border border-hairline bg-canvas-soft p-10">
<div
aria-hidden
className="pointer-events-none absolute -right-20 -top-24 h-72 w-72 rounded-full opacity-55 blur-3xl"
style={{
backgroundImage:
"radial-gradient(circle, color-mix(in srgb, var(--gradient-mint) 55%, transparent), transparent 70%)",
}}
/>
<div className="relative flex items-start gap-5">
<div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-surface-strong text-foreground">
<Workflow size={30} />
</div>
<div className="flex-1">
<div className="inline-flex rounded-full border border-cyan-500/30 bg-cyan-500/10 px-3 py-1 text-xs font-semibold text-cyan-300">
<div className="caption-label inline-flex rounded-full bg-surface-strong px-3 py-1 text-muted-foreground">
</div>
<h2 className="mt-5 text-2xl font-bold"></h2>
<h2 className="font-display display-sm mt-5 text-ink">
</h2>
<p className="mt-3 max-w-2xl text-sm leading-7 text-[#9aa6bd]">
<p className="mt-3 max-w-2xl text-[15px] leading-7 text-body">
</p>
<div className="mt-6 flex gap-3">
<button className="flex h-10 items-center gap-2 rounded-xl bg-cyan-400 px-4 text-sm font-semibold text-[#04121a]">
<div className="mt-7 flex gap-3">
<Button size="lg" className="gap-2">
<Plus size={16} />
</button>
</Button>
<button className="flex h-10 items-center gap-2 rounded-xl border border-[#1b2233] bg-[#0d121d] px-4 text-sm font-semibold text-[#9aa6bd] hover:text-white">
<Button
variant="outline"
size="lg"
className="gap-2 border-hairline-strong text-foreground hover:bg-surface-strong"
>
<ArrowRight size={16} />
</button>
</Button>
</div>
</div>
</div>
@@ -67,28 +81,30 @@ export function AssistantWorkflowPage() {
/>
</section>
<section className="rounded-2xl border border-[#1b2233] bg-[#0f1521] p-6">
<div className="mb-5 flex items-center justify-between">
<div>
<h2 className="text-lg font-bold"></h2>
<p className="mt-1 text-sm text-[#5d6880]">
React Flow
</p>
</div>
<section className="rounded-2xl border border-hairline bg-card p-6 shadow-sm">
<div className="mb-5">
<h2 className="font-display display-sm text-ink"></h2>
<p className="mt-1 text-sm text-muted-foreground">
React Flow
</p>
</div>
<div className="flex items-center gap-3 overflow-x-auto rounded-2xl border border-[#1b2233] bg-[#0d121d] p-5">
<div className="flex items-center gap-3 overflow-x-auto rounded-2xl border border-hairline bg-canvas-soft p-5">
{["开始", "意图识别", "知识库检索", "模型回答", "工具调用", "结束"].map(
(item, index) => (
<div key={item} className="flex items-center gap-3">
<div className="min-w-[128px] rounded-2xl border border-[#273249] bg-[#111827] p-4 text-center">
<div className="text-sm font-semibold">{item}</div>
<div className="mt-1 text-xs text-[#5d6880]">
<div className="min-w-[128px] rounded-xl border border-hairline bg-card p-4 text-center shadow-sm">
<div className="text-sm font-medium text-foreground">
{item}
</div>
<div className="mt-1 text-xs text-muted-soft">
Node {index + 1}
</div>
</div>
{index < 5 && <ArrowRight size={18} className="text-[#5d6880]" />}
{index < 5 && (
<ArrowRight size={18} className="shrink-0 text-muted-soft" />
)}
</div>
),
)}
@@ -108,13 +124,15 @@ function FeatureCard({
description: string;
}) {
return (
<div className="rounded-2xl border border-[#1b2233] bg-[#0f1521] p-5">
<div className="mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-cyan-500/10 text-cyan-300">
<div className="rounded-2xl border border-hairline bg-card p-6 shadow-sm transition-shadow hover:shadow-md">
<div className="mb-4 flex h-10 w-10 items-center justify-center rounded-full bg-surface-strong text-foreground">
{icon}
</div>
<div className="font-bold">{title}</div>
<p className="mt-2 text-sm leading-6 text-[#5d6880]">{description}</p>
<div className="font-medium text-foreground">{title}</div>
<p className="mt-2 text-sm leading-6 text-muted-foreground">
{description}
</p>
</div>
);
}
}

View File

@@ -1,3 +1,11 @@
import { PlaceholderPage } from "./PlaceholderPage";
export function ComponentsPage() {
return <div className="text-3xl font-bold"></div>;
}
return (
<PlaceholderPage
label="资源管理"
title="组件库"
description="统一管理大语言模型、语音识别、声音资源、知识库与工具插件。"
/>
);
}

View File

@@ -1,3 +1,11 @@
import { PlaceholderPage } from "./PlaceholderPage";
export function HistoryPage() {
return <div className="text-3xl font-bold"></div>;
}
return (
<PlaceholderPage
label="运行记录"
title="历史记录"
description="查看助手的对话历史、运行日志与调用明细。"
/>
);
}

View File

@@ -1,4 +1,4 @@
import { Boxes, Plus, Video } from "lucide-react";
import { Boxes, Plus } from "lucide-react";
import type { NavKey } from "@/components/layout/AppShell";
import { Button } from "@/components/ui/button";
@@ -8,47 +8,58 @@ type HomePageProps = {
export function HomePage({ onNavigate }: HomePageProps) {
return (
<div className="mx-auto flex w-full max-w-[1180px] flex-col gap-6 pt-[4vh]">
<section className="relative overflow-hidden rounded-3xl border border-[#1b2233] bg-[radial-gradient(circle_at_top_left,rgba(46,161,255,.18),transparent_36%),#0d121d] p-8">
<div className="mb-8 flex items-center gap-4">
<div className="relative flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl bg-gradient-to-br from-cyan-400 to-blue-500 text-white shadow-lg shadow-cyan-500/30">
<Video size={28} />
<div className="absolute inset-0 rounded-2xl ring-1 ring-inset ring-white/20" />
<div className="mx-auto flex w-full max-w-[1180px] flex-col gap-12 pt-[3vh]">
{/* Hero band — atmospheric gradient orbs behind editorial display copy */}
<section className="relative overflow-hidden rounded-3xl border border-hairline bg-canvas-soft px-10 py-16">
<div
aria-hidden
className="pointer-events-none absolute -right-24 -top-24 h-80 w-80 rounded-full opacity-60 blur-3xl"
style={{
backgroundImage:
"radial-gradient(circle, color-mix(in srgb, var(--gradient-sky) 55%, transparent), transparent 70%)",
}}
/>
<div
aria-hidden
className="pointer-events-none absolute -bottom-28 left-10 h-72 w-72 rounded-full opacity-50 blur-3xl"
style={{
backgroundImage:
"radial-gradient(circle, color-mix(in srgb, var(--gradient-lavender) 55%, transparent), transparent 70%)",
}}
/>
<div className="relative max-w-2xl">
<div className="caption-label text-muted-soft">AI · </div>
<h1 className="font-display display-xl mt-5 text-ink">
</h1>
<p className="mt-6 max-w-xl text-[16px] leading-7 text-body">
AI
</p>
<div className="mt-9 flex gap-3">
<Button
size="lg"
className="gap-2 px-5"
onClick={() => onNavigate("assistant-prompt")}
>
<Plus size={17} />
</Button>
<Button
variant="outline"
size="lg"
className="gap-2 border-hairline-strong text-foreground hover:bg-surface-strong"
onClick={() => onNavigate("components")}
>
<Boxes size={17} />
</Button>
</div>
<div>
<div className="text-sm font-semibold text-blue-400">
AI ·
</div>
<h1 className="mt-2 text-4xl font-bold tracking-tight text-[#e9eef7]">
</h1>
</div>
</div>
<p className="max-w-2xl text-[15px] leading-7 text-[#9aa6bd]">
AI
</p>
<div className="mt-8 flex gap-3">
<Button
size="lg"
className="rounded-xl gap-2"
onClick={() => onNavigate("assistant-prompt")}
>
<Plus size={17} />
</Button>
<Button
variant="outline"
size="lg"
className="rounded-xl gap-2 border-[#1b2233] bg-[#0f1521] text-[#9aa6bd] hover:bg-[#151e30] hover:text-[#e9eef7] hover:border-[#2a3550]"
onClick={() => onNavigate("components")}
>
<Boxes size={17} />
</Button>
</div>
</section>
</div>

View File

@@ -0,0 +1,53 @@
type PlaceholderPageProps = {
label: string;
title: string;
description: string;
};
export function PlaceholderPage({
label,
title,
description,
}: PlaceholderPageProps) {
return (
<div className="mx-auto flex w-full max-w-[1180px] flex-col gap-8">
<div>
<div className="caption-label text-muted-soft">{label}</div>
<h1 className="font-display display-lg mt-3 text-ink">{title}</h1>
<p className="mt-3 max-w-2xl text-[15px] leading-7 text-muted-foreground">
{description}
</p>
</div>
<section className="relative overflow-hidden rounded-3xl border border-hairline bg-canvas-soft px-10 py-20 text-center">
<div
aria-hidden
className="pointer-events-none absolute -right-24 top-0 h-72 w-72 rounded-full opacity-50 blur-3xl"
style={{
backgroundImage:
"radial-gradient(circle, color-mix(in srgb, var(--gradient-sky) 50%, transparent), transparent 70%)",
}}
/>
<div
aria-hidden
className="pointer-events-none absolute -left-20 bottom-0 h-64 w-64 rounded-full opacity-45 blur-3xl"
style={{
backgroundImage:
"radial-gradient(circle, color-mix(in srgb, var(--gradient-lavender) 50%, transparent), transparent 70%)",
}}
/>
<div className="relative">
<div className="caption-label inline-flex rounded-full bg-surface-strong px-3 py-1 text-muted-foreground">
</div>
<p className="font-display display-sm mx-auto mt-5 max-w-md text-ink">
</p>
<p className="mx-auto mt-3 max-w-md text-sm leading-7 text-body">
</p>
</div>
</section>
</div>
);
}

View File

@@ -1,3 +1,11 @@
import { PlaceholderPage } from "./PlaceholderPage";
export function ProfilePage() {
return <div className="text-3xl font-bold"></div>;
}
return (
<PlaceholderPage
label="账户设置"
title="个人中心"
description="管理账户信息、团队成员、密钥与偏好设置。"
/>
);
}

View File

@@ -1,3 +1,11 @@
import { PlaceholderPage } from "./PlaceholderPage";
export function TestPage() {
return <div className="text-3xl font-bold"></div>;
}
return (
<PlaceholderPage
label="实时调试"
title="测试助手"
description="在发布前通过实时视频对话测试助手的表现与交互体验。"
/>
);
}

View File

@@ -1,3 +1,11 @@
import { PlaceholderPage } from "./PlaceholderPage";
export function WorkflowPage() {
return <div className="text-3xl font-bold"></div>;
}
return (
<PlaceholderPage
label="流程编排"
title="工作流"
description="管理与编排可复用的助手工作流,支持多轮任务与工具调用。"
/>
);
}

View File

@@ -5,7 +5,7 @@ import { Slot } from "radix-ui"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"group/button inline-flex shrink-0 items-center justify-center rounded-4xl border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/30 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"group/button inline-flex shrink-0 items-center justify-center rounded-full border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/30 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {