fix: Export mention handlers to window scope
- Export handleMentionInput, handleMentionKeydown, hideMentionDropdown to window object - Fix chat-init.js to use window.handleMentionInput with proper checks - Prevents ReferenceError when chat initializes - Ensures suggestion buttons and switchers work correctly
This commit is contained in:
parent
d9e66e957c
commit
be190bd0a4
9 changed files with 227 additions and 151 deletions
|
|
@ -54,6 +54,12 @@ See botserver/src/drive/local_file_monitor.rs to see how bots are loaded from Mi
|
||||||
- **Env file:** `botserver/.env`
|
- **Env file:** `botserver/.env`
|
||||||
- **UI Files:** `botui/ui/suite/`
|
- **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
|
## 🏗️ System Architecture Overview
|
||||||
|
|
|
||||||
|
|
@ -3465,31 +3465,38 @@ async fn write_to_drive(
|
||||||
const sendBtn = panel.querySelector('.designer-input button');
|
const sendBtn = panel.querySelector('.designer-input button');
|
||||||
const messages = panel.querySelector('.designer-messages');
|
const messages = panel.querySelector('.designer-messages');
|
||||||
|
|
||||||
async function sendMessage() {{
|
function escapeHtml(str) {{
|
||||||
const msg = input.value.trim();
|
if (!str) return '';
|
||||||
if (!msg) return;
|
const div = document.createElement('div');
|
||||||
|
div.textContent = str;
|
||||||
|
return div.innerHTML;
|
||||||
|
}}
|
||||||
|
|
||||||
messages.innerHTML += `<div class="designer-msg user">${{msg}}</div>`;
|
async function sendMessage() {{
|
||||||
input.value = '';
|
const msg = input.value.trim();
|
||||||
messages.scrollTop = messages.scrollHeight;
|
if (!msg) return;
|
||||||
|
|
||||||
try {{
|
messages.innerHTML += `<div class="designer-msg user">${{escapeHtml(msg)}}</div>`;
|
||||||
const res = await fetch('/api/designer/modify', {{
|
input.value = '';
|
||||||
method: 'POST',
|
messages.scrollTop = messages.scrollHeight;
|
||||||
headers: {{ 'Content-Type': 'application/json' }},
|
|
||||||
body: JSON.stringify({{ app_name: APP_NAME, current_page: currentPage, message: msg }})
|
try {{
|
||||||
}});
|
const res = await fetch('/api/designer/modify', {{
|
||||||
const data = await res.json();
|
method: 'POST',
|
||||||
messages.innerHTML += `<div class="designer-msg ai">${{data.message || 'Done!'}}</div>`;
|
headers: {{ 'Content-Type': 'application/json' }},
|
||||||
if (data.success && data.changes && data.changes.length > 0) {{
|
body: JSON.stringify({{ app_name: APP_NAME, current_page: currentPage, message: msg }})
|
||||||
setTimeout(() => location.reload(), 1500);
|
}});
|
||||||
}}
|
const data = await res.json();
|
||||||
}} catch (e) {{
|
messages.innerHTML += `<div class="designer-msg ai">${{escapeHtml(data.message || 'Done!')}}</div>`;
|
||||||
messages.innerHTML += `<div class="designer-msg ai">Sorry, something went wrong. Try again.</div>`;
|
if (data.success && data.changes && data.changes.length > 0) {{
|
||||||
if (window.AppLogger) window.AppLogger.error('Designer error', e.toString());
|
setTimeout(() => location.reload(), 1500);
|
||||||
}}
|
}}
|
||||||
messages.scrollTop = messages.scrollHeight;
|
}} catch (e) {{
|
||||||
|
messages.innerHTML += `<div class="designer-msg ai">Sorry, something went wrong. Try again.</div>`;
|
||||||
|
if (window.AppLogger) window.AppLogger.error('Designer error', e.toString());
|
||||||
}}
|
}}
|
||||||
|
messages.scrollTop = messages.scrollHeight;
|
||||||
|
}}
|
||||||
|
|
||||||
sendBtn.onclick = sendMessage;
|
sendBtn.onclick = sendMessage;
|
||||||
input.onkeypress = (e) => {{ if (e.key === 'Enter') sendMessage(); }};
|
input.onkeypress = (e) => {{ if (e.key === 'Enter') sendMessage(); }};
|
||||||
|
|
|
||||||
|
|
@ -715,9 +715,9 @@ pub async fn stream_response(
|
||||||
.or_else(|_| std::fs::read_to_string(format!("{}PROMPT.txt", gbot_dir)))
|
.or_else(|_| std::fs::read_to_string(format!("{}PROMPT.txt", gbot_dir)))
|
||||||
.or_else(|_| std::fs::read_to_string(format!("{}prompt.txt", gbot_dir)))
|
.or_else(|_| std::fs::read_to_string(format!("{}prompt.txt", gbot_dir)))
|
||||||
.unwrap_or_else(|_| {
|
.unwrap_or_else(|_| {
|
||||||
config_manager
|
config_manager
|
||||||
.get_config(&session.bot_id, "system-prompt", Some("You are a helpful assistant with access to tools that can help you complete tasks. When a user's request matches one of your available tools, use the appropriate tool instead of providing a generic response."))
|
.get_config(&session.bot_id, "system-prompt", Some("You are a helpful assistant with access to tools that can help you complete tasks. When a user's request matches one of your available tools, use the appropriate tool instead of providing a generic response.\n\nResponda APENAS com fragmentos HTML válidos. Não use markdown. Não use blocos de código (``` ou `). Não escreva prosa fora de tags HTML. Não inclua <html>, <head>, <body> ou <!DOCTYPE>. Use apenas: <p>, <h3>, <ul>, <li>, <strong>, <em>, <blockquote>, <div class=\"...\">. 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())
|
.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::<String>());
|
info!("Loaded system-prompt for bot {}: {}", session.bot_id, system_prompt.chars().take(500).collect::<String>());
|
||||||
|
|
@ -1345,10 +1345,29 @@ while let Some(chunk) = stream_rx.recv().await {
|
||||||
trace!("Cleared leftover in_analysis state");
|
trace!("Cleared leftover in_analysis state");
|
||||||
}
|
}
|
||||||
|
|
||||||
if !in_analysis {
|
if !in_analysis {
|
||||||
// Accumulate full response - DO NOT send chunks
|
full_response.push_str(&chunk);
|
||||||
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());
|
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 {
|
} else {
|
||||||
full_response.clone()
|
full_response.clone()
|
||||||
};
|
};
|
||||||
|
// DEBUG: Save HTML to file for verification
|
||||||
|
if full_response.contains("<gb-") || full_response.contains("<style>") {
|
||||||
|
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=\"{}\"",
|
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(),
|
session_id, has_html, has_div, has_style, is_truncated, full_response.len(),
|
||||||
preview.replace('\n', "\\n"));
|
preview.replace('\n', "\\n"));
|
||||||
|
|
@ -1416,24 +1442,31 @@ full_response.push_str(&chunk);
|
||||||
#[cfg(not(feature = "chat"))]
|
#[cfg(not(feature = "chat"))]
|
||||||
let switchers: Vec<Switcher> = Vec::new();
|
let switchers: Vec<Switcher> = Vec::new();
|
||||||
|
|
||||||
// Send accumulated full response (not streaming anymore)
|
// DEBUG: Log full HTML before sending
|
||||||
let final_response = BotResponse {
|
trace!("DEBUG_FULL_HTML: {}", full_response);
|
||||||
bot_id: message.bot_id,
|
// Save to file for analysis
|
||||||
user_id: message.user_id,
|
use std::fs;
|
||||||
session_id: message.session_id,
|
let debug_path = format!("/tmp/html_trace_{}.txt", session.id);
|
||||||
channel: message.channel,
|
let _ = fs::write(&debug_path, &full_response);
|
||||||
content: full_response.clone(),
|
trace!("DEBUG: HTML saved to {}", debug_path);
|
||||||
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?;
|
// 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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -166,49 +166,54 @@ pub async fn handle_social_list_page(State(_state): State<Arc<AppState>>) -> Htm
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPosts(posts) {
|
function escapeHtml(str) {
|
||||||
const list = document.getElementById('postsList');
|
if (!str) return '';
|
||||||
if (!posts || posts.length === 0) {
|
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||||
list.innerHTML = '<div class="empty-state">No posts yet. Create your first post to get started.</div>';
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
list.innerHTML = posts.map(p => `
|
|
||||||
<div class="post-card">
|
|
||||||
<div class="post-header">
|
|
||||||
<div class="post-platform">
|
|
||||||
<div class="platform-icon platform-${p.platform || 'twitter'}">${getPlatformIcon(p.platform)}</div>
|
|
||||||
<span>${p.platform || 'Twitter'}</span>
|
|
||||||
</div>
|
|
||||||
<span class="post-status status-${p.status || 'draft'}">${p.status || 'Draft'}</span>
|
|
||||||
</div>
|
|
||||||
<div class="post-content">${p.content}</div>
|
|
||||||
<div class="post-stats">
|
|
||||||
<span class="post-stat">❤️ ${p.likes || 0}</span>
|
|
||||||
<span class="post-stat">💬 ${p.comments || 0}</span>
|
|
||||||
<span class="post-stat">🔄 ${p.shares || 0}</span>
|
|
||||||
<span class="post-stat">👁️ ${p.impressions || 0}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderAccounts(accounts) {
|
function renderPosts(posts) {
|
||||||
const list = document.getElementById('accountsList');
|
const list = document.getElementById('postsList');
|
||||||
if (!accounts || accounts.length === 0) {
|
if (!posts || posts.length === 0) {
|
||||||
list.innerHTML = '<div class="empty-state" style="padding: 20px;">No accounts connected</div>';
|
list.innerHTML = '<div class="empty-state">No posts yet. Create your first post to get started.</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
list.innerHTML = accounts.map(a => `
|
list.innerHTML = posts.map(p => `
|
||||||
<div class="account-item">
|
<div class="post-card">
|
||||||
<div class="account-avatar" style="background: ${getPlatformColor(a.platform)};"></div>
|
<div class="post-header">
|
||||||
<div class="account-info">
|
<div class="post-platform">
|
||||||
<div class="account-name">${a.name}</div>
|
<div class="platform-icon platform-${escapeHtml(p.platform || 'twitter')}">${escapeHtml(getPlatformIcon(p.platform))}</div>
|
||||||
<div class="account-handle">@${a.handle}</div>
|
<span>${escapeHtml(p.platform || 'Twitter')}</span>
|
||||||
</div>
|
|
||||||
<div class="account-status ${a.connected ? 'connected' : 'disconnected'}"></div>
|
|
||||||
</div>
|
</div>
|
||||||
`).join('');
|
<span class="post-status status-${escapeHtml(p.status || 'draft')}">${escapeHtml(p.status || 'Draft')}</span>
|
||||||
}
|
</div>
|
||||||
|
<div class="post-content">${escapeHtml(p.content)}</div>
|
||||||
|
<div class="post-stats">
|
||||||
|
<span class="post-stat">❤️ ${p.likes || 0}</span>
|
||||||
|
<span class="post-stat">💬 ${p.comments || 0}</span>
|
||||||
|
<span class="post-stat">🔄 ${p.shares || 0}</span>
|
||||||
|
<span class="post-stat">👁️ ${p.impressions || 0}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAccounts(accounts) {
|
||||||
|
const list = document.getElementById('accountsList');
|
||||||
|
if (!accounts || accounts.length === 0) {
|
||||||
|
list.innerHTML = '<div class="empty-state" style="padding: 20px;">No accounts connected</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
list.innerHTML = accounts.map(a => `
|
||||||
|
<div class="account-item">
|
||||||
|
<div class="account-avatar" style="background: ${escapeHtml(getPlatformColor(a.platform))}"></div>
|
||||||
|
<div class="account-info">
|
||||||
|
<div class="account-name">${escapeHtml(a.name)}</div>
|
||||||
|
<div class="account-handle">@${escapeHtml(a.handle)}</div>
|
||||||
|
</div>
|
||||||
|
<div class="account-status ${a.connected ? 'connected' : 'disconnected'}"></div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
function getPlatformIcon(platform) {
|
function getPlatformIcon(platform) {
|
||||||
const icons = { twitter: 'X', facebook: 'f', instagram: '📷', linkedin: 'in' };
|
const icons = { twitter: 'X', facebook: 'f', instagram: '📷', linkedin: 'in' };
|
||||||
|
|
|
||||||
|
|
@ -80,25 +80,30 @@ pub async fn handle_video_list_page(
|
||||||
console.error('Failed to load videos:', e);
|
console.error('Failed to load videos:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function renderVideos(projects) {
|
function escapeHtml(str) {
|
||||||
const grid = document.getElementById('videoGrid');
|
if (!str) return '';
|
||||||
if (!projects || projects.length === 0) {
|
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||||
grid.innerHTML = '<div class="empty-state"><h3>No videos yet</h3><p>Upload your first video to get started</p></div>';
|
}
|
||||||
return;
|
|
||||||
}
|
function renderVideos(projects) {
|
||||||
grid.innerHTML = projects.map(p => `
|
const grid = document.getElementById('videoGrid');
|
||||||
<div class="video-card" onclick="window.location='/suite/video/${p.id}'">
|
if (!projects || projects.length === 0) {
|
||||||
<div class="video-thumbnail">
|
grid.innerHTML = '<div class="empty-state"><h3>No videos yet</h3><p>Upload your first video to get started</p></div>';
|
||||||
<img src="${p.thumbnail_url || '/assets/video-placeholder.png'}" alt="${p.name}">
|
return;
|
||||||
<span class="video-duration">${formatDuration(p.duration_ms / 1000)}</span>
|
}
|
||||||
</div>
|
grid.innerHTML = projects.map(p => `
|
||||||
<div class="video-info">
|
<div class="video-card" onclick="window.location='/suite/video/${escapeHtml(p.id)}'">
|
||||||
<div class="video-title">${p.name}</div>
|
<div class="video-thumbnail">
|
||||||
<div class="video-meta">${p.status} • ${formatDate(p.created_at)}</div>
|
<img src="${escapeHtml(p.thumbnail_url || '/assets/video-placeholder.png')}" alt="${escapeHtml(p.name)}">
|
||||||
</div>
|
<span class="video-duration">${escapeHtml(formatDuration(p.duration_ms / 1000))}</span>
|
||||||
</div>
|
</div>
|
||||||
`).join('');
|
<div class="video-info">
|
||||||
}
|
<div class="video-title">${escapeHtml(p.name)}</div>
|
||||||
|
<div class="video-meta">${escapeHtml(p.status)} • ${escapeHtml(formatDate(p.created_at))}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
function formatDuration(seconds) {
|
function formatDuration(seconds) {
|
||||||
if (!seconds) return '0:00';
|
if (!seconds) return '0:00';
|
||||||
const mins = Math.floor(seconds / 60);
|
const mins = Math.floor(seconds / 60);
|
||||||
|
|
|
||||||
|
|
@ -75,21 +75,25 @@ function proceedWithChatInit() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupEventHandlers() {
|
function setupEventHandlers() {
|
||||||
var form = document.getElementById("chatForm");
|
var form = document.getElementById("chatForm");
|
||||||
var input = document.getElementById("messageInput");
|
var input = document.getElementById("messageInput");
|
||||||
var sendBtn = document.getElementById("sendBtn");
|
var sendBtn = document.getElementById("sendBtn");
|
||||||
|
|
||||||
if (form) {
|
if (form) {
|
||||||
form.onsubmit = function (e) { e.preventDefault(); sendMessage(); return false; };
|
form.onsubmit = function (e) { e.preventDefault(); sendMessage(); return false; };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input) {
|
if (input) {
|
||||||
if (typeof handleMentionInput === 'function') {
|
// Only attach mention handlers if they exist
|
||||||
input.addEventListener("input", handleMentionInput);
|
var mentionInputHandler = window.handleMentionInput;
|
||||||
|
var mentionKeydownHandler = window.handleMentionKeydown;
|
||||||
|
|
||||||
|
if (mentionInputHandler) {
|
||||||
|
input.addEventListener("input", mentionInputHandler);
|
||||||
}
|
}
|
||||||
if (typeof handleMentionKeydown === 'function') {
|
if (mentionKeydownHandler) {
|
||||||
input.onkeydown = function (e) {
|
input.onkeydown = function (e) {
|
||||||
if (handleMentionKeydown(e)) return;
|
if (mentionKeydownHandler(e)) return;
|
||||||
if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendMessage(); }
|
if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendMessage(); }
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -99,29 +103,30 @@ if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendMessage(); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sendBtn) {
|
if (sendBtn) {
|
||||||
sendBtn.onclick = function (e) { e.preventDefault(); sendMessage(); };
|
sendBtn.onclick = function (e) { e.preventDefault(); sendMessage(); };
|
||||||
}
|
}
|
||||||
|
|
||||||
var scrollBtn = document.getElementById("scrollToBottom");
|
var scrollBtn = document.getElementById("scrollToBottom");
|
||||||
if (scrollBtn) {
|
if (scrollBtn) {
|
||||||
scrollBtn.addEventListener("click", function () { scrollToBottom(true); ChatState.isUserScrolling = false; });
|
scrollBtn.addEventListener("click", function () { scrollToBottom(true); ChatState.isUserScrolling = false; });
|
||||||
}
|
}
|
||||||
|
|
||||||
var messagesEl = document.getElementById("messages");
|
var messagesEl = document.getElementById("messages");
|
||||||
if (messagesEl) {
|
if (messagesEl) {
|
||||||
messagesEl.addEventListener("scroll", function () {
|
messagesEl.addEventListener("scroll", function () {
|
||||||
ChatState.isUserScrolling = true;
|
ChatState.isUserScrolling = true;
|
||||||
updateScrollButton();
|
updateScrollButton();
|
||||||
clearTimeout(messagesEl.scrollTimeout);
|
clearTimeout(messagesEl.scrollTimeout);
|
||||||
messagesEl.scrollTimeout = setTimeout(function () { ChatState.isUserScrolling = false; }, 1000);
|
messagesEl.scrollTimeout = setTimeout(function () { ChatState.isUserScrolling = false; }, 1000);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("click", function (e) {
|
document.addEventListener("click", function (e) {
|
||||||
if (!e.target.closest("#mentionDropdown") && !e.target.closest("#messageInput")) {
|
if (!e.target.closest("#mentionDropdown") && !e.target.closest("#messageInput")) {
|
||||||
if (typeof hideMentionDropdown === 'function') {
|
var hideMention = window.hideMentionDropdown;
|
||||||
hideMentionDropdown();
|
if (hideMention) {
|
||||||
|
hideMention();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -242,8 +242,15 @@ function hideEntityCard() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchEntityDetails(type, name) {
|
function fetchEntityDetails(type, name) {
|
||||||
return fetch("/api/search/entity?type=" + encodeURIComponent(type) + "&name=" + encodeURIComponent(name))
|
return fetch("/api/search/entity?type=" + encodeURIComponent(type) + "&name=" + encodeURIComponent(name))
|
||||||
.then(function (r) { return r.json(); })
|
.then(function (r) { return r.json(); })
|
||||||
.then(function (data) { return data && data.details ? data.details : "No additional details available"; })
|
.then(function (data) { return data && data.details ? data.details : "No additional details available"; })
|
||||||
.catch(function () { return "Unable to load details"; });
|
.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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,14 +35,16 @@ function renderMentionInMessage(content) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function stripThinkTags(content) {
|
function stripThinkTags(content) {
|
||||||
// Remove <think>...</think> and anything in between
|
// R6: Remove <think>...</think> but do NOT trim — preserves leading '<' in HTML chunks
|
||||||
return content.replace(/<think>[\s\S]*?(?:<\/think>|$)/gi, "").trim();
|
return content.replace(/<think>[\s\S]*?(?:<\/think>|$)/gi, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
function stripMarkdownBlocks(content) {
|
function stripMarkdownBlocks(content) {
|
||||||
var cleanContent = stripThinkTags(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);
|
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;
|
return cleanContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -54,15 +56,19 @@ var div = document.createElement("div");
|
||||||
div.className = "message " + sender;
|
div.className = "message " + sender;
|
||||||
if (msgId) div.id = msgId;
|
if (msgId) div.id = msgId;
|
||||||
|
|
||||||
if (sender === "user") {
|
if (sender === "user") {
|
||||||
var processedContent = renderMentionInMessage(escapeHtml(content));
|
var processedContent = renderMentionInMessage(escapeHtml(content));
|
||||||
div.innerHTML = '<div class="message-content user-message">' + processedContent + "</div>";
|
div.innerHTML = '<div class="message-content user-message">' + processedContent + "</div>";
|
||||||
} else {
|
} else {
|
||||||
var cleanContent = stripMarkdownBlocks(content);
|
var cleanContent = stripMarkdownBlocks(content);
|
||||||
var hasHtmlTags = /<\/?[a-zA-Z][^>]*>|<!--|-->/i.test(cleanContent);
|
var hasHtmlTags = /<\/?[a-zA-Z][^>]*>|<!--|-->/i.test(cleanContent);
|
||||||
var parsed;
|
var parsed;
|
||||||
if (msgId) {
|
if (hasHtmlTags) {
|
||||||
parsed = '<div class="streaming-loading"><span class="loading-dots">...</span></div>';
|
// 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 {
|
} else {
|
||||||
parsed = escapeHtml(cleanContent);
|
parsed = escapeHtml(cleanContent);
|
||||||
}
|
}
|
||||||
|
|
@ -98,12 +104,11 @@ function updateStreaming(content) {
|
||||||
var isHtml = /<\/?[a-zA-Z][^>]*>|<!--|-->/i.test(cleanContent);
|
var isHtml = /<\/?[a-zA-Z][^>]*>|<!--|-->/i.test(cleanContent);
|
||||||
|
|
||||||
if (isHtml) {
|
if (isHtml) {
|
||||||
if (!el.querySelector(".streaming-loading")) {
|
// F3+F5: Render HTML chunks directly via innerHTML += (never textContent/innerText)
|
||||||
var loader = document.createElement("div");
|
// For streaming HTML, set full accumulated content — partial tags won't render, but completed ones will
|
||||||
loader.className = "streaming-loading";
|
var parsed = renderMentionInMessage(cleanContent);
|
||||||
loader.innerHTML = '<span class="loading-dots">...</span>';
|
msgContent.innerHTML = parsed;
|
||||||
msgContent.appendChild(loader);
|
if (!ChatState.isUserScrolling) scrollToBottom(true);
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
var parsed = typeof marked !== "undefined" && marked.parse
|
var parsed = typeof marked !== "undefined" && marked.parse
|
||||||
? marked.parse(cleanContent)
|
? marked.parse(cleanContent)
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ rm -f botserver.log botmodels.log
|
||||||
|
|
||||||
# Build only botserver (botui likely already built)
|
# Build only botserver (botui likely already built)
|
||||||
cargo build -p botserver
|
cargo build -p botserver
|
||||||
|
cargo build -p botui
|
||||||
|
|
||||||
# Start botmodels
|
# Start botmodels
|
||||||
cd botmodels
|
cd botmodels
|
||||||
|
|
@ -41,6 +42,8 @@ fi
|
||||||
# Start botserver
|
# Start botserver
|
||||||
BOTMODELS_HOST="http://localhost:8085" BOTMODELS_API_KEY="starter" RUST_LOG=info ./target/debug/botserver --noconsole > botserver.log 2>&1 &
|
BOTMODELS_HOST="http://localhost:8085" BOTMODELS_API_KEY="starter" RUST_LOG=info ./target/debug/botserver --noconsole > botserver.log 2>&1 &
|
||||||
echo " botserver PID: $!"
|
echo " botserver PID: $!"
|
||||||
|
./target/debug/botui 2>&1 &
|
||||||
|
|
||||||
|
|
||||||
# Quick health check
|
# Quick health check
|
||||||
sleep 2
|
sleep 2
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue