diff --git a/aha-ui/pnpm-workspace.yaml b/aha-ui/pnpm-workspace.yaml index 5ed0b5a..220bd0b 100644 --- a/aha-ui/pnpm-workspace.yaml +++ b/aha-ui/pnpm-workspace.yaml @@ -1,2 +1,3 @@ allowBuilds: esbuild: true + msw: true diff --git a/aha-ui/src-tauri/src/lib.rs b/aha-ui/src-tauri/src/lib.rs index 1fb9168..ad17446 100644 --- a/aha-ui/src-tauri/src/lib.rs +++ b/aha-ui/src-tauri/src/lib.rs @@ -60,6 +60,11 @@ struct ServerStatusResponse { // ── Helpers ────────────────────────────────────────── +#[tauri::command] +fn get_default_save_dir() -> Result { + aha::utils::get_default_save_dir().ok_or_else(|| "无法获取 home 目录".to_string()) +} + fn get_save_dir() -> Result { aha::utils::get_default_save_dir().ok_or_else(|| "无法获取 home 目录".to_string()) } @@ -206,10 +211,13 @@ fn get_model_detail(model_id: String) -> Result { } #[tauri::command] -async fn download_model(model_id: String) -> Result<(), String> { +async fn download_model(model_id: String, save_dir: Option) -> Result<(), String> { use aha::utils::download_model; - let save_dir = get_save_dir()?; + let save_dir = match save_dir { + Some(dir) => dir, + None => get_save_dir()?, + }; download_model(&model_id, &save_dir, 3) .await .map_err(|e| format!("下载失败: {}", e)) @@ -417,6 +425,7 @@ pub fn run() { stop_server, get_server_status, clear_logs, + get_default_save_dir, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/aha-ui/src/components/layout/data/sidebar-data.ts b/aha-ui/src/components/layout/data/sidebar-data.ts index e99bcfc..b559603 100644 --- a/aha-ui/src/components/layout/data/sidebar-data.ts +++ b/aha-ui/src/components/layout/data/sidebar-data.ts @@ -8,6 +8,7 @@ import { Palette, Bell, Monitor, + } from 'lucide-react' import { type SidebarData } from '../types' @@ -52,6 +53,11 @@ export const sidebarData: SidebarData = { title: 'Settings', icon: Settings, items: [ + { + title: 'Model', + url: '/settings/model', + icon: Package, + }, { title: 'Profile', url: '/settings', diff --git a/aha-ui/src/features/models/index.tsx b/aha-ui/src/features/models/index.tsx index f6824a6..b69bb00 100644 --- a/aha-ui/src/features/models/index.tsx +++ b/aha-ui/src/features/models/index.tsx @@ -9,6 +9,8 @@ import { Main } from "@/components/layout/main" import { ProfileDropdown } from "@/components/profile-dropdown" import { ThemeSwitch } from "@/components/theme-switch" +const SAVE_DIR_KEY = "aha-model-save-dir" + interface ModelInfo { model_id: string owner: string @@ -67,7 +69,8 @@ export function ModelsPage() { setDownloading(modelId) setError(null) try { - await invoke("download_model", { modelId }) + const saveDir = localStorage.getItem(SAVE_DIR_KEY) || null + await invoke("download_model", { modelId, saveDir }) await loadModels() } catch (e) { setError(String(e)) diff --git a/aha-ui/src/features/settings/index.tsx b/aha-ui/src/features/settings/index.tsx index a1f4937..0842b51 100644 --- a/aha-ui/src/features/settings/index.tsx +++ b/aha-ui/src/features/settings/index.tsx @@ -1,5 +1,4 @@ import { Outlet } from '@tanstack/react-router' -import { Monitor, Bell, Palette, Wrench, UserCog } from 'lucide-react' import { Separator } from '@/components/ui/separator' import { ConfigDrawer } from '@/components/config-drawer' import { Header } from '@/components/layout/header' @@ -7,35 +6,6 @@ import { Main } from '@/components/layout/main' import { ProfileDropdown } from '@/components/profile-dropdown' import { Search } from '@/components/search' import { ThemeSwitch } from '@/components/theme-switch' -import { SidebarNav } from './components/sidebar-nav' - -const sidebarNavItems = [ - { - title: 'Profile', - href: '/settings', - icon: , - }, - { - title: 'Account', - href: '/settings/account', - icon: , - }, - { - title: 'Appearance', - href: '/settings/appearance', - icon: , - }, - { - title: 'Notifications', - href: '/settings/notifications', - icon: , - }, - { - title: 'Display', - href: '/settings/display', - icon: , - }, -] export function Settings() { return ( @@ -58,13 +28,8 @@ export function Settings() {

-
- -
- -
+
+
diff --git a/aha-ui/src/features/settings/model/index.tsx b/aha-ui/src/features/settings/model/index.tsx new file mode 100644 index 0000000..f61467c --- /dev/null +++ b/aha-ui/src/features/settings/model/index.tsx @@ -0,0 +1,74 @@ +import { useState, useEffect } from "react" +import { invoke } from "@tauri-apps/api/core" +import { FolderOpen } from "lucide-react" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { ContentSection } from "../components/content-section" + +const SAVE_DIR_KEY = "aha-model-save-dir" + +export function SettingsModel() { + const [saveDir, setSaveDir] = useState("") + const [defaultDir, setDefaultDir] = useState("") + const [saved, setSaved] = useState(false) + + useEffect(() => { + invoke("get_default_save_dir") + .then(setDefaultDir) + .catch(() => {}) + + const stored = localStorage.getItem(SAVE_DIR_KEY) + if (stored) setSaveDir(stored) + }, []) + + const handleSave = () => { + if (saveDir) { + localStorage.setItem(SAVE_DIR_KEY, saveDir) + } else { + localStorage.removeItem(SAVE_DIR_KEY) + } + setSaved(true) + setTimeout(() => setSaved(false), 2000) + } + + const handleReset = () => { + setSaveDir("") + localStorage.removeItem(SAVE_DIR_KEY) + } + + return ( + +
+
+ + setSaveDir(e.target.value)} + placeholder={defaultDir || "~/.aha/"} + /> +

+ 留空则使用默认路径:{defaultDir || "~/.aha/"} + 。模型将下载到该目录下的 {`{model_id}`} 子文件夹中。 +

+
+ +
+ + +
+
+
+ ) +} diff --git a/aha-ui/src/routeTree.gen.ts b/aha-ui/src/routeTree.gen.ts index 41674f7..38506a1 100644 --- a/aha-ui/src/routeTree.gen.ts +++ b/aha-ui/src/routeTree.gen.ts @@ -26,6 +26,7 @@ import { Route as authForgotPasswordRouteImport } from './routes/(auth)/forgot-p import { Route as AuthenticatedSettingsRouteRouteImport } from './routes/_authenticated/settings/route' import { Route as AuthenticatedSettingsIndexRouteImport } from './routes/_authenticated/settings/index' import { Route as AuthenticatedSettingsNotificationsRouteImport } from './routes/_authenticated/settings/notifications' +import { Route as AuthenticatedSettingsModelRouteImport } from './routes/_authenticated/settings/model' import { Route as AuthenticatedSettingsDisplayRouteImport } from './routes/_authenticated/settings/display' import { Route as AuthenticatedSettingsAppearanceRouteImport } from './routes/_authenticated/settings/appearance' import { Route as AuthenticatedSettingsAccountRouteImport } from './routes/_authenticated/settings/account' @@ -117,6 +118,12 @@ const AuthenticatedSettingsNotificationsRoute = path: '/notifications', getParentRoute: () => AuthenticatedSettingsRouteRoute, } as any) +const AuthenticatedSettingsModelRoute = + AuthenticatedSettingsModelRouteImport.update({ + id: '/model', + path: '/model', + getParentRoute: () => AuthenticatedSettingsRouteRoute, + } as any) const AuthenticatedSettingsDisplayRoute = AuthenticatedSettingsDisplayRouteImport.update({ id: '/display', @@ -154,6 +161,7 @@ export interface FileRoutesByFullPath { '/settings/account': typeof AuthenticatedSettingsAccountRoute '/settings/appearance': typeof AuthenticatedSettingsAppearanceRoute '/settings/display': typeof AuthenticatedSettingsDisplayRoute + '/settings/model': typeof AuthenticatedSettingsModelRoute '/settings/notifications': typeof AuthenticatedSettingsNotificationsRoute '/settings/': typeof AuthenticatedSettingsIndexRoute } @@ -174,6 +182,7 @@ export interface FileRoutesByTo { '/settings/account': typeof AuthenticatedSettingsAccountRoute '/settings/appearance': typeof AuthenticatedSettingsAppearanceRoute '/settings/display': typeof AuthenticatedSettingsDisplayRoute + '/settings/model': typeof AuthenticatedSettingsModelRoute '/settings/notifications': typeof AuthenticatedSettingsNotificationsRoute '/settings': typeof AuthenticatedSettingsIndexRoute } @@ -197,6 +206,7 @@ export interface FileRoutesById { '/_authenticated/settings/account': typeof AuthenticatedSettingsAccountRoute '/_authenticated/settings/appearance': typeof AuthenticatedSettingsAppearanceRoute '/_authenticated/settings/display': typeof AuthenticatedSettingsDisplayRoute + '/_authenticated/settings/model': typeof AuthenticatedSettingsModelRoute '/_authenticated/settings/notifications': typeof AuthenticatedSettingsNotificationsRoute '/_authenticated/settings/': typeof AuthenticatedSettingsIndexRoute } @@ -220,6 +230,7 @@ export interface FileRouteTypes { | '/settings/account' | '/settings/appearance' | '/settings/display' + | '/settings/model' | '/settings/notifications' | '/settings/' fileRoutesByTo: FileRoutesByTo @@ -240,6 +251,7 @@ export interface FileRouteTypes { | '/settings/account' | '/settings/appearance' | '/settings/display' + | '/settings/model' | '/settings/notifications' | '/settings' id: @@ -262,6 +274,7 @@ export interface FileRouteTypes { | '/_authenticated/settings/account' | '/_authenticated/settings/appearance' | '/_authenticated/settings/display' + | '/_authenticated/settings/model' | '/_authenticated/settings/notifications' | '/_authenticated/settings/' fileRoutesById: FileRoutesById @@ -401,6 +414,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthenticatedSettingsNotificationsRouteImport parentRoute: typeof AuthenticatedSettingsRouteRoute } + '/_authenticated/settings/model': { + id: '/_authenticated/settings/model' + path: '/model' + fullPath: '/settings/model' + preLoaderRoute: typeof AuthenticatedSettingsModelRouteImport + parentRoute: typeof AuthenticatedSettingsRouteRoute + } '/_authenticated/settings/display': { id: '/_authenticated/settings/display' path: '/display' @@ -429,6 +449,7 @@ interface AuthenticatedSettingsRouteRouteChildren { AuthenticatedSettingsAccountRoute: typeof AuthenticatedSettingsAccountRoute AuthenticatedSettingsAppearanceRoute: typeof AuthenticatedSettingsAppearanceRoute AuthenticatedSettingsDisplayRoute: typeof AuthenticatedSettingsDisplayRoute + AuthenticatedSettingsModelRoute: typeof AuthenticatedSettingsModelRoute AuthenticatedSettingsNotificationsRoute: typeof AuthenticatedSettingsNotificationsRoute AuthenticatedSettingsIndexRoute: typeof AuthenticatedSettingsIndexRoute } @@ -438,6 +459,7 @@ const AuthenticatedSettingsRouteRouteChildren: AuthenticatedSettingsRouteRouteCh AuthenticatedSettingsAccountRoute: AuthenticatedSettingsAccountRoute, AuthenticatedSettingsAppearanceRoute: AuthenticatedSettingsAppearanceRoute, AuthenticatedSettingsDisplayRoute: AuthenticatedSettingsDisplayRoute, + AuthenticatedSettingsModelRoute: AuthenticatedSettingsModelRoute, AuthenticatedSettingsNotificationsRoute: AuthenticatedSettingsNotificationsRoute, AuthenticatedSettingsIndexRoute: AuthenticatedSettingsIndexRoute, diff --git a/aha-ui/src/routes/_authenticated/settings/model.tsx b/aha-ui/src/routes/_authenticated/settings/model.tsx new file mode 100644 index 0000000..a3bf1bb --- /dev/null +++ b/aha-ui/src/routes/_authenticated/settings/model.tsx @@ -0,0 +1,6 @@ +import { createFileRoute } from '@tanstack/react-router' +import { SettingsModel } from '@/features/settings/model' + +export const Route = createFileRoute('/_authenticated/settings/model')({ + component: SettingsModel, +}) diff --git a/src/server/mod.rs b/src/server/mod.rs index c01a93a..5c881e1 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -72,6 +72,8 @@ pub(crate) async fn start_http_server( // Health check and model info endpoints builder = builder.mount("/", routes![api::health, api::models]); + // OpenAI-compatible model listing endpoint: /v1/models + builder = builder.mount("/v1", routes![api::models]); // Shutdown endpoint builder = builder.manage(shutdown_flag); builder = builder.mount("/", routes![api::shutdown]);