2026-07-15 17:02:06 +03:00
|
|
|
//! AI Script panel — AI-powered procedural drawing script generation.
|
2026-07-11 18:13:03 +03:00
|
|
|
//!
|
2026-07-15 17:02:06 +03:00
|
|
|
//! Uses an LLM to generate HCIE-Script DSL commands from natural language
|
|
|
|
|
//! descriptions. Supports Ollama, Claude, and OpenAI providers. The generated
|
|
|
|
|
//! script can be executed directly or copied to the script editor panel.
|
2026-07-11 18:13:03 +03:00
|
|
|
|
2026-07-15 17:02:06 +03:00
|
|
|
use crate::app::{HcieIcedApp, Message};
|
2026-07-11 18:13:03 +03:00
|
|
|
use iced::widget::{button, column, container, horizontal_rule, row, scrollable, text, text_input};
|
|
|
|
|
use iced::{Element, Length};
|
|
|
|
|
|
2026-07-15 17:02:06 +03:00
|
|
|
/// Build the AI script panel view from application state.
|
2026-07-11 18:13:03 +03:00
|
|
|
///
|
2026-07-15 17:02:06 +03:00
|
|
|
/// Displays provider selection, URL/model inputs, a prompt text area,
|
|
|
|
|
/// action buttons (Generate, Clear, Run, Copy), and the generated script
|
|
|
|
|
/// output with error display.
|
|
|
|
|
pub fn view(app: &HcieIcedApp) -> Element<'static, Message> {
|
2026-07-16 18:41:05 +03:00
|
|
|
let colors = app.theme_state.colors();
|
2026-07-15 17:02:06 +03:00
|
|
|
// ── Provider selector row ────────────────────────────
|
2026-07-11 18:13:03 +03:00
|
|
|
let provider_label = text("Provider").size(10);
|
2026-07-15 17:02:06 +03:00
|
|
|
let ollama_btn = button(text("Ollama").size(10))
|
|
|
|
|
.on_press(Message::AiScriptProviderChanged("ollama".to_string()))
|
|
|
|
|
.padding([4, 8]);
|
|
|
|
|
let claude_btn = button(text("Claude").size(10))
|
|
|
|
|
.on_press(Message::AiScriptProviderChanged("claude".to_string()))
|
|
|
|
|
.padding([4, 8]);
|
|
|
|
|
let openai_btn = button(text("OpenAI").size(10))
|
|
|
|
|
.on_press(Message::AiScriptProviderChanged("openai".to_string()))
|
|
|
|
|
.padding([4, 8]);
|
|
|
|
|
let provider_selector = row![ollama_btn, claude_btn, openai_btn].spacing(4);
|
2026-07-11 18:13:03 +03:00
|
|
|
|
2026-07-15 17:02:06 +03:00
|
|
|
// ── URL and model inputs ─────────────────────────────
|
|
|
|
|
let url_input = text_input("Base URL", &app.ai_script_url)
|
|
|
|
|
.width(Length::Fill)
|
|
|
|
|
.on_input(Message::AiScriptUrlChanged);
|
2026-07-11 18:13:03 +03:00
|
|
|
|
2026-07-15 17:02:06 +03:00
|
|
|
let model_input = text_input("Model", &app.ai_script_model)
|
|
|
|
|
.width(Length::Fill)
|
|
|
|
|
.on_input(Message::AiScriptModelChanged);
|
2026-07-11 18:13:03 +03:00
|
|
|
|
2026-07-15 17:02:06 +03:00
|
|
|
// ── Prompt input ─────────────────────────────────────
|
|
|
|
|
let prompt_input = text_input("Describe what to draw...", &app.ai_script_prompt)
|
|
|
|
|
.width(Length::Fill)
|
|
|
|
|
.on_input(Message::AiScriptPromptChanged);
|
2026-07-11 18:13:03 +03:00
|
|
|
|
2026-07-15 17:02:06 +03:00
|
|
|
// ── Action buttons ───────────────────────────────────
|
|
|
|
|
let generate_btn = if app.ai_script_is_running {
|
|
|
|
|
button(text("Generating...").size(11)).padding([6, 12])
|
|
|
|
|
} else {
|
|
|
|
|
button(text("Generate Script").size(11))
|
|
|
|
|
.on_press(Message::AiScriptGenerate)
|
|
|
|
|
.padding([6, 12])
|
|
|
|
|
};
|
2026-07-11 18:13:03 +03:00
|
|
|
|
2026-07-16 18:41:05 +03:00
|
|
|
let run_btn = if app.ai_script_output.is_empty() || app.ai_script_is_running {
|
|
|
|
|
button(text("Run Script").size(11)).padding([6, 12])
|
|
|
|
|
} else {
|
|
|
|
|
button(text("Run Script").size(11))
|
|
|
|
|
.on_press(Message::AiScriptRun)
|
|
|
|
|
.padding([6, 12])
|
|
|
|
|
};
|
2026-07-11 18:13:03 +03:00
|
|
|
|
2026-07-16 18:41:05 +03:00
|
|
|
let copy_btn = if app.ai_script_output.is_empty() {
|
|
|
|
|
button(text("To Editor").size(11)).padding([6, 12])
|
|
|
|
|
} else {
|
|
|
|
|
button(text("To Editor").size(11))
|
|
|
|
|
.on_press(Message::AiScriptCopy)
|
|
|
|
|
.padding([6, 12])
|
|
|
|
|
};
|
|
|
|
|
let copy_output_btn = if app.ai_script_output.is_empty() {
|
|
|
|
|
button(text("Copy Output").size(11)).padding([6, 12])
|
|
|
|
|
} else {
|
|
|
|
|
button(text("Copy Output").size(11))
|
|
|
|
|
.on_press(Message::AiScriptCopyOutput)
|
|
|
|
|
.padding([6, 12])
|
|
|
|
|
};
|
2026-07-11 18:13:03 +03:00
|
|
|
|
|
|
|
|
let clear_btn = button(text("Clear").size(11))
|
2026-07-15 17:02:06 +03:00
|
|
|
.on_press(Message::AiScriptClear)
|
2026-07-11 18:13:03 +03:00
|
|
|
.padding([6, 12]);
|
|
|
|
|
|
2026-07-15 17:02:06 +03:00
|
|
|
// ── Script output area ───────────────────────────────
|
|
|
|
|
let script_content = if app.ai_script_output.is_empty() {
|
|
|
|
|
text("Generated script will appear here...").size(10)
|
|
|
|
|
} else {
|
|
|
|
|
text(app.ai_script_output.clone()).size(10)
|
|
|
|
|
};
|
2026-07-11 18:13:03 +03:00
|
|
|
|
2026-07-16 18:41:05 +03:00
|
|
|
let script_area = container(scrollable(script_content).height(Length::Fill))
|
|
|
|
|
.padding(8)
|
|
|
|
|
.style(move |_theme| iced::widget::container::Style {
|
|
|
|
|
background: Some(iced::Background::Color(colors.bg_app)),
|
|
|
|
|
border: iced::Border::default().rounded(4).color(colors.border_high),
|
|
|
|
|
..Default::default()
|
|
|
|
|
});
|
2026-07-11 18:13:03 +03:00
|
|
|
|
2026-07-15 17:02:06 +03:00
|
|
|
// ── Error display ────────────────────────────────────
|
|
|
|
|
let error_section: Element<'static, Message> = if let Some(ref err) = app.ai_script_error {
|
|
|
|
|
text(format!("Error: {}", err))
|
|
|
|
|
.size(10)
|
2026-07-16 18:41:05 +03:00
|
|
|
.color(colors.danger)
|
2026-07-15 17:02:06 +03:00
|
|
|
.into()
|
|
|
|
|
} else {
|
2026-07-16 18:41:05 +03:00
|
|
|
app.ai_script_notice.as_ref().map_or_else(
|
|
|
|
|
|| column![].into(),
|
|
|
|
|
|notice| {
|
|
|
|
|
text(notice.clone())
|
|
|
|
|
.size(10)
|
|
|
|
|
.color(colors.accent_green)
|
|
|
|
|
.into()
|
|
|
|
|
},
|
|
|
|
|
)
|
2026-07-15 17:02:06 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ── Assemble panel ───────────────────────────────────
|
2026-07-11 18:13:03 +03:00
|
|
|
let panel = column![
|
2026-07-14 14:14:10 +03:00
|
|
|
text("AI Script Generator").size(13),
|
2026-07-11 18:13:03 +03:00
|
|
|
horizontal_rule(1),
|
|
|
|
|
provider_label,
|
|
|
|
|
provider_selector,
|
|
|
|
|
url_input,
|
|
|
|
|
model_input,
|
2026-07-16 18:41:05 +03:00
|
|
|
text("Model discovery is unavailable; enter the exact provider model ID.")
|
|
|
|
|
.size(9)
|
|
|
|
|
.color(colors.text_disabled),
|
2026-07-11 18:13:03 +03:00
|
|
|
horizontal_rule(1),
|
|
|
|
|
prompt_input,
|
2026-07-15 17:02:06 +03:00
|
|
|
row![generate_btn, clear_btn].spacing(4),
|
2026-07-11 18:13:03 +03:00
|
|
|
horizontal_rule(1),
|
|
|
|
|
script_area,
|
2026-07-15 17:02:06 +03:00
|
|
|
error_section,
|
|
|
|
|
horizontal_rule(1),
|
2026-07-16 18:41:05 +03:00
|
|
|
row![run_btn, copy_btn, copy_output_btn].spacing(4),
|
2026-07-11 18:13:03 +03:00
|
|
|
]
|
|
|
|
|
.spacing(4)
|
|
|
|
|
.padding(8);
|
|
|
|
|
|
|
|
|
|
container(panel)
|
|
|
|
|
.width(Length::Fill)
|
|
|
|
|
.height(Length::Fill)
|
|
|
|
|
.style(move |_theme| iced::widget::container::Style {
|
2026-07-16 18:41:05 +03:00
|
|
|
background: Some(iced::Background::Color(colors.bg_panel)),
|
|
|
|
|
border: iced::Border::default().color(colors.border_high).width(1),
|
2026-07-11 18:13:03 +03:00
|
|
|
..Default::default()
|
|
|
|
|
})
|
|
|
|
|
.into()
|
|
|
|
|
}
|