diff --git a/.github/workflows/release-app.yml b/.github/workflows/release-app.yml index cca637cfd..ca7bf498b 100644 --- a/.github/workflows/release-app.yml +++ b/.github/workflows/release-app.yml @@ -163,27 +163,21 @@ jobs: projectPath: "./examples/apps/screenpipe-app-tauri" tauriScript: bunx tauri -v - - name: Get version from Cargo.toml - id: get_version - run: | - VERSION=$(grep '^version =' examples/apps/screenpipe-app-tauri/src-tauri/Cargo.toml | sed 's/.*= "\(.*\)"/\1/') - echo "VERSION=$VERSION" >> $GITHUB_OUTPUT - - name: Create CrabNebula Cloud Release Draft uses: crabnebula-dev/cloud-release@v0.1.0 with: - command: release draft ${{ secrets.CN_APP_SLUG }} --framework tauri ${{ steps.get_version.outputs.VERSION }} + command: release draft ${{ secrets.CN_APP_SLUG }} --framework tauri api-key: ${{ secrets.CN_API_KEY }} - name: Upload Assets to CrabNebula Cloud uses: crabnebula-dev/cloud-release@v0.1.0 with: - command: release upload ${{ secrets.CN_APP_SLUG }} --framework tauri ${{ steps.get_version.outputs.VERSION }} + command: release upload ${{ secrets.CN_APP_SLUG }} --framework tauri api-key: ${{ secrets.CN_API_KEY }} path: ./examples/apps/screenpipe-app-tauri/src-tauri - name: Publish CrabNebula Cloud Release uses: crabnebula-dev/cloud-release@v0.1.0 with: - command: release publish ${{ secrets.CN_APP_SLUG }} --framework tauri ${{ steps.get_version.outputs.VERSION }} + command: release publish ${{ secrets.CN_APP_SLUG }} --framework tauri api-key: ${{ secrets.CN_API_KEY }} diff --git a/examples/apps/screenpipe-app-tauri/components/screenpipe-status.tsx b/examples/apps/screenpipe-app-tauri/components/screenpipe-status.tsx index 817175ed6..498d77d75 100644 --- a/examples/apps/screenpipe-app-tauri/components/screenpipe-status.tsx +++ b/examples/apps/screenpipe-app-tauri/components/screenpipe-status.tsx @@ -18,6 +18,10 @@ import { CodeBlock } from "@/components/ui/codeblock"; import { MemoizedReactMarkdown } from "./markdown"; import { invoke } from "@tauri-apps/api/core"; import { spinner } from "./spinner"; +import { + isPermissionGranted, + sendNotification, +} from "@tauri-apps/plugin-notification"; interface HealthCheckResponse { status: string; @@ -35,6 +39,9 @@ const HealthStatus = ({ className }: { className?: string }) => { const [isDialogOpen, setIsDialogOpen] = useState(false); const [isStopping, setIsStopping] = useState(false); const [isStarting, setIsStarting] = useState(false); + const [lastNotificationTime, setLastNotificationTime] = useState< + number | null + >(null); const fetchHealth = async () => { try { @@ -77,6 +84,36 @@ const HealthStatus = ({ className }: { className?: string }) => { return () => clearInterval(interval); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + + useEffect(() => { + const checkAndNotify = async () => { + if (health && health.status === "Unhealthy") { + const now = Date.now(); + const lastNotification = localStorage.getItem( + "lastUnhealthyNotification" + ); + const lastNotificationTime = lastNotification + ? parseInt(lastNotification, 10) + : 0; + + if (now - lastNotificationTime > 3600000) { + // 1 hour in milliseconds + const permissionGranted = await isPermissionGranted(); + if (permissionGranted) { + sendNotification({ + title: "Screenpipe Status Alert", + body: "Screenpipe is currently unhealthy. Please try to restart. It could also be useful to press 'Stop' in the status badge just in case.", + }); + localStorage.setItem("lastUnhealthyNotification", now.toString()); + setLastNotificationTime(now); + } + } + } + }; + + checkAndNotify(); + }, [health]); + const logCommands = `# Stream the log: tail -f $HOME/.screenpipe/screenpipe.log diff --git a/examples/apps/screenpipe-app-tauri/components/settings.tsx b/examples/apps/screenpipe-app-tauri/components/settings.tsx index ee5b9df33..9b4a16f0e 100644 --- a/examples/apps/screenpipe-app-tauri/components/settings.tsx +++ b/examples/apps/screenpipe-app-tauri/components/settings.tsx @@ -19,23 +19,16 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { Badge } from "@/components/ui/badge"; -import { PrettyLink } from "./pretty-link"; import { MemoizedReactMarkdown } from "./markdown"; import { Separator } from "@/components/ui/separator"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import remarkGfm from "remark-gfm"; -import remarkMath from "remark-math"; -import { CodeBlock } from "./ui/codeblock"; -import { ChatMessageActions } from "./chat-message-actions"; -import { useCopyToClipboard } from "@/lib/hooks/use-copy-to-clipboard"; -import { cn } from "@/lib/utils"; -import { IconCheck, IconCopy } from "./ui/icons"; import { invoke } from "@tauri-apps/api/core"; import { spinner } from "./spinner"; export function Settings({ className }: { className?: string }) { const { settings, updateSettings } = useSettings(); const [localSettings, setLocalSettings] = React.useState(settings); + const [isLoading, setIsLoading] = React.useState(false); const handleApiKeyChange = (e: React.ChangeEvent) => { setLocalSettings({ ...localSettings, openaiApiKey: e.target.value }); @@ -48,6 +41,34 @@ export function Settings({ className }: { className?: string }) { updateSettings({ ...localSettings, useOllama: checked }); }; + const handleCloudAudioToggle = async (checked: boolean) => { + if (isLoading) return; + setIsLoading(true); + try { + setLocalSettings({ ...localSettings, useCloudAudio: checked }); + await updateSettings({ ...localSettings, useCloudAudio: checked }); + // Restart screenpipe with new settings + await restartScreenpipe(); + // user feedback + await new Promise((resolve) => setTimeout(resolve, 1000)); + } catch (error) { + console.error("Failed to update cloud audio setting:", error); + } finally { + setIsLoading(false); + } + }; + + const restartScreenpipe = async () => { + try { + await invoke("kill_all_sreenpipes"); + // sleep 1s + await new Promise((resolve) => setTimeout(resolve, 1000)); + await invoke("spawn_screenpipe"); + } catch (error) { + console.error("Failed to restart screenpipe:", error); + } + }; + React.useEffect(() => { setLocalSettings(settings); }, [settings]); @@ -65,7 +86,7 @@ export function Settings({ className }: { className?: string }) { Settings - + Settings @@ -210,6 +231,36 @@ export function Settings({ className }: { className?: string }) { + +
+
+ + + {isLoading && ( +
+ {spinner} +
+ )} +
+
+

+ Toggle to use cloud-based audio processing instead of local + processing. Cloud processing may provide better accuracy but + requires an internet connection. This will restart + screenpipe background process. +

+
+
diff --git a/examples/apps/screenpipe-app-tauri/lib/hooks/use-settings.tsx b/examples/apps/screenpipe-app-tauri/lib/hooks/use-settings.tsx index 6b01d8555..13f9cae94 100644 --- a/examples/apps/screenpipe-app-tauri/lib/hooks/use-settings.tsx +++ b/examples/apps/screenpipe-app-tauri/lib/hooks/use-settings.tsx @@ -1,44 +1,81 @@ import { useState, useEffect } from "react"; +import { Store } from "@tauri-apps/plugin-store"; +import { homeDir } from "@tauri-apps/api/path"; +import { join } from "@tauri-apps/api/path"; interface Settings { openaiApiKey: string; useOllama: boolean; isLoading: boolean; - useCli: boolean; + useCloudAudio: boolean; } +let store: Store | null = null; + export function useSettings() { const [settings, setSettings] = useState({ openaiApiKey: "", useOllama: false, isLoading: true, - useCli: false, + useCloudAudio: true, }); useEffect(() => { - const loadSettings = () => { - const savedKey = localStorage.getItem("openaiApiKey") || ""; - const savedUseOllama = localStorage.getItem("useOllama") === "true"; - const savedUseCli = localStorage.getItem("useCli") === "true"; - setSettings({ - openaiApiKey: savedKey, - useOllama: savedUseOllama, - isLoading: false, - useCli: savedUseCli, - }); + const initStore = async () => { + const home = await homeDir(); + const storePath = await join(home, ".screenpipe", "store.bin"); + store = new Store(storePath); + }; + + const loadSettings = async () => { + if (!store) { + await initStore(); + } + + try { + await store!.load(); + const savedKey = ((await store!.get("openaiApiKey")) as string) || ""; + const savedUseOllama = + ((await store!.get("useOllama")) as boolean) || false; + const savedUseCloudAudio = + ((await store!.get("useCloudAudio")) as boolean) ?? true; + + setSettings({ + openaiApiKey: savedKey, + useOllama: savedUseOllama, + isLoading: false, + useCloudAudio: savedUseCloudAudio, + }); + } catch (error) { + console.error("Failed to load settings:", error); + setSettings((prevSettings) => ({ ...prevSettings, isLoading: false })); + } }; loadSettings(); }, []); - const updateSettings = (newSettings: Partial) => { - setSettings((prevSettings) => { - const updatedSettings = { ...prevSettings, ...newSettings }; - localStorage.setItem("openaiApiKey", updatedSettings.openaiApiKey); - localStorage.setItem("useOllama", updatedSettings.useOllama.toString()); - localStorage.setItem("useCli", updatedSettings.useCli.toString()); - return updatedSettings; - }); + const updateSettings = async (newSettings: Partial) => { + if (!store) { + await initStore(); + } + + try { + const updatedSettings = { ...settings, ...newSettings }; + await store!.set("openaiApiKey", updatedSettings.openaiApiKey); + await store!.set("useOllama", updatedSettings.useOllama); + await store!.set("useCloudAudio", updatedSettings.useCloudAudio); + await store!.save(); + setSettings(updatedSettings); + } catch (error) { + console.error("Failed to update settings:", error); + } }; return { settings, updateSettings }; } + +async function initStore() { + const home = await homeDir(); + const storePath = await join(home, ".screenpipe", "store.bin"); + store = new Store(storePath); +} diff --git a/examples/apps/screenpipe-app-tauri/src-tauri/Cargo.toml b/examples/apps/screenpipe-app-tauri/src-tauri/Cargo.toml index 892af1563..21ab83056 100644 --- a/examples/apps/screenpipe-app-tauri/src-tauri/Cargo.toml +++ b/examples/apps/screenpipe-app-tauri/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "screenpipe-app" -version = "0.1.2" +version = "0.1.3" description = "" authors = ["you"] license = "" diff --git a/examples/apps/screenpipe-app-tauri/src-tauri/src/main.rs b/examples/apps/screenpipe-app-tauri/src-tauri/src/main.rs index 9bbad906d..7b3ac801b 100755 --- a/examples/apps/screenpipe-app-tauri/src-tauri/src/main.rs +++ b/examples/apps/screenpipe-app-tauri/src-tauri/src/main.rs @@ -106,16 +106,30 @@ async fn spawn_screenpipe( } fn spawn_sidecar(app: &tauri::AppHandle) -> Result { - // sleep 1s hack - std::thread::sleep(std::time::Duration::from_secs(1)); - let sidecar = app.shell().sidecar("screenpipe").unwrap(); - let (_, child) = sidecar - .args(["--port", "3030", "--debug", "--self-healing"]) - .spawn() - .map_err(|e| e.to_string())?; - debug!("Spawned sidecar"); + // Get the current settings + let stores = app.state::>(); + let base_dir = get_base_dir(None).expect("Failed to ensure local data directory"); + + let path = base_dir.join("store.bin"); + + let use_cloud_audio = with_store(app.clone(), stores, path, |store| { + Ok(store + .get("useCloudAudio") + .and_then(|v| v.as_bool()) + .unwrap_or(true)) // Default to true if not set + }) + .map_err(|e| e.to_string())?; + + let mut args = vec!["--port", "3030", "--debug", "--self-healing"]; + if !use_cloud_audio { + args.push("--cloud-audio-off"); + } + + let (_, child) = sidecar.args(&args).spawn().map_err(|e| e.to_string())?; + + debug!("Spawned sidecar with args: {:?}", args); Ok(child) } @@ -176,12 +190,8 @@ async fn main() { // Set the TESSDATA_PREFIX environment variable let tessdata_path = exe_dir.join("tessdata"); env::set_var("TESSDATA_PREFIX", tessdata_path); - - // ! hopefully it passes to CLI too } - // let cli = app.cli().matches().expect("Failed to get CLI matches"); - // Get the autostart manager let autostart_manager = app.autolaunch(); // Enable autostart @@ -205,7 +215,7 @@ async fn main() { builder .filter(None, LevelFilter::Info) .filter_module("tokenizers", LevelFilter::Error) - // .filter_module("rusty_tesseract", LevelFilter::Error) + .filter_module("rusty_tesseract", LevelFilter::Error) .filter_module("symphonia", LevelFilter::Error); if debug { @@ -213,8 +223,6 @@ async fn main() { builder.filter_module("app", LevelFilter::Debug); } - // debug!("all param: {:?}", cli.args); - let log_file = File::create(format!("{}/screenpipe-app.log", base_dir.to_string_lossy())).unwrap(); let multi_writer = MultiWriter::new(vec![ @@ -247,7 +255,8 @@ async fn main() { |store| { if store.keys().count() == 0 { // Set default values - store.insert("analytics_enabled".to_string(), Value::Bool(true))?; + store.insert("analyticsEnabled".to_string(), Value::Bool(true))?; + store.insert("useCloudAudio".to_string(), Value::Bool(true))?; store.insert( "config".to_string(), serde_json::to_value(Config::default())?, @@ -263,7 +272,7 @@ async fn main() { store.save()?; let is_analytics_enabled = store - .get("analytics_enabled") + .get("analyticsEnabled") .unwrap_or(&Value::Bool(true)) .as_bool() .unwrap_or(true); @@ -298,33 +307,9 @@ async fn main() { app.run(|_app_handle, event| match event { tauri::RunEvent::Ready { .. } => { debug!("Ready event"); - // tauri::async_runtime::spawn(async move { - // // let _ = start_server().await; - // start_screenpipe_server_new(app.app_handle().clone()); - // }); } tauri::RunEvent::ExitRequested { .. } => { debug!("ExitRequested event"); - // tauri::async_runtime::spawn(async move { - // tx.send(()).unwrap(); - // }); - // TODO less dirty stop :D - // tauri::async_runtime::spawn(async move { - // let _ = tokio::process::Command::new("pkill") - // .arg("-f") - // .arg("screenpipe") - // .output() - // .await; - // }); - // kill sidecar - // let sidecar_state = app.app_handle().state::(); - // let mut sidecar = sidecar_state.0.lock().unwrap(); - // sidecar - // .take() - // .unwrap() - // .kill() - // .map_err(|e| e.to_string()) - // .unwrap(); } _ => {} });