feat(panel): PR Reviewer + Documenter note cards with verdict pill

This commit is contained in:
Renn F
2026-06-21 03:02:46 +02:00
parent 2e490a06d7
commit 6a14312a0c
2 changed files with 125 additions and 24 deletions
@@ -9,14 +9,57 @@ import { Badge } from "@/components/ui/badge";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Markdown } from "@/components/ui/markdown"; import { Markdown } from "@/components/ui/markdown";
import { FileText, Code, TestTube, Shield, Edit3, Eye, Check, X, Plus } from "lucide-react"; import {
FileText,
Code,
TestTube,
Shield,
GitPullRequest,
BookText,
Edit3,
Eye,
Check,
X,
Plus,
} from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
interface TabNotesProps { interface TabNotesProps {
task: Task; task: Task;
} }
type NoteField = "quick_context" | "dev_notes" | "qa_notes" | "auditor_notes"; type NoteField =
| "quick_context"
| "dev_notes"
| "qa_notes"
| "auditor_notes"
| "pr_reviewer_notes"
| "doc_notes";
// The PR reviewer's verdict pill, read from the structured source of truth.
function prReviewBadge(task: Task): React.ReactNode {
const verdict = (
task.notes_structured as
| { pr_review?: { verdict?: string } }
| null
| undefined
)?.pr_review?.verdict;
if (!verdict) {
return (
<Badge variant="outline" className="ml-2 text-teal-600 border-teal-300">
Review Gate
</Badge>
);
}
const map: Record<string, { label: string; cls: string }> = {
approved: { label: "Approved", cls: "bg-green-500" },
passed: { label: "Passed", cls: "bg-green-500" },
changes_requested: { label: "Changes Requested", cls: "bg-amber-500" },
failed: { label: "Failed", cls: "bg-red-500" },
};
const v = map[verdict] ?? { label: verdict, cls: "bg-gray-500" };
return <Badge className={`ml-2 ${v.cls} text-white`}>{v.label}</Badge>;
}
interface NoteCardProps { interface NoteCardProps {
task: Task; task: Task;
@@ -27,7 +70,14 @@ interface NoteCardProps {
bgClass?: string; bgClass?: string;
} }
function EditableNoteCard({ task, field, title, icon, badge, bgClass }: NoteCardProps) { function EditableNoteCard({
task,
field,
title,
icon,
badge,
bgClass,
}: NoteCardProps) {
const updateTask = useUpdateTask(); const updateTask = useUpdateTask();
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const [localEditValue, setLocalEditValue] = useState(""); const [localEditValue, setLocalEditValue] = useState("");
@@ -92,11 +142,7 @@ function EditableNoteCard({ task, field, title, icon, badge, bgClass }: NoteCard
{title} {title}
{badge} {badge}
</CardTitle> </CardTitle>
<Button <Button size="sm" variant="ghost" onClick={startEditing}>
size="sm"
variant="ghost"
onClick={startEditing}
>
<Plus className="h-4 w-4 mr-1" /> <Plus className="h-4 w-4 mr-1" />
Add Add
</Button> </Button>
@@ -125,7 +171,10 @@ function EditableNoteCard({ task, field, title, icon, badge, bgClass }: NoteCard
</CardTitle> </CardTitle>
{isEditing ? ( {isEditing ? (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Tabs value={editMode} onValueChange={(v) => setEditMode(v as "write" | "preview")}> <Tabs
value={editMode}
onValueChange={(v) => setEditMode(v as "write" | "preview")}
>
<TabsList className="h-8"> <TabsList className="h-8">
<TabsTrigger value="write" className="text-xs px-2 h-6"> <TabsTrigger value="write" className="text-xs px-2 h-6">
<Edit3 className="h-3 w-3 mr-1" /> <Edit3 className="h-3 w-3 mr-1" />
@@ -155,11 +204,7 @@ function EditableNoteCard({ task, field, title, icon, badge, bgClass }: NoteCard
</Button> </Button>
</div> </div>
) : ( ) : (
<Button <Button size="sm" variant="ghost" onClick={startEditing}>
size="sm"
variant="ghost"
onClick={startEditing}
>
<Edit3 className="h-4 w-4 mr-1" /> <Edit3 className="h-4 w-4 mr-1" />
Edit Edit
</Button> </Button>
@@ -180,16 +225,21 @@ function EditableNoteCard({ task, field, title, icon, badge, bgClass }: NoteCard
autoFocus autoFocus
/> />
) : ( ) : (
<div className={`min-h-[150px] p-4 rounded-lg ${bgClass ?? "bg-muted/50"}`}> <div
className={`min-h-[150px] p-4 rounded-lg ${bgClass ?? "bg-muted/50"}`}
>
{editValue ? ( {editValue ? (
<Markdown className="text-sm">{editValue}</Markdown> <Markdown className="text-sm">{editValue}</Markdown>
) : ( ) : (
<p className="text-muted-foreground text-sm italic">Nothing to preview</p> <p className="text-muted-foreground text-sm italic">
Nothing to preview
</p>
)} )}
</div> </div>
)} )}
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Markdown supported. Press Ctrl/Cmd + Enter to save, Escape to cancel. Markdown supported. Press Ctrl/Cmd + Enter to save, Escape to
cancel.
</p> </p>
</div> </div>
) : ( ) : (
@@ -216,7 +266,11 @@ export function TabNotes({ task }: TabNotesProps) {
field="quick_context" field="quick_context"
title="Quick Context" title="Quick Context"
icon={<FileText className="h-5 w-5" />} icon={<FileText className="h-5 w-5" />}
badge={<Badge variant="outline" className="ml-2">For Resumption</Badge>} badge={
<Badge variant="outline" className="ml-2">
For Resumption
</Badge>
}
bgClass="bg-blue-50 dark:bg-blue-950 border border-blue-200 dark:border-blue-800" bgClass="bg-blue-50 dark:bg-blue-950 border border-blue-200 dark:border-blue-800"
/> />
@@ -229,6 +283,15 @@ export function TabNotes({ task }: TabNotesProps) {
bgClass="bg-muted/50" bgClass="bg-muted/50"
/> />
{/* Documenter Notes */}
<EditableNoteCard
task={task}
field="doc_notes"
title="Documenter Notes"
icon={<BookText className="h-5 w-5" />}
bgClass="bg-muted/50"
/>
{/* QA Notes */} {/* QA Notes */}
<EditableNoteCard <EditableNoteCard
task={task} task={task}
@@ -237,28 +300,55 @@ export function TabNotes({ task }: TabNotesProps) {
icon={<TestTube className="h-5 w-5" />} icon={<TestTube className="h-5 w-5" />}
badge={ badge={
<Badge <Badge
variant={task.qa_verified === true ? "default" : task.qa_verified === false ? "destructive" : "secondary"} variant={
task.qa_verified === true
? "default"
: task.qa_verified === false
? "destructive"
: "secondary"
}
className="ml-2" className="ml-2"
> >
{task.qa_verified === true ? "Passed" : task.qa_verified === false ? "Failed" : "Pending"} {task.qa_verified === true
? "Passed"
: task.qa_verified === false
? "Failed"
: "Pending"}
</Badge> </Badge>
} }
bgClass={ bgClass={
task.qa_verified === true task.qa_verified === true
? "bg-green-50 dark:bg-green-950 border border-green-200 dark:border-green-800" ? "bg-green-50 dark:bg-green-950 border border-green-200 dark:border-green-800"
: task.qa_verified === false : task.qa_verified === false
? "bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-800" ? "bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-800"
: "bg-muted/50" : "bg-muted/50"
} }
/> />
{/* PR Reviewer Notes */}
<EditableNoteCard
task={task}
field="pr_reviewer_notes"
title="PR Reviewer Notes"
icon={<GitPullRequest className="h-5 w-5" />}
badge={prReviewBadge(task)}
bgClass="bg-teal-50 dark:bg-teal-950 border border-teal-200 dark:border-teal-800"
/>
{/* Auditor Notes */} {/* Auditor Notes */}
<EditableNoteCard <EditableNoteCard
task={task} task={task}
field="auditor_notes" field="auditor_notes"
title="Auditor Notes" title="Auditor Notes"
icon={<Shield className="h-5 w-5" />} icon={<Shield className="h-5 w-5" />}
badge={<Badge variant="outline" className="ml-2 text-purple-600 border-purple-300">Confidential</Badge>} badge={
<Badge
variant="outline"
className="ml-2 text-purple-600 border-purple-300"
>
Confidential
</Badge>
}
bgClass="bg-purple-50 dark:bg-purple-950 border border-purple-200 dark:border-purple-800" bgClass="bg-purple-50 dark:bg-purple-950 border border-purple-200 dark:border-purple-800"
/> />
</div> </div>
+12 -1
View File
@@ -303,7 +303,12 @@ export interface Task {
dev_notes: string | null; dev_notes: string | null;
qa_notes: string | null; qa_notes: string | null;
auditor_notes: string | null; auditor_notes: string | null;
pr_reviewer_notes?: string | null;
doc_notes?: string | null;
quick_context: string | null; quick_context: string | null;
// Structured content (source of truth; the *_notes fields are rendered mirrors)
notes_structured?: Record<string, unknown> | null;
orchestration_markers?: Record<string, unknown> | null;
// Review Status // Review Status
self_verified: boolean; self_verified: boolean;
qa_verified: boolean | null; qa_verified: boolean | null;
@@ -957,7 +962,13 @@ export interface CodeReviewResponse {
// Learnings // Learnings
export interface LearningRecordRequest { export interface LearningRecordRequest {
content: string; content: string;
category: "error_handling" | "performance" | "testing" | "pattern" | "tool" | "other"; category:
| "error_handling"
| "performance"
| "testing"
| "pattern"
| "tool"
| "other";
team?: "backend" | "frontend" | "ux_ui"; team?: "backend" | "frontend" | "ux_ui";
shareable?: boolean; shareable?: boolean;
tags?: string[]; tags?: string[];