小糯 🐱 6f73544e96 feat(board): dynamic agent profiles from OGraph events (#33)
- Remove hardcoded AGENT_NAME_MAP, load profiles from agent_profile_updated events
- OGraphClient.loadAgentProfiles() fetches agent objects and replays profile events (LWW)
- Hardcoded fallback map retained for graceful degradation
- TaskDialog accepts dynamic agents list as prop
- App.tsx loads profiles in parallel with tasks on startup
2026-04-13 16:51:04 +08:00

200 lines
6.2 KiB
TypeScript

import { useState, useEffect } from "react"
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
DialogDescription,
} from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import {
ALL_STATUSES,
ALL_PRIORITIES,
STATUS_CONFIG,
PRIORITY_CONFIG,
type Agent,
type Task,
type TaskStatus,
type TaskPriority,
type CreateTaskInput,
} from "@/types"
interface TaskDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
task: Task | null // null = creating new task
defaultStatus?: TaskStatus
agents: Agent[]
onSave: (data: CreateTaskInput & { id?: number; status?: TaskStatus }) => void
onDelete?: (id: number) => void
}
export function TaskDialog({ open, onOpenChange, task, defaultStatus, agents, onSave, onDelete }: TaskDialogProps) {
const [title, setTitle] = useState("")
const [description, setDescription] = useState("")
const [priority, setPriority] = useState<TaskPriority>("p2")
const [status, setStatus] = useState<TaskStatus>(defaultStatus ?? "backlog")
const [assigneeId, setAssigneeId] = useState<string>("unassigned")
useEffect(() => {
if (task) {
setTitle(task.title)
setDescription(task.description)
setPriority(task.priority)
setStatus(task.status)
setAssigneeId(task.assigneeId ? String(task.assigneeId) : "unassigned")
} else {
setTitle("")
setDescription("")
setPriority("p2")
setStatus(defaultStatus ?? "backlog")
setAssigneeId("unassigned")
}
}, [task, defaultStatus, open])
function handleSubmit(e: React.FormEvent) {
e.preventDefault()
if (!title.trim()) return
onSave({
id: task?.id,
title: title.trim(),
description: description.trim(),
priority,
status,
assigneeId: assigneeId === "unassigned" ? null : Number(assigneeId),
})
onOpenChange(false)
}
const isEditing = task !== null
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[480px]">
<DialogHeader>
<DialogTitle>{isEditing ? "Edit Task" : "New Task"}</DialogTitle>
<DialogDescription className="text-zinc-500">
{isEditing ? "Update the task details below." : "Fill in the details to create a new task."}
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
{/* Title */}
<div className="space-y-1.5">
<label htmlFor="task-title" className="text-sm font-medium text-zinc-300">
Title
</label>
<Input
id="task-title"
placeholder="Task title..."
value={title}
onChange={(e) => setTitle(e.target.value)}
autoFocus
/>
</div>
{/* Description */}
<div className="space-y-1.5">
<label htmlFor="task-desc" className="text-sm font-medium text-zinc-300">
Description
</label>
<Textarea
id="task-desc"
placeholder="Add a description..."
value={description}
onChange={(e) => setDescription(e.target.value)}
rows={3}
/>
</div>
{/* Priority & Status row */}
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1.5">
<label className="text-sm font-medium text-zinc-300">Priority</label>
<Select value={priority} onValueChange={(v) => setPriority(v as TaskPriority)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{ALL_PRIORITIES.map((p) => (
<SelectItem key={p} value={p}>
{PRIORITY_CONFIG[p].icon} {PRIORITY_CONFIG[p].label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<label className="text-sm font-medium text-zinc-300">Status</label>
<Select value={status} onValueChange={(v) => setStatus(v as TaskStatus)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{ALL_STATUSES.map((s) => (
<SelectItem key={s} value={s}>
{STATUS_CONFIG[s].label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* Assignee */}
<div className="space-y-1.5">
<label className="text-sm font-medium text-zinc-300">Assignee</label>
<Select value={assigneeId} onValueChange={setAssigneeId}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="unassigned">Unassigned</SelectItem>
{agents.map((agent) => (
<SelectItem key={agent.id} value={String(agent.id)}>
{agent.emoji} {agent.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<DialogFooter className="gap-2">
{isEditing && onDelete && (
<Button
type="button"
variant="destructive"
className="mr-auto"
onClick={() => {
onDelete(task.id)
onOpenChange(false)
}}
>
Delete
</Button>
)}
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button type="submit" disabled={!title.trim()}>
{isEditing ? "Save Changes" : "Create Task"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)
}