feat: 首页下方剧本评测展示页替换为ScriptReviewShowcase,团队token监控替换为ModelGenerationShowcase

This commit is contained in:
OmniAI Developer
2026-06-02 22:45:16 +08:00
parent d4d8cc528d
commit 1cac62d14a
7 changed files with 2150 additions and 12 deletions
+84 -11
View File
@@ -12,7 +12,11 @@ import type { WebViewKey, WebImageWorkbenchTool } from "../../types";
import { useScrollEntrance } from "../../hooks/useScrollEntrance";
import WelcomeSplash from "./WelcomeSplash";
import ToolboxSection from "./ToolboxSection";
import ScriptReviewVisual from "./ScriptReviewVisual";
import ScriptReviewShowcase from "./ScriptReviewShowcase";
import ModelGenerationShowcase from "./ModelGenerationShowcase";
import ecommerceTemplate1 from "../../assets/home-features/home-ecommerce-template-1.png";
import ecommerceTemplate2 from "../../assets/home-features/home-ecommerce-template-2.png";
import ecommerceTemplate3 from "../../assets/home-features/home-ecommerce-template-3.png";
function ScrollEntrance({ children, className, ...rest }: { children: React.ReactNode; className?: string } & React.HTMLAttributes<HTMLElement>) {
const { ref, isVisible } = useScrollEntrance<HTMLElement>();
@@ -61,14 +65,14 @@ const HOME_FEATURES = [
stats: ["六维评分", "质量量化", "逐项优化"],
},
{
key: "token",
eyebrow: "Team Tokens",
title: "团队 Token 监控",
description: "实时追踪团队 Token 消耗、项目分布和成员使用情况,让预算、配额和成本都能被清楚管理。",
key: "model",
eyebrow: "AI Generation",
title: "模型生成",
description: "通过AI模型生成文本、图片、视频,三种模式覆盖全内容类型,Agent对话式交互智能产出。",
imageUrl: featureTokenImage,
actionLabel: "查看面板",
icon: <DashboardOutlined />,
stats: ["实时概览", "成员明细", "成本分析"],
actionLabel: "开始生成",
icon: <ThunderboltOutlined />,
stats: ["文本生成", "图片生成", "视频生成"],
},
{
key: "ecommerce",
@@ -89,6 +93,34 @@ const HOME_EXPERIENCE_POINTS = [
{ label: "电商", meta: "商品视觉", tone: "amber" },
];
const HOME_ECOMMERCE_TEMPLATES = [
{
title: "卖点详情图",
tag: "详情",
meta: "中文卖点标注",
imageUrl: ecommerceTemplate1,
},
{
title: "场景主图",
tag: "主图",
meta: "商品氛围构图",
imageUrl: ecommerceTemplate2,
},
{
title: "虚拟模特",
tag: "模特",
meta: "使用场景延展",
imageUrl: ecommerceTemplate3,
},
];
const HOME_ECOMMERCE_TOOLS = [
{ title: "主图", meta: "平台首图" },
{ title: "详情", meta: "卖点拆解" },
{ title: "模特", meta: "虚拟模特" },
{ title: "短视频", meta: "首帧方案" },
];
const HOME_CAROUSEL_SLOTS = [-4, -3, -2, -1, 0, 1, 2, 3, 4];
const HOME_CAROUSEL_TRANSITION_MS = 860;
@@ -127,6 +159,43 @@ function getHomeCarouselCardStyle(offset: number): CSSProperties {
} as CSSProperties;
}
function EcommerceFeatureShowcase() {
return (
<div className="omni-home-ecommerce-showcase">
<div className="omni-home-ecommerce-showcase__depth" />
<div className="omni-home-ecommerce-showcase__grain" />
<div className="omni-home-ecommerce-showcase__prompt">
<span> + </span>
<strong></strong>
<p></p>
</div>
<div className="omni-home-ecommerce-showcase__tools" aria-hidden="true">
{HOME_ECOMMERCE_TOOLS.map((item) => (
<div key={item.title} className="omni-home-ecommerce-showcase__tool">
<b>{item.title}</b>
<small>{item.meta}</small>
</div>
))}
</div>
<div className="omni-home-ecommerce-showcase__gallery" aria-hidden="true">
{HOME_ECOMMERCE_TEMPLATES.map((item, index) => (
<article key={item.title} className={`omni-home-ecommerce-showcase__shot is-${index + 1}`}>
<img src={item.imageUrl} alt="" />
<div>
<span>{item.tag}</span>
<strong>{item.title}</strong>
<small>{item.meta}</small>
</div>
</article>
))}
</div>
</div>
);
}
function HomePage({ onOpenGenerate, onOpenEcommerce, onOpenScriptReview, onOpenTokenMonitor, onSelectView, onOpenImageTool }: HomePageProps) {
const [splashDismissed, setSplashDismissed] = useState(() => sessionStorage.getItem("omniai:splash-seen") === "1");
const [activeSlideIndex, setActiveSlideIndex] = useState(0);
@@ -205,8 +274,8 @@ function HomePage({ onOpenGenerate, onOpenEcommerce, onOpenScriptReview, onOpenT
(onOpenScriptReview ?? onOpenGenerate)();
return;
}
if (featureKey === "token") {
(onOpenTokenMonitor ?? onOpenGenerate)();
if (featureKey === "model") {
onOpenGenerate();
return;
}
if (featureKey === "ecommerce") {
@@ -312,7 +381,11 @@ function HomePage({ onOpenGenerate, onOpenEcommerce, onOpenScriptReview, onOpenT
</div>
<div className="omni-home__feature-visual" aria-hidden="true">
{feature.key === "script" ? (
<ScriptReviewVisual />
<ScriptReviewShowcase />
) : feature.key === "model" ? (
<ModelGenerationShowcase />
) : feature.key === "ecommerce" ? (
<EcommerceFeatureShowcase />
) : (
<img src={feature.imageUrl} alt="" />
)}
@@ -0,0 +1,282 @@
import { useEffect, useRef, useState } from "react";
type ShowMode = "agent" | "image" | "video";
const MODE_TABS = [
{ key: "agent" as const, icon: "🤖", title: "Agent 模式", desc: "文本生成,对话式交互,智能推理" },
{ key: "image" as const, icon: "🖼️", title: "图片模式", desc: "图像生成,风格迁移,场景合成" },
{ key: "video" as const, icon: "🎬", title: "视频模式", desc: "视频生成,动态场景,数字人演绎" },
];
const AGENT_OUTPUTS = [
{ tag: "Agent", title: "营销文案", preview: "「未来已来,腕间即达」—— 全新智能手表,让每一秒都充满可能。健康监测、智能提醒、时尚设计,三合一体验。从晨跑配速到会议提醒,它比你更懂你的节奏。" },
{ tag: "Agent", title: "产品描述", preview: "搭载最新AI芯片,支持实时心率/血氧/睡眠监测,50米防水,AMOLED视网膜屏,续航长达14天。钛合金表壳 + 硅胶快拆表带,商务运动自由切换。" },
{ tag: "Agent", title: "剧本大纲", preview: "第一幕:品牌故事引入 → 第二幕:产品亮点展示 → 第三幕:用户见证与CTA引导。" },
];
const IMAGE_OUTPUTS = [
{ tag: "Image", title: "写实风格", icon: "📷", styleClass: "realistic" },
{ tag: "Image", title: "插画风格", icon: "🎨", styleClass: "illustration" },
{ tag: "Image", title: "电商风格", icon: "🛍️", styleClass: "ecommerce" },
];
const VIDEO_OUTPUTS = [
{ tag: "Video", title: "数字人播报", duration: "0:15" },
{ tag: "Video", title: "场景动画", duration: "0:30" },
{ tag: "Video", title: "产品展示", duration: "0:20" },
];
function ModelGenerationShowcase() {
const [mode, setMode] = useState<ShowMode>("agent");
const [selectedImageOpt, setSelectedImageOpt] = useState(0);
const [, setAnimated] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const el = containerRef.current;
if (!el) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry?.isIntersecting) {
setAnimated(true);
observer.disconnect();
}
},
{ threshold: 0.2 },
);
observer.observe(el);
return () => observer.disconnect();
}, []);
const workflowSteps = [
{ label: "选择模式", active: mode === "agent" },
{ label: "输入描述", active: mode === "image" },
{ label: "AI生成", active: mode === "video" },
{ label: "输出内容", active: false },
];
return (
<div className="omni-model-gen-showcase" ref={containerRef}>
{/* Left Panel */}
<div className="mgs-left-panel">
<div className="mgs-brand-section">
<h1><br /></h1>
<p className="mgs-subtitle">AI模型生成文本</p>
</div>
<div className="mgs-mode-tabs">
{MODE_TABS.map((tab) => (
<div
key={tab.key}
className={`mgs-mode-tab${mode === tab.key ? " is-active" : ""}`}
onClick={() => setMode(tab.key)}
role="button"
tabIndex={0}
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") setMode(tab.key); }}
>
<div className="mgs-mode-icon">{tab.icon}</div>
<div className="mgs-mode-info">
<h3>{tab.title}</h3>
<p>{tab.desc}</p>
</div>
</div>
))}
</div>
<div className="mgs-workflow">
<div className="mgs-workflow-title"></div>
<div className="mgs-workflow-steps">
{workflowSteps.map((step, i) => (
<span key={step.label}>
{i > 0 && <span className="mgs-wf-arrow"></span>}
<span className={`mgs-wf-step${step.active ? " is-active" : ""}`}>{step.label}</span>
</span>
))}
</div>
</div>
</div>
{/* Center Area */}
<div className="mgs-center-area">
<div className="mgs-input-card">
{/* Agent Mode */}
{mode === "agent" && (
<div className="mgs-mode-content is-active">
<div className="mgs-card-header">
<div className="mgs-card-mode-badge"><span className="mgs-dot" /> Agent </div>
<div className="mgs-card-status">线 · GPT-4o</div>
</div>
<div className="mgs-prompt-area">
<textarea
className="mgs-prompt-input"
placeholder="描述你的需求...&#10;例如:帮我生成一段智能手表的营销文案,突出健康监测功能"
readOnly
/>
</div>
<div className="mgs-options">
{["营销文案", "产品描述", "剧本大纲", "数据分析"].map((opt, i) => (
<span key={opt} className={`mgs-opt${i === 0 ? " is-selected" : ""}`}>{opt}</span>
))}
</div>
<div className="mgs-agent-output-area">
<div className="mgs-agent-result">
<div className="mgs-agent-result-label">AI </div>
<div className="mgs-agent-result-text">
</div>
</div>
</div>
<div className="mgs-chat-input-row">
<input className="mgs-chat-input" placeholder="继续对话或修改需求..." readOnly />
<button className="mgs-chat-send" type="button" tabIndex={-1}> </button>
</div>
</div>
)}
{/* Image Mode */}
{mode === "image" && (
<div className="mgs-mode-content is-active">
<div className="mgs-card-header">
<div className="mgs-card-mode-badge"><span className="mgs-dot" /> </div>
<div className="mgs-card-status">SDXL · Flux</div>
</div>
<div className="mgs-prompt-area">
<textarea
className="mgs-prompt-input"
placeholder="描述你想生成的图片...&#10;例如:未来科技感城市,赛博朋克风格,霓虹灯光"
readOnly
/>
</div>
<div className="mgs-options">
{["写实风", "插画风", "电商风", "3D渲染"].map((opt, i) => (
<span
key={opt}
className={`mgs-opt${selectedImageOpt === i ? " is-selected" : ""}`}
onClick={() => setSelectedImageOpt(i)}
>
{opt}
</span>
))}
</div>
<div className="mgs-img-grid">
<div className="mgs-img-cell">🎨</div>
<div className="mgs-img-cell">🖼</div>
<div className="mgs-img-cell"></div>
<div className="mgs-img-cell">🌈</div>
</div>
</div>
)}
{/* Video Mode */}
{mode === "video" && (
<div className="mgs-mode-content is-active">
<div className="mgs-card-header">
<div className="mgs-card-mode-badge"><span className="mgs-dot" /> </div>
<div className="mgs-card-status">Sora · Kling</div>
</div>
<div className="mgs-video-config">
<div className="mgs-config-row">
<span className="mgs-config-label"></span>
<span className="mgs-config-value">1080p</span>
</div>
<div className="mgs-config-row">
<span className="mgs-config-label"></span>
<span className="mgs-config-value">15s</span>
</div>
<div className="mgs-config-row">
<span className="mgs-config-label"></span>
<span className="mgs-config-value">30fps</span>
</div>
<div className="mgs-config-row">
<span className="mgs-config-label"></span>
<span className="mgs-config-value"></span>
</div>
</div>
<div className="mgs-video-preview">
<div className="mgs-play-btn">
<svg viewBox="0 0 24 24"><polygon points="6,3 20,12 6,21" /></svg>
</div>
</div>
<div className="mgs-video-timeline">
<div className="mgs-video-timeline-fill" />
</div>
</div>
)}
</div>
</div>
{/* Right Panel */}
<div className="mgs-right-panel">
{/* Agent Outputs */}
{mode === "agent" && (
<div className="mgs-output-section">
<div className="mgs-section-label">
<span className="mgs-section-dot is-green" />
Agent
</div>
<div className="mgs-output-cards">
{AGENT_OUTPUTS.map((item) => (
<div key={item.title} className="mgs-out-card">
<div className="mgs-out-card-top">
<span className="mgs-out-tag">{item.tag}</span>
<span className="mgs-out-title">{item.title}</span>
</div>
<div className="mgs-out-preview">{item.preview}</div>
</div>
))}
</div>
</div>
)}
{/* Image Outputs */}
{mode === "image" && (
<div className="mgs-output-section">
<div className="mgs-section-label">
<span className="mgs-section-dot is-blue" />
</div>
<div className="mgs-output-cards">
{IMAGE_OUTPUTS.map((item) => (
<div key={item.title} className="mgs-out-card">
<div className="mgs-out-card-top">
<span className="mgs-out-tag is-img">{item.tag}</span>
<span className="mgs-out-title">{item.title}</span>
</div>
<div className={`mgs-out-img-placeholder is-${item.styleClass}`}>{item.icon}</div>
</div>
))}
</div>
</div>
)}
{/* Video Outputs */}
{mode === "video" && (
<div className="mgs-output-section">
<div className="mgs-section-label">
<span className="mgs-section-dot is-purple" />
</div>
<div className="mgs-output-cards">
{VIDEO_OUTPUTS.map((item) => (
<div key={item.title} className="mgs-out-card">
<div className="mgs-out-card-top">
<span className="mgs-out-tag is-video">{item.tag}</span>
<span className="mgs-out-title">{item.title}</span>
</div>
<div className="mgs-out-video-placeholder">
<div className="mgs-mini-play">
<svg viewBox="0 0 24 24"><polygon points="6,3 20,12 6,21" /></svg>
</div>
<span className="mgs-video-duration">{item.duration}</span>
</div>
</div>
))}
</div>
</div>
)}
</div>
</div>
);
}
export default ModelGenerationShowcase;
+208
View File
@@ -0,0 +1,208 @@
import { useEffect, useRef, useState } from "react";
const DIMS = [
{ name: "钩子设计", score: 16, max: 20, hue: 145, desc: "吸引力·悬念·黄金三秒", isPerfect: false, isLow: false },
{ name: "角色塑造", score: 15, max: 15, hue: 155, desc: "立体度·动机·弧光", isPerfect: true, isLow: false },
{ name: "剧情结构", score: 16, max: 20, hue: 165, desc: "起承转合·节奏·冲突", isPerfect: false, isLow: false },
{ name: "逻辑严密", score: 12, max: 15, hue: 175, desc: "自洽·伏笔·因果链", isPerfect: false, isLow: false },
{ name: "场景构建", score: 10, max: 15, hue: 185, desc: "空间·视听·画面感", isPerfect: false, isLow: true },
{ name: "内容深度", score: 8, max: 15, hue: 195, desc: "主题·情感·思想内核", isPerfect: false, isLow: true },
];
const HIGHLIGHTS = [
{ dim: "角色塑造", score: "15/15 ★", text: "满分表现!人物立体度与弧光设计均达高水准" },
{ dim: "钩子设计", score: "16/20", text: "开篇悬念精准,黄金三秒有效抓住注意力" },
{ dim: "剧情结构", score: "16/20", text: "起承转合清晰,节奏把控得当,冲突自然递进" },
];
const WEAKNESSES = [
{ dim: "内容深度", score: "8/15", text: "主题偏表层,情感共鸣与思想内核挖掘不足" },
{ dim: "场景构建", score: "10/15", text: "空间描写模糊,视听语言薄弱,画面感欠缺" },
{ dim: "逻辑严密", score: "12/15", text: "世界观细节不足,部分伏笔缺乏回收" },
];
const OPTIMIZATIONS = [
{ dim: "场景构建 → 提升", priority: "高优先", priorityClass: "badge-red", text: "增加具体空间描写与视听语言,强化沉浸感" },
{ dim: "内容深度 → 深挖", priority: "高优先", priorityClass: "badge-red", text: "围绕核心冲突深化人物选择与情感层次" },
{ dim: "逻辑严密 → 补强", priority: "中优先", priorityClass: "badge-orange", text: "补充世界观细节,强化因果链与伏笔回收" },
];
function animateNumber(el: HTMLElement | null, target: number, duration: number) {
if (!el) return;
const start = performance.now();
const targetEl = el;
function tick(now: number) {
const p = Math.min((now - start) / duration, 1);
targetEl.textContent = String(Math.round((1 - Math.pow(1 - p, 3)) * target));
if (p < 1) requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
}
function ScriptReviewShowcase() {
const [animated, setAnimated] = useState(false);
const scoreRef = useRef<HTMLSpanElement>(null);
const barRefs = useRef<(HTMLDivElement | null)[]>([]);
const scoreValRefs = useRef<(HTMLSpanElement | null)[]>([]);
useEffect(() => {
const el = document.getElementById("script-review-showcase");
if (!el) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry?.isIntersecting) {
setAnimated(true);
observer.disconnect();
}
},
{ threshold: 0.25 },
);
observer.observe(el);
return () => observer.disconnect();
}, []);
useEffect(() => {
if (!animated) return;
const timer = setTimeout(() => {
animateNumber(scoreRef.current, 77, 1400);
barRefs.current.forEach((bar, i) => {
if (!bar) return;
const pct = parseFloat(bar.dataset.pct ?? "0");
setTimeout(() => { bar.style.height = `${pct}%`; }, i * 100 + 400);
});
scoreValRefs.current.forEach((el, i) => {
setTimeout(() => animateNumber(el, parseInt(el?.dataset.target ?? "0"), 800), i * 100 + 400);
});
}, 500);
return () => clearTimeout(timer);
}, [animated]);
return (
<div className="omni-script-review-showcase" id="script-review-showcase">
{/* Score Hero */}
<div className="srs-score-hero">
<div className="srs-score-left">
<div className="srs-score-circle">
<div className="srs-score-circle-inner">
<span className="srs-score-num" ref={scoreRef}>0</span>
<span className="srs-score-den">/ 100</span>
</div>
</div>
<div className="srs-score-meta">
<div className="srs-score-grade">A </div>
<div className="srs-score-tags">
<span className="srs-score-tag"></span>
<span className="srs-score-tag">58min</span>
<span className="srs-score-tag">6</span>
</div>
</div>
</div>
<div className="srs-score-divider" />
<div className="srs-score-right">
<div className="srs-score-proj">广 · </div>
<div className="srs-score-summary">
</div>
</div>
</div>
{/* Vertical Bar Chart */}
<div className="srs-chart-card">
<div className="srs-chart-title"> Dimension Breakdown</div>
<div className="srs-chart-body">
{DIMS.map((dim, i) => {
const pct = dim.score / dim.max;
return (
<div key={dim.name} className="srs-chart-col">
<div className="srs-chart-bar-wrap">
<div className="srs-chart-bar-bg" style={{ height: "100%" }} />
<div
ref={(el) => { barRefs.current[i] = el; }}
className={`srs-chart-bar-fill${dim.isPerfect ? " is-perfect" : ""}${dim.isLow ? " is-low" : ""}`}
data-pct={String(Math.round(pct * 100))}
style={{ height: "0%" }}
>
<div className="srs-chart-bar-score">
<span
ref={(el) => { scoreValRefs.current[i] = el; }}
data-target={String(dim.score)}
>0</span>
<span className="srs-chart-bar-sub">/{dim.max}</span>
{dim.isPerfect && <span className="srs-chart-bar-star"></span>}
</div>
</div>
</div>
<div className="srs-chart-col-label">
<div className="srs-chart-col-name">{dim.name}</div>
<div className="srs-chart-col-desc">{dim.desc}</div>
</div>
</div>
);
})}
</div>
</div>
{/* Triple Section */}
<div className="srs-triple-section">
{/* Highlights */}
<div className="srs-section-card is-highlight">
<div className="srs-section-header">
<div className="srs-section-icon"></div>
<span className="srs-section-label"></span>
</div>
<div className="srs-section-list">
{HIGHLIGHTS.map((item) => (
<div key={item.dim} className="srs-section-item">
<div className="srs-section-item-head">
<span className="srs-section-item-dim">{item.dim}</span>
<span className="srs-section-item-score is-green">{item.score}</span>
</div>
<div className="srs-section-item-text">{item.text}</div>
</div>
))}
</div>
</div>
{/* Weaknesses */}
<div className="srs-section-card is-weakness">
<div className="srs-section-header">
<div className="srs-section-icon"></div>
<span className="srs-section-label"></span>
</div>
<div className="srs-section-list">
{WEAKNESSES.map((item) => (
<div key={item.dim} className="srs-section-item">
<div className="srs-section-item-head">
<span className="srs-section-item-dim">{item.dim}</span>
<span className="srs-section-item-score is-red">{item.score}</span>
</div>
<div className="srs-section-item-text">{item.text}</div>
</div>
))}
</div>
</div>
{/* Optimization */}
<div className="srs-section-card is-optimize">
<div className="srs-section-header">
<div className="srs-section-icon"></div>
<span className="srs-section-label"></span>
</div>
<div className="srs-section-list">
{OPTIMIZATIONS.map((item) => (
<div key={item.dim} className="srs-section-item">
<div className="srs-section-item-head">
<span className="srs-section-item-dim">{item.dim}</span>
<span className={`srs-section-item-badge ${item.priorityClass}`}>{item.priority}</span>
</div>
<div className="srs-section-item-text">{item.text}</div>
</div>
))}
</div>
</div>
</div>
</div>
);
}
export default ScriptReviewShowcase;