Expand LibTV starter interactions
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-04-18 14:39:21 +08:00
parent 4551eee9d1
commit db657b3ea5

View File

@ -53,7 +53,7 @@ type ActivePanel =
| "account"
| null;
type ActiveStarterScene = "character-three-view" | null;
type ActiveStarterScene = "story-script" | "character-three-view" | "first-frame" | "audio-to-video" | null;
type SceneToolbarMenu = "panorama" | "orbit" | "lighting" | "grid" | "hd" | "crop" | null;
type ComposerMenu =
| "style"
@ -212,6 +212,45 @@ const CHARACTER_IMAGE_URL =
"https://liblibai-online.liblib.cloud/web/ea95cb51f3d94addbb92dc003cd33b49/cca9e085-dccc-4f72-aef7-d68dc6688bcf.png";
const CHARACTER_THREE_VIEW_URL =
"https://liblibai-online.liblib.cloud/web/ea95cb51f3d94addbb92dc003cd33b49/7a32ba0b-7b5b-46b1-a86c-e0904482adf7.png";
const FIRST_FRAME_IMAGE_URL =
"https://liblibai-online.liblib.cloud/web/28aa80df5944499487381983a8f9bbdd/da241fab-3569-49aa-ae4e-c20de9f718e0.png";
const AUDIO_SOURCE_URL =
"https://liblibai-online.liblib.cloud/web/28aa80df5944499487381983a8f9bbdd/3ca0886d-d35f-4048-95bf-cc655e9c57fd.mp3";
const AUDIO_REFERENCE_IMAGE_URL =
"https://liblibai-online.liblib.cloud/web/ea95cb51f3d94addbb92dc003cd33b49/1342d344-2d95-41a1-a2aa-4d20869a318c.png";
const STORY_SCRIPT_PROMPT = "根据我上传的剧本生成一个完整的故事脚本";
const AUDIO_TO_VIDEO_PROMPT =
"根据上传的音频生成对应场景画面,镜头语言、节奏、音乐匹配情绪变化,电影级质感。";
const STORY_SCRIPT_TEXT = `《我在盛唐写天下》
类型:古风 / 穿越 / 爽文漫剧
时长建议:60-90秒
基调:热血 x 盛唐史诗感 x 爽点节奏
【序幕】
【现代 · 深夜办公室】
键盘声急促。电脑屏幕蓝光刺眼。沈昭昭(女,28岁),面色苍白,伏案加班。手机提示音不断。她抬头,视线模糊,黑屏。
【第一幕】
【盛唐 · 金銮殿 · 日】
鼓声震天。沈昭昭猛然睁眼,已跪在大殿中央,身着男装官服,双手被押。百官列立,殿宇巍峨。
大臣厉声:“大逆不道!假称诗才惊世,欺君罔上,当斩!”
沈昭昭震惊低语:“这是……唐朝?”
龙椅之上,皇帝目光沉沉。
皇帝:“既言才华盖世,当殿作诗。若不能,斩。”
刀光映入她瞳孔。大殿死寂。
【第二幕】
沈昭昭缓缓起身。呼吸渐稳。她望向殿外,长安城阳光万里。
她低声:“既来盛唐……便与盛唐争一争。”
抬头,高声吟诵:“君不见黄河之水天上来。”
声音回荡大殿。群臣震动。皇帝缓缓站起。诗声激昂,如惊雷落殿。
【第三幕】
殿外风起。长安街市灯火连绵。
皇帝低声:“此诗,当传万世。”
沈昭昭目光坚定。她轻声自语:“既然来了……那便写尽三万里长安。”
镜头推远。盛唐山河展开。`;
function normalizeCanvasTitle(title?: string | null) {
const value = String(title || "").trim();
@ -610,12 +649,19 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
const createStarter = useCallback(
(starter: (typeof STARTER_CARDS)[number]) => {
if (starter.title === "角色三视图") {
const starterScenes: Record<string, Exclude<ActiveStarterScene, null>> = {
故事脚本生成: "story-script",
角色三视图: "character-three-view",
首帧图生视频: "first-frame",
音频生视频: "audio-to-video",
};
const starterScene = starterScenes[starter.title];
if (starterScene) {
setNodes([]);
setEdges([]);
setSelectedNodeId(null);
setActivePanel(null);
setActiveStarterScene("character-three-view");
setActiveStarterScene(starterScene);
return;
}
@ -752,7 +798,10 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
))}
</div>
{activeStarterScene === "story-script" ? <StoryScriptScene /> : null}
{activeStarterScene === "character-three-view" ? <CharacterThreeViewScene /> : null}
{activeStarterScene === "first-frame" ? <FirstFrameVideoScene /> : null}
{activeStarterScene === "audio-to-video" ? <AudioToVideoScene /> : null}
{nodes.length === 0 && !activePanel && !activeStarterScene ? (
<div className="-translate-x-1/2 -translate-y-1/2 absolute left-1/2 top-[51%] z-20 w-[900px] max-w-[calc(100vw-180px)]">
@ -855,7 +904,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
{activePanel === "assets" ? (
<LibModal onClose={() => setActivePanel(null)} title="素材库">
<EmptyState icon={Layers} title="暂无素材" description="素材库、上传和项目素材接口待接入。" />
<AssetLibraryPanel />
</LibModal>
) : null}
@ -905,26 +954,13 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
{activePanel === "support" ? (
<LibModal onClose={() => setActivePanel(null)} title="支持中心">
<EmptyState icon={Headphones} title="联系客服" description="工单、客服和帮助文档入口待接入。" />
<SupportPanel />
</LibModal>
) : null}
{activePanel === "skills" ? (
<LibModal onClose={() => setActivePanel(null)} title="LibTV Skills">
<div className="space-y-4">
<EmptyState icon={Sparkles} title="OpenAPI Access Key" description="Access Key 管理弹窗和权限接口待接入。" />
<div className="grid grid-cols-5 gap-2">
{Object.entries(capturedCounts.byType || {}).map(([type, count]) => {
const meta = TOOL_TYPE_META[type] || TOOL_TYPE_META.video;
return (
<div className="rounded-[8px] bg-white/6 p-3 text-center" key={type}>
<div className="text-[18px] text-white">{String(count)}</div>
<div className="mt-1 text-[11px] text-white/42">{meta.label}</div>
</div>
);
})}
</div>
</div>
<SkillsAccessPanel />
</LibModal>
) : null}
@ -966,19 +1002,19 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
{activePanel === "member" ? (
<LibModal onClose={() => setActivePanel(null)} title="会员中心">
<EmptyState icon={Crown} title="会员权益" description="会员超市、特惠活动、积分和支付接口待接入。" />
<MemberCenterPanel />
</LibModal>
) : null}
{activePanel === "notifications" ? (
<LibModal onClose={() => setActivePanel(null)} title="通知">
<EmptyState icon={Bell} title="暂无通知" description="生成任务、审核和系统消息待接入。" />
<NotificationsPanel />
</LibModal>
) : null}
{activePanel === "account" ? (
<LibModal onClose={() => setActivePanel(null)} title="账号">
<EmptyState icon={UserRound} title="当前账号" description="账号中心、额度和设置入口待接入。" />
<AccountPanel />
</LibModal>
) : null}
@ -1362,6 +1398,328 @@ function MagnetGlyph() {
);
}
function StoryScriptScene() {
return (
<div className="pointer-events-none absolute inset-0 z-30" data-canvas-overlay="true">
<PresetFloatingToolbar
items={[
["拆分场景", "按镜号拆分脚本接口待接入。"],
["生成分镜", "分镜生成接口待接入。"],
["表格视图", "脚本表格视图接口待接入。"],
["导出", "脚本导出接口待接入。"],
]}
/>
<div className="pointer-events-auto absolute left-1/2 top-[196px] flex -translate-x-1/2 items-start gap-[112px]">
<StoryTextPanel />
<SceneConnector className="mt-[170px]" />
<ScriptGeneratorPanel />
</div>
<PresetComposerPanel
credit="8"
prompt={STORY_SCRIPT_PROMPT}
submitMessage="故事脚本生成接口待接入。"
title="脚本生成器"
/>
</div>
);
}
function FirstFrameVideoScene() {
return (
<div className="pointer-events-none absolute inset-0 z-30" data-canvas-overlay="true">
<PresetFloatingToolbar
items={[
["高清", "首帧高清接口待接入。"],
["扩图", "首帧扩图接口待接入。"],
["替换背景", "背景替换接口待接入。"],
["下载", "图片下载接口待接入。"],
]}
/>
<div className="pointer-events-auto absolute left-1/2 top-[190px] flex -translate-x-1/2 items-start gap-[108px]">
<PresetMediaPanel imageUrl={FIRST_FRAME_IMAGE_URL} label="首帧" wide />
<SceneConnector className="mt-[178px]" />
<VideoResultPanel label="视频" />
</div>
<PresetComposerPanel
credit="28"
prompt="以当前图为首帧生成视频。"
submitMessage="首帧图生视频接口待接入。"
thumbnailUrl={FIRST_FRAME_IMAGE_URL}
title="首帧图生视频"
/>
</div>
);
}
function AudioToVideoScene() {
return (
<div className="pointer-events-none absolute inset-0 z-30" data-canvas-overlay="true">
<PresetFloatingToolbar
items={[
["节奏匹配", "音频节奏解析接口待接入。"],
["镜头语言", "镜头语言配置接口待接入。"],
["生成音效", "音效生成接口待接入。"],
["下载", "素材下载接口待接入。"],
]}
/>
<div className="pointer-events-auto absolute left-1/2 top-[178px] flex -translate-x-1/2 items-start gap-[98px]">
<div className="flex flex-col gap-5">
<AudioResourcePanel />
<PresetMediaPanel imageUrl={AUDIO_REFERENCE_IMAGE_URL} label="图片" small />
</div>
<SceneConnector className="mt-[212px]" />
<VideoResultPanel label="视频" />
</div>
<PresetComposerPanel
credit="32"
prompt={AUDIO_TO_VIDEO_PROMPT}
submitMessage="音频生视频接口待接入。"
thumbnailUrl={AUDIO_REFERENCE_IMAGE_URL}
title="音频生视频"
/>
</div>
);
}
function PresetFloatingToolbar({ items }: { items: Array<[string, string]> }) {
return (
<div className="pointer-events-auto absolute left-1/2 top-[94px] flex h-[58px] -translate-x-1/2 items-center gap-2 rounded-[16px] border border-white/8 bg-[#262626]/96 px-4 text-white/78 shadow-[0_20px_64px_rgba(0,0,0,0.42)] backdrop-blur-[10px]">
{items.map(([label, message]) => (
<button
className="flex h-10 items-center gap-2 rounded-[10px] px-3 text-[15px] hover:bg-white/8 hover:text-white"
key={label}
onClick={() => toast.info(message)}
type="button"
>
<span className="flex h-5 w-5 items-center justify-center rounded-[5px] border border-white/18 text-[11px] text-white/58">
{label.slice(0, 1)}
</span>
{label}
</button>
))}
</div>
);
}
function SceneConnector({ className }: { className?: string }) {
return (
<div className={cn("relative h-7 w-[108px]", className)}>
<div className="absolute left-0 top-1/2 h-[2px] w-full -translate-y-1/2 bg-gradient-to-r from-[#5aa8ff] via-[#7a86ff] to-[#b6bcff]" />
<button
className="absolute left-1/2 top-1/2 flex h-7 w-7 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full border border-white/45 bg-[#222] text-[22px] leading-none text-white/72"
onClick={() => toast.info("连接素材节点接口待接入。")}
type="button"
>
+
</button>
</div>
);
}
function StoryTextPanel() {
return (
<div className="w-[560px]">
<div className="mb-2 flex items-center gap-1 text-[20px] font-semibold text-white/50">
<FileText className="h-5 w-5" />
剧本
</div>
<div className="h-[390px] overflow-hidden rounded-[10px] border border-white/12 bg-[#f4f5f7] text-[#1f2329] shadow-[0_18px_60px_rgba(0,0,0,0.36)]">
<div className="flex h-11 items-center justify-between border-[#dfe2e8] border-b px-4 text-[13px] text-[#697080]">
<span>输入内容</span>
<button className="rounded-[8px] px-2 py-1 text-[#4f5cff] hover:bg-black/5" onClick={() => toast.info("编辑剧本接口待接入。")} type="button">
编辑
</button>
</div>
<div className="h-[calc(100%-44px)] overflow-y-auto whitespace-pre-wrap px-5 py-4 text-[14px] leading-7">
{STORY_SCRIPT_TEXT}
</div>
</div>
</div>
);
}
function ScriptGeneratorPanel() {
const rows = [
["01", "现代办公室", "深夜加班,键盘声急促,人物体力透支", "环境键盘声"],
["02", "盛唐大殿", "女主惊醒跪于金銮殿,百官压迫", "鼓声与低频"],
["03", "长安街市", "诗声落下,盛唐山河展开", "史诗音乐起"],
];
return (
<div className="w-[560px]">
<div className="mb-2 flex items-center gap-1 text-[20px] font-semibold text-white/50">
<GridTilesGlyph />
脚本生成器
</div>
<div className="h-[390px] overflow-hidden rounded-[10px] border border-white/12 bg-white text-[#1f2329] shadow-[0_18px_60px_rgba(0,0,0,0.36)]">
<div className="flex h-12 items-center justify-between border-[#e5e7eb] border-b px-4">
<span className="font-semibold text-[15px]">故事脚本</span>
<span className="rounded-full bg-[#eef0ff] px-3 py-1 text-[12px] text-[#5161ff]">待生成</span>
</div>
<div className="grid grid-cols-[64px_120px_1fr_110px] border-[#eef0f3] border-b bg-[#f7f8fa] text-[12px] text-[#697080]">
{["镜号", "场景", "画面", "声音"].map((item) => (
<div className="border-[#eef0f3] border-r px-3 py-2 last:border-r-0" key={item}>{item}</div>
))}
</div>
{rows.map((row) => (
<div className="grid grid-cols-[64px_120px_1fr_110px] border-[#eef0f3] border-b text-[12px]" key={row[0]}>
{row.map((cell) => (
<div className="border-[#eef0f3] border-r px-3 py-3 last:border-r-0" key={cell}>{cell}</div>
))}
</div>
))}
<button
className="m-4 h-10 w-[calc(100%-32px)] rounded-[10px] bg-[#111] text-[13px] font-semibold text-white"
onClick={() => toast.info("故事脚本生成接口待接入。")}
type="button"
>
生成完整脚本
</button>
</div>
</div>
);
}
function PresetMediaPanel({
imageUrl,
label,
small = false,
wide = false,
}: {
imageUrl: string;
label: string;
small?: boolean;
wide?: boolean;
}) {
return (
<div className={cn("relative", small ? "w-[430px]" : wide ? "w-[620px]" : "w-[560px]")}>
<div className="mb-2 flex items-center gap-1 text-[20px] font-semibold text-white/50">
<ImageIcon className="h-5 w-5" />
{label}
</div>
<div className={cn("relative overflow-hidden rounded-[10px] border border-white/12 bg-white shadow-[0_18px_60px_rgba(0,0,0,0.36)]", small ? "h-[220px]" : "h-[390px]")}>
<img alt={label} className="h-full w-full object-cover" src={imageUrl} />
<button
className="absolute right-3 top-3 flex h-10 w-10 items-center justify-center rounded-[8px] bg-black/56 text-white/90"
onClick={() => toast.info(`${label}素材替换接口待接入。`)}
type="button"
>
↥
</button>
</div>
</div>
);
}
function VideoResultPanel({ label }: { label: string }) {
return (
<div className="w-[560px]">
<div className="mb-2 flex items-center gap-1 text-[20px] font-semibold text-white/50">
<Video className="h-5 w-5" />
{label}
</div>
<div className="relative flex h-[390px] items-center justify-center overflow-hidden rounded-[10px] border border-white/12 bg-[#101010] shadow-[0_18px_60px_rgba(0,0,0,0.36)]">
<div className="absolute inset-0 bg-[radial-gradient(circle,rgba(255,255,255,0.12)_1px,transparent_1px)] bg-[length:18px_18px] opacity-20" />
<div className="relative text-center">
<div className="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-white/8 text-white/70">
<Video className="h-7 w-7" />
</div>
<div className="mt-4 text-[18px] font-semibold text-white/78">待生成视频</div>
<div className="mt-2 text-[13px] text-white/38">生成后将在这里预览</div>
</div>
<button
className="absolute right-3 top-3 rounded-[8px] bg-white/8 px-3 py-2 text-[12px] text-white/72 hover:bg-white/12"
onClick={() => toast.info("视频全屏预览待接入。")}
type="button"
>
↗
</button>
</div>
</div>
);
}
function AudioResourcePanel() {
return (
<div className="w-[430px]">
<div className="mb-2 flex items-center gap-1 text-[20px] font-semibold text-white/50">
<Music2 className="h-5 w-5" />
音频
</div>
<div className="rounded-[10px] border border-white/12 bg-[#f4f5f7] p-4 text-[#1f2329] shadow-[0_18px_60px_rgba(0,0,0,0.36)]">
<div className="flex items-center gap-4">
<button
className="flex h-12 w-12 items-center justify-center rounded-full bg-[#111] text-white"
onClick={() => toast.info("音频播放控制待接入。")}
type="button"
>
▶
</button>
<div className="min-w-0 flex-1">
<div className="truncate text-[14px] font-semibold">情绪配乐.mp3</div>
<div className="mt-1 text-[12px] text-[#697080]">音频生视频参考素材</div>
</div>
<a className="rounded-[8px] px-2 py-1 text-[12px] text-[#4f5cff] hover:bg-black/5" href={AUDIO_SOURCE_URL} rel="noreferrer" target="_blank">
试听
</a>
</div>
<div className="mt-4 flex h-12 items-end gap-1">
{Array.from({ length: 38 }).map((_, index) => (
<span
className="flex-1 rounded-full bg-[#7b86ff]"
key={index}
style={{ height: `${18 + ((index * 17) % 28)}px`, opacity: 0.34 + ((index % 5) * 0.12) }}
/>
))}
</div>
</div>
</div>
);
}
function PresetComposerPanel({
credit,
prompt,
submitMessage,
thumbnailUrl,
title,
}: {
credit: string;
prompt: string;
submitMessage: string;
thumbnailUrl?: string;
title: string;
}) {
return (
<div className="pointer-events-auto absolute bottom-[46px] left-1/2 h-[238px] w-[972px] -translate-x-1/2 rounded-[18px] border border-white/8 bg-[#262626]/98 p-5 text-white shadow-[0_24px_84px_rgba(0,0,0,0.48)]">
<button className="absolute right-5 top-5 text-[26px] text-white/45" onClick={() => toast.info("展开提示词编辑器待接入。")} type="button">↗</button>
<div className="mb-5 flex items-center gap-3">
<span className="rounded-[10px] border border-white/10 bg-[#292929] px-3 py-2 text-[14px] text-white/62">{title}</span>
{thumbnailUrl ? (
<div className="h-[54px] w-[54px] overflow-hidden rounded-[10px]">
<img alt="" className="h-full w-full object-cover" src={thumbnailUrl} />
</div>
) : null}
</div>
<div className="text-[20px] text-white/54">{prompt}</div>
<div className="absolute bottom-5 left-5 right-5 flex items-center gap-6 text-[17px] text-white/78">
<ComposerControl label="Lib Nano Pro" icon="◉" dropdown onClick={() => toast.info("模型选择接口待接入。")} />
<ComposerControl label="16:9" icon="▭" dropdown onClick={() => toast.info("比例选择接口待接入。")} />
<ComposerControl label="2K" dropdown onClick={() => toast.info("清晰度选择接口待接入。")} />
<ComposerControl label="摄像机" icon="▣" onClick={() => toast.info("镜头配置接口待接入。")} />
<ComposerControl label="1张" dropdown onClick={() => toast.info("生成数量接口待接入。")} />
<ComposerControl label={credit} icon="⚡" muted onClick={() => toast.info("积分估算接口待接入。")} />
<button
className="ml-auto flex h-11 w-11 items-center justify-center rounded-[12px] bg-white/76 text-[24px] text-black hover:bg-white"
onClick={() => toast.info(submitMessage)}
type="button"
>
↑
</button>
</div>
</div>
);
}
function CharacterThreeViewScene() {
const [toolbarMenu, setToolbarMenu] = useState<SceneToolbarMenu>(null);
@ -2171,6 +2529,207 @@ function ToggleRow({
);
}
function AssetLibraryPanel() {
const assets = [
{ label: "角色图", type: "图片", url: CHARACTER_IMAGE_URL },
{ label: "角色三视图", type: "图片", url: CHARACTER_THREE_VIEW_URL },
{ label: "首帧", type: "图片", url: FIRST_FRAME_IMAGE_URL },
{ label: "音频参考图", type: "图片", url: AUDIO_REFERENCE_IMAGE_URL },
];
return (
<div className="space-y-4">
<div className="flex flex-wrap gap-2">
{["项目素材", "图片", "视频", "音频", "风格", "标记"].map((item, index) => (
<button
className={cn(
"h-8 rounded-[8px] border border-white/8 px-3 text-[12px] text-white/56 hover:bg-white/8 hover:text-white",
index === 0 && "bg-white text-black hover:bg-white hover:text-black"
)}
key={item}
type="button"
>
{item}
</button>
))}
</div>
<button
className="flex h-20 w-full items-center justify-center rounded-[8px] border border-dashed border-white/16 bg-white/5 text-[13px] text-white/54 hover:bg-white/8 hover:text-white"
onClick={() => toast.info("素材上传接口待接入。")}
type="button"
>
<Plus className="mr-2 h-4 w-4" />
上传或拖入素材
</button>
<div className="grid grid-cols-2 gap-3">
{assets.map((asset) => (
<button
className="overflow-hidden rounded-[8px] border border-white/8 bg-white/5 text-left hover:border-white/20 hover:bg-white/8"
key={asset.label}
onClick={() => toast.info(`${asset.label}选择接口待接入。`)}
type="button"
>
<div className="h-28 bg-cover bg-center" style={{ backgroundImage: `url(${asset.url})` }} />
<div className="flex items-center justify-between px-3 py-2">
<span className="text-[13px] text-white/82">{asset.label}</span>
<span className="rounded-[6px] bg-white/8 px-2 py-0.5 text-[11px] text-white/42">{asset.type}</span>
</div>
</button>
))}
</div>
</div>
);
}
function SkillsAccessPanel() {
return (
<div className="space-y-4">
<div className="rounded-[8px] border border-white/8 bg-white/5 p-4">
<div className="flex items-center justify-between">
<div>
<div className="font-semibold text-[15px] text-white">OpenAPI Access Key</div>
<div className="mt-1 text-[12px] text-white/42">用于 LibTV Skills 调用、模型能力和自动化任务。</div>
</div>
<button
className="h-9 rounded-[8px] bg-white px-3 text-[12px] font-semibold text-black hover:bg-white/90"
onClick={() => toast.info("Access Key 创建接口待接入。")}
type="button"
>
创建 Key
</button>
</div>
<div className="mt-4 rounded-[8px] border border-white/8 bg-black/20 px-3 py-3 text-[13px] text-white/42">
暂无 Access Key
</div>
</div>
<div className="grid grid-cols-5 gap-2">
{Object.entries(capturedCounts.byType || {}).map(([type, count]) => {
const meta = TOOL_TYPE_META[type] || TOOL_TYPE_META.video;
return (
<div className="rounded-[8px] bg-white/6 p-3 text-center" key={type}>
<div className="text-[18px] text-white">{String(count)}</div>
<div className="mt-1 text-[11px] text-white/42">{meta.label}</div>
</div>
);
})}
</div>
</div>
);
}
function MemberCenterPanel() {
return (
<div className="space-y-4">
<div className="rounded-[8px] border border-[#ffc65d]/25 bg-[#2a2418] p-4">
<div className="text-[12px] text-[#fad6a4]">限时 39 折</div>
<div className="mt-1 text-[22px] font-bold text-white">会员特惠39折</div>
<div className="mt-1 text-[13px] text-white/52">会员超市、模型权益、生成额度与积分套餐待接入。</div>
<button
className="mt-4 h-10 rounded-[8px] bg-[#ffc65d] px-4 text-[13px] font-bold text-black hover:bg-[#ffd282]"
onClick={() => toast.info("会员购买接口待接入。")}
type="button"
>
查看权益
</button>
</div>
<div className="grid grid-cols-3 gap-3">
{[
["100", "可用积分"],
["67", "模型能力"],
["25", "工作流预设"],
].map(([value, label]) => (
<div className="rounded-[8px] border border-white/8 bg-white/5 p-3 text-center" key={label}>
<div className="text-[20px] font-semibold text-white">{value}</div>
<div className="mt-1 text-[12px] text-white/42">{label}</div>
</div>
))}
</div>
</div>
);
}
function NotificationsPanel() {
return (
<div className="space-y-3">
{["全部", "生成任务", "系统通知", "会员活动"].map((item, index) => (
<button
className={cn(
"mr-2 h-8 rounded-[8px] border border-white/8 px-3 text-[12px] text-white/56 hover:bg-white/8 hover:text-white",
index === 0 && "bg-white text-black hover:bg-white hover:text-black"
)}
key={item}
type="button"
>
{item}
</button>
))}
{[
["生成任务", "任务状态、进度轮询和失败原因接口待接入。"],
["系统通知", "审核、账户和平台消息接口待接入。"],
["会员活动", "会员超市与活动消息接口待接入。"],
].map(([title, description]) => (
<div className="rounded-[8px] border border-white/8 bg-white/5 p-3" key={title}>
<div className="text-[13px] font-semibold text-white/82">{title}</div>
<div className="mt-1 text-[12px] text-white/42">{description}</div>
</div>
))}
</div>
);
}
function AccountPanel() {
return (
<div className="space-y-4">
<div className="flex items-center gap-3 rounded-[8px] border border-white/8 bg-white/5 p-4">
<img alt="avatar" className="h-12 w-12 rounded-full object-cover" src={TARGET_AVATAR} />
<div className="min-w-0 flex-1">
<div className="truncate text-[15px] font-semibold text-white">微信用户82a0d8</div>
<div className="mt-1 text-[12px] text-white/42">账号中心、团队空间和额度设置待接入。</div>
</div>
<button
className="h-9 rounded-[8px] border border-white/8 px-3 text-[12px] text-white/62 hover:bg-white/8 hover:text-white"
onClick={() => toast.info("账号设置接口待接入。")}
type="button"
>
设置
</button>
</div>
<div className="grid grid-cols-2 gap-3">
{[
["100", "当前积分"],
["个人空间", "当前空间"],
].map(([value, label]) => (
<div className="rounded-[8px] border border-white/8 bg-white/5 p-3" key={label}>
<div className="text-[18px] font-semibold text-white">{value}</div>
<div className="mt-1 text-[12px] text-white/42">{label}</div>
</div>
))}
</div>
</div>
);
}
function SupportPanel() {
return (
<div className="space-y-3">
{[
["使用教程", "打开 LibTV 视频画布教程。"],
["联系客服", "提交工单或联系在线客服。"],
["问题反馈", "反馈画布、生成和支付问题。"],
].map(([title, description]) => (
<button
className="w-full rounded-[8px] border border-white/8 bg-white/5 p-3 text-left hover:bg-white/8"
key={title}
onClick={() => toast.info(`${title}入口待接入。`)}
type="button"
>
<div className="text-[13px] font-semibold text-white/82">{title}</div>
<div className="mt-1 text-[12px] text-white/42">{description}</div>
</button>
))}
</div>
);
}
function LibModal({
children,
onClose,