BuddAI/frontend/index.html

277 lines
No EOL
15 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="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🔥</text></svg>">
<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>
<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;
}
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: 900px; display: flex; flex-direction: column; height: 100%; }
.chat-container { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 15px; }
.message { padding: 15px; border-radius: 8px; max-width: 85%; line-height: 1.5; }
.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; }
</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) => {
const validLang = language || 'text';
const escapeHtml = (unsafe) => unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
return `<div class="code-wrapper">
<div class="code-header">
<span>${validLang}</span>
<button class="copy-code-btn" onclick="window.copyToClipboard(this)">Copy</button>
</div>
<pre><code class="hljs ${validLang}">${escapeHtml(code)}</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);
});
};
const { useState, useEffect, useRef } = React;
function App() {
const [history, setHistory] = 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 endRef = useRef(null);
const abortControllerRef = useRef(null);
const scrollToBottom = () => {
endRef.current?.scrollIntoView({ behavior: "smooth" });
};
useEffect(() => {
document.body.className = theme === 'light' ? 'light-mode' : '';
}, [theme]);
useEffect(() => {
scrollToBottom();
}, [history]);
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);
// 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 sendMessage = async (textOverride = null) => {
const msgText = typeof textOverride === 'string' ? textOverride : input;
if (!msgText.trim()) return;
const userMsg = { role: "user", content: msgText };
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 }]);
} 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 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'}}>
<h3 style={{margin:0}}>🔥 BuddAI v3.0</h3>
<span className={`status-badge ${status}`}>{status}</span>
</div>
<div style={{display:'flex', gap:'10px'}}>
<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="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: [] };
return (
<div key={i} className={`message ${msg.role}`}>
<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>
</>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
</script>
</body>
</html>