- New features for start.bas

This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2025-10-13 17:43:03 -03:00
parent 733f7cba10
commit 3aeb3ebc70
16 changed files with 836 additions and 166 deletions

1
Cargo.lock generated
View file

@ -1021,6 +1021,7 @@ dependencies = [
"native-tls", "native-tls",
"num-format", "num-format",
"qdrant-client", "qdrant-client",
"rand 0.9.2",
"redis", "redis",
"regex", "regex",
"reqwest 0.12.23", "reqwest 0.12.23",

View file

@ -58,3 +58,4 @@ zip = "2.2"
time = "0.3.44" time = "0.3.44"
aws-sdk-s3 = "1.108.0" aws-sdk-s3 = "1.108.0"
headless_chrome = { version = "1.0.18", optional = true } headless_chrome = { version = "1.0.18", optional = true }
rand = "0.9.2"

19
scripts/dev/build_prompt.sh → add-req.sh Executable file → Normal file
View file

@ -1,16 +1,15 @@
#!/bin/bash #!/bin/bash
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" PROJECT_ROOT="$SCRIPT_DIR"
OUTPUT_FILE="$SCRIPT_DIR/prompt.out" OUTPUT_FILE="$SCRIPT_DIR/prompt.out"
rm $OUTPUT_FILE rm $OUTPUT_FILE
echo "Consolidated LLM Context" > "$OUTPUT_FILE" echo "Consolidated LLM Context" > "$OUTPUT_FILE"
prompts=( prompts=(
"../../prompts/dev/shared.md" "./prompts/dev/shared.md"
"../../Cargo.toml" "./Cargo.toml"
#"../../prompts/dev/fix.md" "./prompts/dev/generation.md"
"../../prompts/dev/generation.md"
) )
for file in "${prompts[@]}"; do for file in "${prompts[@]}"; do
@ -23,12 +22,12 @@ dirs=(
#"automation" #"automation"
#"basic" #"basic"
"bot" "bot"
"channels" #"channels"
"config" "config"
"context" #"context"
#"email" #"email"
#"file" #"file"
"llm" #"llm"
#"llm_legacy" #"llm_legacy"
#"org" #"org"
"session" "session"
@ -36,7 +35,7 @@ dirs=(
#"tests" #"tests"
#"tools" #"tools"
#"web_automation" #"web_automation"
"whatsapp" #"whatsapp"
) )
for dir in "${dirs[@]}"; do for dir in "${dirs[@]}"; do
find "$PROJECT_ROOT/src/$dir" -name "*.rs" | while read file; do find "$PROJECT_ROOT/src/$dir" -name "*.rs" | while read file; do
@ -54,6 +53,8 @@ cat "$PROJECT_ROOT/src/basic/keywords/hear_talk.rs" >> "$OUTPUT_FILE"
echo "$PROJECT_ROOT/src/basic/mod.rs">> "$OUTPUT_FILE" echo "$PROJECT_ROOT/src/basic/mod.rs">> "$OUTPUT_FILE"
cat "$PROJECT_ROOT/src/basic/mod.rs" >> "$OUTPUT_FILE" cat "$PROJECT_ROOT/src/basic/mod.rs" >> "$OUTPUT_FILE"
echo "$PROJECT_ROOT/templates/annoucements.gbai/annoucements.gbdialog/start.bas" >> "$OUTPUT_FILE"
cat "$PROJECT_ROOT/templates/annoucements.gbai/annoucements.gbdialog/start.bas" >> "$OUTPUT_FILE"
echo "" >> "$OUTPUT_FILE" echo "" >> "$OUTPUT_FILE"

View file

@ -1,3 +1,9 @@
# LLM
Zed Assistant: Groq + GPT OSS 120B |
FIX Manual: DeepSeek | ChatGPT 120B | Claude 4.5 Thinking | Mistral
ADD Manual: Claude/DeepSeek -> DeepSeek
# DEV # DEV
curl -sSL https://get.livekit.io | bash curl -sSL https://get.livekit.io | bash

6
docs/GLOSSARY.md Normal file
View file

@ -0,0 +1,6 @@
RPM: Requests per minute
RPD: Requests per day
TPM: Tokens per minute
TPD: Tokens per day
ASH: Audio seconds per hour
ASD: Audio seconds per day

59
fix-errors.sh Executable file
View file

@ -0,0 +1,59 @@
#!/bin/bash
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$SCRIPT_DIR"
OUTPUT_FILE="$SCRIPT_DIR/prompt.out"
rm $OUTPUT_FILE
echo "Please, fix this consolidated LLM Context" > "$OUTPUT_FILE"
prompts=(
"./prompts/dev/shared.md"
"./Cargo.toml"
"./prompts/dev/fix.md"
)
for file in "${prompts[@]}"; do
cat "$file" >> "$OUTPUT_FILE"
echo "" >> "$OUTPUT_FILE"
done
dirs=(
#"auth"
#"automation"
#"basic"
"bot"
#"channels"
#"config"
#"context"
#"email"
#"file"
#"llm"
#"llm_legacy"
#"org"
"session"
"shared"
#"tests"
#"tools"
#"web_automation"
#"whatsapp"
)
for dir in "${dirs[@]}"; do
find "$PROJECT_ROOT/src/$dir" -name "*.rs" | while read file; do
echo $file >> "$OUTPUT_FILE"
cat "$file" >> "$OUTPUT_FILE"
echo "" >> "$OUTPUT_FILE"
done
done
# Also append the specific files you mentioned
echo "$PROJECT_ROOT/src/main.rs" >> "$OUTPUT_FILE"
cat "$PROJECT_ROOT/src/main.rs" >> "$OUTPUT_FILE"
cat "$PROJECT_ROOT/src/basic/keywords/hear_talk.rs" >> "$OUTPUT_FILE"
echo "$PROJECT_ROOT/src/basic/mod.rs">> "$OUTPUT_FILE"
cat "$PROJECT_ROOT/src/basic/mod.rs" >> "$OUTPUT_FILE"
echo "" >> "$OUTPUT_FILE"
cargo build --message-format=short 2>&1 | grep -E 'error' >> "$OUTPUT_FILE"

View file

@ -1,11 +1,13 @@
use crate::shared::models::{BotResponse, UserSession};
use crate::shared::state::AppState; use crate::shared::state::AppState;
use crate::{channels::ChannelAdapter, shared::models::UserSession}; use log::{debug, error, info};
use log::info;
use rhai::{Dynamic, Engine, EvalAltResult}; use rhai::{Dynamic, Engine, EvalAltResult};
use std::sync::Arc;
use uuid::Uuid;
pub fn hear_keyword(state: &AppState, user: UserSession, engine: &mut Engine) { pub fn hear_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
let session_id = user.id; let session_id = user.id;
let cache = state.redis_client.clone(); let state_clone = Arc::clone(&state);
engine engine
.register_custom_syntax(&["HEAR", "$ident$"], true, move |_context, inputs| { .register_custom_syntax(&["HEAR", "$ident$"], true, move |_context, inputs| {
@ -19,22 +21,24 @@ pub fn hear_keyword(state: &AppState, user: UserSession, engine: &mut Engine) {
variable_name variable_name
); );
let cache_clone = cache.clone(); let state_for_spawn = Arc::clone(&state_clone);
let session_id_clone = session_id; let session_id_clone = session_id;
let var_name_clone = variable_name.clone(); let var_name_clone = variable_name.clone();
tokio::spawn(async move { tokio::spawn(async move {
log::debug!( debug!(
"HEAR: Starting async task for session {} and variable '{}'", "HEAR: Setting session {} to wait for input for variable '{}'",
session_id_clone, session_id_clone, var_name_clone
var_name_clone
); );
if let Some(cache_client) = &cache_clone { let mut session_manager = state_for_spawn.session_manager.lock().await;
let mut conn = match cache_client.get_multiplexed_async_connection().await { session_manager.mark_waiting(session_id_clone);
if let Some(redis_client) = &state_for_spawn.redis_client {
let mut conn = match redis_client.get_multiplexed_async_connection().await {
Ok(conn) => conn, Ok(conn) => conn,
Err(e) => { Err(e) => {
log::error!("Failed to connect to cache: {}", e); error!("Failed to connect to cache: {}", e);
return; return;
} }
}; };
@ -56,10 +60,8 @@ pub fn hear_keyword(state: &AppState, user: UserSession, engine: &mut Engine) {
.unwrap(); .unwrap();
} }
pub fn talk_keyword(state: &AppState, user: UserSession, engine: &mut Engine) { pub fn talk_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
use crate::shared::models::BotResponse; let state_clone = Arc::clone(&state);
let state_clone = state.clone();
let user_clone = user.clone(); let user_clone = user.clone();
engine engine
@ -68,37 +70,97 @@ pub fn talk_keyword(state: &AppState, user: UserSession, engine: &mut Engine) {
info!("TALK command executed: {}", message); info!("TALK command executed: {}", message);
let response = BotResponse { let state_for_spawn = Arc::clone(&state_clone);
bot_id: "default_bot".to_string(), let user_clone_spawn = user_clone.clone();
user_id: user_clone.user_id.to_string(), let message_clone = message.clone();
session_id: user_clone.id.to_string(),
channel: "basic".to_string(),
content: message,
message_type: 1,
stream_token: None,
is_complete: true,
};
let state_for_spawn = state_clone.clone();
tokio::spawn(async move { tokio::spawn(async move {
if let Err(e) = state_for_spawn.web_adapter.send_message(response).await { debug!("TALK: Sending message via WebSocket: {}", message_clone);
log::error!("Failed to send TALK message: {}", e);
let bot_id =
std::env::var("BOT_GUID").unwrap_or_else(|_| "default_bot".to_string());
let response = BotResponse {
bot_id: bot_id,
user_id: user_clone_spawn.user_id.to_string(),
session_id: user_clone_spawn.id.to_string(),
channel: "web".to_string(),
content: message_clone,
message_type: 1,
stream_token: None,
is_complete: true,
};
let response_channels = state_for_spawn.response_channels.lock().await;
if let Some(tx) = response_channels.get(&user_clone_spawn.id.to_string()) {
if let Err(e) = tx.send(response).await {
error!("Failed to send TALK message via WebSocket: {}", e);
} else {
debug!("TALK message sent successfully via WebSocket");
}
} else {
debug!(
"No WebSocket connection found for session {}, sending via web adapter",
user_clone_spawn.id
);
if let Err(e) = state_for_spawn
.web_adapter
.send_message_to_session(&user_clone_spawn.id.to_string(), response)
.await
{
error!("Failed to send TALK message via web adapter: {}", e);
} else {
debug!("TALK message sent successfully via web adapter");
}
} }
}); });
Ok(Dynamic::UNIT) Ok(Dynamic::UNIT)
}) })
.unwrap(); .unwrap();
}
pub fn set_context_keyword(state: &AppState, user: UserSession, engine: &mut Engine) {
let cache = state.redis_client.clone();
engine engine
.register_custom_syntax( .register_custom_syntax(&["SET_USER", "$expr$"], true, move |context, inputs| {
&["SET", "CONTEXT", "$expr$"], let user_id_str = context.eval_expression_tree(&inputs[0])?.to_string();
true,
move |context, inputs| { info!("SET USER command executed with ID: {}", user_id_str);
match Uuid::parse_str(&user_id_str) {
Ok(user_id) => {
debug!("Successfully parsed user UUID: {}", user_id);
let state_for_spawn = Arc::clone(&state_clone);
let user_clone_spawn = user_clone.clone();
tokio::spawn(async move {
let mut session_manager = state_for_spawn.session_manager.lock().await;
if let Err(e) = session_manager.update_user_id(user_clone_spawn.id, user_id)
{
debug!("Failed to update user ID in session: {}", e);
} else {
info!(
"Updated session {} to user ID: {}",
user_clone_spawn.id, user_id
);
}
});
}
Err(e) => {
debug!("Invalid UUID format for SET USER: {}", e);
}
}
Ok(Dynamic::UNIT)
})
.unwrap();
pub fn set_context_keyword(state: &AppState, user: UserSession, engine: &mut Engine) {
let cache = state.redis_client.clone();
engine
.register_custom_syntax(&["SET_CONTEXT", "$expr$"], true, move |context, inputs| {
let context_value = context.eval_expression_tree(&inputs[0])?.to_string(); let context_value = context.eval_expression_tree(&inputs[0])?.to_string();
info!("SET CONTEXT command executed: {}", context_value); info!("SET CONTEXT command executed: {}", context_value);
@ -112,7 +174,7 @@ pub fn set_context_keyword(state: &AppState, user: UserSession, engine: &mut Eng
let mut conn = match cache_client.get_multiplexed_async_connection().await { let mut conn = match cache_client.get_multiplexed_async_connection().await {
Ok(conn) => conn, Ok(conn) => conn,
Err(e) => { Err(e) => {
log::error!("Failed to connect to cache: {}", e); error!("Failed to connect to cache: {}", e);
return; return;
} }
}; };
@ -126,7 +188,7 @@ pub fn set_context_keyword(state: &AppState, user: UserSession, engine: &mut Eng
}); });
Ok(Dynamic::UNIT) Ok(Dynamic::UNIT)
}, })
) .unwrap();
.unwrap(); }
} }

View file

@ -14,7 +14,7 @@ pub mod set_schedule;
pub mod wait; pub mod wait;
#[cfg(feature = "email")] #[cfg(feature = "email")]
pub mod create_draft; pub mod create_draft_keyword;
#[cfg(feature = "web_automation")] #[cfg(feature = "web_automation")]
pub mod get_website; pub mod get_website;

View file

@ -28,7 +28,7 @@ use self::keywords::create_draft_keyword;
use self::keywords::get_website::get_website_keyword; use self::keywords::get_website::get_website_keyword;
pub struct ScriptService { pub struct ScriptService {
engine: Engine, pub engine: Engine,
state: Arc<AppState>, state: Arc<AppState>,
user: UserSession, user: UserSession,
} }
@ -56,8 +56,8 @@ impl ScriptService {
print_keyword(&state, user.clone(), &mut engine); print_keyword(&state, user.clone(), &mut engine);
on_keyword(&state, user.clone(), &mut engine); on_keyword(&state, user.clone(), &mut engine);
set_schedule_keyword(&state, user.clone(), &mut engine); set_schedule_keyword(&state, user.clone(), &mut engine);
hear_keyword(&state, user.clone(), &mut engine); hear_keyword(state.clone(), user.clone(), &mut engine);
talk_keyword(&state, user.clone(), &mut engine); talk_keyword(state.clone(), user.clone(), &mut engine);
set_context_keyword(&state, user.clone(), &mut engine); set_context_keyword(&state, user.clone(), &mut engine);
#[cfg(feature = "web_automation")] #[cfg(feature = "web_automation")]
@ -141,6 +141,7 @@ impl ScriptService {
"HEAR", "HEAR",
"TALK", "TALK",
"SET CONTEXT", "SET CONTEXT",
"SET USER",
]; ];
let is_basic_command = basic_commands.iter().any(|&cmd| trimmed.starts_with(cmd)); let is_basic_command = basic_commands.iter().any(|&cmd| trimmed.starts_with(cmd));

View file

@ -125,6 +125,37 @@ impl BotOrchestrator {
Ok(()) Ok(())
} }
pub async fn send_direct_message(
&self,
session_id: &str,
channel: &str,
content: &str,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
debug!(
"Sending direct message to session {}: '{}'",
session_id, content
);
let bot_response = BotResponse {
bot_id: "default_bot".to_string(),
user_id: "default_user".to_string(),
session_id: session_id.to_string(),
channel: channel.to_string(),
content: content.to_string(),
message_type: 1,
stream_token: None,
is_complete: true,
};
if let Some(adapter) = self.state.channels.lock().unwrap().get(channel) {
adapter.send_message(bot_response).await?;
debug!("Direct message sent successfully");
} else {
warn!("No channel adapter found for channel: {}", channel);
}
Ok(())
}
pub async fn process_message( pub async fn process_message(
&self, &self,
message: UserMessage, message: UserMessage,
@ -143,28 +174,32 @@ impl BotOrchestrator {
warn!("Invalid user ID provided, generated new UUID: {}", new_id); warn!("Invalid user ID provided, generated new UUID: {}", new_id);
new_id new_id
}); });
let bot_id = Uuid::parse_str(&message.bot_id)
.unwrap_or_else(|_| Uuid::parse_str("00000000-0000-0000-0000-000000000000").unwrap()); let bot_id = if let Ok(bot_guid) = std::env::var("BOT_GUID") {
Uuid::parse_str(&bot_guid).unwrap_or_else(|_| {
warn!("Invalid BOT_GUID from env, using nil UUID");
Uuid::nil()
})
} else {
warn!("BOT_GUID not set in environment, using nil UUID");
Uuid::nil()
};
debug!("Parsed user_id: {}, bot_id: {}", user_id, bot_id); debug!("Parsed user_id: {}, bot_id: {}", user_id, bot_id);
let session = { let session = {
let mut session_manager = self.state.session_manager.lock().await; let mut session_manager = self.state.session_manager.lock().await;
match session_manager.get_user_session(user_id, bot_id)? { match session_manager.get_or_create_user_session(user_id, bot_id, "New Conversation")? {
Some(session) => { Some(session) => {
debug!("Found existing session: {}", session.id); debug!("Found existing session: {}", session.id);
session session
} }
None => { None => {
info!( error!(
"Creating new session for user {} with bot {}", "Failed to create session for user {} with bot {}",
user_id, bot_id user_id, bot_id
); );
let new_session = return Err("Failed to create session".into());
session_manager.create_session(user_id, bot_id, "New Conversation")?;
debug!("New session created: {}", new_session.id);
Self::run_start_script(&new_session, Arc::clone(&self.state)).await;
new_session
} }
} }
}; };
@ -296,43 +331,34 @@ impl BotOrchestrator {
); );
debug!("Message content: '{}'", message.content); debug!("Message content: '{}'", message.content);
let mut user_id = Uuid::parse_str(&message.user_id).unwrap_or_else(|_| { let user_id = Uuid::parse_str(&message.user_id).unwrap_or_else(|_| {
let new_id = Uuid::new_v4(); let new_id = Uuid::new_v4();
warn!("Invalid user ID, generated new: {}", new_id); warn!("Invalid user ID, generated new: {}", new_id);
new_id new_id
}); });
let bot_id = Uuid::parse_str(&message.bot_id).unwrap_or_else(|_| {
warn!("Invalid bot ID, using nil UUID"); let bot_id = if let Ok(bot_guid) = std::env::var("BOT_GUID") {
Uuid::parse_str(&bot_guid).unwrap_or_else(|_| {
warn!("Invalid BOT_GUID from env, using nil UUID");
Uuid::nil()
})
} else {
warn!("BOT_GUID not set in environment, using nil UUID");
Uuid::nil() Uuid::nil()
}); };
debug!("User ID: {}, Bot ID: {}", user_id, bot_id); debug!("User ID: {}, Bot ID: {}", user_id, bot_id);
let mut auth = self.state.auth_service.lock().await;
let user_exists = auth.get_user_by_id(user_id)?;
if user_exists.is_none() {
debug!("User {} not found, creating anonymous user", user_id);
user_id = auth.create_user("anonymous1", "anonymous@local", "password")?;
info!("Created new anonymous user: {}", user_id);
} else {
user_id = user_exists.unwrap().id;
debug!("Found existing user: {}", user_id);
}
let session = { let session = {
let mut sm = self.state.session_manager.lock().await; let mut sm = self.state.session_manager.lock().await;
match sm.get_user_session(user_id, bot_id)? { match sm.get_or_create_user_session(user_id, bot_id, "New Conversation")? {
Some(sess) => { Some(sess) => {
debug!("Using existing session: {}", sess.id); debug!("Using existing session: {}", sess.id);
sess sess
} }
None => { None => {
info!("Creating new session for streaming"); error!("Failed to create session for streaming");
let new_session = sm.create_session(user_id, bot_id, "New Conversation")?; return Err("Failed to create session".into());
debug!("New session created: {}", new_session.id);
Self::run_start_script(&new_session, Arc::clone(&self.state)).await;
new_session
} }
} }
}; };
@ -557,23 +583,27 @@ impl BotOrchestrator {
warn!("Invalid user ID, generated new: {}", new_id); warn!("Invalid user ID, generated new: {}", new_id);
new_id new_id
}); });
let bot_id = Uuid::parse_str(&message.bot_id)
.unwrap_or_else(|_| Uuid::parse_str("00000000-0000-0000-0000-000000000000").unwrap()); let bot_id = if let Ok(bot_guid) = std::env::var("BOT_GUID") {
Uuid::parse_str(&bot_guid).unwrap_or_else(|_| {
warn!("Invalid BOT_GUID from env, using nil UUID");
Uuid::nil()
})
} else {
warn!("BOT_GUID not set in environment, using nil UUID");
Uuid::nil()
};
let session = { let session = {
let mut session_manager = self.state.session_manager.lock().await; let mut session_manager = self.state.session_manager.lock().await;
match session_manager.get_user_session(user_id, bot_id)? { match session_manager.get_or_create_user_session(user_id, bot_id, "New Conversation")? {
Some(session) => { Some(session) => {
debug!("Found existing session: {}", session.id); debug!("Found existing session: {}", session.id);
session session
} }
None => { None => {
info!("Creating new session for tools processing"); error!("Failed to create session for tools processing");
let new_session = return Err("Failed to create session".into());
session_manager.create_session(user_id, bot_id, "New Conversation")?;
debug!("New session created: {}", new_session.id);
Self::run_start_script(&new_session, Arc::clone(&self.state)).await;
new_session
} }
} }
}; };
@ -705,10 +735,17 @@ impl BotOrchestrator {
Ok(()) Ok(())
} }
async fn run_start_script(session: &UserSession, state: Arc<AppState>) { pub async fn run_start_script(
info!("Running start script for session: {}", session.id); session: &UserSession,
state: Arc<AppState>,
let start_script_path = "start.bas"; token_id: Option<String>,
) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
info!(
"Running start script for session: {} with token_id: {:?}",
session.id, token_id
);
let start_script_path = "./templates/annoucements.gbai/annoucements.gbdialog/start.bas";
let start_script = match std::fs::read_to_string(start_script_path) { let start_script = match std::fs::read_to_string(start_script_path) {
Ok(content) => { Ok(content) => {
debug!("Loaded start script from {}", start_script_path); debug!("Loaded start script from {}", start_script_path);
@ -720,31 +757,39 @@ impl BotOrchestrator {
} }
}; };
debug!("Start script content for session {}: {}", session.id, start_script); debug!(
"Start script content for session {}: {}",
session.id, start_script
);
let session_clone = session.clone(); let session_clone = session.clone();
let state_clone = state.clone(); let state_clone = state.clone();
tokio::spawn(async move {
let state_for_run = state_clone.clone(); let script_service = crate::basic::ScriptService::new(state_clone, session_clone.clone());
match crate::basic::ScriptService::new(state_clone, session_clone.clone())
.compile(&start_script) if let Some(token_id_value) = token_id {
.and_then(|ast| { debug!("Token ID available for script: {}", token_id_value);
crate::basic::ScriptService::new(state_for_run, session_clone.clone()).run(&ast) }
}) {
Ok(_) => { match script_service
info!( .compile(&start_script)
"Start script executed successfully for session {}", .and_then(|ast| script_service.run(&ast))
session_clone.id {
); Ok(result) => {
} info!(
Err(e) => { "Start script executed successfully for session {}, result: {}",
error!( session_clone.id, result
"Failed to run start script for session {}: {}", );
session_clone.id, e Ok(true)
);
}
} }
}); Err(e) => {
error!(
"Failed to run start script for session {}: {}",
session_clone.id, e
);
Ok(false)
}
}
} }
pub async fn send_warning( pub async fn send_warning(
@ -795,6 +840,73 @@ impl BotOrchestrator {
} }
} }
} }
pub async fn trigger_auto_welcome(
&self,
session_id: &str,
user_id: &str,
_bot_id: &str,
token_id: Option<String>,
) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
info!(
"Triggering auto welcome for session: {} with token_id: {:?}",
session_id, token_id
);
let user_uuid = Uuid::parse_str(user_id).unwrap_or_else(|_| {
let new_id = Uuid::new_v4();
warn!("Invalid user ID, generated new: {}", new_id);
new_id
});
let bot_uuid = if let Ok(bot_guid) = std::env::var("BOT_GUID") {
Uuid::parse_str(&bot_guid).unwrap_or_else(|_| {
warn!("Invalid BOT_GUID from env, using nil UUID");
Uuid::nil()
})
} else {
warn!("BOT_GUID not set in environment, using nil UUID");
Uuid::nil()
};
let session = {
let mut session_manager = self.state.session_manager.lock().await;
match session_manager.get_or_create_user_session(
user_uuid,
bot_uuid,
"New Conversation",
)? {
Some(session) => {
debug!("Found existing session: {}", session.id);
session
}
None => {
error!("Failed to create session for auto welcome");
return Ok(false);
}
}
};
let result = Self::run_start_script(&session, Arc::clone(&self.state), token_id).await?;
info!(
"Auto welcome completed for session: {} with result: {}",
session_id, result
);
Ok(result)
}
async fn get_web_response_channel(
&self,
session_id: &str,
) -> Result<mpsc::Sender<BotResponse>, Box<dyn std::error::Error + Send + Sync>> {
let response_channels = self.state.response_channels.lock().await;
if let Some(tx) = response_channels.get(session_id) {
Ok(tx.clone())
} else {
Err("No response channel found for session".into())
}
}
} }
impl Default for BotOrchestrator { impl Default for BotOrchestrator {
@ -831,10 +943,12 @@ async fn websocket_handler(
.add_connection(session_id.clone(), tx.clone()) .add_connection(session_id.clone(), tx.clone())
.await; .await;
let bot_id = std::env::var("BOT_GUID").unwrap_or_else(|_| "default_bot".to_string());
orchestrator orchestrator
.send_event( .send_event(
"default_user", "default_user",
"default_bot", &bot_id,
&session_id, &session_id,
"web", "web",
"session_start", "session_start",
@ -845,6 +959,19 @@ async fn websocket_handler(
) )
.await .await
.ok(); .ok();
let orchestrator_clone = BotOrchestrator::new(Arc::clone(&data));
let session_id_clone = session_id.clone();
tokio::spawn(async move {
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
if let Err(e) = orchestrator_clone
.trigger_auto_welcome(&session_id_clone, "default_user", &bot_id, None)
.await
{
error!("Failed to trigger auto welcome: {}", e);
}
});
let web_adapter = data.web_adapter.clone(); let web_adapter = data.web_adapter.clone();
let session_id_clone1 = session_id.clone(); let session_id_clone1 = session_id.clone();
let session_id_clone2 = session_id.clone(); let session_id_clone2 = session_id.clone();
@ -883,8 +1010,11 @@ async fn websocket_handler(
message_count += 1; message_count += 1;
debug!("Received WebSocket message {}: {}", message_count, text); debug!("Received WebSocket message {}: {}", message_count, text);
let bot_id =
std::env::var("BOT_GUID").unwrap_or_else(|_| "default_bot".to_string());
let user_message = UserMessage { let user_message = UserMessage {
bot_id: "default_bot".to_string(), bot_id: bot_id,
user_id: "default_user".to_string(), user_id: "default_user".to_string(),
session_id: session_id_clone2.clone(), session_id: session_id_clone2.clone(),
channel: "web".to_string(), channel: "web".to_string(),
@ -903,10 +1033,14 @@ async fn websocket_handler(
} }
WsMessage::Close(_) => { WsMessage::Close(_) => {
info!("WebSocket close received for session {}", session_id_clone2); info!("WebSocket close received for session {}", session_id_clone2);
let bot_id =
std::env::var("BOT_GUID").unwrap_or_else(|_| "default_bot".to_string());
orchestrator orchestrator
.send_event( .send_event(
"default_user", "default_user",
"default_bot", &bot_id,
&session_id_clone2, &session_id_clone2,
"web", "web",
"session_end", "session_end",
@ -1067,17 +1201,112 @@ async fn voice_stop(
} }
} }
#[actix_web::post("/api/start")]
async fn start_session(
data: web::Data<AppState>,
info: web::Json<serde_json::Value>,
) -> Result<HttpResponse> {
let session_id = info
.get("session_id")
.and_then(|s| s.as_str())
.unwrap_or("");
let token_id = info
.get("token_id")
.and_then(|t| t.as_str())
.map(|s| s.to_string());
info!(
"Starting session: {} with token_id: {:?}",
session_id, token_id
);
let session_uuid = match Uuid::parse_str(session_id) {
Ok(uuid) => uuid,
Err(_) => {
warn!("Invalid session ID format: {}", session_id);
return Ok(
HttpResponse::BadRequest().json(serde_json::json!({"error": "Invalid session ID"}))
);
}
};
let session = {
let mut session_manager = data.session_manager.lock().await;
match session_manager.get_session_by_id(session_uuid) {
Ok(Some(s)) => {
debug!("Found existing session: {}", session_uuid);
s
}
Ok(None) => {
warn!("Session not found: {}", session_uuid);
return Ok(HttpResponse::NotFound()
.json(serde_json::json!({"error": "Session not found"})));
}
Err(e) => {
error!("Error retrieving session {}: {}", session_uuid, e);
return Ok(HttpResponse::InternalServerError()
.json(serde_json::json!({"error": "Failed to retrieve session"})));
}
}
};
let result = BotOrchestrator::run_start_script(&session, Arc::clone(&data), token_id).await;
match result {
Ok(true) => {
info!(
"Start script completed successfully for session: {}",
session_id
);
Ok(HttpResponse::Ok().json(serde_json::json!({
"status": "started",
"session_id": session.id,
"result": "success"
})))
}
Ok(false) => {
warn!("Start script returned false for session: {}", session_id);
Ok(HttpResponse::Ok().json(serde_json::json!({
"status": "started",
"session_id": session.id,
"result": "failed"
})))
}
Err(e) => {
error!(
"Error running start script for session {}: {}",
session_id, e
);
Ok(HttpResponse::InternalServerError()
.json(serde_json::json!({"error": e.to_string()})))
}
}
}
#[actix_web::post("/api/sessions")] #[actix_web::post("/api/sessions")]
async fn create_session(data: web::Data<AppState>) -> Result<HttpResponse> { async fn create_session(data: web::Data<AppState>) -> Result<HttpResponse> {
info!("Creating new session"); info!("Creating new session");
let user_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(); let user_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap();
let bot_id = Uuid::parse_str("00000000-0000-0000-0000-000000000000").unwrap();
let bot_id = if let Ok(bot_guid) = std::env::var("BOT_GUID") {
Uuid::parse_str(&bot_guid).unwrap_or_else(|_| {
warn!("Invalid BOT_GUID from env, using nil UUID");
Uuid::nil()
})
} else {
warn!("BOT_GUID not set in environment, using nil UUID");
Uuid::nil()
};
let session = { let session = {
let mut session_manager = data.session_manager.lock().await; let mut session_manager = data.session_manager.lock().await;
match session_manager.create_session(user_id, bot_id, "New Conversation") { match session_manager.get_or_create_user_session(user_id, bot_id, "New Conversation") {
Ok(s) => s, Ok(Some(s)) => s,
Ok(None) => {
error!("Failed to create session");
return Ok(HttpResponse::InternalServerError()
.json(serde_json::json!({"error": "Failed to create session"})));
}
Err(e) => { Err(e) => {
error!("Failed to create session: {}", e); error!("Failed to create session: {}", e);
return Ok(HttpResponse::InternalServerError() return Ok(HttpResponse::InternalServerError()

View file

@ -1,5 +1,5 @@
use async_trait::async_trait; use async_trait::async_trait;
use log::info; use log::{debug, info};
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::{mpsc, Mutex}; use tokio::sync::{mpsc, Mutex};
@ -32,6 +32,28 @@ impl WebChannelAdapter {
pub async fn remove_connection(&self, session_id: &str) { pub async fn remove_connection(&self, session_id: &str) {
self.connections.lock().await.remove(session_id); self.connections.lock().await.remove(session_id);
} }
pub async fn send_message_to_session(
&self,
session_id: &str,
message: BotResponse,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let connections = self.connections.lock().await;
if let Some(tx) = connections.get(session_id) {
if let Err(e) = tx.send(message).await {
log::error!(
"Failed to send message to WebSocket session {}: {}",
session_id,
e
);
return Err(Box::new(e));
}
debug!("Message sent to WebSocket session: {}", session_id);
Ok(())
} else {
debug!("No WebSocket connection found for session: {}", session_id);
Err("No WebSocket connection found".into())
}
}
} }
#[async_trait] #[async_trait]

View file

@ -27,8 +27,9 @@ mod tools;
mod whatsapp; mod whatsapp;
use crate::bot::{ use crate::bot::{
create_session, get_session_history, get_sessions, index, set_mode_handler, static_files, create_session, get_session_history, get_sessions, index, set_mode_handler, start_session,
voice_start, voice_stop, websocket_handler, whatsapp_webhook, whatsapp_webhook_verify, static_files, voice_start, voice_stop, websocket_handler, whatsapp_webhook,
whatsapp_webhook_verify,
}; };
use crate::channels::{VoiceAdapter, WebChannelAdapter}; use crate::channels::{VoiceAdapter, WebChannelAdapter};
use crate::config::AppConfig; use crate::config::AppConfig;
@ -188,6 +189,7 @@ async fn main() -> std::io::Result<()> {
.service(voice_stop) .service(voice_stop)
.service(create_session) .service(create_session)
.service(get_sessions) .service(get_sessions)
.service(start_session)
.service(get_session_history) .service(get_session_history)
.service(set_mode_handler) .service(set_mode_handler)
.service(chat_completions_local) .service(chat_completions_local)

View file

@ -1,7 +1,7 @@
use chrono::Utc; use chrono::Utc;
use diesel::prelude::*; use diesel::prelude::*;
use diesel::PgConnection; use diesel::PgConnection;
use log::info; use log::{debug, error, info, warn};
use redis::Client; use redis::Client;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -41,13 +41,16 @@ impl SessionManager {
&mut self, &mut self,
session_id: Uuid, session_id: Uuid,
input: String, input: String,
) -> Result<(), Box<dyn Error + Send + Sync>> { ) -> Result<Option<String>, Box<dyn Error + Send + Sync>> {
info!( info!(
"SessionManager.provide_input called for session {}", "SessionManager.provide_input called for session {}",
session_id session_id
); );
if let Some(sess) = self.sessions.get_mut(&session_id) { if let Some(sess) = self.sessions.get_mut(&session_id) {
sess.data = input; sess.data = input;
self.waiting_for_input.remove(&session_id);
Ok(Some("user_input".to_string()))
} else { } else {
let sess = SessionData { let sess = SessionData {
id: session_id, id: session_id,
@ -55,9 +58,9 @@ impl SessionManager {
data: input, data: input,
}; };
self.sessions.insert(session_id, sess); self.sessions.insert(session_id, sess);
self.waiting_for_input.remove(&session_id);
Ok(Some("user_input".to_string()))
} }
self.waiting_for_input.remove(&session_id);
Ok(())
} }
pub fn is_waiting_for_input(&self, session_id: &Uuid) -> bool { pub fn is_waiting_for_input(&self, session_id: &Uuid) -> bool {
@ -69,6 +72,20 @@ impl SessionManager {
info!("Session {} marked as waiting for input", session_id); info!("Session {} marked as waiting for input", session_id);
} }
pub fn get_session_by_id(
&mut self,
session_id: Uuid,
) -> Result<Option<UserSession>, Box<dyn Error + Send + Sync>> {
use crate::shared::models::user_sessions::dsl::*;
let result = user_sessions
.filter(id.eq(session_id))
.first::<UserSession>(&mut self.conn)
.optional()?;
Ok(result)
}
pub fn get_user_session( pub fn get_user_session(
&mut self, &mut self,
uid: Uuid, uid: Uuid,
@ -86,6 +103,21 @@ impl SessionManager {
Ok(result) Ok(result)
} }
pub fn get_or_create_user_session(
&mut self,
uid: Uuid,
bid: Uuid,
session_title: &str,
) -> Result<Option<UserSession>, Box<dyn Error + Send + Sync>> {
if let Some(existing) = self.get_user_session(uid, bid)? {
debug!("Found existing session: {}", existing.id);
return Ok(Some(existing));
}
info!("Creating new session for user {} with bot {}", uid, bid);
self.create_session(uid, bid, session_title).map(Some)
}
pub fn create_session( pub fn create_session(
&mut self, &mut self,
uid: Uuid, uid: Uuid,
@ -93,21 +125,35 @@ impl SessionManager {
session_title: &str, session_title: &str,
) -> Result<UserSession, Box<dyn Error + Send + Sync>> { ) -> Result<UserSession, Box<dyn Error + Send + Sync>> {
use crate::shared::models::user_sessions::dsl::*; use crate::shared::models::user_sessions::dsl::*;
use crate::shared::models::users::dsl as users_dsl;
// Return an existing session if one already matches the user, bot, and title.
if let Some(existing) = user_sessions
.filter(user_id.eq(uid))
.filter(bot_id.eq(bid))
.filter(title.eq(session_title))
.first::<UserSession>(&mut self.conn)
.optional()?
{
return Ok(existing);
}
let now = Utc::now(); let now = Utc::now();
// Insert the new session and retrieve the full record in one step. let user_exists: Option<Uuid> = users_dsl::users
.filter(users_dsl::id.eq(uid))
.select(users_dsl::id)
.first(&mut self.conn)
.optional()?;
if user_exists.is_none() {
warn!(
"User {} does not exist in database, creating placeholder user",
uid
);
diesel::insert_into(users_dsl::users)
.values((
users_dsl::id.eq(uid),
users_dsl::username.eq(format!("anonymous_{}", rand::random::<u32>())),
users_dsl::email.eq(format!("anonymous_{}@local", rand::random::<u32>())),
users_dsl::password_hash.eq("placeholder"),
users_dsl::is_active.eq(true),
users_dsl::created_at.eq(now),
users_dsl::updated_at.eq(now),
))
.execute(&mut self.conn)?;
info!("Created placeholder user: {}", uid);
}
let inserted: UserSession = diesel::insert_into(user_sessions) let inserted: UserSession = diesel::insert_into(user_sessions)
.values(( .values((
id.eq(Uuid::new_v4()), id.eq(Uuid::new_v4()),
@ -121,8 +167,13 @@ impl SessionManager {
updated_at.eq(now), updated_at.eq(now),
)) ))
.returning(UserSession::as_returning()) .returning(UserSession::as_returning())
.get_result(&mut self.conn)?; .get_result(&mut self.conn)
.map_err(|e| {
error!("Failed to create session in database: {}", e);
e
})?;
info!("New session created: {}", inserted.id);
Ok(inserted) Ok(inserted)
} }
@ -139,7 +190,8 @@ impl SessionManager {
let next_index = message_history let next_index = message_history
.filter(session_id.eq(sess_id)) .filter(session_id.eq(sess_id))
.count() .count()
.get_result::<i64>(&mut self.conn)?; .get_result::<i64>(&mut self.conn)
.unwrap_or(0);
diesel::insert_into(message_history) diesel::insert_into(message_history)
.values(( .values((
@ -154,23 +206,39 @@ impl SessionManager {
)) ))
.execute(&mut self.conn)?; .execute(&mut self.conn)?;
debug!(
"Message saved for session {} with index {}",
sess_id, next_index
);
Ok(()) Ok(())
} }
pub fn get_conversation_history( pub fn get_conversation_history(
&mut self, &mut self,
_sess_id: Uuid, sess_id: Uuid,
_uid: Uuid, _uid: Uuid,
) -> Result<Vec<(String, String)>, Box<dyn Error + Send + Sync>> { ) -> Result<Vec<(String, String)>, Box<dyn Error + Send + Sync>> {
// use crate::shared::models::message_history::dsl::*; use crate::shared::models::message_history::dsl::*;
// let messages = message_history let messages = message_history
// .filter(session_id.eq(sess_id)) .filter(session_id.eq(sess_id))
// .order(message_index.asc()) .order(message_index.asc())
// .select((role, content_encrypted)) .select((role, content_encrypted))
// .load::<(String, String)>(&mut self.conn)?; .load::<(i32, String)>(&mut self.conn)?;
Ok(vec![]) let history = messages
.into_iter()
.map(|(other_role, content)| {
let role_str = match other_role {
0 => "user".to_string(),
1 => "assistant".to_string(),
_ => "unknown".to_string(),
};
(role_str, content)
})
.collect();
Ok(history)
} }
pub fn get_user_sessions( pub fn get_user_sessions(
@ -195,10 +263,16 @@ impl SessionManager {
) -> Result<(), Box<dyn Error + Send + Sync>> { ) -> Result<(), Box<dyn Error + Send + Sync>> {
use crate::shared::models::user_sessions::dsl::*; use crate::shared::models::user_sessions::dsl::*;
let user_uuid = Uuid::parse_str(uid)?; let user_uuid = Uuid::parse_str(uid).map_err(|e| {
let bot_uuid = Uuid::parse_str(bid)?; warn!("Invalid user ID format: {}", uid);
e
})?;
let bot_uuid = Uuid::parse_str(bid).map_err(|e| {
warn!("Invalid bot ID format: {}", bid);
e
})?;
diesel::update( let updated_count = diesel::update(
user_sessions user_sessions
.filter(user_id.eq(user_uuid)) .filter(user_id.eq(user_uuid))
.filter(bot_id.eq(bot_uuid)), .filter(bot_id.eq(bot_uuid)),
@ -206,6 +280,35 @@ impl SessionManager {
.set((answer_mode.eq(mode), updated_at.eq(chrono::Utc::now()))) .set((answer_mode.eq(mode), updated_at.eq(chrono::Utc::now())))
.execute(&mut self.conn)?; .execute(&mut self.conn)?;
if updated_count == 0 {
warn!("No session found for user {} and bot {}", uid, bid);
} else {
debug!(
"Answer mode updated to {} for user {} and bot {}",
mode, uid, bid
);
}
Ok(())
}
pub fn update_user_id(
&mut self,
session_id: Uuid,
new_user_id: Uuid,
) -> Result<(), Box<dyn Error + Send + Sync>> {
use crate::shared::models::user_sessions::dsl::*;
let updated_count = diesel::update(user_sessions.filter(id.eq(session_id)))
.set((user_id.eq(new_user_id), updated_at.eq(chrono::Utc::now())))
.execute(&mut self.conn)?;
if updated_count == 0 {
warn!("No session found with ID: {}", session_id);
} else {
info!("Updated session {} to user ID: {}", session_id, new_user_id);
}
Ok(()) Ok(())
} }
} }

View file

@ -0,0 +1,8 @@
TALK "Welcome to General Bots! What is your name?"
HEAR name
TALK "Hello, " + name
text = GET "default.pdf"
SET_CONTEXT text
resume = LLM "Build a resume from " + text

View file

@ -1,8 +1,5 @@
TALK "Welcome to General Bots!" TALK "Welcome to General Bots!"
TALK "What is your name?"
HEAR name HEAR name
TALK "Hello, " + name TALK "Hello " + name + ", nice to meet you!"
SET_USER "92fcffaa-bf0a-41a9-8d99-5541709d695b"
text = GET "default.pdf"
SET CONTEXT text
resume = LLM "Build a resume from " + text

View file

@ -8,6 +8,7 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.2/anime.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.2/anime.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/livekit-client/dist/livekit-client.umd.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/livekit-client/dist/livekit-client.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style> <style>
@import url("https://fonts.googleapis.com/css2?family=Orbitron:wght@400;600;800&family=Inter:wght@400;600&display=swap"); @import url("https://fonts.googleapis.com/css2?family=Orbitron:wght@400;600;800&family=Inter:wght@400;600&display=swap");
@ -350,6 +351,123 @@
opacity: 0.5; opacity: 0.5;
} }
} }
/* Markdown Styles */
.markdown-content {
line-height: 1.6;
}
.markdown-content h1,
.markdown-content h2,
.markdown-content h3,
.markdown-content h4,
.markdown-content h5,
.markdown-content h6 {
margin-top: 1.5em;
margin-bottom: 0.5em;
color: var(--dante-gold);
font-weight: 600;
}
.markdown-content h1 {
font-size: 1.8em;
border-bottom: 2px solid var(--dante-gold);
padding-bottom: 0.3em;
}
.markdown-content h2 {
font-size: 1.5em;
}
.markdown-content h3 {
font-size: 1.3em;
}
.markdown-content p {
margin-bottom: 1em;
}
.markdown-content ul,
.markdown-content ol {
margin-bottom: 1em;
padding-left: 2em;
}
.markdown-content li {
margin-bottom: 0.5em;
}
.markdown-content code {
background: rgba(255, 215, 0, 0.1);
color: var(--dante-gold2);
padding: 0.2em 0.4em;
border-radius: 3px;
font-family: "Courier New", monospace;
font-size: 0.9em;
}
.markdown-content pre {
background: rgba(0, 10, 31, 0.8);
border: 1px solid rgba(255, 215, 0, 0.3);
border-radius: 8px;
padding: 1em;
overflow-x: auto;
margin-bottom: 1em;
}
.markdown-content pre code {
background: none;
padding: 0;
color: #e0e0e0;
}
.markdown-content blockquote {
border-left: 4px solid var(--dante-gold);
padding-left: 1em;
margin-left: 0;
margin-bottom: 1em;
color: #ccc;
font-style: italic;
}
.markdown-content table {
width: 100%;
border-collapse: collapse;
margin-bottom: 1em;
}
.markdown-content th,
.markdown-content td {
border: 1px solid rgba(255, 215, 0, 0.3);
padding: 0.5em;
text-align: left;
}
.markdown-content th {
background: rgba(255, 215, 0, 0.1);
color: var(--dante-gold);
font-weight: 600;
}
.markdown-content a {
color: var(--dante-gold2);
text-decoration: none;
border-bottom: 1px dotted var(--dante-gold2);
}
.markdown-content a:hover {
border-bottom: 1px solid var(--dante-gold2);
}
.markdown-content strong {
color: var(--dante-gold2);
font-weight: 600;
}
.markdown-content em {
color: #ffed4e;
font-style: italic;
}
</style> </style>
</head> </head>
<body class="relative overflow-hidden flex"> <body class="relative overflow-hidden flex">
@ -457,6 +575,16 @@
const sendBtn = document.getElementById("sendBtn"); const sendBtn = document.getElementById("sendBtn");
const newChatBtn = document.getElementById("newChatBtn"); const newChatBtn = document.getElementById("newChatBtn");
// Configure marked for markdown parsing
marked.setOptions({
highlight: function (code, lang) {
// Simple syntax highlighting - you could integrate highlight.js here
return `<pre><code class="language-${lang}">${code}</code></pre>`;
},
breaks: true,
gfm: true,
});
// Initialize // Initialize
createNewSession(); createNewSession();
@ -487,6 +615,13 @@
if (isVoiceMode) { if (isVoiceMode) {
await startVoiceSession(); await startVoiceSession();
} }
await fetch("/api/start", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
session_id: currentSessionId,
}),
});
} }
function switchSession(sessionId) { function switchSession(sessionId) {
@ -645,10 +780,16 @@
const msg = document.createElement("div"); const msg = document.createElement("div");
msg.className = "mb-8"; msg.className = "mb-8";
// Parse markdown for assistant messages
const processedContent =
role === "assistant" || role === "voice"
? marked.parse(content)
: content;
if (role === "user") { if (role === "user") {
msg.innerHTML = `<div class="flex justify-end"><div class="glass neon-border rounded-2xl px-6 py-4 max-w-3xl text-lg text-yellow-100 font-semibold shadow-2xl">${content}</div></div>`; msg.innerHTML = `<div class="flex justify-end"><div class="glass neon-border rounded-2xl px-6 py-4 max-w-3xl text-lg text-yellow-100 font-semibold shadow-2xl">${content}</div></div>`;
} else if (role === "assistant") { } else if (role === "assistant") {
msg.innerHTML = `<div class="flex justify-start"><div class="flex gap-4 max-w-3xl"><div class="w-12 h-12 rounded-xl neon-border flex items-center justify-center flex-shrink-0 shine shadow-2xl"><span class="text-2xl neon-text font-extrabold">D</span></div><div class="glass border-2 border-yellow-400/30 rounded-2xl px-6 py-4 flex-1 text-blue-50 font-medium text-lg shadow-2xl" id="${streaming ? msgId : ""}">${streaming ? "" : content}</div></div></div>`; msg.innerHTML = `<div class="flex justify-start"><div class="flex gap-4 max-w-3xl"><div class="w-12 h-12 rounded-xl neon-border flex items-center justify-center flex-shrink-0 shine shadow-2xl"><span class="text-2xl neon-text font-extrabold">D</span></div><div class="glass border-2 border-yellow-400/30 rounded-2xl px-6 py-4 flex-1 text-blue-50 font-medium text-lg shadow-2xl markdown-content" id="${streaming ? msgId : ""}">${streaming ? "" : processedContent}</div></div></div>`;
} else { } else {
// Voice message // Voice message
msg.innerHTML = `<div class="flex justify-start"><div class="flex gap-4 max-w-3xl"><div class="w-12 h-12 rounded-xl neon-border flex items-center justify-center flex-shrink-0 shine shadow-2xl"><span class="text-2xl neon-text font-extrabold">D</span></div><div class="glass border-2 border-green-400/30 rounded-2xl px-6 py-4 flex-1 text-green-100 font-medium text-lg shadow-2xl">${content}</div></div></div>`; msg.innerHTML = `<div class="flex justify-start"><div class="flex gap-4 max-w-3xl"><div class="w-12 h-12 rounded-xl neon-border flex items-center justify-center flex-shrink-0 shine shadow-2xl"><span class="text-2xl neon-text font-extrabold">D</span></div><div class="glass border-2 border-green-400/30 rounded-2xl px-6 py-4 flex-1 text-green-100 font-medium text-lg shadow-2xl">${content}</div></div></div>`;
@ -667,7 +808,10 @@
function updateLastMessage(content) { function updateLastMessage(content) {
const m = document.getElementById(streamingMessageId); const m = document.getElementById(streamingMessageId);
if (m) { if (m) {
m.textContent += content; // Parse markdown incrementally during streaming
const currentContent = m.textContent || m.innerText;
const newContent = currentContent + content;
m.innerHTML = marked.parse(newContent);
messagesDiv.scrollTop = messagesDiv.scrollHeight; messagesDiv.scrollTop = messagesDiv.scrollHeight;
} }
} }
@ -896,6 +1040,34 @@
}), }),
}); });
}; };
// Test markdown functionality
window.testMarkdown = function () {
const markdownContent = `# Título Principal
## Subtítulo
Este é um **texto em negrito** e este é um *texto em itálico*.
### Lista de Itens:
- Primeiro item
- Segundo item
- Terceiro item
### Código:
\`\`\`javascript
function exemplo() {
console.log("Olá, mundo!");
return 42;
}
\`\`\`
> Esta é uma citação importante sobre o assunto.
[Link para documentação](https://exemplo.com)`;
addMessage("assistant", markdownContent);
};
</script> </script>
</body> </body>
</html> </html>