mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-11 12:18:59 +00:00
fix(ui): heartbeat logs crash + header/card improvements
- Fix HeartbeatLog interface field names (snake_case → camelCase) to match backend JSON tags — fixes white screen crash on logs dialog open - Format duration with formatDuration(), show token usage per log entry - Fix logs dialog padding to match standard pattern + close button overlap - Show model override and last error on heartbeat card - Replace status badge with colored dot in agent header - Compact header badges on mobile (icon-only for evolving/static) - Switch sidebar to tablet breakpoint (1024px) for earlier collapse
This commit is contained in:
1 parent
ef1a1463ac
commit
b6ff57ddab
5 files changed
+55
-35
No files matched your search
@@ -6,7 +6,7 @@ import { Topbar } from "./topbar";
|
||||
import { ErrorBoundary } from "@/components/shared/error-boundary";
|
||||
import { useUiStore } from "@/stores/use-ui-store";
|
||||
import { useAuthStore } from "@/stores/use-auth-store";
|
||||
import { useIsMobile } from "@/hooks/use-media-query";
|
||||
import { useIsTablet } from "@/hooks/use-media-query";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function AppLayout() {
|
||||
@@ -16,7 +16,7 @@ export function AppLayout() {
|
||||
const mobileSidebarOpen = useUiStore((s) => s.mobileSidebarOpen);
|
||||
const setMobileSidebarOpen = useUiStore((s) => s.setMobileSidebarOpen);
|
||||
const connected = useAuthStore((s) => s.connected);
|
||||
const isMobile = useIsMobile();
|
||||
const isMobile = useIsTablet();
|
||||
|
||||
return (
|
||||
<div className="flex h-dvh overflow-hidden safe-top">
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { AgentData } from "@/types/agent";
|
||||
import type { HeartbeatConfig } from "@/pages/agents/hooks/use-agent-heartbeat";
|
||||
import { useCountdown } from "@/hooks/use-countdown";
|
||||
import { agentDisplayName, agentKeyDisplay } from "./agent-display-utils";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface AgentHeaderProps {
|
||||
agent: AgentData;
|
||||
@@ -51,21 +52,29 @@ export function AgentHeader({ agent, heartbeat, onBack, onDelete, onAdvanced, on
|
||||
{agent.is_default && (
|
||||
<Star className="h-3.5 w-3.5 shrink-0 fill-amber-400 text-amber-400" />
|
||||
)}
|
||||
<Badge
|
||||
variant={
|
||||
agent.status === "active"
|
||||
? "success"
|
||||
: agent.status === "summon_failed"
|
||||
? "destructive"
|
||||
: "secondary"
|
||||
}
|
||||
className="text-[10px]"
|
||||
>
|
||||
{agent.status === "summon_failed" ? t("detail.summonFailed") : agent.status}
|
||||
</Badge>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge variant="outline" className="text-[10px]">{agent.agent_type}</Badge>
|
||||
<span
|
||||
className={cn(
|
||||
"inline-block h-2.5 w-2.5 shrink-0 rounded-full",
|
||||
agent.status === "active"
|
||||
? "bg-emerald-500"
|
||||
: agent.status === "summon_failed"
|
||||
? "bg-destructive"
|
||||
: "bg-muted-foreground/50",
|
||||
)}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className="text-xs">
|
||||
{agent.status === "summon_failed" ? t("detail.summonFailed") : agent.status}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge variant="outline" className="text-[10px]">
|
||||
<span className="hidden sm:inline">{agent.agent_type}</span>
|
||||
<span className="sm:hidden">{agent.agent_type === "predefined" ? "P" : "O"}</span>
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className="max-w-[260px] text-xs">
|
||||
{agent.agent_type === "predefined" ? t("card.predefinedTooltip") : t("card.openTooltip")}
|
||||
@@ -78,8 +87,8 @@ export function AgentHeader({ agent, heartbeat, onBack, onDelete, onAdvanced, on
|
||||
variant={selfEvolve ? "default" : "outline"}
|
||||
className={`text-[10px] ${selfEvolve ? "bg-violet-100 text-violet-700 hover:bg-violet-100 dark:bg-violet-900/30 dark:text-violet-300" : "text-muted-foreground"}`}
|
||||
>
|
||||
<Sparkles className="mr-0.5 h-2.5 w-2.5" />
|
||||
{selfEvolve ? t("detail.evolving") : t("detail.static")}
|
||||
<Sparkles className="h-2.5 w-2.5 sm:mr-0.5" />
|
||||
<span className="hidden sm:inline">{selfEvolve ? t("detail.evolving") : t("detail.static")}</span>
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className="max-w-[240px] text-xs">
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { useMinLoading } from "@/hooks/use-min-loading";
|
||||
import type { HeartbeatLog } from "@/pages/agents/hooks/use-agent-heartbeat";
|
||||
import { formatRelativeTime, formatDate } from "@/lib/format";
|
||||
import { formatRelativeTime, formatDate, formatDuration } from "@/lib/format";
|
||||
|
||||
const PAGE_SIZE = 20;
|
||||
|
||||
@@ -69,8 +69,8 @@ export function HeartbeatLogsDialog({
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-h-[90vh] w-[95vw] sm:max-w-3xl flex flex-col gap-0 p-0">
|
||||
<DialogHeader className="px-4 pt-4 pb-3 sm:px-6 sm:pt-6 flex-row items-center justify-between gap-2">
|
||||
<DialogContent className="max-h-[90vh] w-[95vw] flex flex-col sm:max-w-3xl">
|
||||
<DialogHeader className="flex-row items-center justify-between gap-2 pr-8">
|
||||
<DialogTitle>{t("heartbeat.logsTitle")}</DialogTitle>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -84,7 +84,7 @@ export function HeartbeatLogsDialog({
|
||||
</Button>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="overflow-y-auto min-h-0 max-h-[60vh] -mx-0 px-4 sm:px-6 overscroll-contain">
|
||||
<div className="overflow-y-auto min-h-0 max-h-[60vh] -mx-4 px-4 sm:-mx-6 sm:px-6 overscroll-contain">
|
||||
{fetching && logs.length === 0 ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="h-6 w-6 animate-spin rounded-full border-2 border-muted-foreground border-t-transparent" />
|
||||
@@ -98,13 +98,18 @@ export function HeartbeatLogsDialog({
|
||||
{logs.map((log) => (
|
||||
<div key={log.id} className="rounded-md border p-3 text-sm space-y-1">
|
||||
<div className="flex items-center justify-between gap-2 flex-wrap">
|
||||
<span className="text-xs text-muted-foreground" title={formatDate(log.ran_at)}>
|
||||
{formatRelativeTime(log.ran_at)}
|
||||
<span className="text-xs text-muted-foreground" title={formatDate(log.ranAt)}>
|
||||
{formatRelativeTime(log.ranAt)}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{log.duration_ms != null && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{log.duration_ms}ms
|
||||
<div className="flex items-center gap-1.5">
|
||||
{(log.inputTokens != null || log.outputTokens != null) && (
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
{log.inputTokens ?? 0}↓ {log.outputTokens ?? 0}↑
|
||||
</span>
|
||||
)}
|
||||
{log.durationMs != null && (
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
{formatDuration(log.durationMs)}
|
||||
</span>
|
||||
)}
|
||||
<Badge variant={statusVariant(log.status)} className="text-[10px]">
|
||||
@@ -115,8 +120,8 @@ export function HeartbeatLogsDialog({
|
||||
{log.summary && (
|
||||
<p className="text-xs text-muted-foreground line-clamp-2">{log.summary}</p>
|
||||
)}
|
||||
{log.skip_reason && (
|
||||
<p className="text-xs text-muted-foreground italic">{log.skip_reason}</p>
|
||||
{log.skipReason && (
|
||||
<p className="text-xs text-muted-foreground italic">{log.skipReason}</p>
|
||||
)}
|
||||
{log.error && (
|
||||
<p className="text-xs text-destructive">{log.error}</p>
|
||||
@@ -128,7 +133,7 @@ export function HeartbeatLogsDialog({
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
<div className="flex items-center justify-between gap-2 px-4 py-3 sm:px-6 border-t text-xs text-muted-foreground">
|
||||
<div className="flex items-center justify-between gap-2 border-t pt-3 text-xs text-muted-foreground">
|
||||
<span>
|
||||
{total > 0
|
||||
? t("heartbeat.logsPagination", {
|
||||
|
||||
@@ -88,6 +88,9 @@ export function HeartbeatCard({ heartbeat }: HeartbeatCardProps) {
|
||||
{config.channel && (
|
||||
<span>{config.channel}{config.chatId ? `/${config.chatId}` : ""}</span>
|
||||
)}
|
||||
{config.model && (
|
||||
<span className="text-violet-600 dark:text-violet-400">{config.model}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-x-3 gap-y-0.5">
|
||||
@@ -106,6 +109,9 @@ export function HeartbeatCard({ heartbeat }: HeartbeatCardProps) {
|
||||
<span>{t("heartbeat.runs")}: {config.runCount}</span>
|
||||
<span>{t("heartbeat.suppressed")}: {config.suppressCount}</span>
|
||||
</div>
|
||||
{config.lastError && config.lastStatus === "error" && (
|
||||
<p className="text-xs text-destructive truncate">{config.lastError}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 pt-1">
|
||||
|
||||
@@ -40,11 +40,11 @@ export interface HeartbeatLog {
|
||||
status: string;
|
||||
summary?: string;
|
||||
error?: string;
|
||||
duration_ms?: number;
|
||||
input_tokens?: number;
|
||||
output_tokens?: number;
|
||||
skip_reason?: string;
|
||||
ran_at: string;
|
||||
durationMs?: number;
|
||||
inputTokens?: number;
|
||||
outputTokens?: number;
|
||||
skipReason?: string;
|
||||
ranAt: string;
|
||||
}
|
||||
|
||||
/** Return type of useAgentHeartbeat, for passing as props. */
|
||||
|
||||
Reference in new issue
Block a user