mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-10-03 09:05:17 -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 { ConnectionForm } from '@/components/ServerSettings/ConnectionForm';
|
||||||
|
import { DataFolders } from '@/components/ServerSettings/DataFolders';
|
||||||
import { ServerStatus } from '@/components/ServerSettings/ServerStatus';
|
import { ServerStatus } from '@/components/ServerSettings/ServerStatus';
|
||||||
import { UpdateStatus } from '@/components/ServerSettings/UpdateStatus';
|
import { UpdateStatus } from '@/components/ServerSettings/UpdateStatus';
|
||||||
import { ProviderSettings } from '@/components/ServerSettings/ProviderSettings';
|
import { ProviderSettings } from '@/components/ServerSettings/ProviderSettings';
|
||||||
@@ -13,6 +14,7 @@ export function ServerTab() {
|
|||||||
<ServerStatus />
|
<ServerStatus />
|
||||||
</div>
|
</div>
|
||||||
<ProviderSettings />
|
<ProviderSettings />
|
||||||
|
<DataFolders />
|
||||||
{platform.metadata.isTauri && <UpdateStatus />}
|
{platform.metadata.isTauri && <UpdateStatus />}
|
||||||
<div className="py-8 text-center text-sm text-muted-foreground">
|
<div className="py-8 text-center text-sm text-muted-foreground">
|
||||||
Created by{' '}
|
Created by{' '}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import type {
|
|||||||
ModelStatusListResponse,
|
ModelStatusListResponse,
|
||||||
ModelDownloadRequest,
|
ModelDownloadRequest,
|
||||||
ActiveTasksResponse,
|
ActiveTasksResponse,
|
||||||
|
FolderPathsResponse,
|
||||||
StoryCreate,
|
StoryCreate,
|
||||||
StoryResponse,
|
StoryResponse,
|
||||||
StoryDetailResponse,
|
StoryDetailResponse,
|
||||||
@@ -57,6 +58,11 @@ class ApiClient {
|
|||||||
return this.request<HealthResponse>('/health');
|
return this.request<HealthResponse>('/health');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// System
|
||||||
|
async getSystemFolders(): Promise<FolderPathsResponse> {
|
||||||
|
return this.request<FolderPathsResponse>('/system/folders');
|
||||||
|
}
|
||||||
|
|
||||||
// Profiles
|
// Profiles
|
||||||
async createProfile(data: VoiceProfileCreate): Promise<VoiceProfileResponse> {
|
async createProfile(data: VoiceProfileCreate): Promise<VoiceProfileResponse> {
|
||||||
return this.request<VoiceProfileResponse>('/profiles', {
|
return this.request<VoiceProfileResponse>('/profiles', {
|
||||||
|
|||||||
@@ -128,6 +128,12 @@ export interface ActiveTasksResponse {
|
|||||||
generations: ActiveGenerationTask[];
|
generations: ActiveGenerationTask[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FolderPathsResponse {
|
||||||
|
data_dir: string;
|
||||||
|
models_dir: string;
|
||||||
|
providers_dir: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface StoryCreate {
|
export interface StoryCreate {
|
||||||
name: string;
|
name: string;
|
||||||
description?: 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 {
|
export interface PlatformFilesystem {
|
||||||
saveFile(filename: string, blob: Blob, filters?: FileFilter[]): Promise<void>;
|
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 {
|
export interface UpdateStatus {
|
||||||
|
|||||||
@@ -74,6 +74,19 @@ async def shutdown():
|
|||||||
return {"message": "Shutting down..."}
|
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)
|
@app.get("/health", response_model=models.HealthResponse)
|
||||||
async def health():
|
async def health():
|
||||||
"""Health check endpoint."""
|
"""Health check endpoint."""
|
||||||
|
|||||||
@@ -170,6 +170,13 @@ class ActiveTasksResponse(BaseModel):
|
|||||||
generations: List[ActiveGenerationTask]
|
generations: List[ActiveGenerationTask]
|
||||||
|
|
||||||
|
|
||||||
|
class FolderPathsResponse(BaseModel):
|
||||||
|
"""Response model for system folder paths."""
|
||||||
|
data_dir: str
|
||||||
|
models_dir: str
|
||||||
|
providers_dir: str
|
||||||
|
|
||||||
|
|
||||||
class AudioChannelCreate(BaseModel):
|
class AudioChannelCreate(BaseModel):
|
||||||
"""Request model for creating an audio channel."""
|
"""Request model for creating an audio channel."""
|
||||||
name: str = Field(..., min_length=1, max_length=100)
|
name: str = Field(..., min_length=1, max_length=100)
|
||||||
|
|||||||
Binary file not shown.
@@ -56,7 +56,7 @@
|
|||||||
},
|
},
|
||||||
"plugins": {
|
"plugins": {
|
||||||
"shell": {
|
"shell": {
|
||||||
"open": true
|
"open": "^((mailto:\\w+)|(tel:\\w+)|(https?://\\w+)|(/[^\\s]+)|([a-zA-Z]:\\\\[^\\s]*)).*"
|
||||||
},
|
},
|
||||||
"updater": {
|
"updater": {
|
||||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUxRENBQkRBQjdBNTM1OTIKUldTU05hVzMycXZjNGJGcUxmcVVocll2QjdSaTJNdlFxR2M3VDJsMnVvbDdyZGRPMmRlOW9aWTcK",
|
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUxRENBQkRBQjdBNTM1OTIKUldTU05hVzMycXZjNGJGcUxmcVVocll2QjdSaTJNdlFxR2M3VDJsMnVvbDdyZGRPMmRlOW9aWTcK",
|
||||||
|
|||||||
@@ -27,4 +27,15 @@ export const tauriFilesystem: PlatformFilesystem = {
|
|||||||
document.body.removeChild(a);
|
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);
|
window.URL.revokeObjectURL(url);
|
||||||
document.body.removeChild(a);
|
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