feat: migrate Doctor page to shadcn Card/Badge/Button

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
zhixian
2026-02-17 01:51:14 +09:00
parent 8a88bd0565
commit 499e74aaf4

View File

@@ -1,14 +1,25 @@
import React, { useEffect, useMemo, useReducer, useState } from "react";
import { useEffect, useMemo, useReducer, useState } from "react";
import { api } from "../lib/api";
import { initialState, reducer } from "../lib/state";
import type { MemoryFile, SessionFile } from "../lib/types";
import {
Card,
CardHeader,
CardTitle,
CardContent,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
function formatBytes(bytes: number) {
if (bytes <= 0) return "0 B";
const units = ["B", "KB", "MB", "GB"];
let index = 0;
let value = bytes;
while (value >= 1024 && index < units.length - 1) { value /= 1024; index += 1; }
while (value >= 1024 && index < units.length - 1) {
value /= 1024;
index += 1;
}
return `${value.toFixed(1)} ${units[index]}`;
}
@@ -20,7 +31,9 @@ export function Doctor() {
const hasReport = Boolean(state.doctor);
const autoFixable = hasReport
? state.doctor!.issues.filter((issue) => issue.autoFixable).map((issue) => issue.id)
? state.doctor!.issues
.filter((issue) => issue.autoFixable)
.map((issue) => issue.id)
: [];
const agents = useMemo(() => {
@@ -48,37 +61,62 @@ export function Doctor() {
);
function refreshData() {
api.listMemoryFiles().then(setMemoryFiles).catch(() => setDataMessage("Failed to load memory files"));
api.listSessionFiles().then(setSessionFiles).catch(() => setDataMessage("Failed to load session files"));
api
.listMemoryFiles()
.then(setMemoryFiles)
.catch(() => setDataMessage("Failed to load memory files"));
api
.listSessionFiles()
.then(setSessionFiles)
.catch(() => setDataMessage("Failed to load session files"));
}
useEffect(() => {
api
.runDoctor()
.then((report) => dispatch({ type: "setDoctor", doctor: report }))
.catch(() => dispatch({ type: "setMessage", message: "Failed to run doctor" }));
.catch(() =>
dispatch({ type: "setMessage", message: "Failed to run doctor" }),
);
refreshData();
}, []);
return (
<section>
<h2>Doctor</h2>
<h2 className="text-2xl font-bold text-text-main mb-4">Doctor</h2>
{/* ── Config Diagnostics ── */}
{/* Config Diagnostics */}
{state.doctor && (
<div>
<p>Health score: {state.doctor.score}</p>
<ul>
<p className="text-sm text-text-main/70 mb-3">
Health score: {state.doctor.score}
</p>
<div className="space-y-2">
{state.doctor.issues.map((issue) => (
<li key={issue.id}>
{issue.severity.toUpperCase()} {issue.message}
<div
key={issue.id}
className="flex items-center gap-2 text-sm"
>
{issue.severity === "error" && (
<Badge variant="destructive">ERROR</Badge>
)}
{issue.severity === "warn" && (
<Badge variant="secondary">WARN</Badge>
)}
{issue.severity === "info" && (
<Badge variant="outline">INFO</Badge>
)}
<span>{issue.message}</span>
{issue.autoFixable && (
<button
<Button
size="sm"
onClick={() => {
api
.fixIssues([issue.id])
.then(() => api.runDoctor())
.then((report) => dispatch({ type: "setDoctor", doctor: report }))
.then((report) =>
dispatch({ type: "setDoctor", doctor: report }),
)
.catch(() =>
dispatch({
type: "setMessage",
@@ -88,105 +126,160 @@ export function Doctor() {
}}
>
fix
</button>
</Button>
)}
</li>
</div>
))}
</ul>
<button
onClick={() => {
api
.fixIssues(autoFixable)
.then(() => api.runDoctor())
.then((report) => dispatch({ type: "setDoctor", doctor: report }))
.catch(() =>
dispatch({
type: "setMessage",
message: "Failed to fix all issues",
}),
);
}}
disabled={!autoFixable.length}
>
Fix all auto issues
</button>
<button
onClick={() =>
api
.runDoctor()
.then((report) => dispatch({ type: "setDoctor", doctor: report }))
.catch(() => dispatch({ type: "setMessage", message: "Refresh failed" }))
}
>
Refresh
</button>
</div>
<div className="flex gap-2 mt-3">
<Button
variant="outline"
onClick={() => {
api
.fixIssues(autoFixable)
.then(() => api.runDoctor())
.then((report) =>
dispatch({ type: "setDoctor", doctor: report }),
)
.catch(() =>
dispatch({
type: "setMessage",
message: "Failed to fix all issues",
}),
);
}}
disabled={!autoFixable.length}
>
Fix all auto issues
</Button>
<Button
variant="outline"
onClick={() =>
api
.runDoctor()
.then((report) =>
dispatch({ type: "setDoctor", doctor: report }),
)
.catch(() =>
dispatch({
type: "setMessage",
message: "Refresh failed",
}),
)
}
>
Refresh
</Button>
</div>
</div>
)}
{!hasReport ? <button onClick={() => api.runDoctor().then((report) => dispatch({ type: "setDoctor", doctor: report }))}>Run Doctor</button> : null}
<p>{state.message}</p>
{!hasReport ? (
<Button
onClick={() =>
api
.runDoctor()
.then((report) =>
dispatch({ type: "setDoctor", doctor: report }),
)
}
>
Run Doctor
</Button>
) : null}
<p className="text-sm text-text-main/70 mt-2">{state.message}</p>
{/* ── Data Cleanup ── */}
<h3>Data Cleanup</h3>
{dataMessage && <p>{dataMessage}</p>}
{/* Data Cleanup */}
<h3 className="text-lg font-semibold text-text-main mt-6 mb-3">
Data Cleanup
</h3>
{dataMessage && (
<p className="text-sm text-text-main/70 mt-2">{dataMessage}</p>
)}
<div className="status-grid">
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3">
{/* Memory */}
<div className="card">
<h4>Memory</h4>
<p>{memoryFiles.length} files ({formatBytes(totalMemoryBytes)})</p>
<button
disabled={memoryFiles.length === 0}
onClick={() => {
api
.clearMemory()
.then((count) => {
setDataMessage(`Cleared ${count} memory file(s)`);
refreshData();
})
.catch(() => setDataMessage("Failed to clear memory"));
}}
>
Clear all memory
</button>
</div>
<Card className="bg-panel border-border-subtle">
<CardHeader>
<CardTitle>Memory</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm mb-2">
{memoryFiles.length} files ({formatBytes(totalMemoryBytes)})
</p>
<Button
size="sm"
disabled={memoryFiles.length === 0}
onClick={() => {
api
.clearMemory()
.then((count) => {
setDataMessage(`Cleared ${count} memory file(s)`);
refreshData();
})
.catch(() => setDataMessage("Failed to clear memory"));
}}
>
Clear all memory
</Button>
</CardContent>
</Card>
{/* Sessions */}
<div className="card">
<h4>Sessions</h4>
<p>{sessionFiles.length} files ({formatBytes(totalSessionBytes)})</p>
{agents.map((a) => (
<div key={a.agent} style={{ display: "flex", alignItems: "center", gap: 8, margin: "4px 0" }}>
<span>{a.agent}: {a.count} files ({formatBytes(a.size)})</span>
<button
onClick={() => {
api
.clearAgentSessions(a.agent)
.then((count) => {
setDataMessage(`Cleared ${count} session file(s) for ${a.agent}`);
refreshData();
})
.catch(() => setDataMessage(`Failed to clear sessions for ${a.agent}`));
}}
<Card className="bg-panel border-border-subtle">
<CardHeader>
<CardTitle>Sessions</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm mb-2">
{sessionFiles.length} files ({formatBytes(totalSessionBytes)})
</p>
{agents.map((a) => (
<div
key={a.agent}
className="flex items-center gap-2 my-1"
>
Clear
</button>
</div>
))}
<button
disabled={sessionFiles.length === 0}
onClick={() => {
api
.clearAllSessions()
.then((count) => {
setDataMessage(`Cleared ${count} session file(s)`);
refreshData();
})
.catch(() => setDataMessage("Failed to clear sessions"));
}}
>
Clear all sessions
</button>
</div>
<span className="text-sm">
{a.agent}: {a.count} files ({formatBytes(a.size)})
</span>
<Button
size="sm"
onClick={() => {
api
.clearAgentSessions(a.agent)
.then((count) => {
setDataMessage(
`Cleared ${count} session file(s) for ${a.agent}`,
);
refreshData();
})
.catch(() =>
setDataMessage(
`Failed to clear sessions for ${a.agent}`,
),
);
}}
>
Clear
</Button>
</div>
))}
<Button
size="sm"
disabled={sessionFiles.length === 0}
onClick={() => {
api
.clearAllSessions()
.then((count) => {
setDataMessage(`Cleared ${count} session file(s)`);
refreshData();
})
.catch(() => setDataMessage("Failed to clear sessions"));
}}
>
Clear all sessions
</Button>
</CardContent>
</Card>
</div>
</section>
);