feat: more completion for M4

This commit is contained in:
2026-04-13 21:20:41 +02:00
parent eca5fafc1c
commit a96a6ee31c
15 changed files with 527 additions and 46 deletions

View File

@@ -222,6 +222,7 @@ pub fn view<'a>(
state: &'a MediaEditorState,
locale: UiLocale,
data_dir: Option<&Path>,
ai_enabled: bool,
) -> Element<'a, Message> {
let header = inputs::toolbar(
vec![
@@ -230,18 +231,18 @@ pub fn view<'a>(
vec![
if state.mime_type.starts_with("image/") {
button(text(t(locale, "editor.aiAnalyze")).size(13))
.on_press(Message::MediaEditor(MediaEditorMsg::AnalyzeWithAi))
.on_press_maybe(ai_enabled.then_some(Message::MediaEditor(MediaEditorMsg::AnalyzeWithAi)))
.padding([6, 16])
.into()
} else {
Space::new(0, 0).into()
},
button(text(t(locale, "editor.detectLanguage")).size(13))
.on_press(Message::MediaEditor(MediaEditorMsg::DetectLanguage))
.on_press_maybe(ai_enabled.then_some(Message::MediaEditor(MediaEditorMsg::DetectLanguage)))
.padding([6, 16])
.into(),
button(text(t(locale, "editor.translate")).size(13))
.on_press(Message::MediaEditor(MediaEditorMsg::TranslateMetadata))
.on_press_maybe(ai_enabled.then_some(Message::MediaEditor(MediaEditorMsg::TranslateMetadata)))
.padding([6, 16])
.into(),
button(text(t(locale, "common.save")).size(13))

View File

@@ -372,6 +372,7 @@ pub fn view<'a>(
state: &'a PostEditorState,
locale: UiLocale,
word_wrap: bool,
ai_enabled: bool,
preview_widget: Option<Element<'a, Message>>,
) -> Element<'a, Message> {
let on_translation = state.active_language != state.canonical_language;
@@ -389,7 +390,7 @@ pub fn view<'a>(
.size(13)
.shaping(Shaping::Advanced),
)
.on_press(Message::PostEditor(PostEditorMsg::ToggleQuickActions))
.on_press_maybe(ai_enabled.then_some(Message::PostEditor(PostEditorMsg::ToggleQuickActions)))
.padding([6, 16])
.style(status_bar::dropdown_trigger)
.into();
@@ -479,10 +480,10 @@ pub fn view<'a>(
Space::with_width(Length::Fill),
container(
column![
quick_action_item(locale, t(locale, "editor.aiAnalyze"), PostEditorMsg::AnalyzeWithAi),
quick_action_item(locale, t(locale, "editor.suggestTaxonomy"), PostEditorMsg::AnalyzeTaxonomy),
quick_action_item(locale, t(locale, "editor.translate"), PostEditorMsg::Translate),
quick_action_item(locale, t(locale, "editor.detectLanguage"), PostEditorMsg::DetectLanguage),
quick_action_item(locale, t(locale, "editor.aiAnalyze"), PostEditorMsg::AnalyzeWithAi, ai_enabled),
quick_action_item(locale, t(locale, "editor.suggestTaxonomy"), PostEditorMsg::AnalyzeTaxonomy, ai_enabled),
quick_action_item(locale, t(locale, "editor.translate"), PostEditorMsg::Translate, ai_enabled),
quick_action_item(locale, t(locale, "editor.detectLanguage"), PostEditorMsg::DetectLanguage, ai_enabled),
]
.spacing(4)
)
@@ -574,7 +575,7 @@ pub fn view<'a>(
|lang| Message::PostEditor(PostEditorMsg::LanguageChanged(lang)),
);
let detect_language = button(text(t(locale, "editor.detectLanguage")).size(12).shaping(Shaping::Advanced))
.on_press(Message::PostEditor(PostEditorMsg::DetectLanguage))
.on_press_maybe(ai_enabled.then_some(Message::PostEditor(PostEditorMsg::DetectLanguage)))
.padding([6, 12]);
let template_input = inputs::labeled_input(
&t(locale, "editor.templateSlug"),
@@ -979,10 +980,10 @@ fn mode_button<'a>(
.into()
}
fn quick_action_item<'a>(locale: UiLocale, label: String, msg: PostEditorMsg) -> Element<'a, Message> {
fn quick_action_item<'a>(locale: UiLocale, label: String, msg: PostEditorMsg, enabled: bool) -> Element<'a, Message> {
let _ = locale;
button(text(label).size(12).shaping(Shaping::Advanced))
.on_press(Message::PostEditor(msg))
.on_press_maybe(enabled.then_some(Message::PostEditor(msg)))
.padding([6, 12])
.style(status_bar::dropdown_item)
.width(Length::Fixed(220.0))

View File

@@ -124,6 +124,7 @@ pub fn view<'a>(
tabs,
active_tab,
locale,
offline_mode,
data_dir,
post_preview_widget,
post_editors,
@@ -347,6 +348,7 @@ fn route_content_area<'a>(
tabs: &'a [Tab],
active_tab: Option<&'a str>,
locale: UiLocale,
offline_mode: bool,
data_dir: Option<&'a Path>,
post_preview_widget: Option<Element<'a, Message>>,
post_editors: &'a HashMap<String, PostEditorState>,
@@ -376,14 +378,14 @@ fn route_content_area<'a>(
ContentRoute::Post(tab_id) => {
if let Some(state) = post_editors.get(tab_id) {
let wrap = settings_state.map(|s| s.wrap_long_lines).unwrap_or(true);
post_editor::view(state, locale, wrap, post_preview_widget)
post_editor::view(state, locale, wrap, is_ai_enabled(settings_state, offline_mode), post_preview_widget)
} else {
loading_view(locale)
}
}
ContentRoute::Media(tab_id) => {
if let Some(state) = media_editors.get(tab_id) {
media_editor::view(state, locale, data_dir)
media_editor::view(state, locale, data_dir, is_ai_enabled(settings_state, offline_mode))
} else {
loading_view(locale)
}
@@ -488,6 +490,20 @@ fn route_kind<'a>(
}
}
fn is_ai_enabled(settings_state: Option<&SettingsViewState>, offline_mode: bool) -> bool {
let Some(state) = settings_state else {
return false;
};
if offline_mode {
!state.airplane_endpoint_url.trim().is_empty() && !state.airplane_endpoint_model.trim().is_empty()
} else {
!state.online_endpoint_url.trim().is_empty()
&& !state.online_endpoint_model.trim().is_empty()
&& (state.online_api_key_configured || !state.online_api_key_input.trim().is_empty())
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -597,6 +613,23 @@ mod tests {
_ => panic!("expected site validation route"),
}
}
#[test]
fn ai_actions_require_configured_endpoint_for_current_mode() {
let mut settings = SettingsViewState::default();
assert!(!is_ai_enabled(Some(&settings), false));
assert!(!is_ai_enabled(Some(&settings), true));
settings.online_endpoint_url = "https://api.example.com/v1".to_string();
settings.online_endpoint_model = "gpt-4.1-mini".to_string();
settings.online_api_key_configured = true;
assert!(is_ai_enabled(Some(&settings), false));
assert!(!is_ai_enabled(Some(&settings), true));
settings.airplane_endpoint_url = "http://localhost:11434/v1".to_string();
settings.airplane_endpoint_model = "llama3.2".to_string();
assert!(is_ai_enabled(Some(&settings), true));
}
}
fn loading_view<'a>(locale: UiLocale) -> Element<'a, Message> {