| import { Button } from "./components/ui/button"; |
| import { AppNode, AppState, Message, nodeLabel, useMessages, useProjectId } from "./lib/state"; |
| import { useCallback, useEffect, useState } from "react"; |
| import { useNodes } from "@xyflow/react"; |
| import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "./components/ui/accordion"; |
| import { Badge } from "./components/ui/badge"; |
| import { CircleAlert, CircleX } from "lucide-react"; |
| |
| export function Messages() { |
| const nodes = useNodes<AppNode>(); |
| const projectId = useProjectId(); |
| const [nodeMap, setNodeMap] = useState<Map<string, AppNode>>(new Map()); |
| useEffect(() => { |
| setNodeMap(new Map(nodes.map((n) => [n.id, n]))); |
| }, [nodes, setNodeMap]); |
| const onClick = useCallback((_?: (state: AppState) => void) => { |
| return () => { |
| // TODO(gio): visual hints |
| // if (fn) { |
| // fn(store); |
| // } |
| }; |
| }, []); |
| const messages = useMessages(); |
| const [grouped, setGrouped] = useState<Map<string, Message[]>>(new Map()); |
| useEffect(() => { |
| const g = new Map<string, Message[]>(); |
| if (projectId == null) { |
| g.set("global", [ |
| { |
| id: "global", |
| nodeId: undefined, |
| message: "Create a new project or select existing one to get started", |
| type: "FATAL", |
| }, |
| ]); |
| } else { |
| messages.forEach((m) => { |
| const id = m.nodeId || "global"; |
| const existing: Message[] = g.get(id) || []; |
| existing.push(m); |
| g.set(id, existing); |
| }); |
| } |
| setGrouped(g); |
| }, [projectId, messages, setGrouped]); |
| const [open, setOpen] = useState<string[]>([...grouped.keys()]); |
| useEffect(() => { |
| // TODO(gio): do not reopen closed ones |
| setOpen([...grouped.keys()]); |
| }, [grouped, setOpen]); |
| return ( |
| <Accordion type="multiple" value={open} onValueChange={(v) => setOpen(v)} className="h-full max-h-full"> |
| {[...grouped.entries()].map(([id, messages]) => ( |
| <AccordionItem key={id} value={id}> |
| <AccordionTrigger className="flex flex-row-reverse !gap-1 !justify-end !h-fit !py-0"> |
| <Badge className="h-5 min-w-5 rounded-full px-2 font-mono tabular-nums"> |
| {messages.length} |
| </Badge> |
| <div>{id === "global" ? "Global" : nodeLabel(nodeMap.get(id)!)}</div> |
| </AccordionTrigger> |
| <AccordionContent className="flex flex-col !px-1"> |
| {messages.map((m) => ( |
| <Button |
| key={m.id} |
| variant="ghost" |
| className="justify-start !h-fit !py-0 flex flex-row gap-1 items-center" |
| onMouseOver={onClick(m.onHighlight)} |
| onMouseLeave={onClick(m.onLooseHighlight)} |
| onClick={onClick(m.onClick)} |
| > |
| {m.type === "WARNING" && <CircleAlert className="w-4 h-4" color="brown" />} |
| {m.type === "FATAL" && <CircleX className="w-4 h-4" color="red" />} |
| {m.type === "INFO" && <div className="w-4 h-4" />} |
| <div>{m.message}</div> |
| </Button> |
| ))} |
| </AccordionContent> |
| </AccordionItem> |
| ))} |
| </Accordion> |
| ); |
| } |