feat: AI问答独立域名 chat.newassetchain.io,CBPP公开原则,登录/注册引导
This commit is contained in:
parent
c8e5e7189f
commit
baf65ac757
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue