true message

This commit is contained in:
Manus
2026-03-26 05:41:44 -04:00
parent d396004294
commit 8096ce4dfd
9 changed files with 409 additions and 626 deletions

View File

@@ -250,6 +250,8 @@ export default function Chat() {
>([]);
const [input, setInput] = useState("");
const [isThinking, setIsThinking] = useState(false);
const [retryAttempt, setRetryAttempt] = useState(0);
const [lastError, setLastError] = useState<{ message: string; isRetryable: boolean } | null>(null);
const scrollRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
@@ -328,6 +330,10 @@ export default function Chat() {
const respTs = getTs();
// Clear error state on success
setLastError(null);
setRetryAttempt(0);
if (result.success) {
// Update conversation history
setConversationHistory((prev) => [
@@ -362,21 +368,33 @@ export default function Chat() {
}
} catch (err: any) {
setMessages((prev) => prev.filter((m) => m.id !== thinkingId));
const errorMsg = err.message || "Unknown error";
const isRetryable = errorMsg.includes("timeout") || errorMsg.includes("unavailable") || errorMsg.includes("ECONNREFUSED");
setLastError({ message: errorMsg, isRetryable });
setRetryAttempt((prev) => prev + 1);
setMessages((prev) => [
...prev,
{
id: `err-${Date.now()}`,
role: "assistant" as const,
content: `Network Error: ${err.message}`,
content: `Network Error (Attempt ${retryAttempt + 1}): ${errorMsg}${isRetryable ? "\n\nRetrying automatically..." : ""}`,
timestamp: getTs(),
isError: true,
},
]);
// Auto-retry if retryable and under max attempts
if (isRetryable && retryAttempt < 2) {
setTimeout(() => {
sendMessage();
}, 1000 * Math.pow(2, retryAttempt));
}
} finally {
setIsThinking(false);
setTimeout(() => inputRef.current?.focus(), 100);
}
};
} };
const agents = agentsQuery.data ?? [];
const activeAgents = agents.filter((a) => a.isActive && !(a as any).isOrchestrator);

View File

@@ -6,7 +6,7 @@
* Colors: Cyan primary, green/amber/red for resource thresholds
* Typography: JetBrains Mono for all metrics
*/
import { useEffect, useState, useMemo } from "react";
import { useEffect, useState } from "react";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Progress } from "@/components/ui/progress";
@@ -30,65 +30,6 @@ import { trpc } from "@/lib/trpc";
const NODE_VIS =
"https://d2xsxph8kpxj0f.cloudfront.net/97147719/ZEGAT83geRq9CNvryykaQv/node-visualization-eDRHrwiVpLDMaH6VnWFsxn.webp";
// ── Sparkline ─────────────────────────────────────────────────────────────────────
function Sparkline({
points,
color = "#22d3ee",
width = 80,
height = 24,
}: {
points: number[];
color?: string;
width?: number;
height?: number;
}) {
if (points.length < 2) {
return (
<svg width={width} height={height} className="opacity-30">
<line x1={0} y1={height / 2} x2={width} y2={height / 2} stroke={color} strokeWidth={1} strokeDasharray="2 2" />
</svg>
);
}
const max = Math.max(...points, 1);
const min = 0;
const range = max - min || 1;
const step = width / (points.length - 1);
const pathD = points
.map((v, i) => {
const x = i * step;
const y = height - ((v - min) / range) * (height - 2) - 1;
return `${i === 0 ? "M" : "L"} ${x.toFixed(1)} ${y.toFixed(1)}`;
})
.join(" ");
// Fill area under line
const lastX = (points.length - 1) * step;
const fillD = `${pathD} L ${lastX.toFixed(1)} ${height} L 0 ${height} Z`;
return (
<svg width={width} height={height} className="overflow-visible">
<defs>
<linearGradient id={`sg-${color.replace("#", "")}`} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={color} stopOpacity={0.25} />
<stop offset="100%" stopColor={color} stopOpacity={0.02} />
</linearGradient>
</defs>
<path d={fillD} fill={`url(#sg-${color.replace("#", "")})`} />
<path d={pathD} stroke={color} strokeWidth={1.5} fill="none" strokeLinejoin="round" strokeLinecap="round" />
{/* Current value dot */}
{(() => {
const last = points[points.length - 1];
const x = (points.length - 1) * step;
const y = height - ((last - min) / range) * (height - 2) - 1;
return <circle cx={x.toFixed(1)} cy={y.toFixed(1)} r={2} fill={color} />;
})()}
</svg>
);
}
// ─── Helpers ─────────────────────────────────────────────────────────────────
function getResourceColor(value: number) {
@@ -183,26 +124,6 @@ export default function Nodes() {
retry: 2,
});
// Poll historical metrics every 30 seconds (matches collector interval)
const { data: metricsHistory } = trpc.nodes.allMetricsLatest.useQuery(undefined, {
refetchInterval: 30_000,
refetchIntervalInBackground: true,
retry: 1,
});
// Build sparkline data map: containerId/name → { cpuPoints, memPoints }
const sparklineMap = useMemo(() => {
const map = new Map<string, { cpuPoints: number[]; memPoints: number[] }>();
if (!metricsHistory?.byContainer) return map;
for (const [id, pts] of Object.entries(metricsHistory.byContainer)) {
map.set(id, {
cpuPoints: pts.map(p => p.cpu),
memPoints: pts.map(p => p.mem),
});
}
return map;
}, [metricsHistory]);
// Track last refresh time
useEffect(() => {
if (nodesData) setLastRefresh(new Date());
@@ -214,11 +135,6 @@ export default function Nodes() {
setLastRefresh(new Date());
};
// Helper: get sparkline for a container by id or name
function getSparkline(id: string, name: string) {
return sparklineMap.get(id) ?? sparklineMap.get(name) ?? null;
}
// Build a map: containerName → stats
const statsMap = new Map<string, { cpuPct: number; memUseMB: number; memLimMB: number; memPct: number }>();
if (statsData?.stats) {
@@ -526,19 +442,6 @@ export default function Nodes() {
</span>
</div>
<div className="flex items-center gap-3 text-[10px] font-mono text-muted-foreground flex-shrink-0 ml-2">
{/* Sparkline CPU */}
{(() => {
const spark = getSparkline(c.id, c.name);
if (spark && spark.cpuPoints.length >= 2) {
return (
<div className="flex flex-col items-end gap-0.5">
<Sparkline points={spark.cpuPoints} color="#22d3ee" width={60} height={18} />
<span className="text-[9px] text-muted-foreground/60">CPU 1h</span>
</div>
);
}
return null;
})()}
{c.cpuPct > 0 && (
<span>
CPU:{" "}
@@ -624,19 +527,6 @@ export default function Nodes() {
<span className="text-[10px] font-mono text-muted-foreground">{s.id}</span>
</div>
<div className="flex items-center gap-4 text-[11px] font-mono text-muted-foreground">
{/* Sparkline for standalone container */}
{(() => {
const spark = getSparkline(s.id, s.name);
if (spark && spark.cpuPoints.length >= 2) {
return (
<div className="flex flex-col items-center gap-0.5">
<Sparkline points={spark.cpuPoints} color="#22d3ee" width={72} height={20} />
<span className="text-[9px] text-muted-foreground/60">CPU 1h</span>
</div>
);
}
return null;
})()}
<span>
CPU: <span className={getResourceColor(s.cpuPct)}>{s.cpuPct.toFixed(1)}%</span>
</span>