mirror of
https://github.com/JamesTheGiblet/BuddAI.git
synced 2026-01-08 21:58:40 +00:00
Add initial test script to print "hello"
This commit is contained in:
parent
f0e2c975aa
commit
962e167667
8 changed files with 595 additions and 0 deletions
BIN
.gitignore
vendored
BIN
.gitignore
vendored
Binary file not shown.
0
data/training_data.jsonl
Normal file
0
data/training_data.jsonl
Normal file
594
data/uploads/index/index.html
Normal file
594
data/uploads/index/index.html
Normal file
|
|
@ -0,0 +1,594 @@
|
|||
<!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">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="/favicon-192x192.png">
|
||||
<!-- 'theme-color' is not supported by Firefox, Firefox for Android, Opera.
|
||||
It is used by Chrome and some Android browsers for UI theming. -->
|
||||
<!-- <meta name="theme-color" content="#1e1e1e"> -->
|
||||
<!-- For Safari on iOS -->
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<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 [feedbackGiven, setFeedbackGiven] = 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 socketRef = 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);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Initialize WebSocket
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${protocol}//${window.location.host}/api/ws/chat`;
|
||||
socketRef.current = new WebSocket(wsUrl);
|
||||
|
||||
return () => {
|
||||
if (socketRef.current) socketRef.current.close();
|
||||
};
|
||||
}, []);
|
||||
|
||||
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 handleFeedback = async (messageId, positive) => {
|
||||
if (!messageId || feedbackGiven[messageId]) return;
|
||||
|
||||
setFeedbackGiven(prev => ({ ...prev, [messageId]: positive ? 'positive' : 'negative' }));
|
||||
|
||||
try {
|
||||
await fetch("/api/feedback", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ message_id: messageId, positive: positive })
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Feedback submission failed", e);
|
||||
// Revert UI on failure
|
||||
setFeedbackGiven(prev => { const n = {...prev}; delete n[messageId]; return n; });
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
// Use WebSocket if available
|
||||
if (socketRef.current && socketRef.current.readyState === WebSocket.OPEN) {
|
||||
// Add placeholder for streaming response
|
||||
setHistory(prev => [...prev, { role: "assistant", content: "" }]);
|
||||
|
||||
socketRef.current.send(JSON.stringify({
|
||||
message: msgText,
|
||||
forge_mode: forgeMode,
|
||||
user_id: "default" // In a real app, get from auth context
|
||||
}));
|
||||
|
||||
socketRef.current.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.type === 'token') {
|
||||
setHistory(prev => {
|
||||
const newHistory = [...prev];
|
||||
const lastMsg = newHistory[newHistory.length - 1];
|
||||
if (lastMsg.role === 'assistant') {
|
||||
lastMsg.content += data.content;
|
||||
}
|
||||
return newHistory;
|
||||
});
|
||||
} else if (data.type === 'end') {
|
||||
setLoading(false);
|
||||
setHistory(prev => {
|
||||
const newHistory = [...prev];
|
||||
const lastMsg = newHistory[newHistory.length - 1];
|
||||
if (lastMsg && lastMsg.role === 'assistant') {
|
||||
lastMsg.id = data.message_id;
|
||||
}
|
||||
return newHistory;
|
||||
});
|
||||
if (!currentSessionId) fetchSessions();
|
||||
}
|
||||
};
|
||||
} else {
|
||||
// Fallback to HTTP
|
||||
try {
|
||||
const res = await fetch("/api/chat", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ message: msgText, forge_mode: forgeMode })
|
||||
});
|
||||
const data = await res.json();
|
||||
setHistory(prev => [...prev, { role: "assistant", content: data.response, id: data.message_id }]);
|
||||
if (!currentSessionId) fetchSessions();
|
||||
} catch (err) {
|
||||
setHistory(prev => [...prev, { role: "assistant", content: "Error connecting to BuddAI server." }]);
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
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.2</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>
|
||||
)}
|
||||
{msg.role === 'assistant' && msg.id && !loading && (
|
||||
<div className="feedback-btns">
|
||||
<button
|
||||
className={`feedback-btn ${feedbackGiven[msg.id] === 'positive' ? 'selected' : ''}`}
|
||||
onClick={() => handleFeedback(msg.id, true)}
|
||||
disabled={!!feedbackGiven[msg.id]}
|
||||
title="Good response"
|
||||
>👍</button>
|
||||
<button
|
||||
className={`feedback-btn ${feedbackGiven[msg.id] === 'negative' ? 'selected' : ''}`}
|
||||
onClick={() => handleFeedback(msg.id, false)}
|
||||
disabled={!!feedbackGiven[msg.id]}
|
||||
title="Bad response"
|
||||
>👎</button>
|
||||
</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>
|
||||
BIN
data/uploads/test/icons/favicon-16x16.png
Normal file
BIN
data/uploads/test/icons/favicon-16x16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 457 B |
BIN
data/uploads/test/icons/favicon-192x192.png
Normal file
BIN
data/uploads/test/icons/favicon-192x192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
BIN
data/uploads/test/icons/favicon-32x32.png
Normal file
BIN
data/uploads/test/icons/favicon-32x32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1,017 B |
BIN
data/uploads/test/icons/icon.png
Normal file
BIN
data/uploads/test/icons/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
1
data/uploads/test/test.py
Normal file
1
data/uploads/test/test.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
print("hello")
|
||||
Loading…
Add table
Add a link
Reference in a new issue