feat: replace GatewayHero with new dashboard layout

- Refactored `GatewayHero.tsx` to display a new dashboard header layout based on a provided mockup.
- The new design features a greeting, top status cards (Services, Clusters, Alerts), a central search bar, and quick access buttons.
- Bottom graphical cards for "Network Load" and "Global Mesh" were implemented using static styling mocks.
- Retained the core functionality of the central prompt input to route queries to `/xworkmate`.
- Adjusted the homepage spacing in `page.tsx` to accommodate the new top section while keeping the existing `UnifiedNavigation`, `StatsSection`, `ShortcutsSection`, and `Footer`.
- Verified UI changes against the mockup and handled minor review feedback (fixed greeting punctuation and header text contrast).

Co-authored-by: cloud-neutral <4133689+cloud-neutral@users.noreply.github.com>
This commit is contained in:
google-labs-jules[bot] 2026-03-19 04:52:43 +00:00
parent 634614340a
commit 75c58a181b
5 changed files with 222 additions and 289 deletions

58
dev.log
View File

@ -1,31 +1,39 @@
▲ Next.js 16.1.6 (Turbopack)
> dashboard@1.0.0 dev
> bash scripts/Dev-MCP-Server.sh && next dev --turbo
[MCP] Cleaning stale Chrome processes...
[MCP] Starting Chrome (remote debugging)...
[MCP] Waiting for Chrome DevTools endpoint...
[MCP] Chrome DevTools ready on port 9222
[MCP] (Optional) Pre-warming chrome-devtools-mcp...
[MCP] Done. You can now run: npm run dev
▲ Next.js 16.1.2 (Turbopack)
- Local: http://localhost:3000 - Local: http://localhost:3000
- Network: http://192.168.0.2:3000 - Network: http://192.168.0.2:3000
✓ Starting... ✓ Starting...
✓ Ready in 2.5s
○ Compiling /services ... Attention: Next.js now collects completely anonymous telemetry regarding usage.
GET /services 200 in 6.8s (compile: 6.4s, render: 327ms) This information is used to shape Next.js' roadmap and prioritize features.
You can learn more, including how to opt-out if you'd not like to participate in this anonymous program, by visiting the following URL:
https://nextjs.org/telemetry
✓ Ready in 4.4s
○ Compiling / ...
GET / 200 in 11.8s (compile: 11.0s, render: 755ms)
GET / 200 in 513ms (compile: 159ms, render: 354ms)
GET /api/integrations/defaults 200 in 1276ms (compile: 1242ms, render: 35ms)
[runtime-config] Loaded env: PROD [runtime-config] Loaded env: PROD
GET /api/auth/session 200 in 1021ms (compile: 993ms, render: 29ms)
GET /services 200 in 87ms (compile: 5ms, render: 82ms) GET /api/auth/session 200 in 2.2s (compile: 2.2s, render: 15ms)
GET /api/auth/session 200 in 11ms (compile: 3ms, render: 7ms)
✓ Compiled in 310ms GET /api/marketing/home-stats 200 in 1435ms (compile: 1425ms, render: 9ms)
GET /services 200 in 441ms (compile: 149ms, render: 293ms)
GET /api/auth/session 200 in 16ms (compile: 5ms, render: 10ms) GET / 200 in 150ms (compile: 5ms, render: 145ms)
GET /services 200 in 252ms (compile: 100ms, render: 152ms)
GET /api/auth/session 200 in 14ms (compile: 5ms, render: 8ms) GET /api/auth/session 200 in 16ms (compile: 4ms, render: 12ms)
GET /services 200 in 251ms (compile: 86ms, render: 165ms)
GET /api/auth/session 200 in 13ms (compile: 4ms, render: 8ms) GET /api/integrations/defaults 200 in 20ms (compile: 12ms, render: 8ms)
GET /services 200 in 281ms (compile: 94ms, render: 187ms)
GET /api/auth/session 200 in 12ms (compile: 4ms, render: 8ms) GET /api/marketing/home-stats 200 in 29ms (compile: 22ms, render: 7ms)
GET /api/blogs/latest?limit=7 200 in 37ms (compile: 29ms, render: 8ms)

2
next-env.d.ts vendored
View File

@ -1,6 +1,6 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts"; import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@ -96,12 +96,12 @@ export default function HomePage() {
> >
<div className="relative flex-1 overflow-y-auto"> <div className="relative flex-1 overflow-y-auto">
<div className="relative w-full px-2 pb-10 sm:px-3 sm:pb-12 lg:px-4"> <div className="relative w-full px-2 pb-10 sm:px-3 sm:pb-12 lg:px-4">
<main className="relative space-y-4 pt-4 sm:space-y-5 sm:pt-5"> <main className="relative space-y-6 pt-4 sm:space-y-8 sm:pt-6">
<HeroSection /> <HeroSection />
<StatsSection /> <StatsSection />
<ShortcutsSection /> <ShortcutsSection />
</main> </main>
<div className="relative"> <div className="relative mt-12">
<Footer /> <Footer />
</div> </div>
</div> </div>

View File

@ -1,12 +1,14 @@
"use client"; "use client";
import { import {
Search,
ArrowRight, ArrowRight,
Cloud, Cloud,
Send, Bell,
Sparkles, Sun,
SunMedium, Wrench,
Workflow, History,
Globe,
} from "lucide-react"; } from "lucide-react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
@ -14,23 +16,8 @@ import { useState } from "react";
import { useLanguage } from "@/i18n/LanguageProvider"; import { useLanguage } from "@/i18n/LanguageProvider";
import type { IntegrationDefaults } from "@/lib/openclaw/types"; import type { IntegrationDefaults } from "@/lib/openclaw/types";
import { useUserStore } from "@/lib/userStore"; import { useUserStore } from "@/lib/userStore";
import { cn } from "@/lib/utils";
import {
buildHomeGatewayHeroViewModel,
type HomeGatewayStatusNode,
} from "@/components/home/gatewayHeroModel";
import { useGatewayHero } from "@/components/home/useGatewayHero"; import { useGatewayHero } from "@/components/home/useGatewayHero";
function toneClasses(tone: HomeGatewayStatusNode["tone"]): string {
if (tone === "healthy") {
return "border-lime-300/70 bg-lime-50/90 text-lime-950";
}
if (tone === "warning") {
return "border-amber-300/70 bg-amber-50/95 text-amber-950";
}
return "border-slate-200 bg-white/90 text-slate-900";
}
function resolveDisplayName(params: { function resolveDisplayName(params: {
isChinese: boolean; isChinese: boolean;
name?: string | null; name?: string | null;
@ -65,8 +52,7 @@ export function GatewayHero({
const router = useRouter(); const router = useRouter();
const user = useUserStore((state) => state.user); const user = useUserStore((state) => state.user);
const [prompt, setPrompt] = useState(""); const [prompt, setPrompt] = useState("");
const { bootstrap, bootstrapError, bootstrapLoading, gatewayConfigured, sendPrompt, sendState } = const { bootstrap, sendState } = useGatewayHero(defaults);
useGatewayHero(defaults);
const displayName = resolveDisplayName({ const displayName = resolveDisplayName({
isChinese, isChinese,
@ -74,38 +60,10 @@ export function GatewayHero({
username: user?.username, username: user?.username,
email: user?.email, email: user?.email,
}); });
const model = buildHomeGatewayHeroViewModel({
isChinese,
displayName,
bootstrap,
bootstrapError,
connected: gatewayConfigured && !bootstrapError,
});
const badgeClass =
model.statusTone === "healthy"
? "bg-lime-400 text-lime-950 shadow-[0_0_30px_rgba(163,230,53,0.55)]"
: model.statusTone === "warning"
? "bg-amber-300 text-amber-950"
: "bg-slate-200 text-slate-700";
const periodAccent =
model.period === "morning"
? "from-lime-100/90 via-white to-sky-50"
: model.period === "afternoon"
? "from-amber-50 via-white to-cyan-50"
: model.period === "evening"
? "from-emerald-50 via-white to-slate-100"
: "from-slate-100 via-white to-indigo-50";
const responseText = sendState.responseText.trim();
const sessionKey = const sessionKey =
sendState.activeSessionKey || bootstrap?.activeSessionKey || ""; sendState.activeSessionKey || bootstrap?.activeSessionKey || "";
async function handleSend(): Promise<void> {
if (!prompt.trim()) {
return;
}
await sendPrompt(prompt);
}
function openWorkspace(): void { function openWorkspace(): void {
const query = new URLSearchParams(); const query = new URLSearchParams();
if (prompt.trim()) { if (prompt.trim()) {
@ -118,232 +76,194 @@ export function GatewayHero({
router.push(suffix ? `/xworkmate?${suffix}` : "/xworkmate"); router.push(suffix ? `/xworkmate?${suffix}` : "/xworkmate");
} }
return ( function handleInputSubmit() {
<section className="relative overflow-hidden rounded-[1.6rem] border border-lime-200/85 bg-white shadow-[0_24px_70px_rgba(143,214,38,0.12)]"> if (!prompt.trim()) return;
<div openWorkspace();
aria-hidden }
className={cn(
"pointer-events-none absolute inset-0 bg-gradient-to-br",
periodAccent,
)}
/>
<div
aria-hidden
className="pointer-events-none absolute inset-0 bg-[linear-gradient(rgba(163,230,53,0.08)_1px,transparent_1px),linear-gradient(90deg,rgba(163,230,53,0.08)_1px,transparent_1px)] bg-[size:32px_32px] opacity-40"
/>
<div className="relative p-5 sm:p-6 lg:p-8">
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div className="space-y-3">
<div className="inline-flex items-center gap-2 rounded-full border border-lime-200 bg-white/85 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.28em] text-slate-600">
<SunMedium className="h-3.5 w-3.5 text-lime-600" />
{model.panelLabel}
</div>
<div className="space-y-1">
<p className="text-[1.05rem] font-semibold text-slate-800">
{model.greeting}
</p>
<h1 className="text-[2rem] font-semibold tracking-[-0.05em] text-slate-950 sm:text-[2.35rem]">
{model.headline}
</h1>
<p className="max-w-2xl text-[15px] leading-7 text-slate-600 sm:text-[16px]">
{model.summary}
</p>
</div>
</div>
<div className="flex items-start gap-3 self-start rounded-[1.2rem] border border-white/80 bg-white/85 px-4 py-3 shadow-[0_12px_30px_rgba(15,23,42,0.06)] backdrop-blur"> return (
<div <section className="relative w-full overflow-hidden bg-[#fafafa] pt-12 pb-16 px-6 lg:px-12 font-sans rounded-3xl border border-gray-100 shadow-sm">
className={cn( <div
"flex h-12 w-12 items-center justify-center rounded-full text-sm font-bold", className="absolute inset-0 z-0 opacity-40 pointer-events-none"
badgeClass, style={{
)} backgroundImage: `linear-gradient(to right, #e5e7eb 1px, transparent 1px), linear-gradient(to bottom, #e5e7eb 1px, transparent 1px)`,
> backgroundSize: "40px 40px",
{model.statusTone === "healthy" ? "✓" : model.statusTone === "warning" ? "!" : "·"} }}
/>
<div className="relative z-10 max-w-5xl mx-auto flex flex-col items-center">
{/* Header */}
<div className="w-full flex justify-between items-center mb-10">
<div className="flex items-center gap-4">
<div className="flex items-center justify-center w-12 h-12 bg-blue-100 text-blue-500 rounded-2xl shadow-sm">
<Sun size={24} className="text-blue-500" />
</div>
<h1 className="text-3xl font-bold text-gray-900 tracking-tight">
{isChinese ? "早上好," : "Good morning, "}{displayName}
</h1>
</div>
<div className="flex flex-col items-end">
<span className="text-xs font-semibold tracking-wider text-gray-500 mb-1">
SYSTEM STATUS
</span>
<div className="flex items-center gap-1.5 text-blue-500 font-bold text-sm tracking-wide">
<span className="w-2 h-2 rounded-full bg-blue-500 opacity-50"></span>
OPTIMIZED
</div>
</div>
</div>
{/* 3 Top Cards */}
<div className="w-full grid grid-cols-1 md:grid-cols-3 gap-6 mb-12">
{/* Services Card */}
<div className="bg-white rounded-[2rem] p-6 shadow-sm border border-gray-100 flex flex-col justify-between">
<div className="text-sm font-semibold text-gray-800 mb-4">
Services
</div>
<div>
<div className="text-4xl font-bold text-blue-500 flex items-baseline gap-1 font-mono tracking-tight">
Normal <span className="w-2 h-2 rounded-full bg-blue-500 mb-1"></span>
</div> </div>
<div> <div className="text-xs text-gray-400 mt-2 font-medium">
<div className="text-[13px] font-semibold uppercase tracking-[0.18em] text-slate-500"> All 12 microservices active
{isChinese ? "首屏状态" : "Hero Status"}
</div>
<div className="mt-1 text-[1.6rem] font-semibold tracking-[-0.05em] text-slate-950">
{model.statusBadge}
</div>
<p className="mt-1 max-w-[18rem] text-sm leading-6 text-slate-600">
{model.statusDescription}
</p>
</div> </div>
</div> </div>
</div> </div>
<div className="grid gap-5 lg:grid-cols-[minmax(0,1.15fr)_minmax(20rem,0.85fr)]"> {/* Clusters Card */}
<div className="rounded-[1.35rem] border border-lime-200/80 bg-white/88 p-5 shadow-[0_18px_40px_rgba(15,23,42,0.05)]"> <div className="bg-white rounded-[2rem] p-6 shadow-sm border border-gray-100 flex flex-col justify-between relative">
<div className="flex items-center gap-3"> <div className="flex justify-between items-start mb-4">
<div className="h-3 w-3 rounded-full bg-lime-400 shadow-[0_0_18px_rgba(163,230,53,0.9)]" /> <div className="text-sm font-semibold text-gray-800">Clusters</div>
<div className="h-px flex-1 bg-lime-200" /> <Cloud className="text-gray-400 w-5 h-5" />
</div>
<div>
<div className="flex items-baseline gap-2">
<span className="text-4xl font-bold text-gray-900 tracking-tight">
4/4
</span>
<span className="text-xs font-semibold text-gray-400 tracking-wider">
ONLINE
</span>
</div> </div>
<div className="mt-5 space-y-5"> <div className="w-full h-1.5 bg-blue-100 rounded-full mt-3">
{model.statusNodes.map((node) => ( <div className="w-full h-full bg-blue-300 rounded-full"></div>
<div key={node.key} className="flex items-center gap-4">
<div className="relative flex w-[9.5rem] items-center gap-3 text-[1rem] font-semibold text-slate-800">
<div className="h-3 w-3 rounded-full bg-lime-400 shadow-[0_0_12px_rgba(163,230,53,0.75)]" />
<span>{node.label}</span>
</div>
<div className="h-10 w-px bg-slate-200" />
<div className={cn("min-w-0 rounded-full border px-4 py-2 text-lg font-semibold", toneClasses(node.tone))}>
{node.value}
</div>
</div>
))}
</div>
<div className="mt-6 grid gap-3 sm:grid-cols-2">
<div className="rounded-[1rem] border border-slate-200 bg-slate-50/90 p-4">
<div className="flex items-center gap-2 text-sm font-semibold text-slate-700">
<Workflow className="h-4 w-4 text-slate-500" />
{isChinese ? "最近会话" : "Recent Sessions"}
</div>
<div className="mt-3 space-y-2">
{model.recentSessions.length > 0 ? (
model.recentSessions.map((session) => (
<div
key={session.key}
className="rounded-[0.9rem] border border-white bg-white/90 px-3 py-2 text-sm text-slate-700"
>
{session.derivedTitle ||
session.displayName ||
session.lastMessagePreview ||
session.key}
</div>
))
) : (
<p className="text-sm leading-6 text-slate-500">
{isChinese
? "当前还没有可展示的会话摘要。"
: "No recent session summary is available yet."}
</p>
)}
</div>
</div>
<div className="rounded-[1rem] border border-slate-200 bg-slate-50/90 p-4">
<div className="flex items-center gap-2 text-sm font-semibold text-slate-700">
<Cloud className="h-4 w-4 text-slate-500" />
{isChinese ? "可用代理" : "Available Agents"}
</div>
<div className="mt-3 flex flex-wrap gap-2">
{model.featuredAgents.length > 0 ? (
model.featuredAgents.map((agent) => (
<div
key={agent.id}
className="inline-flex items-center gap-2 rounded-full border border-white bg-white/95 px-3 py-2 text-sm font-medium text-slate-700"
>
<Sparkles className="h-3.5 w-3.5 text-lime-600" />
{agent.name}
</div>
))
) : (
<p className="text-sm leading-6 text-slate-500">
{isChinese
? "未从 Gateway 拉到代理摘要,先使用默认助手。"
: "No agent summary was returned yet, so the default assistant remains available."}
</p>
)}
</div>
</div>
</div> </div>
</div> </div>
</div>
<div className="rounded-[1.35rem] border border-sky-200/70 bg-[linear-gradient(180deg,rgba(255,255,255,0.96),rgba(241,247,255,0.95))] p-4 shadow-[0_22px_45px_rgba(96,165,250,0.14)]"> {/* Alerts Card */}
<div className="rounded-[1.1rem] border border-white/80 bg-white/95 p-4 shadow-[inset_0_0_0_1px_rgba(148,163,184,0.08)]"> <div className="bg-white rounded-[2rem] p-6 shadow-sm border border-gray-100 flex flex-col justify-between relative">
<textarea <div className="flex justify-between items-start mb-4">
value={prompt} <div className="text-sm font-semibold text-gray-800">Alerts</div>
onChange={(event) => setPrompt(event.target.value)} <Bell className="text-gray-400 w-5 h-5" />
onKeyDown={(event) => { </div>
if ((event.metaKey || event.ctrlKey) && event.key === "Enter") { <div>
event.preventDefault(); <div className="flex items-baseline gap-2">
void handleSend(); <span className="text-4xl font-bold text-gray-300 tracking-tight">
} 0
}} </span>
placeholder={ <span className="text-xs font-semibold text-gray-400 tracking-wider">
isChinese ? "有什么想问的?" : "What would you like to ask?" CRITICAL
} </span>
className="min-h-[150px] w-full resize-none bg-transparent text-[1.05rem] leading-8 text-slate-700 outline-none placeholder:text-slate-400" </div>
/> <div className="text-xs text-gray-400 mt-3 font-medium">
<div className="mt-4 flex items-center justify-between gap-3 border-t border-slate-100 pt-3"> Last check: 2 mins ago
<div className="text-xs leading-5 text-slate-500"> </div>
{gatewayConfigured </div>
? isChinese </div>
? "首页会直接通过 Gateway 发起首轮对话。" </div>
: "The homepage sends the first live turn through the gateway."
: isChinese {/* Central Prompt Input Bar */}
? "当前未配置 Gateway可先跳到 XWorkmate。" <div className="w-full max-w-4xl bg-white rounded-[2rem] shadow-[0_8px_30px_rgb(0,0,0,0.04)] border border-gray-100 p-2 flex items-center mb-10 transition-shadow focus-within:shadow-[0_8px_30px_rgb(0,0,0,0.08)]">
: "Gateway is not configured yet. You can continue in XWorkmate."} <div className="pl-4 pr-2 text-gray-400">
</div> <Search className="w-6 h-6" />
<button </div>
type="button" <input
onClick={() => void handleSend()} type="text"
disabled={!prompt.trim() || sendState.isSending || !gatewayConfigured} className="flex-1 bg-transparent border-none outline-none text-lg text-gray-700 placeholder:text-gray-400 px-2 py-3"
className="tactile-button tactile-button-primary min-h-10 px-4 text-sm disabled:cursor-not-allowed disabled:opacity-60" placeholder={
> isChinese ? "有什么想问的?" : "What would you like to ask?"
<Send className="h-4 w-4" /> }
{sendState.isSending value={prompt}
? isChinese onChange={(e) => setPrompt(e.target.value)}
? "发送中" onKeyDown={(e) => {
: "Sending" if (e.key === "Enter") {
: isChinese e.preventDefault();
? "发送" handleInputSubmit();
: "Send"} }
</button> }}
/>
<button
className="w-12 h-12 bg-blue-500 hover:bg-blue-600 transition-colors rounded-full flex items-center justify-center text-white shadow-md disabled:opacity-50 ml-2"
onClick={handleInputSubmit}
disabled={!prompt.trim()}
>
<ArrowRight className="w-6 h-6" />
</button>
</div>
{/* Quick Access */}
<div className="flex flex-col items-center mb-12">
<div className="text-xs font-semibold tracking-widest text-gray-400 mb-4 uppercase">
Quick Access
</div>
<div className="flex gap-3">
<button className="flex items-center gap-2 bg-blue-100 hover:bg-blue-200 text-blue-600 px-5 py-2.5 rounded-full text-sm font-medium transition-colors">
<Wrench className="w-4 h-4" />
{isChinese ? "常用工具" : "Common Tools"}
</button>
<button className="flex items-center gap-2 bg-white hover:bg-gray-50 text-gray-600 px-5 py-2.5 rounded-full text-sm font-medium border border-gray-100 shadow-sm transition-colors">
<History className="w-4 h-4" />
{isChinese ? "最近使用" : "Recent"}
</button>
</div>
</div>
{/* 2 Bottom Cards */}
<div className="w-full grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Network Load Card */}
<div className="bg-[#f5f5f7] rounded-[2rem] p-8 shadow-sm border border-gray-100 overflow-hidden relative min-h-[220px]">
<h3 className="text-xl font-bold text-gray-900 mb-2">
Network Load
</h3>
<p className="text-sm text-gray-500">
Real-time traffic distribution across global nodes.
</p>
{/* Bar Chart Mockup */}
<div className="absolute bottom-0 left-0 right-0 flex items-end justify-between px-6 gap-2 h-32">
<div className="w-full bg-[#d0e1fd] rounded-t-full h-[30%]"></div>
<div className="w-full bg-[#d0e1fd] rounded-t-full h-[50%]"></div>
<div className="w-full bg-[#d0e1fd] rounded-t-full h-[65%]"></div>
<div className="w-full bg-[#abc5fa] rounded-t-full h-[90%]"></div>
<div className="w-full bg-[#d0e1fd] rounded-t-full h-[45%]"></div>
<div className="w-full bg-[#d0e1fd] rounded-t-full h-[35%]"></div>
<div className="w-full bg-[#d0e1fd] rounded-t-full h-[55%]"></div>
</div>
</div>
{/* Global Mesh Card */}
<div className="bg-[#111] rounded-[2rem] p-8 shadow-lg overflow-hidden relative min-h-[220px] text-white">
{/* Background globe glow effect mockup */}
<div className="absolute -right-10 -bottom-10 w-72 h-72 bg-blue-900/30 rounded-full blur-3xl"></div>
<div className="absolute right-0 top-0 w-64 h-64 bg-emerald-900/20 rounded-full blur-3xl"></div>
<div className="relative z-10 flex flex-col h-full justify-between">
<div>
<div className="inline-block bg-blue-500 text-white text-[10px] font-bold px-3 py-1 rounded-full tracking-wider mb-4 uppercase">
Active Deployment
</div> </div>
<h3 className="text-2xl font-bold text-white">Global Mesh</h3>
</div> </div>
<div className="mt-4 rounded-[1rem] border border-slate-200 bg-white/92 p-4"> <div className="flex justify-between items-end mt-12">
<div className="flex items-center justify-between gap-3"> <div>
<div className="text-sm font-semibold text-slate-700"> <div className="text-[10px] font-bold tracking-wider text-gray-400 mb-1 uppercase">
{isChinese ? "快速入口" : "Quick Access"} Latency
</div> </div>
<button <div className="text-xl font-bold">24ms</div>
type="button"
onClick={openWorkspace}
className="inline-flex items-center gap-1 text-sm font-semibold text-slate-500 transition hover:text-slate-900"
>
{isChinese ? "继续到工作台" : "Continue in workspace"}
<ArrowRight className="h-4 w-4" />
</button>
</div>
<div className="mt-3 flex flex-wrap gap-2">
{model.quickPrompts.map((item) => (
<button
type="button"
key={item}
onClick={() => setPrompt(item)}
className="rounded-full border border-slate-200 bg-white px-3 py-2 text-sm text-slate-700 transition hover:border-lime-300 hover:bg-lime-50"
>
{item}
</button>
))}
</div>
</div>
<div className="mt-4 rounded-[1rem] border border-slate-200 bg-white/92 p-4">
<div className="text-sm font-semibold text-slate-700">
{isChinese ? "首轮演示结果" : "First Turn Demo"}
</div>
<div className="mt-3 min-h-[120px] rounded-[0.95rem] border border-dashed border-slate-200 bg-slate-50/75 p-4 text-sm leading-7 text-slate-600">
{sendState.errorMessage ? (
<p className="text-amber-700">{sendState.errorMessage}</p>
) : responseText ? (
<p>{responseText}</p>
) : bootstrapLoading ? (
<p>{isChinese ? "正在同步 Gateway 状态…" : "Syncing gateway state…"}</p>
) : (
<p>
{isChinese
? "发送一个 prompt这里会显示首页首轮真实响应。"
: "Send a prompt to show the first live response directly on the homepage."}
</p>
)}
</div> </div>
<Globe className="w-6 h-6 text-gray-300" />
</div> </div>
</div> </div>
</div> </div>

5
start-dev.js Normal file
View File

@ -0,0 +1,5 @@
const { exec } = require("child_process");
const process = exec("npx next dev --turbo -p 3000");
process.stdout.on("data", (data) => console.log(data));
process.stderr.on("data", (data) => console.error(data));