botserver/src/designer/designer_api/utils.rs
Rodrigo Rodriguez 5ea171d126
Some checks failed
BotServer CI / build (push) Failing after 1m34s
Refactor: Split large files into modular subdirectories
Split 20+ files over 1000 lines into focused subdirectories for better
maintainability and code organization. All changes maintain backward
compatibility through re-export wrappers.

Major splits:
- attendance/llm_assist.rs (2074→7 modules)
- basic/keywords/face_api.rs → face_api/ (7 modules)
- basic/keywords/file_operations.rs → file_ops/ (8 modules)
- basic/keywords/hear_talk.rs → hearing/ (6 modules)
- channels/wechat.rs → wechat/ (10 modules)
- channels/youtube.rs → youtube/ (5 modules)
- contacts/mod.rs → contacts_api/ (6 modules)
- core/bootstrap/mod.rs → bootstrap/ (5 modules)
- core/shared/admin.rs → admin_*.rs (5 modules)
- designer/canvas.rs → canvas_api/ (6 modules)
- designer/mod.rs → designer_api/ (6 modules)
- docs/handlers.rs → handlers_api/ (11 modules)
- drive/mod.rs → drive_handlers.rs, drive_types.rs
- learn/mod.rs → types.rs
- main.rs → main_module/ (7 modules)
- meet/webinar.rs → webinar_api/ (8 modules)
- paper/mod.rs → (10 modules)
- security/auth.rs → auth_api/ (7 modules)
- security/passkey.rs → (4 modules)
- sources/mod.rs → sources_api/ (5 modules)
- tasks/mod.rs → task_api/ (5 modules)

Stats: 38,040 deletions, 1,315 additions across 318 files

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-12 21:09:30 +00:00

169 lines
4.6 KiB
Rust

use chrono::{DateTime, Utc};
use crate::core::shared::state::AppState;
pub fn get_default_files() -> Vec<(String, String, DateTime<Utc>)> {
vec![
(
"welcome".to_string(),
"Welcome Dialog".to_string(),
Utc::now(),
),
("faq".to_string(), "FAQ Bot".to_string(), Utc::now()),
(
"support".to_string(),
"Customer Support".to_string(),
Utc::now(),
),
]
}
pub fn get_default_dialog_content() -> String {
"' Welcome Dialog\n\
' Created with Dialog Designer\n\
\n\
SUB Main()\n\
TALK \"Hello! How can I help you today?\"\n\
\n\
answer = HEAR\n\
\n\
IF answer LIKE \"*help*\" THEN\n\
TALK \"I'm here to assist you.\"\n\
ELSE IF answer LIKE \"*bye*\" THEN\n\
TALK \"Goodbye!\"\n\
ELSE\n\
TALK \"I understand: \" + answer\n\
END IF\n\
END SUB\n"
.to_string()
}
pub async fn load_from_drive(
state: &AppState,
bucket: &str,
path: &str,
) -> Result<String, String> {
let s3_client = state
.drive
.as_ref()
.ok_or_else(|| "S3 service not available".to_string())?;
let result = s3_client
.get_object()
.bucket(bucket)
.key(path)
.send()
.await
.map_err(|e| format!("Failed to read file from drive: {e}"))?;
let bytes = result
.body
.collect()
.await
.map_err(|e| format!("Failed to read file body: {e}"))?
.into_bytes();
String::from_utf8(bytes.to_vec()).map_err(|e| format!("File is not valid UTF-8: {e}"))
}
pub struct DialogNode {
pub id: String,
pub node_type: String,
pub content: String,
pub x: i32,
pub y: i32,
}
pub fn parse_basic_to_nodes(content: &str) -> Vec<DialogNode> {
let mut nodes = Vec::new();
let mut y_pos = 100;
for (i, line) in content.lines().enumerate() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('\'') {
continue;
}
let upper = trimmed.to_uppercase();
let node_type = if upper.starts_with("TALK ") {
"talk"
} else if upper.starts_with("HEAR") {
"hear"
} else if upper.starts_with("IF ") {
"if"
} else if upper.starts_with("FOR ") {
"for"
} else if upper.starts_with("SET ") || upper.contains(" = ") {
"set"
} else if upper.starts_with("CALL ") {
"call"
} else if upper.starts_with("SUB ") {
"sub"
} else {
continue;
};
nodes.push(DialogNode {
id: format!("node-{}", i),
node_type: node_type.to_string(),
content: trimmed.to_string(),
x: 400,
y: y_pos,
});
y_pos += 80;
}
nodes
}
pub fn format_node_html(node: &DialogNode) -> String {
let mut html = String::new();
html.push_str("<div class=\"canvas-node node-");
html.push_str(&node.node_type);
html.push_str("\" id=\"");
html.push_str(&html_escape(&node.id));
html.push_str("\" style=\"left: ");
html.push_str(&node.x.to_string());
html.push_str("px; top: ");
html.push_str(&node.y.to_string());
html.push_str("px;\" draggable=\"true\">");
html.push_str("<div class=\"node-header\">");
html.push_str("<span class=\"node-type\">");
html.push_str(&node.node_type.to_uppercase());
html.push_str("</span>");
html.push_str("</div>");
html.push_str("<div class=\"node-content\">");
html.push_str(&html_escape(&node.content));
html.push_str("</div>");
html.push_str("<div class=\"node-ports\">");
html.push_str("<div class=\"port port-in\"></div>");
html.push_str("<div class=\"port port-out\"></div>");
html.push_str("</div>");
html.push_str("</div>");
html
}
pub fn format_relative_time(time: DateTime<Utc>) -> String {
let now = Utc::now();
let duration = now.signed_duration_since(time);
if duration.num_seconds() < 60 {
"just now".to_string()
} else if duration.num_minutes() < 60 {
format!("{}m ago", duration.num_minutes())
} else if duration.num_hours() < 24 {
format!("{}h ago", duration.num_hours())
} else if duration.num_days() < 7 {
format!("{}d ago", duration.num_days())
} else {
time.format("%b %d").to_string()
}
}
pub fn html_escape(s: &str) -> String {
s.replace('&', "&amp;")
.replace('<', "&lt;")
.replace('>', "&gt;")
.replace('"', "&quot;")
.replace('\'', "&#39;")
}