|
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
|
|