import type { ReactElement } from 'react' import { useEffect, useState } from 'react' import { Button, Card, Form, Input, Space, Typography } from '@arco-design/web-react' import { IconBook, IconCheckCircleFill, IconCloseCircleFill, IconLaunch, IconLoading, IconSafe, IconUserGroup, } from '@arco-design/web-react/icon' const { Title, Text, Paragraph } = Typography type ConnectionStatus = 'checking' | 'online' | 'offline' export function LoginPage({ onLogin }: { onLogin: (input: { server: string; email: string; password: string }) => void }) { const [server, setServer] = useState('http://localhost:9150') const [email, setEmail] = useState('demo@senlin.ai') const [password, setPassword] = useState('password123') const [status, setStatus] = useState('checking') useEffect(() => { const controller = new AbortController() const timer = window.setTimeout(() => { void checkServerStatus(server, controller.signal, setStatus) }, 300) return () => { controller.abort() window.clearTimeout(timer) } }, [server]) return (
S 森林Agent
私有部署 · 安全可控的智能协作平台 知识沉淀 · 团队协作 · AI Agent v1.0.0 隐私政策 服务协议
登录到您的 SenlinAI 连接私有工作台
} /> checkServerStatus(server, undefined, setStatus)} />
记住服务器地址
安全 · 专注 · 高效 SenlinAI 是面向知识工作者与内部团队的私有部署工作台,帮助团队在统一空间中收集信息、整理知识、协同决策。 } title="私有部署,数据自有" desc="所有数据与模型运行在您的环境中,安全可控。" /> } title="知识沉淀,结构清晰" desc="从收集到整理,形成可复用的团队知识库。" /> } title="协同高效,聚焦成果" desc="围绕项目与任务协同,减少沟通成本,专注交付。" />
) } function ConnectionCard({ status, onCheck }: { status: ConnectionStatus; onCheck: () => void }) { const icon = status === 'checking' ? : status === 'online' ? : const title = status === 'checking' ? '正在检查' : status === 'online' ? '连接正常' : '连接异常' return (
{icon}
{title}
) } async function checkServerStatus( server: string, signal: AbortSignal | undefined, setStatus: (status: ConnectionStatus) => void, ) { const baseUrl = normalizeBaseUrl(server) setStatus('checking') try { const response = await fetch(`${baseUrl}/api/status`, { signal }) if (!response.ok) throw new Error(`HTTP ${response.status}`) const payload = await response.json() as { timestamp_ms?: number } if (typeof payload.timestamp_ms !== 'number') throw new Error('invalid status payload') setStatus('online') } catch (error) { if (error instanceof DOMException && error.name === 'AbortError') return setStatus('offline') } } function normalizeBaseUrl(value: string) { const trimmed = value.trim() return trimmed.endsWith('/') ? trimmed.slice(0, -1) : trimmed } function ValuePoint({ icon, title, desc }: { icon: ReactElement; title: string; desc: string }) { return (
{icon}
{title} {desc}
) }