1. Add 登陆功能

2. 调整字体大小
3. 新增部分功能
This commit is contained in:
wangwei
2026-06-05 18:00:31 +08:00
parent 06e0967128
commit 9fea9c6a53
58 changed files with 5028 additions and 322 deletions
@@ -5,6 +5,12 @@ import { NewAnalysisModal } from './NewAnalysisModal';
import { useComplianceAnalysis } from './useComplianceAnalysis';
import type { FindingEvent, SourceEvent, AnalysisMeta } from './useComplianceAnalysis';
const TOKEN_KEY = 'auth_token';
function authHeader(): Record<string, string> {
const t = localStorage.getItem(TOKEN_KEY);
return t ? { Authorization: `Bearer ${t}` } : {};
}
const STATUS_LABEL: Record<string, string> = { ok: 'Covered', warn: 'Gap', risk: 'Critical', info: 'Info' };
const SOURCE_TYPE_LABEL: Record<string, string> = { text: 'Pasted Text', doc: 'Indexed Document', upload: 'Uploaded File' };
@@ -71,7 +77,7 @@ function useFindingChat() {
try {
const res = await fetch(`/api/v1/compliance/chat/${findingIdx ?? 0}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: { 'Content-Type': 'application/json', ...authHeader() },
body: JSON.stringify({ query: q, segment_context: segmentContext }),
signal: ctrl.signal,
});
@@ -1,6 +1,12 @@
import { useState, useRef, useEffect } from 'react';
import { X, Upload, FileText, Database } from 'lucide-react';
const TOKEN_KEY = 'auth_token';
function authHeader(): Record<string, string> {
const t = localStorage.getItem(TOKEN_KEY);
return t ? { Authorization: `Bearer ${t}` } : {};
}
interface DocOption {
id: string;
name: string;
@@ -30,7 +36,7 @@ export function NewAnalysisModal({ onClose, onSubmit }: Props) {
// Fetch indexed docs for "From Document" tab
useEffect(() => {
fetch('/api/v1/documents/management-list')
fetch('/api/v1/documents/management-list', { headers: authHeader() })
.then(r => r.json())
.then(d => {
const list: DocOption[] = (d?.documents ?? d ?? []).map((item: Record<string, unknown>) => ({
@@ -1,5 +1,11 @@
import { useState, useCallback, useRef } from 'react';
const TOKEN_KEY = 'auth_token';
function authHeader(): Record<string, string> {
const t = localStorage.getItem(TOKEN_KEY);
return t ? { Authorization: `Bearer ${t}` } : {};
}
export type AnalysisStatus = 'idle' | 'streaming' | 'done' | 'error';
export interface SourceEvent {
@@ -78,6 +84,7 @@ export function useComplianceAnalysis() {
try {
const res = await fetch('/api/v1/compliance/analyze-stream', {
method: 'POST',
headers: authHeader(),
body: formData,
signal: ctrl.signal,
});
+9 -3
View File
@@ -3,6 +3,12 @@ import { Topbar } from '../../components/layout/Topbar';
import { Upload, Search, Download, Trash2, RefreshCw, AlertTriangle } from 'lucide-react';
import { UploadModal } from './UploadModal';
const TOKEN_KEY = 'auth_token';
function authHeader(): Record<string, string> {
const t = localStorage.getItem(TOKEN_KEY);
return t ? { Authorization: `Bearer ${t}` } : {};
}
interface Doc {
id: string;
name: string;
@@ -79,7 +85,7 @@ export function DocsPage() {
const fetchDocs = useCallback(() => {
setLoading(true);
fetch('/api/v1/documents/management-list')
fetch('/api/v1/documents/management-list', { headers: authHeader() })
.then(r => r.json())
.then(d => {
if (!Array.isArray(d?.documents)) { setLoading(false); return; }
@@ -132,7 +138,7 @@ export function DocsPage() {
async function retryDoc(id: string) {
setRetrying(r => new Set([...r, id]));
try {
await fetch(`/api/v1/documents/${id}/retry`, { method: 'POST' });
await fetch(`/api/v1/documents/${id}/retry`, { method: 'POST', headers: authHeader() });
setTimeout(() => {
setRetrying(r => { const s = new Set(r); s.delete(id); return s; });
setRefreshKey(k => k + 1);
@@ -155,7 +161,7 @@ export function DocsPage() {
setDeleting(new Set(ids));
await Promise.allSettled(
ids.map(id => fetch(`/api/v1/documents/${id}`, { method: 'DELETE' }))
ids.map(id => fetch(`/api/v1/documents/${id}`, { method: 'DELETE', headers: authHeader() }))
);
setDeleting(new Set());
+37 -30
View File
@@ -1,6 +1,12 @@
import { useState, useRef, useCallback } from 'react';
import { X, Upload } from 'lucide-react';
const TOKEN_KEY = 'auth_token';
function authHeader(): Record<string, string> {
const t = localStorage.getItem(TOKEN_KEY);
return t ? { Authorization: `Bearer ${t}` } : {};
}
interface Props {
onClose: () => void;
onComplete?: () => void; // called when all uploads finish (indexed)
@@ -44,11 +50,6 @@ function docStatusToStages(status: DocStatus): StageState[] {
}
}
// Generate a short unique ID client-side (matches backend's 8-char uuid prefix pattern)
function genDocId(): string {
return Math.random().toString(36).slice(2, 10);
}
export function UploadModal({ onClose, onComplete }: Props) {
const [files, setFiles] = useState<File[]>([]);
const [regType, setRegType] = useState(REG_TYPES[0]);
@@ -93,13 +94,14 @@ export function UploadModal({ onClose, onComplete }: Props) {
const pollStatus = useCallback((docId: string, resolve: () => void, reject: (msg: string) => void) => {
let attempts = 0;
const MAX_ATTEMPTS = 120; // 4 minutes at 2s interval
const MAX_ATTEMPTS = 450; // 15 minutes at 2s interval — Aliyun DocMind can take several minutes
stopPolling();
pollTimer.current = setInterval(async () => {
attempts++;
try {
const res = await fetch(`/api/v1/documents/status/${docId}`);
const res = await fetch(`/api/v1/documents/status/${docId}`, { headers: authHeader() });
if (!res.ok) {
// Transient HTTP error (e.g. 502 during restart) — keep polling until timeout.
if (attempts > MAX_ATTEMPTS) { stopPolling(); reject('Polling timeout'); }
return;
}
@@ -114,7 +116,7 @@ export function UploadModal({ onClose, onComplete }: Props) {
reject(data.message ?? 'Processing failed');
} else if (attempts > MAX_ATTEMPTS) {
stopPolling();
reject('Processing timeout — check Document Management for status');
reject('Processing timeout (15 min) — check Document Management for status');
}
} catch {
// network hiccup — keep polling
@@ -127,37 +129,42 @@ export function UploadModal({ onClose, onComplete }: Props) {
setCurrentFileIdx(idx);
setDocStatus('idle');
const docId = genDocId();
const form = new FormData();
form.append('file', file);
form.append('doc_id', docId);
form.append('doc_name', file.name);
form.append('regulation_type', regType);
if (version) form.append('version', version);
form.append('generate_summary', 'false');
// Fire upload this is a long-running synchronous call on the backend.
// We start polling immediately so the UI updates as the backend writes status transitions.
const uploadPromise = fetch('/api/v1/documents/upload', { method: 'POST', body: form });
// Upload the file first — response contains the authoritative doc_id.
// Without waiting here we risk polling an ID the server has not yet created.
let docId: string;
const uploadRes = await fetch('/api/v1/documents/upload', {
method: 'POST',
headers: authHeader(),
body: form,
});
if (!uploadRes.ok) {
const detail = await uploadRes.text().catch(() => uploadRes.statusText);
throw new Error(`${file.name}: ${uploadRes.status} ${detail}`);
}
const uploadData = await uploadRes.json() as { doc_id: string; status: string };
docId = uploadData.doc_id;
// Start polling after a short delay so the backend has time to create the document record
// If backend processed synchronously (sync=true or status already 'indexed'), resolve immediately.
if (uploadData.status === 'indexed') {
setDocStatus('indexed');
return;
}
if (uploadData.status === 'failed') {
setDocStatus('failed');
throw new Error(`${file.name}: Processing failed on server`);
}
// Otherwise start polling the authoritative doc_id returned by the server.
setDocStatus(uploadData.status as DocStatus);
await new Promise<void>((res, rej) => {
const reject = (msg: string) => rej(new Error(msg));
// Begin polling immediately — backend creates the record synchronously before processing
setTimeout(() => pollStatus(docId, res, reject), 800);
// Also handle the upload response (in case processing finishes before poll catches it)
uploadPromise.then(async httpRes => {
if (!httpRes.ok) {
const detail = await httpRes.text().catch(() => httpRes.statusText);
stopPolling();
reject(`${file.name}: ${httpRes.status} ${detail}`);
}
// Upload succeeded — polling will catch the final status
}).catch(err => {
stopPolling();
reject(err instanceof Error ? err.message : 'Upload error');
});
pollStatus(docId, res, (msg: string) => rej(new Error(msg)));
});
}
+80
View File
@@ -0,0 +1,80 @@
import React, { FormEvent, useState } from 'react';
import { useAuth } from '../../contexts';
export function LoginPage() {
const { login } = useAuth();
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
async function handleSubmit(e: FormEvent) {
e.preventDefault();
if (!username.trim() || !password.trim()) return;
setError('');
setLoading(true);
try {
await login(username.trim(), password);
} catch (err) {
setError(err instanceof Error ? err.message : 'Login failed');
} finally {
setLoading(false);
}
}
return (
<div className="login-page">
<div className="login-card">
<div className="login-brand">
<img src="/company-logo.ico" alt="T-Systems" className="login-logo" />
<div className="login-brand-text">
<div className="login-brand-name">T-Systems</div>
<div className="login-brand-sub">AI Regulation Hub</div>
</div>
</div>
<h2 className="login-title">Sign in</h2>
<form onSubmit={handleSubmit} className="login-form">
<div className="login-field">
<label className="login-label" htmlFor="username">Username</label>
<input
id="username"
type="text"
className="login-input"
value={username}
onChange={e => setUsername(e.target.value)}
autoFocus
autoComplete="username"
disabled={loading}
placeholder="e.g. admin"
/>
</div>
<div className="login-field">
<label className="login-label" htmlFor="password">Password</label>
<input
id="password"
type="password"
className="login-input"
value={password}
onChange={e => setPassword(e.target.value)}
autoComplete="current-password"
disabled={loading}
/>
</div>
{error && <p className="login-error">{error}</p>}
<button type="submit" className="login-btn" disabled={loading}>
{loading ? 'Signing in…' : 'Sign in'}
</button>
</form>
<p className="login-hint">
Demo accounts: <code>admin</code> / <code>legal</code> / <code>ehs</code> / <code>readonly</code>
</p>
</div>
</div>
);
}
+3 -1
View File
@@ -22,7 +22,8 @@ const STEPS = [
export function OverviewPage() {
const navigate = useNavigate();
return (
<div className="overview-page">
<div className="overview-scroll-wrapper">
<div className="overview-page">
<section className="overview-hero">
<p className="hero-eyebrow">T-Systems · AI Regulation Hub</p>
<h1 className="hero-title">AI Compliance,<br />Automated end-to-end</h1>
@@ -83,5 +84,6 @@ export function OverviewPage() {
</div>
</section>
</div>
</div>
);
}
@@ -2,6 +2,12 @@ import { useState, useEffect, useRef } from 'react';
import { Topbar } from '../../components/layout/Topbar';
import { RefreshCw, Play, Square, ExternalLink } from 'lucide-react';
const TOKEN_KEY = 'auth_token';
function authHeader(): Record<string, string> {
const t = localStorage.getItem(TOKEN_KEY);
return t ? { Authorization: `Bearer ${t}` } : {};
}
interface Signal {
id: string;
source: string;
@@ -101,14 +107,14 @@ export function PerceptionPage() {
const abortRef = useRef<AbortController | null>(null);
useEffect(() => {
fetch('/api/v1/perception/stats')
fetch('/api/v1/perception/stats', { headers: authHeader() })
.then(r => r.json())
.then(setStats)
.catch(() => setStats({ total: 47, high_impact: 7, medium_impact: 18, last_90_days: 14 }));
}, []);
useEffect(() => {
fetch('/api/v1/perception/events?limit=100')
fetch('/api/v1/perception/events?limit=100', { headers: authHeader() })
.then(r => r.json())
.then(d => {
if (Array.isArray(d?.events) && d.events.length > 0) {
@@ -135,7 +141,7 @@ export function PerceptionPage() {
const ctrl = new AbortController();
abortRef.current = ctrl;
// Backend: POST /api/v1/perception/events/{id}/analyze → SSE stream
fetch(`/api/v1/perception/events/${selected.id}/analyze`, { method: 'POST', signal: ctrl.signal })
fetch(`/api/v1/perception/events/${selected.id}/analyze`, { method: 'POST', headers: authHeader(), signal: ctrl.signal })
.then(async res => {
if (!res.body) { setAiOutput('No stream available.'); setStreaming(false); return; }
const reader = res.body.getReader();
+8 -2
View File
@@ -2,6 +2,12 @@ import { useState, useRef, useEffect, useCallback } from 'react';
import { Topbar } from '../../components/layout/Topbar';
import { Send, Download } from 'lucide-react';
const TOKEN_KEY = 'auth_token';
function authHeader(): Record<string, string> {
const t = localStorage.getItem(TOKEN_KEY);
return t ? { Authorization: `Bearer ${t}` } : {};
}
interface Message {
id: string;
role: 'user' | 'assistant';
@@ -87,7 +93,7 @@ export function RagChatPage() {
// Fetch quick questions from backend on mount
useEffect(() => {
fetch('/api/v1/rag/quick-questions')
fetch('/api/v1/rag/quick-questions', { headers: authHeader() })
.then(r => r.json())
.then(d => {
if (Array.isArray(d?.questions) && d.questions.length > 0) {
@@ -136,7 +142,7 @@ export function RagChatPage() {
const res = await fetch('/api/v1/rag/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: { 'Content-Type': 'application/json', ...authHeader() },
body: JSON.stringify(body),
signal: ctrl.signal,
});
+9 -3
View File
@@ -3,6 +3,12 @@ import { Topbar } from '../../components/layout/Topbar';
import { Search, Upload, Download, RefreshCw, CheckCircle, XCircle, AlertTriangle, Info } from 'lucide-react';
import { UploadModal } from '../Docs/UploadModal';
const TOKEN_KEY = 'auth_token';
function authHeader(): Record<string, string> {
const t = localStorage.getItem(TOKEN_KEY);
return t ? { Authorization: `Bearer ${t}` } : {};
}
// ── API types ──────────────────────────────────────────────────────────────
interface Stats {
documents_total: number;
@@ -83,9 +89,9 @@ export function StatusPage() {
// Fetch all three endpoints in parallel
Promise.allSettled([
fetch('/api/v1/status/stats').then(r => r.json()),
fetch('/api/v1/status/health').then(r => r.json()),
fetch('/api/v1/status/config').then(r => r.json()),
fetch('/api/v1/status/stats', { headers: authHeader() }).then(r => r.json()),
fetch('/api/v1/status/health', { headers: authHeader() }).then(r => r.json()),
fetch('/api/v1/status/config', { headers: authHeader() }).then(r => r.json()),
]).then(([statsRes, healthRes, configRes]) => {
if (statsRes.status === 'fulfilled') setStats(statsRes.value);
else setStats({ documents_total: 0, documents_indexed: 0, documents_failed: 0, chunks_total: 0 });