Files
codex-app-server/cloud/console-react/src/App.tsx
T
host 08484302ac console: 项目导航移到左侧栏,工作台不再承担项目模块
左侧栏 ProjectNav:项目完整列表(名 + 短 id + 我的角色,
选中高亮,点击切换)+ 创建项目(名/GitHub 仓库/repo_url)
+ 选中项目成员(邀请/改角色/新增移除);工作台删除
sec-projects 一节与任务列表里的项目下拉框,侧栏为唯一
项目选择器,自动选第一个项目逻辑不变。
薄模块补齐(均为已核实后端路由):成员移除 DELETE、
自动化详情(GET 单条 + 产出)与编辑 PATCH、分诊条目点进
任务/自动化详情、Workers 搜索与在线筛选、管理区设置配额
POST。主线新功能(Todos、Browser、taskMeta 等)原样保留,
未做任何回退。console 标记单测本地 53 项通过。
2026-10-30 07:43:54 +00:00

155 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState } from "react";
import { ConsoleContext, useConsole } from "./context";
import { useConsoleStore } from "./hooks/useConsoleStore";
import { useAutoRefresh, useIntervalMs } from "./hooks/useAutoRefresh";
import DataCards from "./sections/DataCards";
import DispatchSection from "./sections/DispatchSection";
import TaskListSection from "./sections/TaskListSection";
import ConversationSection from "./sections/ConversationSection";
import TaskDetailPanel from "./sections/TaskDetailPanel";
import ApiKeySection from "./sections/ApiKeySection";
import AdminSection from "./sections/AdminSection";
import ProjectNav from "./sections/ProjectNav";
import EnvSection from "./sections/EnvSection";
import EnvContextPanel from "./sections/EnvContextPanel";
import WorkersSection from "./sections/WorkersSection";
import HealthPanel from "./sections/HealthPanel";
import SkillsSection from "./sections/SkillsSection";
import SkillsToolsPanel from "./sections/SkillsToolsPanel";
import AutomationsSection from "./sections/AutomationsSection";
import TriageSection from "./sections/TriageSection";
import TodosSection from "./sections/TodosSection";
// Redesign (2026-10-30): the old layout is gone — no top tab bar, no
// data-cards row on top of a long stacked page, no detail stack
// hanging under the conversation. The new shell is an IDE-style
// workspace that never scrolls as one page:
// 左侧栏:导航(按工作流分组)+ 项目模块(ProjectNav:项目列表 +
// 创建项目 + 成员,id nav-projects,是全站唯一的项目选择器;
// 因此不再有独立的“项目与成员”视图,旧 ProjectsSection 已删,
// 工作台也不再带项目选择器)+ 数据卡片(紧凑版,始终可见)
// 任务工作台:左 = 任务列表轨(搜索/筛选/分页),中 = 派发 + 对话,
// 右 = 任务详情检查器(概览/协作/评审与审批/线程四个内页签,
// 见 TaskDetailPanel)
// 其余功能各成独立页面(项目 / 资源 / 自动化 / 系统),从左侧栏
// 切入,页面内只放该组功能,不再把 11 个模块堆在同一页。
// Selected task survives view switches: the store lives in App,
// above the views, so currentTask / detail / chat state is not
// unmounted with a view. The view is synced to location.hash
// (#view-tasks etc.) so a reload or a shared link lands on the
// same view; unknown hashes fall back to the task workspace.
type ViewId = "tasks" | "todos" | "resources" | "automation" | "admin";
const VIEWS: Array<[ViewId, string, string]> = [
["tasks", "任务工作台", "任务"],
["todos", "项目待办", "任务"],
["resources", "环境与 Workers", "管理"],
["automation", "技能与自动化", "管理"],
["admin", "系统与 Key", "管理"],
];
const VIEW_TITLES: Record<ViewId, string> = Object.fromEntries(
VIEWS.map(([id, title]) => [id, title]),
) as Record<ViewId, string>;
function viewFromHash(): ViewId {
const h = window.location.hash.replace("#view-", "");
return (VIEWS.some(([id]) => id === h) ? h : "tasks") as ViewId;
}
function SideNav({ view, setView }: { view: ViewId; setView: (v: ViewId) => void }) {
return (
<aside className="shell-side">
<div className="shell-brand">Codex 命令台</div>
<nav className="shell-nav" aria-label="导航">
{["任务", "管理"].map((group) => (
<div key={group} className="shell-nav-group">
<div className="shell-nav-group-label">{group}</div>
{VIEWS.filter(([, , g]) => g === group).map(([id, label]) => (
<button key={id} type="button" className={view === id ? "active" : ""} onClick={() => setView(id)}>{label}</button>
))}
</div>
))}
</nav>
<div id="nav-projects" className="shell-project-nav">
<ProjectNav />
</div>
<div className="shell-stats">
<div className="shell-stats-label">概览</div>
<DataCards />
</div>
</aside>
);
}
function TopStatus() {
const ctx = useConsole();
return <span className={ctx.keySaved ? "dash-status ok" : "dash-status off"}>{ctx.keySaved ? "● 已连接" : "○ 未连接"}</span>;
}
export default function App() {
const store = useConsoleStore();
useAutoRefresh((selectFocused) => store.refreshAll(selectFocused), 10000);
useIntervalMs(() => store.refreshSlow(), 60000);
const [view, setViewState] = useState<ViewId>(() => viewFromHash());
const [composerOpen, setComposerOpen] = useState(true);
const setView = (v: ViewId) => {
setViewState(v);
try { window.history.replaceState(null, "", "#view-" + v); } catch { /* noop */ }
};
useEffect(() => {
const onHash = () => setViewState(viewFromHash());
window.addEventListener("hashchange", onHash);
return () => window.removeEventListener("hashchange", onHash);
}, []);
const scrollToDispatch = () => {
setView("tasks");
setComposerOpen(true);
setTimeout(() => document.getElementById("sec-dispatch")?.scrollIntoView({ behavior: "smooth" }), 0);
};
return (
<ConsoleContext.Provider value={store}>
<div className="shell">
<SideNav view={view} setView={setView} />
<div className="shell-main">
<header className="shell-header">
<h1>{VIEW_TITLES[view]}</h1>
<TopStatus />
<span className="muted shell-hide-sm">{store.healthText}</span>
<span className="muted shell-hide-sm">{store.quotaText}</span>
<span className="muted shell-stamp">自动刷新 · 每 10 秒 · 已刷新 {store.refreshStamp}</span>
</header>
{view === "tasks" ? (
<div className="workspace">
<section className="workspace-rail" aria-label="任务列表">
<h2>任务列表</h2>
<TaskListSection onNew={scrollToDispatch} />
</section>
<section className="workspace-center">
<div className="composer-toggle">
<button type="button" onClick={() => setComposerOpen(!composerOpen)}>
{composerOpen ? "收起新建任务" : "新建任务"}
</button>
</div>
{composerOpen ? <section id="sec-dispatch"><DispatchSection /></section> : null}
<section className="card workspace-conv">
<h2>对话</h2>
<ConversationSection />
</section>
</section>
<section className="workspace-inspector" aria-label="任务详情">
<TaskDetailPanel />
</section>
</div>
) : (
<main className="page">
{view === "todos" ? (<><TodosSection /></>)
: view === "resources" ? (<><EnvSection /><EnvContextPanel /><WorkersSection /><HealthPanel /></>)
: view === "automation" ? (<><SkillsSection /><SkillsToolsPanel /><AutomationsSection /><TriageSection /></>)
: (<><AdminSection /><ApiKeySection /></>)}
</main>
)}
</div>
</div>
</ConsoleContext.Provider>
);
}