feat: refresh frontend visual design

This commit is contained in:
2026-04-23 11:00:29 +08:00
parent 562c95c4b5
commit 3dd5c454cb
11 changed files with 1262 additions and 336 deletions

View File

@@ -355,10 +355,9 @@ export default function CreateTaskPage() {
<div className="workbench-header">
<div>
<span className="header-kicker">AI Video Studio</span>
<h3></h3>
<h3></h3>
<p className="muted">
URL
</p>
</div>
<div className="row">
@@ -376,6 +375,27 @@ export default function CreateTaskPage() {
</div>
</div>
<div className="studio-command-bar">
<div className="studio-command-pill">
<strong>{selectedModel?.frontendTitle ?? "等待加载"}</strong>
</div>
<div className="studio-command-pill">
<strong>
{activeDuration}s · {activeResolution} · {activeRatio}
</strong>
</div>
<div className="studio-command-pill">
<strong>{estimatedPoints} </strong>
</div>
<div className="studio-command-pill">
<strong>{selectedAssetCount} </strong>
</div>
</div>
<div className="studio-summary-grid">
<article className="studio-stat">
<span></span>
@@ -644,7 +664,7 @@ export default function CreateTaskPage() {
<div>
<span className="header-kicker">Asset Dock</span>
<h3></h3>
<p className="muted"></p>
<p className="muted"></p>
</div>
<div className="workspace-tabs">
<span className="tab-chip active"></span>

View File

@@ -15,6 +15,13 @@ export default function TasksPage() {
refetchInterval: 4_000,
});
const tasks = tasksQuery.data ?? [];
const runningCount = tasks.filter((task) =>
["queued", "submitted", "running"].includes(task.taskStatus),
).length;
const succeededCount = tasks.filter((task) => task.taskStatus === "succeeded").length;
const failedCount = tasks.filter((task) => task.taskStatus === "failed").length;
return (
<section className="panel">
<div className="toolbar">
@@ -27,13 +34,36 @@ export default function TasksPage() {
</Link>
</div>
<div className="task-stat-band">
<article className="task-stat-card">
<span></span>
<strong>{tasks.length}</strong>
<small></small>
</article>
<article className="task-stat-card">
<span></span>
<strong>{runningCount}</strong>
<small></small>
</article>
<article className="task-stat-card">
<span></span>
<strong>{succeededCount}</strong>
<small></small>
</article>
<article className="task-stat-card">
<span>/</span>
<strong>{failedCount}</strong>
<small></small>
</article>
</div>
<div className="list-grid" style={{ marginTop: 18 }}>
{tasksQuery.data?.map((task) => (
<div className="list-item" key={task.taskNo}>
{tasks.map((task) => (
<article className="list-item task-stream-card" key={task.taskNo}>
<div className="toolbar">
<div>
<strong>{task.taskNo}</strong>
<div className="muted">
<div className="muted task-stream-meta">
{dayjs(task.createdAt).format("YYYY-MM-DD HH:mm:ss")} · {task.durationSeconds}
s
</div>
@@ -59,15 +89,19 @@ export default function TasksPage() {
</Link>
{task.resultVideoUrl ? (
<a className="primary-button" href={task.resultVideoUrl} target="_blank">
<a
className="primary-button"
href={task.resultVideoUrl}
rel="noreferrer"
target="_blank"
>
</a>
) : null}
</div>
</div>
</article>
))}
</div>
</section>
);
}

View File

@@ -17,26 +17,118 @@ export default function DashboardPage() {
});
const data = dashboardQuery.data;
const summaryCards = [
{
label: "用户总数",
value: data?.users ?? 0,
copy: "平台已创建账户数",
},
{
label: "已支付订单",
value: data?.paidOrders ?? 0,
copy: "已完成支付的充值订单",
},
{
label: "任务总数",
value: data?.tasks ?? 0,
copy: "全链路视频任务累计数量",
},
{
label: "成功率",
value: `${data?.successRate ?? 0}%`,
copy: "基于任务完成状态的成功占比",
},
];
return (
<section className="stats-grid">
<article className="stat-card">
<h3></h3>
<div className="value">{data?.users ?? 0}</div>
</article>
<article className="stat-card">
<h3></h3>
<div className="value">{data?.paidOrders ?? 0}</div>
</article>
<article className="stat-card">
<h3></h3>
<div className="value">{data?.tasks ?? 0}</div>
</article>
<article className="stat-card">
<h3></h3>
<div className="value">{data?.successRate ?? 0}%</div>
</article>
</section>
<div className="admin-dashboard-stack">
<section className="panel dashboard-stage">
<div className="dashboard-stage-copy">
<span className="header-kicker">Control overview</span>
<h3></h3>
<p className="muted">
MVP
</p>
</div>
<div className="dashboard-stage-grid">
<div className="dashboard-mini-card">
<span>线</span>
<strong> + + </strong>
<small></small>
</div>
<div className="dashboard-mini-card">
<span>线</span>
<strong> -&gt; </strong>
<small></small>
</div>
<div className="dashboard-mini-card">
<span>线</span>
<strong> mock / </strong>
<small></small>
</div>
</div>
</section>
<section className="stats-grid">
{summaryCards.map((card) => (
<article className="stat-card" key={card.label}>
<h3>{card.label}</h3>
<div className="value">{card.value}</div>
<p className="muted">{card.copy}</p>
</article>
))}
</section>
<section className="two-col-grid">
<article className="panel">
<div className="toolbar">
<div>
<h3></h3>
<p className="muted"></p>
</div>
</div>
<div className="ops-feed">
<div className="ops-feed-item">
<strong>1. </strong>
<span></span>
</div>
<div className="ops-feed-item">
<strong>2. </strong>
<span></span>
</div>
<div className="ops-feed-item">
<strong>3. </strong>
<span></span>
</div>
</div>
</article>
<article className="panel">
<div className="toolbar">
<div>
<h3></h3>
<p className="muted"></p>
</div>
</div>
<div className="dashboard-stage-grid">
<div className="dashboard-mini-card">
<span></span>
<strong> / / </strong>
<small></small>
</div>
<div className="dashboard-mini-card">
<span></span>
<strong> / / </strong>
<small></small>
</div>
<div className="dashboard-mini-card">
<span></span>
<strong> / / </strong>
<small></small>
</div>
</div>
</article>
</section>
</div>
);
}

View File

@@ -1,6 +1,8 @@
"use client";
import { startTransition, useState } from "react";
import { ArrowRight, Blocks, Coins, Settings2, ShieldCheck } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { api, ApiError } from "@/lib/api";
@@ -29,23 +31,58 @@ export default function AdminLoginPage() {
}
return (
<div className="login-grid">
<section style={{ padding: 48, display: "flex", flexDirection: "column", justifyContent: "space-between" }}>
<div>
<div className="login-grid auth-grid-admin">
<section className="auth-hero auth-hero-admin">
<div className="auth-copy">
<div className="brand-kicker">Ops Console</div>
<h1 style={{ fontSize: 64, lineHeight: 0.95, margin: "12px 0", fontFamily: "var(--font-display)" }}>
</h1>
<p className="muted" style={{ maxWidth: 580 }}>
MVP
<h1 className="headline"></h1>
<p className="subcopy">
`Linear/Vercel`
</p>
<div className="auth-chip-row">
<span className="auth-chip">Provider routing</span>
<span className="auth-chip">Pricing control</span>
<span className="auth-chip">Audit trail</span>
</div>
</div>
<div className="showcase-stack">
<div className="showcase-card showcase-card-hero">
<div className="toolbar">
<span className="status-badge tone-success">Admin access</span>
<span className="mini-note">Operational overview</span>
</div>
<h3></h3>
<p className="muted">
MVP
</p>
</div>
<div className="showcase-card-grid">
<article className="showcase-card">
<Blocks size={18} />
<strong></strong>
<span></span>
</article>
<article className="showcase-card">
<Coins size={18} />
<strong></strong>
<span></span>
</article>
<article className="showcase-card">
<Settings2 size={18} />
<strong></strong>
<span>便</span>
</article>
</div>
</div>
</section>
<section className="fullscreen-shell">
<form className="auth-card" onSubmit={handleSubmit}>
<div className="brand-kicker">AIVideo Admin</div>
<h3></h3>
<form className="auth-card auth-card-strong" onSubmit={handleSubmit}>
<div className="auth-card-head">
<div className="brand-kicker">AIVideo Admin</div>
<h3></h3>
<p className="muted"></p>
</div>
<div className="form-stack">
<label className="field-label">
@@ -66,14 +103,23 @@ export default function AdminLoginPage() {
}
/>
</label>
{error ? <div className="muted" style={{ color: "var(--danger)" }}>{error}</div> : null}
{error ? <div className="inline-feedback is-error">{error}</div> : null}
<button className="primary-button" type="submit">
{loading ? "登录中..." : "进入后台"}
{loading ? null : <ArrowRight size={16} />}
</button>
<div className="auth-action-row">
<Link href="/login" className="ghost-button">
</Link>
<div className="field-note">
<ShieldCheck size={14} />
</div>
</div>
</div>
</form>
</section>
</div>
);
}

View File

@@ -0,0 +1,17 @@
import { IBM_Plex_Mono, Manrope, Syne } from "next/font/google";
export const displayFont = Syne({
subsets: ["latin"],
variable: "--font-display",
});
export const bodyFont = Manrope({
subsets: ["latin"],
variable: "--font-body",
});
export const monoFont = IBM_Plex_Mono({
subsets: ["latin"],
variable: "--font-mono",
weight: ["400", "500", "600"],
});

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,13 @@
import type { Metadata } from "next";
import { bodyFont, displayFont, monoFont } from "@/app/fonts";
import { Providers } from "@/components/providers";
import "./globals.css";
export const metadata: Metadata = {
title: "AIVideo",
description: "AI 视频生成平台用户前台",
description: "AIVideo AI 视频创作与运营控制台",
};
export default function RootLayout({
@@ -15,7 +16,10 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html lang="zh-CN">
<html
lang="zh-CN"
className={`${displayFont.variable} ${bodyFont.variable} ${monoFont.variable}`}
>
<body className="app-fonts">
<Providers>{children}</Providers>
</body>

View File

@@ -1,6 +1,7 @@
"use client";
import { startTransition, useState } from "react";
import { ArrowRight, Clapperboard, Coins, FolderKanban, Sparkles } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/navigation";
@@ -28,27 +29,57 @@ export default function LoginPage() {
return (
<div className="auth-grid">
<section className="auth-hero">
<div>
<section className="auth-hero auth-hero-studio">
<div className="auth-copy">
<div className="eyebrow">Creative Workbench</div>
<h1 className="headline"> AI 线</h1>
<h1 className="headline"> AI 线</h1>
<p className="subcopy">
mock
OpenAI / Seedance
`RunwayML`
</p>
<div className="auth-chip-row">
<span className="auth-chip">Prompt to video</span>
<span className="auth-chip">Asset-aware workflow</span>
<span className="auth-chip">Realtime task polling</span>
</div>
</div>
<div className="panel">
<h3></h3>
<p className="muted">
</p>
<div className="showcase-stack">
<div className="showcase-card showcase-card-hero">
<div className="toolbar">
<span className="status-badge tone-success">Studio ready</span>
<span className="mini-note">Source mode</span>
</div>
<h3></h3>
<p className="muted">
mock SQLite
</p>
</div>
<div className="showcase-card-grid">
<article className="showcase-card">
<Clapperboard size={18} />
<strong></strong>
<span></span>
</article>
<article className="showcase-card">
<FolderKanban size={18} />
<strong></strong>
<span></span>
</article>
<article className="showcase-card">
<Coins size={18} />
<strong></strong>
<span></span>
</article>
</div>
</div>
</section>
<section className="fullscreen-shell">
<form className="auth-card" onSubmit={handleSubmit}>
<div className="eyebrow">Welcome Back</div>
<h2 style={{ marginTop: 8 }}> AIVideo</h2>
<form className="auth-card auth-card-strong" onSubmit={handleSubmit}>
<div className="auth-card-head">
<div className="eyebrow">Welcome Back</div>
<h2> AIVideo</h2>
<p className="muted"></p>
</div>
<div className="form-stack">
<label className="field-label">
@@ -69,17 +100,23 @@ export default function LoginPage() {
}
/>
</label>
{error ? <div className="muted" style={{ color: "var(--danger)" }}>{error}</div> : null}
{error ? <div className="inline-feedback is-error">{error}</div> : null}
<button className="primary-button" disabled={loading} type="submit">
{loading ? "登录中..." : "登录并进入工作台"}
{loading ? null : <ArrowRight size={16} />}
</button>
<Link href="/register" className="ghost-button">
</Link>
<div className="auth-action-row">
<Link href="/register" className="ghost-button">
</Link>
<div className="field-note">
<Sparkles size={14} />
</div>
</div>
</div>
</form>
</section>
</div>
);
}

View File

@@ -16,25 +16,25 @@ import {
} from "lucide-react";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import { useEffect } from "react";
import { useEffect, useMemo } from "react";
import { api } from "@/lib/api";
const navigation = [
{ href: "/admin/dashboard", label: "仪表盘", icon: ChartColumnBig },
{ href: "/admin/users", label: "用户管理", icon: Users },
{ href: "/admin/recharge-orders", label: "充值订单", icon: Coins },
{ href: "/admin/redeem-codes", label: "兑换密钥", icon: KeySquare },
{ href: "/admin/growth-rules", label: "增长奖励", icon: Link2 },
{ href: "/admin/invite-relations", label: "邀请关系", icon: Link2 },
{ href: "/admin/provider-accounts", label: "供应商账号", icon: Workflow },
{ href: "/admin/provider-models", label: "供应商模型", icon: Blocks },
{ href: "/admin/video-models", label: "平台模型", icon: Package2 },
{ href: "/admin/video-model-bindings", label: "模型绑定", icon: Workflow },
{ href: "/admin/pricing-rules", label: "价格规则", icon: Coins },
{ href: "/admin/video-tasks", label: "视频任务", icon: Workflow },
{ href: "/admin/callback-logs", label: "回调日志", icon: ChartColumnBig },
{ href: "/admin/system-config", label: "系统配置", icon: Settings2 },
{ href: "/admin/dashboard", label: "仪表盘", description: "查看系统总览、支付与任务健康度", icon: ChartColumnBig },
{ href: "/admin/users", label: "用户管理", description: "维护用户状态、余额与基本资料", icon: Users },
{ href: "/admin/recharge-orders", label: "充值订单", description: "跟踪充值流转与支付状态", icon: Coins },
{ href: "/admin/redeem-codes", label: "兑换密钥", description: "批量生成、停用与审计兑换码", icon: KeySquare },
{ href: "/admin/growth-rules", label: "增长奖励", description: "配置注册奖励与邀请返利规则", icon: Link2 },
{ href: "/admin/invite-relations", label: "邀请关系", description: "查看邀请码链路与归因结果", icon: Link2 },
{ href: "/admin/provider-accounts", label: "供应商账号", description: "管理各供应商 baseUrl 与鉴权信息", icon: Workflow },
{ href: "/admin/provider-models", label: "供应商模型", description: "维护供应商模型能力与协议", icon: Blocks },
{ href: "/admin/video-models", label: "平台模型", description: "定义前台可见的统一模型层", icon: Package2 },
{ href: "/admin/video-model-bindings", label: "模型绑定", description: "决定平台模型到供应商模型的路由", icon: Workflow },
{ href: "/admin/pricing-rules", label: "价格规则", description: "按模型与版本维护积分定价", icon: Coins },
{ href: "/admin/video-tasks", label: "视频任务", description: "处理异常任务与追踪回调结果", icon: Workflow },
{ href: "/admin/callback-logs", label: "回调日志", description: "查看供应商回调负载与重放线索", icon: ChartColumnBig },
{ href: "/admin/system-config", label: "系统配置", description: "维护站点公告、策略与公开配置", icon: Settings2 },
];
export function AdminShell({ children }: { children: React.ReactNode }) {
@@ -51,6 +51,14 @@ export function AdminShell({ children }: { children: React.ReactNode }) {
}
}, [meQuery.error, router]);
const current = useMemo(
() =>
[...navigation]
.sort((left, right) => right.href.length - left.href.length)
.find((item) => pathname.startsWith(item.href)) ?? navigation[0],
[pathname],
);
if (meQuery.isLoading || !meQuery.data) {
return (
<div className="fullscreen-shell">
@@ -60,16 +68,43 @@ export function AdminShell({ children }: { children: React.ReactNode }) {
}
return (
<div className="shell-grid">
<div className="shell-grid shell-grid-admin">
<aside className="shell-sidebar">
<div className="brand-block">
<div className="brand-block brand-block-admin">
<div className="brand-visual" aria-hidden="true">
<span className="brand-orb brand-orb-primary" />
<span className="brand-orb brand-orb-secondary" />
</div>
<span className="brand-kicker">AIVIDEO ADMIN</span>
<h1></h1>
<p></p>
<p></p>
<div className="brand-pill-row">
<span className="brand-pill">Ops control</span>
<span className="brand-pill">Pricing</span>
<span className="brand-pill">Provider routing</span>
</div>
</div>
<div className="sidebar-note">
<div className="sidebar-note-label">Operations Grid</div>
<div className="sidebar-metrics">
<div className="sidebar-metric">
<strong>Models</strong>
<span></span>
</div>
<div className="sidebar-metric">
<strong>Economy</strong>
<span></span>
</div>
<div className="sidebar-metric">
<strong>Audit</strong>
<span></span>
</div>
</div>
</div>
<nav className="sidebar-nav">
{navigation.map((item) => {
{navigation.map((item, index) => {
const Icon = item.icon;
return (
<Link
@@ -80,14 +115,23 @@ export function AdminShell({ children }: { children: React.ReactNode }) {
})}
>
<Icon size={18} />
<span>{item.label}</span>
<div>
<span>{item.label}</span>
<small>{item.description}</small>
</div>
<div className="nav-index">{String(index + 1).padStart(2, "0")}</div>
</Link>
);
})}
</nav>
<div className="profile-card">
<div>
<div className="profile-card-main">
<div className="profile-avatar">
{String((meQuery.data as { nickname: string }).nickname ?? "AD")
.slice(0, 2)
.toUpperCase()}
</div>
<div className="profile-name">
{(meQuery.data as { nickname: string }).nickname}
</div>
@@ -95,6 +139,16 @@ export function AdminShell({ children }: { children: React.ReactNode }) {
{(meQuery.data as { username: string }).username}
</div>
</div>
<div className="profile-card-stats">
<div className="profile-stat">
<span></span>
<strong>Admin</strong>
</div>
<div className="profile-stat">
<span></span>
<strong>Control</strong>
</div>
</div>
<button
className="ghost-button"
onClick={async () => {
@@ -109,9 +163,19 @@ export function AdminShell({ children }: { children: React.ReactNode }) {
</aside>
<main className="shell-main">
<header className="shell-header">
<div className="shell-header-content">
<div className="header-kicker">Operations Console</div>
<h2>{current.label}</h2>
<p className="shell-header-copy">{current.description}</p>
</div>
<div className="shell-header-meta">
<div className="header-chip header-chip-success"></div>
<div className="header-chip"> / / </div>
</div>
</header>
<section className="shell-content">{children}</section>
</main>
</div>
);
}

View File

@@ -1,6 +1,7 @@
"use client";
import { startTransition, useState } from "react";
import { ArrowRight, Coins, Gift, Link2, Sparkles } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/navigation";
@@ -36,24 +37,57 @@ export function RegisterForm({ inviteCode = "" }: { inviteCode?: string }) {
return (
<div className="auth-grid">
<section className="auth-hero">
<div>
<section className="auth-hero auth-hero-studio">
<div className="auth-copy">
<div className="eyebrow">New Account</div>
<h1 className="headline"></h1>
<h1 className="headline"></h1>
<p className="subcopy">
</p>
<div className="auth-chip-row">
<span className="auth-chip">Signup rewards</span>
<span className="auth-chip">Invite tracking</span>
<span className="auth-chip">Wallet ready</span>
</div>
</div>
<div className="panel">
<h3></h3>
<p className="muted"></p>
<div className="showcase-stack">
<div className="showcase-card showcase-card-hero">
<div className="toolbar">
<span className="status-badge tone-soft">Growth loop</span>
<span className="mini-note">Reward rules active</span>
</div>
<h3></h3>
<p className="muted">
</p>
</div>
<div className="showcase-card-grid">
<article className="showcase-card">
<Gift size={18} />
<strong></strong>
<span></span>
</article>
<article className="showcase-card">
<Link2 size={18} />
<strong></strong>
<span></span>
</article>
<article className="showcase-card">
<Coins size={18} />
<strong></strong>
<span></span>
</article>
</div>
</div>
</section>
<section className="fullscreen-shell">
<form className="auth-card" onSubmit={handleSubmit}>
<div className="eyebrow">Create Account</div>
<h2 style={{ marginTop: 8 }}></h2>
<form className="auth-card auth-card-strong" onSubmit={handleSubmit}>
<div className="auth-card-head">
<div className="eyebrow">Create Account</div>
<h2></h2>
<p className="muted"></p>
</div>
<div className="form-stack">
<label className="field-label">
@@ -84,17 +118,23 @@ export function RegisterForm({ inviteCode = "" }: { inviteCode?: string }) {
}
/>
</label>
{error ? <div className="muted" style={{ color: "var(--danger)" }}>{error}</div> : null}
{error ? <div className="inline-feedback is-error">{error}</div> : null}
<button className="primary-button" disabled={loading} type="submit">
{loading ? "注册中..." : "注册并领取初始积分"}
{loading ? null : <ArrowRight size={16} />}
</button>
<Link href="/login" className="ghost-button">
</Link>
<div className="auth-action-row">
<Link href="/login" className="ghost-button">
</Link>
<div className="field-note">
<Sparkles size={14} />
</div>
</div>
</div>
</form>
</section>
</div>
);
}

View File

@@ -105,20 +105,47 @@ export function SiteShell({ children }: { children: React.ReactNode }) {
}
return (
<div className="shell-grid">
<div className="shell-grid shell-grid-studio">
<aside className="shell-sidebar">
<div className="brand-block">
<div className="brand-block brand-block-studio">
<div className="brand-visual" aria-hidden="true">
<span className="brand-orb brand-orb-primary" />
<span className="brand-orb brand-orb-secondary" />
</div>
<span className="brand-kicker">AI VIDEO STUDIO</span>
<h1>AIVideo</h1>
<p></p>
<p></p>
<div className="brand-pill-row">
<span className="brand-pill">Source mode</span>
<span className="brand-pill">Mock providers</span>
<span className="brand-pill">Local assets</span>
</div>
<div className="brand-status">
<span className="status-dot" />
线
</div>
</div>
<div className="sidebar-note">
<div className="sidebar-note-label">Pipeline Focus</div>
<div className="sidebar-metrics">
<div className="sidebar-metric">
<strong>Prompt</strong>
<span></span>
</div>
<div className="sidebar-metric">
<strong>Assets</strong>
<span></span>
</div>
<div className="sidebar-metric">
<strong>Tasks</strong>
<span></span>
</div>
</div>
</div>
<nav className="sidebar-nav">
{navigation.map((item) => {
{navigation.map((item, index) => {
const Icon = item.icon;
return (
<Link
@@ -133,6 +160,7 @@ export function SiteShell({ children }: { children: React.ReactNode }) {
<span>{item.label}</span>
<small>{item.description}</small>
</div>
<div className="nav-index">{String(index + 1).padStart(2, "0")}</div>
</Link>
);
})}
@@ -148,6 +176,16 @@ export function SiteShell({ children }: { children: React.ReactNode }) {
<div className="profile-name">{data.nickname || data.username}</div>
<div className="profile-meta">{data.email}</div>
</div>
<div className="profile-card-stats">
<div className="profile-stat">
<span></span>
<strong>Creator</strong>
</div>
<div className="profile-stat">
<span>ID</span>
<strong>{(data.publicId || "AIVIDEO").slice(-8)}</strong>
</div>
</div>
<button
className="ghost-button"
onClick={async () => {
@@ -163,7 +201,7 @@ export function SiteShell({ children }: { children: React.ReactNode }) {
<main className="shell-main">
<header className="shell-header">
<div>
<div className="shell-header-content">
<div className="header-kicker">Creative Ops Console</div>
<h2>{title.label}</h2>
<p className="shell-header-copy">{title.description}</p>
@@ -173,7 +211,8 @@ export function SiteShell({ children }: { children: React.ReactNode }) {
<Activity size={14} />
</div>
<div className="header-chip">{data.publicId}</div>
<div className="header-chip">{data.publicId || "UNAVAILABLE"}</div>
<div className="header-chip"> / / mock </div>
</div>
</header>
<section className="shell-content">{children}</section>