1 import { useState, useRef, useEffect } from "react";
2 import { Button } from "@/components/ui/button";
3 import { Input } from "@/components/ui/input";
4 import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
5 import { Badge } from "@/components/ui/badge";
6 import {
7 Bot,
8 Send,
9 X,
10 User,
11 Sparkles,
12 BookOpen,
13 Layers,
14 Zap,
15 ExternalLink,
16 RotateCcw,
17 Wrench,
18 Maximize2,
19 Minimize2
20 } from "lucide-react";
21 import { motion, AnimatePresence } from "framer-motion";
22 import { ScrollArea } from "@/components/ui/scroll-area";
23 import GuideDetailModal from "./guide-detail-modal";
24 import ManualDetailModal from "./manual-detail-modal";
25 import { TECHNICAL_MANUALS, type TechnicalManual } from "../../../shared/technical-manuals";
26 import type { RepairGuide } from "@shared/schema";
27
28 type Message = {
29 id: string;
30 role: "user" | "assistant";
31 content: string;
32 suggestedGuides?: {
33 id: string;
34 title: string;
35 category: string;
36 difficulty: string;
37 estimatedTime: string;
38 }[];
39 suggestedManuals?: {
40 id: string;
41 title: string;
42 code: string;
43 category: string;
44 }[];
45 voltageRailsMentioned?: {
46 rail: string;
47 voltage: string;
48 location: string;
49 description: string;
50 }[];
51 quickActions?: string[];
52 timestamp: string;
53 };
54
55 const INITIAL_MESSAGE: Message = {
56 id: "msg-init",
57 role: "assistant",
58 content: "Hello! I'm your Master Technical Repair AI. I have full indexed access to all **624 multi-brand repair guides** and **8 technical manuals** (power rails, torque specs, schematics, and crisis flash protocols). How can I assist with your repair?",
59 quickActions: [
60 "Troubleshoot No Power / Dead Board",
61 "BSOD Minidump & Driver Triage",
62 "Thermal Throttling & PTM7950 Repasting",
63 "Display & Hinge Replacement Protocol"
64 ],
65 timestamp: "Just now"
66 };
67
68 export default function AIAssistant() {
69 const [isOpen, setIsOpen] = useState(false);
70 const [isExpanded, setIsExpanded] = useState(false);
71 const [messages, setMessages] = useState<Message[]>([INITIAL_MESSAGE]);
72 const [input, setInput] = useState("");
73 const [isTyping, setIsTyping] = useState(false);
74 const scrollRef = useRef<HTMLDivElement>(null);
75
76 // Modal inspection states
77 const [selectedGuideId, setSelectedGuideId] = useState<string | null>(null);
78 const [selectedManual, setSelectedManual] = useState<TechnicalManual | null>(null);
79
80 useEffect(() => {
81 if (scrollRef.current) {
82 scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
83 }
84 }, [messages, isTyping]);
85
86 const handleOpenGuideById = (guideId: string) => {
87 setSelectedGuideId(guideId);
88 };
89
90 const handleOpenManualById = (manualId: string) => {
91 const manual = TECHNICAL_MANUALS.find(m => m.id === manualId);
92 if (manual) setSelectedManual(manual);
93 };
94
95 const handleSendQuery = async (queryText?: string) => {
96 const textToSend = (queryText || input).trim();
97 if (!textToSend || isTyping) return;
98
99 const userMessage: Message = {
100 id: `usr-${Date.now()}`,
101 role: "user",
102 content: textToSend,
103 timestamp: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
104 };
105
106 const newMessages = [...messages, userMessage];
107 setMessages(newMessages);
108 setInput("");
109 setIsTyping(true);
110
111 try {
112 // Call live Technical AI endpoint
113 const response = await fetch("/api/ai/chat", {
114 method: "POST",
115 headers: { "Content-Type": "application/json" },
116 body: JSON.stringify({
117 messages: newMessages.map(m => ({ role: m.role, content: m.content })),
118 })
119 });
120
121 if (response.ok) {
122 const data = await response.json();
123 const assistantMessage: Message = {
124 id: `ai-${Date.now()}`,
125 role: "assistant",
126 content: data.message || "Analysis complete.",
127 suggestedGuides: data.suggestedGuides,
128 suggestedManuals: data.suggestedManuals,
129 voltageRailsMentioned: data.voltageRailsMentioned,
130 quickActions: data.quickActions,
131 timestamp: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
132 };
133 setMessages(prev => [...prev, assistantMessage]);
134 } else {
135 throw new Error("Server response was not ok");
136 }
137 } catch (error) {
138 // Generate instant local fallback
139 const q = textToSend.toLowerCase();
140 let replyContent = "Here are the recommended procedures from our 624 repair guides:";
141 let suggestedManuals = TECHNICAL_MANUALS.slice(0, 2);
142 let voltageRailsMentioned = [
143 { rail: "+19V_DCIN", voltage: "19.0V - 20.0V", location: "DC Jack / Input FETs", description: "Primary system power" },
144 { rail: "+3.3V_ALW", voltage: "3.3V", location: "Coil PL301", description: "Standby power for Super I/O & power sequencing" }
145 ];
146
147 if (q.includes("power") || q.includes("dead") || q.includes("charge")) {
148 replyContent = "### ⚡ Power Rail Bench Sequence:\n1. Verify **+19V_DCIN** or **+20V_PD**.\n2. Measure standby coils: **+3.3V_ALW (PL301)** and **+5.0V_ALW (PL302)**.\n3. Verify **NBSWON#** power button pull-down signal to Super I/O EC.";
149 } else if (q.includes("bsod") || q.includes("crash")) {
150 replyContent = "### 🛑 BSOD & Crash Resolution:\n1. Run **DDU in Safe Mode** and reinstall clean WHQL drivers.\n2. Execute `DISM /Online /Cleanup-Image /RestoreHealth` and `sfc /scannow`.\n3. Run 4 passes of **MemTest86** to test DDR4/DDR5 RAM.";
151 } else if (q.includes("heat") || q.includes("fan") || q.includes("temp")) {
152 replyContent = "### 🔥 Thermal Protocol:\n1. Clean copper exhaust radiator fins.\n2. Apply **Honeywell PTM7950 (0.25mm)** phase-change pad.\n3. Tighten heatsink screws diagonally (1->2->3->4) to 0.25 Nm.";
153 }
154
155 setMessages(prev => [
156 ...prev,
157 {
158 id: `ai-${Date.now()}`,
159 role: "assistant",
160 content: replyContent,
161 suggestedManuals,
162 voltageRailsMentioned,
163 quickActions: [
164 "Check Standby Power Rails",
165 "Run Diagnostic Flowchart",
166 "Open Software Hub"
167 ],
168 timestamp: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
169 }
170 ]);
171 } finally {
172 setIsTyping(false);
173 }
174 };
175
176 const handleResetChat = () => {
177 setMessages([INITIAL_MESSAGE]);
178 };
179
180 return (
181 <>
182 {/* Floating Toggle Button */}
183 <button
184 onClick={() => setIsOpen(true)}
185 className="fixed bottom-18 sm:bottom-6 right-4 sm:right-6 h-12 w-12 sm:h-13 sm:w-13 rounded-2xl bg-primary text-primary-foreground shadow-2xl flex items-center justify-center hover:scale-110 active:scale-95 transition-all z-40 group"
186 title="Open Technical Repair AI"
187 >
188 <Bot className="h-5 w-5 sm:h-6 sm:w-6 transition-transform group-hover:rotate-12" />
189 <div className="absolute -top-1 -right-1 h-3.5 w-3.5 bg-emerald-500 rounded-full border-2 border-background animate-pulse" />
190 </button>
191
192 <AnimatePresence>
193 {isOpen && (
194 <motion.div
195 initial={{ opacity: 0, y: 30, scale: 0.92 }}
196 animate={{ opacity: 1, y: 0, scale: 1 }}
197 exit={{ opacity: 0, y: 30, scale: 0.92 }}
198 transition={{ duration: 0.2 }}
199 className={`fixed bottom-18 sm:bottom-6 right-3 sm:right-6 z-50 flex flex-col transition-all ${
200 isExpanded
201 ? "w-[95vw] sm:w-[650px] h-[80vh] max-h-[750px]"
202 : "w-[94vw] sm:w-[440px] h-[540px] max-h-[80vh]"
203 }`}
204 >
205 <Card className="flex-1 border-border/40 shadow-2xl rounded-3xl overflow-hidden bg-card/95 backdrop-blur-3xl flex flex-col">
206
207 {/* Header */}
208 <CardHeader className="bg-primary/10 border-b border-border/40 py-3.5 px-4 flex flex-row items-center justify-between">
209 <div className="flex items-center gap-3">
210 <div className="h-10 w-10 rounded-xl bg-primary text-primary-foreground flex items-center justify-center shadow-sm">
211 <Bot className="h-5 w-5" />
212 </div>
213 <div>
214 <div className="flex items-center gap-2">
215 <CardTitle className="text-sm font-black">Technical Repair AI</CardTitle>
216 <Badge variant="outline" className="text-[10px] bg-primary/10 text-primary border-primary/20 py-0 px-1.5 font-bold">
217 624 Guides Live
218 </Badge>
219 </div>
220 <div className="flex items-center gap-1 text-[10px] text-emerald-600 dark:text-emerald-400 font-bold uppercase tracking-wider">
221 <div className="h-1.5 w-1.5 rounded-full bg-emerald-500 animate-pulse" /> Hardware Diagnostics Active
222 </div>
223 </div>
224 </div>
225
226 <div className="flex items-center gap-1">
227 <Button
228 variant="ghost"
229 size="icon"
230 onClick={handleResetChat}
231 className="rounded-full h-8 w-8 text-muted-foreground hover:text-foreground"
232 title="Clear Conversation"
233 >
234 <RotateCcw className="h-3.5 w-3.5" />
235 </Button>
236 <Button
237 variant="ghost"
238 size="icon"
239 onClick={() => setIsExpanded(!isExpanded)}
240 className="rounded-full h-8 w-8 text-muted-foreground hover:text-foreground hidden sm:flex"
241 title={isExpanded ? "Minimize" : "Expand"}
242 >
243 {isExpanded ? <Minimize2 className="h-3.5 w-3.5" /> : <Maximize2 className="h-3.5 w-3.5" />}
244 </Button>
245 <Button
246 variant="ghost"
247 size="icon"
248 onClick={() => setIsOpen(false)}
249 className="rounded-full h-8 w-8 text-muted-foreground hover:text-foreground"
250 >
251 <X className="h-4 w-4" />
252 </Button>
253 </div>
254 </CardHeader>
255
256 {/* Message List */}
257 <CardContent className="flex-1 flex flex-col p-0 overflow-hidden bg-background/50">
258 <div ref={scrollRef} className="flex-1 p-4 overflow-y-auto space-y-4">
259 {messages.map((msg) => (
260 <div key={msg.id} className={`flex flex-col ${msg.role === "user" ? "items-end" : "items-start"}`}>
261 <div className={`max-w-[88%] p-3.5 rounded-2xl text-xs sm:text-sm leading-relaxed ${
262 msg.role === "user"
263 ? "bg-primary text-primary-foreground rounded-tr-none shadow-sm"
264 : "bg-card border border-border/40 shadow-sm rounded-tl-none text-card-foreground"
265 }`}>
266 <div className="whitespace-pre-wrap">{msg.content}</div>
267
268 {/* Voltage Rails if mentioned */}
269 {msg.voltageRailsMentioned && msg.voltageRailsMentioned.length > 0 && (
270 <div className="mt-3 pt-2.5 border-t border-border/30 space-y-1.5">
271 <div className="flex items-center gap-1.5 text-[11px] font-bold text-amber-500">
272 <Zap className="h-3.5 w-3.5" /> Multimeter Test Points:
273 </div>
274 <div className="grid grid-cols-1 gap-1.5">
275 {msg.voltageRailsMentioned.map((vr, vIdx) => (
276 <div key={vIdx} className="p-2 rounded-xl bg-amber-500/10 border border-amber-500/20 text-[11px] space-y-0.5">
277 <div className="flex items-center justify-between font-bold">
278 <span className="font-mono text-amber-600 dark:text-amber-400">{vr.rail}</span>
279 <Badge variant="outline" className="text-[9px] py-0 px-1 font-mono font-bold bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/30">
280 {vr.voltage}
281 </Badge>
282 </div>
283 <div className="text-muted-foreground text-[10px]">Test Location: {vr.location}</div>
284 <div className="text-foreground/80 text-[10px]">{vr.description}</div>
285 </div>
286 ))}
287 </div>
288 </div>
289 )}
290
291 {/* Suggested Guides */}
292 {msg.suggestedGuides && msg.suggestedGuides.length > 0 && (
293 <div className="mt-3 pt-2.5 border-t border-border/30 space-y-1.5">
294 <div className="flex items-center gap-1.5 text-[11px] font-bold text-primary">
295 <BookOpen className="h-3.5 w-3.5" /> Matching Verified Guides:
296 </div>
297 <div className="space-y-1">
298 {msg.suggestedGuides.map((g, gIdx) => (
299 <button
300 key={gIdx}
301 onClick={() => handleOpenGuideById(g.id)}
302 className="w-full text-left p-2 rounded-xl bg-primary/5 hover:bg-primary/10 border border-primary/20 transition-all flex items-center justify-between group"
303 >
304 <div className="truncate mr-2">
305 <div className="font-bold text-xs text-foreground truncate group-hover:text-primary">{g.title}</div>
306 <div className="text-[10px] text-muted-foreground">{g.category} • {g.estimatedTime} • {g.difficulty}</div>
307 </div>
308 <ExternalLink className="h-3.5 w-3.5 text-primary flex-shrink-0" />
309 </button>
310 ))}
311 </div>
312 </div>
313 )}
314
315 {/* Suggested Technical Manuals */}
316 {msg.suggestedManuals && msg.suggestedManuals.length > 0 && (
317 <div className="mt-3 pt-2.5 border-t border-border/30 space-y-1.5">
318 <div className="flex items-center gap-1.5 text-[11px] font-bold text-emerald-600 dark:text-emerald-400">
319 <Layers className="h-3.5 w-3.5" /> Technical Manuals & Schematics:
320 </div>
321 <div className="space-y-1">
322 {msg.suggestedManuals.map((m, mIdx) => (
323 <button
324 key={mIdx}
325 onClick={() => handleOpenManualById(m.id)}
326 className="w-full text-left p-2 rounded-xl bg-emerald-500/5 hover:bg-emerald-500/10 border border-emerald-500/20 transition-all flex items-center justify-between group"
327 >
328 <div className="truncate mr-2">
329 <div className="font-bold text-xs text-foreground truncate group-hover:text-emerald-600 dark:group-hover:text-emerald-400">{m.title}</div>
330 <div className="text-[10px] text-muted-foreground">Code: {m.code} • {m.category}</div>
331 </div>
332 <ExternalLink className="h-3.5 w-3.5 text-emerald-500 flex-shrink-0" />
333 </button>
334 ))}
335 </div>
336 </div>
337 )}
338
339 {/* Quick action followups */}
340 {msg.quickActions && msg.quickActions.length > 0 && (
341 <div className="mt-3 pt-2 flex flex-wrap gap-1.5">
342 {msg.quickActions.map((qa, qaIdx) => (
343 <button
344 key={qaIdx}
345 onClick={() => handleSendQuery(qa)}
346 className="text-[10px] font-semibold px-2.5 py-1 rounded-full bg-muted/60 hover:bg-primary/10 hover:text-primary border border-border/40 transition-colors text-left"
347 >
348 ⚡ {qa}
349 </button>
350 ))}
351 </div>
352 )}
353 </div>
354 <span className="text-[9px] text-muted-foreground mt-1 px-1">{msg.timestamp}</span>
355 </div>
356 ))}
357
358 {isTyping && (
359 <div className="flex flex-col items-start">
360 <div className="bg-card border border-border/40 p-3 rounded-2xl rounded-tl-none shadow-sm flex items-center gap-1.5">
361 <div className="h-2 w-2 bg-primary rounded-full animate-bounce" />
362 <div className="h-2 w-2 bg-primary rounded-full animate-bounce [animation-delay:0.15s]" />
363 <div className="h-2 w-2 bg-primary rounded-full animate-bounce [animation-delay:0.3s]" />
364 <span className="text-[11px] text-muted-foreground ml-1.5 font-semibold">Analyzing schematics & 624 guides...</span>
365 </div>
366 </div>
367 )}
368 </div>
369
370 {/* Input Bar */}
371 <div className="p-3 border-t border-border/40 bg-card/80 backdrop-blur-md">
372 <div className="flex gap-2">
373 <Input
374 value={input}
375 onChange={(e) => setInput(e.target.value)}
376 onKeyDown={(e) => e.key === "Enter" && !e.shiftKey && handleSendQuery()}
377 placeholder="Ask about voltage rails, torque, teardowns, BSOD..."
378 className="rounded-xl bg-muted/30 border-border/40 focus-visible:ring-primary/20 h-10 text-xs sm:text-sm"
379 />
380 <Button
381 onClick={() => handleSendQuery()}
382 disabled={!input.trim() || isTyping}
383 className="rounded-xl h-10 w-10 p-0 shadow-md bg-primary hover:bg-primary/90 text-primary-foreground flex-shrink-0"
384 >
385 <Send className="h-4 w-4" />
386 </Button>
387 </div>
388 </div>
389 </CardContent>
390 </Card>
391 </motion.div>
392 )}
393 </AnimatePresence>
394
395 {/* Guide Detail Modal */}
396 {selectedGuideId && (
397 <GuideDetailModal
398 guideId={selectedGuideId}
399 onClose={() => setSelectedGuideId(null)}
400 />
401 )}
402
403 {/* Manual Detail Modal */}
404 {selectedManual && (
405 <ManualDetailModal
406 manual={selectedManual}
407 onClose={() => setSelectedManual(null)}
408 />
409 )}
410 </>
411 );
412 }
413