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,
AlertCircle, CheckCircle2, Info, MessageSquare,
Plus, Trash2, Clock, ChevronDown, ChevronUp,
ExternalLink, Zap, SquarePen,
ExternalLink, Zap, SquarePen, LogIn, UserPlus, LogOut,
} from "lucide-react";
import { useLocation } from "wouter";
// ─── 类型定义 ─────────────────────────────────────────────────────
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 }> = {
knowledge_qa: { name: "知识问答", desc: "NAC公链技术与规则", color: "text-blue-400", icon: "BookOpen" },
compliance: { name: "合规审查", desc: "RWA合规与监管分析", color: "text-red-400", icon: "Shield" },
translation: { name: "多语翻译", desc: "10种语言专业翻译", color: "text-green-400", icon: "Languages" },
approval_assist: { name: "审批助手", desc: "资产审批流程辅助", color: "text-purple-400", icon: "ClipboardCheck" },
knowledge_qa: { name: "知识问答", desc: "NAC公链技术与规则", color: "text-blue-400", icon: "BookOpen" },
compliance: { name: "合规审查", desc: "RWA合规与监管分析", color: "text-red-400", icon: "Shield" },
translation: { name: "多语翻译", desc: "10种语言专业翻译", color: "text-green-400", icon: "Languages" },
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 }) {
const pct = Math.round(confidence * 100);
@ -63,7 +66,7 @@ function SourceBadge({ source, onClick }: { source: string; onClick: () => void
<button
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"
title={`点击跳转到知识库${source}`}
title={`查看来源${source}`}
>
<BookOpen className="w-2.5 h-2.5 shrink-0" />
<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"}`}>
{/* 发送者名称 */}
<span className="text-xs text-zinc-500 font-medium px-1">
{isUser ? "你" : "NAC公链AI"}
</span>
{/* 消息内容 */}
<div
className={`rounded-2xl px-4 py-3 text-sm leading-relaxed whitespace-pre-wrap break-words ${
isUser
@ -132,7 +132,6 @@ function MessageBubble({
</div>
)}
{/* 建议标签 */}
{!isUser && msg.suggestions && msg.suggestions.length > 0 && (
<div className="flex flex-wrap gap-1 px-1">
{msg.suggestions.map((s, i) => (
@ -185,7 +184,7 @@ function ConversationListItem({
);
}
// ─── 欢迎屏幕(无消息时) ─────────────────────────────────────────
// ─── 欢迎屏幕 ─────────────────────────────────────────────────────
function WelcomeScreen({
agentType,
suggestedQuestions,
@ -200,18 +199,17 @@ function WelcomeScreen({
return (
<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">
<IconComp className="w-8 h-8 text-white" />
</div>
<h1 className="text-2xl font-bold text-white mb-2">NAC公链AI</h1>
{info && (
<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 && (
<div className="w-full max-w-lg">
<p className="text-xs text-zinc-500 mb-3"></p>
@ -241,28 +239,27 @@ export default function AIAgents() {
const [showHistory, setShowHistory] = useState(true);
const messagesEndRef = useRef<HTMLDivElement>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const [, navigate] = useLocation();
// 来源引用跳转
const handleSourceClick = (source: string) => {
const keyword = source.split("·").pop() || source;
navigate(`/knowledge-base?search=${encodeURIComponent(keyword)}`);
};
// 获取当前用户(公开接口,未登录返回 null
const { data: currentUser } = trpc.nacAuth.whoami.useQuery(undefined, {
retry: false,
refetchOnWindowFocus: false,
});
// 获取 Agent 列表和状态
const { data: agentData, isLoading: agentLoading } = trpc.aiAgent.list.useQuery();
const { data: statusData } = trpc.aiAgent.status.useQuery();
// 获取会话列表
// 获取会话列表(已登录用户才有历史)
const { data: convData, refetch: refetchConvs } = trpc.aiAgent.listConversations.useQuery(
{ agentType: selectedAgent || undefined, limit: 50 },
{ enabled: true }
{ enabled: !!currentUser }
);
// 加载历史消息
const loadHistoryQuery = trpc.aiAgent.getConversation.useQuery(
const loadHistoryQuery = trpc.aiAgent.loadHistory.useQuery(
{ conversationId: activeConvId! },
{ enabled: !!activeConvId, refetchOnWindowFocus: false }
{ enabled: !!activeConvId && !!currentUser, refetchOnWindowFocus: false }
);
useEffect(() => {
@ -286,6 +283,13 @@ export default function AIAgents() {
onError: (e) => toast.error(`删除失败: ${e.message}`),
});
// 登出
const logoutMutation = trpc.nacAuth.logout.useMutation({
onSuccess: () => {
window.location.reload();
},
});
// 发送消息
const chatMutation = trpc.aiAgent.chat.useMutation({
onSuccess: (response) => {
@ -300,7 +304,7 @@ export default function AIAgents() {
if (!activeConvId && response.conversationId) {
setActiveConvId(response.conversationId);
}
refetchConvs();
if (currentUser) refetchConvs();
},
onError: (error) => {
toast.error(`响应失败: ${error.message}`);
@ -345,7 +349,7 @@ export default function AIAgents() {
agentType: selectedAgent,
userMessage: text,
conversationId: activeConvId || undefined,
persistHistory: true,
persistHistory: !!currentUser, // 仅登录用户持久化历史
});
};
@ -361,7 +365,7 @@ export default function AIAgents() {
const agentInfo = AGENT_LABELS[selectedAgent];
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 className="flex-1 overflow-y-auto mt-4 px-3">
<button
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" />}
</button>
{/* 历史会话(仅登录用户) */}
{currentUser && (
<div className="flex-1 overflow-y-auto mt-4 px-3">
<button
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" />}
</button>
{showHistory && (
<div className="mt-1 space-y-0.5 pb-4">
{conversations.length === 0 ? (
<p className="text-xs text-zinc-700 text-center py-4"></p>
) : (
conversations.map(conv => (
<ConversationListItem
key={conv.conversationId}
conv={conv}
isActive={activeConvId === conv.conversationId}
onClick={() => handleSelectConversation(conv)}
onDelete={() => deleteConvMutation.mutate({ conversationId: conv.conversationId })}
/>
))
)}
{showHistory && (
<div className="mt-1 space-y-0.5 pb-4">
{conversations.length === 0 ? (
<p className="text-xs text-zinc-700 text-center py-4"></p>
) : (
conversations.map(conv => (
<ConversationListItem
key={conv.conversationId}
conv={conv}
isActive={activeConvId === conv.conversationId}
onClick={() => handleSelectConversation(conv)}
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>
{/*
@ -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">
{/* 左AI 信息 */}
<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">
{agentInfo && (() => {
@ -482,6 +499,8 @@ export default function AIAgents() {
</p>
</div>
</div>
{/* 右:用户区域 */}
<div className="flex items-center gap-2">
{activeConvId && (
<Button
@ -494,6 +513,49 @@ export default function AIAgents() {
</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>
@ -521,7 +583,7 @@ export default function AIAgents() {
{/* 消息列表 */}
{messages.map((msg, i) => (
<MessageBubble key={i} msg={msg} onSourceClick={handleSourceClick} />
<MessageBubble key={i} msg={msg} />
))}
{/* AI 思考中 */}
@ -577,7 +639,7 @@ export default function AIAgents() {
</div>
</div>
<p className="text-center text-[10px] text-zinc-700 mt-2">
NAC公链AI NAC
NAC公链AI NAC · · CBPP原则
</p>
</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;
}
}