Merge branch 'main' of github.com:GeneralBots/BotServer
This commit is contained in:
commit
2a6c5c21ed
4 changed files with 490 additions and 64 deletions
|
|
@ -3,7 +3,7 @@ use crate::shared::models::{Automation, TriggerKind};
|
||||||
use crate::shared::state::AppState;
|
use crate::shared::state::AppState;
|
||||||
use chrono::{DateTime, Datelike, Timelike, Utc};
|
use chrono::{DateTime, Datelike, Timelike, Utc};
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use log::{error, info, warn};
|
use log::{error, info, trace, warn};
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
@ -17,6 +17,7 @@ pub struct AutomationService {
|
||||||
|
|
||||||
impl AutomationService {
|
impl AutomationService {
|
||||||
pub fn new(state: Arc<AppState>, scripts_dir: &str) -> Self {
|
pub fn new(state: Arc<AppState>, scripts_dir: &str) -> Self {
|
||||||
|
trace!("Creating AutomationService with scripts_dir='{}'", scripts_dir);
|
||||||
Self {
|
Self {
|
||||||
state,
|
state,
|
||||||
scripts_dir: scripts_dir.to_string(),
|
scripts_dir: scripts_dir.to_string(),
|
||||||
|
|
@ -24,6 +25,7 @@ impl AutomationService {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn spawn(self) -> tokio::task::JoinHandle<()> {
|
pub fn spawn(self) -> tokio::task::JoinHandle<()> {
|
||||||
|
trace!("Spawning AutomationService background task");
|
||||||
let service = Arc::new(self);
|
let service = Arc::new(self);
|
||||||
tokio::task::spawn_local({
|
tokio::task::spawn_local({
|
||||||
let service = service.clone();
|
let service = service.clone();
|
||||||
|
|
@ -32,9 +34,11 @@ impl AutomationService {
|
||||||
let mut last_check = Utc::now();
|
let mut last_check = Utc::now();
|
||||||
loop {
|
loop {
|
||||||
interval.tick().await;
|
interval.tick().await;
|
||||||
|
trace!("Automation cycle tick started; last_check={}", last_check);
|
||||||
if let Err(e) = service.run_cycle(&mut last_check).await {
|
if let Err(e) = service.run_cycle(&mut last_check).await {
|
||||||
error!("Automation cycle error: {}", e);
|
error!("Automation cycle error: {}", e);
|
||||||
}
|
}
|
||||||
|
trace!("Automation cycle tick completed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -44,45 +48,75 @@ impl AutomationService {
|
||||||
&self,
|
&self,
|
||||||
last_check: &mut DateTime<Utc>,
|
last_check: &mut DateTime<Utc>,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
trace!("Running automation cycle; last_check={}", last_check);
|
||||||
let automations = self.load_active_automations().await?;
|
let automations = self.load_active_automations().await?;
|
||||||
|
trace!("Loaded {} active automations", automations.len());
|
||||||
self.check_table_changes(&automations, *last_check).await;
|
self.check_table_changes(&automations, *last_check).await;
|
||||||
self.process_schedules(&automations).await;
|
self.process_schedules(&automations).await;
|
||||||
*last_check = Utc::now();
|
*last_check = Utc::now();
|
||||||
|
trace!("Automation cycle finished; new last_check={}", last_check);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn load_active_automations(&self) -> Result<Vec<Automation>, diesel::result::Error> {
|
async fn load_active_automations(&self) -> Result<Vec<Automation>, diesel::result::Error> {
|
||||||
|
trace!("Loading active automations from database");
|
||||||
use crate::shared::models::system_automations::dsl::*;
|
use crate::shared::models::system_automations::dsl::*;
|
||||||
let mut conn = self.state.conn.lock().unwrap();
|
let mut conn = self.state.conn.lock().unwrap();
|
||||||
system_automations
|
let result = system_automations
|
||||||
.filter(is_active.eq(true))
|
.filter(is_active.eq(true))
|
||||||
.load::<Automation>(&mut *conn)
|
.load::<Automation>(&mut *conn);
|
||||||
.map_err(Into::into)
|
trace!("Database query for active automations completed");
|
||||||
|
result.map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn check_table_changes(&self, automations: &[Automation], since: DateTime<Utc>) {
|
async fn check_table_changes(&self, automations: &[Automation], since: DateTime<Utc>) {
|
||||||
|
trace!("Checking table changes since={}", since);
|
||||||
for automation in automations {
|
for automation in automations {
|
||||||
let trigger_kind = match crate::shared::models::TriggerKind::from_i32(automation.kind) {
|
trace!(
|
||||||
|
"Checking automation id={} kind={} target={:?}",
|
||||||
|
automation.id,
|
||||||
|
automation.kind,
|
||||||
|
automation.target
|
||||||
|
);
|
||||||
|
|
||||||
|
let trigger_kind = match TriggerKind::from_i32(automation.kind) {
|
||||||
Some(k) => k,
|
Some(k) => k,
|
||||||
None => continue,
|
None => {
|
||||||
|
trace!("Skipping automation {}: invalid TriggerKind", automation.id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if !matches!(
|
if !matches!(
|
||||||
trigger_kind,
|
trigger_kind,
|
||||||
TriggerKind::TableUpdate | TriggerKind::TableInsert | TriggerKind::TableDelete
|
TriggerKind::TableUpdate | TriggerKind::TableInsert | TriggerKind::TableDelete
|
||||||
) {
|
) {
|
||||||
|
trace!(
|
||||||
|
"Skipping automation {}: trigger_kind {:?} not table-related",
|
||||||
|
automation.id,
|
||||||
|
trigger_kind
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let table = match &automation.target {
|
let table = match &automation.target {
|
||||||
Some(t) => t,
|
Some(t) => t,
|
||||||
None => continue,
|
None => {
|
||||||
|
trace!("Skipping automation {}: no table target", automation.id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let column = match trigger_kind {
|
let column = match trigger_kind {
|
||||||
TriggerKind::TableInsert => "created_at",
|
TriggerKind::TableInsert => "created_at",
|
||||||
_ => "updated_at",
|
_ => "updated_at",
|
||||||
};
|
};
|
||||||
|
trace!(
|
||||||
|
"Building query for table='{}' column='{}' trigger_kind={:?}",
|
||||||
|
table,
|
||||||
|
column,
|
||||||
|
trigger_kind
|
||||||
|
);
|
||||||
|
|
||||||
let query = format!(
|
let query = format!(
|
||||||
"SELECT COUNT(*) as count FROM {} WHERE {} > $1",
|
"SELECT COUNT(*) as count FROM {} WHERE {} > $1",
|
||||||
|
|
@ -104,11 +138,23 @@ impl AutomationService {
|
||||||
|
|
||||||
match count_result {
|
match count_result {
|
||||||
Ok(result) if result.count > 0 => {
|
Ok(result) if result.count > 0 => {
|
||||||
|
trace!(
|
||||||
|
"Detected {} change(s) in table='{}'; triggering automation {}",
|
||||||
|
result.count,
|
||||||
|
table,
|
||||||
|
automation.id
|
||||||
|
);
|
||||||
drop(conn_guard);
|
drop(conn_guard);
|
||||||
self.execute_action(&automation.param).await;
|
self.execute_action(&automation.param).await;
|
||||||
self.update_last_triggered(automation.id).await;
|
self.update_last_triggered(automation.id).await;
|
||||||
}
|
}
|
||||||
Ok(_result) => {}
|
Ok(result) => {
|
||||||
|
trace!(
|
||||||
|
"No changes detected for automation {} (count={})",
|
||||||
|
automation.id,
|
||||||
|
result.count
|
||||||
|
);
|
||||||
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Error checking changes for table '{}': {}", table, e);
|
error!("Error checking changes for table '{}': {}", table, e);
|
||||||
}
|
}
|
||||||
|
|
@ -118,12 +164,31 @@ impl AutomationService {
|
||||||
|
|
||||||
async fn process_schedules(&self, automations: &[Automation]) {
|
async fn process_schedules(&self, automations: &[Automation]) {
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
|
trace!(
|
||||||
|
"Processing scheduled automations at UTC={}",
|
||||||
|
now.format("%Y-%m-%d %H:%M:%S")
|
||||||
|
);
|
||||||
for automation in automations {
|
for automation in automations {
|
||||||
if let Some(TriggerKind::Scheduled) = TriggerKind::from_i32(automation.kind) {
|
if let Some(TriggerKind::Scheduled) = TriggerKind::from_i32(automation.kind) {
|
||||||
|
trace!(
|
||||||
|
"Evaluating schedule pattern={:?} for automation {}",
|
||||||
|
automation.schedule,
|
||||||
|
automation.id
|
||||||
|
);
|
||||||
if let Some(pattern) = &automation.schedule {
|
if let Some(pattern) = &automation.schedule {
|
||||||
if Self::should_run_cron(pattern, now.timestamp()) {
|
if Self::should_run_cron(pattern, now.timestamp()) {
|
||||||
|
trace!(
|
||||||
|
"Pattern matched; executing automation {} param='{}'",
|
||||||
|
automation.id,
|
||||||
|
automation.param
|
||||||
|
);
|
||||||
self.execute_action(&automation.param).await;
|
self.execute_action(&automation.param).await;
|
||||||
self.update_last_triggered(automation.id).await;
|
self.update_last_triggered(automation.id).await;
|
||||||
|
} else {
|
||||||
|
trace!(
|
||||||
|
"Pattern did not match for automation {}",
|
||||||
|
automation.id
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -131,6 +196,7 @@ impl AutomationService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update_last_triggered(&self, automation_id: Uuid) {
|
async fn update_last_triggered(&self, automation_id: Uuid) {
|
||||||
|
trace!("Updating last_triggered for automation_id={}", automation_id);
|
||||||
use crate::shared::models::system_automations::dsl::*;
|
use crate::shared::models::system_automations::dsl::*;
|
||||||
let mut conn = self.state.conn.lock().unwrap();
|
let mut conn = self.state.conn.lock().unwrap();
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
|
|
@ -142,30 +208,45 @@ impl AutomationService {
|
||||||
"Failed to update last_triggered for automation {}: {}",
|
"Failed to update last_triggered for automation {}: {}",
|
||||||
automation_id, e
|
automation_id, e
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
trace!("Successfully updated last_triggered for {}", automation_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn should_run_cron(pattern: &str, timestamp: i64) -> bool {
|
fn should_run_cron(pattern: &str, timestamp: i64) -> bool {
|
||||||
|
trace!("Evaluating cron pattern='{}' at timestamp={}", pattern, timestamp);
|
||||||
let parts: Vec<&str> = pattern.split_whitespace().collect();
|
let parts: Vec<&str> = pattern.split_whitespace().collect();
|
||||||
if parts.len() != 5 {
|
if parts.len() != 5 {
|
||||||
|
trace!("Invalid cron pattern '{}'", pattern);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
let dt = match DateTime::<Utc>::from_timestamp(timestamp, 0) {
|
let dt = match DateTime::<Utc>::from_timestamp(timestamp, 0) {
|
||||||
Some(dt) => dt,
|
Some(dt) => dt,
|
||||||
None => return false,
|
None => {
|
||||||
|
trace!("Invalid timestamp={}", timestamp);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let minute = dt.minute() as i32;
|
let minute = dt.minute() as i32;
|
||||||
let hour = dt.hour() as i32;
|
let hour = dt.hour() as i32;
|
||||||
let day = dt.day() as i32;
|
let day = dt.day() as i32;
|
||||||
let month = dt.month() as i32;
|
let month = dt.month() as i32;
|
||||||
let weekday = dt.weekday().num_days_from_monday() as i32;
|
let weekday = dt.weekday().num_days_from_monday() as i32;
|
||||||
[minute, hour, day, month, weekday]
|
let match_result = [minute, hour, day, month, weekday]
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.all(|(i, &val)| Self::cron_part_matches(parts[i], val))
|
.all(|(i, &val)| Self::cron_part_matches(parts[i], val));
|
||||||
|
trace!(
|
||||||
|
"Cron pattern='{}' result={} at {}",
|
||||||
|
pattern,
|
||||||
|
match_result,
|
||||||
|
dt
|
||||||
|
);
|
||||||
|
match_result
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cron_part_matches(part: &str, value: i32) -> bool {
|
fn cron_part_matches(part: &str, value: i32) -> bool {
|
||||||
|
trace!("Checking cron part '{}' against value={}", part, value);
|
||||||
if part == "*" {
|
if part == "*" {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -183,46 +264,38 @@ impl AutomationService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn execute_action(&self, param: &str) {
|
async fn execute_action(&self, param: &str) {
|
||||||
// Get bot_id early to use in Redis key
|
trace!("Starting execute_action with param='{}'", param);
|
||||||
let bot_id_string = env::var("BOT_GUID").unwrap_or_else(|_| "default_bot".to_string());
|
let bot_id_string = env::var("BOT_GUID").unwrap_or_else(|_| "default_bot".to_string());
|
||||||
let bot_id = Uuid::parse_str(&bot_id_string).unwrap_or_else(|_| Uuid::new_v4());
|
let bot_id = Uuid::parse_str(&bot_id_string).unwrap_or_else(|_| Uuid::new_v4());
|
||||||
|
trace!("Resolved bot_id={} for param='{}'", bot_id, param);
|
||||||
|
|
||||||
// Check if this job is already running for this bot
|
|
||||||
let is_bas_file = param.ends_with(".bas");
|
|
||||||
let redis_key = format!("job:running:{}:{}", bot_id, param);
|
let redis_key = format!("job:running:{}:{}", bot_id, param);
|
||||||
|
trace!("Redis key for job tracking: {}", redis_key);
|
||||||
|
|
||||||
// Try to check if job is running using Redis
|
|
||||||
if let Some(redis_client) = &self.state.redis_client {
|
if let Some(redis_client) = &self.state.redis_client {
|
||||||
match redis_client.get_multiplexed_async_connection().await {
|
match redis_client.get_multiplexed_async_connection().await {
|
||||||
Ok(mut conn) => {
|
Ok(mut conn) => {
|
||||||
// Check if key exists
|
trace!("Connected to Redis; checking if job '{}' is running", param);
|
||||||
let is_running: Result<bool, redis::RedisError> = redis::cmd("EXISTS")
|
let is_running: Result<bool, redis::RedisError> = redis::cmd("EXISTS")
|
||||||
.arg(&redis_key)
|
.arg(&redis_key)
|
||||||
.query_async(&mut conn)
|
.query_async(&mut conn)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
if let Ok(true) = is_running {
|
if let Ok(true) = is_running {
|
||||||
if is_bas_file {
|
|
||||||
warn!(
|
warn!(
|
||||||
"⚠️ Job '{}' is already running for bot '{}', skipping execution to allow only one .bas execution per bot",
|
"Job '{}' is already running for bot '{}'; skipping execution",
|
||||||
param, bot_id
|
param, bot_id
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
info!(
|
|
||||||
"Job '{}' is already running for bot '{}', skipping execution",
|
|
||||||
param, bot_id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark job as running (set with 300 second expiry as safety)
|
|
||||||
let _: Result<(), redis::RedisError> = redis::cmd("SETEX")
|
let _: Result<(), redis::RedisError> = redis::cmd("SETEX")
|
||||||
.arg(&redis_key)
|
.arg(&redis_key)
|
||||||
.arg(300) // 5 minutes expiry
|
.arg(300)
|
||||||
.arg("1")
|
.arg("1")
|
||||||
.query_async(&mut conn)
|
.query_async(&mut conn)
|
||||||
.await;
|
.await;
|
||||||
|
trace!("Job '{}' marked as running in Redis", param);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Failed to connect to Redis for job tracking: {}", e);
|
warn!("Failed to connect to Redis for job tracking: {}", e);
|
||||||
|
|
@ -231,16 +304,20 @@ impl AutomationService {
|
||||||
}
|
}
|
||||||
|
|
||||||
let full_path = Path::new(&self.scripts_dir).join(param);
|
let full_path = Path::new(&self.scripts_dir).join(param);
|
||||||
|
trace!("Resolved full path: {}", full_path.display());
|
||||||
|
|
||||||
let script_content = match tokio::fs::read_to_string(&full_path).await {
|
let script_content = match tokio::fs::read_to_string(&full_path).await {
|
||||||
Ok(content) => content,
|
Ok(content) => {
|
||||||
|
trace!("Script '{}' read successfully", param);
|
||||||
|
content
|
||||||
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to read script {}: {}", full_path.display(), e);
|
error!("Failed to read script {}: {}", full_path.display(), e);
|
||||||
// Clean up running flag on error
|
|
||||||
self.cleanup_job_flag(&bot_id, param).await;
|
self.cleanup_job_flag(&bot_id, param).await;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
info!("Executing action with param: {} for bot: {}", param, bot_id);
|
|
||||||
let user_session = crate::shared::models::UserSession {
|
let user_session = crate::shared::models::UserSession {
|
||||||
id: Uuid::new_v4(),
|
id: Uuid::new_v4(),
|
||||||
user_id: Uuid::new_v4(),
|
user_id: Uuid::new_v4(),
|
||||||
|
|
@ -252,30 +329,46 @@ impl AutomationService {
|
||||||
created_at: Utc::now(),
|
created_at: Utc::now(),
|
||||||
updated_at: Utc::now(),
|
updated_at: Utc::now(),
|
||||||
};
|
};
|
||||||
|
trace!(
|
||||||
|
"Created temporary UserSession id={} for bot_id={}",
|
||||||
|
user_session.id,
|
||||||
|
bot_id
|
||||||
|
);
|
||||||
|
|
||||||
let script_service = ScriptService::new(Arc::clone(&self.state), user_session);
|
let script_service = ScriptService::new(Arc::clone(&self.state), user_session);
|
||||||
let ast = match script_service.compile(&script_content) {
|
let ast = match script_service.compile(&script_content) {
|
||||||
Ok(ast) => ast,
|
Ok(ast) => {
|
||||||
|
trace!("Compilation successful for script '{}'", param);
|
||||||
|
ast
|
||||||
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Error compiling script: {}", e);
|
error!("Error compiling script '{}': {}", param, e);
|
||||||
|
self.cleanup_job_flag(&bot_id, param).await;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
trace!("Running compiled script '{}'", param);
|
||||||
match script_service.run(&ast) {
|
match script_service.run(&ast) {
|
||||||
Ok(_result) => {
|
Ok(_) => {
|
||||||
info!("Script executed successfully");
|
info!("Script '{}' executed successfully", param);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Error executing script: {}", e);
|
error!("Error executing script '{}': {}", param, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up running flag after execution
|
trace!("Cleaning up Redis flag for job '{}'", param);
|
||||||
self.cleanup_job_flag(&bot_id, param).await;
|
self.cleanup_job_flag(&bot_id, param).await;
|
||||||
|
trace!("Finished execute_action for '{}'", param);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn cleanup_job_flag(&self, bot_id: &Uuid, param: &str) {
|
async fn cleanup_job_flag(&self, bot_id: &Uuid, param: &str) {
|
||||||
|
trace!(
|
||||||
|
"Cleaning up Redis flag for bot_id={} param='{}'",
|
||||||
|
bot_id,
|
||||||
|
param
|
||||||
|
);
|
||||||
let redis_key = format!("job:running:{}:{}", bot_id, param);
|
let redis_key = format!("job:running:{}:{}", bot_id, param);
|
||||||
|
|
||||||
if let Some(redis_client) = &self.state.redis_client {
|
if let Some(redis_client) = &self.state.redis_client {
|
||||||
|
|
@ -285,6 +378,7 @@ impl AutomationService {
|
||||||
.arg(&redis_key)
|
.arg(&redis_key)
|
||||||
.query_async(&mut conn)
|
.query_async(&mut conn)
|
||||||
.await;
|
.await;
|
||||||
|
trace!("Removed Redis key '{}'", redis_key);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Failed to connect to Redis for cleanup: {}", e);
|
warn!("Failed to connect to Redis for cleanup: {}", e);
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ pub fn llm_keyword(state: Arc<AppState>, _user: UserSession, engine: &mut Engine
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
match rx.recv_timeout(Duration::from_secs(180)) {
|
match rx.recv_timeout(Duration::from_secs(500)) {
|
||||||
Ok(Ok(result)) => Ok(Dynamic::from(result)),
|
Ok(Ok(result)) => Ok(Dynamic::from(result)),
|
||||||
Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
|
Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
|
||||||
e.to_string().into(),
|
e.to_string().into(),
|
||||||
|
|
|
||||||
|
|
@ -189,14 +189,22 @@ async fn start_llm_server(
|
||||||
std::env::set_var("OMP_PROC_BIND", "close");
|
std::env::set_var("OMP_PROC_BIND", "close");
|
||||||
|
|
||||||
// "cd {} && numactl --interleave=all ./llama-server -m {} --host 0.0.0.0 --port {} --threads 20 --threads-batch 40 --temp 0.7 --parallel 1 --repeat-penalty 1.1 --ctx-size 8192 --batch-size 8192 -n 4096 --mlock --no-mmap --flash-attn --no-kv-offload --no-mmap &",
|
// "cd {} && numactl --interleave=all ./llama-server -m {} --host 0.0.0.0 --port {} --threads 20 --threads-batch 40 --temp 0.7 --parallel 1 --repeat-penalty 1.1 --ctx-size 8192 --batch-size 8192 -n 4096 --mlock --no-mmap --flash-attn --no-kv-offload --no-mmap &",
|
||||||
|
if cfg!(windows) {
|
||||||
|
let mut cmd = tokio::process::Command::new("cmd");
|
||||||
|
cmd.arg("/C").arg(format!(
|
||||||
|
"cd {} && .\\llama-server.exe -m {} --host 0.0.0.0 --port {} --top_p 0.95 --temp 0.6 --flash-attn on --ctx-size 4096 --repeat-penalty 1.2 -ngl 20 ",
|
||||||
|
llama_cpp_path, model_path, port
|
||||||
|
));
|
||||||
|
cmd.spawn()?;
|
||||||
|
} else {
|
||||||
let mut cmd = tokio::process::Command::new("sh");
|
let mut cmd = tokio::process::Command::new("sh");
|
||||||
cmd.arg("-c").arg(format!(
|
cmd.arg("-c").arg(format!(
|
||||||
"cd {} && ./llama-server -m {} --host 0.0.0.0 --port {} --top_p 0.95 --temp 0.6 --flash-attn on --ctx-size 4096 --repeat-penalty 1.2 -ngl 20 &",
|
"cd {} && ./llama-server -m {} --host 0.0.0.0 --port {} --top_p 0.95 --temp 0.6 --flash-attn on --ctx-size 4096 --repeat-penalty 1.2 -ngl 20 &",
|
||||||
llama_cpp_path, model_path, port
|
llama_cpp_path, model_path, port
|
||||||
));
|
));
|
||||||
|
|
||||||
cmd.spawn()?;
|
cmd.spawn()?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -207,16 +215,24 @@ async fn start_embedding_server(
|
||||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
let port = url.split(':').last().unwrap_or("8082");
|
let port = url.split(':').last().unwrap_or("8082");
|
||||||
|
|
||||||
|
if cfg!(windows) {
|
||||||
|
let mut cmd = tokio::process::Command::new("cmd");
|
||||||
|
cmd.arg("/c").arg(format!(
|
||||||
|
"cd {} && .\\llama-server.exe -m {} --host 0.0.0.0 --port {} --embedding --n-gpu-layers 99",
|
||||||
|
llama_cpp_path, model_path, port
|
||||||
|
));
|
||||||
|
cmd.spawn()?;
|
||||||
|
} else {
|
||||||
let mut cmd = tokio::process::Command::new("sh");
|
let mut cmd = tokio::process::Command::new("sh");
|
||||||
cmd.arg("-c").arg(format!(
|
cmd.arg("-c").arg(format!(
|
||||||
"cd {} && ./llama-server -m {} --host 0.0.0.0 --port {} --embedding --n-gpu-layers 99 &",
|
"cd {} && ./llama-server -m {} --host 0.0.0.0 --port {} --embedding --n-gpu-layers 99 &",
|
||||||
llama_cpp_path, model_path, port
|
llama_cpp_path, model_path, port
|
||||||
));
|
));
|
||||||
|
|
||||||
cmd.spawn()?;
|
cmd.spawn()?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn is_server_running(url: &str) -> bool {
|
async fn is_server_running(url: &str) -> bool {
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::Client::new();
|
||||||
match client.get(&format!("{}/health", url)).send().await {
|
match client.get(&format!("{}/health", url)).send().await {
|
||||||
|
|
@ -275,7 +291,7 @@ pub async fn chat_completions_local(
|
||||||
|
|
||||||
// Send request to llama.cpp server
|
// Send request to llama.cpp server
|
||||||
let client = Client::builder()
|
let client = Client::builder()
|
||||||
.timeout(Duration::from_secs(180)) // 2 minute timeout
|
.timeout(Duration::from_secs(500)) // 2 minute timeout
|
||||||
.build()
|
.build()
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
error!("Error creating HTTP client: {}", e);
|
error!("Error creating HTTP client: {}", e);
|
||||||
|
|
|
||||||
332
web/index.html
332
web/index.html
|
|
@ -213,6 +213,7 @@
|
||||||
max-width: 900px;
|
max-width: 900px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
#emptyState {
|
#emptyState {
|
||||||
|
|
@ -612,6 +613,93 @@
|
||||||
background: rgba(255, 215, 0, 0.5);
|
background: rgba(255, 215, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* NEW STYLES FOR IMPROVEMENTS */
|
||||||
|
|
||||||
|
/* Scroll to bottom button */
|
||||||
|
.scroll-to-bottom {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background: rgba(255, 215, 0, 0.8);
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: #0a0e27;
|
||||||
|
font-size: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-to-bottom:hover {
|
||||||
|
background: rgba(255, 215, 0, 1);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Continue button for interrupted responses */
|
||||||
|
.continue-button {
|
||||||
|
display: inline-block;
|
||||||
|
background: rgba(255, 215, 0, 0.2);
|
||||||
|
border: 1px solid rgba(255, 215, 0, 0.4);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
color: #ffd700;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-top: 10px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.continue-button:hover {
|
||||||
|
background: rgba(255, 215, 0, 0.3);
|
||||||
|
box-shadow: 0 0 10px rgba(255, 215, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Context usage indicator */
|
||||||
|
.context-indicator {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 90px;
|
||||||
|
right: 20px;
|
||||||
|
width: 120px;
|
||||||
|
background: rgba(10, 14, 39, 0.9);
|
||||||
|
border: 1px solid rgba(255, 215, 0, 0.3);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
z-index: 100;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-progress {
|
||||||
|
height: 6px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 3px;
|
||||||
|
margin-top: 5px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-progress-bar {
|
||||||
|
height: 100%;
|
||||||
|
background: #90ee90;
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: width 0.3s ease, background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-progress-bar.warning {
|
||||||
|
background: #ffd700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-progress-bar.danger {
|
||||||
|
background: #ff6b6b;
|
||||||
|
}
|
||||||
|
|
||||||
/* Mobile Responsiveness */
|
/* Mobile Responsiveness */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.sidebar {
|
.sidebar {
|
||||||
|
|
@ -676,6 +764,20 @@
|
||||||
width: 14px;
|
width: 14px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.scroll-to-bottom {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
font-size: 16px;
|
||||||
|
bottom: 15px;
|
||||||
|
right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-indicator {
|
||||||
|
bottom: 70px;
|
||||||
|
right: 15px;
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
|
|
@ -707,6 +809,18 @@
|
||||||
.empty-title {
|
.empty-title {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.scroll-to-bottom {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-indicator {
|
||||||
|
bottom: 65px;
|
||||||
|
right: 10px;
|
||||||
|
width: 90px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
@ -765,6 +879,19 @@
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- New elements for improvements -->
|
||||||
|
<button class="scroll-to-bottom" id="scrollToBottom" style="display: none">
|
||||||
|
↓
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="context-indicator" id="contextIndicator" style="display: none">
|
||||||
|
<div>Contexto</div>
|
||||||
|
<div id="contextPercentage">0%</div>
|
||||||
|
<div class="context-progress">
|
||||||
|
<div class="context-progress-bar" id="contextProgressBar" style="width: 0%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
let ws = null;
|
let ws = null;
|
||||||
let currentSessionId = null;
|
let currentSessionId = null;
|
||||||
|
|
@ -781,13 +908,21 @@
|
||||||
let reconnectAttempts = 0;
|
let reconnectAttempts = 0;
|
||||||
const maxReconnectAttempts = 5;
|
const maxReconnectAttempts = 5;
|
||||||
let reconnectTimeout = null;
|
let reconnectTimeout = null;
|
||||||
|
let thinkingTimeout = null;
|
||||||
|
let lastMessageLength = 0;
|
||||||
|
let contextUsage = 0;
|
||||||
|
let isUserScrolling = false;
|
||||||
|
let autoScrollEnabled = true;
|
||||||
|
|
||||||
const messagesDiv = document.getElementById("messages");
|
const messagesDiv = document.getElementById("messages");
|
||||||
const input = document.getElementById("messageInput");
|
const input = document.getElementById("messageInput");
|
||||||
const sendBtn = document.getElementById("sendBtn");
|
const sendBtn = document.getElementById("sendBtn");
|
||||||
const newChatBtn = document.getElementById("newChatBtn");
|
const newChatBtn = document.getElementById("newChatBtn");
|
||||||
const connectionStatus =
|
const connectionStatus = document.getElementById("connectionStatus");
|
||||||
document.getElementById("connectionStatus");
|
const scrollToBottomBtn = document.getElementById("scrollToBottom");
|
||||||
|
const contextIndicator = document.getElementById("contextIndicator");
|
||||||
|
const contextPercentage = document.getElementById("contextPercentage");
|
||||||
|
const contextProgressBar = document.getElementById("contextProgressBar");
|
||||||
|
|
||||||
marked.setOptions({
|
marked.setOptions({
|
||||||
breaks: true,
|
breaks: true,
|
||||||
|
|
@ -828,6 +963,61 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Scroll management
|
||||||
|
messagesDiv.addEventListener("scroll", function() {
|
||||||
|
// Check if user is scrolling manually
|
||||||
|
const isAtBottom = messagesDiv.scrollHeight - messagesDiv.scrollTop <= messagesDiv.clientHeight + 100;
|
||||||
|
|
||||||
|
if (!isAtBottom) {
|
||||||
|
isUserScrolling = true;
|
||||||
|
showScrollToBottomButton();
|
||||||
|
} else {
|
||||||
|
isUserScrolling = false;
|
||||||
|
hideScrollToBottomButton();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function scrollToBottom() {
|
||||||
|
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
||||||
|
isUserScrolling = false;
|
||||||
|
hideScrollToBottomButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showScrollToBottomButton() {
|
||||||
|
scrollToBottomBtn.style.display = "flex";
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideScrollToBottomButton() {
|
||||||
|
scrollToBottomBtn.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollToBottomBtn.addEventListener("click", scrollToBottom);
|
||||||
|
|
||||||
|
// Context usage management
|
||||||
|
function updateContextUsage(usage) {
|
||||||
|
contextUsage = usage;
|
||||||
|
const percentage = Math.min(100, Math.round(usage * 100));
|
||||||
|
|
||||||
|
contextPercentage.textContent = `${percentage}%`;
|
||||||
|
contextProgressBar.style.width = `${percentage}%`;
|
||||||
|
|
||||||
|
// Update color based on usage
|
||||||
|
if (percentage >= 90) {
|
||||||
|
contextProgressBar.className = "context-progress-bar danger";
|
||||||
|
} else if (percentage >= 70) {
|
||||||
|
contextProgressBar.className = "context-progress-bar warning";
|
||||||
|
} else {
|
||||||
|
contextProgressBar.className = "context-progress-bar";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show indicator if usage is above 50%
|
||||||
|
if (percentage >= 50) {
|
||||||
|
contextIndicator.style.display = "block";
|
||||||
|
} else {
|
||||||
|
contextIndicator.style.display = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function initializeAuth() {
|
async function initializeAuth() {
|
||||||
try {
|
try {
|
||||||
updateConnectionStatus("connecting");
|
updateConnectionStatus("connecting");
|
||||||
|
|
@ -888,6 +1078,8 @@
|
||||||
<p class="empty-subtitle">Seu assistente de IA avançado</p>
|
<p class="empty-subtitle">Seu assistente de IA avançado</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
// Reset context usage for new session
|
||||||
|
updateContextUsage(0);
|
||||||
if (isVoiceMode) {
|
if (isVoiceMode) {
|
||||||
await startVoiceSession();
|
await startVoiceSession();
|
||||||
}
|
}
|
||||||
|
|
@ -935,11 +1127,14 @@
|
||||||
<p class="empty-subtitle">Seu assistente de IA avançado</p>
|
<p class="empty-subtitle">Seu assistente de IA avançado</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
updateContextUsage(0);
|
||||||
} else {
|
} else {
|
||||||
// Display existing history
|
// Display existing history
|
||||||
history.forEach(([role, content]) => {
|
history.forEach(([role, content]) => {
|
||||||
addMessage(role, content, false);
|
addMessage(role, content, false);
|
||||||
});
|
});
|
||||||
|
// Estimate context usage based on message count
|
||||||
|
updateContextUsage(history.length / 2); // Assuming 20 messages is 100% context
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to load session history:", error);
|
console.error("Failed to load session history:", error);
|
||||||
|
|
@ -984,6 +1179,11 @@
|
||||||
);
|
);
|
||||||
updateConnectionStatus("disconnected");
|
updateConnectionStatus("disconnected");
|
||||||
|
|
||||||
|
// If we were streaming and connection was lost, show continue button
|
||||||
|
if (isStreaming) {
|
||||||
|
showContinueButton();
|
||||||
|
}
|
||||||
|
|
||||||
if (reconnectAttempts < maxReconnectAttempts) {
|
if (reconnectAttempts < maxReconnectAttempts) {
|
||||||
reconnectAttempts++;
|
reconnectAttempts++;
|
||||||
const delay = Math.min(1000 * reconnectAttempts, 10000);
|
const delay = Math.min(1000 * reconnectAttempts, 10000);
|
||||||
|
|
@ -1013,6 +1213,11 @@
|
||||||
emptyState.remove();
|
emptyState.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle context usage if provided
|
||||||
|
if (response.context_usage !== undefined) {
|
||||||
|
updateContextUsage(response.context_usage);
|
||||||
|
}
|
||||||
|
|
||||||
// Handle complete messages
|
// Handle complete messages
|
||||||
if (response.is_complete) {
|
if (response.is_complete) {
|
||||||
if (isStreaming) {
|
if (isStreaming) {
|
||||||
|
|
@ -1055,6 +1260,9 @@
|
||||||
case "warn":
|
case "warn":
|
||||||
showWarning(eventData.message);
|
showWarning(eventData.message);
|
||||||
break;
|
break;
|
||||||
|
case "context_usage":
|
||||||
|
updateContextUsage(eventData.usage);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1088,14 +1296,28 @@
|
||||||
ease: "power2.out",
|
ease: "power2.out",
|
||||||
});
|
});
|
||||||
|
|
||||||
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
// Auto-scroll to show thinking indicator
|
||||||
|
if (!isUserScrolling) {
|
||||||
|
scrollToBottom();
|
||||||
|
} else {
|
||||||
|
showScrollToBottomButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set timeout to automatically hide thinking indicator after 30 seconds
|
||||||
|
// This handles cases where the server restarts and doesn't send thinking_end
|
||||||
|
thinkingTimeout = setTimeout(() => {
|
||||||
|
if (isThinking) {
|
||||||
|
hideThinkingIndicator();
|
||||||
|
showWarning("O servidor pode estar ocupado. A resposta está demorando demais.");
|
||||||
|
}
|
||||||
|
}, 60000);
|
||||||
|
|
||||||
isThinking = true;
|
isThinking = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideThinkingIndicator() {
|
function hideThinkingIndicator() {
|
||||||
if (!isThinking) return;
|
if (!isThinking) return;
|
||||||
const thinkingDiv =
|
const thinkingDiv = document.getElementById("thinking-indicator");
|
||||||
document.getElementById("thinking-indicator");
|
|
||||||
if (thinkingDiv) {
|
if (thinkingDiv) {
|
||||||
gsap.to(thinkingDiv, {
|
gsap.to(thinkingDiv, {
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
|
|
@ -1107,6 +1329,11 @@
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// Clear the timeout if thinking ends normally
|
||||||
|
if (thinkingTimeout) {
|
||||||
|
clearTimeout(thinkingTimeout);
|
||||||
|
thinkingTimeout = null;
|
||||||
|
}
|
||||||
isThinking = false;
|
isThinking = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1123,7 +1350,11 @@
|
||||||
ease: "power2.out",
|
ease: "power2.out",
|
||||||
});
|
});
|
||||||
|
|
||||||
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
if (!isUserScrolling) {
|
||||||
|
scrollToBottom();
|
||||||
|
} else {
|
||||||
|
showScrollToBottomButton();
|
||||||
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (warningDiv.parentNode) {
|
if (warningDiv.parentNode) {
|
||||||
|
|
@ -1136,6 +1367,62 @@
|
||||||
}, 5000);
|
}, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showContinueButton() {
|
||||||
|
const continueDiv = document.createElement("div");
|
||||||
|
continueDiv.className = "message-container";
|
||||||
|
continueDiv.innerHTML = `
|
||||||
|
<div class="assistant-message">
|
||||||
|
<div class="assistant-avatar">D</div>
|
||||||
|
<div class="assistant-message-content">
|
||||||
|
<p>A conexão foi interrompida. Clique em "Continuar" para tentar recuperar a resposta.</p>
|
||||||
|
<button class="continue-button" onclick="continueInterruptedResponse()">Continuar</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
messagesDiv.appendChild(continueDiv);
|
||||||
|
|
||||||
|
gsap.to(continueDiv, {
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
duration: 0.5,
|
||||||
|
ease: "power2.out",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isUserScrolling) {
|
||||||
|
scrollToBottom();
|
||||||
|
} else {
|
||||||
|
showScrollToBottomButton();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function continueInterruptedResponse() {
|
||||||
|
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||||
|
connectWebSocket();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a continue request to the server
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
const continueData = {
|
||||||
|
bot_id: "default_bot",
|
||||||
|
user_id: currentUserId,
|
||||||
|
session_id: currentSessionId,
|
||||||
|
channel: "web",
|
||||||
|
content: "continue",
|
||||||
|
message_type: 3, // Special message type for continue requests
|
||||||
|
media_url: null,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.send(JSON.stringify(continueData));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the continue button
|
||||||
|
const continueButtons = document.querySelectorAll('.continue-button');
|
||||||
|
continueButtons.forEach(button => {
|
||||||
|
button.parentElement.parentElement.parentElement.remove();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function addMessage(
|
function addMessage(
|
||||||
role,
|
role,
|
||||||
content,
|
content,
|
||||||
|
|
@ -1161,6 +1448,8 @@
|
||||||
<div class="user-message-content">${escapeHtml(content)}</div>
|
<div class="user-message-content">${escapeHtml(content)}</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
// Update context usage when user sends a message
|
||||||
|
updateContextUsage(contextUsage + 0.05); // Simulate 5% increase per message
|
||||||
} else if (role === "assistant") {
|
} else if (role === "assistant") {
|
||||||
msg.innerHTML = `
|
msg.innerHTML = `
|
||||||
<div class="assistant-message">
|
<div class="assistant-message">
|
||||||
|
|
@ -1170,6 +1459,15 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
// Update context usage when assistant responds
|
||||||
|
updateContextUsage(contextUsage + 0.03); // Simulate 3% increase per response
|
||||||
|
} else if (role === "voice") {
|
||||||
|
msg.innerHTML = `
|
||||||
|
<div class="assistant-message">
|
||||||
|
<div class="assistant-avatar">🎤</div>
|
||||||
|
<div class="assistant-message-content">${content}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
} else {
|
} else {
|
||||||
msg.innerHTML = `
|
msg.innerHTML = `
|
||||||
<div class="assistant-message">
|
<div class="assistant-message">
|
||||||
|
|
@ -1188,14 +1486,25 @@
|
||||||
ease: "power2.out",
|
ease: "power2.out",
|
||||||
});
|
});
|
||||||
|
|
||||||
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
// Auto-scroll to bottom if user isn't manually scrolling
|
||||||
|
if (!isUserScrolling) {
|
||||||
|
scrollToBottom();
|
||||||
|
} else {
|
||||||
|
showScrollToBottomButton();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateStreamingMessage(content) {
|
function updateStreamingMessage(content) {
|
||||||
const msgElement = document.getElementById(streamingMessageId);
|
const msgElement = document.getElementById(streamingMessageId);
|
||||||
if (msgElement) {
|
if (msgElement) {
|
||||||
msgElement.innerHTML = marked.parse(content);
|
msgElement.innerHTML = marked.parse(content);
|
||||||
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
|
||||||
|
// Auto-scroll to bottom if user isn't manually scrolling
|
||||||
|
if (!isUserScrolling) {
|
||||||
|
scrollToBottom();
|
||||||
|
} else {
|
||||||
|
showScrollToBottomButton();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1206,6 +1515,13 @@
|
||||||
currentStreamingContent,
|
currentStreamingContent,
|
||||||
);
|
);
|
||||||
msgElement.removeAttribute("id");
|
msgElement.removeAttribute("id");
|
||||||
|
|
||||||
|
// Auto-scroll to bottom if user isn't manually scrolling
|
||||||
|
if (!isUserScrolling) {
|
||||||
|
scrollToBottom();
|
||||||
|
} else {
|
||||||
|
showScrollToBottomButton();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue