true message
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user