diff --git a/crates/openpencil-shell-core/src/widgets/ai_chat_model_picker.rs b/crates/openpencil-shell-core/src/widgets/ai_chat_model_picker.rs index 3d89b2b67..145149c5b 100644 --- a/crates/openpencil-shell-core/src/widgets/ai_chat_model_picker.rs +++ b/crates/openpencil-shell-core/src/widgets/ai_chat_model_picker.rs @@ -6,9 +6,8 @@ //! Search is intentionally omitted in this slice — the discovered //! catalogs are short; a search row lands with live CLI re-query. -use crate::agent_settings_state::AgentProvider; -use crate::chat_models::ModelEntry; use crate::theme::Theme; +use op_editor_core::chat::{AgentProvider, ModelEntry}; use crate::widgets::brand_icons::{paint_brand_logo, paint_opencode_logo, BrandLogo}; use crate::widgets::icons::{draw_icon, Icon}; use crate::widgets::property_panel_inputs::to_jian_color; diff --git a/crates/openpencil-shell-core/src/widgets/ai_chat_panel.rs b/crates/openpencil-shell-core/src/widgets/ai_chat_panel.rs index c4b29c09d..141b2de6f 100644 --- a/crates/openpencil-shell-core/src/widgets/ai_chat_panel.rs +++ b/crates/openpencil-shell-core/src/widgets/ai_chat_panel.rs @@ -12,11 +12,13 @@ //! Send / focus input / pick example. Full keyboard plumbing lives //! on the host (`apply_text` / `apply_send`). -use crate::document::{ChatRole, ChatState, Document}; use crate::theme::Theme; +use crate::widgets::editor_state_ext::{theme_for, translate}; use crate::widgets::icons::{draw_icon, Icon}; use crate::widgets::{LayoutBox, LayoutCx, PaintCx, Widget, WidgetId}; use crate::{Color, Point2D, Rect, TextLayout}; +use op_editor_core::chat::{ChatRole, ChatState}; +use op_editor_core::EditorState; pub const AI_CHAT_WIDTH: f32 = 380.0; pub const AI_CHAT_HEIGHT: f32 = 460.0; @@ -126,24 +128,25 @@ pub struct AIChatPlaceholder<'a> { } impl<'a> AIChatPlaceholder<'a> { - pub fn from_document(doc: &'a Document) -> Self { - Self::from_document_at(doc, 0) + pub fn from_editor(state: &'a EditorState) -> Self { + Self::from_editor_at(state, 0) } - /// Same as `from_document` but threads through the host's + /// Same as `from_editor` but threads through the host's /// current millisecond timestamp so the caret can blink. - pub fn from_document_at(doc: &'a Document, now_ms: u64) -> Self { + pub fn from_editor_at(state: &'a EditorState, now_ms: u64) -> Self { + let ui = &state.editor_ui; Self { id: WidgetId::new(7000), - theme: doc.theme(), - state: &doc.chat, + theme: theme_for(ui), + state: &state.chat, now_ms, - label_new_chat: doc.t("ai.newChat").to_string(), - label_start_with_ai: doc.t("ai.tryExample").to_string(), - label_input_placeholder: doc.t("ai.designWithAgent").to_string(), - label_tip_select_elements: doc.t("ai.tipSelectElements").to_string(), - label_no_models: doc.t("ai.noModelsConnected").to_string(), - model_picker_open: doc.ui.chat_model_picker_open, + label_new_chat: translate(ui, "ai.newChat").to_string(), + label_start_with_ai: translate(ui, "ai.tryExample").to_string(), + label_input_placeholder: translate(ui, "ai.designWithAgent").to_string(), + label_tip_select_elements: translate(ui, "ai.tipSelectElements").to_string(), + label_no_models: translate(ui, "ai.noModelsConnected").to_string(), + model_picker_open: ui.chat_model_picker_open, } } @@ -607,7 +610,7 @@ fn paint_messages( cx: &mut PaintCx<'_>, theme: &Theme, body_rect: Rect, - messages: &[crate::document::ChatMessage], + messages: &[op_editor_core::chat::ChatMessage], ) { cx.backend.save(); cx.backend.clip_rect(body_rect); @@ -664,8 +667,8 @@ mod tests { #[test] fn layout_reports_fixed_size() { - let doc = Document::sample(); - let p = AIChatPlaceholder::from_document(&doc); + let s = EditorState::new(); + let p = AIChatPlaceholder::from_editor(&s); let cx = LayoutCx { available_width: 9999.0, dpi: 1.0, @@ -682,8 +685,8 @@ mod tests { #[test] fn hit_test_resolves_input_focus() { - let doc = Document::sample(); - let panel = AIChatPlaceholder::from_document(&doc); + let s = EditorState::new(); + let panel = AIChatPlaceholder::from_editor(&s); let rect = Rect::xywh(0.0, 0.0, AI_CHAT_WIDTH, AI_CHAT_HEIGHT); // Click near the input center → FocusInput. let p = Point2D::new(60.0, AI_CHAT_HEIGHT - PAD - INPUT_HEIGHT / 2.0); @@ -692,8 +695,8 @@ mod tests { #[test] fn hit_test_resolves_send_at_right() { - let doc = Document::sample(); - let panel = AIChatPlaceholder::from_document(&doc); + let s = EditorState::new(); + let panel = AIChatPlaceholder::from_editor(&s); let rect = Rect::xywh(0.0, 0.0, AI_CHAT_WIDTH, AI_CHAT_HEIGHT); let send_x = AI_CHAT_WIDTH - PAD - 20.0; let p = Point2D::new(send_x, AI_CHAT_HEIGHT - PAD - INPUT_HEIGHT / 2.0); @@ -702,8 +705,8 @@ mod tests { #[test] fn hit_test_resolves_first_example_when_empty() { - let doc = Document::sample(); // chat empty by default - let panel = AIChatPlaceholder::from_document(&doc); + let s = EditorState::new(); // chat empty by default + let panel = AIChatPlaceholder::from_editor(&s); let rect = Rect::xywh(0.0, 0.0, AI_CHAT_WIDTH, AI_CHAT_HEIGHT); // First example card: top-left of grid. let card_w = (AI_CHAT_WIDTH - PAD * 2.0 - 8.0) / 2.0; @@ -718,8 +721,8 @@ mod tests { #[test] fn hit_test_header_returns_drag_handle() { - let doc = Document::sample(); - let panel = AIChatPlaceholder::from_document(&doc); + let s = EditorState::new(); + let panel = AIChatPlaceholder::from_editor(&s); let rect = Rect::xywh(0.0, 0.0, AI_CHAT_WIDTH, AI_CHAT_HEIGHT); // Click in the empty header band (between title and icons). let p = Point2D::new(AI_CHAT_WIDTH / 2.0, 16.0); diff --git a/crates/openpencil-shell-native/src/widget_host/click.rs b/crates/openpencil-shell-native/src/widget_host/click.rs index 5107bf0ef..5ff6665b5 100644 --- a/crates/openpencil-shell-native/src/widget_host/click.rs +++ b/crates/openpencil-shell-native/src/widget_host/click.rs @@ -131,7 +131,7 @@ impl WidgetHostNative { self.refresh_paint_doc(); // AI chat panel sits above canvas — check first. if let Some(chat_rect) = self.ai_chat_rect(viewport_width, viewport_height) { - let panel = AIChatPlaceholder::from_document(&self.paint_doc); + let panel = AIChatPlaceholder::from_editor(&self.editor_state); if let Some(hit) = panel.hit_test(chat_rect, Point2D::new(x, y)) { match hit { AIChatHit::FocusInput => { diff --git a/crates/openpencil-shell-native/src/widget_host/paint.rs b/crates/openpencil-shell-native/src/widget_host/paint.rs index ae5489fcd..97cb96288 100644 --- a/crates/openpencil-shell-native/src/widget_host/paint.rs +++ b/crates/openpencil-shell-native/src/widget_host/paint.rs @@ -198,7 +198,7 @@ impl WidgetHostNative { // of the toolbar in any overlap region (matches the // user's requested z-order: chat above toolbar). if let Some(chat_rect) = self.ai_chat_rect(viewport_width, viewport_height) { - let chat = AIChatPlaceholder::from_document_at(doc, self.now_ms); + let chat = AIChatPlaceholder::from_editor_at(&self.editor_state, self.now_ms); let mut cx = PaintCx { backend: &mut *frame, }; diff --git a/crates/openpencil-shell-native/src/widget_host/press.rs b/crates/openpencil-shell-native/src/widget_host/press.rs index f1e62b505..597e3d1cd 100644 --- a/crates/openpencil-shell-native/src/widget_host/press.rs +++ b/crates/openpencil-shell-native/src/widget_host/press.rs @@ -450,7 +450,7 @@ impl WidgetHostNative { // order. DragHandle starts a chat drag; other AI hits // defer to apply_click. if let Some(chat_rect) = self.ai_chat_rect(viewport_width, viewport_height) { - let panel = AIChatPlaceholder::from_document(&self.paint_doc); + let panel = AIChatPlaceholder::from_editor(&self.editor_state); if let Some(hit) = panel.hit_test(chat_rect, Point2D::new(x, y)) { if matches!(hit, AIChatHit::DragHandle) { self.chat_drag = Some(ChatDragState { diff --git a/crates/openpencil-shell-web/src/widget_host/paint.rs b/crates/openpencil-shell-web/src/widget_host/paint.rs index 6c0d2ddef..fda22b4fa 100644 --- a/crates/openpencil-shell-web/src/widget_host/paint.rs +++ b/crates/openpencil-shell-web/src/widget_host/paint.rs @@ -158,7 +158,7 @@ impl WidgetHost { } if let Some(chat_rect) = self.ai_chat_rect(viewport_width, viewport_height) { - let chat = AIChatPlaceholder::from_document(doc); + let chat = AIChatPlaceholder::from_editor(&self.editor_state); let mut cx = PaintCx { backend: &mut *backend, }; diff --git a/crates/openpencil-shell-web/src/widget_host/press.rs b/crates/openpencil-shell-web/src/widget_host/press.rs index 7f3e71867..415d0196b 100644 --- a/crates/openpencil-shell-web/src/widget_host/press.rs +++ b/crates/openpencil-shell-web/src/widget_host/press.rs @@ -292,7 +292,7 @@ impl WidgetHost { // click inside its rect is consumed here, even when // that point lies inside the toolbar rect underneath. if let Some(chat_rect) = self.ai_chat_rect(viewport_width, viewport_height) { - let panel = AIChatPlaceholder::from_document(&self.paint_doc); + let panel = AIChatPlaceholder::from_editor(&self.editor_state); if let Some(hit) = panel.hit_test(chat_rect, Point2D::new(x, y)) { if matches!(hit, AIChatHit::DragHandle) { self.chat_drag = Some(ChatDragState { @@ -481,7 +481,7 @@ impl WidgetHost { // clicks don't fall through to the canvas. self.refresh_paint_doc(); if let Some(chat_rect) = self.ai_chat_rect(viewport_w, viewport_h) { - let panel = AIChatPlaceholder::from_document(&self.paint_doc); + let panel = AIChatPlaceholder::from_editor(&self.editor_state); if let Some(hit) = panel.hit_test(chat_rect, Point2D::new(x, y)) { match hit { AIChatHit::FocusInput => {