use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum CaptureMethod {
Screen,
Window(String),
BrowserUrl(String),
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct VisualScore {
pub composition: f64,
pub hierarchy: f64,
pub readability: f64,
pub consistency: f64,
pub accessibility: f64,
pub overall: f64,
#[serde(default)]
pub suggestions: Vec<String>,
}
impl VisualScore {
pub fn compute_overall(&mut self) {
self.overall = self.composition * 0.20
+ self.hierarchy * 0.20
+ self.readability * 0.25
+ self.consistency * 0.15
+ self.accessibility * 0.20;
}
}
#[derive(Debug, Clone)]
pub struct VisualFeedbackLoop {
pub max_iterations: usize,
pub quality_threshold: f64,
pub vision_model_id: String,
pub capture_method: CaptureMethod,
}
impl Default for VisualFeedbackLoop {
fn default() -> Self {
Self {
max_iterations: 5,
quality_threshold: 0.8,
vision_model_id: "vision".to_string(),
capture_method: CaptureMethod::Screen,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VisualLoopResult {
pub iterations: usize,
pub threshold_met: bool,
pub score_history: Vec<VisualScore>,
pub final_score: VisualScore,
}
pub fn build_critic_prompt(
context: &str,
previous_score: Option<&VisualScore>,
iteration: usize,
) -> String {
let mut prompt = format!(
"You are evaluating a visual design for the following task:\n\n{}\n\n\
This is iteration {} of the visual feedback loop.\n\n",
context,
iteration + 1
);
if let Some(prev) = previous_score {
prompt.push_str(&format!(
"Previous iteration scores:\n\
- Composition: {:.0}\n\
- Hierarchy: {:.0}\n\
- Readability: {:.0}\n\
- Consistency: {:.0}\n\
- Accessibility: {:.0}\n\
- Overall: {:.0}\n\
Previous suggestions: {}\n\n",
prev.composition,
prev.hierarchy,
prev.readability,
prev.consistency,
prev.accessibility,
prev.overall,
prev.suggestions.join("; "),
));
}
prompt.push_str(
"Analyze the screenshot and respond with ONLY a JSON object (no markdown, no explanation):\n\
```json\n\
{\n \
\"composition\": <0-100>,\n \
\"hierarchy\": <0-100>,\n \
\"readability\": <0-100>,\n \
\"consistency\": <0-100>,\n \
\"accessibility\": <0-100>,\n \
\"overall\": <weighted average>,\n \
\"suggestions\": [\"specific improvement 1\", \"specific improvement 2\"]\n\
}\n\
```"
);
prompt
}
pub fn parse_critic_response(response: &str) -> Result<VisualScore> {
let trimmed = response.trim();
let json_str = if let Some(start) = trimmed.find('{') {
if let Some(end) = trimmed.rfind('}') {
&trimmed[start..=end]
} else {
trimmed
}
} else {
trimmed
};
serde_json::from_str(json_str)
.context("Failed to parse VLM critic response as VisualScore JSON")
}
#[cfg(test)]
#[path = "../../tests/unit/orchestration/visual_loop/visual_loop_test.rs"]
mod tests;