Merge remote changes
This commit is contained in:
commit
21bcde15a6
9 changed files with 401 additions and 1659 deletions
15
AGENTS.md
15
AGENTS.md
|
|
@ -17,7 +17,7 @@ I AM IN DEV ENV, but sometimes, pasting from PROD, do not treat my env as prod!
|
||||||
> - ❌ **NEVER** write internal IPs to logs or output
|
> - ❌ **NEVER** write internal IPs to logs or output
|
||||||
> - When debugging network issues, mask IPs (e.g., "10.x.x.x" instead of "10.16.164.222")
|
> - When debugging network issues, mask IPs (e.g., "10.x.x.x" instead of "10.16.164.222")
|
||||||
> - Use hostnames instead of IPs in configs and documentation
|
> - Use hostnames instead of IPs in configs and documentation
|
||||||
See botserver/src/drive/local_file_monitor.rs to see how to load from /opt/gbo/data the list of development bots.
|
See botserver/src/drive/local_file_monitor.rs to see how bots are loaded from the drive (MinIO storage).
|
||||||
- ❌ **NEVER** use `cargo clean` - causes 30min rebuilds, use `./reset.sh` for database issues
|
- ❌ **NEVER** use `cargo clean` - causes 30min rebuilds, use `./reset.sh` for database issues
|
||||||
|
|
||||||
>
|
>
|
||||||
|
|
@ -72,7 +72,7 @@ User Message (WebSocket)
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
┌─────────────────────────────────┐
|
┌─────────────────────────────────┐
|
||||||
│ 2. start.bas Execution │ /opt/gbo/data/{bot}.gbai/...
|
│ 2. start.bas Execution │ Drive (MinIO) storage
|
||||||
│ - Runs ONCE per session │ {bot}.gbdialog/start.bas
|
│ - Runs ONCE per session │ {bot}.gbdialog/start.bas
|
||||||
│ - ADD_SUGGESTION calls │ Adds button suggestions
|
│ - ADD_SUGGESTION calls │ Adds button suggestions
|
||||||
│ - Sets Redis flag │ prevents re-run
|
│ - Sets Redis flag │ prevents re-run
|
||||||
|
|
@ -179,7 +179,7 @@ END TABLE
|
||||||
|
|
||||||
### {tool}.bas - Tool Scripts
|
### {tool}.bas - Tool Scripts
|
||||||
|
|
||||||
**Location:** `/opt/gbo/data/{bot}.gbai/{bot}.gbdialog/{tool}.bas`
|
**Location:** Drive storage: `{bot}.gbdialog/{tool}.bas`
|
||||||
**Compiled to:** `{tool}.ast` (in memory or `/opt/gbo/work/`)
|
**Compiled to:** `{tool}.ast` (in memory or `/opt/gbo/work/`)
|
||||||
**Execution:** Via `CALL "tool"` or TOOL_EXEC (type 6)
|
**Execution:** Via `CALL "tool"` or TOOL_EXEC (type 6)
|
||||||
|
|
||||||
|
|
@ -437,9 +437,8 @@ END SWITCH
|
||||||
## 🧭 LLM Navigation Guide
|
## 🧭 LLM Navigation Guide
|
||||||
|
|
||||||
### Reading This Workspace
|
### Reading This Workspace
|
||||||
/opt/gbo/data is a place also for bots.
|
|
||||||
**For LLMs analyzing this codebase:**
|
**For LLMs analyzing this codebase:**
|
||||||
0. Bots are in drive, each bucket is a bot. Respect LOAD_ONLY.
|
0. Bots are in drive (MinIO storage), each bucket is a bot. Respect LOAD_ONLY.
|
||||||
1. Start with **[Component Dependency Graph](../README.md#-component-dependency-graph)** in README to understand relationships
|
1. Start with **[Component Dependency Graph](../README.md#-component-dependency-graph)** in README to understand relationships
|
||||||
2. Review **[Module Responsibility Matrix](../README.md#-module-responsibility-matrix)** for what each module does
|
2. Review **[Module Responsibility Matrix](../README.md#-module-responsibility-matrix)** for what each module does
|
||||||
3. Study **[Data Flow Patterns](../README.md#-data-flow-patterns)** to understand execution flow
|
3. Study **[Data Flow Patterns](../README.md#-data-flow-patterns)** to understand execution flow
|
||||||
|
|
@ -898,7 +897,7 @@ curl -X POST http://localhost:9000/api/features \
|
||||||
-d '{"name": "test"}'
|
-d '{"name": "test"}'
|
||||||
|
|
||||||
# 4. Test via BASIC script
|
# 4. Test via BASIC script
|
||||||
# Create test.bas in /opt/gbo/data/testbot.gbai/testbot.gbdialog/
|
# Create test.bas in {bot}.gbdialog/ folder (drive storage)
|
||||||
# NEW_FEATURE "test"
|
# NEW_FEATURE "test"
|
||||||
|
|
||||||
# 5. Check logs
|
# 5. Check logs
|
||||||
|
|
@ -1381,7 +1380,7 @@ Continue on gb/ workspace. Follow AGENTS.md strictly:
|
||||||
- **Host OS:** Ubuntu LTS
|
- **Host OS:** Ubuntu LTS
|
||||||
- **Container engine:** Incus (LXC-based)
|
- **Container engine:** Incus (LXC-based)
|
||||||
- **Base path:** `/opt/gbo/` (General Bots Operations)
|
- **Base path:** `/opt/gbo/` (General Bots Operations)
|
||||||
- **Data path:** `/opt/gbo/data` — shared data, configs, bot definitions
|
- **Data path:** `/opt/gbo/data` — shared data, configs (bot definitions are in MinIO drive storage)
|
||||||
- **Bin path:** `/opt/gbo/bin` — compiled binaries
|
- **Bin path:** `/opt/gbo/bin` — compiled binaries
|
||||||
- **Conf path:** `/opt/gbo/conf` — service configurations
|
- **Conf path:** `/opt/gbo/conf` — service configurations
|
||||||
- **Log path:** `/opt/gbo/logs` — application logs
|
- **Log path:** `/opt/gbo/logs` — application logs
|
||||||
|
|
@ -1390,7 +1389,7 @@ Continue on gb/ workspace. Follow AGENTS.md strictly:
|
||||||
|
|
||||||
| Role | Service | Typical Port | Notes |
|
| Role | Service | Typical Port | Notes |
|
||||||
|------|---------|-------------|-------|
|
|------|---------|-------------|-------|
|
||||||
| **dns** | CoreDNS | 53 | DNS resolution, zone files in `/opt/gbo/data` |
|
| **dns** | CoreDNS | 53 | DNS resolution, zone files in `/opt/gbo/conf/` |
|
||||||
| **proxy** | Caddy | 80/443 | Reverse proxy, TLS termination |
|
| **proxy** | Caddy | 80/443 | Reverse proxy, TLS termination |
|
||||||
| **tables** | PostgreSQL | 5432 | Primary database |
|
| **tables** | PostgreSQL | 5432 | Primary database |
|
||||||
| **email** | Stalwart | 993/465/587 | Mail server (IMAPS, SMTPS, Submission) |
|
| **email** | Stalwart | 993/465/587 | Mail server (IMAPS, SMTPS, Submission) |
|
||||||
|
|
|
||||||
53
PROMPT.md
Normal file
53
PROMPT.md
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
# System Prompt — Bot Salesianos
|
||||||
|
|
||||||
|
## IDENTIDADE E PROPÓSITO
|
||||||
|
|
||||||
|
Você é o assistente virtual da Escola Salesiana. Sua missão é transmitir informações com clareza, profundidade e didática.
|
||||||
|
salesianos.br e apenas Brasil. Inspetoria São João Bosco.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## REGRA MÁXIMA E ABSOLUTA PARA BUSCA DE DADOS (RAMAIS, NOMES, SETORES)
|
||||||
|
|
||||||
|
VOCÊ DEVE OBEDECER ESTA REGRA ACIMA DE QUALQUER OUTRA:
|
||||||
|
|
||||||
|
1. QUANDO O USUÁRIO PERGUNTAR POR UM RAMAL, NOME OU TELEFONE:
|
||||||
|
**VOCÊ ESTÁ PROIBIDO DE PEDIR MAIS INFORMAÇÕES OU CLARIFICAÇÕES.**
|
||||||
|
|
||||||
|
2. Você deve olhar IMEDIATAMENTE para o contexto fornecido (Knowledge Base) e procurar QUALQUER correspondência com o nome ou setor solicitado.
|
||||||
|
|
||||||
|
3. SE ENCONTRAR O NOME NOS DADOS FORNECIDOS (MESMO QUE SEJA PARCIAL):
|
||||||
|
**RESPONDA APENAS COM O NOME E O RAMAL ENCONTRADOS.**
|
||||||
|
Não diga que "não tem certeza", não peça "o nome completo", não peça "o setor". Apenas liste o que encontrou no contexto!
|
||||||
|
|
||||||
|
Exemplo Obrigatório:
|
||||||
|
Se o usuário perguntar: "Qual o ramal do João?"
|
||||||
|
Você procura no contexto. Se encontrar "João Silva 123" e "João Souza 456".
|
||||||
|
Você RESPONDE EXATAMENTE ASSIM:
|
||||||
|
<div style="padding: 16px; background-color: #FAF9F6; color: #3D4852; font-family: sans-serif;">
|
||||||
|
<h3 style="color: #4A6FA5;">Ramais Encontrados</h3>
|
||||||
|
<ul>
|
||||||
|
<li><strong>João Silva</strong> - Ramal 123</li>
|
||||||
|
<li><strong>João Souza</strong> - Ramal 456</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
**NUNCA DIGA "Ainda não tenho um ramal confirmado". SE ESTÁ NO CONTEXTO, É O RAMAL CORRETO. APENAS MOSTRE-O.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## REGRAS DE OUTPUT (HTML PURO)
|
||||||
|
|
||||||
|
1. **OUTPUT DIRETO — HTML PURO** — Não use ```, não use markdown, não use backticks!
|
||||||
|
2. **Comece com <div> e termine com </div>**
|
||||||
|
3. Não use marcações como ```html
|
||||||
|
4. **CONTRASTE DE CORES**: Fundo escuro exige texto #FFFFFF. Fundo claro exige texto escuro (#1A1A2E).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MENSAGEM FINAL OBRIGATÓRIA
|
||||||
|
|
||||||
|
No final de cada resposta, coloque:
|
||||||
|
Você também pode me perguntar sobre:... e 3 opções curtas.
|
||||||
|
|
||||||
|
**LEMBRE-SE: VOCÊ ESTÁ ESTRITAMENTE PROIBIDO DE PEDIR CLARIFICAÇÃO PARA RAMAIS. ENTREGUE O RESULTADO IMEDIATAMENTE BASEADO NO CONTEXTO!**
|
||||||
|
|
@ -1388,13 +1388,19 @@ pub fn convert_keywords_to_lowercase(script: &str) -> String {
|
||||||
let keyword = pattern.replace(r"\s+", "_");
|
let keyword = pattern.replace(r"\s+", "_");
|
||||||
|
|
||||||
// Build function call
|
// Build function call
|
||||||
|
// Special handling for ADD_SWITCHER which uses "as" syntax
|
||||||
|
let output = if keyword == "ADD_SWITCHER" && params.len() == 2 {
|
||||||
|
format!("ADD_SWITCHER {} as {};", params[0], params[1])
|
||||||
|
} else {
|
||||||
let params_str = if params.is_empty() {
|
let params_str = if params.is_empty() {
|
||||||
String::new()
|
String::new()
|
||||||
} else {
|
} else {
|
||||||
params.join(", ")
|
params.join(", ")
|
||||||
};
|
};
|
||||||
|
format!("{}({});", keyword, params_str)
|
||||||
|
};
|
||||||
|
|
||||||
result.push_str(&format!("{}({});", keyword, params_str));
|
result.push_str(&output);
|
||||||
result.push('\n');
|
result.push('\n');
|
||||||
converted = true;
|
converted = true;
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -612,18 +612,21 @@ impl BotOrchestrator {
|
||||||
};
|
};
|
||||||
|
|
||||||
let system_prompt = if !message.active_switchers.is_empty() {
|
let system_prompt = if !message.active_switchers.is_empty() {
|
||||||
|
log::debug!("Switchers active: {:?}", message.active_switchers);
|
||||||
let switcher_prompts = resolve_active_switchers(
|
let switcher_prompts = resolve_active_switchers(
|
||||||
self.state.cache.as_ref(),
|
self.state.cache.as_ref(),
|
||||||
&session.bot_id.to_string(),
|
&session.bot_id.to_string(),
|
||||||
&session.id.to_string(),
|
&session.id.to_string(),
|
||||||
&message.active_switchers,
|
&message.active_switchers,
|
||||||
);
|
);
|
||||||
|
log::debug!("Switcher prompts: {}", switcher_prompts);
|
||||||
if switcher_prompts.is_empty() {
|
if switcher_prompts.is_empty() {
|
||||||
system_prompt
|
system_prompt
|
||||||
} else {
|
} else {
|
||||||
format!("{system_prompt}\n\n{switcher_prompts}")
|
format!("{system_prompt}\n\n{switcher_prompts}")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
log::debug!("No active switchers for this message");
|
||||||
system_prompt
|
system_prompt
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,12 @@ pub fn normalize_etag(etag: &str) -> String {
|
||||||
|
|
||||||
impl DriveMonitor {
|
impl DriveMonitor {
|
||||||
pub async fn start_monitoring(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
pub async fn start_monitoring(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
log::info!("DriveMonitor monitoring started for bucket: {}", self.bucket_name);
|
log::trace!("DriveMonitor monitoring started for bucket: {}", self.bucket_name);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
// Reentrancy protection: skip if previous scan is still running
|
// Reentrancy protection: skip if previous scan is still running
|
||||||
if self.is_processing.load(Ordering::Relaxed) {
|
if self.is_processing.load(Ordering::Relaxed) {
|
||||||
log::debug!("DriveMonitor still processing, skipping iteration");
|
log::trace!("DriveMonitor still processing, skipping iteration");
|
||||||
} else {
|
} else {
|
||||||
self.is_processing.store(true, Ordering::Relaxed);
|
self.is_processing.store(true, Ordering::Relaxed);
|
||||||
if let Err(e) = self.scan_bucket().await {
|
if let Err(e) = self.scan_bucket().await {
|
||||||
|
|
@ -31,13 +31,13 @@ impl DriveMonitor {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn scan_bucket(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
async fn scan_bucket(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
log::info!("DriveMonitor: Starting scan of bucket {}", self.bucket_name);
|
log::trace!("DriveMonitor: Starting scan of bucket {}", self.bucket_name);
|
||||||
let start = std::time::Instant::now();
|
let start = std::time::Instant::now();
|
||||||
|
|
||||||
if let Some(s3) = &self.state.drive {
|
if let Some(s3) = &self.state.drive {
|
||||||
match s3.list_objects_with_metadata(&self.bucket_name, None).await {
|
match s3.list_objects_with_metadata(&self.bucket_name, None).await {
|
||||||
Ok(objects) => {
|
Ok(objects) => {
|
||||||
log::info!("Found {} objects in bucket {}", objects.len(), self.bucket_name);
|
log::trace!("Found {} objects in bucket {}", objects.len(), self.bucket_name);
|
||||||
|
|
||||||
let bot_name = self.bucket_name.strip_suffix(".gbai").unwrap_or(&self.bucket_name);
|
let bot_name = self.bucket_name.strip_suffix(".gbai").unwrap_or(&self.bucket_name);
|
||||||
|
|
||||||
|
|
@ -78,7 +78,7 @@ impl DriveMonitor {
|
||||||
etag,
|
etag,
|
||||||
None,
|
None,
|
||||||
) {
|
) {
|
||||||
Ok(_) => log::info!("Added/updated drive_files for: {} ({})", full_key, file_type),
|
Ok(_) => log::info!("DriveMonitor: Added/updated drive_files for: {} ({})", full_key, file_type),
|
||||||
Err(e) => log::error!("Failed to upsert {}: {}", full_key, e),
|
Err(e) => log::error!("Failed to upsert {}: {}", full_key, e),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -86,7 +86,7 @@ impl DriveMonitor {
|
||||||
self.sync_bas_to_work(bot_name, &obj.key).await;
|
self.sync_bas_to_work(bot_name, &obj.key).await;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log::debug!("{} unchanged, skipping upsert", full_key);
|
log::trace!("{} unchanged, skipping upsert", full_key);
|
||||||
}
|
}
|
||||||
|
|
||||||
if needs_reindex && file_type == "kb" {
|
if needs_reindex && file_type == "kb" {
|
||||||
|
|
@ -112,7 +112,7 @@ impl DriveMonitor {
|
||||||
}
|
}
|
||||||
|
|
||||||
let elapsed = start.elapsed();
|
let elapsed = start.elapsed();
|
||||||
log::info!("DriveMonitor: Completed scan of {} in {:.2?}", self.bucket_name, elapsed);
|
log::trace!("DriveMonitor: Completed scan of {} in {:.2?}", self.bucket_name, elapsed);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -255,7 +255,7 @@ impl DriveMonitor {
|
||||||
if let Err(e) = config_manager.set_config(&self.bot_id, key, value) {
|
if let Err(e) = config_manager.set_config(&self.bot_id, key, value) {
|
||||||
log::error!("Failed to set config {}={} for bot {}: {}", key, value, bot_name, e);
|
log::error!("Failed to set config {}={} for bot {}: {}", key, value, bot_name, e);
|
||||||
} else {
|
} else {
|
||||||
log::info!("Synced config {}={} for bot {}", key, value, bot_name);
|
log::trace!("Synced config {}={} for bot {}", key, value, bot_name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -295,7 +295,7 @@ impl DriveMonitor {
|
||||||
if let Err(e) = std::fs::write(&work_path, &content) {
|
if let Err(e) = std::fs::write(&work_path, &content) {
|
||||||
log::error!("Failed to write {} to work dir: {}", work_path.display(), e);
|
log::error!("Failed to write {} to work dir: {}", work_path.display(), e);
|
||||||
} else {
|
} else {
|
||||||
log::info!("Synced {} to work dir {}", s3_key, work_path.display());
|
log::trace!("Synced {} to work dir {}", s3_key, work_path.display());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
|
||||||
97
botui/ui/suite/chat/chat-main.js
Normal file
97
botui/ui/suite/chat/chat-main.js
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
// Chat Main Module - Initializes chat and coordinates between modules
|
||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
function notify(message, type) {
|
||||||
|
type = type || "info";
|
||||||
|
if (window.GBAlerts) {
|
||||||
|
window.GBAlerts.show(message, type);
|
||||||
|
} else {
|
||||||
|
console.log("[" + type + "]", message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initChat() {
|
||||||
|
console.log("Chat module initialized");
|
||||||
|
setupEventHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupEventHandlers() {
|
||||||
|
var form = document.getElementById("chatForm");
|
||||||
|
var input = document.getElementById("messageInput");
|
||||||
|
|
||||||
|
if (form) {
|
||||||
|
form.onsubmit = function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
sendMessage();
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input) {
|
||||||
|
input.addEventListener("input", handleMentionInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
var scrollBtn = document.getElementById("scrollToBottom");
|
||||||
|
if (scrollBtn) {
|
||||||
|
scrollBtn.addEventListener("click", function() {
|
||||||
|
scrollToBottom(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("click", function (e) {
|
||||||
|
if (
|
||||||
|
!e.target.closest("#mentionDropdown") &&
|
||||||
|
!e.target.closest("#messageInput")
|
||||||
|
) {
|
||||||
|
hideMentionDropdown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendMessage() {
|
||||||
|
var input = document.getElementById("messageInput");
|
||||||
|
if (!input) return;
|
||||||
|
|
||||||
|
var content = input.value.trim();
|
||||||
|
if (!content) return;
|
||||||
|
|
||||||
|
// Get active switchers
|
||||||
|
var activeSwitcherIds = getActiveSwitcherIds();
|
||||||
|
console.log('Sending message with active_switchers:', activeSwitcherIds);
|
||||||
|
|
||||||
|
// Add user message
|
||||||
|
addMessage("user", content);
|
||||||
|
|
||||||
|
// Clear input
|
||||||
|
input.value = "";
|
||||||
|
input.focus();
|
||||||
|
|
||||||
|
// Send via WebSocket
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
bot_id: currentBotId,
|
||||||
|
user_id: currentUserId,
|
||||||
|
session_id: currentSessionId,
|
||||||
|
channel: "web",
|
||||||
|
content: content,
|
||||||
|
message_type: MessageType.USER,
|
||||||
|
active_switchers: activeSwitcherIds,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
notify("Not connected to server. Message not sent.", "warning");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expose to global scope
|
||||||
|
window.sendMessage = sendMessage;
|
||||||
|
window.initChat = initChat;
|
||||||
|
|
||||||
|
// Initialize on load
|
||||||
|
if (document.readyState === "loading") {
|
||||||
|
document.addEventListener("DOMContentLoaded", initChat);
|
||||||
|
} else {
|
||||||
|
initChat();
|
||||||
|
}
|
||||||
|
})();
|
||||||
53
botui/ui/suite/chat/chat-messages.js
Normal file
53
botui/ui/suite/chat/chat-messages.js
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
// Chat Messages Module - Handles message rendering and display
|
||||||
|
function addMessage(role, content, messageId) {
|
||||||
|
var messages = document.getElementById("messages");
|
||||||
|
if (!messages) return;
|
||||||
|
|
||||||
|
var messageDiv = document.createElement("div");
|
||||||
|
messageDiv.className = "message message-" + role;
|
||||||
|
if (messageId) {
|
||||||
|
messageDiv.dataset.messageId = messageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (role === "bot") {
|
||||||
|
messageDiv.innerHTML = marked.parse(content);
|
||||||
|
} else {
|
||||||
|
messageDiv.textContent = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
messages.appendChild(messageDiv);
|
||||||
|
scrollToBottom(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollToBottom(animate) {
|
||||||
|
var messages = document.getElementById("messages");
|
||||||
|
if (!messages) return;
|
||||||
|
|
||||||
|
if (animate) {
|
||||||
|
messages.scrollTo({
|
||||||
|
top: messages.scrollHeight,
|
||||||
|
behavior: "smooth"
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
messages.scrollTop = messages.scrollHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showThinking() {
|
||||||
|
var messages = document.getElementById("messages");
|
||||||
|
if (!messages) return;
|
||||||
|
|
||||||
|
var thinking = document.createElement("div");
|
||||||
|
thinking.className = "message message-bot thinking";
|
||||||
|
thinking.id = "thinking-indicator";
|
||||||
|
thinking.textContent = "Thinking...";
|
||||||
|
messages.appendChild(thinking);
|
||||||
|
scrollToBottom(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideThinking() {
|
||||||
|
var thinking = document.getElementById("thinking-indicator");
|
||||||
|
if (thinking) {
|
||||||
|
thinking.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
90
botui/ui/suite/chat/chat-switchers.js
Normal file
90
botui/ui/suite/chat/chat-switchers.js
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
// Chat Switchers Module - Manages format switchers (tables, infographic, cards, etc.)
|
||||||
|
// Uses event delegation for better reliability with dynamic content
|
||||||
|
|
||||||
|
var activeSwitchers = new Set();
|
||||||
|
var switcherDefinitions = [];
|
||||||
|
|
||||||
|
function renderBotSwitchers(switchers) {
|
||||||
|
if (!switchers || switchers.length === 0) return;
|
||||||
|
|
||||||
|
var existingIds = {};
|
||||||
|
switcherDefinitions.forEach(function(sw) { existingIds[sw.id] = true; });
|
||||||
|
|
||||||
|
switchers.forEach(function(sw) {
|
||||||
|
if (!existingIds[sw.id]) {
|
||||||
|
switcherDefinitions.push({
|
||||||
|
id: sw.id,
|
||||||
|
label: sw.label || sw.id,
|
||||||
|
icon: sw.icon || '🔀',
|
||||||
|
color: sw.color || '#666'
|
||||||
|
});
|
||||||
|
existingIds[sw.id] = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
renderSwitchers();
|
||||||
|
|
||||||
|
var container = document.getElementById("switchers");
|
||||||
|
if (container && switcherDefinitions.length > 0) {
|
||||||
|
container.style.display = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSwitchers() {
|
||||||
|
var container = document.getElementById("switcherChips");
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
container.innerHTML = switcherDefinitions.map(function(sw) {
|
||||||
|
var isActive = activeSwitchers.has(sw.id);
|
||||||
|
return (
|
||||||
|
'<div class="switcher-chip' + (isActive ? ' active' : '') + '" ' +
|
||||||
|
'data-switch-id="' + sw.id + '" ' +
|
||||||
|
'style="--switcher-color: ' + sw.color + '; ' +
|
||||||
|
(isActive ? 'color: ' + sw.color + ' background: ' + sw.color + '; ' : '') + '">' +
|
||||||
|
'<span class="switcher-chip-icon">' + sw.icon + '</span>' +
|
||||||
|
'<span>' + sw.label + '</span>' +
|
||||||
|
'</div>'
|
||||||
|
);
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
// Event delegation - attach once to parent
|
||||||
|
if (!container.dataset.hasClickHandler) {
|
||||||
|
container.addEventListener('click', function(e) {
|
||||||
|
var chip = e.target.closest('.switcher-chip');
|
||||||
|
if (chip) {
|
||||||
|
var switcherId = chip.getAttribute('data-switch-id');
|
||||||
|
if (switcherId) {
|
||||||
|
toggleSwitcher(switcherId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
container.dataset.hasClickHandler = 'true';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSwitcher(switcherId) {
|
||||||
|
console.log('toggleSwitcher called with:', switcherId);
|
||||||
|
console.log('activeSwitchers before:', Array.from(activeSwitchers));
|
||||||
|
if (activeSwitchers.has(switcherId)) {
|
||||||
|
activeSwitchers.delete(switcherId);
|
||||||
|
console.log('Deleted switcher:', switcherId);
|
||||||
|
} else {
|
||||||
|
activeSwitchers.add(switcherId);
|
||||||
|
console.log('Added switcher:', switcherId);
|
||||||
|
}
|
||||||
|
console.log('activeSwitchers after:', Array.from(activeSwitchers));
|
||||||
|
|
||||||
|
// Re-render to show active state
|
||||||
|
renderSwitchers();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActiveSwitcherIds() {
|
||||||
|
var ids = Array.from(activeSwitchers);
|
||||||
|
console.log('getActiveSwitcherIds returning:', ids);
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSwitchers() {
|
||||||
|
activeSwitchers.clear();
|
||||||
|
renderSwitchers();
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue