fix(studio): unify visible copy across core views

This commit is contained in:
Ma
2026-03-30 12:42:01 +08:00
parent 67197d322e
commit 3e27c5b074
10 changed files with 56 additions and 31 deletions

View File

@@ -108,7 +108,7 @@ export function Sidebar({ nav, activePage, t, sse }: {
onClick={nav.toConfig}
/>
<SidebarItem
label="Daemon"
label={t("nav.daemon")}
icon="⟳"
active={activePage === "daemon"}
onClick={nav.toDaemon}
@@ -116,7 +116,7 @@ export function Sidebar({ nav, activePage, t, sse }: {
badgeColor={daemon?.running ? "text-emerald-500" : undefined}
/>
<SidebarItem
label="Logs"
label={t("nav.logs")}
icon="☰"
active={activePage === "logs"}
onClick={nav.toLogs}

View File

@@ -8,10 +8,21 @@ const strings = {
"nav.newBook": { zh: "新建书籍", en: "New Book" },
"nav.config": { zh: "配置", en: "Config" },
"nav.daemon": { zh: "守护进程", en: "Daemon" },
"nav.logs": { zh: "日志", en: "Logs" },
"nav.style": { zh: "风格分析", en: "Style" },
"nav.connected": { zh: "已连接", en: "Connected" },
"nav.disconnected": { zh: "未连接", en: "Disconnected" },
// Common
"common.loading": { zh: "加载中...", en: "Loading..." },
"common.error": { zh: "错误", en: "Error" },
"common.edit": { zh: "编辑", en: "Edit" },
"common.save": { zh: "保存", en: "Save" },
"common.saving": { zh: "保存中...", en: "Saving..." },
"common.cancel": { zh: "取消", en: "Cancel" },
"common.refresh": { zh: "刷新", en: "Refresh" },
"common.export": { zh: "导出", en: "Export" },
// Dashboard
"dash.title": { zh: "书籍列表", en: "Books" },
"dash.noBooks": { zh: "还没有书", en: "No books yet" },
@@ -28,6 +39,7 @@ const strings = {
"book.draftOnly": { zh: "仅草稿", en: "Draft Only" },
"book.approveAll": { zh: "全部通过", en: "Approve All" },
"book.analytics": { zh: "数据分析", en: "Analytics" },
"book.truthFiles": { zh: "事实文件", en: "Truth Files" },
"book.noChapters": { zh: "暂无章节,点击「写下一章」开始", en: 'No chapters yet. Click "Write Next" to start.' },
"book.approve": { zh: "通过", en: "Approve" },
"book.reject": { zh: "驳回", en: "Reject" },
@@ -86,6 +98,19 @@ const strings = {
"daemon.stopping": { zh: "停止中...", en: "Stopping..." },
"daemon.waitingEvents": { zh: "等待守护进程事件...", en: "Waiting for daemon events..." },
"daemon.startHint": { zh: "启动守护进程后,这里会显示事件日志。", en: "Start the daemon to see event logs here." },
"daemon.eventLog": { zh: "事件日志", en: "Event Log" },
// Logs
"logs.title": { zh: "日志", en: "Logs" },
"logs.empty": { zh: "暂无日志。执行写作、草稿或守护进程操作后,这里会出现内容。", en: "No log entries. Logs appear after running write/draft/daemon operations." },
"logs.showingRecent": { zh: "显示 inkos.log 的最近 100 条记录", en: "Showing last 100 entries from inkos.log" },
// Truth Files
"truth.title": { zh: "事实文件", en: "Truth Files" },
"truth.empty": { zh: "暂无事实文件", en: "No truth files" },
"truth.notFound": { zh: "文件不存在", en: "File not found" },
"truth.selectFile": { zh: "选择一个文件查看内容", en: "Select a file to view" },
"truth.chars": { zh: "字符", en: "chars" },
// Style
"style.title": { zh: "风格分析", en: "Style Analysis" },

View File

@@ -20,8 +20,8 @@ export function Analytics({ bookId, nav, theme, t }: { bookId: string; nav: Nav;
const c = useColors(theme);
const { data, loading, error } = useApi<AnalyticsData>(`/books/${bookId}/analytics`);
if (loading) return <div className={c.muted}>Loading...</div>;
if (error) return <div className="text-red-400">Error: {error}</div>;
if (loading) return <div className={c.muted}>{t("common.loading")}</div>;
if (error) return <div className="text-red-400">{t("common.error")}: {error}</div>;
if (!data) return null;
const statuses = Object.entries(data.statusDistribution);

View File

@@ -89,8 +89,8 @@ export function BookDetail({ bookId, nav, theme, t, sse }: { bookId: string; nav
refetch();
};
if (loading) return <div className={c.muted}>Loading...</div>;
if (error) return <div className="text-red-400">Error: {error}</div>;
if (loading) return <div className={c.muted}>{t("common.loading")}</div>;
if (error) return <div className="text-red-400">{t("common.error")}: {error}</div>;
if (!data) return null;
const { book, chapters } = data;
@@ -110,8 +110,8 @@ export function BookDetail({ bookId, nav, theme, t, sse }: { bookId: string; nav
<h1 className="text-2xl font-semibold">{book.title}</h1>
<div className={`flex gap-3 mt-1 text-sm ${c.muted}`}>
<span>{book.genre}</span>
<span>{chapters.length} chapters</span>
<span>{totalWords.toLocaleString()} words</span>
<span>{chapters.length} {t("dash.chapters")}</span>
<span>{totalWords.toLocaleString()} {t("book.words")}</span>
{book.language === "en" && <span className="text-blue-400">EN</span>}
{book.fanficMode && <span className="text-purple-400">fanfic:{book.fanficMode}</span>}
</div>
@@ -144,7 +144,7 @@ export function BookDetail({ bookId, nav, theme, t, sse }: { bookId: string; nav
onClick={() => (nav as { toTruth?: (id: string) => void }).toTruth?.(bookId)}
className={`px-4 py-2 text-sm ${c.btnSecondary} rounded-md transition-colors`}
>
Truth Files
{t("book.truthFiles")}
</button>
<button
onClick={() => nav.toAnalytics(bookId)}
@@ -157,7 +157,7 @@ export function BookDetail({ bookId, nav, theme, t, sse }: { bookId: string; nav
download
className={`px-4 py-2 text-sm ${c.btnSecondary} rounded-md transition-colors inline-flex items-center`}
>
Export
{t("common.export")}
</a>
</div>
</div>

View File

@@ -26,8 +26,8 @@ export function ChapterReader({ bookId, chapterNumber, nav, theme, t }: {
`/books/${bookId}/chapters/${chapterNumber}`,
);
if (loading) return <div className={c.muted}>Loading...</div>;
if (error) return <div className="text-red-400">Error: {error}</div>;
if (loading) return <div className={c.muted}>{t("common.loading")}</div>;
if (error) return <div className="text-red-400">{t("common.error")}: {error}</div>;
if (!data) return null;
// Split markdown content into title and body

View File

@@ -26,8 +26,8 @@ export function ConfigView({ nav, theme, t }: { nav: Nav; theme: Theme; t: TFunc
const [saving, setSaving] = useState(false);
const [form, setForm] = useState<Record<string, unknown>>({});
if (loading) return <div className="text-muted-foreground py-20 text-center text-sm">Loading...</div>;
if (error) return <div className="text-destructive py-20 text-center">Error: {error}</div>;
if (loading) return <div className="text-muted-foreground py-20 text-center text-sm">{t("common.loading")}</div>;
if (error) return <div className="text-destructive py-20 text-center">{t("common.error")}: {error}</div>;
if (!data) return null;
const startEdit = () => {
@@ -64,7 +64,7 @@ export function ConfigView({ nav, theme, t }: { nav: Nav; theme: Theme; t: TFunc
<h1 className="font-serif text-3xl">{t("config.title")}</h1>
{!editing && (
<button onClick={startEdit} className={`px-3 py-2 text-xs rounded-md ${c.btnSecondary}`}>
Edit
{t("common.edit")}
</button>
)}
</div>
@@ -121,10 +121,10 @@ export function ConfigView({ nav, theme, t }: { nav: Nav; theme: Theme; t: TFunc
{editing && (
<div className="flex gap-2 justify-end">
<button onClick={() => setEditing(false)} className={`px-4 py-2.5 text-sm rounded-md ${c.btnSecondary}`}>
Cancel
{t("common.cancel")}
</button>
<button onClick={handleSave} disabled={saving} className={`px-4 py-2.5 text-sm rounded-md ${c.btnPrimary} disabled:opacity-50`}>
{saving ? "Saving..." : "Save"}
{saving ? t("common.saving") : t("common.save")}
</button>
</div>
)}

View File

@@ -88,7 +88,7 @@ export function DaemonControl({ nav, theme, t, sse }: { nav: Nav; theme: Theme;
{/* Daemon event log */}
<div className={`border ${c.cardStatic} rounded-lg`}>
<div className="px-5 py-3.5 border-b border-border">
<span className="text-sm uppercase tracking-wide text-muted-foreground font-medium">Event Log</span>
<span className="text-sm uppercase tracking-wide text-muted-foreground font-medium">{t("daemon.eventLog")}</span>
</div>
<div className="p-4 max-h-[500px] overflow-y-auto">
{daemonEvents.length > 0 ? (

View File

@@ -36,8 +36,8 @@ export function Dashboard({ nav, sse, theme, t }: { nav: Nav; sse: { messages: R
void refetch();
}, [refetch, sse.messages]);
if (loading) return <div className="text-muted-foreground py-20 text-center text-sm">Loading...</div>;
if (error) return <div className="text-destructive py-20 text-center">Error: {error}</div>;
if (loading) return <div className="text-muted-foreground py-20 text-center text-sm">{t("common.loading")}</div>;
if (error) return <div className="text-destructive py-20 text-center">{t("common.error")}: {error}</div>;
/* ── Empty state — vertically centered in the available viewport ── */
if (!data?.books.length) {

View File

@@ -30,16 +30,16 @@ export function LogViewer({ nav, theme, t }: { nav: Nav; theme: Theme; t: TFunct
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<button onClick={nav.toDashboard} className={c.link}>{t("bread.home")}</button>
<span className="text-border">/</span>
<span className="text-foreground">Logs</span>
<span className="text-foreground">{t("logs.title")}</span>
</div>
<div className="flex items-baseline justify-between">
<h1 className="font-serif text-3xl">Logs</h1>
<h1 className="font-serif text-3xl">{t("logs.title")}</h1>
<button
onClick={() => refetch()}
className={`px-4 py-2.5 text-sm rounded-md ${c.btnSecondary}`}
>
Refresh
{t("common.refresh")}
</button>
</div>
@@ -68,14 +68,14 @@ export function LogViewer({ nav, theme, t }: { nav: Nav; theme: Theme; t: TFunct
</div>
) : (
<div className="text-muted-foreground text-sm italic py-12 text-center">
No log entries. Logs appear after running write/draft/daemon operations.
{t("logs.empty")}
</div>
)}
</div>
</div>
<p className="text-sm text-muted-foreground">
Showing last 100 entries from <code className="font-mono bg-muted/50 px-1 rounded">inkos.log</code>
{t("logs.showingRecent")}
</p>
</div>
);

View File

@@ -30,10 +30,10 @@ export function TruthFiles({ bookId, nav, theme, t }: { bookId: string; nav: Nav
<span className="text-border">/</span>
<button onClick={() => nav.toBook(bookId)} className={c.link}>{bookId}</button>
<span className="text-border">/</span>
<span className="text-foreground">Truth Files</span>
<span className="text-foreground">{t("truth.title")}</span>
</div>
<h1 className="font-serif text-3xl">Truth Files</h1>
<h1 className="font-serif text-3xl">{t("truth.title")}</h1>
<div className="grid grid-cols-[240px_1fr] gap-6">
{/* File list */}
@@ -49,11 +49,11 @@ export function TruthFiles({ bookId, nav, theme, t }: { bookId: string; nav: Nav
}`}
>
<div className="font-mono text-sm truncate">{f.name}</div>
<div className="text-xs text-muted-foreground mt-0.5">{f.size.toLocaleString()} chars</div>
<div className="text-xs text-muted-foreground mt-0.5">{f.size.toLocaleString()} {t("truth.chars")}</div>
</button>
))}
{(!data?.files || data.files.length === 0) && (
<div className="px-3 py-4 text-sm text-muted-foreground text-center">No truth files</div>
<div className="px-3 py-4 text-sm text-muted-foreground text-center">{t("truth.empty")}</div>
)}
</div>
@@ -62,9 +62,9 @@ export function TruthFiles({ bookId, nav, theme, t }: { bookId: string; nav: Nav
{selected && fileData?.content ? (
<pre className="text-sm leading-relaxed whitespace-pre-wrap font-mono text-foreground/80">{fileData.content}</pre>
) : selected && fileData?.content === null ? (
<div className="text-muted-foreground text-sm">File not found</div>
<div className="text-muted-foreground text-sm">{t("truth.notFound")}</div>
) : (
<div className="text-muted-foreground/50 text-sm italic">Select a file to view</div>
<div className="text-muted-foreground/50 text-sm italic">{t("truth.selectFile")}</div>
)}
</div>
</div>