441 lines
17 KiB
TypeScript
441 lines
17 KiB
TypeScript
"use client";
|
||
|
||
import { useState, useEffect, useCallback } from "react";
|
||
import Card from "@/components/ui/Card";
|
||
|
||
const today = () => new Date().toISOString().split("T")[0];
|
||
|
||
type Severity = "low" | "medium" | "high" | "critical";
|
||
type BugStatus = "open" | "closed";
|
||
|
||
interface Bug {
|
||
description: string;
|
||
severity: Severity;
|
||
status: BugStatus;
|
||
}
|
||
|
||
interface FormState {
|
||
date: string;
|
||
testsRun: string[];
|
||
bugsFound: Bug[];
|
||
codeQualityScore: number;
|
||
recommendations: string[];
|
||
}
|
||
|
||
const defaultForm: FormState = {
|
||
date: today(),
|
||
testsRun: [""],
|
||
bugsFound: [],
|
||
codeQualityScore: 0,
|
||
recommendations: [""],
|
||
};
|
||
|
||
const severityColors: Record<Severity, string> = {
|
||
low: "text-emerald-400 bg-emerald-500/10 border-emerald-500/30",
|
||
medium: "text-amber-400 bg-amber-500/10 border-amber-500/30",
|
||
high: "text-orange-400 bg-orange-500/10 border-orange-500/30",
|
||
critical: "text-red-400 bg-red-500/10 border-red-500/30",
|
||
};
|
||
|
||
export default function AmunPage() {
|
||
const [form, setForm] = useState<FormState>(defaultForm);
|
||
const [saving, setSaving] = useState(false);
|
||
const [saved, setSaved] = useState(false);
|
||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||
const [loadingDate, setLoadingDate] = useState(false);
|
||
const [history, setHistory] = useState<FormState[]>([]);
|
||
|
||
const loadSessionForDate = useCallback(async (date: string) => {
|
||
setLoadingDate(true);
|
||
try {
|
||
const res = await fetch(`/api/amun?date=${date}`);
|
||
const data = await res.json();
|
||
if (data) {
|
||
setForm({
|
||
date: data.date,
|
||
testsRun: typeof data.testsRun === "string" ? JSON.parse(data.testsRun) : (data.testsRun || [""]),
|
||
bugsFound: typeof data.bugsFound === "string" ? JSON.parse(data.bugsFound) : (data.bugsFound || []),
|
||
codeQualityScore: data.codeQualityScore || 0,
|
||
recommendations: typeof data.recommendations === "string" ? JSON.parse(data.recommendations) : (data.recommendations || [""]),
|
||
});
|
||
} else {
|
||
setForm({ ...defaultForm, date });
|
||
}
|
||
} catch {
|
||
setForm({ ...defaultForm, date });
|
||
} finally {
|
||
setLoadingDate(false);
|
||
}
|
||
}, []);
|
||
|
||
const loadHistory = useCallback(async () => {
|
||
try {
|
||
const res = await fetch("/api/amun?limit=10");
|
||
const data = await res.json();
|
||
if (Array.isArray(data)) {
|
||
setHistory(
|
||
data.map((s) => ({
|
||
date: s.date,
|
||
testsRun: typeof s.testsRun === "string" ? JSON.parse(s.testsRun) : (s.testsRun || []),
|
||
bugsFound: typeof s.bugsFound === "string" ? JSON.parse(s.bugsFound) : (s.bugsFound || []),
|
||
codeQualityScore: s.codeQualityScore || 0,
|
||
recommendations: typeof s.recommendations === "string" ? JSON.parse(s.recommendations) : (s.recommendations || []),
|
||
}))
|
||
);
|
||
}
|
||
} catch {
|
||
// ignore
|
||
}
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
loadSessionForDate(today());
|
||
loadHistory();
|
||
}, [loadSessionForDate, loadHistory]);
|
||
|
||
const validate = (): boolean => {
|
||
const errs: Record<string, string> = {};
|
||
if (form.codeQualityScore < 0 || form.codeQualityScore > 100) {
|
||
errs.codeQualityScore = "Score must be between 0 and 100";
|
||
}
|
||
setErrors(errs);
|
||
return Object.keys(errs).length === 0;
|
||
};
|
||
|
||
const handleSave = async () => {
|
||
if (!validate()) return;
|
||
setSaving(true);
|
||
try {
|
||
const res = await fetch("/api/amun", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(form),
|
||
});
|
||
if (!res.ok) {
|
||
const err = await res.json();
|
||
setErrors({ submit: err.error || "Failed to save" });
|
||
} else {
|
||
setSaved(true);
|
||
setTimeout(() => setSaved(false), 3000);
|
||
loadHistory();
|
||
}
|
||
} catch {
|
||
setErrors({ submit: "Network error. Please try again." });
|
||
} finally {
|
||
setSaving(false);
|
||
}
|
||
};
|
||
|
||
const updateArrayItem = (field: "testsRun" | "recommendations", index: number, value: string) => {
|
||
setForm((prev) => {
|
||
const arr = [...prev[field]];
|
||
arr[index] = value;
|
||
return { ...prev, [field]: arr };
|
||
});
|
||
};
|
||
|
||
const addArrayItem = (field: "testsRun" | "recommendations") => {
|
||
setForm((prev) => ({ ...prev, [field]: [...prev[field], ""] }));
|
||
};
|
||
|
||
const removeArrayItem = (field: "testsRun" | "recommendations", index: number) => {
|
||
setForm((prev) => ({ ...prev, [field]: prev[field].filter((_, i) => i !== index) }));
|
||
};
|
||
|
||
const addBug = () => {
|
||
setForm((prev) => ({
|
||
...prev,
|
||
bugsFound: [...prev.bugsFound, { description: "", severity: "medium", status: "open" }],
|
||
}));
|
||
};
|
||
|
||
const updateBug = (index: number, field: keyof Bug, value: string) => {
|
||
setForm((prev) => {
|
||
const bugs = [...prev.bugsFound];
|
||
bugs[index] = { ...bugs[index], [field]: value as never };
|
||
return { ...prev, bugsFound: bugs };
|
||
});
|
||
};
|
||
|
||
const removeBug = (index: number) => {
|
||
setForm((prev) => ({ ...prev, bugsFound: prev.bugsFound.filter((_, i) => i !== index) }));
|
||
};
|
||
|
||
const scoreColor = (score: number) => {
|
||
if (score >= 80) return "text-emerald-400";
|
||
if (score >= 60) return "text-amber-400";
|
||
if (score >= 40) return "text-orange-400";
|
||
return "text-red-400";
|
||
};
|
||
|
||
const scoreBarColor = (score: number) => {
|
||
if (score >= 80) return "bg-emerald-500";
|
||
if (score >= 60) return "bg-amber-500";
|
||
if (score >= 40) return "bg-orange-500";
|
||
return "bg-red-500";
|
||
};
|
||
|
||
const openBugs = form.bugsFound.filter((b) => b.status === "open");
|
||
const closedBugs = form.bugsFound.filter((b) => b.status === "closed");
|
||
|
||
return (
|
||
<div className="p-6 max-w-4xl mx-auto">
|
||
{/* Header */}
|
||
<div className="flex items-center justify-between mb-6">
|
||
<div>
|
||
<h1 className="text-xl font-bold text-white flex items-center gap-2">
|
||
<span className="text-purple-400">⚙️</span> Amun Dev Agent
|
||
</h1>
|
||
<p className="text-sm text-slate-500 mt-0.5">Development testing & code quality dashboard</p>
|
||
</div>
|
||
<div className="flex items-center gap-3">
|
||
<input
|
||
type="date"
|
||
value={form.date}
|
||
onChange={(e) => {
|
||
setForm((prev) => ({ ...prev, date: e.target.value }));
|
||
loadSessionForDate(e.target.value);
|
||
}}
|
||
className="bg-[#1a1d27] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-300 focus:outline-none focus:border-purple-500/50"
|
||
/>
|
||
<button
|
||
onClick={handleSave}
|
||
disabled={saving || loadingDate}
|
||
className="px-4 py-2 bg-purple-600 hover:bg-purple-500 disabled:opacity-50 text-white font-semibold text-sm rounded-lg transition-colors"
|
||
>
|
||
{saving ? "Saving…" : saved ? "✓ Saved" : "Save Session"}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{errors.submit && (
|
||
<div className="mb-4 p-3 bg-red-500/10 border border-red-500/30 rounded-lg text-red-400 text-sm">
|
||
{errors.submit}
|
||
</div>
|
||
)}
|
||
|
||
{/* Stats Row */}
|
||
<div className="grid grid-cols-3 gap-4 mb-5">
|
||
<div className="bg-[#13151f] border border-[#1e2130] rounded-xl p-4">
|
||
<p className="text-xs text-slate-500 mb-1">Tests Run</p>
|
||
<p className="text-2xl font-bold text-white">{form.testsRun.filter((t) => t.trim()).length}</p>
|
||
</div>
|
||
<div className="bg-[#13151f] border border-[#1e2130] rounded-xl p-4">
|
||
<p className="text-xs text-slate-500 mb-1">Open Bugs</p>
|
||
<p className={`text-2xl font-bold ${openBugs.length > 0 ? "text-red-400" : "text-emerald-400"}`}>
|
||
{openBugs.length}
|
||
</p>
|
||
</div>
|
||
<div className="bg-[#13151f] border border-[#1e2130] rounded-xl p-4">
|
||
<p className="text-xs text-slate-500 mb-1">Code Quality</p>
|
||
<p className={`text-2xl font-bold ${scoreColor(form.codeQualityScore)}`}>
|
||
{form.codeQualityScore}%
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-5">
|
||
{/* Code Quality Score */}
|
||
<Card title="📊 Code Quality Score" accent="purple">
|
||
{errors.codeQualityScore && (
|
||
<p className="text-xs text-red-400 mb-2">{errors.codeQualityScore}</p>
|
||
)}
|
||
<div className="flex items-center gap-4">
|
||
<input
|
||
type="range"
|
||
min="0"
|
||
max="100"
|
||
value={form.codeQualityScore}
|
||
onChange={(e) => setForm((p) => ({ ...p, codeQualityScore: parseInt(e.target.value) }))}
|
||
className="flex-1 accent-purple-500"
|
||
/>
|
||
<div className="flex items-center gap-2">
|
||
<input
|
||
type="number"
|
||
min="0"
|
||
max="100"
|
||
value={form.codeQualityScore}
|
||
onChange={(e) =>
|
||
setForm((p) => ({
|
||
...p,
|
||
codeQualityScore: Math.min(100, Math.max(0, parseInt(e.target.value) || 0)),
|
||
}))
|
||
}
|
||
className="w-16 bg-[#0f1117] border border-[#2a2d3a] rounded-lg px-2 py-1.5 text-sm text-slate-200 focus:outline-none focus:border-purple-500/50 text-center"
|
||
/>
|
||
<span className="text-sm text-slate-500">%</span>
|
||
</div>
|
||
</div>
|
||
<div className="mt-3 h-2 bg-[#0f1117] rounded-full overflow-hidden border border-[#2a2d3a]">
|
||
<div
|
||
className={`h-full rounded-full transition-all duration-300 ${scoreBarColor(form.codeQualityScore)}`}
|
||
style={{ width: `${form.codeQualityScore}%` }}
|
||
/>
|
||
</div>
|
||
</Card>
|
||
|
||
{/* Tests Run */}
|
||
<Card title="🧪 Tests Run" accent="blue">
|
||
<div className="space-y-2">
|
||
{form.testsRun.map((t, i) => (
|
||
<div key={i} className="flex gap-2">
|
||
<span className="text-xs text-blue-500/70 font-mono mt-2.5 w-4">▸</span>
|
||
<input
|
||
type="text"
|
||
value={t}
|
||
onChange={(e) => updateArrayItem("testsRun", i, e.target.value)}
|
||
placeholder={`Test ${i + 1} (e.g. unit:auth, e2e:checkout)`}
|
||
className="flex-1 bg-[#0f1117] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-blue-500/50 font-mono"
|
||
/>
|
||
{form.testsRun.length > 1 && (
|
||
<button
|
||
onClick={() => removeArrayItem("testsRun", i)}
|
||
className="text-slate-600 hover:text-red-400 transition-colors text-lg leading-none mt-1"
|
||
>
|
||
×
|
||
</button>
|
||
)}
|
||
</div>
|
||
))}
|
||
<button
|
||
onClick={() => addArrayItem("testsRun")}
|
||
className="text-xs text-blue-400 hover:text-blue-300 transition-colors mt-1"
|
||
>
|
||
+ Add test
|
||
</button>
|
||
</div>
|
||
</Card>
|
||
|
||
{/* Bugs Found */}
|
||
<Card title="🐛 Bugs Found" subtitle={`${openBugs.length} open · ${closedBugs.length} closed`} accent="red">
|
||
<div className="space-y-3">
|
||
{form.bugsFound.length === 0 && (
|
||
<p className="text-sm text-slate-600 italic">No bugs logged yet.</p>
|
||
)}
|
||
{form.bugsFound.map((bug, i) => (
|
||
<div key={i} className="p-3 bg-[#0f1117] rounded-lg border border-[#2a2d3a]">
|
||
<div className="flex gap-2 mb-2">
|
||
<input
|
||
type="text"
|
||
value={bug.description}
|
||
onChange={(e) => updateBug(i, "description", e.target.value)}
|
||
placeholder="Bug description"
|
||
className="flex-1 bg-[#13151f] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-red-500/50"
|
||
/>
|
||
<button
|
||
onClick={() => removeBug(i)}
|
||
className="text-slate-600 hover:text-red-400 transition-colors text-lg leading-none px-1"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
<div className="flex gap-2">
|
||
<select
|
||
value={bug.severity}
|
||
onChange={(e) => updateBug(i, "severity", e.target.value)}
|
||
className={`flex-1 bg-[#13151f] border rounded-lg px-3 py-1.5 text-xs focus:outline-none ${severityColors[bug.severity]}`}
|
||
>
|
||
<option value="low">Low</option>
|
||
<option value="medium">Medium</option>
|
||
<option value="high">High</option>
|
||
<option value="critical">Critical</option>
|
||
</select>
|
||
<button
|
||
onClick={() => updateBug(i, "status", bug.status === "open" ? "closed" : "open")}
|
||
className={`px-3 py-1.5 rounded-lg text-xs font-medium border transition-colors ${
|
||
bug.status === "open"
|
||
? "bg-red-500/10 border-red-500/30 text-red-400 hover:bg-red-500/20"
|
||
: "bg-emerald-500/10 border-emerald-500/30 text-emerald-400 hover:bg-emerald-500/20"
|
||
}`}
|
||
>
|
||
{bug.status === "open" ? "● Open" : "✓ Closed"}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
))}
|
||
<button
|
||
onClick={addBug}
|
||
className="text-xs text-red-400 hover:text-red-300 transition-colors mt-1"
|
||
>
|
||
+ Log bug
|
||
</button>
|
||
</div>
|
||
</Card>
|
||
|
||
{/* Recommendations */}
|
||
<Card title="💡 Recommendations" accent="amber">
|
||
<div className="space-y-2">
|
||
{form.recommendations.map((r, i) => (
|
||
<div key={i} className="flex gap-2">
|
||
<span className="text-xs text-amber-500/70 font-mono mt-2.5 w-4">→</span>
|
||
<input
|
||
type="text"
|
||
value={r}
|
||
onChange={(e) => updateArrayItem("recommendations", i, e.target.value)}
|
||
placeholder={`Recommendation ${i + 1}`}
|
||
className="flex-1 bg-[#0f1117] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-amber-500/50"
|
||
/>
|
||
{form.recommendations.length > 1 && (
|
||
<button
|
||
onClick={() => removeArrayItem("recommendations", i)}
|
||
className="text-slate-600 hover:text-red-400 transition-colors text-lg leading-none mt-1"
|
||
>
|
||
×
|
||
</button>
|
||
)}
|
||
</div>
|
||
))}
|
||
<button
|
||
onClick={() => addArrayItem("recommendations")}
|
||
className="text-xs text-amber-400 hover:text-amber-300 transition-colors mt-1"
|
||
>
|
||
+ Add recommendation
|
||
</button>
|
||
</div>
|
||
</Card>
|
||
|
||
{/* Session History */}
|
||
{history.length > 0 && (
|
||
<Card title="📅 Recent Sessions" subtitle="Last 10 Amun sessions">
|
||
<div className="space-y-2">
|
||
{history.map((s) => (
|
||
<button
|
||
key={s.date}
|
||
onClick={() => loadSessionForDate(s.date)}
|
||
className="w-full flex items-center justify-between p-3 bg-[#0f1117] rounded-lg border border-[#2a2d3a] hover:border-purple-500/30 transition-colors text-left"
|
||
>
|
||
<div className="flex items-center gap-3">
|
||
<span className="text-xs text-slate-500 font-mono">{s.date}</span>
|
||
<span className="text-xs text-slate-400">
|
||
{s.testsRun.filter((t) => t.trim()).length} tests
|
||
</span>
|
||
{s.bugsFound.filter((b) => b.status === "open").length > 0 && (
|
||
<span className="text-xs text-red-400">
|
||
{s.bugsFound.filter((b) => b.status === "open").length} open bugs
|
||
</span>
|
||
)}
|
||
</div>
|
||
<span className={`text-sm font-bold ${scoreColor(s.codeQualityScore)}`}>
|
||
{s.codeQualityScore}%
|
||
</span>
|
||
</button>
|
||
))}
|
||
</div>
|
||
</Card>
|
||
)}
|
||
|
||
{/* Save Button */}
|
||
<div className="flex justify-end pb-6">
|
||
<button
|
||
onClick={handleSave}
|
||
disabled={saving}
|
||
className="px-6 py-2.5 bg-purple-600 hover:bg-purple-500 disabled:opacity-50 text-white font-semibold text-sm rounded-lg transition-colors"
|
||
>
|
||
{saving ? "Saving…" : saved ? "✓ Session Saved!" : "Save Amun Session"}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|