use inquire::validator::Validation;
use inquire::{Confirm, Select, Text};
use strum::{IntoEnumIterator, VariantNames};
use crate::cli::schema_builder::build_response_format_interactive;
use crate::{Account, Flarer, JsonOptions, ResponseFormat, Tool};
const BANNER: &str = r"
Welcome to
FFFFFFFF LL AAAA RRRRRR EEEEEEEE RRRRRR
FF LL AA AA RR RR EE RR RR
FFFFFF LL AA AA RRRRRR EEEEEE RRRRRR
FF LL AAAAAAAA RR RR EE RR RR
FF LLLLLLLL AA AA RR RR EEEEEEEE RR RR
CLI tool to interact with Cloudflare's Browser Rendering API.
";
#[derive(Debug, Clone)]
pub struct InteractiveConfig {
pub url: String,
pub tool: Tool,
pub json_options: JsonOptions,
}
pub async fn interactive_init() -> crate::Result<Flarer> {
println!("{BANNER}");
let account = match Account::from_env() {
Ok(a) => a,
Err(_) => prompt_for_account()?,
};
let flarer = Flarer::builder().account(account).build()?;
flarer.verify().await?;
Ok(flarer)
}
fn prompt_for_account() -> crate::Result<Account> {
let id = Text::new("Enter your Cloudflare Account ID:")
.with_help_message("https://developers.cloudflare.com/workers-ai/get-started/rest-api/")
.prompt()
.map_err(|e| crate::FlarerError::Config(e.to_string()))?;
let token = Text::new("Enter your Cloudflare API Token:")
.with_help_message("https://developers.cloudflare.com/workers-ai/get-started/rest-api/")
.prompt()
.map_err(|e| crate::FlarerError::Config(e.to_string()))?;
Ok(Account::new(id, token))
}
pub async fn interactive_prompt() -> crate::Result<InteractiveConfig> {
let url = url_entry_with_retry()?;
let tool = select_tool()?;
let json_options = if matches!(tool, Tool::Json) {
json_options_handler().await?
} else {
JsonOptions::default()
};
Ok(InteractiveConfig {
url,
tool,
json_options,
})
}
fn url_entry_with_retry() -> crate::Result<String> {
loop {
let raw = Text::new("Enter URL:")
.with_help_message("e.g. https://example.com")
.with_validator(|input: &str| {
if url::Url::parse(input).is_ok() {
Ok(Validation::Valid)
} else {
Ok(Validation::Invalid(
"Must be a valid absolute URL (https://...)".into(),
))
}
})
.prompt()
.map_err(|e| crate::FlarerError::Config(e.to_string()))?;
if url::Url::parse(&raw).is_ok() {
return Ok(raw);
}
let retry = Confirm::new("Invalid URL. Try again?")
.with_default(true)
.prompt()
.map_err(|e| crate::FlarerError::Config(e.to_string()))?;
if !retry {
return Err(crate::FlarerError::Config("URL entry cancelled".into()));
}
}
}
fn select_tool() -> crate::Result<Tool> {
let names: Vec<&'static str> = Tool::VARIANTS.to_vec();
let chosen = Select::new("Select tool:", names)
.with_help_message("Use arrow keys to navigate")
.prompt()
.map_err(|e| crate::FlarerError::Config(e.to_string()))?;
Tool::iter()
.find(|t| <&'static str>::from(*t) == chosen)
.ok_or_else(|| crate::FlarerError::Unexpected(format!("unknown tool {}", chosen)))
}
async fn json_options_handler() -> crate::Result<JsonOptions> {
let choice = Select::new(
"Configure JSON tool:",
vec![
"Add a prompt only",
"Define a structured response format only",
"Both",
"Neither",
],
)
.with_help_message("Use arrow keys to navigate")
.prompt()
.map_err(|e| crate::FlarerError::Config(e.to_string()))?;
let mut options = JsonOptions::default();
match choice {
"Add a prompt only" => {
options.prompt = Some(prompt_text()?);
}
"Define a structured response format only" => {
options.response_format = Some(prompt_response_format().await?);
}
"Both" => {
options.prompt = Some(prompt_text()?);
options.response_format = Some(prompt_response_format().await?);
}
_ => {}
}
Ok(options)
}
fn prompt_text() -> crate::Result<String> {
Text::new("Enter your prompt:")
.with_help_message("Free-form instruction for the LLM extractor")
.prompt()
.map_err(|e| crate::FlarerError::Config(e.to_string()))
}
async fn prompt_response_format() -> crate::Result<ResponseFormat> {
let parsing = Select::new(
"How would you like to provide the response_format?",
vec![
"Paste a JSON schema",
"Load a schema.json file",
"Build it interactively step by step",
],
)
.with_help_message("Use arrow keys to navigate")
.prompt()
.map_err(|e| crate::FlarerError::Config(e.to_string()))?;
match parsing {
"Paste a JSON schema" => paste_format(),
"Load a schema.json file" => json_file_input(),
"Build it interactively step by step" => build_response_format_interactive().await,
_ => Err(crate::FlarerError::Unexpected(
"unknown response_format option".into(),
)),
}
}
fn paste_format() -> crate::Result<ResponseFormat> {
let raw = Text::new("Enter your JSON response_format:")
.with_help_message("Press Enter to submit")
.with_validator(
|input: &str| match serde_json::from_str::<serde_json::Value>(input) {
Ok(_) => Ok(Validation::Valid),
Err(e) => Ok(Validation::Invalid(format!("Invalid JSON: {}", e).into())),
},
)
.prompt()
.map_err(|e| crate::FlarerError::Config(e.to_string()))?;
Ok(serde_json::from_str(&raw)?)
}
fn json_file_input() -> crate::Result<ResponseFormat> {
loop {
let path = Text::new("Path to schema.json:")
.with_help_message("e.g. ./schema.json")
.prompt()
.map_err(|e| crate::FlarerError::Config(e.to_string()))?;
if !std::path::Path::new(&path).exists() {
println!("File does not exist. Try again.");
continue;
}
let content = std::fs::read_to_string(&path)?;
return Ok(serde_json::from_str(&content)?);
}
}