use ferro_json_ui::COMPONENT_CATALOG;
use regex::Regex;
use std::fs;
use std::path::Path;
use crate::commands::generate_routes;
pub fn call_anthropic(system: &str, user_prompt: &str) -> Result<String, String> {
let api_key = std::env::var("ANTHROPIC_API_KEY").map_err(|_| {
"ANTHROPIC_API_KEY not set. Export it with:\n \
export ANTHROPIC_API_KEY=sk-ant-...\n\
Or use --no-ai for a static template."
.to_string()
})?;
let model = std::env::var("FERRO_AI_MODEL").unwrap_or_else(|_| "claude-sonnet-4-5".to_string());
let body = serde_json::json!({
"model": model,
"max_tokens": 8192,
"temperature": 0.2,
"system": [
{
"type": "text",
"text": system,
"cache_control": {"type": "ephemeral"}
}
],
"messages": [
{"role": "user", "content": user_prompt},
{"role": "assistant", "content": "//!"}
]
});
let client = reqwest::blocking::Client::builder()
.timeout(std::time::Duration::from_secs(60))
.build()
.map_err(|e| format!("Failed to create HTTP client: {e}"))?;
let response = client
.post("https://api.anthropic.com/v1/messages")
.header("x-api-key", &api_key)
.header("anthropic-version", "2023-06-01")
.header("content-type", "application/json")
.json(&body)
.send()
.map_err(|e| format!("API request failed: {e}"))?;
let status = response.status();
let text = response
.text()
.map_err(|e| format!("Failed to read response body: {e}"))?;
if !status.is_success() {
return Err(format!("Anthropic API error ({status}): {text}"));
}
let json: serde_json::Value =
serde_json::from_str(&text).map_err(|e| format!("Failed to parse response JSON: {e}"))?;
let response_text = json["content"]
.as_array()
.and_then(|arr| arr.first())
.and_then(|item| item["text"].as_str())
.ok_or_else(|| format!("Unexpected response structure: {text}"))?;
Ok(format!("//!{response_text}"))
}
pub fn build_view_context(name: &str, description: &str) -> (String, String) {
let system = format!(
"You are a Ferro framework JSON-UI view code generator. Generate only valid Rust source \
code for src/views/ files.\n\n\
Rules:\n\
- Import only types actually used from `use ferro::{{...}};`\n\
- Use the builder pattern: `JsonUiView::new().title().layout().component()`\n\
- Use .layout(\"app\") unless the view is for auth (use \"auth\")\n\
- Use real route handler names for actions when matching routes exist\n\
- Use data_path bindings for form fields when matching model fields exist\n\
- Return ONLY Rust source code, no explanation\n\n\
{COMPONENT_CATALOG}\n\n\
<example>\n\
Input: user_list view showing all users in a table with edit and delete actions\n\
Output:\n\
//! User List JSON-UI view\n\n\
use ferro::{{\n\
Action, Component, ComponentNode, JsonUiView, TableColumn, TableProps, TextElement, \
TextProps,\n\
}};\n\n\
pub fn view() -> JsonUiView {{\n\
JsonUiView::new()\n\
.title(\"User List\")\n\
.layout(\"app\")\n\
.component(ComponentNode {{\n\
key: \"heading\".to_string(),\n\
component: Component::Text(TextProps {{\n\
content: \"User List\".to_string(),\n\
element: TextElement::H1,\n\
}}),\n\
action: None,\n\
visibility: None,\n\
}})\n\
.component(ComponentNode {{\n\
key: \"users_table\".to_string(),\n\
component: Component::Table(TableProps {{\n\
columns: vec![\n\
TableColumn {{ key: \"name\".to_string(), label: \"Name\".to_string(), \
format: None }},\n\
TableColumn {{ key: \"email\".to_string(), label: \
\"Email\".to_string(), format: None }},\n\
],\n\
data_path: \"users\".to_string(),\n\
row_actions: Some(vec![\n\
Action::get(\"user_controller.edit\"),\n\
Action::delete(\"user_controller.destroy\").confirm_danger(\"Delete \
user\"),\n\
]),\n\
empty_message: Some(\"No users found.\".to_string()),\n\
sortable: None,\n\
sort_column: None,\n\
sort_direction: None,\n\
}}),\n\
action: None,\n\
visibility: None,\n\
}})\n\
}}\n\
</example>",
);
let mut user_prompt = String::new();
let models = scan_models();
if !models.is_empty() {
user_prompt.push_str("## Project Models\n");
user_prompt.push_str(&models);
user_prompt.push('\n');
}
let routes = scan_routes();
if !routes.is_empty() {
user_prompt.push_str("## Project Routes\n");
user_prompt.push_str(&routes);
user_prompt.push('\n');
}
user_prompt.push_str(&format!(
"Generate `src/views/{name}.rs`:\n\
View name: {name}\n\
Description: {description}",
));
(system, user_prompt)
}
fn scan_models() -> String {
let models_dir = Path::new("src/models");
if !models_dir.exists() {
return String::new();
}
let struct_re = Regex::new(r"pub\s+struct\s+(\w+)\s*\{").unwrap();
let field_re = Regex::new(r"pub\s+(\w+)\s*:\s*([^,\n]+)").unwrap();
let mut output = String::new();
let entries: Vec<_> = match fs::read_dir(models_dir) {
Ok(entries) => entries.filter_map(|e| e.ok()).collect(),
Err(_) => return String::new(),
};
for entry in entries {
let path = entry.path();
if path.extension().is_none_or(|ext| ext != "rs") {
continue;
}
if path.file_name().is_some_and(|n| n == "mod.rs") {
continue;
}
let content = match fs::read_to_string(&path) {
Ok(c) => c,
Err(_) => continue,
};
for struct_cap in struct_re.captures_iter(&content) {
let struct_name = &struct_cap[1];
let struct_start = struct_cap.get(0).unwrap().end();
let rest = &content[struct_start..];
let mut depth = 1;
let mut struct_end = rest.len();
for (i, ch) in rest.chars().enumerate() {
match ch {
'{' => depth += 1,
'}' => {
depth -= 1;
if depth == 0 {
struct_end = i;
break;
}
}
_ => {}
}
}
let struct_body = &rest[..struct_end];
let fields: Vec<String> = field_re
.captures_iter(struct_body)
.map(|cap| {
let field_name = cap[1].trim();
let field_type = cap[2].trim().trim_end_matches(',');
format!("{field_name} ({field_type})")
})
.collect();
if !fields.is_empty() {
output.push_str(&format!("### {}: {}\n", struct_name, fields.join(", ")));
}
}
}
output
}
fn scan_routes() -> String {
let routes_file = Path::new("src/routes.rs");
if !routes_file.exists() {
return String::new();
}
let content = match fs::read_to_string(routes_file) {
Ok(c) => c,
Err(_) => return String::new(),
};
let routes = generate_routes::parse_routes_file(&content);
let mut output = String::new();
for route in &routes {
let method = match route.method {
generate_routes::HttpMethod::Get => "GET",
generate_routes::HttpMethod::Post => "POST",
generate_routes::HttpMethod::Put => "PUT",
generate_routes::HttpMethod::Patch => "PATCH",
generate_routes::HttpMethod::Delete => "DELETE",
};
let name_suffix = route
.name
.as_ref()
.map(|n| format!(" (name: \"{n}\")"))
.unwrap_or_default();
output.push_str(&format!(
"{} {} -> {}::{}{}\n",
method, route.path, route.handler_module, route.handler_fn, name_suffix
));
}
output
}