mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-18 14:20:42 -07:00
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:
@@ -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{' '}
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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.
@@ -56,7 +56,7 @@
|
||||
},
|
||||
"plugins": {
|
||||
"shell": {
|
||||
"open": true
|
||||
"open": "^((mailto:\\w+)|(tel:\\w+)|(https?://\\w+)|(/[^\\s]+)|([a-zA-Z]:\\\\[^\\s]*)).*"
|
||||
},
|
||||
"updater": {
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUxRENBQkRBQjdBNTM1OTIKUldTU05hVzMycXZjNGJGcUxmcVVocll2QjdSaTJNdlFxR2M3VDJsMnVvbDdyZGRPMmRlOW9aWTcK",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user