gpt-engineer-app[bot] commited on
Commit
5e2060f
·
2 Parent(s): 445e7e99cdf298

Added Qwen chatbot to dashboard

Browse files

X-Lovable-Edit-ID: edt-28a972e2-2b61-41bf-873d-c64c0ed89e18

src/components/qwen-bot.tsx ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useState } from "react";
2
+ import { cn } from "@/lib/utils";
3
+ import { Bot, Sparkles, X } from "lucide-react";
4
+
5
+ type QwenBotProps = {
6
+ status: "idle" | "analyzing" | "complete" | "failed";
7
+ summary?: string | null;
8
+ severity?: string;
9
+ error?: string | null;
10
+ };
11
+
12
+ /**
13
+ * Floating Qwen mascot — an animated robot that "speaks" the latest analysis.
14
+ * Sits in the bottom-right corner of the dashboard.
15
+ */
16
+ export function QwenBot({ status, summary, severity, error }: QwenBotProps) {
17
+ const [open, setOpen] = useState(true);
18
+ const [typed, setTyped] = useState("");
19
+
20
+ const message =
21
+ status === "analyzing"
22
+ ? "Reasoning through the incident… engaging Event, Safety, Risk and Documentation agents."
23
+ : status === "failed"
24
+ ? `I hit a snag: ${error ?? "unknown error"}.`
25
+ : summary ?? "Hi, I'm Qwen — submit or select an incident and I'll analyze it for you.";
26
+
27
+ // Typewriter effect
28
+ useEffect(() => {
29
+ setTyped("");
30
+ if (!open) return;
31
+ let i = 0;
32
+ const id = setInterval(() => {
33
+ i++;
34
+ setTyped(message.slice(0, i));
35
+ if (i >= message.length) clearInterval(id);
36
+ }, 14);
37
+ return () => clearInterval(id);
38
+ }, [message, open]);
39
+
40
+ const moodColor =
41
+ status === "failed"
42
+ ? "text-severity-critical"
43
+ : status === "analyzing"
44
+ ? "text-primary"
45
+ : severity === "critical" || severity === "high"
46
+ ? "text-severity-high"
47
+ : "text-primary";
48
+
49
+ return (
50
+ <div className="fixed bottom-6 right-6 z-50 flex items-end gap-3 pointer-events-none">
51
+ {/* Speech bubble */}
52
+ {open && (
53
+ <div className="pointer-events-auto max-w-xs rounded-2xl rounded-br-sm border border-border bg-surface-elevated/95 backdrop-blur p-4 shadow-[var(--shadow-glow)] animate-in fade-in slide-in-from-bottom-2">
54
+ <div className="flex items-center justify-between mb-2">
55
+ <div className="flex items-center gap-1.5">
56
+ <Sparkles className={cn("h-3 w-3", moodColor)} />
57
+ <span className="text-[10px] uppercase tracking-widest font-mono text-muted-foreground">
58
+ Qwen · {status}
59
+ </span>
60
+ </div>
61
+ <button
62
+ onClick={() => setOpen(false)}
63
+ className="text-muted-foreground hover:text-foreground"
64
+ aria-label="Dismiss"
65
+ >
66
+ <X className="h-3 w-3" />
67
+ </button>
68
+ </div>
69
+ <p className="text-xs leading-relaxed text-foreground/90 whitespace-pre-wrap">
70
+ {typed}
71
+ {typed.length < message.length && <span className="inline-block w-1 h-3 bg-primary ml-0.5 animate-pulse" />}
72
+ </p>
73
+ </div>
74
+ )}
75
+
76
+ {/* Bot avatar */}
77
+ <button
78
+ onClick={() => setOpen((v) => !v)}
79
+ className={cn(
80
+ "pointer-events-auto relative h-16 w-16 rounded-full border-2 flex items-center justify-center transition-all hover:scale-105",
81
+ "bg-gradient-to-br from-surface-elevated to-surface",
82
+ status === "analyzing"
83
+ ? "border-primary shadow-[0_0_30px_hsl(var(--primary)/0.5)]"
84
+ : status === "failed"
85
+ ? "border-severity-critical"
86
+ : "border-primary/40 shadow-[var(--shadow-glow)]"
87
+ )}
88
+ aria-label="Toggle Qwen bot"
89
+ >
90
+ {/* Pulse ring while analyzing */}
91
+ {status === "analyzing" && (
92
+ <span className="absolute inset-0 rounded-full border-2 border-primary animate-ping opacity-60" />
93
+ )}
94
+ {/* Robot face */}
95
+ <div className="relative">
96
+ <Bot className={cn("h-8 w-8", moodColor)} />
97
+ {/* eyes blink */}
98
+ <span className="absolute -top-0.5 left-1.5 h-1 w-1 rounded-full bg-primary animate-pulse" />
99
+ <span className="absolute -top-0.5 right-1.5 h-1 w-1 rounded-full bg-primary animate-pulse" />
100
+ </div>
101
+ {/* Status dot */}
102
+ <span
103
+ className={cn(
104
+ "absolute bottom-0 right-0 h-3 w-3 rounded-full border-2 border-background",
105
+ status === "complete" && "bg-severity-low",
106
+ status === "analyzing" && "bg-primary animate-pulse",
107
+ status === "failed" && "bg-severity-critical",
108
+ status === "idle" && "bg-muted-foreground"
109
+ )}
110
+ />
111
+ </button>
112
+ </div>
113
+ );
114
+ }
src/routes/dashboard.index.tsx CHANGED
@@ -3,6 +3,7 @@ import { useEffect, useState, useCallback } from "react";
3
  import { supabase } from "@/integrations/supabase/client";
4
  import { UploadDialog } from "@/components/upload-dialog";
5
  import { AgentPipeline } from "@/components/agent-pipeline";
 
6
  import { SeverityBadge } from "@/components/severity-badge";
7
  import { ScrollArea } from "@/components/ui/scroll-area";
8
  import { Button } from "@/components/ui/button";
@@ -109,6 +110,13 @@ function IncidentsPage() {
109
  {selected ? <IncidentDetail incident={selected} onRerun={() => rerun(selected.id)} onDelete={() => remove(selected.id)} /> : <DetailEmpty />}
110
  </ScrollArea>
111
  </div>
 
 
 
 
 
 
 
112
  </>
113
  );
114
  }
 
3
  import { supabase } from "@/integrations/supabase/client";
4
  import { UploadDialog } from "@/components/upload-dialog";
5
  import { AgentPipeline } from "@/components/agent-pipeline";
6
+ import { QwenBot } from "@/components/qwen-bot";
7
  import { SeverityBadge } from "@/components/severity-badge";
8
  import { ScrollArea } from "@/components/ui/scroll-area";
9
  import { Button } from "@/components/ui/button";
 
110
  {selected ? <IncidentDetail incident={selected} onRerun={() => rerun(selected.id)} onDelete={() => remove(selected.id)} /> : <DetailEmpty />}
111
  </ScrollArea>
112
  </div>
113
+
114
+ <QwenBot
115
+ status={(selected?.status as any) ?? "idle"}
116
+ summary={selected?.analysis?.summary}
117
+ severity={selected?.severity}
118
+ error={selected?.error}
119
+ />
120
  </>
121
  );
122
  }