feat(iced): implement AI script generator with working LLM calls
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
//! AI Script Generator — LLM-powered procedural drawing script generation.
|
||||
//!
|
||||
//! Provides async functions to call Ollama, Claude, and OpenAI APIs to generate
|
||||
//! HCIE-Script DSL commands from natural language descriptions. Also includes
|
||||
/// the system prompt, provider configuration, and output cleaning utilities.
|
||||
|
||||
/// System prompt sent to LLMs when generating HCIE-Script DSL commands.
|
||||
pub const SYSTEM_PROMPT: &str = r#"You are HCIE-Script, a code generator for a procedural drawing DSL.
|
||||
Output ONLY valid HCIE-Script DSL commands with no explanation, no markdown, no code fences.
|
||||
|
||||
Available commands:
|
||||
- layer Name — create raster layer
|
||||
- vector_layer Name — create vector layer
|
||||
- select_layer Name
|
||||
- brush STYLE — round, square, pencil, inkpen, charcoal, watercolor, marker, oil, airbrush, spray, clouds, dirt, tree, meadow, rock, bristle, leaf, wetpaint, sketch, hatch, calligraphy, blender, mixer, glow, crayon
|
||||
- color #RRGGBB — set foreground color
|
||||
- size N — brush size in pixels
|
||||
- opacity N — 0..1
|
||||
- hardness N — 0..1
|
||||
- flow N — 0..2
|
||||
- pressure N / pressure_start N / pressure_end N — 0..1
|
||||
- wiggle N — per-point jitter
|
||||
- draw x1,y1 x2,y2 ... — multi-point stroke (coords in pixels)
|
||||
- line x1,y1 x2,y2
|
||||
- rect x1,y1 x2,y2 — filled rectangle in CURRENT color
|
||||
- circle cx,cy — single brush dab
|
||||
- triangle x1,y1 x2,y2 x3,y3 — vector triangle
|
||||
- bezier x1,y1 cx1,cy1 cx2,cy2 x2,y2 [steps]
|
||||
- scatter N x,y w,h [size] — golden-angle scatter
|
||||
- gradient x1,y1 x2,y2 #hex1 #hex2 ... N
|
||||
- set key=value key=value — multi-set
|
||||
- var name value — declare variables, reference as $name
|
||||
- repeat N { ... } — loop with $i (index), $n (count)
|
||||
- blend MODE — normal, multiply, overlay, screen, etc.
|
||||
|
||||
Golden rules:
|
||||
1. Create a layer BEFORE drawing on it.
|
||||
2. Use brush BEFORE draw/line/bezier.
|
||||
3. Drawing commands operate on the CURRENT layer and CURRENT brush state.
|
||||
4. Coordinates are pixel values (0..800 width, 0..600 height).
|
||||
5. Use gradient for smooth skies and backgrounds.
|
||||
6. Use scatter for grass, flowers, stars, foliage.
|
||||
7. Use bezier/curve for smooth natural lines.
|
||||
8. Build scenes back-to-front: sky → background → midground → foreground.
|
||||
"#;
|
||||
|
||||
/// LLM provider configuration.
|
||||
pub struct Provider {
|
||||
pub base_url: String,
|
||||
pub model: String,
|
||||
}
|
||||
|
||||
/// Generate a script via the Ollama /api/chat endpoint (async).
|
||||
///
|
||||
/// Sends the system prompt and user prompt as a chat completion request with
|
||||
/// `stream: false`. Returns the cleaned DSL script text on success.
|
||||
pub async fn generate_via_ollama(provider: &Provider, prompt: &str) -> Result<String, String> {
|
||||
let url = format!("{}/api/chat", provider.base_url.trim_end_matches('/'));
|
||||
let body = serde_json::json!({
|
||||
"model": provider.model,
|
||||
"messages": [
|
||||
{"role": "system", "content": SYSTEM_PROMPT},
|
||||
{"role": "user", "content": prompt}
|
||||
],
|
||||
"stream": false,
|
||||
"options": { "num_predict": 2048 }
|
||||
});
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(120))
|
||||
.build()
|
||||
.map_err(|e| format!("Client error: {}", e))?;
|
||||
|
||||
let resp = client
|
||||
.post(&url)
|
||||
.json(&body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Request failed: {}", e))?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
return Err(format!("API error: {}", resp.status()));
|
||||
}
|
||||
|
||||
let json: serde_json::Value = resp.json().await.map_err(|e| format!("Parse error: {}", e))?;
|
||||
let content = json["message"]["content"]
|
||||
.as_str()
|
||||
.ok_or_else(|| "No content in response".to_string())?;
|
||||
|
||||
Ok(clean_script_output(content))
|
||||
}
|
||||
|
||||
/// Generate a script via the Claude /v1/messages endpoint (async).
|
||||
///
|
||||
/// Uses the Anthropic Messages API format with the system prompt as a
|
||||
/// top-level `system` field. Returns the cleaned DSL script text.
|
||||
pub async fn generate_via_claude(provider: &Provider, prompt: &str) -> Result<String, String> {
|
||||
let url = format!("{}/v1/messages", provider.base_url.trim_end_matches('/'));
|
||||
let body = serde_json::json!({
|
||||
"model": provider.model,
|
||||
"max_tokens": 2048,
|
||||
"system": SYSTEM_PROMPT,
|
||||
"messages": [
|
||||
{"role": "user", "content": prompt}
|
||||
]
|
||||
});
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(120))
|
||||
.build()
|
||||
.map_err(|e| format!("Client error: {}", e))?;
|
||||
|
||||
let resp = client
|
||||
.post(&url)
|
||||
.json(&body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Request failed: {}", e))?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
let status = resp.status();
|
||||
let text = resp.text().await.unwrap_or_default();
|
||||
return Err(format!("API error {}: {}", status, text));
|
||||
}
|
||||
|
||||
let json: serde_json::Value = resp.json().await.map_err(|e| format!("Parse error: {}", e))?;
|
||||
let content = json["content"][0]["text"]
|
||||
.as_str()
|
||||
.ok_or_else(|| "No content in response".to_string())?;
|
||||
|
||||
Ok(clean_script_output(content))
|
||||
}
|
||||
|
||||
/// Generate a script via the OpenAI /v1/chat/completions endpoint (async).
|
||||
///
|
||||
/// Uses the standard Chat Completions format with the system prompt as the
|
||||
/// first message. Returns the cleaned DSL script text.
|
||||
pub async fn generate_via_openai(provider: &Provider, prompt: &str) -> Result<String, String> {
|
||||
let url = format!("{}/v1/chat/completions", provider.base_url.trim_end_matches('/'));
|
||||
let body = serde_json::json!({
|
||||
"model": provider.model,
|
||||
"messages": [
|
||||
{"role": "system", "content": SYSTEM_PROMPT},
|
||||
{"role": "user", "content": prompt}
|
||||
],
|
||||
"max_tokens": 2048
|
||||
});
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(120))
|
||||
.build()
|
||||
.map_err(|e| format!("Client error: {}", e))?;
|
||||
|
||||
let resp = client
|
||||
.post(&url)
|
||||
.json(&body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Request failed: {}", e))?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
let status = resp.status();
|
||||
let text = resp.text().await.unwrap_or_default();
|
||||
return Err(format!("API error {}: {}", status, text));
|
||||
}
|
||||
|
||||
let json: serde_json::Value = resp.json().await.map_err(|e| format!("Parse error: {}", e))?;
|
||||
let content = json["choices"][0]["message"]["content"]
|
||||
.as_str()
|
||||
.ok_or_else(|| "No content in response".to_string())?;
|
||||
|
||||
Ok(clean_script_output(content))
|
||||
}
|
||||
|
||||
/// Strip markdown code fences and leading/trailing whitespace from LLM output.
|
||||
///
|
||||
/// LLMs often wrap DSL output in ```dsl, ```script, ```hcie, or plain ``` fences.
|
||||
/// This function removes those wrappers to extract the raw script text.
|
||||
pub fn clean_script_output(text: &str) -> String {
|
||||
let text = text.trim();
|
||||
let text = text.strip_prefix("```dsl").unwrap_or(text);
|
||||
let text = text.strip_prefix("```script").unwrap_or(text);
|
||||
let text = text.strip_prefix("```hcie").unwrap_or(text);
|
||||
let text = text.strip_prefix("```").unwrap_or(text);
|
||||
let text = text.strip_suffix("```").unwrap_or(text);
|
||||
text.trim().to_string()
|
||||
}
|
||||
|
||||
/// Generate a script using the given provider type and configuration (async).
|
||||
///
|
||||
/// Dispatches to the appropriate provider function based on the `provider_type`
|
||||
/// string ("ollama", "claude", or "openai"). Defaults to Ollama.
|
||||
pub async fn generate_script(provider_type: &str, provider: &Provider, prompt: &str) -> Result<String, String> {
|
||||
match provider_type {
|
||||
"claude" => generate_via_claude(provider, prompt).await,
|
||||
"openai" => generate_via_openai(provider, prompt).await,
|
||||
_ => generate_via_ollama(provider, prompt).await,
|
||||
}
|
||||
}
|
||||
@@ -163,6 +163,20 @@ pub struct HcieIcedApp {
|
||||
pub script_error_line: Option<usize>,
|
||||
/// Whether a script is currently executing.
|
||||
pub script_is_running: bool,
|
||||
/// AI script generator — natural language prompt input.
|
||||
pub ai_script_prompt: String,
|
||||
/// AI script generator — generated DSL script output.
|
||||
pub ai_script_output: String,
|
||||
/// AI script generator — last error message (if any).
|
||||
pub ai_script_error: Option<String>,
|
||||
/// AI script generator — whether a generation request is in progress.
|
||||
pub ai_script_is_running: bool,
|
||||
/// AI script generator — selected provider type ("ollama", "claude", "openai").
|
||||
pub ai_script_provider: String,
|
||||
/// AI script generator — model name for the selected provider.
|
||||
pub ai_script_model: String,
|
||||
/// AI script generator — base URL for the selected provider.
|
||||
pub ai_script_url: String,
|
||||
}
|
||||
|
||||
/// A recently opened file entry.
|
||||
@@ -438,6 +452,17 @@ pub enum Message {
|
||||
AiChatStreamChunk(String),
|
||||
AiChatStreamFinished(Result<String, String>),
|
||||
|
||||
// ── AI Script Generator ─────────────────────────────
|
||||
AiScriptPromptChanged(String),
|
||||
AiScriptProviderChanged(String),
|
||||
AiScriptModelChanged(String),
|
||||
AiScriptUrlChanged(String),
|
||||
AiScriptGenerate,
|
||||
AiScriptRun,
|
||||
AiScriptCopy,
|
||||
AiScriptClear,
|
||||
AiScriptResult(Result<String, String>),
|
||||
|
||||
// ── Clipboard ───────────────────────────────────────
|
||||
CopyImage,
|
||||
PasteImage,
|
||||
@@ -642,6 +667,13 @@ impl HcieIcedApp {
|
||||
script_error: None,
|
||||
script_error_line: None,
|
||||
script_is_running: false,
|
||||
ai_script_prompt: String::new(),
|
||||
ai_script_output: String::new(),
|
||||
ai_script_error: None,
|
||||
ai_script_is_running: false,
|
||||
ai_script_provider: "ollama".to_string(),
|
||||
ai_script_model: "qwen2.5:7b".to_string(),
|
||||
ai_script_url: "http://localhost:11434".to_string(),
|
||||
};
|
||||
|
||||
// Apply saved settings to tool state
|
||||
@@ -2452,6 +2484,104 @@ impl HcieIcedApp {
|
||||
}
|
||||
}
|
||||
|
||||
// ── AI Script Generator ───────────────────────
|
||||
Message::AiScriptPromptChanged(text) => {
|
||||
self.ai_script_prompt = text;
|
||||
}
|
||||
Message::AiScriptProviderChanged(provider) => {
|
||||
self.ai_script_provider = provider.clone();
|
||||
// Set sensible defaults when switching providers
|
||||
match provider.as_str() {
|
||||
"ollama" => {
|
||||
self.ai_script_url = "http://localhost:11434".to_string();
|
||||
self.ai_script_model = "qwen2.5:7b".to_string();
|
||||
}
|
||||
"claude" => {
|
||||
self.ai_script_url = "https://api.anthropic.com".to_string();
|
||||
self.ai_script_model = "claude-sonnet-4-20250514".to_string();
|
||||
}
|
||||
"openai" => {
|
||||
self.ai_script_url = "https://api.openai.com/v1".to_string();
|
||||
self.ai_script_model = "gpt-4o".to_string();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Message::AiScriptModelChanged(model) => {
|
||||
self.ai_script_model = model;
|
||||
}
|
||||
Message::AiScriptUrlChanged(url) => {
|
||||
self.ai_script_url = url;
|
||||
}
|
||||
Message::AiScriptGenerate => {
|
||||
if self.ai_script_prompt.trim().is_empty() || self.ai_script_is_running {
|
||||
return Task::none();
|
||||
}
|
||||
self.ai_script_is_running = true;
|
||||
self.ai_script_error = None;
|
||||
self.ai_script_output.clear();
|
||||
|
||||
let prompt = self.ai_script_prompt.clone();
|
||||
let provider_type = self.ai_script_provider.clone();
|
||||
let provider = crate::ai_script::Provider {
|
||||
base_url: self.ai_script_url.clone(),
|
||||
model: self.ai_script_model.clone(),
|
||||
};
|
||||
|
||||
return Task::perform(
|
||||
async move {
|
||||
crate::ai_script::generate_script(&provider_type, &provider, &prompt).await
|
||||
},
|
||||
Message::AiScriptResult,
|
||||
);
|
||||
}
|
||||
Message::AiScriptRun => {
|
||||
if self.ai_script_output.is_empty() {
|
||||
return Task::none();
|
||||
}
|
||||
match crate::script::parser::parse_dsl_script(&self.ai_script_output) {
|
||||
Ok(actions) => {
|
||||
self.ai_script_error = None;
|
||||
crate::script::executor::execute_actions(
|
||||
&mut self.documents[self.active_doc].engine,
|
||||
&actions,
|
||||
);
|
||||
self.documents[self.active_doc].modified = true;
|
||||
self.refresh_composite_if_needed();
|
||||
return Task::perform(async {}, |_| Message::CompositeRefresh);
|
||||
}
|
||||
Err(e) => {
|
||||
self.ai_script_error = Some(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::AiScriptCopy => {
|
||||
if !self.ai_script_output.is_empty() {
|
||||
// Copy script to the script editor panel
|
||||
self.script_text = self.ai_script_output.clone();
|
||||
self.script_content = iced::widget::text_editor::Content::with_text(
|
||||
&self.ai_script_output,
|
||||
);
|
||||
}
|
||||
}
|
||||
Message::AiScriptClear => {
|
||||
self.ai_script_prompt.clear();
|
||||
self.ai_script_output.clear();
|
||||
self.ai_script_error = None;
|
||||
self.ai_script_is_running = false;
|
||||
}
|
||||
Message::AiScriptResult(result) => {
|
||||
self.ai_script_is_running = false;
|
||||
match result {
|
||||
Ok(script) => {
|
||||
self.ai_script_output = script;
|
||||
}
|
||||
Err(e) => {
|
||||
self.ai_script_error = Some(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Clipboard ─────────────────────────────────
|
||||
Message::CopyImage => {
|
||||
let doc = &self.documents[self.active_doc];
|
||||
@@ -3064,6 +3194,7 @@ impl HcieIcedApp {
|
||||
(7, 7) => self.dock.reopen_pane(PaneType::Geometry),
|
||||
(7, 8) => self.dock.reopen_pane(PaneType::Script),
|
||||
(7, 9) => self.dock.reopen_pane(PaneType::LayerDetails),
|
||||
(7, 10) => self.dock.reopen_pane(PaneType::AiScript),
|
||||
|
||||
// ══════════════════════════════════════════
|
||||
// More menu (8) — Photopea-style
|
||||
|
||||
@@ -44,6 +44,8 @@ pub enum PaneType {
|
||||
Geometry,
|
||||
/// Tool Settings panel.
|
||||
ToolSettings,
|
||||
/// AI Script Generator panel.
|
||||
AiScript,
|
||||
}
|
||||
|
||||
impl PaneType {
|
||||
@@ -62,6 +64,7 @@ impl PaneType {
|
||||
PaneType::LayerDetails => "Layer Details",
|
||||
PaneType::Geometry => "Geometry",
|
||||
PaneType::ToolSettings => "Tool Settings",
|
||||
PaneType::AiScript => "AI Script Generator",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -225,7 +228,7 @@ impl DockState {
|
||||
|
||||
if let Some(target) = target_pane {
|
||||
let axis = match pane_type {
|
||||
PaneType::Layers | PaneType::Properties | PaneType::History | PaneType::LayerDetails => {
|
||||
PaneType::Layers | PaneType::Properties | PaneType::History | PaneType::LayerDetails | PaneType::AiScript => {
|
||||
iced::widget::pane_grid::Axis::Horizontal
|
||||
}
|
||||
_ => iced::widget::pane_grid::Axis::Vertical,
|
||||
|
||||
@@ -94,6 +94,9 @@ pub fn dock_view<'a>(
|
||||
PaneType::AiChat => {
|
||||
crate::panels::ai_chat_panel::view(&app.ai_chat_state, colors)
|
||||
}
|
||||
PaneType::AiScript => {
|
||||
crate::panels::ai_script_panel::view(app)
|
||||
}
|
||||
PaneType::LayerDetails => {
|
||||
let doc = &app.documents[app.active_doc];
|
||||
crate::panels::layer_details::view(&doc.cached_layers, doc.engine.active_layer_id(), colors)
|
||||
@@ -294,6 +297,7 @@ fn pane_type_icon(pane_type: PaneType) -> Option<&'static str> {
|
||||
PaneType::LayerDetails => Some("icons/panel-layers.svg"),
|
||||
PaneType::Geometry => Some("icons/vector_rect.svg"),
|
||||
PaneType::ToolSettings => Some("icons/panel-settings.svg"),
|
||||
PaneType::AiScript => Some("icons/panel-ai.svg"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
//! Launches the Iced application with the HCIE image editor.
|
||||
|
||||
mod ai_chat;
|
||||
mod ai_script;
|
||||
mod app;
|
||||
mod canvas;
|
||||
mod color_picker;
|
||||
|
||||
@@ -1,61 +1,76 @@
|
||||
//! AI Script panel — AI-powered script generation.
|
||||
//! AI Script panel — AI-powered procedural drawing script generation.
|
||||
//!
|
||||
//! Uses an LLM to generate procedural drawing scripts from natural language.
|
||||
//! Supports Ollama, Claude, and OpenAI providers.
|
||||
//! 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.
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::app::{HcieIcedApp, Message};
|
||||
use iced::widget::{button, column, container, horizontal_rule, row, scrollable, text, text_input};
|
||||
use iced::{Element, Length};
|
||||
|
||||
/// Build the AI script panel.
|
||||
/// Build the AI script panel view from application state.
|
||||
///
|
||||
/// Provides a UI for:
|
||||
/// - Entering a natural language prompt
|
||||
/// - Selecting an AI provider (Ollama, Claude, OpenAI)
|
||||
/// - Generating a script from the prompt
|
||||
/// - Viewing and running the generated script
|
||||
#[allow(dead_code)]
|
||||
pub fn view() -> Element<'static, Message> {
|
||||
/// 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> {
|
||||
// ── Provider selector row ────────────────────────────
|
||||
let provider_label = text("Provider").size(10);
|
||||
let provider_selector = row![
|
||||
button(text("Ollama").size(10)).on_press(Message::NoOp).padding([4, 8]),
|
||||
button(text("Claude").size(10)).on_press(Message::NoOp).padding([4, 8]),
|
||||
button(text("OpenAI").size(10)).on_press(Message::NoOp).padding([4, 8]),
|
||||
]
|
||||
.spacing(4);
|
||||
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);
|
||||
|
||||
let url_input = text_input("Base URL", "http://localhost:11434")
|
||||
.width(Length::Fill);
|
||||
// ── URL and model inputs ─────────────────────────────
|
||||
let url_input = text_input("Base URL", &app.ai_script_url)
|
||||
.width(Length::Fill)
|
||||
.on_input(Message::AiScriptUrlChanged);
|
||||
|
||||
let model_input = text_input("Model", "llama3")
|
||||
.width(Length::Fill);
|
||||
let model_input = text_input("Model", &app.ai_script_model)
|
||||
.width(Length::Fill)
|
||||
.on_input(Message::AiScriptModelChanged);
|
||||
|
||||
let prompt_input = text_input("Describe what to draw...", "")
|
||||
.width(Length::Fill);
|
||||
// ── Prompt input ─────────────────────────────────────
|
||||
let prompt_input = text_input("Describe what to draw...", &app.ai_script_prompt)
|
||||
.width(Length::Fill)
|
||||
.on_input(Message::AiScriptPromptChanged);
|
||||
|
||||
let generate_btn = button(text("Generate Script").size(11))
|
||||
.on_press(Message::NoOp)
|
||||
.padding([6, 12]);
|
||||
// ── 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])
|
||||
};
|
||||
|
||||
let run_btn = button(text("Run Script").size(11))
|
||||
.on_press(Message::NoOp)
|
||||
.on_press(Message::AiScriptRun)
|
||||
.padding([6, 12]);
|
||||
|
||||
let copy_btn = button(text("Copy").size(11))
|
||||
.on_press(Message::NoOp)
|
||||
let copy_btn = button(text("Copy to Editor").size(11))
|
||||
.on_press(Message::AiScriptCopy)
|
||||
.padding([6, 12]);
|
||||
|
||||
let clear_btn = button(text("Clear").size(11))
|
||||
.on_press(Message::NoOp)
|
||||
.on_press(Message::AiScriptClear)
|
||||
.padding([6, 12]);
|
||||
|
||||
// Script output area
|
||||
let script_output = text("Generated script will appear here...")
|
||||
.size(10)
|
||||
;
|
||||
// ── 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)
|
||||
};
|
||||
|
||||
let script_area = container(
|
||||
scrollable(script_output).height(Length::Fill)
|
||||
scrollable(script_content).height(Length::Fill)
|
||||
)
|
||||
.padding(8)
|
||||
.style(move |_theme| iced::widget::container::Style {
|
||||
@@ -64,6 +79,17 @@ pub fn view() -> Element<'static, Message> {
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
// ── Error display ────────────────────────────────────
|
||||
let error_section: Element<'static, Message> = if let Some(ref err) = app.ai_script_error {
|
||||
text(format!("Error: {}", err))
|
||||
.size(10)
|
||||
.color(iced::Color::from_rgb(0.9, 0.3, 0.3))
|
||||
.into()
|
||||
} else {
|
||||
column![].into()
|
||||
};
|
||||
|
||||
// ── Assemble panel ───────────────────────────────────
|
||||
let panel = column![
|
||||
text("AI Script Generator").size(13),
|
||||
horizontal_rule(1),
|
||||
@@ -73,9 +99,12 @@ pub fn view() -> Element<'static, Message> {
|
||||
model_input,
|
||||
horizontal_rule(1),
|
||||
prompt_input,
|
||||
row![generate_btn, run_btn, copy_btn, clear_btn].spacing(4),
|
||||
row![generate_btn, clear_btn].spacing(4),
|
||||
horizontal_rule(1),
|
||||
script_area,
|
||||
error_section,
|
||||
horizontal_rule(1),
|
||||
row![run_btn, copy_btn].spacing(4),
|
||||
]
|
||||
.spacing(4)
|
||||
.padding(8);
|
||||
|
||||
@@ -283,6 +283,7 @@ fn all_menus(recent_files: &[crate::app::RecentFileEntry]) -> Vec<MenuDef> {
|
||||
MenuItem::new("Geometry"),
|
||||
MenuItem::new("Script"),
|
||||
MenuItem::new("Layer Details"),
|
||||
MenuItem::new("AI Script Generator"),
|
||||
],
|
||||
},
|
||||
// ── More menu (Photopea-style) ──
|
||||
|
||||
Reference in New Issue
Block a user