feat: add 89cat Dashboard, Management, Security, and FAQ pages
Some checks failed
Electron E2E / Electron E2E (macos-latest) (push) Has been cancelled
Electron E2E / Electron E2E (ubuntu-latest) (push) Has been cancelled
Electron E2E / Electron E2E (windows-latest) (push) Has been cancelled

Add 4 new pages (Dashboard home, Management Center, Security Check, FAQ)
with route restructuring and i18n translations in zh/en.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
zyh
2026-06-04 10:01:26 +08:00
parent 8ea306d91c
commit d34818cc83
10 changed files with 1022 additions and 6 deletions

View File

@@ -10,6 +10,10 @@ import i18n from './i18n';
import { MainLayout } from './components/layout/MainLayout';
import { TooltipProvider } from '@/components/ui/tooltip';
import { Models } from './pages/Models';
import { Dashboard } from './pages/Dashboard';
import { Management } from './pages/Management';
import { Security } from './pages/Security';
import { Faq } from './pages/Faq';
import { Chat } from './pages/Chat';
import { Agents } from './pages/Agents';
import { Channels } from './pages/Channels';
@@ -201,12 +205,16 @@ function App() {
{/* Main application routes */}
<Route element={<MainLayout />}>
<Route path="/" element={<Chat />} />
<Route path="/" element={<Dashboard />} />
<Route path="/chat" element={<Chat />} />
<Route path="/models" element={<Models />} />
<Route path="/agents" element={<Agents />} />
<Route path="/channels" element={<Channels />} />
<Route path="/skills" element={<Skills />} />
<Route path="/cron" element={<Cron />} />
<Route path="/management" element={<Management />} />
<Route path="/security" element={<Security />} />
<Route path="/faq" element={<Faq />} />
<Route path="/image-generation" element={devModeUnlocked ? <ImageGenerationPage /> : <Navigate to="/" replace />} />
<Route path="/dreams" element={devModeUnlocked ? <Dreams /> : <Navigate to="/" replace />} />
<Route path="/settings/*" element={<Settings />} />

View File

@@ -24,6 +24,8 @@ import {
ImagePlus,
Moon,
ChevronRight,
LayoutDashboard,
MessageSquare,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { rendererExtensionRegistry } from '@/extensions/registry';
@@ -157,7 +159,7 @@ export function Sidebar() {
}, [isMac, sidebarCollapsed]);
const navigate = useNavigate();
const isOnChat = useLocation().pathname === '/';
const isOnChat = useLocation().pathname === '/chat';
const getSessionLabel = (key: string, displayName?: string, label?: string) =>
sessionLabels[key] ?? label ?? displayName ?? key;
@@ -309,6 +311,8 @@ export function Sidebar() {
const extraNavItems = rendererExtensionRegistry.getExtraNavItems();
const coreNavItems = [
{ to: '/', icon: <LayoutDashboard className="h-4 w-4" strokeWidth={2} />, label: t('sidebar.dashboard'), testId: 'sidebar-nav-dashboard' },
{ to: '/chat', icon: <MessageSquare className="h-4 w-4" strokeWidth={2} />, label: t('sidebar.chat'), testId: 'sidebar-nav-chat' },
{ to: '/models', icon: <Cpu className="h-4 w-4" strokeWidth={2} />, label: t('sidebar.models'), testId: 'sidebar-nav-models' },
{ to: '/agents', icon: <Bot className="h-4 w-4" strokeWidth={2} />, label: t('sidebar.agents'), testId: 'sidebar-nav-agents' },
{ to: '/channels', icon: <Network className="h-4 w-4" strokeWidth={2} />, label: t('sidebar.channels'), testId: 'sidebar-nav-channels' },
@@ -391,7 +395,7 @@ export function Sidebar() {
onClick={() => {
const { messages } = useChatStore.getState();
if (messages.length > 0) newSession();
navigate('/');
navigate('/chat');
}}
className={cn(
'sidebar-nav-text flex items-center gap-2 rounded-lg px-2.5 py-2 transition-colors',
@@ -482,7 +486,7 @@ export function Sidebar() {
} else {
switchSession(s.key);
}
navigate('/');
navigate('/chat');
}}
onDoubleClick={() => handleStartRename(s.key, sessionLabel)}
className={cn(

View File

@@ -6,7 +6,7 @@
"skills": "Skills",
"agents": "Agents",
"channels": "Channels",
"dashboard": "Dashboard",
"dashboard": "Home",
"settings": "Settings",
"devConsole": "Developer Console",
"models": "Models",

View File

@@ -1,4 +1,50 @@
{
"title": "89cat",
"subtitle": "Your AI Smart Assistant",
"cards": {
"management": {
"title": "Management",
"desc": "Start or stop 89cat, access super admin console"
},
"tokenRecharge": {
"title": "Token Recharge",
"desc": "Visit 89.is89.com for token recharge"
},
"plugins": {
"title": "Plugins",
"desc": "Install and manage 89 plugins, OpenClaw skills"
},
"security": {
"title": "Security Check",
"desc": "Update virus definitions and scan system security"
},
"apiKey": {
"title": "Work Key",
"desc": "Enter your API key to get started, no URL needed"
},
"faq": {
"title": "FAQ",
"desc": "View usage help and frequently asked questions"
},
"language": {
"title": "Language",
"desc": "Switch the interface display language"
}
},
"apiKey": {
"placeholder": "Enter API Key",
"validate": "Validate",
"valid": "Key verified successfully",
"invalidKey": "Invalid key, please check and try again",
"validateFailed": "Validation failed, please check network"
},
"gatewayRunning": "89cat Running",
"gatewayStopped": "89cat Stopped",
"stop89cat": "Stop 89cat",
"start89cat": "Start 89cat",
"adminConsole": "89 Super AI Brain Management Center",
"adminConsoleDesc": "Management center runs in the cloud to protect your privacy",
"openAdminConsole": "Open Admin Console",
"gateway": "Gateway",
"channels": "Channels",
"skills": "Skills",
@@ -110,5 +156,55 @@
"relayModelRequired": "Model name is required",
"relayModelInvalid": "Enter only the model name, for example gpt-image-2. Do not include provider/model."
}
},
"security": {
"virusDbVersion": "Virus DB Version",
"lastScan": "Last Scan",
"updateDb": "Update Virus DB",
"startScan": "Start Scan",
"idle": "Waiting for virus database update",
"updatingDb": "Updating virus database...",
"readyToScan": "Virus database ready",
"scanning": "Scanning...",
"scanComplete": "Scan Complete",
"filesScanned": "Files Scanned",
"threatsFound": "{{count}} threat(s) found",
"noThreats": "No threats found",
"neverScanned": "Never scanned",
"updateDbFirst": "Please update virus database first",
"clickToScan": "Virus database is ready, you can start scanning",
"scanResult": "Scan completed",
"scanSummary": "Scanned {{files}} files in {{time}} seconds"
},
"faq": {
"sections": {
"install": "Installation",
"usage": "Usage",
"key": "API Key",
"security": "Security",
"other": "Other"
},
"q": {
"howToInstall": "How to install 89cat?",
"systemRequirements": "What are the system requirements?",
"howToUse": "How to start using 89cat?",
"stopRestart": "How to stop or restart 89cat?",
"whereToGetKey": "Where to get an API Key?",
"keyNotWorking": "What if my key shows as invalid?",
"isMyDataSafe": "Is my data safe?",
"howToScan": "How to run a security check?",
"contactUs": "How to contact support?"
},
"a": {
"howToInstall": "Download the installer for your platform from the official website, double-click to run and it will complete installation automatically. After installation, 89cat icons will appear on your desktop and start menu.",
"systemRequirements": "Supports Windows 10+, macOS 11+, Linux (Ubuntu 20.04+). Minimum 4GB RAM, 8GB recommended. 1GB free disk space required.",
"howToUse": "Enter your API Key in the \"Work Key\" card on the home page and click Validate. Once verified, you can chat with AI on the Chat page.",
"stopRestart": "Go to the \"Management\" page, click \"Stop 89cat\" to stop the service, and click \"Start 89cat\" to restart.",
"whereToGetKey": "Visit 89.is89.com to register an account and obtain an API Key.",
"keyNotWorking": "Check if the key is entered correctly (case-sensitive and no spaces), confirm it hasn't expired, and verify network connectivity. Contact support if issues persist.",
"isMyDataSafe": "89cat securely stores your API Key using the system's native keychain. The management center runs in the cloud while your personal data stays local, ensuring full privacy protection.",
"howToScan": "Go to the \"Security Check\" page, click \"Update Virus DB\" first, then click \"Start Scan\" to perform a full system security scan.",
"contactUs": "Send email to public@valuecell.ai or visit 89.is89.com for more help."
}
}
}

View File

@@ -6,7 +6,7 @@
"skills": "技能",
"agents": "Agents",
"channels": "频道",
"dashboard": "仪表盘",
"dashboard": "首页",
"settings": "设置",
"devConsole": "开发者控制台",
"models": "模型",

View File

@@ -1,4 +1,50 @@
{
"title": "89猫",
"subtitle": "你的AI智能助手",
"cards": {
"management": {
"title": "管理中心",
"desc": "停止或启动89猫访问超级管理后台"
},
"tokenRecharge": {
"title": "Token充值",
"desc": "前往89.is89.com进行Token充值"
},
"plugins": {
"title": "插件安装",
"desc": "安装和管理89插件、OpenClaw技能"
},
"security": {
"title": "安全检查",
"desc": "升级病毒库并扫描系统安全"
},
"apiKey": {
"title": "工作钥匙",
"desc": "输入API Key即可开始工作无需填写网址"
},
"faq": {
"title": "常见问题",
"desc": "查看使用帮助和常见问题解答"
},
"language": {
"title": "语言选择",
"desc": "切换界面显示语言"
}
},
"apiKey": {
"placeholder": "请输入API Key",
"validate": "验证",
"valid": "Key验证通过",
"invalidKey": "Key无效请检查后重试",
"validateFailed": "验证失败,请检查网络连接"
},
"gatewayRunning": "89猫运行中",
"gatewayStopped": "89猫已停止",
"stop89cat": "停止使用89猫",
"start89cat": "打开使用89猫",
"adminConsole": "89超级人工智能脑管理中心",
"adminConsoleDesc": "管理中心运行在云端,保护您的个人隐私",
"openAdminConsole": "前往管理中心",
"gateway": "网关",
"channels": "频道",
"skills": "技能",
@@ -110,5 +156,55 @@
"relayModelRequired": "请填写模型名",
"relayModelInvalid": "请只填写模型名,例如 gpt-image-2不要填写 provider/model。"
}
},
"security": {
"virusDbVersion": "病毒库版本",
"lastScan": "上次扫描时间",
"updateDb": "升级病毒库",
"startScan": "开始扫描",
"idle": "等待升级病毒库",
"updatingDb": "正在升级病毒库...",
"readyToScan": "病毒库已就绪",
"scanning": "正在扫描中...",
"scanComplete": "扫描完成",
"filesScanned": "扫描文件数",
"threatsFound": "发现 {{count}} 个威胁",
"noThreats": "未发现威胁",
"neverScanned": "暂未扫描",
"updateDbFirst": "请先升级病毒库",
"clickToScan": "病毒库已就绪,可以开始扫描",
"scanResult": "扫描已完成",
"scanSummary": "共扫描 {{files}} 个文件,耗时 {{time}} 秒"
},
"faq": {
"sections": {
"install": "安装相关",
"usage": "使用相关",
"key": "Key相关",
"security": "安全相关",
"other": "其他"
},
"q": {
"howToInstall": "如何安装89猫",
"systemRequirements": "89猫的系统要求是什么",
"howToUse": "如何开始使用89猫",
"stopRestart": "如何停止或重启89猫",
"whereToGetKey": "从哪里获取API Key",
"keyNotWorking": "Key显示无效怎么办",
"isMyDataSafe": "我的数据安全吗?",
"howToScan": "如何进行安全检查?",
"contactUs": "如何联系客服?"
},
"a": {
"howToInstall": "从官方网站下载对应平台的安装包双击运行即可自动完成安装。安装完成后桌面和开始菜单都会出现89猫图标。",
"systemRequirements": "支持 Windows 10+、macOS 11+、Linux (Ubuntu 20.04+)。最低需要4GB内存推荐8GB。需要1GB可用磁盘空间。",
"howToUse": "在首页的\"工作钥匙\"卡片中输入你的API Key并点击验证验证通过后即可在聊天页面与AI对话。",
"stopRestart": "进入\"管理中心\"页面,点击\"停止使用89猫\"按钮即可停止服务,再次点击\"打开使用89猫\"即可重新启动。",
"whereToGetKey": "请访问 89.is89.com 注册账号并获取API Key。",
"keyNotWorking": "请检查Key是否输入正确注意大小写和空格确认Key未过期确认网络连接正常。如仍有问题请联系客服。",
"isMyDataSafe": "89猫使用系统原生密钥链安全存储你的API Key。管理中心运行在云端你的个人数据保留在本地隐私受到充分保护。",
"howToScan": "进入\"安全检查\"页面,先点击\"升级病毒库\",升级完成后点击\"开始扫描\"即可对系统进行全面安全扫描。",
"contactUs": "请发送邮件至 public@valuecell.ai 或访问 89.is89.com 获取更多帮助。"
}
}
}

View File

@@ -0,0 +1,273 @@
/**
* Dashboard page — main landing page with 7 feature entry cards
*/
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import {
Building2,
Coins,
Puzzle,
ShieldCheck,
KeyRound,
HelpCircle,
Globe,
Eye,
EyeOff,
Check,
X,
Loader2,
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { useSettingsStore } from '@/stores/settings';
import { SUPPORTED_LANGUAGES } from '@/i18n';
import { hostApiFetch } from '@/lib/host-api';
import { cn } from '@/lib/utils';
function ApiKeyCard() {
const { t } = useTranslation('dashboard');
const [showKey, setShowKey] = useState(false);
const [key, setKey] = useState('');
const [validating, setValidating] = useState(false);
const [result, setResult] = useState<'idle' | 'valid' | 'invalid'>('idle');
const [errorMsg, setErrorMsg] = useState('');
const handleValidate = async () => {
const trimmed = key.trim();
if (!trimmed) return;
setValidating(true);
setResult('idle');
setErrorMsg('');
try {
const res = await hostApiFetch<{ valid: boolean; message?: string }>(
'/api/provider-accounts/validate',
{
method: 'POST',
body: JSON.stringify({
vendorId: 'cat89',
apiKey: trimmed,
}),
},
);
if (res.valid) {
setResult('valid');
} else {
setResult('invalid');
setErrorMsg(res.message || t('apiKey.invalidKey'));
}
} catch {
setResult('invalid');
setErrorMsg(t('apiKey.validateFailed'));
} finally {
setValidating(false);
}
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
void handleValidate();
}
};
return (
<div className="space-y-3">
<div className="flex items-center gap-2">
<div className="relative flex-1">
<Input
type={showKey ? 'text' : 'password'}
placeholder={t('apiKey.placeholder')}
value={key}
onChange={(e) => {
setKey(e.target.value);
setResult('idle');
setErrorMsg('');
}}
onKeyDown={handleKeyDown}
className={cn(
'pr-10 font-mono',
result === 'valid' && 'border-green-500 focus-visible:ring-green-500',
result === 'invalid' && 'border-red-500 focus-visible:ring-red-500',
)}
/>
<button
type="button"
onClick={() => setShowKey(!showKey)}
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
{showKey ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
<Button
size="sm"
onClick={() => { void handleValidate(); }}
disabled={validating || !key.trim()}
className="shrink-0"
>
{validating ? <Loader2 className="h-4 w-4 animate-spin" /> : t('apiKey.validate')}
</Button>
</div>
{result === 'valid' && (
<div className="flex items-center gap-1 text-sm text-green-600 dark:text-green-400">
<Check className="h-4 w-4" />
<span>{t('apiKey.valid')}</span>
</div>
)}
{result === 'invalid' && (
<div className="flex items-center gap-1 text-sm text-red-600 dark:text-red-400">
<X className="h-4 w-4" />
<span>{errorMsg}</span>
</div>
)}
</div>
);
}
interface DashboardCardProps {
icon: React.ReactNode;
title: string;
description: string;
onClick: () => void;
testId?: string;
}
function DashboardCard({ icon, title, description, onClick, testId }: DashboardCardProps) {
return (
<button
data-testid={testId}
onClick={onClick}
className={cn(
'group flex flex-col items-center gap-3 rounded-2xl border border-border/60 p-6',
'bg-card text-left transition-all',
'hover:border-primary/30 hover:bg-accent/30 hover:shadow-md',
'active:scale-[0.98]',
)}
>
<div className="flex h-14 w-14 items-center justify-center rounded-xl bg-primary/10 text-primary transition-colors group-hover:bg-primary/20 group-hover:scale-110">
{icon}
</div>
<div className="text-center">
<h3 className="text-base font-semibold text-foreground">{title}</h3>
<p className="mt-1 text-sm text-muted-foreground">{description}</p>
</div>
</button>
);
}
export function Dashboard() {
const navigate = useNavigate();
const { t } = useTranslation('dashboard');
const language = useSettingsStore((s) => s.language);
const setLanguage = useSettingsStore((s) => s.setLanguage);
// Filter to only zh and en for 89猫
const visibleLanguages = SUPPORTED_LANGUAGES.filter(
(lang) => lang.code === 'zh' || lang.code === 'en',
);
const cards: DashboardCardProps[] = [
{
icon: <Building2 className="h-7 w-7" strokeWidth={1.5} />,
title: t('cards.management.title'),
description: t('cards.management.desc'),
onClick: () => navigate('/management'),
testId: 'dashboard-card-management',
},
{
icon: <Coins className="h-7 w-7" strokeWidth={1.5} />,
title: t('cards.tokenRecharge.title'),
description: t('cards.tokenRecharge.desc'),
onClick: () => window.electron.openExternal('https://89.is89.com'),
testId: 'dashboard-card-token',
},
{
icon: <Puzzle className="h-7 w-7" strokeWidth={1.5} />,
title: t('cards.plugins.title'),
description: t('cards.plugins.desc'),
onClick: () => navigate('/skills'),
testId: 'dashboard-card-plugins',
},
{
icon: <ShieldCheck className="h-7 w-7" strokeWidth={1.5} />,
title: t('cards.security.title'),
description: t('cards.security.desc'),
onClick: () => navigate('/security'),
testId: 'dashboard-card-security',
},
{
icon: <HelpCircle className="h-7 w-7" strokeWidth={1.5} />,
title: t('cards.faq.title'),
description: t('cards.faq.desc'),
onClick: () => navigate('/faq'),
testId: 'dashboard-card-faq',
},
];
return (
<div className="mx-auto max-w-3xl space-y-8 py-4">
{/* Header */}
<div className="text-center">
<h1 className="text-3xl font-serif font-normal tracking-tight text-foreground">
{t('title')}
</h1>
<p className="mt-2 text-muted-foreground">{t('subtitle')}</p>
</div>
{/* Feature Cards Grid */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{cards.map((card) => (
<DashboardCard key={card.testId} {...card} />
))}
{/* API Key Card — inline with input */}
<div className="flex flex-col gap-3 rounded-2xl border border-border/60 bg-card p-6 sm:col-span-2 lg:col-span-3">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-primary/10 text-primary">
<KeyRound className="h-5 w-5" strokeWidth={1.5} />
</div>
<div>
<h3 className="text-base font-semibold text-foreground">
{t('cards.apiKey.title')}
</h3>
<p className="text-sm text-muted-foreground">
{t('cards.apiKey.desc')}
</p>
</div>
</div>
<ApiKeyCard />
</div>
{/* Language Selector Card */}
<div className="flex flex-col items-center gap-3 rounded-2xl border border-border/60 bg-card p-6 sm:col-span-2 lg:col-span-3">
<div className="flex items-center gap-3 self-start">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-primary/10 text-primary">
<Globe className="h-5 w-5" strokeWidth={1.5} />
</div>
<div className="text-left">
<h3 className="text-base font-semibold text-foreground">
{t('cards.language.title')}
</h3>
<p className="text-sm text-muted-foreground">
{t('cards.language.desc')}
</p>
</div>
</div>
<div className="flex w-full flex-wrap gap-2">
{visibleLanguages.map((lang) => (
<Button
key={lang.code}
variant={language === lang.code ? 'default' : 'outline'}
size="sm"
onClick={() => setLanguage(lang.code)}
>
{lang.label}
</Button>
))}
</div>
</div>
</div>
</div>
);
}

187
src/pages/Faq/index.tsx Normal file
View File

@@ -0,0 +1,187 @@
/**
* FAQ page — accordion-style Q&A grouped by category
*/
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ChevronDown, HelpCircle } from 'lucide-react';
import { cn } from '@/lib/utils';
interface FaqItem {
q: string;
a: string;
}
interface FaqSection {
title: string;
items: FaqItem[];
}
function FaqAccordion({ item, defaultOpen }: { item: FaqItem; defaultOpen: boolean }) {
const [open, setOpen] = useState(defaultOpen);
return (
<div className="rounded-xl border border-border/60 bg-card overflow-hidden">
<button
type="button"
onClick={() => setOpen(!open)}
className="flex w-full items-center justify-between gap-3 px-5 py-4 text-left transition-colors hover:bg-accent/30"
>
<span className="text-sm font-medium text-foreground">{item.q}</span>
<ChevronDown
className={cn(
'h-4 w-4 shrink-0 text-muted-foreground transition-transform',
open && 'rotate-180',
)}
/>
</button>
{open && (
<div className="px-5 pb-4">
<p className="text-sm text-muted-foreground leading-relaxed whitespace-pre-wrap">
{item.a}
</p>
</div>
)}
</div>
);
}
function FaqSectionBlock({ section, defaultOpen }: { section: FaqSection; defaultOpen: boolean }) {
const [open, setOpen] = useState(defaultOpen);
return (
<div className="space-y-3">
<button
type="button"
onClick={() => setOpen(!open)}
className="flex w-full items-center gap-2 text-left"
>
<ChevronDown
className={cn(
'h-4 w-4 text-muted-foreground transition-transform',
open && 'rotate-180',
)}
/>
<h2 className="text-lg font-semibold text-foreground">{section.title}</h2>
</button>
{open && (
<div className="space-y-2">
{section.items.map((item, idx) => (
<FaqAccordion key={idx} item={item} defaultOpen={idx === 0 && defaultOpen} />
))}
</div>
)}
</div>
);
}
export function Faq() {
const { t } = useTranslation('dashboard');
const sections: FaqSection[] = [
{
title: t('faq.sections.install', '安装相关'),
items: [
{
q: t('faq.q.howToInstall', '如何安装89猫'),
a: t(
'faq.a.howToInstall',
'从官方网站下载对应平台的安装包双击运行即可自动完成安装。安装完成后桌面和开始菜单都会出现89猫图标。',
),
},
{
q: t('faq.q.systemRequirements', '89猫的系统要求是什么'),
a: t(
'faq.a.systemRequirements',
'支持 Windows 10+、macOS 11+、Linux (Ubuntu 20.04+)。最低需要4GB内存推荐8GB。需要1GB可用磁盘空间。',
),
},
],
},
{
title: t('faq.sections.usage', '使用相关'),
items: [
{
q: t('faq.q.howToUse', '如何开始使用89猫'),
a: t(
'faq.a.howToUse',
'在首页的"工作钥匙"卡片中输入你的API Key并点击验证验证通过后即可在聊天页面与AI对话。',
),
},
{
q: t('faq.q.stopRestart', '如何停止或重启89猫'),
a: t(
'faq.a.stopRestart',
'进入"管理中心"页面,点击"停止使用89猫"按钮即可停止服务,再次点击"打开使用89猫"即可重新启动。',
),
},
],
},
{
title: t('faq.sections.key', 'Key相关'),
items: [
{
q: t('faq.q.whereToGetKey', '从哪里获取API Key'),
a: t('faq.a.whereToGetKey', '请访问 89.is89.com 注册账号并获取API Key。'),
},
{
q: t('faq.q.keyNotWorking', 'Key显示无效怎么办'),
a: t(
'faq.a.keyNotWorking',
'请检查Key是否输入正确注意大小写和空格确认Key未过期确认网络连接正常。如仍有问题请联系客服。',
),
},
],
},
{
title: t('faq.sections.security', '安全相关'),
items: [
{
q: t('faq.q.isMyDataSafe', '我的数据安全吗?'),
a: t(
'faq.a.isMyDataSafe',
'89猫使用系统原生密钥链安全存储你的API Key。管理中心运行在云端你的个人数据保留在本地隐私受到充分保护。',
),
},
{
q: t('faq.q.howToScan', '如何进行安全检查?'),
a: t(
'faq.a.howToScan',
'进入"安全检查"页面,先点击"升级病毒库",升级完成后点击"开始扫描"即可对系统进行全面安全扫描。',
),
},
],
},
{
title: t('faq.sections.other', '其他'),
items: [
{
q: t('faq.q.contactUs', '如何联系客服?'),
a: t(
'faq.a.contactUs',
'请发送邮件至 public@valuecell.ai 或访问 89.is89.com 获取更多帮助。',
),
},
],
},
];
return (
<div className="mx-auto max-w-2xl space-y-8 py-4">
<div className="text-center">
<div className="flex items-center justify-center gap-2">
<HelpCircle className="h-7 w-7 text-primary" strokeWidth={1.5} />
<h1 className="text-3xl font-serif font-normal tracking-tight text-foreground">
{t('cards.faq.title')}
</h1>
</div>
<p className="mt-2 text-muted-foreground">{t('cards.faq.desc')}</p>
</div>
<div className="space-y-6">
{sections.map((section, idx) => (
<FaqSectionBlock key={idx} section={section} defaultOpen={idx === 0} />
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,92 @@
/**
* Management Center page — Gateway control + admin console link
*/
import { useTranslation } from 'react-i18next';
import { ExternalLink, Power, PowerOff, Loader2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { useGatewayStore } from '@/stores/gateway';
import { cn } from '@/lib/utils';
export function Management() {
const { t } = useTranslation('dashboard');
const gatewayStatus = useGatewayStore((s) => s.status);
const startGateway = useGatewayStore((s) => s.start);
const stopGateway = useGatewayStore((s) => s.stop);
const isRunning = gatewayStatus.state === 'running';
const isTransitioning = gatewayStatus.state === 'starting';
const handleToggle = async () => {
if (isRunning) {
await stopGateway();
} else {
await startGateway();
}
};
return (
<div className="mx-auto max-w-2xl space-y-8 py-4">
<div className="text-center">
<h1 className="text-3xl font-serif font-normal tracking-tight text-foreground">
{t('cards.management.title')}
</h1>
<p className="mt-2 text-muted-foreground">{t('cards.management.desc')}</p>
</div>
{/* Gateway Toggle Card */}
<div className="rounded-2xl border border-border/60 bg-card p-8 text-center space-y-4">
<div className="flex items-center justify-center gap-2">
<Badge
variant="outline"
className={cn(
'px-3 py-1 text-sm',
isRunning && 'border-green-500 text-green-600 dark:text-green-400',
!isRunning && 'border-red-500 text-red-600 dark:text-red-400',
)}
>
{isRunning ? t('gatewayRunning', '89猫运行中') : t('gatewayStopped', '89猫已停止')}
</Badge>
</div>
<Button
size="lg"
variant={isRunning ? 'destructive' : 'default'}
disabled={isTransitioning}
onClick={() => { void handleToggle(); }}
className="min-w-40"
>
{isTransitioning ? (
<Loader2 className="h-5 w-5 animate-spin" />
) : isRunning ? (
<PowerOff className="h-5 w-5" />
) : (
<Power className="h-5 w-5" />
)}
<span className="ml-2">
{isRunning
? t('stop89cat', '停止使用89猫')
: t('start89cat', '打开使用89猫')}
</span>
</Button>
</div>
{/* Admin Console Link */}
<div className="rounded-2xl border border-border/60 bg-card p-8 text-center space-y-4">
<h2 className="text-xl font-semibold text-foreground">
{t('adminConsole', '89超级人工智能脑管理中心')}
</h2>
<p className="text-sm text-muted-foreground">
{t('adminConsoleDesc', '管理中心运行在云端,保护您的个人隐私')}
</p>
<Button
size="lg"
variant="outline"
onClick={() => window.electron.openExternal('https://89.is89.com')}
>
<ExternalLink className="h-5 w-5" />
<span className="ml-2">{t('openAdminConsole', '前往管理中心')}</span>
</Button>
</div>
</div>
);
}

View File

@@ -0,0 +1,260 @@
/**
* Security Check page — static UI with simulated scan flow
*/
import { useState, useCallback, useRef, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { ShieldCheck, Download, Play, Check, Loader2, Clock, FileSearch } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Progress } from '@/components/ui/progress';
import { cn } from '@/lib/utils';
type ScanStep = 'idle' | 'updating' | 'ready' | 'scanning' | 'done';
const SCAN_PATHS = [
'C:\\Windows\\System32\\',
'C:\\Program Files\\',
'C:\\Users\\Documents\\',
'D:\\Downloads\\',
'C:\\Temp\\',
];
const getRandomThreats = () => Math.floor(Math.random() * 5);
export function Security() {
const { t } = useTranslation('dashboard');
const [step, setStep] = useState<ScanStep>('idle');
const [updateProgress, setUpdateProgress] = useState(0);
const [scanProgress, setScanProgress] = useState(0);
const [currentPath, setCurrentPath] = useState('');
const [filesScanned, setFilesScanned] = useState(0);
const [threatsFound, setThreatsFound] = useState(0);
const [scanTime, setScanTime] = useState(0);
const [lastScanTime, setLastScanTime] = useState<string>(t('security.neverScanned'));
const [virusDbVersion, setVirusDbVersion] = useState('2026.06.01-v1');
const updateTimerRef = useRef<ReturnType<typeof setInterval>>();
const scanTimerRef = useRef<ReturnType<typeof setInterval>>();
useEffect(() => {
return () => {
clearInterval(updateTimerRef.current);
clearInterval(scanTimerRef.current);
};
}, []);
const handleUpdateDb = useCallback(() => {
setStep('updating');
setUpdateProgress(0);
updateTimerRef.current = setInterval(() => {
setUpdateProgress((prev) => {
if (prev >= 100) {
clearInterval(updateTimerRef.current);
setStep('ready');
setVirusDbVersion(`2026.06.04-v${Math.floor(Math.random() * 10) + 1}`);
return 100;
}
return prev + Math.random() * 15 + 5;
});
}, 300);
}, []);
const handleStartScan = useCallback(() => {
setStep('scanning');
setScanProgress(0);
setFilesScanned(0);
setThreatsFound(0);
setScanTime(0);
setCurrentPath(SCAN_PATHS[0]);
const startTime = Date.now();
let pathIndex = 0;
scanTimerRef.current = setInterval(() => {
setScanProgress((prev) => {
const next = prev + Math.random() * 4 + 1;
if (next >= 100) {
clearInterval(scanTimerRef.current);
setStep('done');
setFilesScanned(Math.floor(Math.random() * 5000) + 10000);
setThreatsFound(getRandomThreats());
setScanTime(Math.floor((Date.now() - startTime) / 1000));
setLastScanTime(new Date().toLocaleString());
setCurrentPath('');
return 100;
}
// Rotate scan paths
const newPathIndex = Math.min(
Math.floor((next / 100) * SCAN_PATHS.length),
SCAN_PATHS.length - 1,
);
if (newPathIndex !== pathIndex) {
pathIndex = newPathIndex;
setCurrentPath(SCAN_PATHS[pathIndex]);
}
setFilesScanned((prev2) => prev2 + Math.floor(Math.random() * 50) + 10);
return next;
});
}, 200);
}, []);
const getStepLabel = () => {
switch (step) {
case 'idle':
return t('security.idle');
case 'updating':
return t('security.updatingDb');
case 'ready':
return t('security.readyToScan');
case 'scanning':
return t('security.scanning');
case 'done':
return t('security.scanComplete');
}
};
return (
<div className="mx-auto max-w-2xl space-y-8 py-4">
<div className="text-center">
<h1 className="text-3xl font-serif font-normal tracking-tight text-foreground">
{t('cards.security.title')}
</h1>
<p className="mt-2 text-muted-foreground">{t('cards.security.desc')}</p>
</div>
{/* Virus DB Status */}
<div className="rounded-2xl border border-border/60 bg-card p-6 space-y-4">
<div className="flex items-center justify-between">
<div>
<h3 className="font-semibold text-foreground">{t('security.virusDbVersion')}</h3>
<p className="text-sm text-muted-foreground">{virusDbVersion}</p>
</div>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Clock className="h-4 w-4" />
<span>
{t('security.lastScan')}: {lastScanTime}
</span>
</div>
</div>
<Button
onClick={handleUpdateDb}
disabled={step === 'updating' || step === 'scanning'}
className="w-full"
variant="outline"
>
{step === 'updating' ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Download className="h-4 w-4" />
)}
<span className="ml-2">{t('security.updateDb')}</span>
</Button>
{step === 'updating' && (
<div className="space-y-1">
<Progress value={updateProgress} className="h-2" />
<p className="text-xs text-muted-foreground text-right">
{Math.floor(updateProgress)}%
</p>
</div>
)}
</div>
{/* Scan Section */}
<div className="rounded-2xl border border-border/60 bg-card p-6 space-y-4">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-primary/10 text-primary">
<ShieldCheck className="h-5 w-5" strokeWidth={1.5} />
</div>
<div>
<h3 className="font-semibold text-foreground">{getStepLabel()}</h3>
<p className="text-sm text-muted-foreground">
{step === 'idle' && t('security.updateDbFirst', '请先升级病毒库')}
{step === 'ready' && t('security.clickToScan', '病毒库已就绪,可以开始扫描')}
{step === 'scanning' && currentPath}
{step === 'done' && t('security.scanResult', '扫描已完成')}
</p>
</div>
</div>
<Button
onClick={handleStartScan}
disabled={step !== 'ready' || step === 'scanning'}
className="w-full"
variant={step === 'done' ? 'outline' : 'default'}
>
{step === 'scanning' ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Play className="h-4 w-4" />
)}
<span className="ml-2">{t('security.startScan')}</span>
</Button>
{/* Scan Progress */}
{(step === 'scanning' || step === 'done') && (
<div className="space-y-3">
<Progress
value={scanProgress}
className={cn('h-2', step === 'done' && '[&>div]:bg-green-500')}
/>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">
<FileSearch className="inline h-3.5 w-3.5 mr-1" />
{t('security.filesScanned')}: {filesScanned.toLocaleString()}
</span>
<span className="text-muted-foreground">
{Math.floor(scanProgress)}%
</span>
</div>
</div>
)}
{/* Result */}
{step === 'done' && (
<div
className={cn(
'rounded-xl border p-4 text-center',
threatsFound > 0
? 'border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-950'
: 'border-green-200 bg-green-50 dark:border-green-800 dark:bg-green-950',
)}
>
<div className="flex items-center justify-center gap-2">
<Check
className={cn(
'h-5 w-5',
threatsFound > 0
? 'text-red-600 dark:text-red-400'
: 'text-green-600 dark:text-green-400',
)}
/>
<span
className={cn(
'font-semibold',
threatsFound > 0
? 'text-red-700 dark:text-red-300'
: 'text-green-700 dark:text-green-300',
)}
>
{threatsFound > 0
? t('security.threatsFound', { count: threatsFound })
: t('security.noThreats')}
</span>
</div>
<p className="mt-1 text-sm text-muted-foreground">
{t('security.scanSummary', {
files: filesScanned.toLocaleString(),
time: scanTime,
})}
</p>
</div>
)}
</div>
</div>
);
}