BuddAI/frontend/index.html

512 lines
No EOL
28 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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; transition: width 0.3s, opacity 0.3s; overflow: hidden; }
.sidebar-left.collapsed { width: 0; opacity: 0; border: none; }
.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; display: flex; justify-content: space-between; align-items: center; }
.session-info { flex: 1; overflow: hidden; }
.rename-input { width: 100%; background: var(--input-bg); color: var(--text-color); border: 1px solid var(--btn-bg); padding: 4px; border-radius: 3px; font-family: inherit; }
.edit-icon, .delete-icon { opacity: 0; cursor: pointer; padding: 4px; font-size: 1.1em; margin-left: 2px; }
.session-item:hover .edit-icon, .session-item:hover .delete-icon { opacity: 0.5; }
.edit-icon:hover, .delete-icon:hover { opacity: 1; }
.delete-icon:hover { color: #ff4444; }
.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 [showSidebar, setShowSidebar] = useState(true);
const [editingSession, setEditingSession] = useState(null);
const [renameText, setRenameText] = useState("");
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 handleRename = async (e) => {
if (e.key === 'Enter') {
await fetch("/api/session/rename", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ session_id: editingSession, title: renameText })
});
setEditingSession(null);
fetchSessions();
}
};
const handleDeleteSession = async (e, sessionId) => {
e.stopPropagation();
if (!window.confirm("Delete this chat?")) return;
await fetch("/api/session/delete", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ session_id: sessionId })
});
if (sessionId === currentSessionId) {
setHistory([]);
setCurrentSessionId(null);
}
fetchSessions();
};
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={() => setShowSidebar(!showSidebar)}>{showSidebar ? 'Hide History' : 'Show History'}</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 ${!showSidebar ? 'collapsed' : ''}`}>
<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)}>
{editingSession === s.id ? (
<input
className="rename-input"
value={renameText}
onChange={e => setRenameText(e.target.value)}
onKeyDown={handleRename}
onBlur={() => setEditingSession(null)}
autoFocus
onClick={e => e.stopPropagation()}
/>
) : (
<>
<div className="session-info">
<span className="session-date">{s.title || (new Date(s.date).toLocaleDateString() + ' ' + new Date(s.date).toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'}))}</span>
{!s.title && <span className="session-id">{s.id}</span>}
</div>
<div style={{display:'flex'}}>
<span className="edit-icon" onClick={(e) => {
e.stopPropagation();
setEditingSession(s.id);
setRenameText(s.title || "");
}}></span>
<span className="delete-icon" onClick={(e) => handleDeleteSession(e, s.id)}>🗑</span>
</div>
</>
)}
</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>