feat: add UI build to build process

- Created separate build script to handle both CLI and UI building
- Added automatic UI dependency installation
- Copy built UI artifacts to dist directory

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
musistudio
2025-07-30 11:15:05 +08:00
parent 31db041084
commit 112d7ef8f9
57 changed files with 13581 additions and 365 deletions

227
ui/src/App.tsx Normal file
View File

@@ -0,0 +1,227 @@
import { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { SettingsDialog } from "@/components/SettingsDialog";
import { Transformers } from "@/components/Transformers";
import { Providers } from "@/components/Providers";
import { Router } from "@/components/Router";
import { Button } from "@/components/ui/button";
import { useConfig } from "@/components/ConfigProvider";
import { api } from "@/lib/api";
import { Settings, Languages, Save, RefreshCw } from "lucide-react";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Toast } from "@/components/ui/toast";
import "@/styles/animations.css";
function App() {
const { t, i18n } = useTranslation();
const navigate = useNavigate();
const { config, error } = useConfig();
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
const [isCheckingAuth, setIsCheckingAuth] = useState(true);
const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' | 'warning' } | null>(null);
useEffect(() => {
const checkAuth = async () => {
// If we already have a config, we're authenticated
if (config) {
setIsCheckingAuth(false);
return;
}
// For empty API key, allow access without checking config
const apiKey = localStorage.getItem('apiKey');
if (!apiKey) {
setIsCheckingAuth(false);
return;
}
// If we don't have a config, try to fetch it
try {
await api.getConfig();
// If successful, we don't need to do anything special
// The ConfigProvider will handle setting the config
} catch (err) {
// If it's a 401, the API client will redirect to login
// For other errors, we still show the app to display the error
console.error('Error checking auth:', err);
// Redirect to login on authentication error
if ((err as Error).message === 'Unauthorized') {
navigate('/login');
}
} finally {
setIsCheckingAuth(false);
}
};
checkAuth();
// Listen for unauthorized events
const handleUnauthorized = () => {
navigate('/login');
};
window.addEventListener('unauthorized', handleUnauthorized);
return () => {
window.removeEventListener('unauthorized', handleUnauthorized);
};
}, [config, navigate]);
const saveConfig = async () => {
if (config) {
try {
// Save to API
const response = await api.updateConfig(config);
// Show success message or handle as needed
console.log('Config saved successfully');
// 根据响应信息进行提示
if (response && typeof response === 'object' && 'success' in response) {
const apiResponse = response as { success: boolean; message?: string };
if (apiResponse.success) {
setToast({ message: apiResponse.message || t('app.config_saved_success'), type: 'success' });
} else {
setToast({ message: apiResponse.message || t('app.config_saved_failed'), type: 'error' });
}
} else {
// 默认成功提示
setToast({ message: t('app.config_saved_success'), type: 'success' });
}
} catch (error) {
console.error('Failed to save config:', error);
// Handle error appropriately
setToast({ message: t('app.config_saved_failed') + ': ' + (error as Error).message, type: 'error' });
}
}
};
const saveConfigAndRestart = async () => {
if (config) {
try {
// Save to API
const response = await api.updateConfig(config);
// Check if save was successful before restarting
let saveSuccessful = true;
if (response && typeof response === 'object' && 'success' in response) {
const apiResponse = response as { success: boolean; message?: string };
if (!apiResponse.success) {
saveSuccessful = false;
setToast({ message: apiResponse.message || t('app.config_saved_failed'), type: 'error' });
}
}
// Only restart if save was successful
if (saveSuccessful) {
// Restart service
const response = await api.restartService();
// Show success message or handle as needed
console.log('Config saved and service restarted successfully');
// 根据响应信息进行提示
if (response && typeof response === 'object' && 'success' in response) {
const apiResponse = response as { success: boolean; message?: string };
if (apiResponse.success) {
setToast({ message: apiResponse.message || t('app.config_saved_restart_success'), type: 'success' });
}
} else {
// 默认成功提示
setToast({ message: t('app.config_saved_restart_success'), type: 'success' });
}
}
} catch (error) {
console.error('Failed to save config and restart:', error);
// Handle error appropriately
setToast({ message: t('app.config_saved_restart_failed') + ': ' + (error as Error).message, type: 'error' });
}
}
};
if (isCheckingAuth) {
return <div>Loading...</div>;
}
if (error) {
return <div>Error: {error.message}</div>;
}
if (!config) {
return <div>Loading...</div>;
}
return (
<div className="h-screen bg-gray-50 font-sans">
<header className="flex h-16 items-center justify-between border-b bg-white px-6">
<h1 className="text-xl font-semibold text-gray-800">{t('app.title')}</h1>
<div className="flex items-center gap-2">
<Button variant="ghost" size="icon" onClick={() => setIsSettingsOpen(true)} className="transition-all-ease hover:scale-110">
<Settings className="h-5 w-5" />
</Button>
<Popover>
<PopoverTrigger asChild>
<Button variant="ghost" size="icon" className="transition-all-ease hover:scale-110">
<Languages className="h-5 w-5" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-32 p-2">
<div className="space-y-1">
<Button
variant={i18n.language.startsWith('en') ? 'default' : 'ghost'}
className="w-full justify-start transition-all-ease hover:scale-[1.02]"
onClick={() => i18n.changeLanguage('en')}
>
English
</Button>
<Button
variant={i18n.language.startsWith('zh') ? 'default' : 'ghost'}
className="w-full justify-start transition-all-ease hover:scale-[1.02]"
onClick={() => i18n.changeLanguage('zh')}
>
</Button>
</div>
</PopoverContent>
</Popover>
<Button onClick={saveConfig} variant="outline" className="transition-all-ease hover:scale-[1.02] active:scale-[0.98]">
<Save className="mr-2 h-4 w-4" />
{t('app.save')}
</Button>
<Button onClick={saveConfigAndRestart} className="transition-all-ease hover:scale-[1.02] active:scale-[0.98]">
<RefreshCw className="mr-2 h-4 w-4" />
{t('app.save_and_restart')}
</Button>
</div>
</header>
<main className="flex h-[calc(100vh-4rem)] gap-4 p-4">
<div className="w-3/5">
<Providers />
</div>
<div className="flex w-2/5 flex-col gap-4">
<div className="h-3/5">
<Router />
</div>
<div className="flex-1">
<Transformers />
</div>
</div>
</main>
<SettingsDialog isOpen={isSettingsOpen} onOpenChange={setIsSettingsOpen} />
{toast && (
<Toast
message={toast.message}
type={toast.type}
onClose={() => setToast(null)}
/>
)}
</div>
);
}
export default App;