Changeset 505f39a for lan-frontend/src/components/AIAssistantDrawer.jsx
- Timestamp:
- 01/21/26 13:35:44 (6 months ago)
- Branches:
- master
- Children:
- d42aac3
- Parents:
- 2058e5c
- File:
-
- 1 edited
-
lan-frontend/src/components/AIAssistantDrawer.jsx (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
lan-frontend/src/components/AIAssistantDrawer.jsx
r2058e5c r505f39a 1 import React from "react";1 import React, {useEffect, useRef, useState} from "react"; 2 2 3 import { useEffect, useRef, useState } from "react"; 3 export default function AIAssistantDrawer({ 4 open, 5 onClose, 6 computers, 7 scope, 8 setScope, 9 apiBase = "", // ако сакаш можеш да пратиш VITE_API_BASE тука 10 }) { 11 const [messages, setMessages] = useState([ 12 { 13 role: "assistant", 14 text: "Постави прашање за Sysmon, процеси, мрежа. Можеш да избереш компјутер (scope) горе.", 15 }, 16 ]); 17 const [input, setInput] = useState(""); 18 const [sending, setSending] = useState(false); 19 const bottomRef = useRef(null); 4 20 5 export default function AIAssistantDrawer({ open, onClose, computers, scope, setScope }) { 6 const [messages, setMessages] = useState([ 7 { role: "assistant", text: "Постави прашање за Sysmon, процеси, мрежа. Можеш да избереш компјутер (scope) горе." }, 8 ]); 9 const [input, setInput] = useState(""); 10 const [sending, setSending] = useState(false); 11 const bottomRef = useRef(null); 21 useEffect(() => { 22 if (open) { 23 // затвори со Escape 24 const onKey = (e) => { 25 if (e.key === "Escape") onClose?.(); 26 }; 27 window.addEventListener("keydown", onKey); 28 return () => window.removeEventListener("keydown", onKey); 29 } 30 }, [open, onClose]); 12 31 13 useEffect(() => {14 if (bottomRef.current) bottomRef.current.scrollIntoView({ behavior: "smooth"});15 }, [messages, open]);32 useEffect(() => { 33 if (bottomRef.current) bottomRef.current.scrollIntoView({behavior: "smooth"}); 34 }, [messages, open]); 16 35 17 async function send() {18 const q = input.trim();19 if (!q || sending) return;36 async function send() { 37 const q = input.trim(); 38 if (!q || sending) return; 20 39 21 setInput("");22 setMessages((m) => [...m, { role: "user", text: q}]);23 setSending(true);40 setInput(""); 41 setMessages((m) => [...m, {role: "user", text: q}]); 42 setSending(true); 24 43 25 try { 26 const r = await fetch("/api/chat", { 27 method: "POST", 28 headers: { "Content-Type": "application/json" }, 29 body: JSON.stringify({ question: q, computer_name: scope || null }), 30 }); 31 const data = await r.json(); 32 setMessages((m) => [...m, { role: "assistant", text: data.answer || "(no answer)" }]); 33 } catch (e) { 34 setMessages((m) => [...m, { role: "assistant", text: `Грешка: ${String(e.message || e)}` }]); 35 } finally { 36 setSending(false); 44 try { 45 const base = (apiBase || "").replace(/\/$/, ""); // тргни trailing / 46 const url = `${base}/api/chat`; 47 48 const r = await fetch(url, { 49 method: "POST", 50 headers: {"Content-Type": "application/json"}, 51 credentials: "include", 52 body: JSON.stringify({question: q, computer_name: scope || null}), 53 }); 54 55 56 const data = await r.json().catch(() => ({})); 57 if (!r.ok) { 58 setMessages((m) => [...m, { 59 role: "assistant", 60 text: `Error ${r.status}: ${data.error || "Request failed"}` 61 }]); 62 } else { 63 setMessages((m) => [...m, {role: "assistant", text: data.answer || "(no answer)"}]); 64 } 65 setMessages((m) => [ 66 ...m, 67 {role: "assistant", text: data.answer || "(no answer)"}, 68 ]); 69 } catch (e) { 70 setMessages((m) => [ 71 ...m, 72 {role: "assistant", text: `Грешка: ${String(e?.message || e)}`}, 73 ]); 74 } finally { 75 setSending(false); 76 } 37 77 } 38 }39 78 40 if (!open) return null;79 if (!open) return null; 41 80 42 return ( 43 <> 44 <div className="drawer-backdrop" onMouseDown={onClose} /> 45 <div className="drawer" onMouseDown={(e) => e.stopPropagation()}> 46 <div className="drawer-head"> 47 <div> 48 <div className="drawer-title">🤖 AI Assistant</div> 49 <div className="drawer-sub">Uses /api/chat (RAG over your logs)</div> 50 </div> 51 <button className="btn" onClick={onClose}>Close</button> 52 </div> 81 return ( 82 <> 83 <div className="aiDrawer-backdrop" onMouseDown={onClose}/> 53 84 54 <div style={{ padding: "12px 16px", borderBottom: "1px solid var(--border)" }}>55 <div className="row">56 <select className="select" value={scope || ""} onChange={(e) => setScope(e.target.value || null)}>57 <option value="">All computers</option>58 {(computers || []).map((c) => (59 <option key={c.name} value={c.name}>{c.name}</option>60 ))}61 </select>62 </div>63 <div className="small">Tip: “Top процеси по RAM” или “сомнителни sysmon events”</div>64 </div>85 <aside 86 className="aiDrawer" 87 role="dialog" 88 aria-modal="true" 89 onMouseDown={(e) => e.stopPropagation()} 90 > 91 <header className="aiDrawer-head"> 92 <div> 93 <div className="aiDrawer-title">🤖 AI Assistant</div> 94 <div className="aiDrawer-sub">RAG over your logs</div> 95 </div> 65 96 66 <div className="drawer-body"> 67 {messages.map((m, i) => ( 68 <div key={i} className={`bubble ${m.role === "user" ? "user" : ""}`}> 69 <div style={{ fontSize: 11, color: "var(--dim)", marginBottom: 6 }}> 70 {m.role === "user" ? "You" : "Assistant"} 71 </div> 72 <div style={{ whiteSpace: "pre-wrap" }}>{m.text}</div> 73 </div> 74 ))} 75 <div ref={bottomRef} /> 76 </div> 97 <button className="aiBtn" onClick={onClose} aria-label="Close"> 98 ✕ 99 </button> 100 </header> 77 101 78 <div className="drawer-foot"> 79 <div className="row"> 80 <textarea 81 className="textarea" 102 <div className="aiDrawer-toolbar"> 103 <select 104 className="aiSelect" 105 value={scope || ""} 106 onChange={(e) => setScope(e.target.value || null)} 107 > 108 <option value="">All computers</option> 109 {(computers || []).map((c) => ( 110 <option key={c.name} value={c.name}> 111 {c.name} 112 </option> 113 ))} 114 </select> 115 116 <div className="aiHint"> 117 Tip: “Top процеси по RAM” / “сомнителни sysmon events” 118 </div> 119 </div> 120 121 <main className="aiDrawer-body"> 122 {messages.map((m, i) => ( 123 <div 124 key={i} 125 className={`aiMsg ${m.role === "user" ? "user" : "assistant"}`} 126 > 127 <div className="aiMsg-meta">{m.role === "user" ? "You" : "Assistant"}</div> 128 <div className="aiMsg-text">{m.text}</div> 129 </div> 130 ))} 131 <div ref={bottomRef}/> 132 </main> 133 134 <footer className="aiDrawer-foot"> 135 <textarea 136 className="aiTextarea" 82 137 rows={2} 83 138 value={input} … … 85 140 onChange={(e) => setInput(e.target.value)} 86 141 onKeyDown={(e) => { 87 if (e.key === "Enter" && !e.shiftKey) {88 e.preventDefault();89 send();90 }142 if (e.key === "Enter" && !e.shiftKey) { 143 e.preventDefault(); 144 send(); 145 } 91 146 }} 92 /> 93 <button className="btn primary" onClick={send} disabled={sending || !input.trim()}> 94 {sending ? "Sending…" : "Send"} 95 </button> 96 </div> 97 </div> 98 </div> 99 </> 100 ); 147 /> 148 <button 149 className="aiBtn aiBtnPrimary" 150 onClick={send} 151 disabled={sending || !input.trim()} 152 > 153 {sending ? "Sending…" : "Send"} 154 </button> 155 </footer> 156 </aside> 157 </> 158 ); 101 159 }
Note:
See TracChangeset
for help on using the changeset viewer.
