| import type { ReactNode } from "react"; |
| import type { UiStatus, ViewId } from "./types"; |
|
|
| export function Icon({ name, size = 14 }: { name: string; size?: number }) { |
| const paths: Record<string, ReactNode> = { |
| search: <><circle cx="7" cy="7" r="5" /><path d="M11 11l3 3" /></>, |
| settings: <><circle cx="8" cy="8" r="2.5" /><path d="M8 1v2M8 13v2M1 8h2M13 8h2M3.5 3.5l1.4 1.4M11.1 11.1l1.4 1.4M3.5 12.5l1.4-1.4M11.1 4.9l1.4-1.4" /></>, |
| play: <path d="M4 3l9 5-9 5z" fill="currentColor" />, |
| stop: <rect x="4" y="4" width="8" height="8" rx="1.5" fill="currentColor" />, |
| upload: <><path d="M8 11V3M5 6l3-3 3 3" /><path d="M2 11v2h12v-2" /></>, |
| download: <><path d="M8 3v8M5 8l3 3 3-3" /><path d="M2 13h12" /></>, |
| plus: <path d="M8 3v10M3 8h10" />, |
| close: <path d="M3 3l10 10M13 3L3 13" />, |
| retry: <><path d="M2 8a6 6 0 1 1 1.8 4.3" /><path d="M2 13v-3h3" /></>, |
| trash: <><path d="M3 4h10" /><path d="M6 4V2.5h4V4" /><path d="M5 6l.5 7h5L11 6" /><path d="M7 7.5v4M9 7.5v4" /></>, |
| file: <><path d="M3 2h6l4 4v8H3z" /><path d="M9 2v4h4" /></>, |
| check: <path d="M3 8l3 3 7-7" />, |
| diff: <><path d="M5 1v10M5 1l-2 2M5 1l2 2" /><path d="M11 15V5M11 15l-2-2M11 15l2-2" /></>, |
| }; |
| return ( |
| <svg width={size} height={size} viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"> |
| {paths[name] || null} |
| </svg> |
| ); |
| } |
|
|
| export function StatusPill({ state }: { state: UiStatus }) { |
| const map: Record<UiStatus, string> = { |
| idle: "Idle", |
| queued: "Queued", |
| running: "Running", |
| done: "Done", |
| failed: "Failed", |
| stopped: "Stopped", |
| }; |
| return ( |
| <span className={`pill ${state}`}> |
| <span className="pill-dot"></span> |
| {map[state]} |
| </span> |
| ); |
| } |
|
|
| export function TopBar({ |
| view, |
| onView, |
| onSettings, |
| }: { |
| view: ViewId; |
| onView: (view: ViewId) => void; |
| onSettings: () => void; |
| }) { |
| const tabs: Array<{ id: ViewId; num: string; label: string }> = [ |
| { id: "polish", num: "01", label: "任务润色" }, |
| { id: "api", num: "02", label: "API 分组" }, |
| { id: "mcp", num: "03", label: "MCP 分组" }, |
| ]; |
| return ( |
| <header className="topbar"> |
| <div className="topbar-brand"> |
| <div className="brand-mark">T</div> |
| <div className="brand-title">Task Polisher</div> |
| <div className="brand-sub">local</div> |
| </div> |
| <nav className="view-tabs" aria-label="视图"> |
| {tabs.map((tab) => ( |
| <button key={tab.id} className={`view-tab ${view === tab.id ? "is-active" : ""}`} onClick={() => onView(tab.id)}> |
| <span className="view-tab-num">{tab.num}</span> |
| <span>{tab.label}</span> |
| </button> |
| ))} |
| </nav> |
| <div className="topbar-right"> |
| <button className="btn btn-ghost btn-sm" onClick={onSettings}> |
| <Icon name="settings" size={12} /> 设置 |
| </button> |
| </div> |
| </header> |
| ); |
| } |
|
|
| export function StatusBar({ |
| view, |
| total, |
| done, |
| failed, |
| model, |
| concurrency, |
| message, |
| }: { |
| view: ViewId; |
| total: number; |
| done: number; |
| failed: number; |
| model: string; |
| concurrency: number; |
| message: string; |
| }) { |
| return ( |
| <footer className="statusbar"> |
| <span className="statusbar-item"> |
| <span className={`status-dot ${failed ? "warn" : "idle"}`}></span> |
| <strong>{message || "idle"}</strong> |
| </span> |
| <span className="statusbar-item">view <strong>{view}</strong></span> |
| <span className="statusbar-item">queue <strong>{done}/{total}</strong></span> |
| {failed > 0 && <span className="statusbar-item"><span className="status-dot err"></span>failed <strong>{failed}</strong></span>} |
| <span className="statusbar-spacer"></span> |
| <span className="statusbar-item">model <strong>{model}</strong></span> |
| <span className="statusbar-item">conc <strong>{concurrency}</strong></span> |
| <span className="statusbar-item">storage <strong>local</strong></span> |
| </footer> |
| ); |
| } |
|
|