diff --git a/AGENTS.md b/AGENTS.md index 23d712e0..b3f500fc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -54,6 +54,12 @@ See botserver/src/drive/local_file_monitor.rs to see how bots are loaded from Mi - **Env file:** `botserver/.env` - **UI Files:** `botui/ui/suite/` +### BotUI Development Mode +**IMPORTANT:** BotUI serves static HTML/JS/CSS files directly from `botui/ui/` - **NO recompilation needed** for frontend changes. +- Changes to `.html`, `.js`, `.css` files in `botui/ui/` take effect immediately on page refresh +- Only Rust code changes in `botui/src/` require rebuild with `cargo build -p botui` +- This is "gate desligada" (gate off) mode - static assets served directly from filesystem + --- ## 🏗️ System Architecture Overview diff --git a/botserver/src/auto_task/app_generator.rs b/botserver/src/auto_task/app_generator.rs index 0b3fa3b3..b9238dea 100644 --- a/botserver/src/auto_task/app_generator.rs +++ b/botserver/src/auto_task/app_generator.rs @@ -3465,31 +3465,38 @@ async fn write_to_drive( const sendBtn = panel.querySelector('.designer-input button'); const messages = panel.querySelector('.designer-messages'); - async function sendMessage() {{ - const msg = input.value.trim(); - if (!msg) return; +function escapeHtml(str) {{ + if (!str) return ''; + const div = document.createElement('div'); + div.textContent = str; + return div.innerHTML; +}} - messages.innerHTML += `
,
,. Cada tag que você abrir DEVE ser fechada corretamente. Comece sua resposta diretamente com uma tag HTML, nunca com texto puro.")) + .unwrap_or_else(|_| "You are a helpful General Bots assistant.".to_string()) }); info!("Loaded system-prompt for bot {}: {}", session.bot_id, system_prompt.chars().take(500).collect::()); @@ -1345,10 +1345,29 @@ while let Some(chunk) = stream_rx.recv().await { trace!("Cleared leftover in_analysis state"); } -if !in_analysis { -// Accumulate full response - DO NOT send chunks -full_response.push_str(&chunk); -} + if !in_analysis { + full_response.push_str(&chunk); + // R4+R7: Stream each chunk immediately to the frontend via WebSocket + let response = BotResponse { + bot_id: message.bot_id.clone(), + user_id: message.user_id.clone(), + session_id: message.session_id.clone(), + channel: message.channel.clone(), + content: chunk, + message_type: MessageType::BOT_RESPONSE, + stream_token: None, + is_complete: false, + suggestions: Vec::new(), + switchers: Vec::new(), + context_name: None, + context_length: 0, + context_max_length: 0, + }; + if response_tx.send(response).await.is_err() { + warn!("stream_exit: Response channel closed for session {}", session.id); + break; + } + } } info!("llm_end: Streaming loop ended for session {}, chunk_count={}, full_response_len={}", session.id, chunk_count, full_response.len()); @@ -1362,6 +1381,13 @@ full_response.push_str(&chunk); } else { full_response.clone() }; +// DEBUG: Save HTML to file for verification +if full_response.contains(" ") { + use std::fs; + let debug_filename = format!("/tmp/html_debug_{}.txt", session.id); + let _ = fs::write(&debug_filename, &full_response); + error!("DEBUG: Invalid HTML saved to {}", debug_filename); +} info!("llm_output: session={} has_html={} has_div={} has_style={} is_truncated={} len={} preview=\"{}\"", session_id, has_html, has_div, has_style, is_truncated, full_response.len(), preview.replace('\n', "\\n")); @@ -1416,24 +1442,31 @@ full_response.push_str(&chunk); #[cfg(not(feature = "chat"))] let switchers: Vec = Vec::new(); -// Send accumulated full response (not streaming anymore) -let final_response = BotResponse { -bot_id: message.bot_id, -user_id: message.user_id, -session_id: message.session_id, -channel: message.channel, -content: full_response.clone(), -message_type: MessageType::BOT_RESPONSE, -stream_token: None, -is_complete: true, -suggestions, -switchers, -context_name: None, -context_length: 0, -context_max_length: 0, -}; +// DEBUG: Log full HTML before sending +trace!("DEBUG_FULL_HTML: {}", full_response); +// Save to file for analysis +use std::fs; +let debug_path = format!("/tmp/html_trace_{}.txt", session.id); +let _ = fs::write(&debug_path, &full_response); +trace!("DEBUG: HTML saved to {}", debug_path); -response_tx.send(final_response).await?; +// Send accumulated full response (not streaming anymore) + let final_response = BotResponse { + bot_id: message.bot_id, + user_id: message.user_id, + session_id: message.session_id, + channel: message.channel, + content: full_response.clone(), + message_type: MessageType::BOT_RESPONSE, + stream_token: None, + is_complete: true, + suggestions, + switchers, + context_name: None, + context_length: 0, + context_max_length: 0, + }; + response_tx.send(final_response).await?; Ok(()) } diff --git a/botserver/src/social/ui.rs b/botserver/src/social/ui.rs index b087ebfb..f5d2c93f 100644 --- a/botserver/src/social/ui.rs +++ b/botserver/src/social/ui.rs @@ -166,49 +166,54 @@ pub async fn handle_social_list_page(State(_state): State >) -> Htm } } - function renderPosts(posts) { - const list = document.getElementById('postsList'); - if (!posts || posts.length === 0) { - list.innerHTML = ' No posts yet. Create your first post to get started.'; - return; - } - list.innerHTML = posts.map(p => ` --- `).join(''); - } +function escapeHtml(str) { + if (!str) return ''; + return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); +} - function renderAccounts(accounts) { - const list = document.getElementById('accountsList'); - if (!accounts || accounts.length === 0) { - list.innerHTML = '--- - ${p.platform || 'Twitter'} -- ${p.status || 'Draft'} -${p.content}-- ❤️ ${p.likes || 0} - 💬 ${p.comments || 0} - 🔄 ${p.shares || 0} - 👁️ ${p.impressions || 0} --No accounts connected'; - return; - } - list.innerHTML = accounts.map(a => ` -- --- +function renderPosts(posts) { + const list = document.getElementById('postsList'); + if (!posts || posts.length === 0) { + list.innerHTML = '${a.name}-@${a.handle}-No posts yet. Create your first post to get started.'; + return; + } + list.innerHTML = posts.map(p => ` +++ `).join(''); +} + +function renderAccounts(accounts) { + const list = document.getElementById('accountsList'); + if (!accounts || accounts.length === 0) { + list.innerHTML = '+++ + ${escapeHtml(p.platform || 'Twitter')}- `).join(''); - } + ${escapeHtml(p.status || 'Draft')} +${escapeHtml(p.content)}++ ❤️ ${p.likes || 0} + 💬 ${p.comments || 0} + 🔄 ${p.shares || 0} + 👁️ ${p.impressions || 0} ++No accounts connected'; + return; + } + list.innerHTML = accounts.map(a => ` ++ ++ `).join(''); +} function getPlatformIcon(platform) { const icons = { twitter: 'X', facebook: 'f', instagram: '📷', linkedin: 'in' }; diff --git a/botserver/src/video/ui.rs b/botserver/src/video/ui.rs index 0923c1d0..6f69bf02 100644 --- a/botserver/src/video/ui.rs +++ b/botserver/src/video/ui.rs @@ -80,25 +80,30 @@ pub async fn handle_video_list_page( console.error('Failed to load videos:', e); } } - function renderVideos(projects) { - const grid = document.getElementById('videoGrid'); - if (!projects || projects.length === 0) { - grid.innerHTML = '++ +${escapeHtml(a.name)}+@${escapeHtml(a.handle)}+'; - return; - } - grid.innerHTML = projects.map(p => ` -No videos yet
Upload your first video to get started
-- `).join(''); - } +function escapeHtml(str) { + if (!str) return ''; + return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); +} + +function renderVideos(projects) { + const grid = document.getElementById('videoGrid'); + if (!projects || projects.length === 0) { + grid.innerHTML = '--- ${formatDuration(p.duration_ms / 1000)} -
--${p.name}- -'; + return; + } + grid.innerHTML = projects.map(p => ` +No videos yet
Upload your first video to get started
++ `).join(''); +} function formatDuration(seconds) { if (!seconds) return '0:00'; const mins = Math.floor(seconds / 60); diff --git a/botui/ui/suite/chat/chat-init.js b/botui/ui/suite/chat/chat-init.js index 54ccf5a7..ebc817ca 100644 --- a/botui/ui/suite/chat/chat-init.js +++ b/botui/ui/suite/chat/chat-init.js @@ -75,21 +75,25 @@ function proceedWithChatInit() { } function setupEventHandlers() { - var form = document.getElementById("chatForm"); - var input = document.getElementById("messageInput"); - var sendBtn = document.getElementById("sendBtn"); +var form = document.getElementById("chatForm"); +var input = document.getElementById("messageInput"); +var sendBtn = document.getElementById("sendBtn"); - if (form) { - form.onsubmit = function (e) { e.preventDefault(); sendMessage(); return false; }; - } +if (form) { +form.onsubmit = function (e) { e.preventDefault(); sendMessage(); return false; }; +} if (input) { -if (typeof handleMentionInput === 'function') { -input.addEventListener("input", handleMentionInput); +// Only attach mention handlers if they exist +var mentionInputHandler = window.handleMentionInput; +var mentionKeydownHandler = window.handleMentionKeydown; + +if (mentionInputHandler) { +input.addEventListener("input", mentionInputHandler); } -if (typeof handleMentionKeydown === 'function') { +if (mentionKeydownHandler) { input.onkeydown = function (e) { -if (handleMentionKeydown(e)) return; +if (mentionKeydownHandler(e)) return; if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendMessage(); } }; } else { @@ -99,29 +103,30 @@ if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendMessage(); } } } - if (sendBtn) { - sendBtn.onclick = function (e) { e.preventDefault(); sendMessage(); }; - } +if (sendBtn) { +sendBtn.onclick = function (e) { e.preventDefault(); sendMessage(); }; +} - var scrollBtn = document.getElementById("scrollToBottom"); - if (scrollBtn) { - scrollBtn.addEventListener("click", function () { scrollToBottom(true); ChatState.isUserScrolling = false; }); - } +var scrollBtn = document.getElementById("scrollToBottom"); +if (scrollBtn) { +scrollBtn.addEventListener("click", function () { scrollToBottom(true); ChatState.isUserScrolling = false; }); +} - var messagesEl = document.getElementById("messages"); - if (messagesEl) { - messagesEl.addEventListener("scroll", function () { - ChatState.isUserScrolling = true; - updateScrollButton(); - clearTimeout(messagesEl.scrollTimeout); - messagesEl.scrollTimeout = setTimeout(function () { ChatState.isUserScrolling = false; }, 1000); - }); - } +var messagesEl = document.getElementById("messages"); +if (messagesEl) { +messagesEl.addEventListener("scroll", function () { +ChatState.isUserScrolling = true; +updateScrollButton(); +clearTimeout(messagesEl.scrollTimeout); +messagesEl.scrollTimeout = setTimeout(function () { ChatState.isUserScrolling = false; }, 1000); +}); +} document.addEventListener("click", function (e) { if (!e.target.closest("#mentionDropdown") && !e.target.closest("#messageInput")) { -if (typeof hideMentionDropdown === 'function') { -hideMentionDropdown(); +var hideMention = window.hideMentionDropdown; +if (hideMention) { +hideMention(); } } }); diff --git a/botui/ui/suite/chat/chat-mentions.js b/botui/ui/suite/chat/chat-mentions.js index fc079f17..9b77ab88 100644 --- a/botui/ui/suite/chat/chat-mentions.js +++ b/botui/ui/suite/chat/chat-mentions.js @@ -242,8 +242,15 @@ function hideEntityCard() { } function fetchEntityDetails(type, name) { - return fetch("/api/search/entity?type=" + encodeURIComponent(type) + "&name=" + encodeURIComponent(name)) - .then(function (r) { return r.json(); }) - .then(function (data) { return data && data.details ? data.details : "No additional details available"; }) - .catch(function () { return "Unable to load details"; }); +return fetch("/api/search/entity?type=" + encodeURIComponent(type) + "&name=" + encodeURIComponent(name)) +.then(function (r) { return r.json(); }) +.then(function (data) { return data && data.details ? data.details : "No additional details available"; }) +.catch(function () { return "Unable to load details"; }); +} + +// Export functions to global scope for use in chat-init.js +if (typeof window !== 'undefined') { +window.handleMentionInput = handleMentionInput; +window.handleMentionKeydown = handleMentionKeydown; +window.hideMentionDropdown = hideMentionDropdown; } diff --git a/botui/ui/suite/chat/chat-messages.js b/botui/ui/suite/chat/chat-messages.js index be0349f6..daef51bb 100644 --- a/botui/ui/suite/chat/chat-messages.js +++ b/botui/ui/suite/chat/chat-messages.js @@ -35,14 +35,16 @@ function renderMentionInMessage(content) { } function stripThinkTags(content) { - // Remove+++ ${escapeHtml(formatDuration(p.duration_ms / 1000))} +
++${escapeHtml(p.name)}+ +... and anything in between - return content.replace(/[\s\S]*?(?:<\/think>|$)/gi, "").trim(); + // R6: Remove ... but do NOT trim — preserves leading '<' in HTML chunks + return content.replace(/[\s\S]*?(?:<\/think>|$)/gi, ""); } function stripMarkdownBlocks(content) { var cleanContent = stripThinkTags(content); + var hasHtmlTags = /<\/?[a-zA-Z][^>]*>|/i.test(cleanContent); + if (hasHtmlTags) return cleanContent; var htmlMatch = cleanContent.match(/^```(?:html|xml)?\s*\n([\s\S]+?)\n?```$/i); - if (htmlMatch) return htmlMatch[1].trim(); + if (htmlMatch) return htmlMatch[1]; return cleanContent; } @@ -54,15 +56,19 @@ var div = document.createElement("div"); div.className = "message " + sender; if (msgId) div.id = msgId; -if (sender === "user") { -var processedContent = renderMentionInMessage(escapeHtml(content)); -div.innerHTML = '"; + if (sender === "user") { + var processedContent = renderMentionInMessage(escapeHtml(content)); + div.innerHTML = '"; } else { var cleanContent = stripMarkdownBlocks(content); var hasHtmlTags = /<\/?[a-zA-Z][^>]*>|/i.test(cleanContent); var parsed; - if (msgId) { - parsed = ' ...'; + if (hasHtmlTags) { + // F3: HTML content from LLM — render raw via innerHTML (never textContent) + parsed = cleanContent; + } else if (msgId) { + // Streaming message with no HTML yet — show placeholder + parsed = ""; } else { parsed = escapeHtml(cleanContent); } @@ -98,12 +104,11 @@ function updateStreaming(content) { var isHtml = /<\/?[a-zA-Z][^>]*>|/i.test(cleanContent); if (isHtml) { - if (!el.querySelector(".streaming-loading")) { - var loader = document.createElement("div"); - loader.className = "streaming-loading"; - loader.innerHTML = '...'; - msgContent.appendChild(loader); - } + // F3+F5: Render HTML chunks directly via innerHTML += (never textContent/innerText) + // For streaming HTML, set full accumulated content — partial tags won't render, but completed ones will + var parsed = renderMentionInMessage(cleanContent); + msgContent.innerHTML = parsed; + if (!ChatState.isUserScrolling) scrollToBottom(true); } else { var parsed = typeof marked !== "undefined" && marked.parse ? marked.parse(cleanContent) diff --git a/restart.sh b/restart.sh index 2e5f0e14..cdc101b6 100755 --- a/restart.sh +++ b/restart.sh @@ -11,6 +11,7 @@ rm -f botserver.log botmodels.log # Build only botserver (botui likely already built) cargo build -p botserver +cargo build -p botui # Start botmodels cd botmodels @@ -41,6 +42,8 @@ fi # Start botserver BOTMODELS_HOST="http://localhost:8085" BOTMODELS_API_KEY="starter" RUST_LOG=info ./target/debug/botserver --noconsole > botserver.log 2>&1 & echo " botserver PID: $!" +./target/debug/botui 2>&1 & + # Quick health check sleep 2