summary refs log tree commit diff
diff options
context:
space:
mode:
authorVika <vika@fireburn.ru>2024-09-04 22:15:32 +0300
committerVika <vika@fireburn.ru>2024-09-04 22:15:32 +0300
commit406509dc549cb58d788a3dbd5572f28008c84546 (patch)
treeaff6115c0bfbd12ca447503d6d850feccfd9ad89
parentf16cac2d35487b1772d1c2524ed223c779f45f23 (diff)
downloadbowl-406509dc549cb58d788a3dbd5572f28008c84546.tar.zst
Make LLM enhancements optional
-rw-r--r--Cargo.toml4
-rw-r--r--default.nix10
-rw-r--r--flake.nix4
-rw-r--r--meson.options7
-rw-r--r--src/components/post_editor.rs45
-rw-r--r--src/components/preferences.rs137
-rw-r--r--src/components/smart_summary.rs1
-rw-r--r--src/lib.rs15
-rw-r--r--src/meson.build6
9 files changed, 165 insertions, 64 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 13b6aa2..b1e2933 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -5,6 +5,10 @@ authors = ["Vika <vika@fireburn.ru>"]
 license = "AGPL-3.0-only"
 edition = "2021"
 
+[features]
+smart-summary = []
+default = ["smart-summary"]
+
 [dependencies]
 adw = { version = "0.7.0", package = "libadwaita", features = ["v1_5"] }
 futures = "0.3.30"
diff --git a/default.nix b/default.nix
index d406776..881b656 100644
--- a/default.nix
+++ b/default.nix
@@ -3,6 +3,8 @@
 , desktop-file-utils
 , gtk4, libadwaita, libpanel, libsoup_3, libsecret
 , librsvg, glib-networking
+
+, withLLMEnhancements ? true
 }:
 
 let
@@ -29,8 +31,6 @@ let
     };
     strictDeps = true;
 
-    # cargoExtraArgs can be used to inject features
-
     buildInputs = [
       gtk4 libadwaita libsoup_3 libsecret
       librsvg glib-networking
@@ -43,7 +43,9 @@ let
       platforms = ["aarch64-linux" "x86_64-linux"];
       mainProgram = "bowl";
     };
-  };
+  } // (lib.optionalAttrs (!withLLMEnhancements) {
+    cargoExtraArgs = lib.optionalString (!withLLMEnhancements) "--no-default-features";
+  });
 
   cargoArtifacts = craneLib.buildDepsOnly args;
   args' = args // { inherit cargoArtifacts; };
@@ -69,6 +71,8 @@ in craneLib.mkCargoDerivation (args' // {
   checkPhase = "mesonCheckPhase";
   installPhase = "mesonInstallPhase";
 
+  mesonFlags = lib.optional (!withLLMEnhancements) "-Dllm=false";
+
   nativeBuildInputs = args'.nativeBuildInputs ++ [
     rustc # Only needed for Meson to successfully detect the Rust toolchain
 
diff --git a/flake.nix b/flake.nix
index c190706..4b78a24 100644
--- a/flake.nix
+++ b/flake.nix
@@ -31,6 +31,9 @@
   in {
     packages = {
       bowl = bowl;
+      bowl-no-llm = bowl.override {
+        withLLMEnhancements = false;
+      };
       default = self.packages.${system}.bowl;
       # Needed for translations
       xtr = pkgs.callPackage ./pkgs/xtr {};
@@ -38,6 +41,7 @@
 
     checks = {
       bowl = self.packages.${system}.bowl;
+      bowl-no-llm = self.packages.${system}.bowl-no-llm;
 
       deps = self.packages.${system}.bowl.cargoArtifacts;
       clippy = self.packages.${system}.bowl.clippy;
diff --git a/meson.options b/meson.options
index 7decbf0..2b9608d 100644
--- a/meson.options
+++ b/meson.options
@@ -7,4 +7,11 @@ option(
   ],
   value: 'default',
   description: 'The build profile for Bowl. One of "default" or "development".'
+)
+
+option(
+  'llm',
+  type: 'boolean',
+  value: true,
+  description: 'Whether to enable optional LLM enhancement features.'
 )
\ No newline at end of file
diff --git a/src/components/post_editor.rs b/src/components/post_editor.rs
index f2268ad..c42b06a 100644
--- a/src/components/post_editor.rs
+++ b/src/components/post_editor.rs
@@ -1,11 +1,12 @@
 use gettextrs::*;
-use crate::components;
 use crate::components::tag_pill::*;
 use adw::prelude::*;
 
 use glib::translate::IntoGlib;
 use gtk::GridLayoutChild;
-use relm4::{gtk, prelude::{ComponentController, Controller, DynamicIndex}, factory::FactoryVecDeque, Component, ComponentParts, ComponentSender, RelmWidgetExt};
+use relm4::{factory::FactoryVecDeque, gtk, prelude::{Controller, DynamicIndex}, Component, ComponentParts, ComponentSender, RelmWidgetExt};
+#[cfg(feature = "smart-summary")]
+use relm4::prelude::ComponentController;
 
 #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, glib::Enum)]
 #[enum_type(name = "MicropubVisibility")]
@@ -87,7 +88,8 @@ pub(crate) struct PostEditor<E> {
 
     #[do_not_track] wide_layout: gtk::GridLayout,
 
-    #[do_not_track] smart_summary: Controller<components::SmartSummaryButton>,
+    #[cfg(feature = "smart-summary")]
+    #[do_not_track] smart_summary: Controller<crate::components::SmartSummaryButton>,
     _err: std::marker::PhantomData<E>
 }
 
@@ -104,7 +106,8 @@ impl<E> PostEditor<E> {
 #[allow(private_interfaces)] // intentional
 #[allow(clippy::manual_non_exhaustive)] // false positive
 pub enum Input<E: std::error::Error + std::fmt::Debug + Send + 'static> {
-    #[doc(hidden)] SmartSummary(components::smart_summary::Output),
+    #[cfg(feature = "smart-summary")]
+    #[doc(hidden)] SmartSummary(crate::components::smart_summary::Output),
     #[doc(hidden)] VisibilitySelected(Visibility),
     #[doc(hidden)] AddTagFromBuffer,
     #[doc(hidden)] RemoveTag(DynamicIndex),
@@ -115,7 +118,10 @@ pub enum Input<E: std::error::Error + std::fmt::Debug + Send + 'static> {
 
 #[relm4::component(pub)]
 impl<E: std::error::Error + std::fmt::Debug + Send + 'static> Component for PostEditor<E> {
-    type Init = (<components::SmartSummaryButton as relm4::Component>::Init, Option<Post>);
+    #[cfg(feature = "smart-summary")]
+    type Init = (soup::Session, Option<Post>);
+    #[cfg(not(feature = "smart-summary"))]
+    type Init = Option<Post>;
     type Output = Option<Post>;
     type Input = Input<E>;
     type CommandOutput = ();
@@ -169,8 +175,6 @@ impl<E: std::error::Error + std::fmt::Debug + Send + 'static> Component for Post
                                 #[track = "model.busy_changed()"]
                                 set_sensitive: !model.busy(),
                             },
-
-                            model.smart_summary.widget(),
                         },
 
                         #[name = "tag_label"]
@@ -331,10 +335,13 @@ impl<E: std::error::Error + std::fmt::Debug + Send + 'static> Component for Post
     }
 
     fn init(
-        (http, init): Self::Init,
+        init: Self::Init,
         root: Self::Root,
         sender: ComponentSender<Self>
     ) -> ComponentParts<Self> {
+        #[cfg(feature = "smart-summary")]
+        let (http, init) = init;
+
         let mut model = Self {
             smart_summary_busy_guard: None,
             sending: false,
@@ -359,7 +366,8 @@ impl<E: std::error::Error + std::fmt::Debug + Send + 'static> Component for Post
 
             wide_layout: gtk::GridLayout::new(),
 
-            smart_summary: components::SmartSummaryButton::builder()
+            #[cfg(feature = "smart-summary")]
+            smart_summary: crate::components::SmartSummaryButton::builder()
                 .launch(http)
                 .forward(sender.input_sender(), Input::SmartSummary),
 
@@ -371,6 +379,9 @@ impl<E: std::error::Error + std::fmt::Debug + Send + 'static> Component for Post
 
         let widgets = view_output!();
 
+        #[cfg(feature = "smart-summary")]
+        widgets.summary_field.append(model.smart_summary.widget());
+
         widgets.visibility_selector.set_expression(Some(
             gtk::ClosureExpression::new::<String>(
                 [] as [gtk::Expression; 0],
@@ -459,11 +470,12 @@ impl<E: std::error::Error + std::fmt::Debug + Send + 'static> Component for Post
     fn update_with_view(&mut self, widgets: &mut Self::Widgets, msg: Self::Input, sender: ComponentSender<Self>, root: &Self::Root) {
         self.reset();
         match msg {
-            Input::SmartSummary(components::SmartSummaryOutput::Start) => {
+            #[cfg(feature = "smart-summary")]
+            Input::SmartSummary(crate::components::SmartSummaryOutput::Start) => {
                 widgets.content_textarea.set_sensitive(false);
                 if self.content_buffer.char_count() == 0 {
                     let _ = self.smart_summary.sender().send(
-                        components::SmartSummaryInput::Cancel
+                        crate::components::SmartSummaryInput::Cancel
                     );
                 } else {
                     let text = self.content_buffer.text(
@@ -476,20 +488,23 @@ impl<E: std::error::Error + std::fmt::Debug + Send + 'static> Component for Post
                         Some(relm4::main_adw_application().mark_busy())
                     );
                     if self.smart_summary.sender().send(
-                        components::SmartSummaryInput::Text(text.into())
+                        crate::components::SmartSummaryInput::Text(text.into())
                     ).is_ok() {
                         self.summary_buffer.set_text("");
                     }
                 }
                 widgets.content_textarea.set_sensitive(true);
             },
-            Input::SmartSummary(components::SmartSummaryOutput::Chunk(text)) => {
+            #[cfg(feature = "smart-summary")]
+            Input::SmartSummary(crate::components::SmartSummaryOutput::Chunk(text)) => {
                 self.summary_buffer.insert_text(self.summary_buffer.length(), text);
             },
-            Input::SmartSummary(components::SmartSummaryOutput::Done) => {
+            #[cfg(feature = "smart-summary")]
+            Input::SmartSummary(crate::components::SmartSummaryOutput::Done) => {
                 self.set_smart_summary_busy_guard(None);
             },
-            Input::SmartSummary(components::SmartSummaryOutput::Error(err)) => {
+            #[cfg(feature = "smart-summary")]
+            Input::SmartSummary(crate::components::SmartSummaryOutput::Error(err)) => {
                 self.set_smart_summary_busy_guard(None);
 
                 let toast = adw::Toast::new(&gettext!("Smart Summary error: {}", err));
diff --git a/src/components/preferences.rs b/src/components/preferences.rs
index 9bbc313..fbf406d 100644
--- a/src/components/preferences.rs
+++ b/src/components/preferences.rs
@@ -1,4 +1,3 @@
-use gettextrs::*;
 use gio::prelude::*;
 use adw::prelude::*;
 use relm4::prelude::*;
@@ -7,38 +6,100 @@ pub struct Preferences {
     settings: gio::Settings,
 }
 
-#[relm4::component(pub)]
+#[cfg(feature = "smart-summary")]
+#[allow(dead_code)]
+struct LanguageModelPreferencesWidgets {
+    page: adw::PreferencesPage,
+
+    general_group: adw::PreferencesGroup,
+    llm_endpoint: adw::EntryRow,
+
+    smart_summary_group: adw::PreferencesGroup,
+    smart_summary_model: adw::EntryRow,
+    smart_summary_system_prompt: adw::EntryRow,
+    smart_summary_prompt_prefix: adw::EntryRow,
+    smart_summary_prompt_suffix: adw::EntryRow,
+}
+
+#[cfg(feature = "smart-summary")]
+impl LanguageModelPreferencesWidgets {
+    fn new(settings: &gio::Settings) -> Self {
+        use gettextrs::*;
+
+        let page = adw::PreferencesPage::builder()
+            .title(gettext("Language Models"))
+            .description(gettext("Settings for the language model integrations."))
+            .icon_name("magic-wand")
+            .build();
+
+        let general_group = adw::PreferencesGroup::builder()
+            .title(gettext("General"))
+            .build();
+        let llm_endpoint = adw::EntryRow::new();
+        general_group.add(&llm_endpoint);
+        page.add(&general_group);
+
+        let smart_summary_group = adw::PreferencesGroup::builder()
+            .title(gettext("Smart Summary"))
+            .build();
+        let smart_summary_model = adw::EntryRow::new();
+        let smart_summary_system_prompt = adw::EntryRow::new();
+        let smart_summary_prompt_prefix = adw::EntryRow::new();
+        let smart_summary_prompt_suffix = adw::EntryRow::new();
+        smart_summary_group.add(&smart_summary_model);
+        smart_summary_group.add(&smart_summary_system_prompt);
+        smart_summary_group.add(&smart_summary_prompt_prefix);
+        smart_summary_group.add(&smart_summary_prompt_suffix);
+        page.add(&smart_summary_group);
+
+        let widgets = Self {
+            page,
+
+            general_group,
+            llm_endpoint,
+
+            smart_summary_group,
+            smart_summary_model,
+            smart_summary_system_prompt,
+            smart_summary_prompt_prefix,
+            smart_summary_prompt_suffix
+        };
+
+        let schema = settings.settings_schema().unwrap();
+
+        for (row, key) in [
+            (&widgets.llm_endpoint, "llm-endpoint"),
+            (&widgets.smart_summary_model, "smart-summary-model"),
+            (&widgets.smart_summary_system_prompt, "smart-summary-system-prompt"),
+            (&widgets.smart_summary_prompt_prefix, "smart-summary-prompt-prefix"),
+            (&widgets.smart_summary_prompt_suffix, "smart-summary-prompt-suffix"),
+        ] {
+            settings.bind(key, row, "text")
+                .get()
+                .set()
+                .build();
+            row.set_title(&gettext(schema.key(key).summary().unwrap()));
+        }
+
+        widgets
+    }
+}
+
+pub struct PreferencesWidgets {
+    #[cfg(feature = "smart-summary")]
+    llm: LanguageModelPreferencesWidgets
+}
+
 impl Component for Preferences {
     type CommandOutput = ();
     type Input = Option<gtk::Widget>;
     type Output = ();
     type Init = ();
+    type Root = adw::PreferencesDialog;
+    type Widgets = PreferencesWidgets;
 
-    view! {
-        #[root]
-        adw::PreferencesDialog {
-            add = &adw::PreferencesPage {
-                set_title: &gettext("Language Models"),
-                set_description: &gettext("Settings for the language model integrations."),
-                set_icon_name: Some("magic-wand"),
-
-                adw::PreferencesGroup {
-                    set_title: &gettext("General"),
-
-                    #[name = "llm_endpoint"]
-                    adw::EntryRow {},
-                },
-
-                adw::PreferencesGroup {
-                    set_title: &gettext("Smart Summary"),
-
-                    #[name = "smart_summary_model"] adw::EntryRow {},
-                    #[name = "smart_summary_system_prompt"] adw::EntryRow {},
-                    #[name = "smart_summary_prompt_prefix"] adw::EntryRow {},
-                    #[name = "smart_summary_prompt_suffix"] adw::EntryRow {},
-                }
-            }
-        }
+    fn init_root() -> Self::Root {
+        adw::PreferencesDialog::new()
     }
 
     fn init(
@@ -51,23 +112,13 @@ impl Component for Preferences {
         };
 
         model.settings.delay();
-        let schema = model.settings.settings_schema().unwrap();
 
-        let widgets = view_output!();
-
-        for (row, key) in [
-            (&widgets.llm_endpoint, "llm-endpoint"),
-            (&widgets.smart_summary_model, "smart-summary-model"),
-            (&widgets.smart_summary_system_prompt, "smart-summary-system-prompt"),
-            (&widgets.smart_summary_prompt_prefix, "smart-summary-prompt-prefix"),
-            (&widgets.smart_summary_prompt_suffix, "smart-summary-prompt-suffix"),
-        ] {
-            model.settings.bind(key, row, "text")
-                .get()
-                .set()
-                .build();
-            row.set_title(&gettext(schema.key(key).summary().unwrap()));
-        }
+        let widgets = PreferencesWidgets {
+            #[cfg(feature = "smart-summary")]
+            llm: LanguageModelPreferencesWidgets::new(&model.settings),
+        };
+        #[cfg(feature = "smart-summary")]
+        root.add(&widgets.llm.page);
 
         root.connect_closed(glib::clone!(
             #[strong(rename_to = settings)]
diff --git a/src/components/smart_summary.rs b/src/components/smart_summary.rs
index 7b2df7d..2795b09 100644
--- a/src/components/smart_summary.rs
+++ b/src/components/smart_summary.rs
@@ -1,3 +1,4 @@
+#![cfg(feature = "smart-summary")]
 use futures::AsyncBufReadExt;
 use gio::prelude::SettingsExtManual;
 use soup::prelude::*;
diff --git a/src/lib.rs b/src/lib.rs
index 6421560..37b54a5 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -4,7 +4,9 @@ use libsecret::prelude::{RetrievableExtManual, RetrievableExt};
 use relm4::{actions::{RelmAction, RelmActionGroup}, gtk, loading_widgets::LoadingWidgets, prelude::{AsyncComponent, AsyncComponentController, AsyncComponentParts, AsyncController, ComponentController, Controller}, AsyncComponentSender, Component, RelmWidgetExt};
 
 pub mod components {
+    #[cfg(feature = "smart-summary")]
     pub(crate) mod smart_summary;
+    #[cfg(feature = "smart-summary")]
     pub(crate) use smart_summary::{
         SmartSummaryButton, Output as SmartSummaryOutput, Input as SmartSummaryInput
     };
@@ -257,9 +259,16 @@ impl AsyncComponent for App {
             micropub: state,
             secret_schema,
 
-            post_editor: components::PostEditor::builder()
-                .launch((http.clone(), None))
-                .forward(sender.input_sender(), Self::Input::PostEditor),
+            post_editor: {
+                #[cfg(feature = "smart-summary")]
+                let init = (http.clone(), None);
+                #[cfg(not(feature = "smart-summary"))]
+                let init = None;
+
+                components::PostEditor::builder()
+                    .launch(init)
+                    .forward(sender.input_sender(), Self::Input::PostEditor)
+            },
             signin: components::SignIn::builder()
                 .launch((glib::Uri::parse(
                     CLIENT_ID_STR, glib::UriFlags::NONE
diff --git a/src/meson.build b/src/meson.build
index 350b911..3bc0c3f 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -10,6 +10,12 @@ else
   message('Building in debug mode')
 endif
 
+cargo_options += [ '--no-default-features' ]
+
+if get_option('llm')
+  cargo_options += [ '--features=smart-summary' ]
+endif
+
 cargo_env = [
     'CARGO_HOME=' + meson.project_build_root() / 'cargo-home',
     'PKGDATADIR=' + pkgdatadir,