mirror of
https://github.com/JamesTheGiblet/BuddAI.git
synced 2026-01-08 21:58:40 +00:00
- Implemented tests for database initialization, SQL injection prevention, auto-learning pattern extraction, module detection, complexity detection, LRU cache performance, session export, actionable suggestions, repository indexing, search query safety, and context window management. - Utilized SQLite for database operations and temporary directories for test isolation. - Included detailed output for test results with color-coded pass/fail indicators.
448 lines
No EOL
24 KiB
HTML
448 lines
No EOL
24 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<link rel="icon" href="/favicon.ico">
|
|
<title>BuddAI Web</title>
|
|
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
|
|
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
|
|
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
|
<link id="hljs-theme" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css">
|
|
<style>
|
|
:root {
|
|
--bg-color: #1e1e1e;
|
|
--text-color: #d4d4d4;
|
|
--header-bg: #252526;
|
|
--border-color: #3e3e3e;
|
|
--input-bg: #3c3c3c;
|
|
--user-msg-bg: #007acc;
|
|
--user-msg-text: white;
|
|
--assistant-msg-bg: #2d2d2d;
|
|
--btn-bg: #0e639c;
|
|
--btn-hover: #1177bb;
|
|
--code-bg: #111;
|
|
--code-border: #444;
|
|
--code-text: #9cdcfe;
|
|
}
|
|
body.light-mode {
|
|
--bg-color: #ffffff;
|
|
--text-color: #333333;
|
|
--header-bg: #f3f3f3;
|
|
--border-color: #e1e1e1;
|
|
--input-bg: #ffffff;
|
|
--user-msg-bg: #0078d4;
|
|
--user-msg-text: white;
|
|
--assistant-msg-bg: #f4f4f4;
|
|
--btn-bg: #0078d4;
|
|
--btn-hover: #106ebe;
|
|
--code-bg: #f6f8fa;
|
|
--code-border: #d1d5da;
|
|
--code-text: #24292e;
|
|
}
|
|
.sidebar-left { width: 260px; background: var(--header-bg); border-right: 1px solid var(--border-color); display: flex; flex-direction: column; flex-shrink: 0; }
|
|
.session-list { flex: 1; overflow-y: auto; }
|
|
.session-item { padding: 12px 15px; cursor: pointer; border-bottom: 1px solid var(--border-color); font-size: 0.85em; color: var(--text-color); transition: background 0.2s; }
|
|
.session-item:hover { background: var(--bg-color); }
|
|
.session-item.active { background: var(--btn-bg); color: white; border-color: var(--btn-bg); }
|
|
.new-chat-btn { margin: 15px; padding: 10px; background: var(--btn-bg); color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; }
|
|
.new-chat-btn:hover { background: var(--btn-hover); }
|
|
.session-date { font-weight: bold; display: block; margin-bottom: 4px; }
|
|
.session-id { font-size: 0.8em; opacity: 0.6; font-family: monospace; }
|
|
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: var(--bg-color); color: var(--text-color); margin: 0; display: flex; justify-content: center; height: 100vh; transition: background 0.3s, color 0.3s; }
|
|
#root { width: 100%; max-width: 100%; display: flex; flex-direction: column; height: 100%; }
|
|
.main-layout { display: flex; flex: 1; overflow: hidden; }
|
|
.chat-section { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
|
.chat-container { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 15px; }
|
|
.side-panel { width: 45%; border-left: 1px solid var(--border-color); display: flex; flex-direction: column; background: var(--bg-color); }
|
|
.side-header { padding: 10px 15px; background: var(--header-bg); border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; font-size: 0.9em; }
|
|
.side-content { flex: 1; overflow: auto; background: var(--code-bg); position: relative; }
|
|
.side-content pre { margin: 0; padding: 15px; min-height: 100%; box-sizing: border-box; }
|
|
.message { padding: 15px; border-radius: 8px; max-width: 85%; line-height: 1.5; }
|
|
.timestamp { font-size: 0.75em; opacity: 0.7; margin-bottom: 4px; display: block; font-family: monospace; }
|
|
.user { align-self: flex-end; background: var(--user-msg-bg); color: var(--user-msg-text); }
|
|
.assistant { align-self: flex-start; background: var(--assistant-msg-bg); border: 1px solid var(--border-color); }
|
|
.input-area { padding: 20px; background: var(--header-bg); border-top: 1px solid var(--border-color); display: flex; gap: 10px; }
|
|
input { flex: 1; padding: 12px; border-radius: 4px; border: 1px solid var(--border-color); background: var(--input-bg); color: var(--text-color); outline: none; }
|
|
button { padding: 12px 24px; background: var(--btn-bg); color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; }
|
|
button:hover { background: var(--btn-hover); }
|
|
.stop-btn { background: #d32f2f; }
|
|
.stop-btn:hover { background: #b71c1c; }
|
|
.header { padding: 15px 20px; background: var(--header-bg); border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; }
|
|
.clear-btn { background: transparent; border: 1px solid var(--border-color); color: var(--text-color); padding: 5px 12px; font-size: 0.8em; cursor: pointer; border-radius: 4px; opacity: 0.8; }
|
|
.clear-btn:hover { background: var(--border-color); opacity: 1; }
|
|
.status-badge { font-size: 0.7rem; padding: 2px 8px; border-radius: 10px; margin-left: 10px; text-transform: uppercase; font-weight: bold; letter-spacing: 0.5px; }
|
|
.online { color: #4caf50; border: 1px solid #4caf50; background: rgba(76, 175, 80, 0.1); }
|
|
.offline { color: #f44336; border: 1px solid #f44336; background: rgba(244, 67, 54, 0.1); }
|
|
.connecting { color: #ff9800; border: 1px solid #ff9800; background: rgba(255, 152, 0, 0.1); }
|
|
.suggestions { margin-top: 10px; display: flex; flex-wrap: wrap; gap: 8px; }
|
|
.suggestion-pill { background: var(--bg-color); border: 1px solid var(--btn-bg); color: var(--btn-bg); padding: 6px 12px; border-radius: 15px; font-size: 0.85em; cursor: pointer; transition: all 0.2s; }
|
|
.suggestion-pill:hover { background: var(--btn-bg); color: white; }
|
|
|
|
/* Code Blocks */
|
|
.code-wrapper { position: relative; margin: 10px 0; border: 1px solid var(--code-border); border-radius: 6px; overflow: hidden; }
|
|
.code-header { display: flex; justify-content: space-between; align-items: center; background: var(--header-bg); padding: 5px 10px; font-size: 0.8em; border-bottom: 1px solid var(--border-color); color: var(--text-color); opacity: 0.8; }
|
|
.copy-code-btn { background: transparent; border: 1px solid var(--border-color); color: var(--text-color); padding: 2px 8px; font-size: 0.9em; cursor: pointer; border-radius: 3px; }
|
|
.copy-code-btn:hover { background: var(--border-color); }
|
|
pre { background: var(--code-bg); padding: 15px; margin: 0; overflow-x: auto; }
|
|
code { font-family: 'Consolas', 'Courier New', monospace; color: var(--code-text); }
|
|
p { margin: 0 0 10px 0; }
|
|
|
|
@keyframes flame-flicker {
|
|
0% { transform: scale(1); filter: drop-shadow(0 0 2px #ff9800); }
|
|
50% { transform: scale(1.1) rotate(-2deg); filter: drop-shadow(0 0 6px #ff4500); }
|
|
100% { transform: scale(1) rotate(2deg); filter: drop-shadow(0 0 2px #ff9800); }
|
|
}
|
|
.loading-flame { font-size: 24px; animation: flame-flicker 0.6s infinite; display: inline-block; }
|
|
.hljs { background: transparent !important; padding: 0 !important; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="root"></div>
|
|
<script type="text/babel">
|
|
// Configure Marked for Code Copy
|
|
const renderer = new marked.Renderer();
|
|
renderer.code = (code, language) => {
|
|
// Handle Marked v12+ signature where first arg is a token object
|
|
if (typeof code === 'object' && code !== null && code.text) {
|
|
language = code.lang;
|
|
code = code.text;
|
|
}
|
|
|
|
const validLang = (language && hljs.getLanguage(language)) ? language : 'plaintext';
|
|
let highlighted = code;
|
|
try {
|
|
const result = hljs.highlight(code, { language: validLang });
|
|
highlighted = result.value || code;
|
|
} catch (e) { /* fallback */ }
|
|
|
|
return `<div class="code-wrapper">
|
|
<div class="code-header">
|
|
<span>${language || 'text'}</span>
|
|
<div style="display:flex; gap:5px;">
|
|
<button class="copy-code-btn" onclick="window.copyToClipboard(this)">Copy</button>
|
|
<button class="copy-code-btn" onclick="window.sendToSidebar(this)">Sidebar</button>
|
|
</div>
|
|
</div>
|
|
<pre><code class="hljs ${validLang}">${highlighted}</code></pre>
|
|
</div>`;
|
|
};
|
|
marked.setOptions({ renderer });
|
|
|
|
window.copyToClipboard = (btn) => {
|
|
const wrapper = btn.closest('.code-wrapper');
|
|
const code = wrapper.querySelector('code').innerText;
|
|
navigator.clipboard.writeText(code).then(() => {
|
|
const original = btn.innerText;
|
|
btn.innerText = 'Copied!';
|
|
setTimeout(() => btn.innerText = original, 2000);
|
|
});
|
|
};
|
|
|
|
window.sendToSidebar = (btn) => {
|
|
const wrapper = btn.closest('.code-wrapper');
|
|
const code = wrapper.querySelector('code').innerText;
|
|
if (window.updateSidebar) window.updateSidebar(code);
|
|
};
|
|
|
|
window.downloadCode = (content) => {
|
|
if (!content) return;
|
|
if (typeof content !== 'string') return;
|
|
let ext = 'txt';
|
|
|
|
// Heuristic for Arduino
|
|
if (content.includes('void setup()') && content.includes('void loop()')) {
|
|
ext = 'ino';
|
|
} else {
|
|
try {
|
|
const result = hljs.highlightAuto(content);
|
|
if (result.language) {
|
|
const map = {
|
|
python: 'py', javascript: 'js', cpp: 'cpp', c: 'c',
|
|
arduino: 'ino', html: 'html', css: 'css',
|
|
csharp: 'cs', java: 'java', bash: 'sh', json: 'json',
|
|
markdown: 'md', sql: 'sql'
|
|
};
|
|
ext = map[result.language] || result.language;
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
|
|
const blob = new Blob([content], { type: 'text/plain' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `buddai_generated.${ext}`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
};
|
|
|
|
const { useState, useEffect, useRef } = React;
|
|
|
|
function App() {
|
|
const [history, setHistory] = useState([]);
|
|
const [sessions, setSessions] = useState([]);
|
|
const [currentSessionId, setCurrentSessionId] = useState(null);
|
|
const [input, setInput] = useState("");
|
|
const [loading, setLoading] = useState(false);
|
|
const [status, setStatus] = useState("connecting");
|
|
const [forgeMode, setForgeMode] = useState("2");
|
|
const [theme, setTheme] = useState("dark");
|
|
const [sidebarContent, setSidebarContent] = useState("");
|
|
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
|
const endRef = useRef(null);
|
|
const abortControllerRef = useRef(null);
|
|
|
|
const scrollToBottom = () => {
|
|
endRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
};
|
|
|
|
useEffect(() => {
|
|
window.updateSidebar = (code) => {
|
|
setSidebarContent(code);
|
|
setIsSidebarOpen(true);
|
|
};
|
|
|
|
document.body.className = theme === 'light' ? 'light-mode' : '';
|
|
const hljsTheme = document.getElementById('hljs-theme');
|
|
if (hljsTheme) {
|
|
hljsTheme.href = theme === 'light'
|
|
? "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-light.min.css"
|
|
: "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css";
|
|
}
|
|
}, [theme]);
|
|
|
|
useEffect(() => {
|
|
scrollToBottom();
|
|
}, [history]);
|
|
|
|
const fetchSessions = () => fetch("/api/sessions").then(res => res.json()).then(data => setSessions(data.sessions || [])).catch(console.error);
|
|
|
|
useEffect(() => {
|
|
// Check System Status
|
|
const checkStatus = async () => {
|
|
try {
|
|
const res = await fetch("/");
|
|
setStatus(res.ok ? "online" : "offline");
|
|
} catch {
|
|
setStatus("offline");
|
|
}
|
|
};
|
|
checkStatus();
|
|
const timer = setInterval(checkStatus, 10000);
|
|
|
|
fetchSessions();
|
|
// Load History
|
|
fetch("/api/history")
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (data.history) {
|
|
setHistory(data.history.filter(m => m.role === 'user' || m.role === 'assistant'));
|
|
}
|
|
})
|
|
.catch(console.error);
|
|
return () => clearInterval(timer);
|
|
}, []);
|
|
|
|
const loadSession = async (sessionId) => {
|
|
setLoading(true);
|
|
try {
|
|
const res = await fetch("/api/session/load", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ session_id: sessionId })
|
|
});
|
|
const data = await res.json();
|
|
setHistory(data.history || []);
|
|
setCurrentSessionId(data.session_id);
|
|
} catch (e) { console.error(e); }
|
|
setLoading(false);
|
|
};
|
|
|
|
const startNewSession = async () => {
|
|
const res = await fetch("/api/session/new", { method: "POST" });
|
|
const data = await res.json();
|
|
setCurrentSessionId(data.session_id);
|
|
setHistory([]);
|
|
fetchSessions();
|
|
};
|
|
|
|
const sendMessage = async (textOverride = null) => {
|
|
const msgText = typeof textOverride === 'string' ? textOverride : input;
|
|
if (!msgText.trim()) return;
|
|
|
|
const userMsg = { role: "user", content: msgText, timestamp: new Date().toISOString() };
|
|
setHistory(prev => [...prev, userMsg]);
|
|
if (!textOverride) setInput("");
|
|
setLoading(true);
|
|
|
|
// Cancel previous request if any
|
|
if (abortControllerRef.current) abortControllerRef.current.abort();
|
|
const controller = new AbortController();
|
|
abortControllerRef.current = controller;
|
|
|
|
try {
|
|
const res = await fetch("/api/chat", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ message: msgText, forge_mode: forgeMode }),
|
|
signal: controller.signal
|
|
});
|
|
const data = await res.json();
|
|
setHistory(prev => [...prev, { role: "assistant", content: data.response }]);
|
|
if (!currentSessionId) fetchSessions(); // Refresh list if this was first msg
|
|
} catch (err) {
|
|
if (err.name === 'AbortError') {
|
|
setHistory(prev => [...prev, { role: "assistant", content: "🛑 *Generation stopped by user.*" }]);
|
|
} else {
|
|
setHistory(prev => [...prev, { role: "assistant", content: "Error connecting to BuddAI server." }]);
|
|
}
|
|
}
|
|
setLoading(false);
|
|
abortControllerRef.current = null;
|
|
};
|
|
|
|
const stopGeneration = () => {
|
|
if (abortControllerRef.current) abortControllerRef.current.abort();
|
|
};
|
|
|
|
const handleFileUpload = async (e) => {
|
|
const file = e.target.files[0];
|
|
if (!file) return;
|
|
|
|
const formData = new FormData();
|
|
formData.append("file", file);
|
|
|
|
setLoading(true);
|
|
setHistory(prev => [...prev, { role: "assistant", content: `📥 Uploading and indexing **${file.name}**...` }]);
|
|
|
|
try {
|
|
const res = await fetch("/api/upload", { method: "POST", body: formData });
|
|
const data = await res.json();
|
|
setHistory(prev => [...prev, { role: "assistant", content: data.message }]);
|
|
} catch (err) {
|
|
setHistory(prev => [...prev, { role: "assistant", content: "❌ Upload failed." }]);
|
|
}
|
|
setLoading(false);
|
|
e.target.value = null;
|
|
};
|
|
|
|
const parseContent = (content) => {
|
|
const parts = content.split("\n\nPROACTIVE: > ");
|
|
const text = parts[0];
|
|
let suggestions = [];
|
|
if (parts.length > 1) {
|
|
// Split "1. Suggestion 2. Suggestion" patterns
|
|
suggestions = parts[1].split(/\d+\.\s/).map(s => s.trim()).filter(s => s);
|
|
}
|
|
return { text, suggestions };
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<div className="header">
|
|
<div style={{display:'flex', alignItems:'center'}}>
|
|
<img src="/favicon.ico" alt="BuddAI" style={{height: '24px', marginRight: '10px'}} />
|
|
<h3 style={{margin:0}}>BuddAI v3</h3>
|
|
<span className={`status-badge ${status}`}>{status}</span>
|
|
</div>
|
|
<div style={{display:'flex', gap:'10px'}}>
|
|
<input
|
|
type="file"
|
|
id="upload-input"
|
|
style={{display:'none'}}
|
|
onChange={handleFileUpload}
|
|
/>
|
|
<button className="clear-btn" onClick={() => document.getElementById('upload-input').click()}>📂 Upload</button>
|
|
<button className="clear-btn" onClick={() => setIsSidebarOpen(!isSidebarOpen)}>{isSidebarOpen ? 'Hide Code' : 'Show Code'}</button>
|
|
<button className="clear-btn" onClick={() => setTheme(t => t === 'dark' ? 'light' : 'dark')}>{theme === 'dark' ? '☀️' : '🌙'}</button>
|
|
<select
|
|
value={forgeMode}
|
|
onChange={(e) => setForgeMode(e.target.value)}
|
|
style={{background: 'var(--input-bg)', color: 'var(--text-color)', border: '1px solid var(--border-color)', padding: '5px', borderRadius: '4px', fontSize: '0.8em'}}>
|
|
<option value="1">Aggressive (Combat)</option>
|
|
<option value="2">Balanced (Standard)</option>
|
|
<option value="3">Graceful (Smooth)</option>
|
|
</select>
|
|
<button className="clear-btn" onClick={() => setHistory([])}>Clear</button>
|
|
</div>
|
|
</div>
|
|
<div className="main-layout">
|
|
<div className="sidebar-left">
|
|
<button className="new-chat-btn" onClick={startNewSession}>+ New Chat</button>
|
|
<div className="session-list">
|
|
{sessions.map(s => (
|
|
<div key={s.id} className={`session-item ${s.id === currentSessionId ? 'active' : ''}`} onClick={() => loadSession(s.id)}>
|
|
<span className="session-date">{new Date(s.date).toLocaleDateString()} {new Date(s.date).toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'})}</span>
|
|
<span className="session-id">{s.id}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<div className="chat-section">
|
|
<div className="chat-container">
|
|
{history.length === 0 && <div style={{textAlign: 'center', marginTop: '50px', color: '#666'}}><p>Ready to build.</p></div>}
|
|
{history.map((msg, i) => {
|
|
const { text, suggestions } = msg.role === 'assistant' ? parseContent(msg.content) : { text: msg.content, suggestions: [] };
|
|
const timeStr = msg.timestamp ? new Date(msg.timestamp).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}) : '';
|
|
return (
|
|
<div key={i} className={`message ${msg.role}`}>
|
|
{timeStr && <span className="timestamp">{timeStr}</span>}
|
|
<div dangerouslySetInnerHTML={{ __html: marked.parse(text) }} />
|
|
{suggestions.length > 0 && (
|
|
<div className="suggestions">
|
|
{suggestions.map((s, idx) => (
|
|
<div key={idx} className="suggestion-pill" onClick={() => sendMessage(s)}>{s}</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
{loading && <div className="message assistant"><span className="loading-flame">🔥</span></div>}
|
|
<div ref={endRef} />
|
|
</div>
|
|
<div className="input-area">
|
|
<input
|
|
value={input}
|
|
onChange={e => setInput(e.target.value)}
|
|
onKeyPress={e => e.key === 'Enter' && sendMessage()}
|
|
placeholder="Ask BuddAI to build something..."
|
|
autoFocus
|
|
/>
|
|
{loading ? (
|
|
<button className="stop-btn" onClick={stopGeneration}>Stop</button>
|
|
) : (
|
|
<button onClick={() => sendMessage()}>Send</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
{isSidebarOpen && (
|
|
<div className="side-panel">
|
|
<div className="side-header">
|
|
<span>Code Workspace</span>
|
|
<div style={{display: 'flex', gap: '5px'}}>
|
|
<button className="copy-code-btn" onClick={() => setSidebarContent("")}>Clear</button>
|
|
<button className="copy-code-btn" onClick={() => navigator.clipboard.writeText(sidebarContent)}>Copy</button>
|
|
<button className="copy-code-btn" onClick={() => window.downloadCode(sidebarContent)}>Download</button>
|
|
</div>
|
|
</div>
|
|
<div className="side-content">
|
|
<pre><code className="hljs" dangerouslySetInnerHTML={{__html: sidebarContent ? hljs.highlightAuto(sidebarContent).value : '// Select code from chat to view here'}} /></pre>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
|
root.render(<App />);
|
|
</script>
|
|
</body>
|
|
</html> |