diff --git a/app/src/components/ServerSettings/DataFolders.tsx b/app/src/components/ServerSettings/DataFolders.tsx new file mode 100644 index 00000000..9338da09 --- /dev/null +++ b/app/src/components/ServerSettings/DataFolders.tsx @@ -0,0 +1,112 @@ +import { Folder01Icon, FolderOpenIcon } from '@hugeicons/core-free-icons'; +import { HugeiconsIcon } from '@hugeicons/react'; +import { Icon } from '@iconify/react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { useSystemFolders } from '@/lib/hooks/useSystemFolders'; +import { usePlatform } from '@/platform/PlatformContext'; + +interface FolderRowProps { + label: string; + description: string; + path: string | undefined; + isLoading: boolean; + canOpen: boolean; + onOpen: () => void; +} + +function FolderRow({ label, description, path, isLoading, canOpen, onOpen }: FolderRowProps) { + return ( +
+
+
+
{label}
+
{description}
+
+ {canOpen && path && ( + + )} +
+ +
+ ); +} + +export function DataFolders() { + const { data: folders, isLoading, error } = useSystemFolders(); + const platform = usePlatform(); + const isTauri = platform.metadata.isTauri; + + const handleOpenFolder = async (path: string | undefined) => { + if (!path) return; + const success = await platform.filesystem.openFolder(path); + if (!success && isTauri) { + console.error('Failed to open folder:', path); + } + }; + + return ( + + + + + Data Folders + + + {isTauri + ? 'Click "Open" to view folders in your file explorer, or copy the paths below.' + : 'These are the server-side folder paths where your data is stored.'} + + + + {error ? ( +
+ + Failed to load folder paths: {error.message} +
+ ) : ( + <> + handleOpenFolder(folders?.data_dir)} + /> + handleOpenFolder(folders?.models_dir)} + /> + handleOpenFolder(folders?.providers_dir)} + /> + + )} +
+
+ ); +} diff --git a/app/src/components/ServerTab/ServerTab.tsx b/app/src/components/ServerTab/ServerTab.tsx index 5a512027..d77c7800 100644 --- a/app/src/components/ServerTab/ServerTab.tsx +++ b/app/src/components/ServerTab/ServerTab.tsx @@ -1,4 +1,5 @@ import { ConnectionForm } from '@/components/ServerSettings/ConnectionForm'; +import { DataFolders } from '@/components/ServerSettings/DataFolders'; import { ServerStatus } from '@/components/ServerSettings/ServerStatus'; import { UpdateStatus } from '@/components/ServerSettings/UpdateStatus'; import { ProviderSettings } from '@/components/ServerSettings/ProviderSettings'; @@ -13,6 +14,7 @@ export function ServerTab() { + {platform.metadata.isTauri && }
Created by{' '} diff --git a/app/src/lib/api/client.ts b/app/src/lib/api/client.ts index b5da41a6..01f548d2 100644 --- a/app/src/lib/api/client.ts +++ b/app/src/lib/api/client.ts @@ -14,6 +14,7 @@ import type { ModelStatusListResponse, ModelDownloadRequest, ActiveTasksResponse, + FolderPathsResponse, StoryCreate, StoryResponse, StoryDetailResponse, @@ -57,6 +58,11 @@ class ApiClient { return this.request('/health'); } + // System + async getSystemFolders(): Promise { + return this.request('/system/folders'); + } + // Profiles async createProfile(data: VoiceProfileCreate): Promise { return this.request('/profiles', { diff --git a/app/src/lib/api/types.ts b/app/src/lib/api/types.ts index dd76ca1e..717496ea 100644 --- a/app/src/lib/api/types.ts +++ b/app/src/lib/api/types.ts @@ -128,6 +128,12 @@ export interface ActiveTasksResponse { generations: ActiveGenerationTask[]; } +export interface FolderPathsResponse { + data_dir: string; + models_dir: string; + providers_dir: string; +} + export interface StoryCreate { name: string; description?: string; diff --git a/app/src/lib/hooks/useSystemFolders.ts b/app/src/lib/hooks/useSystemFolders.ts new file mode 100644 index 00000000..47e92540 --- /dev/null +++ b/app/src/lib/hooks/useSystemFolders.ts @@ -0,0 +1,14 @@ +import { useQuery } from '@tanstack/react-query'; +import { apiClient } from '@/lib/api/client'; +import { useServerStore } from '@/stores/serverStore'; + +export function useSystemFolders() { + const serverUrl = useServerStore((state) => state.serverUrl); + + return useQuery({ + queryKey: ['system', 'folders', serverUrl], + queryFn: () => apiClient.getSystemFolders(), + staleTime: 60000, // Cache for 1 minute - folder paths don't change often + retry: 1, + }); +} diff --git a/app/src/platform/types.ts b/app/src/platform/types.ts index 5ea4d609..6d724fd8 100644 --- a/app/src/platform/types.ts +++ b/app/src/platform/types.ts @@ -10,6 +10,13 @@ export interface FileFilter { export interface PlatformFilesystem { saveFile(filename: string, blob: Blob, filters?: FileFilter[]): Promise; + /** + * Open a folder in the native file explorer. + * On web, this is a no-op since browsers cannot open folders. + * @param path - The absolute path to the folder to open + * @returns true if the folder was opened, false if not supported + */ + openFolder(path: string): Promise; } export interface UpdateStatus { diff --git a/backend/main.py b/backend/main.py index 306a0639..c74ae460 100644 --- a/backend/main.py +++ b/backend/main.py @@ -74,6 +74,19 @@ async def shutdown(): return {"message": "Shutting down..."} +@app.get("/system/folders", response_model=models.FolderPathsResponse) +async def get_system_folders(): + """Get system folder paths for data, models, and providers.""" + from huggingface_hub import constants as hf_constants + from .providers.installer import _get_providers_dir + + return models.FolderPathsResponse( + data_dir=str(config.get_data_dir().absolute()), + models_dir=str(Path(hf_constants.HF_HUB_CACHE).absolute()), + providers_dir=str(_get_providers_dir().absolute()), + ) + + @app.get("/health", response_model=models.HealthResponse) async def health(): """Health check endpoint.""" diff --git a/backend/models.py b/backend/models.py index 59e45405..0540644e 100644 --- a/backend/models.py +++ b/backend/models.py @@ -170,6 +170,13 @@ class ActiveTasksResponse(BaseModel): generations: List[ActiveGenerationTask] +class FolderPathsResponse(BaseModel): + """Response model for system folder paths.""" + data_dir: str + models_dir: str + providers_dir: str + + class AudioChannelCreate(BaseModel): """Request model for creating an audio channel.""" name: str = Field(..., min_length=1, max_length=100) diff --git a/tauri/src-tauri/gen/Assets.car b/tauri/src-tauri/gen/Assets.car index 0fba2b0e..fbe8f711 100644 Binary files a/tauri/src-tauri/gen/Assets.car and b/tauri/src-tauri/gen/Assets.car differ diff --git a/tauri/src-tauri/tauri.conf.json b/tauri/src-tauri/tauri.conf.json index 294d5049..202d22e0 100644 --- a/tauri/src-tauri/tauri.conf.json +++ b/tauri/src-tauri/tauri.conf.json @@ -56,7 +56,7 @@ }, "plugins": { "shell": { - "open": true + "open": "^((mailto:\\w+)|(tel:\\w+)|(https?://\\w+)|(/[^\\s]+)|([a-zA-Z]:\\\\[^\\s]*)).*" }, "updater": { "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUxRENBQkRBQjdBNTM1OTIKUldTU05hVzMycXZjNGJGcUxmcVVocll2QjdSaTJNdlFxR2M3VDJsMnVvbDdyZGRPMmRlOW9aWTcK", diff --git a/tauri/src/platform/filesystem.ts b/tauri/src/platform/filesystem.ts index e37b4d18..7c057a6d 100644 --- a/tauri/src/platform/filesystem.ts +++ b/tauri/src/platform/filesystem.ts @@ -27,4 +27,15 @@ export const tauriFilesystem: PlatformFilesystem = { document.body.removeChild(a); } }, + + async openFolder(path: string): Promise { + try { + const { open } = await import('@tauri-apps/plugin-shell'); + await open(path); + return true; + } catch (error) { + console.error('Failed to open folder:', error); + return false; + } + }, }; diff --git a/web/src/platform/filesystem.ts b/web/src/platform/filesystem.ts index 1f45a49c..b9d74690 100644 --- a/web/src/platform/filesystem.ts +++ b/web/src/platform/filesystem.ts @@ -12,4 +12,10 @@ export const webFilesystem: PlatformFilesystem = { window.URL.revokeObjectURL(url); document.body.removeChild(a); }, + + async openFolder(_path: string): Promise { + // Browsers cannot open local folders for security reasons + // The UI will show the path as a read-only text instead + return false; + }, };