Files
agent/apps/web_v1/src/pages/login.tsx

148 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<ConnectionStatus>('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 (
<section className="login-screen">
<Card className="login-card" bordered>
<div className="login-brand-panel">
<div className="brand-lockup large">
<span className="brand-symbol">S</span>
<Title heading={2}>Agent</Title>
</div>
<Text className="login-slogan"> · </Text>
<Text type="secondary"> · · AI Agent</Text>
<Space className="login-footer-links" size={24}>
<Text type="secondary">v1.0.0</Text>
<Text type="secondary"></Text>
<Text type="secondary"></Text>
</Space>
</div>
<div className="login-form-panel">
<Title heading={4}> SenlinAI</Title>
<Text type="secondary"></Text>
<Form layout="vertical" className="login-form">
<Form.Item label="服务器地址IP 或域名优先)">
<Input value={server} onChange={setServer} placeholder="例如https://10.0.0.15:8080" suffix={<IconLaunch />} />
<ConnectionCard status={status} onCheck={() => checkServerStatus(server, undefined, setStatus)} />
</Form.Item>
<Form.Item label="账号">
<Input value={email} onChange={setEmail} placeholder="请输入用户名或邮箱" />
</Form.Item>
<Form.Item label="密码">
<Input.Password value={password} onChange={setPassword} placeholder="请输入密码" />
</Form.Item>
<div className="login-row">
<Space size={8}>
<span className="check-dot" />
<Text></Text>
</Space>
<Button type="text" size="mini">?</Button>
</div>
<Button type="primary" long size="large" onClick={() => onLogin({ server, email, password })}>
</Button>
</Form>
</div>
<div className="login-value-panel">
<Title heading={3}> · · </Title>
<Paragraph>
SenlinAI
</Paragraph>
<ValuePoint icon={<IconSafe />} title="私有部署,数据自有" desc="所有数据与模型运行在您的环境中,安全可控。" />
<ValuePoint icon={<IconBook />} title="知识沉淀,结构清晰" desc="从收集到整理,形成可复用的团队知识库。" />
<ValuePoint icon={<IconUserGroup />} title="协同高效,聚焦成果" desc="围绕项目与任务协同,减少沟通成本,专注交付。" />
</div>
</Card>
</section>
)
}
function ConnectionCard({ status, onCheck }: { status: ConnectionStatus; onCheck: () => void }) {
const icon = status === 'checking' ? <IconLoading /> : status === 'online' ? <IconCheckCircleFill /> : <IconCloseCircleFill />
const title = status === 'checking' ? '正在检查' : status === 'online' ? '连接正常' : '连接异常'
return (
<div className={`connection-card ${status}`}>
{icon}
<div>
<Text className="connection-title">{title}</Text>
</div>
<Button type="text" size="mini" loading={status === 'checking'} onClick={onCheck}>
</Button>
</div>
)
}
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 (
<div className="value-point">
<span>{icon}</span>
<div>
<Text className="value-title">{title}</Text>
<Text type="secondary">{desc}</Text>
</div>
</div>
)
}