botserver/src/sources/mod.rs

610 lines
23 KiB
Rust
Raw Normal View History

2025-12-02 21:09:43 -03:00
use crate::shared::state::AppState;
use axum::{
extract::{Query, State},
response::{Html, IntoResponse},
routing::get,
Router,
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchQuery {
pub q: Option<String>,
pub category: Option<String>,
}
pub fn configure_sources_routes() -> Router<Arc<AppState>> {
Router::new()
// Tab endpoints - match frontend hx-get endpoints
.route("/api/sources/prompts", get(handle_prompts))
.route("/api/sources/templates", get(handle_templates))
.route("/api/sources/news", get(handle_news))
.route("/api/sources/mcp-servers", get(handle_mcp_servers))
.route("/api/sources/llm-tools", get(handle_llm_tools))
.route("/api/sources/models", get(handle_models))
// Search
.route("/api/sources/search", get(handle_search))
}
/// GET /api/sources/prompts - Prompts tab content
pub async fn handle_prompts(
State(_state): State<Arc<AppState>>,
Query(params): Query<SearchQuery>,
) -> impl IntoResponse {
let category = params.category.unwrap_or_else(|| "all".to_string());
let prompts = get_prompts_data(&category);
let mut html = String::new();
html.push_str("<div class=\"panel-layout\">");
// Categories sidebar
html.push_str("<aside class=\"categories-sidebar\">");
html.push_str("<h3>Categories</h3>");
html.push_str("<div class=\"category-list\">");
let categories = vec![
("all", "All Prompts", ""),
("writing", "Writing", ""),
("coding", "Coding", ""),
("analysis", "Analysis", ""),
("creative", "Creative", ""),
("business", "Business", ""),
("education", "Education", ""),
2025-12-02 21:09:43 -03:00
];
for (id, name, icon) in &categories {
let active = if *id == category { " active" } else { "" };
html.push_str("<button class=\"category-item");
html.push_str(active);
html.push_str("\" hx-get=\"/api/sources/prompts?category=");
html.push_str(id);
html.push_str("\" hx-target=\"#content-area\" hx-swap=\"innerHTML\">");
html.push_str("<span class=\"category-icon\">");
html.push_str(icon);
html.push_str("</span>");
html.push_str("<span class=\"category-name\">");
html.push_str(name);
html.push_str("</span>");
html.push_str("</button>");
}
html.push_str("</div>");
html.push_str("</aside>");
// Prompts grid
html.push_str("<div class=\"content-main\">");
html.push_str("<div class=\"prompts-grid\" id=\"prompts-grid\">");
for prompt in &prompts {
html.push_str("<div class=\"prompt-card\">");
html.push_str("<div class=\"prompt-header\">");
html.push_str("<span class=\"prompt-icon\">");
html.push_str(&prompt.icon);
html.push_str("</span>");
html.push_str("<h4>");
html.push_str(&html_escape(&prompt.title));
html.push_str("</h4>");
html.push_str("</div>");
html.push_str("<p class=\"prompt-description\">");
html.push_str(&html_escape(&prompt.description));
html.push_str("</p>");
html.push_str("<div class=\"prompt-footer\">");
html.push_str("<span class=\"prompt-category\">");
html.push_str(&html_escape(&prompt.category));
html.push_str("</span>");
html.push_str("<button class=\"btn-use\" onclick=\"usePrompt('");
html.push_str(&html_escape(&prompt.id));
html.push_str("')\">Use</button>");
html.push_str("</div>");
html.push_str("</div>");
}
if prompts.is_empty() {
html.push_str("<div class=\"empty-state\">");
html.push_str("<p>No prompts found in this category</p>");
html.push_str("</div>");
}
html.push_str("</div>");
html.push_str("</div>");
html.push_str("</div>");
Html(html)
}
/// GET /api/sources/templates - Templates tab content
pub async fn handle_templates(State(_state): State<Arc<AppState>>) -> impl IntoResponse {
let templates = get_templates_data();
let mut html = String::new();
html.push_str("<div class=\"templates-container\">");
html.push_str("<div class=\"templates-header\">");
html.push_str("<h3>Bot Templates</h3>");
html.push_str("<p>Pre-built bot configurations ready to deploy</p>");
html.push_str("</div>");
html.push_str("<div class=\"templates-grid\">");
for template in &templates {
html.push_str("<div class=\"template-card\">");
html.push_str("<div class=\"template-icon\">");
html.push_str(&template.icon);
html.push_str("</div>");
html.push_str("<div class=\"template-info\">");
html.push_str("<h4>");
html.push_str(&html_escape(&template.name));
html.push_str("</h4>");
html.push_str("<p>");
html.push_str(&html_escape(&template.description));
html.push_str("</p>");
html.push_str("<div class=\"template-meta\">");
html.push_str("<span class=\"template-category\">");
html.push_str(&html_escape(&template.category));
html.push_str("</span>");
html.push_str("</div>");
html.push_str("</div>");
html.push_str("<div class=\"template-actions\">");
html.push_str("<button class=\"btn-preview\">Preview</button>");
html.push_str("<button class=\"btn-use-template\">Use Template</button>");
html.push_str("</div>");
html.push_str("</div>");
}
html.push_str("</div>");
html.push_str("</div>");
Html(html)
}
/// GET /api/sources/news - News tab content
pub async fn handle_news(State(_state): State<Arc<AppState>>) -> impl IntoResponse {
let news_items = vec![
("", "General Bots 6.0 Released", "Major update with improved performance and new features", "2 hours ago"),
("", "New MCP Server Integration", "Connect to external tools more easily with our new MCP support", "1 day ago"),
("", "Analytics Dashboard Update", "Real-time metrics and improved visualizations", "3 days ago"),
("", "Security Enhancement", "Enhanced encryption and authentication options", "1 week ago"),
("", "Multi-language Support", "Now supporting 15+ languages for bot conversations", "2 weeks ago"),
2025-12-02 21:09:43 -03:00
];
let mut html = String::new();
html.push_str("<div class=\"news-container\">");
html.push_str("<div class=\"news-header\">");
html.push_str("<h3>Latest News</h3>");
html.push_str("<p>Updates and announcements from the General Bots team</p>");
html.push_str("</div>");
html.push_str("<div class=\"news-list\">");
for (icon, title, description, time) in &news_items {
html.push_str("<div class=\"news-item\">");
html.push_str("<div class=\"news-icon\">");
html.push_str(icon);
html.push_str("</div>");
html.push_str("<div class=\"news-content\">");
html.push_str("<h4>");
html.push_str(&html_escape(title));
html.push_str("</h4>");
html.push_str("<p>");
html.push_str(&html_escape(description));
html.push_str("</p>");
html.push_str("<span class=\"news-time\">");
html.push_str(time);
html.push_str("</span>");
html.push_str("</div>");
html.push_str("</div>");
}
html.push_str("</div>");
html.push_str("</div>");
Html(html)
}
/// GET /api/sources/mcp-servers - MCP Servers tab content
pub async fn handle_mcp_servers(State(_state): State<Arc<AppState>>) -> impl IntoResponse {
let servers = vec![
("", "Database Server", "PostgreSQL, MySQL, SQLite connections", "Active", true),
("", "Filesystem Server", "Local and cloud file access", "Active", true),
("", "Web Server", "HTTP/REST API integrations", "Active", true),
("", "Email Server", "SMTP/IMAP email handling", "Inactive", false),
("", "Slack Server", "Slack workspace integration", "Active", true),
("", "Analytics Server", "Data processing and reporting", "Active", true),
2025-12-02 21:09:43 -03:00
];
let mut html = String::new();
html.push_str("<div class=\"mcp-container\">");
html.push_str("<div class=\"mcp-header\">");
html.push_str("<h3>MCP Servers</h3>");
html.push_str("<p>Model Context Protocol servers for extended capabilities</p>");
html.push_str("<button class=\"btn-add-server\">+ Add Server</button>");
html.push_str("</div>");
html.push_str("<div class=\"mcp-grid\">");
for (icon, name, description, status, is_active) in &servers {
let status_class = if *is_active { "status-active" } else { "status-inactive" };
html.push_str("<div class=\"mcp-card\">");
html.push_str("<div class=\"mcp-icon\">");
html.push_str(icon);
html.push_str("</div>");
html.push_str("<div class=\"mcp-info\">");
html.push_str("<h4>");
html.push_str(&html_escape(name));
html.push_str("</h4>");
html.push_str("<p>");
html.push_str(&html_escape(description));
html.push_str("</p>");
html.push_str("</div>");
html.push_str("<div class=\"mcp-status ");
html.push_str(status_class);
html.push_str("\">");
html.push_str(status);
html.push_str("</div>");
html.push_str("<div class=\"mcp-actions\">");
html.push_str("<button class=\"btn-configure\">Configure</button>");
if *is_active {
html.push_str("<button class=\"btn-disable\">Disable</button>");
} else {
html.push_str("<button class=\"btn-enable\">Enable</button>");
}
html.push_str("</div>");
html.push_str("</div>");
}
html.push_str("</div>");
html.push_str("</div>");
Html(html)
}
/// GET /api/sources/llm-tools - LLM Tools tab content
pub async fn handle_llm_tools(State(_state): State<Arc<AppState>>) -> impl IntoResponse {
let tools = vec![
("", "Web Search", "Search the web for real-time information", true),
("", "Calculator", "Perform mathematical calculations", true),
("", "Calendar", "Manage calendar events and schedules", true),
("", "Note Taking", "Create and manage notes", true),
("", "Weather", "Get weather forecasts and conditions", false),
("", "News Reader", "Fetch and summarize news articles", false),
("", "URL Fetcher", "Retrieve and parse web content", true),
("", "Code Executor", "Run code snippets safely", false),
2025-12-02 21:09:43 -03:00
];
let mut html = String::new();
html.push_str("<div class=\"tools-container\">");
html.push_str("<div class=\"tools-header\">");
html.push_str("<h3>LLM Tools</h3>");
html.push_str("<p>Extend your bot's capabilities with these tools</p>");
html.push_str("</div>");
html.push_str("<div class=\"tools-grid\">");
for (icon, name, description, enabled) in &tools {
let enabled_class = if *enabled { "enabled" } else { "disabled" };
html.push_str("<div class=\"tool-card ");
html.push_str(enabled_class);
html.push_str("\">");
html.push_str("<div class=\"tool-icon\">");
html.push_str(icon);
html.push_str("</div>");
html.push_str("<div class=\"tool-info\">");
html.push_str("<h4>");
html.push_str(&html_escape(name));
html.push_str("</h4>");
html.push_str("<p>");
html.push_str(&html_escape(description));
html.push_str("</p>");
html.push_str("</div>");
html.push_str("<label class=\"toggle-switch\">");
if *enabled {
html.push_str("<input type=\"checkbox\" checked>");
} else {
html.push_str("<input type=\"checkbox\">");
}
html.push_str("<span class=\"toggle-slider\"></span>");
html.push_str("</label>");
html.push_str("</div>");
}
html.push_str("</div>");
html.push_str("</div>");
Html(html)
}
/// GET /api/sources/models - Models tab content
pub async fn handle_models(State(_state): State<Arc<AppState>>) -> impl IntoResponse {
let models = vec![
("🧠", "GPT-4o", "OpenAI", "Latest multimodal model with vision capabilities", "Active"),
("🧠", "GPT-4o-mini", "OpenAI", "Fast and efficient for most tasks", "Active"),
("🦙", "Llama 3.1 70B", "Meta", "Open source large language model", "Available"),
("🔷", "Claude 3.5 Sonnet", "Anthropic", "Advanced reasoning and analysis", "Available"),
("💎", "Gemini Pro", "Google", "Multimodal AI with long context", "Available"),
("", "Mistral Large", "Mistral AI", "European AI model with strong performance", "Available"),
2025-12-02 21:09:43 -03:00
];
let mut html = String::new();
html.push_str("<div class=\"models-container\">");
html.push_str("<div class=\"models-header\">");
html.push_str("<h3>AI Models</h3>");
html.push_str("<p>Available language models for your bots</p>");
html.push_str("</div>");
html.push_str("<div class=\"models-grid\">");
for (icon, name, provider, description, status) in &models {
let status_class = if *status == "Active" { "model-active" } else { "model-available" };
html.push_str("<div class=\"model-card ");
html.push_str(status_class);
html.push_str("\">");
html.push_str("<div class=\"model-icon\">");
html.push_str(icon);
html.push_str("</div>");
html.push_str("<div class=\"model-info\">");
html.push_str("<div class=\"model-header\">");
html.push_str("<h4>");
html.push_str(&html_escape(name));
html.push_str("</h4>");
html.push_str("<span class=\"model-provider\">");
html.push_str(&html_escape(provider));
html.push_str("</span>");
html.push_str("</div>");
html.push_str("<p>");
html.push_str(&html_escape(description));
html.push_str("</p>");
html.push_str("<div class=\"model-footer\">");
html.push_str("<span class=\"model-status\">");
html.push_str(status);
html.push_str("</span>");
if *status == "Active" {
html.push_str("<button class=\"btn-configure\">Configure</button>");
} else {
html.push_str("<button class=\"btn-activate\">Activate</button>");
}
html.push_str("</div>");
html.push_str("</div>");
html.push_str("</div>");
}
html.push_str("</div>");
html.push_str("</div>");
Html(html)
}
/// GET /api/sources/search - Search across all sources
pub async fn handle_search(
State(_state): State<Arc<AppState>>,
Query(params): Query<SearchQuery>,
) -> impl IntoResponse {
let query = params.q.unwrap_or_default();
if query.is_empty() {
return Html("<div class=\"search-prompt\"><p>Enter a search term</p></div>".to_string());
}
let query_lower = query.to_lowercase();
// Search across prompts
let prompts = get_prompts_data("all");
let matching_prompts: Vec<_> = prompts
.iter()
.filter(|p| {
p.title.to_lowercase().contains(&query_lower)
|| p.description.to_lowercase().contains(&query_lower)
})
.collect();
// Search across templates
let templates = get_templates_data();
let matching_templates: Vec<_> = templates
.iter()
.filter(|t| {
t.name.to_lowercase().contains(&query_lower)
|| t.description.to_lowercase().contains(&query_lower)
})
.collect();
let mut html = String::new();
html.push_str("<div class=\"search-results\">");
html.push_str("<div class=\"search-header\">");
html.push_str("<h3>Search Results for \"");
html.push_str(&html_escape(&query));
html.push_str("\"</h3>");
html.push_str("</div>");
if matching_prompts.is_empty() && matching_templates.is_empty() {
html.push_str("<div class=\"no-results\">");
html.push_str("<p>No results found</p>");
html.push_str("<p class=\"hint\">Try different keywords</p>");
html.push_str("</div>");
} else {
if !matching_prompts.is_empty() {
html.push_str("<div class=\"result-section\">");
html.push_str("<h4>Prompts (");
html.push_str(&matching_prompts.len().to_string());
html.push_str(")</h4>");
html.push_str("<div class=\"results-grid\">");
for prompt in matching_prompts {
html.push_str("<div class=\"result-item prompt-result\">");
html.push_str("<span class=\"result-icon\">");
html.push_str(&prompt.icon);
html.push_str("</span>");
html.push_str("<div class=\"result-info\">");
html.push_str("<strong>");
html.push_str(&html_escape(&prompt.title));
html.push_str("</strong>");
html.push_str("<p>");
html.push_str(&html_escape(&prompt.description));
html.push_str("</p>");
html.push_str("</div>");
html.push_str("</div>");
}
html.push_str("</div>");
html.push_str("</div>");
}
if !matching_templates.is_empty() {
html.push_str("<div class=\"result-section\">");
html.push_str("<h4>Templates (");
html.push_str(&matching_templates.len().to_string());
html.push_str(")</h4>");
html.push_str("<div class=\"results-grid\">");
for template in matching_templates {
html.push_str("<div class=\"result-item template-result\">");
html.push_str("<span class=\"result-icon\">");
html.push_str(&template.icon);
html.push_str("</span>");
html.push_str("<div class=\"result-info\">");
html.push_str("<strong>");
html.push_str(&html_escape(&template.name));
html.push_str("</strong>");
html.push_str("<p>");
html.push_str(&html_escape(&template.description));
html.push_str("</p>");
html.push_str("</div>");
html.push_str("</div>");
}
html.push_str("</div>");
html.push_str("</div>");
}
}
html.push_str("</div>");
Html(html)
}
// Data structures
struct PromptData {
id: String,
title: String,
description: String,
category: String,
icon: String,
}
struct TemplateData {
name: String,
description: String,
category: String,
icon: String,
}
fn get_prompts_data(category: &str) -> Vec<PromptData> {
let all_prompts = vec![
PromptData {
id: "summarize".to_string(),
title: "Summarize Text".to_string(),
description: "Create concise summaries of long documents or articles".to_string(),
category: "writing".to_string(),
icon: "".to_string(),
2025-12-02 21:09:43 -03:00
},
PromptData {
id: "code-review".to_string(),
title: "Code Review".to_string(),
description: "Analyze code for bugs, improvements, and best practices".to_string(),
category: "coding".to_string(),
icon: "".to_string(),
2025-12-02 21:09:43 -03:00
},
PromptData {
id: "data-analysis".to_string(),
title: "Data Analysis".to_string(),
description: "Extract insights and patterns from data sets".to_string(),
category: "analysis".to_string(),
icon: "".to_string(),
2025-12-02 21:09:43 -03:00
},
PromptData {
id: "creative-writing".to_string(),
title: "Creative Writing".to_string(),
description: "Generate stories, poems, and creative content".to_string(),
category: "creative".to_string(),
icon: "".to_string(),
2025-12-02 21:09:43 -03:00
},
PromptData {
id: "email-draft".to_string(),
title: "Email Draft".to_string(),
description: "Compose professional emails quickly".to_string(),
category: "business".to_string(),
icon: "".to_string(),
2025-12-02 21:09:43 -03:00
},
PromptData {
id: "explain-concept".to_string(),
title: "Explain Concept".to_string(),
description: "Break down complex topics into simple explanations".to_string(),
category: "education".to_string(),
icon: "".to_string(),
2025-12-02 21:09:43 -03:00
},
PromptData {
id: "debug-code".to_string(),
title: "Debug Code".to_string(),
description: "Find and fix issues in your code".to_string(),
category: "coding".to_string(),
icon: "🐛".to_string(),
},
PromptData {
id: "meeting-notes".to_string(),
title: "Meeting Notes".to_string(),
description: "Organize and format meeting discussions".to_string(),
category: "business".to_string(),
icon: "".to_string(),
2025-12-02 21:09:43 -03:00
},
];
if category == "all" {
all_prompts
} else {
all_prompts
.into_iter()
.filter(|p| p.category == category)
.collect()
}
}
fn get_templates_data() -> Vec<TemplateData> {
vec![
TemplateData {
name: "Customer Support Bot".to_string(),
description: "Handle customer inquiries and support tickets automatically".to_string(),
category: "Support".to_string(),
icon: "🎧".to_string(),
},
TemplateData {
name: "FAQ Bot".to_string(),
description: "Answer frequently asked questions from your knowledge base".to_string(),
category: "Support".to_string(),
icon: "".to_string(),
2025-12-02 21:09:43 -03:00
},
TemplateData {
name: "Lead Generation Bot".to_string(),
description: "Qualify leads and collect prospect information".to_string(),
category: "Sales".to_string(),
icon: "".to_string(),
2025-12-02 21:09:43 -03:00
},
TemplateData {
name: "Onboarding Bot".to_string(),
description: "Guide new users through your product or service".to_string(),
category: "HR".to_string(),
icon: "👋".to_string(),
},
TemplateData {
name: "Survey Bot".to_string(),
description: "Collect feedback through conversational surveys".to_string(),
category: "Research".to_string(),
icon: "".to_string(),
2025-12-02 21:09:43 -03:00
},
TemplateData {
name: "Appointment Scheduler".to_string(),
description: "Book and manage appointments automatically".to_string(),
category: "Productivity".to_string(),
icon: "".to_string(),
2025-12-02 21:09:43 -03:00
},
]
}
fn html_escape(s: &str) -> String {
s.replace('&', "&amp;")
.replace('<', "&lt;")
.replace('>', "&gt;")
.replace('"', "&quot;")
.replace('\'', "&#39;")
}