feat(svg-editor): implement interactive SVG node editor with parsing and editing capabilities

feat(debug-logger): add DebugSignalLogger plugin for event logging and inspection

feat(plugins): introduce built-in plugins module and integrate debug logger

feat(plugin-integration): create integration layer for mapping Iced Messages to CoreEvents

feat(plugin-registry): establish PluginRegistry for managing plugins and event dispatching
This commit is contained in:
2026-07-20 01:43:22 +03:00
parent 6e0d179be9
commit 2fb47520b3
46 changed files with 4093 additions and 314 deletions
@@ -1,18 +1,152 @@
//! AI Chat panel — AI assistant with streaming SSE responses, tool execution, and DSL scripts.
//! AI Assistant panel with two sub-tabs: ComfyUI pipeline viewer and AI Chat.
//!
//! Full implementation of the AI chat interface including provider selection,
//! streaming response display, chat bubbles, reasoning toggle, and canvas context toggle.
//! Uses ThemeColors for consistent styling.
//! Mirrors the egui AI Assistant panel which has both ComfyUI and AI Chat tabs.
//! The active tab is controlled by `app.ai_assistant_tab` (0 = ComfyUI, 1 = AI Chat).
//! Uses ThemeColors for consistent styling across both tabs.
use crate::ai_chat::{AiChatState, ChatMessage};
use crate::ai_chat::{AiChatState, ChatMessage, ComfyStatus, ComfyUiState};
use crate::app::Message;
use crate::panels::styles;
use crate::theme::ThemeColors;
use iced::widget::{button, column, container, horizontal_rule, row, scrollable, text, text_input};
use iced::{Element, Length};
use crate::app::HcieIcedApp;
/// Build the AI chat panel with full streaming UI.
pub fn view(state: &AiChatState, colors: ThemeColors) -> Element<'static, Message> {
/// Build the AI Assistant panel with tab switcher and sub-tab content.
pub fn view(app: &HcieIcedApp, colors: ThemeColors) -> Element<'static, Message> {
let tab = app.ai_assistant_tab;
// ── Tab bar ──
let comfy_tab_btn = tab_button("ComfyUI", tab == 0, 0, colors);
let chat_tab_btn = tab_button("AI Chat", tab == 1, 1, colors);
let tab_bar = row![comfy_tab_btn, chat_tab_btn]
.spacing(0)
.align_y(iced::Alignment::Center);
// ── Content area ──
let content: Element<'static, Message> = match tab {
0 => comfy_tab_content(&app.comfy_state, colors),
_ => chat_tab_content(&app.ai_chat_state, colors),
};
let panel = column![
tab_bar,
horizontal_rule(1),
content,
]
.spacing(0)
.padding(8);
container(panel)
.width(Length::Fill)
.height(Length::Fill)
.style(move |_theme| styles::panel_background(colors))
.into()
}
/// A tab button that highlights when active.
fn tab_button(
label: &str,
active: bool,
tab_index: usize,
colors: ThemeColors,
) -> Element<'static, Message> {
let label_owned = label.to_string();
let bg = if active { colors.accent } else { colors.bg_active };
let fg = if active { iced::Color::WHITE } else { colors.text_secondary };
button(text(label_owned).size(11))
.on_press(Message::AiAssistantTabChanged(tab_index))
.padding([4, 12])
.style(move |_theme: &iced::Theme, status: iced::widget::button::Status| {
let (background, text_color) = match status {
iced::widget::button::Status::Active => (bg, fg),
iced::widget::button::Status::Hovered => {
(colors.accent_hover, iced::Color::WHITE)
}
iced::widget::button::Status::Pressed => {
(colors.accent, iced::Color::WHITE)
}
iced::widget::button::Status::Disabled => {
(colors.bg_app, colors.text_disabled)
}
};
iced::widget::button::Style {
background: Some(iced::Background::Color(background)),
text_color,
border: iced::Border::default().rounded(4),
..Default::default()
}
})
.into()
}
/// ComfyUI tab content — placeholder pipeline viewer.
fn comfy_tab_content(state: &ComfyUiState, colors: ThemeColors) -> Element<'static, Message> {
let status_label = match state.status {
ComfyStatus::Unknown => "Unknown",
ComfyStatus::Running => "Running",
ComfyStatus::Error => "Error",
};
let status_color = match state.status {
ComfyStatus::Unknown => colors.text_disabled,
ComfyStatus::Running => colors.accent_green,
ComfyStatus::Error => iced::Color::from_rgb(0.95, 0.3, 0.3),
};
let url_input = text_input("http://127.0.0.1:8188", &state.url)
.on_input(|s| Message::ComfyUiUrlChanged(s))
.size(10)
.width(Length::Fill);
let check_btn = button(text("Check Status").size(10))
.on_press(Message::ComfyUiCheckStatus)
.padding([4, 10]);
let status_row = row![
text("Status:").size(10).color(colors.text_secondary),
text(status_label).size(10).color(status_color),
]
.spacing(6)
.align_y(iced::Alignment::Center);
column![
text("ComfyUI Pipeline").size(13).color(colors.text_primary),
text("").size(4),
text("Connect to a running ComfyUI instance to run")
.size(10)
.color(colors.text_disabled),
text("stable diffusion pipelines and AI image generation.")
.size(10)
.color(colors.text_disabled),
text("").size(8),
row![
text("Server URL:").size(10).color(colors.text_secondary),
url_input,
check_btn,
]
.spacing(4)
.align_y(iced::Alignment::Center),
status_row,
text("").size(8),
container(
text("Pipeline playback and node graph editing will be added in a future update.")
.size(10)
.color(colors.text_disabled),
)
.padding(8)
.style(move |_theme| iced::widget::container::Style {
background: Some(iced::Background::Color(colors.bg_hover)),
border: iced::Border::default().rounded(6),
..Default::default()
}),
]
.spacing(4)
.into()
}
/// AI Chat tab content — streaming SSE chat interface.
fn chat_tab_content(state: &AiChatState, colors: ThemeColors) -> Element<'static, Message> {
// ── Provider selector row ──
let provider_label = text("Provider:").size(10).color(colors.text_secondary);
let ollama_btn = make_provider_btn(
@@ -90,11 +224,14 @@ pub fn view(state: &AiChatState, colors: ThemeColors) -> Element<'static, Messag
.spacing(4)
.align_y(iced::Alignment::Center);
let hint = text("Model discovery is unavailable; enter the exact provider model ID.")
.size(9)
.color(colors.text_disabled);
// ── Message list ──
let mut msg_list = column![].spacing(6);
if state.messages.is_empty() && !state.is_streaming {
// Welcome message
let welcome = container(
column![
text("AI Creative Assistant").size(13).color(colors.accent),
@@ -122,13 +259,11 @@ pub fn view(state: &AiChatState, colors: ThemeColors) -> Element<'static, Messag
msg_list = msg_list.push(welcome);
} else {
// Render chat history
for msg in &state.messages {
let bubble = render_message_bubble(msg, colors);
msg_list = msg_list.push(bubble);
}
// Render streaming reasoning if enabled
if state.config.reasoning_enabled && !state.current_reasoning.is_empty() {
let reasoning_bubble = container(
column![
@@ -156,7 +291,6 @@ pub fn view(state: &AiChatState, colors: ThemeColors) -> Element<'static, Messag
msg_list = msg_list.push(reasoning_bubble);
}
// Render streaming response if in progress
if state.is_streaming && !state.current_response.is_empty() {
let streaming_bubble = container(
text(state.current_response.clone())
@@ -178,7 +312,6 @@ pub fn view(state: &AiChatState, colors: ThemeColors) -> Element<'static, Messag
msg_list = msg_list.push(streaming_bubble);
}
// Streaming indicator
if state.is_streaming {
let indicator = row![
text("...").size(10).color(colors.text_disabled),
@@ -219,14 +352,10 @@ pub fn view(state: &AiChatState, colors: ThemeColors) -> Element<'static, Messag
.spacing(4)
.align_y(iced::Alignment::Center);
// ── Assemble panel ──
let panel = column![
text("AI Assistant").size(13).color(colors.text_primary),
column![
provider_row,
config_row,
text("Model discovery is unavailable; enter the exact provider model ID.")
.size(9)
.color(colors.text_disabled),
hint,
toggles_row,
horizontal_rule(1),
scrollable(msg_list).height(Length::Fill),
@@ -234,16 +363,9 @@ pub fn view(state: &AiChatState, colors: ThemeColors) -> Element<'static, Messag
input_row,
]
.spacing(4)
.padding(8);
container(panel)
.width(Length::Fill)
.height(Length::Fill)
.style(move |_theme| styles::panel_background(colors))
.into()
.into()
}
/// Build an action button whose disabled, hover, pressed, and focus-border states are explicit.
fn action_button(
label: &'static str,
message: Option<Message>,
@@ -279,7 +401,6 @@ fn action_button(
.into()
}
/// Render a single chat message as a styled bubble.
fn render_message_bubble(msg: &ChatMessage, colors: ThemeColors) -> Element<'static, Message> {
if msg.is_reasoning {
return container(
@@ -302,7 +423,6 @@ fn render_message_bubble(msg: &ChatMessage, colors: ThemeColors) -> Element<'sta
}
match msg.role.as_str() {
"user" => {
// User bubble — right-aligned, blue tint
let label = text("You").size(9).color(colors.accent);
let content = text(msg.content.clone())
.size(11)
@@ -321,7 +441,6 @@ fn render_message_bubble(msg: &ChatMessage, colors: ThemeColors) -> Element<'sta
row![iced::widget::Space::with_width(Length::Fill), bubble].into()
}
"tool" => {
// Tool result — monospace pill
let content = text(msg.content.clone()).size(10).color(colors.accent);
container(content)
.padding([4, 8])
@@ -333,7 +452,6 @@ fn render_message_bubble(msg: &ChatMessage, colors: ThemeColors) -> Element<'sta
.into()
}
_ => {
// Assistant bubble — left-aligned, slate tint
let label = text("AI").size(9).color(colors.accent_green);
let content = text(msg.content.clone())
.size(11)
@@ -354,7 +472,6 @@ fn render_message_bubble(msg: &ChatMessage, colors: ThemeColors) -> Element<'sta
}
}
/// Create a provider selection button with active state highlighting.
fn make_provider_btn(
label: &str,
is_active: bool,