feat: migrate Doctor page to shadcn Card/Badge/Button
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user