mirror of
https://github.com/Narcooo/inkos.git
synced 2026-05-06 21:43:26 +08:00
fix(studio): unify visible copy across core views
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user