console: 按 Muse 网页端/桌面端视觉重做
仅视觉与布局改动,功能与 API 未改:全屏四栏(56px 图标栏 + 260px 列表 + 弹性对话 + 300px 右侧栏),左侧搜索 + 主要聊天 列表,中间灰/浅蓝气泡对话 + 圆角输入条,右侧头像 + 已连接 (真实 key/健康状态)+ 今天任务清单,下方仍可展开原详情。 旧 v3 松绿/朱印与顶部大标题栏已移除,产物为单文件 (316226 字节,sha f8fea987…,本地重建字节一致)。 console 标记单测未动,本地 53 项通过。
This commit is contained in:
1 parent
5ca5cc91cc
commit
66790b5123
12 files changed
+394
-227
No files matched your search
@@ -19,7 +19,7 @@ export default function ConversationSection() {
|
||||
return (
|
||||
<div className="conversation">
|
||||
<TracePanel />
|
||||
<h3>继续会话</h3>
|
||||
<div className="conv-divider" />
|
||||
<ContinuePanel />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { useConsole } from "../context";
|
||||
import { ModuleIcon, type View } from "../components/IconRail";
|
||||
import { shortId } from "../api/client";
|
||||
import TaskDetailPanel from "./TaskDetailPanel";
|
||||
import MembersPanel from "./MembersPanel";
|
||||
import EnvContextPanel from "./EnvContextPanel";
|
||||
import SkillsToolsPanel from "./SkillsToolsPanel";
|
||||
import HealthPanel from "./HealthPanel";
|
||||
import AutomationsSection from "./AutomationsSection";
|
||||
import TriageSection from "./TriageSection";
|
||||
|
||||
// Right panel, modelled on the Muse screenshot: profile header with a
|
||||
// real connection status (API key / health, never a fake user), an icon
|
||||
// row that switches modules, a 今天 list of recent tasks, and below it
|
||||
// the selected task's detail (or the current module's context) as a
|
||||
// collapsible section. Nothing that was in the old right column is
|
||||
// dropped: the detail/context panels are the same components, only
|
||||
// rendered inside <details> under the today list.
|
||||
export default function RightPanel({ view, onSelect, onClose }: {
|
||||
view: View; onSelect: (v: View) => void; onClose: () => void;
|
||||
}) {
|
||||
const ctx = useConsole();
|
||||
const connected = ctx.keySaved;
|
||||
const today = ctx.tasks.slice().reverse().slice(0, 10);
|
||||
const context = (() => {
|
||||
switch (view) {
|
||||
case "projects": return <MembersPanel />;
|
||||
case "env": return <EnvContextPanel />;
|
||||
case "skills": return <SkillsToolsPanel />;
|
||||
case "workers": case "admin": case "key": return <HealthPanel />;
|
||||
case "automations": return <TriageSection />;
|
||||
case "triage": return <AutomationsSection />;
|
||||
default: return <TaskDetailPanel />;
|
||||
}
|
||||
})();
|
||||
const iconRow: Array<[View, string]> = [["task", "任务"], ["admin", "管理区"], ["automations", "自动化"], ["key", "API Key"]];
|
||||
return (
|
||||
<div>
|
||||
<div className="right-profile">
|
||||
<button type="button" className="icon-btn" aria-label="关闭右侧栏" onClick={onClose}>×</button>
|
||||
<div className="right-avatar" aria-hidden="true">C</div>
|
||||
<div className="right-name">Codex 命令台</div>
|
||||
<div className={connected ? "right-status" : "right-status off"}>
|
||||
{connected ? "● 已连接" : "○ 未连接(先在 API Key 里保存 key)"}
|
||||
</div>
|
||||
<div className="muted">{ctx.healthText}</div>
|
||||
<div className="muted">{ctx.quotaText}</div>
|
||||
<div className="muted">自动刷新 · 每 10 秒 · 已刷新 {ctx.refreshStamp}</div>
|
||||
</div>
|
||||
<div className="right-icon-row" aria-label="快捷方式">
|
||||
{iconRow.map(([v, label]) => (
|
||||
<button key={v} type="button" title={label} aria-label={label} className={view === v ? "active" : ""} onClick={() => onSelect(v)}>
|
||||
<ModuleIcon view={v} />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div>
|
||||
<div className="list-section-head"><span>今天</span></div>
|
||||
{today.length === 0 ? (
|
||||
<div className="muted">今天暂无任务</div>
|
||||
) : (
|
||||
<ul className="today-list">
|
||||
{today.map((t) => {
|
||||
const done = t.status === "completed" || t.status === "merged";
|
||||
const time = t.updated_at ? new Date(t.updated_at * 1000).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "";
|
||||
return (
|
||||
<li key={t.id} className="today-item" onClick={() => { ctx.showDetail(t.id); onSelect("task"); }}>
|
||||
<span className={"today-check" + (done ? " done" : "")}>{done ? "✓" : ""}</span>
|
||||
<span className="today-item-main">
|
||||
<span className="today-item-title">{String(t.prompt || shortId(t.id)).slice(0, 60) || shortId(t.id)}</span>
|
||||
<span className="today-item-sub">{shortId(t.id)} · <span className={"st-" + t.status}>{t.status}</span> · {t.executor || ""}</span>
|
||||
</span>
|
||||
<span className="today-item-time">{time}</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
<details className="right-detail" open>
|
||||
<summary>{view === "task" ? "任务详情" : "模块详情"}</summary>
|
||||
{context}
|
||||
</details>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +1,28 @@
|
||||
import { useConsole } from "../context";
|
||||
import { DataListRaw } from "../components/ui";
|
||||
import { shortId } from "../api/client";
|
||||
|
||||
// Left-rail compact task list. The live 9-column table does not fit a
|
||||
// 300px rail, so the rail shows id + status + prompt + executor; the
|
||||
// full metadata (turn, root turn, host, PR, updated) lives in the right
|
||||
// detail panel for the selected task. Filters/search are the live ones.
|
||||
export default function TaskListSection() {
|
||||
// Left list panel, Muse screenshot style: rounded search on top, a
|
||||
// 主要聊天 section header with a + button, then the task list. The
|
||||
// search input IS the existing task search (id/prompt); the status and
|
||||
// executor filters are kept as a compact row below it — the screenshot
|
||||
// has no filters, but dropping them would cut functionality, so they
|
||||
// stay, just compact. List rows show the prompt truncated on one line
|
||||
// with a status dot, short id, status and executor below it; the
|
||||
// selected row is highlighted grey, as in the screenshot.
|
||||
export default function TaskListSection({ onNew }: { onNew: () => void }) {
|
||||
const ctx = useConsole();
|
||||
return (
|
||||
<div className="left-tasks">
|
||||
<div>
|
||||
<div className="search-box">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" aria-hidden="true">
|
||||
<circle cx="11" cy="11" r="7" /><path d="M21 21l-4.3-4.3" />
|
||||
</svg>
|
||||
<input type="text" placeholder="搜索" aria-label="搜索任务" value={ctx.searchText} onChange={(e) => ctx.setSearchText(e.target.value)} />
|
||||
</div>
|
||||
<div className="list-section-head">
|
||||
<span>主要聊天</span>
|
||||
<button type="button" aria-label="派发新任务" title="派发新任务" onClick={onNew}>+</button>
|
||||
</div>
|
||||
<div className="bar">
|
||||
<select aria-label="状态筛选" value={ctx.statusFilter} onChange={(e) => ctx.setStatusFilter(e.target.value)}>
|
||||
<option value="">状态:全部</option>
|
||||
@@ -19,7 +32,6 @@ export default function TaskListSection() {
|
||||
<option value="">执行器:全部</option><option value="local">local</option><option value="docker">docker</option><option value="cloud">cloud</option>
|
||||
</select>
|
||||
</div>
|
||||
<input type="text" placeholder="搜索任务 id / prompt" value={ctx.searchText} onChange={(e) => ctx.setSearchText(e.target.value)} />
|
||||
<div className="bar">
|
||||
<button onClick={ctx.refreshTasks}>刷新</button>
|
||||
<span className="muted">{ctx.taskCountText}</span>
|
||||
@@ -28,18 +40,21 @@ export default function TaskListSection() {
|
||||
: !ctx.projectId ? <div className="muted">先选项目</div>
|
||||
: ctx.filteredTasks.length === 0 ? <div className="muted">{ctx.tasks.length ? "没有符合筛选条件的任务" : "该项目暂无任务"}</div>
|
||||
: (
|
||||
<DataListRaw>
|
||||
<ul className="muse-list">
|
||||
{ctx.filteredTasks.slice().reverse().map((t) => (
|
||||
<li key={t.id} className={ctx.currentTask?.id === t.id ? "selected-task" : ""} onClick={() => ctx.showDetail(t.id)} style={{ cursor: "pointer" }}>
|
||||
<span className="row-main">
|
||||
<b>{shortId(t.id)}</b> <span className={"st-" + t.status}>{t.status}</span>
|
||||
<br />
|
||||
{t.plan ? "[计划] " : ""}{t.forked_from ? "[分支] " : ""}{t.coordinator_parts ? "[协调] " : ""}{t.parent_task_id ? "[协调子任务] " : ""}{String(t.prompt || "").slice(0, 60)}
|
||||
<br /><span className="muted">{t.executor || ""}</span>
|
||||
</span>
|
||||
<li key={t.id} className={"muse-item" + (ctx.currentTask?.id === t.id ? " selected" : "")} onClick={() => ctx.showDetail(t.id)}>
|
||||
<div className="muse-item-title">
|
||||
{t.plan ? "[计划] " : ""}{t.forked_from ? "[分支] " : ""}{t.coordinator_parts ? "[协调] " : ""}{t.parent_task_id ? "[协调子任务] " : ""}{String(t.prompt || shortId(t.id)).slice(0, 60)}
|
||||
</div>
|
||||
<div className="muse-item-sub">
|
||||
<span className={"task-dot st-" + t.status} />
|
||||
<span>{shortId(t.id)}</span>
|
||||
<span className={"st-" + t.status}>{t.status}</span>
|
||||
<span>{t.executor || ""}</span>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</DataListRaw>
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -38,7 +38,7 @@ export default function ContinuePanel() {
|
||||
<ChatList messages={messages} empty="(暂无会话消息)" />
|
||||
<ChatInput
|
||||
disabled={!canSend}
|
||||
placeholder={hint + ",回车发送,Shift+回车换行"}
|
||||
placeholder="消息"
|
||||
onSend={(text) => void ctx.sendChatMessage(text)}
|
||||
/>
|
||||
<div className="muted">{hint}。{ctx.chatMsg}</div>
|
||||
|
||||
@@ -16,7 +16,7 @@ export default function TracePanel() {
|
||||
}, [ctx.currentTask, ctx.traceEntries, ctx.traceKind, ctx.traceDone]);
|
||||
return (
|
||||
<>
|
||||
<h3>实时链路</h3>
|
||||
<div className="muted">实时链路</div>
|
||||
<Bar>
|
||||
<select aria-label="链路类型" value={ctx.traceKind} onChange={(e) => ctx.setTraceKind(e.target.value)}>
|
||||
<option value="">链路类型:全部</option>
|
||||
|
||||
Reference in new issue
Block a user