blob: 026d123c9f12be90c837a56010172200dcce1b84 [file] [log] [blame]
gio5f2f1002025-03-20 18:38:48 +04001import { Button } from "./components/ui/button";
gio33046722025-05-16 14:49:55 +00002import { AppNode, AppState, Message, nodeLabel, useMessages, useProjectId } from "./lib/state";
gio5f2f1002025-03-20 18:38:48 +04003import { useCallback, useEffect, useState } from "react";
4import { useNodes } from "@xyflow/react";
5import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "./components/ui/accordion";
6import { Badge } from "./components/ui/badge";
gio9fc37c72025-05-16 12:17:18 +00007import { CircleAlert, CircleX } from "lucide-react";
gio5f2f1002025-03-20 18:38:48 +04008
9export function Messages() {
giod0026612025-05-08 13:00:36 +000010 const nodes = useNodes<AppNode>();
gio33046722025-05-16 14:49:55 +000011 const projectId = useProjectId();
giod0026612025-05-08 13:00:36 +000012 const [nodeMap, setNodeMap] = useState<Map<string, AppNode>>(new Map());
13 useEffect(() => {
14 setNodeMap(new Map(nodes.map((n) => [n.id, n])));
15 }, [nodes, setNodeMap]);
gio9fc37c72025-05-16 12:17:18 +000016 const onClick = useCallback((_?: (state: AppState) => void) => {
17 return () => {
18 // TODO(gio): visual hints
19 // if (fn) {
20 // fn(store);
21 // }
22 };
23 }, []);
giod0026612025-05-08 13:00:36 +000024 const messages = useMessages();
25 const [grouped, setGrouped] = useState<Map<string, Message[]>>(new Map());
26 useEffect(() => {
27 const g = new Map<string, Message[]>();
gio33046722025-05-16 14:49:55 +000028 if (projectId == null) {
29 g.set("global", [
30 {
31 id: "global",
32 nodeId: undefined,
33 message: "Create a new project or select existing one to get started",
34 type: "FATAL",
35 },
36 ]);
37 } else {
38 messages.forEach((m) => {
39 const id = m.nodeId || "global";
40 const existing: Message[] = g.get(id) || [];
41 existing.push(m);
42 g.set(id, existing);
43 });
44 }
giod0026612025-05-08 13:00:36 +000045 setGrouped(g);
gio33046722025-05-16 14:49:55 +000046 }, [projectId, messages, setGrouped]);
giod0026612025-05-08 13:00:36 +000047 const [open, setOpen] = useState<string[]>([...grouped.keys()]);
48 useEffect(() => {
49 // TODO(gio): do not reopen closed ones
50 setOpen([...grouped.keys()]);
51 }, [grouped, setOpen]);
52 return (
gio8cadbc72025-05-16 07:51:02 +000053 <Accordion type="multiple" value={open} onValueChange={(v) => setOpen(v)} className="h-full max-h-full">
giod0026612025-05-08 13:00:36 +000054 {[...grouped.entries()].map(([id, messages]) => (
55 <AccordionItem key={id} value={id}>
gio617b1dd2025-05-13 05:45:41 +000056 <AccordionTrigger className="flex flex-row-reverse !gap-1 !justify-end !h-fit !py-0">
gioda120432025-06-02 09:42:26 +000057 <Badge className="h-5 min-w-5 rounded-full px-2 font-mono tabular-nums">
58 {messages.length}
59 </Badge>
giod0026612025-05-08 13:00:36 +000060 <div>{id === "global" ? "Global" : nodeLabel(nodeMap.get(id)!)}</div>
61 </AccordionTrigger>
gio9fc37c72025-05-16 12:17:18 +000062 <AccordionContent className="flex flex-col !px-1">
63 {messages.map((m) => (
64 <Button
65 key={m.id}
66 variant="ghost"
67 className="justify-start !h-fit !py-0 flex flex-row gap-1 items-center"
68 onMouseOver={onClick(m.onHighlight)}
69 onMouseLeave={onClick(m.onLooseHighlight)}
70 onClick={onClick(m.onClick)}
71 >
72 {m.type === "WARNING" && <CircleAlert className="w-4 h-4" color="brown" />}
73 {m.type === "FATAL" && <CircleX className="w-4 h-4" color="red" />}
74 {m.type === "INFO" && <div className="w-4 h-4" />}
75 <div>{m.message}</div>
76 </Button>
77 ))}
giod0026612025-05-08 13:00:36 +000078 </AccordionContent>
79 </AccordionItem>
80 ))}
81 </Accordion>
82 );
83}