feat: AI问答独立域名 chat.newassetchain.io,CBPP公开原则,登录/注册引导

This commit is contained in:
nacadmin 2026-03-01 16:20:49 +08:00
parent c8e5e7189f
commit baf65ac757
3 changed files with 220 additions and 57 deletions

View File

@ -0,0 +1,61 @@
import { Toaster } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip";
import NotFound from "@/pages/NotFound";
import { Route, Switch } from "wouter";
import ErrorBoundary from "./components/ErrorBoundary";
import { ThemeProvider } from "./contexts/ThemeContext";
import Login from "./pages/Login";
import Dashboard from "./pages/Dashboard";
import KnowledgeBase from "./pages/KnowledgeBase";
import Crawlers from "./pages/Crawlers";
import ApprovalCases from "./pages/ApprovalCases";
import TagEngine from "./pages/TagEngine";
import ProtocolRegistry from "./pages/ProtocolRegistry";
import AuditLog from "./pages/AuditLog";
import AIAgents from "./pages/AIAgents";
import NotificationSettings from "./pages/NotificationSettings";
import ArchiveManagement from "./pages/ArchiveManagement";
import RegulatoryMonitor from "./pages/RegulatoryMonitor";
import KnowledgeAnalytics from "./pages/KnowledgeAnalytics";
import ConflictDetector from "./pages/ConflictDetector";
import ChainValidation from "./pages/ChainValidation";
import AdminLayout from "./components/AdminLayout";
function Router() {
return (
<Switch>
<Route path="/login" component={Login} />
<Route path="/" component={() => <AdminLayout><Dashboard /></AdminLayout>} />
<Route path="/knowledge" component={() => <AdminLayout><KnowledgeBase /></AdminLayout>} />
<Route path="/crawlers" component={() => <AdminLayout><Crawlers /></AdminLayout>} />
<Route path="/approvals" component={() => <AdminLayout><ApprovalCases /></AdminLayout>} />
<Route path="/tags" component={() => <AdminLayout><TagEngine /></AdminLayout>} />
<Route path="/protocols" component={() => <AdminLayout><ProtocolRegistry /></AdminLayout>} />
<Route path="/audit" component={() => <AdminLayout><AuditLog /></AdminLayout>} />
<Route path="/ai-agents" component={AIAgents} />
<Route path="/notifications" component={() => <AdminLayout><NotificationSettings /></AdminLayout>} />
<Route path="/archive" component={() => <AdminLayout><ArchiveManagement /></AdminLayout>} />
<Route path="/regulatory-monitor" component={() => <AdminLayout><RegulatoryMonitor /></AdminLayout>} />
<Route path="/knowledge-analytics" component={() => <AdminLayout><KnowledgeAnalytics /></AdminLayout>} />
<Route path="/conflict-detector" component={() => <AdminLayout><ConflictDetector /></AdminLayout>} />
<Route path="/chain-validation" component={() => <AdminLayout><ChainValidation /></AdminLayout>} />
<Route path="/404" component={NotFound} />
<Route component={NotFound} />
</Switch>
);
}
function App() {
return (
<ErrorBoundary>
<ThemeProvider defaultTheme="dark">
<TooltipProvider>
<Toaster />
<Router />
</TooltipProvider>
</ThemeProvider>
</ErrorBoundary>
);
}
export default App;

View File

@ -9,9 +9,8 @@ import {
Send, Bot, User, Loader2, Sparkles, Send, Bot, User, Loader2, Sparkles,
AlertCircle, CheckCircle2, Info, MessageSquare, AlertCircle, CheckCircle2, Info, MessageSquare,
Plus, Trash2, Clock, ChevronDown, ChevronUp, Plus, Trash2, Clock, ChevronDown, ChevronUp,
ExternalLink, Zap, SquarePen, ExternalLink, Zap, SquarePen, LogIn, UserPlus, LogOut,
} from "lucide-react"; } from "lucide-react";
import { useLocation } from "wouter";
// ─── 类型定义 ───────────────────────────────────────────────────── // ─── 类型定义 ─────────────────────────────────────────────────────
type AgentType = "knowledge_qa" | "compliance" | "translation" | "approval_assist"; type AgentType = "knowledge_qa" | "compliance" | "translation" | "approval_assist";
@ -39,12 +38,16 @@ const ICON_MAP: Record<string, React.ElementType> = {
}; };
const AGENT_LABELS: Record<AgentType, { name: string; desc: string; color: string; icon: string }> = { const AGENT_LABELS: Record<AgentType, { name: string; desc: string; color: string; icon: string }> = {
knowledge_qa: { name: "知识问答", desc: "NAC公链技术与规则", color: "text-blue-400", icon: "BookOpen" }, knowledge_qa: { name: "知识问答", desc: "NAC公链技术与规则", color: "text-blue-400", icon: "BookOpen" },
compliance: { name: "合规审查", desc: "RWA合规与监管分析", color: "text-red-400", icon: "Shield" }, compliance: { name: "合规审查", desc: "RWA合规与监管分析", color: "text-red-400", icon: "Shield" },
translation: { name: "多语翻译", desc: "10种语言专业翻译", color: "text-green-400", icon: "Languages" }, translation: { name: "多语翻译", desc: "10种语言专业翻译", color: "text-green-400", icon: "Languages" },
approval_assist: { name: "审批助手", desc: "资产审批流程辅助", color: "text-purple-400", icon: "ClipboardCheck" }, approval_assist: { name: "审批助手", desc: "资产审批流程辅助", color: "text-purple-400", icon: "ClipboardCheck" },
}; };
// NAC 注册/登录地址
const NAC_REGISTER_URL = "https://id.newassetchain.io/";
const NAC_LOGIN_URL = "https://id.newassetchain.io/";
// ─── 置信度徽章 ─────────────────────────────────────────────────── // ─── 置信度徽章 ───────────────────────────────────────────────────
function ConfidenceBadge({ confidence }: { confidence: number }) { function ConfidenceBadge({ confidence }: { confidence: number }) {
const pct = Math.round(confidence * 100); const pct = Math.round(confidence * 100);
@ -63,7 +66,7 @@ function SourceBadge({ source, onClick }: { source: string; onClick: () => void
<button <button
onClick={onClick} onClick={onClick}
className="inline-flex items-center gap-1 text-xs text-blue-400 hover:text-blue-300 transition-colors bg-blue-950/40 rounded px-1.5 py-0.5 border border-blue-800/50" className="inline-flex items-center gap-1 text-xs text-blue-400 hover:text-blue-300 transition-colors bg-blue-950/40 rounded px-1.5 py-0.5 border border-blue-800/50"
title={`点击跳转到知识库${source}`} title={`查看来源${source}`}
> >
<BookOpen className="w-2.5 h-2.5 shrink-0" /> <BookOpen className="w-2.5 h-2.5 shrink-0" />
<span className="max-w-[120px] truncate">{source}</span> <span className="max-w-[120px] truncate">{source}</span>
@ -94,12 +97,9 @@ function MessageBubble({
{/* 内容区 */} {/* 内容区 */}
<div className={`flex flex-col gap-2 max-w-[80%] ${isUser ? "items-end" : "items-start"}`}> <div className={`flex flex-col gap-2 max-w-[80%] ${isUser ? "items-end" : "items-start"}`}>
{/* 发送者名称 */}
<span className="text-xs text-zinc-500 font-medium px-1"> <span className="text-xs text-zinc-500 font-medium px-1">
{isUser ? "你" : "NAC公链AI"} {isUser ? "你" : "NAC公链AI"}
</span> </span>
{/* 消息内容 */}
<div <div
className={`rounded-2xl px-4 py-3 text-sm leading-relaxed whitespace-pre-wrap break-words ${ className={`rounded-2xl px-4 py-3 text-sm leading-relaxed whitespace-pre-wrap break-words ${
isUser isUser
@ -132,7 +132,6 @@ function MessageBubble({
</div> </div>
)} )}
{/* 建议标签 */}
{!isUser && msg.suggestions && msg.suggestions.length > 0 && ( {!isUser && msg.suggestions && msg.suggestions.length > 0 && (
<div className="flex flex-wrap gap-1 px-1"> <div className="flex flex-wrap gap-1 px-1">
{msg.suggestions.map((s, i) => ( {msg.suggestions.map((s, i) => (
@ -185,7 +184,7 @@ function ConversationListItem({
); );
} }
// ─── 欢迎屏幕(无消息时) ───────────────────────────────────────── // ─── 欢迎屏幕 ─────────────────────────────────────────────────────
function WelcomeScreen({ function WelcomeScreen({
agentType, agentType,
suggestedQuestions, suggestedQuestions,
@ -200,18 +199,17 @@ function WelcomeScreen({
return ( return (
<div className="flex flex-col items-center justify-center h-full px-6 py-12 text-center"> <div className="flex flex-col items-center justify-center h-full px-6 py-12 text-center">
{/* Logo 区域 */}
<div className="w-16 h-16 rounded-2xl bg-gradient-to-br from-cyan-500 to-blue-600 flex items-center justify-center mb-5 shadow-lg shadow-blue-900/30"> <div className="w-16 h-16 rounded-2xl bg-gradient-to-br from-cyan-500 to-blue-600 flex items-center justify-center mb-5 shadow-lg shadow-blue-900/30">
<IconComp className="w-8 h-8 text-white" /> <IconComp className="w-8 h-8 text-white" />
</div> </div>
<h1 className="text-2xl font-bold text-white mb-2">NAC公链AI</h1> <h1 className="text-2xl font-bold text-white mb-2">NAC公链AI</h1>
{info && ( {info && (
<p className="text-zinc-400 text-sm mb-1">{info.name} · {info.desc}</p> <p className="text-zinc-400 text-sm mb-1">{info.name} · {info.desc}</p>
)} )}
<p className="text-zinc-600 text-xs mb-8"> NAC · CBPP共识 · Charter合约</p> <p className="text-zinc-600 text-xs mb-8">
NAC · CBPP共识 · Charter合约 ·
</p>
{/* 建议问题 */}
{suggestedQuestions.length > 0 && ( {suggestedQuestions.length > 0 && (
<div className="w-full max-w-lg"> <div className="w-full max-w-lg">
<p className="text-xs text-zinc-500 mb-3"></p> <p className="text-xs text-zinc-500 mb-3"></p>
@ -241,28 +239,27 @@ export default function AIAgents() {
const [showHistory, setShowHistory] = useState(true); const [showHistory, setShowHistory] = useState(true);
const messagesEndRef = useRef<HTMLDivElement>(null); const messagesEndRef = useRef<HTMLDivElement>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null); const textareaRef = useRef<HTMLTextAreaElement>(null);
const [, navigate] = useLocation();
// 来源引用跳转 // 获取当前用户(公开接口,未登录返回 null
const handleSourceClick = (source: string) => { const { data: currentUser } = trpc.nacAuth.whoami.useQuery(undefined, {
const keyword = source.split("·").pop() || source; retry: false,
navigate(`/knowledge-base?search=${encodeURIComponent(keyword)}`); refetchOnWindowFocus: false,
}; });
// 获取 Agent 列表和状态 // 获取 Agent 列表和状态
const { data: agentData, isLoading: agentLoading } = trpc.aiAgent.list.useQuery(); const { data: agentData, isLoading: agentLoading } = trpc.aiAgent.list.useQuery();
const { data: statusData } = trpc.aiAgent.status.useQuery(); const { data: statusData } = trpc.aiAgent.status.useQuery();
// 获取会话列表 // 获取会话列表(已登录用户才有历史)
const { data: convData, refetch: refetchConvs } = trpc.aiAgent.listConversations.useQuery( const { data: convData, refetch: refetchConvs } = trpc.aiAgent.listConversations.useQuery(
{ agentType: selectedAgent || undefined, limit: 50 }, { agentType: selectedAgent || undefined, limit: 50 },
{ enabled: true } { enabled: !!currentUser }
); );
// 加载历史消息 // 加载历史消息
const loadHistoryQuery = trpc.aiAgent.getConversation.useQuery( const loadHistoryQuery = trpc.aiAgent.loadHistory.useQuery(
{ conversationId: activeConvId! }, { conversationId: activeConvId! },
{ enabled: !!activeConvId, refetchOnWindowFocus: false } { enabled: !!activeConvId && !!currentUser, refetchOnWindowFocus: false }
); );
useEffect(() => { useEffect(() => {
@ -286,6 +283,13 @@ export default function AIAgents() {
onError: (e) => toast.error(`删除失败: ${e.message}`), onError: (e) => toast.error(`删除失败: ${e.message}`),
}); });
// 登出
const logoutMutation = trpc.nacAuth.logout.useMutation({
onSuccess: () => {
window.location.reload();
},
});
// 发送消息 // 发送消息
const chatMutation = trpc.aiAgent.chat.useMutation({ const chatMutation = trpc.aiAgent.chat.useMutation({
onSuccess: (response) => { onSuccess: (response) => {
@ -300,7 +304,7 @@ export default function AIAgents() {
if (!activeConvId && response.conversationId) { if (!activeConvId && response.conversationId) {
setActiveConvId(response.conversationId); setActiveConvId(response.conversationId);
} }
refetchConvs(); if (currentUser) refetchConvs();
}, },
onError: (error) => { onError: (error) => {
toast.error(`响应失败: ${error.message}`); toast.error(`响应失败: ${error.message}`);
@ -345,7 +349,7 @@ export default function AIAgents() {
agentType: selectedAgent, agentType: selectedAgent,
userMessage: text, userMessage: text,
conversationId: activeConvId || undefined, conversationId: activeConvId || undefined,
persistHistory: true, persistHistory: !!currentUser, // 仅登录用户持久化历史
}); });
}; };
@ -361,7 +365,7 @@ export default function AIAgents() {
const agentInfo = AGENT_LABELS[selectedAgent]; const agentInfo = AGENT_LABELS[selectedAgent];
return ( return (
<div className="flex h-full overflow-hidden bg-zinc-900 text-white"> <div className="flex h-screen overflow-hidden bg-zinc-900 text-white">
{/* {/*
@ -431,34 +435,46 @@ export default function AIAgents() {
</div> </div>
</div> </div>
{/* 历史会话 */} {/* 历史会话(仅登录用户) */}
<div className="flex-1 overflow-y-auto mt-4 px-3"> {currentUser && (
<button <div className="flex-1 overflow-y-auto mt-4 px-3">
className="w-full flex items-center justify-between px-1 py-1.5 text-[10px] font-semibold text-zinc-600 uppercase tracking-wider hover:text-zinc-400 transition-colors" <button
onClick={() => setShowHistory(v => !v)} className="w-full flex items-center justify-between px-1 py-1.5 text-[10px] font-semibold text-zinc-600 uppercase tracking-wider hover:text-zinc-400 transition-colors"
> onClick={() => setShowHistory(v => !v)}
<span> {conversations.length > 0 && `(${conversations.length})`}</span> >
{showHistory ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />} <span> {conversations.length > 0 && `(${conversations.length})`}</span>
</button> {showHistory ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />}
</button>
{showHistory && ( {showHistory && (
<div className="mt-1 space-y-0.5 pb-4"> <div className="mt-1 space-y-0.5 pb-4">
{conversations.length === 0 ? ( {conversations.length === 0 ? (
<p className="text-xs text-zinc-700 text-center py-4"></p> <p className="text-xs text-zinc-700 text-center py-4"></p>
) : ( ) : (
conversations.map(conv => ( conversations.map(conv => (
<ConversationListItem <ConversationListItem
key={conv.conversationId} key={conv.conversationId}
conv={conv} conv={conv}
isActive={activeConvId === conv.conversationId} isActive={activeConvId === conv.conversationId}
onClick={() => handleSelectConversation(conv)} onClick={() => handleSelectConversation(conv)}
onDelete={() => deleteConvMutation.mutate({ conversationId: conv.conversationId })} onDelete={() => deleteConvMutation.mutate({ conversationId: conv.conversationId })}
/> />
)) ))
)} )}
</div>
)}
</div>
)}
{/* 未登录时:引导提示 */}
{!currentUser && (
<div className="flex-1 flex flex-col justify-end px-3 pb-4 mt-4">
<div className="bg-zinc-800/50 border border-zinc-700/50 rounded-xl p-3 text-center">
<p className="text-xs text-zinc-400 mb-2"></p>
<p className="text-[10px] text-zinc-600"> · CBPP原则</p>
</div> </div>
)} </div>
</div> )}
</div> </div>
{/* {/*
@ -468,6 +484,7 @@ export default function AIAgents() {
{/* 顶部栏 */} {/* 顶部栏 */}
<div className="flex items-center justify-between px-6 py-3.5 border-b border-zinc-800 bg-zinc-900/80 backdrop-blur-sm shrink-0"> <div className="flex items-center justify-between px-6 py-3.5 border-b border-zinc-800 bg-zinc-900/80 backdrop-blur-sm shrink-0">
{/* 左AI 信息 */}
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-xl bg-gradient-to-br from-cyan-500 to-blue-600 flex items-center justify-center"> <div className="w-8 h-8 rounded-xl bg-gradient-to-br from-cyan-500 to-blue-600 flex items-center justify-center">
{agentInfo && (() => { {agentInfo && (() => {
@ -482,6 +499,8 @@ export default function AIAgents() {
</p> </p>
</div> </div>
</div> </div>
{/* 右:用户区域 */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{activeConvId && ( {activeConvId && (
<Button <Button
@ -494,6 +513,49 @@ export default function AIAgents() {
</Button> </Button>
)} )}
{currentUser ? (
/* 已登录:显示用户信息 + 登出 */
<div className="flex items-center gap-2">
<div className="flex items-center gap-2 bg-zinc-800 rounded-lg px-3 py-1.5">
<div className="w-5 h-5 rounded-full bg-indigo-600 flex items-center justify-center">
<User className="w-3 h-3 text-white" />
</div>
<span className="text-xs text-zinc-300 max-w-[120px] truncate">
{currentUser.email}
</span>
</div>
<button
onClick={() => logoutMutation.mutate()}
className="p-1.5 rounded-lg hover:bg-zinc-800 text-zinc-500 hover:text-zinc-300 transition-colors"
title="退出登录"
>
<LogOut className="w-4 h-4" />
</button>
</div>
) : (
/* 未登录:登录 + 注册按钮 */
<div className="flex items-center gap-2">
<a
href={NAC_LOGIN_URL}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 text-xs text-zinc-300 hover:text-white bg-zinc-800 hover:bg-zinc-700 border border-zinc-700 hover:border-zinc-600 rounded-lg px-3 py-1.5 transition-all"
>
<LogIn className="w-3.5 h-3.5" />
</a>
<a
href={NAC_REGISTER_URL}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 text-xs text-white bg-indigo-600 hover:bg-indigo-500 rounded-lg px-3 py-1.5 transition-all"
>
<UserPlus className="w-3.5 h-3.5" />
</a>
</div>
)}
</div> </div>
</div> </div>
@ -521,7 +583,7 @@ export default function AIAgents() {
{/* 消息列表 */} {/* 消息列表 */}
{messages.map((msg, i) => ( {messages.map((msg, i) => (
<MessageBubble key={i} msg={msg} onSourceClick={handleSourceClick} /> <MessageBubble key={i} msg={msg} />
))} ))}
{/* AI 思考中 */} {/* AI 思考中 */}
@ -577,7 +639,7 @@ export default function AIAgents() {
</div> </div>
</div> </div>
<p className="text-center text-[10px] text-zinc-700 mt-2"> <p className="text-center text-[10px] text-zinc-700 mt-2">
NAC公链AI NAC NAC公链AI NAC · · CBPP原则
</p> </p>
</div> </div>
</div> </div>

View File

@ -0,0 +1,40 @@
# chat.newassetchain.io - NAC公链AI 对话界面
# 代理到 nac-admin 服务(端口 9560路由 /ai-agents
server {
listen 80;
server_name chat.newassetchain.io;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name chat.newassetchain.io;
ssl_certificate /root/ssl/_.newassetchain.io.pem;
ssl_certificate_key /root/ssl/_.newassetchain.io.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# 访问根路径直接重定向到 /ai-agents
location = / {
return 301 https://$host/ai-agents;
}
# 所有请求代理到 nac-admin 服务
location / {
proxy_pass http://127.0.0.1:9560;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 300s;
proxy_connect_timeout 75s;
}
}