[taskboard] refactor tasks into full-page workspace
This commit is contained in:
245
components/task-intake-modal.tsx
Normal file
245
components/task-intake-modal.tsx
Normal file
@@ -0,0 +1,245 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CardDescription, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select } from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import type { FleetAgent, TaskPriority, TaskTemplate } from "@/lib/types";
|
||||
|
||||
const PRIORITIES: TaskPriority[] = ["Low", "Medium", "High", "Critical"];
|
||||
|
||||
export function TaskIntakeModal({
|
||||
agents,
|
||||
templates,
|
||||
open,
|
||||
onClose,
|
||||
onCreated,
|
||||
}: {
|
||||
agents: FleetAgent[];
|
||||
templates: TaskTemplate[];
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onCreated: () => Promise<void>;
|
||||
}) {
|
||||
const [formState, setFormState] = useState({
|
||||
templateKey: "",
|
||||
title: "",
|
||||
description: "",
|
||||
assignee: "",
|
||||
priority: "Medium" as TaskPriority,
|
||||
tags: "",
|
||||
repoSlug: "",
|
||||
baseBranch: "main",
|
||||
preferredAgent: "codex",
|
||||
reasoningEffort: "high",
|
||||
modelHint: "",
|
||||
});
|
||||
|
||||
const selectedTemplate = templates.find((template) => template.key === formState.templateKey) || null;
|
||||
const selectedAgent = agents.find((agent) => agent.assignmentKey === formState.assignee) || null;
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
return;
|
||||
}
|
||||
|
||||
function onKeyDown(event: KeyboardEvent) {
|
||||
if (event.key === "Escape") {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
document.body.style.overflow = "hidden";
|
||||
return () => {
|
||||
window.removeEventListener("keydown", onKeyDown);
|
||||
document.body.style.overflow = "";
|
||||
};
|
||||
}, [onClose, open]);
|
||||
|
||||
function applyTemplate(templateKey: string) {
|
||||
const template = templates.find((entry) => entry.key === templateKey) || null;
|
||||
if (!template) {
|
||||
setFormState((current) => ({ ...current, templateKey }));
|
||||
return;
|
||||
}
|
||||
|
||||
setFormState((current) => ({
|
||||
...current,
|
||||
templateKey,
|
||||
title: current.title || template.title,
|
||||
priority: template.defaults.priority,
|
||||
tags: template.tags.join(", "),
|
||||
repoSlug: template.defaults.repoSlug || current.repoSlug,
|
||||
baseBranch: template.defaults.baseBranch || current.baseBranch,
|
||||
preferredAgent: template.defaults.preferredAgent || current.preferredAgent,
|
||||
reasoningEffort: template.defaults.reasoningEffort || current.reasoningEffort,
|
||||
}));
|
||||
}
|
||||
|
||||
async function createTask(event: React.FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
const tags = formState.tags
|
||||
.split(",")
|
||||
.map((tag) => tag.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
await fetch("/api/tasks", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
title: formState.title,
|
||||
description: formState.description,
|
||||
assignee: formState.assignee,
|
||||
priority: formState.priority,
|
||||
tags,
|
||||
template_key: formState.templateKey || null,
|
||||
repo_slug: formState.repoSlug || null,
|
||||
base_branch: formState.baseBranch || null,
|
||||
preferred_agent: formState.preferredAgent || null,
|
||||
reasoning_effort: formState.reasoningEffort || null,
|
||||
model_hint: formState.modelHint || null,
|
||||
family: selectedAgent?.family || selectedTemplate?.family || null,
|
||||
target_host: selectedAgent?.host || selectedTemplate?.defaults.targetHost || "",
|
||||
target_channel: selectedAgent?.channels[0]?.value || selectedTemplate?.defaults.targetChannel || "",
|
||||
dispatch_method: selectedAgent?.defaultDispatchMethod || selectedTemplate?.defaults.dispatchMethod || "manual",
|
||||
}),
|
||||
});
|
||||
|
||||
setFormState({
|
||||
templateKey: "",
|
||||
title: "",
|
||||
description: "",
|
||||
assignee: "",
|
||||
priority: "Medium",
|
||||
tags: "",
|
||||
repoSlug: "",
|
||||
baseBranch: "main",
|
||||
preferredAgent: "codex",
|
||||
reasoningEffort: "high",
|
||||
modelHint: "",
|
||||
});
|
||||
await onCreated();
|
||||
onClose();
|
||||
}
|
||||
|
||||
if (!open) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-start justify-center overflow-y-auto bg-slate-950/70 px-4 py-10 backdrop-blur-sm">
|
||||
<div className="absolute inset-0" onClick={onClose} />
|
||||
<div className="relative w-full max-w-3xl rounded-3xl border border-white/10 bg-slate-950/95 shadow-2xl">
|
||||
<div className="flex items-start justify-between gap-4 border-b border-white/10 px-6 py-5">
|
||||
<div>
|
||||
<CardTitle>New Task</CardTitle>
|
||||
<CardDescription>
|
||||
Create a typed task and route it to the right execution family without leaving the board.
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Button size="sm" variant="ghost" onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<form className="grid gap-3 p-6 md:grid-cols-2" onSubmit={createTask}>
|
||||
<Select value={formState.templateKey} onChange={(event) => applyTemplate(event.target.value)}>
|
||||
<option value="">Select template</option>
|
||||
{templates.map((template) => (
|
||||
<option key={template.key} value={template.key}>
|
||||
{template.title} • {template.family}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
value={formState.assignee}
|
||||
onChange={(event) => setFormState((current) => ({ ...current, assignee: event.target.value }))}
|
||||
>
|
||||
<option value="">Select agent</option>
|
||||
{agents.map((agent) => (
|
||||
<option key={agent.slug} value={agent.assignmentKey}>
|
||||
{agent.name} • {agent.family} • {agent.host}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<Input
|
||||
placeholder="Task title"
|
||||
required
|
||||
value={formState.title}
|
||||
onChange={(event) => setFormState((current) => ({ ...current, title: event.target.value }))}
|
||||
/>
|
||||
<Select
|
||||
value={formState.priority}
|
||||
onChange={(event) =>
|
||||
setFormState((current) => ({ ...current, priority: event.target.value as TaskPriority }))
|
||||
}
|
||||
>
|
||||
{PRIORITIES.map((priority) => (
|
||||
<option key={priority} value={priority}>
|
||||
{priority}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<Input
|
||||
placeholder="Repo slug from repo-map.json"
|
||||
value={formState.repoSlug}
|
||||
onChange={(event) => setFormState((current) => ({ ...current, repoSlug: event.target.value }))}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Base branch"
|
||||
value={formState.baseBranch}
|
||||
onChange={(event) => setFormState((current) => ({ ...current, baseBranch: event.target.value }))}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Preferred swarm agent"
|
||||
value={formState.preferredAgent}
|
||||
onChange={(event) => setFormState((current) => ({ ...current, preferredAgent: event.target.value }))}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Reasoning effort"
|
||||
value={formState.reasoningEffort}
|
||||
onChange={(event) => setFormState((current) => ({ ...current, reasoningEffort: event.target.value }))}
|
||||
/>
|
||||
<Input
|
||||
className="md:col-span-2"
|
||||
placeholder="Tags (comma-separated)"
|
||||
value={formState.tags}
|
||||
onChange={(event) => setFormState((current) => ({ ...current, tags: event.target.value }))}
|
||||
/>
|
||||
<Input
|
||||
className="md:col-span-2"
|
||||
placeholder="Model hint (optional)"
|
||||
value={formState.modelHint}
|
||||
onChange={(event) => setFormState((current) => ({ ...current, modelHint: event.target.value }))}
|
||||
/>
|
||||
<div className="md:col-span-2">
|
||||
<Textarea
|
||||
placeholder="Describe the task, target host, expected outcome, and any validation steps."
|
||||
value={formState.description}
|
||||
onChange={(event) => setFormState((current) => ({ ...current, description: event.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="md:col-span-2 flex flex-wrap items-center justify-between gap-3 border-t border-white/10 pt-3">
|
||||
<p className="text-sm text-slate-400">
|
||||
{selectedAgent
|
||||
? `Dispatch target: ${selectedAgent.family} on ${selectedAgent.host}`
|
||||
: selectedTemplate
|
||||
? `Template dispatch: ${selectedTemplate.defaults.dispatchMethod}`
|
||||
: "Select an agent or template to prefill dispatch metadata."}
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<Button type="button" variant="ghost" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit">Create Task</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user