Add DataFolders component to ServerSettings for managing system folder paths

- Introduced a new DataFolders component to display and manage paths for application data, models, and providers.
- Implemented a FolderRow component for individual folder display, including loading states and open folder functionality.
- Added API client methods and hooks to fetch system folder paths from the backend.
- Updated ServerTab to include the new DataFolders component, enhancing server settings management.
This commit is contained in:
Jamie Pine
2026-02-03 00:32:18 -08:00
parent 73d9b5d70e
commit 793e392e56
12 changed files with 185 additions and 1 deletions
@@ -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 (
<div className="space-y-1.5">
<div className="flex items-center justify-between">
<div>
<div className="text-sm font-medium">{label}</div>
<div className="text-xs text-muted-foreground">{description}</div>
</div>
{canOpen && path && (
<Button
variant="outline"
size="sm"
onClick={onOpen}
disabled={isLoading || !path}
className="shrink-0"
>
<HugeiconsIcon icon={FolderOpenIcon} size={16} className="h-4 w-4 mr-2" />
Open
</Button>
)}
</div>
<Input
value={isLoading ? 'Loading...' : path || 'Not available'}
readOnly
className="font-mono text-xs text-muted-foreground select-all cursor-text"
/>
</div>
);
}
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 (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<HugeiconsIcon icon={Folder01Icon} size={20} className="h-5 w-5" />
Data Folders
</CardTitle>
<CardDescription>
{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.'}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{error ? (
<div className="flex items-center gap-2 text-sm text-destructive">
<Icon icon="lucide:alert-circle" className="h-4 w-4" />
<span>Failed to load folder paths: {error.message}</span>
</div>
) : (
<>
<FolderRow
label="App Data"
description="Voices, generations, and app database"
path={folders?.data_dir}
isLoading={isLoading}
canOpen={isTauri}
onOpen={() => handleOpenFolder(folders?.data_dir)}
/>
<FolderRow
label="Models"
description="Downloaded AI models from HuggingFace Hub"
path={folders?.models_dir}
isLoading={isLoading}
canOpen={isTauri}
onOpen={() => handleOpenFolder(folders?.models_dir)}
/>
<FolderRow
label="Providers"
description="External TTS provider binaries (PyTorch CPU/CUDA)"
path={folders?.providers_dir}
isLoading={isLoading}
canOpen={isTauri}
onOpen={() => handleOpenFolder(folders?.providers_dir)}
/>
</>
)}
</CardContent>
</Card>
);
}
@@ -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() {
<ServerStatus />
</div>
<ProviderSettings />
<DataFolders />
{platform.metadata.isTauri && <UpdateStatus />}
<div className="py-8 text-center text-sm text-muted-foreground">
Created by{' '}
+6
View File
@@ -14,6 +14,7 @@ import type {
ModelStatusListResponse,
ModelDownloadRequest,
ActiveTasksResponse,
FolderPathsResponse,
StoryCreate,
StoryResponse,
StoryDetailResponse,
@@ -57,6 +58,11 @@ class ApiClient {
return this.request<HealthResponse>('/health');
}
// System
async getSystemFolders(): Promise<FolderPathsResponse> {
return this.request<FolderPathsResponse>('/system/folders');
}
// Profiles
async createProfile(data: VoiceProfileCreate): Promise<VoiceProfileResponse> {
return this.request<VoiceProfileResponse>('/profiles', {
+6
View File
@@ -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;
+14
View File
@@ -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,
});
}
+7
View File
@@ -10,6 +10,13 @@ export interface FileFilter {
export interface PlatformFilesystem {
saveFile(filename: string, blob: Blob, filters?: FileFilter[]): Promise<void>;
/**
* 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<boolean>;
}
export interface UpdateStatus {
+13
View File
@@ -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."""
+7
View File
@@ -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)
Binary file not shown.
+1 -1
View File
@@ -56,7 +56,7 @@
},
"plugins": {
"shell": {
"open": true
"open": "^((mailto:\\w+)|(tel:\\w+)|(https?://\\w+)|(/[^\\s]+)|([a-zA-Z]:\\\\[^\\s]*)).*"
},
"updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUxRENBQkRBQjdBNTM1OTIKUldTU05hVzMycXZjNGJGcUxmcVVocll2QjdSaTJNdlFxR2M3VDJsMnVvbDdyZGRPMmRlOW9aWTcK",
+11
View File
@@ -27,4 +27,15 @@ export const tauriFilesystem: PlatformFilesystem = {
document.body.removeChild(a);
}
},
async openFolder(path: string): Promise<boolean> {
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;
}
},
};
+6
View File
@@ -12,4 +12,10 @@ export const webFilesystem: PlatformFilesystem = {
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
},
async openFolder(_path: string): Promise<boolean> {
// Browsers cannot open local folders for security reasons
// The UI will show the path as a read-only text instead
return false;
},
};