NAC_Blockchain/nac-ai-valuation/valuation-ui/public/index.html

1400 lines
50 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NAC AI 资产估值引擎 | NewAssetChain</title>
<meta name="description" content="NAC AI多资产多辖区估值推理引擎 - 基于SDR锚定的XTZH统一定价支持60+司法辖区,一键上链">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>💎</text></svg>">
<style>
:root {
--bg-primary: #0a0e1a;
--bg-secondary: #0f1628;
--bg-card: #141c2e;
--bg-input: #1a2340;
--accent-gold: #d4a843;
--accent-gold-light: #f0c060;
--accent-blue: #3b82f6;
--accent-green: #10b981;
--accent-purple: #8b5cf6;
--text-primary: #e8eaf0;
--text-secondary: #8892a4;
--text-muted: #4a5568;
--border: #1e2d4a;
--border-active: #3b82f6;
--shadow: 0 4px 24px rgba(0,0,0,0.4);
--radius: 12px;
}
* { margin:0; padding:0; box-sizing:border-box; }
body {
font-family: 'PingFang SC', 'Microsoft YaHei', -apple-system, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* ===== HEADER ===== */
.header {
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
padding: 0 20px;
height: 60px;
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
}
.header-left { display:flex; align-items:center; gap:12px; }
.logo {
width: 36px; height: 36px;
background: linear-gradient(135deg, var(--accent-gold), #b8860b);
border-radius: 8px;
display: flex; align-items: center; justify-content: center;
font-size: 18px; font-weight: 900; color: #000;
}
.header-title { font-size: 16px; font-weight: 700; color: var(--text-primary); }
.header-subtitle { font-size: 11px; color: var(--text-secondary); margin-top: 1px; }
.header-right { display:flex; align-items:center; gap:12px; }
/* XTZH价格指示器 */
.xtzh-price-badge {
background: linear-gradient(135deg, rgba(212,168,67,0.15), rgba(212,168,67,0.05));
border: 1px solid rgba(212,168,67,0.3);
border-radius: 20px;
padding: 5px 14px;
display: flex; align-items: center; gap: 8px;
cursor: pointer;
}
.xtzh-price-badge .label { font-size: 10px; color: var(--text-secondary); }
.xtzh-price-badge .price { font-size: 14px; font-weight: 700; color: var(--accent-gold); }
.xtzh-price-badge .source { font-size: 9px; color: var(--text-muted); }
.price-dot { width:6px; height:6px; border-radius:50%; background:var(--accent-green); animation: pulse 2s infinite; }
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.4} }
/* 语言选择 */
.lang-select {
background: var(--bg-card); border: 1px solid var(--border);
color: var(--text-primary); border-radius: 8px;
padding: 5px 10px; font-size: 12px; cursor: pointer;
outline: none;
}
/* ===== MAIN LAYOUT ===== */
.main {
display: flex;
flex: 1;
overflow: hidden;
}
/* ===== SIDEBAR ===== */
.sidebar {
width: 260px;
background: var(--bg-secondary);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
overflow: hidden;
flex-shrink: 0;
}
.sidebar-header {
padding: 16px;
border-bottom: 1px solid var(--border);
display: flex; align-items: center; gap: 8px;
}
.new-chat-btn {
flex: 1;
background: linear-gradient(135deg, var(--accent-gold), #b8860b);
color: #000;
border: none;
border-radius: 8px;
padding: 9px 16px;
font-size: 13px;
font-weight: 700;
cursor: pointer;
transition: opacity 0.2s;
}
.new-chat-btn:hover { opacity: 0.85; }
.clear-btn {
background: var(--bg-card);
border: 1px solid var(--border);
color: var(--text-secondary);
border-radius: 8px;
padding: 9px 12px;
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
}
.clear-btn:hover { border-color: var(--accent-gold); color: var(--accent-gold); }
/* 辖区快速筛选 */
.sidebar-section { padding: 12px 16px 8px; }
.sidebar-section-title {
font-size: 10px; color: var(--text-muted);
text-transform: uppercase; letter-spacing: 1px;
margin-bottom: 8px;
}
.jurisdiction-grid {
display: grid; grid-template-columns: repeat(3, 1fr); gap: 4px;
}
.juris-btn {
background: var(--bg-card);
border: 1px solid var(--border);
color: var(--text-secondary);
border-radius: 6px;
padding: 5px 4px;
font-size: 11px;
cursor: pointer;
text-align: center;
transition: all 0.2s;
}
.juris-btn:hover, .juris-btn.active {
border-color: var(--accent-gold);
color: var(--accent-gold);
background: rgba(212,168,67,0.08);
}
/* 资产类型快速选择 */
.asset-type-list { display: flex; flex-direction: column; gap: 3px; }
.asset-type-btn {
background: var(--bg-card);
border: 1px solid var(--border);
color: var(--text-secondary);
border-radius: 6px;
padding: 6px 10px;
font-size: 12px;
cursor: pointer;
text-align: left;
transition: all 0.2s;
display: flex; align-items: center; gap: 6px;
}
.asset-type-btn:hover, .asset-type-btn.active {
border-color: var(--accent-blue);
color: var(--text-primary);
background: rgba(59,130,246,0.08);
}
/* 历史对话 */
.history-list {
flex: 1;
overflow-y: auto;
padding: 8px;
}
.history-item {
padding: 8px 10px;
border-radius: 8px;
cursor: pointer;
font-size: 12px;
color: var(--text-secondary);
transition: all 0.2s;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 2px;
}
.history-item:hover { background: var(--bg-card); color: var(--text-primary); }
.history-item.active { background: rgba(212,168,67,0.1); color: var(--accent-gold); }
/* ===== CHAT AREA ===== */
.chat-area {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
}
/* XTZH实时价格面板顶部 */
.xtzh-panel {
background: linear-gradient(135deg, rgba(212,168,67,0.08), rgba(59,130,246,0.05));
border-bottom: 1px solid rgba(212,168,67,0.15);
padding: 10px 20px;
display: flex;
align-items: center;
gap: 24px;
flex-shrink: 0;
}
.xtzh-stat { display: flex; flex-direction: column; }
.xtzh-stat .stat-label { font-size: 10px; color: var(--text-muted); }
.xtzh-stat .stat-value { font-size: 14px; font-weight: 700; color: var(--accent-gold); }
.xtzh-stat .stat-sub { font-size: 10px; color: var(--text-secondary); }
.xtzh-divider { width: 1px; height: 30px; background: var(--border); }
/* 消息列表 */
.messages {
flex: 1;
overflow-y: auto;
padding: 20px;
display: flex;
flex-direction: column;
gap: 16px;
}
.messages::-webkit-scrollbar { width: 4px; }
.messages::-webkit-scrollbar-track { background: transparent; }
.messages::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
/* 欢迎界面 */
.welcome {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1;
padding: 40px 20px;
text-align: center;
}
.welcome-icon {
width: 72px; height: 72px;
background: linear-gradient(135deg, var(--accent-gold), #b8860b);
border-radius: 20px;
display: flex; align-items: center; justify-content: center;
font-size: 36px;
margin-bottom: 20px;
box-shadow: 0 8px 32px rgba(212,168,67,0.3);
}
.welcome h1 { font-size: 22px; font-weight: 800; margin-bottom: 8px; }
.welcome p { font-size: 14px; color: var(--text-secondary); max-width: 480px; line-height: 1.6; }
/* 快捷问题卡片 */
.quick-cards {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
margin-top: 28px;
max-width: 560px;
width: 100%;
}
.quick-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 14px 16px;
cursor: pointer;
transition: all 0.2s;
text-align: left;
}
.quick-card:hover {
border-color: var(--accent-gold);
background: rgba(212,168,67,0.05);
transform: translateY(-1px);
}
.quick-card .card-icon { font-size: 20px; margin-bottom: 6px; }
.quick-card .card-title { font-size: 13px; font-weight: 600; color: var(--text-primary); }
.quick-card .card-desc { font-size: 11px; color: var(--text-secondary); margin-top: 3px; line-height: 1.4; }
/* 消息气泡 */
.message { display: flex; gap: 10px; max-width: 100%; }
.message.user { flex-direction: row-reverse; }
.message-avatar {
width: 32px; height: 32px;
border-radius: 8px;
display: flex; align-items: center; justify-content: center;
font-size: 14px; flex-shrink: 0;
}
.message.user .message-avatar {
background: linear-gradient(135deg, var(--accent-blue), #1d4ed8);
}
.message.ai .message-avatar {
background: linear-gradient(135deg, var(--accent-gold), #b8860b);
color: #000; font-weight: 900;
}
.message-content {
max-width: 75%;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 12px 16px;
font-size: 14px;
line-height: 1.7;
}
.message.user .message-content {
background: rgba(59,130,246,0.12);
border-color: rgba(59,130,246,0.25);
}
.message-content pre {
background: rgba(0,0,0,0.3);
border: 1px solid var(--border);
border-radius: 8px;
padding: 12px;
overflow-x: auto;
font-size: 12px;
margin: 8px 0;
}
.message-content table {
border-collapse: collapse;
width: 100%;
margin: 8px 0;
font-size: 13px;
}
.message-content th, .message-content td {
border: 1px solid var(--border);
padding: 6px 10px;
text-align: left;
}
.message-content th { background: rgba(212,168,67,0.1); color: var(--accent-gold); }
.message-content strong { color: var(--accent-gold-light); }
/* 估值结果卡片 */
.valuation-card {
background: linear-gradient(135deg, rgba(212,168,67,0.08), rgba(16,185,129,0.05));
border: 1px solid rgba(212,168,67,0.3);
border-radius: var(--radius);
padding: 16px;
margin-top: 12px;
}
.valuation-card .vc-header {
display: flex; align-items: center; gap: 8px;
margin-bottom: 12px;
}
.valuation-card .vc-title { font-size: 13px; font-weight: 700; color: var(--accent-gold); }
.valuation-card .vc-badge {
font-size: 10px; padding: 2px 8px;
border-radius: 10px; background: rgba(16,185,129,0.15);
color: var(--accent-green); border: 1px solid rgba(16,185,129,0.3);
}
.valuation-grid {
display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px;
}
.valuation-item { display: flex; flex-direction: column; }
.valuation-item .vi-label { font-size: 10px; color: var(--text-muted); }
.valuation-item .vi-value { font-size: 16px; font-weight: 800; color: var(--text-primary); }
.valuation-item .vi-value.gold { color: var(--accent-gold); }
.valuation-item .vi-value.green { color: var(--accent-green); }
.valuation-item .vi-sub { font-size: 10px; color: var(--text-secondary); }
.confidence-bar {
margin-top: 12px;
background: rgba(255,255,255,0.05);
border-radius: 4px;
height: 4px;
overflow: hidden;
}
.confidence-fill {
height: 100%;
border-radius: 4px;
background: linear-gradient(90deg, var(--accent-green), var(--accent-gold));
transition: width 1s ease;
}
.chain-actions {
display: flex; gap: 8px; margin-top: 12px;
}
.chain-btn {
flex: 1; padding: 8px 12px;
border-radius: 8px; border: none;
font-size: 12px; font-weight: 600;
cursor: pointer; transition: all 0.2s;
}
.chain-btn.primary {
background: linear-gradient(135deg, var(--accent-gold), #b8860b);
color: #000;
}
.chain-btn.primary:hover { opacity: 0.85; }
.chain-btn.secondary {
background: var(--bg-input);
border: 1px solid var(--border);
color: var(--text-secondary);
}
.chain-btn.secondary:hover { border-color: var(--accent-blue); color: var(--accent-blue); }
/* 打字动画 */
.typing-indicator {
display: flex; align-items: center; gap: 4px; padding: 4px 0;
}
.typing-dot {
width: 6px; height: 6px;
border-radius: 50%;
background: var(--accent-gold);
animation: typing 1.4s infinite;
}
.typing-dot:nth-child(2) { animation-delay: 0.2s; }
.typing-dot:nth-child(3) { animation-delay: 0.4s; }
@keyframes typing { 0%,60%,100%{transform:translateY(0);opacity:0.4} 30%{transform:translateY(-6px);opacity:1} }
/* 流式输出光标 */
.stream-cursor {
display: inline-block;
width: 2px; height: 14px;
background: var(--accent-gold);
margin-left: 2px;
animation: blink 0.8s infinite;
vertical-align: middle;
}
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:0} }
/* ===== INPUT AREA ===== */
.input-area {
padding: 16px 20px;
background: var(--bg-secondary);
border-top: 1px solid var(--border);
flex-shrink: 0;
}
.input-wrapper {
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 12px 16px;
display: flex;
align-items: flex-end;
gap: 10px;
transition: border-color 0.2s;
}
.input-wrapper:focus-within { border-color: var(--accent-gold); }
.chat-input {
flex: 1;
background: transparent;
border: none;
outline: none;
color: var(--text-primary);
font-size: 14px;
resize: none;
min-height: 20px;
max-height: 120px;
line-height: 1.5;
font-family: inherit;
}
.chat-input::placeholder { color: var(--text-muted); }
.send-btn {
width: 36px; height: 36px;
background: linear-gradient(135deg, var(--accent-gold), #b8860b);
border: none; border-radius: 8px;
cursor: pointer;
display: flex; align-items: center; justify-content: center;
transition: opacity 0.2s; flex-shrink: 0;
}
.send-btn:hover { opacity: 0.85; }
.send-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.send-btn svg { width: 16px; height: 16px; fill: #000; }
/* 底部工具栏 */
.toolbar {
display: flex; align-items: center; gap: 8px;
margin-top: 10px; flex-wrap: wrap;
}
.toolbar-hint { font-size: 11px; color: var(--text-muted); margin-left: auto; }
.tool-chip {
display: flex; align-items: center; gap: 4px;
padding: 4px 10px;
border-radius: 12px;
font-size: 11px;
cursor: pointer;
border: 1px solid var(--border);
background: var(--bg-card);
color: var(--text-secondary);
transition: all 0.2s;
}
.tool-chip:hover { border-color: var(--accent-gold); color: var(--accent-gold); }
.tool-chip.active {
border-color: var(--accent-gold);
background: rgba(212,168,67,0.1);
color: var(--accent-gold);
}
/* 滚动条 */
.sidebar::-webkit-scrollbar,
.history-list::-webkit-scrollbar { width: 3px; }
.sidebar::-webkit-scrollbar-thumb,
.history-list::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
/* 响应式 */
@media (max-width: 768px) {
.sidebar { display: none; }
.xtzh-panel { overflow-x: auto; }
}
</style>
</head>
<body>
<!-- HEADER -->
<div class="header">
<div class="header-left">
<div class="logo">V</div>
<div>
<div class="header-title" data-i18n="title">NAC AI 资产估值引擎</div>
<div class="header-subtitle" data-i18n="subtitle">多资产 · 多辖区 · SDR锚定 · 一键上链</div>
</div>
</div>
<div class="header-right">
<div class="xtzh-price-badge" onclick="refreshXTZHPrice()" title="点击刷新XTZH实时价格">
<div class="price-dot"></div>
<div>
<div class="label">XTZH 实时价格</div>
<div class="price" id="header-xtzh-price">加载中...</div>
<div class="source" id="header-xtzh-source">NAC_SDR_MODEL_V4</div>
</div>
</div>
<select class="lang-select" id="langSelect" onchange="switchLanguage(this.value)">
<option value="zh">🇨🇳 中文</option>
<option value="en">🇺🇸 English</option>
<option value="ar">🇸🇦 العربية</option>
<option value="ja">🇯🇵 日本語</option>
<option value="ko">🇰🇷 한국어</option>
<option value="fr">🇫🇷 Français</option>
<option value="ru">🇷🇺 Русский</option>
<option value="es">🇪🇸 Español</option>
<option value="pt">🇧🇷 Português</option>
</select>
</div>
</div>
<!-- MAIN -->
<div class="main">
<!-- SIDEBAR -->
<div class="sidebar">
<div class="sidebar-header">
<button class="new-chat-btn" onclick="newChat()" data-i18n="newChat">+ 新建估值</button>
<button class="clear-btn" onclick="clearMemory()" title="清除对话记忆">🗑</button>
</div>
<!-- 辖区快速选择 -->
<div class="sidebar-section">
<div class="sidebar-section-title" data-i18n="jurisdiction">司法辖区</div>
<div class="jurisdiction-grid">
<button class="juris-btn active" onclick="setJurisdiction('HK', this)" data-code="HK">🇭🇰 香港</button>
<button class="juris-btn" onclick="setJurisdiction('SG', this)" data-code="SG">🇸🇬 新加坡</button>
<button class="juris-btn" onclick="setJurisdiction('AE', this)" data-code="AE">🇦🇪 阿联酋</button>
<button class="juris-btn" onclick="setJurisdiction('US', this)" data-code="US">🇺🇸 美国</button>
<button class="juris-btn" onclick="setJurisdiction('UK', this)" data-code="UK">🇬🇧 英国</button>
<button class="juris-btn" onclick="setJurisdiction('JP', this)" data-code="JP">🇯🇵 日本</button>
<button class="juris-btn" onclick="setJurisdiction('CN', this)" data-code="CN">🇨🇳 中国</button>
<button class="juris-btn" onclick="setJurisdiction('AU', this)" data-code="AU">🇦🇺 澳洲</button>
<button class="juris-btn" onclick="setJurisdiction('DE', this)" data-code="DE">🇩🇪 德国</button>
</div>
</div>
<!-- 资产类型 -->
<div class="sidebar-section">
<div class="sidebar-section-title" data-i18n="assetType">资产类型</div>
<div class="asset-type-list">
<button class="asset-type-btn active" onclick="setAssetType('real_estate', this)">🏢 不动产</button>
<button class="asset-type-btn" onclick="setAssetType('financial', this)">💹 金融资产</button>
<button class="asset-type-btn" onclick="setAssetType('art', this)">🎨 艺术品</button>
<button class="asset-type-btn" onclick="setAssetType('commodity', this)">🏭 大宗商品</button>
<button class="asset-type-btn" onclick="setAssetType('ip', this)">💡 知识产权</button>
<button class="asset-type-btn" onclick="setAssetType('equity', this)">📊 股权</button>
</div>
</div>
<!-- 历史对话 -->
<div class="sidebar-section">
<div class="sidebar-section-title" data-i18n="history">历史估值</div>
</div>
<div class="history-list" id="historyList"></div>
</div>
<!-- CHAT AREA -->
<div class="chat-area">
<!-- XTZH实时数据面板 -->
<div class="xtzh-panel" id="xtzhPanel">
<div class="xtzh-stat">
<div class="stat-label">XTZH/USD</div>
<div class="stat-value" id="panel-price">--</div>
<div class="stat-sub">SDR锚定</div>
</div>
<div class="xtzh-divider"></div>
<div class="xtzh-stat">
<div class="stat-label">SDR权重</div>
<div class="stat-value" id="panel-sdr">--</div>
<div class="stat-sub">五货币篮子</div>
</div>
<div class="xtzh-divider"></div>
<div class="xtzh-stat">
<div class="stat-label">黄金覆盖率</div>
<div class="stat-value" id="panel-gold">--</div>
<div class="stat-sub">储备保障</div>
</div>
<div class="xtzh-divider"></div>
<div class="xtzh-stat">
<div class="stat-label">当前辖区</div>
<div class="stat-value" id="panel-jurisdiction">HK</div>
<div class="stat-sub" id="panel-juris-name">香港</div>
</div>
<div class="xtzh-divider"></div>
<div class="xtzh-stat">
<div class="stat-label">资产类型</div>
<div class="stat-value" id="panel-asset-type">不动产</div>
<div class="stat-sub" id="panel-gnacs">GNACS-01</div>
</div>
</div>
<!-- 消息区域 -->
<div class="messages" id="messages">
<!-- 欢迎界面 -->
<div class="welcome" id="welcome">
<div class="welcome-icon">💎</div>
<h1 data-i18n="welcomeTitle">NAC AI 资产估值引擎</h1>
<p data-i18n="welcomeDesc">基于白皮书自研推理引擎支持20大类资产、60+司法辖区。采用SDR锚定的XTZH统一定价机制市场法/收益法/成本法多方法融合估值一键上链生成TOKEN权证。</p>
<div class="quick-cards">
<div class="quick-card" onclick="quickAsk('香港九龙区120平米住宅公寓市值800万港元请估算XTZH价值')">
<div class="card-icon">🏠</div>
<div class="card-title">住宅不动产估值</div>
<div class="card-desc">香港/新加坡/阿联酋住宅资产XTZH换算</div>
</div>
<div class="quick-card" onclick="quickAsk('新加坡CBD商业写字楼1200平米年租金收入180万SGD请用收益法估值')">
<div class="card-icon">🏢</div>
<div class="card-title">商业地产收益法</div>
<div class="card-desc">基于年租金收益的资本化率估值</div>
</div>
<div class="quick-card" onclick="quickAsk('请解释XTZH的SDR锚定定价机制当前价格如何计算')">
<div class="card-icon">💰</div>
<div class="card-title">XTZH定价机制</div>
<div class="card-desc">SDR五货币篮子+黄金储备保障原理</div>
</div>
<div class="quick-card" onclick="quickAsk('资产上链流程是什么从估值到TOKEN生成需要哪些步骤')">
<div class="card-icon">⛓️</div>
<div class="card-title">一键上链流程</div>
<div class="card-desc">估值→合规→TOKEN→权证→代币发行</div>
</div>
</div>
</div>
</div>
<!-- 输入区域 -->
<div class="input-area">
<div class="input-wrapper">
<textarea
class="chat-input"
id="chatInput"
rows="1"
placeholder="描述您的资产类型、位置、面积、价格等AI将自动估算XTZH价值..."
onkeydown="handleKeyDown(event)"
oninput="autoResize(this)"
></textarea>
<button class="send-btn" id="sendBtn" onclick="sendMessage()" title="发送">
<svg viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>
</button>
</div>
<div class="toolbar">
<div class="tool-chip active" id="chip-valuation" onclick="toggleMode('valuation', this)">💎 估值模式</div>
<div class="tool-chip" id="chip-chain" onclick="toggleMode('chain', this)">⛓️ 上链申请</div>
<div class="tool-chip" id="chip-compare" onclick="toggleMode('compare', this)">📊 辖区对比</div>
<div class="tool-chip" id="chip-explain" onclick="toggleMode('explain', this)">📖 方法论</div>
<div class="toolbar-hint">Enter发送 · Shift+Enter换行</div>
</div>
</div>
</div>
</div>
<script>
// ============================================================
// 配置
// ============================================================
const CONFIG = {
valuationApiBase: '/api/v4',
inferenceApiBase: '/api/inference',
maxMemory: 10,
lang: 'zh'
};
// 多语言
const I18N = {
zh: {
title: 'NAC AI 资产估值引擎',
subtitle: '多资产 · 多辖区 · SDR锚定 · 一键上链',
newChat: '+ 新建估值',
jurisdiction: '司法辖区',
assetType: '资产类型',
history: '历史估值',
welcomeTitle: 'NAC AI 资产估值引擎',
welcomeDesc: '基于白皮书自研推理引擎支持20大类资产、60+司法辖区。采用SDR锚定的XTZH统一定价机制市场法/收益法/成本法多方法融合估值一键上链生成TOKEN权证。',
placeholder: '描述您的资产类型、位置、面积、价格等AI将自动估算XTZH价值...',
sending: '正在分析...',
chainBtn: '申请上链',
detailBtn: '查看详情',
confidence: '置信度',
finalXTZH: '最终XTZH',
finalUSD: '美元估值',
xtzhPrice: 'XTZH价格',
stakeRequired: '质押要求(80%)',
engineMode: '引擎模式',
methods: '估值方法',
jurisdiction_label: '辖区',
},
en: {
title: 'NAC AI Asset Valuation Engine',
subtitle: 'Multi-Asset · Multi-Jurisdiction · SDR-Pegged · One-Click On-Chain',
newChat: '+ New Valuation',
jurisdiction: 'Jurisdiction',
assetType: 'Asset Type',
history: 'History',
welcomeTitle: 'NAC AI Asset Valuation Engine',
welcomeDesc: 'Proprietary inference engine supporting 20+ asset categories and 60+ jurisdictions. Unified XTZH pricing via SDR basket, multi-method valuation fusion, one-click on-chain TOKEN generation.',
placeholder: 'Describe your asset (type, location, area, price...) and AI will estimate XTZH value...',
sending: 'Analyzing...',
chainBtn: 'Apply On-Chain',
detailBtn: 'View Details',
confidence: 'Confidence',
finalXTZH: 'Final XTZH',
finalUSD: 'USD Valuation',
xtzhPrice: 'XTZH Price',
stakeRequired: 'Stake Required (80%)',
engineMode: 'Engine Mode',
methods: 'Methods Used',
jurisdiction_label: 'Jurisdiction',
}
};
// ============================================================
// 状态管理
// ============================================================
let state = {
messages: [],
sessionId: generateSessionId(),
conversationMemory: [],
currentJurisdiction: 'HK',
currentAssetType: 'real_estate',
currentMode: 'valuation',
xtzhPrice: null,
isStreaming: false,
history: JSON.parse(localStorage.getItem('nac_valuation_history') || '[]')
};
function generateSessionId() {
return 'vs_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
}
// ============================================================
// XTZH实时价格
// ============================================================
async function fetchXTZHPrice() {
try {
const res = await fetch(CONFIG.valuationApiBase + '/xtzh-price');
if (!res.ok) throw new Error('HTTP ' + res.status);
const data = await res.json();
state.xtzhPrice = data;
const usd = parseFloat(data.usd || data.price || 4.3944).toFixed(4);
const sdr = data.sdrBasket ? Object.values(data.sdrBasket).reduce((a,b)=>a+b,0).toFixed(2) : '1.00';
const gold = data.goldCoverage ? (data.goldCoverage * 100).toFixed(0) + '%' : '125%';
document.getElementById('header-xtzh-price').textContent = '$' + usd;
document.getElementById('header-xtzh-source').textContent = data.source || 'NAC_SDR_MODEL_V4';
document.getElementById('panel-price').textContent = '$' + usd;
document.getElementById('panel-sdr').textContent = sdr;
document.getElementById('panel-gold').textContent = gold;
} catch(e) {
// 降级显示
document.getElementById('header-xtzh-price').textContent = '$4.3944';
document.getElementById('panel-price').textContent = '$4.3944';
document.getElementById('panel-sdr').textContent = '1.00';
document.getElementById('panel-gold').textContent = '125%';
}
}
function refreshXTZHPrice() {
document.getElementById('header-xtzh-price').textContent = '刷新中...';
fetchXTZHPrice();
}
// ============================================================
// 辖区和资产类型
// ============================================================
const JURISDICTION_NAMES = {
HK: '香港', SG: '新加坡', AE: '阿联酋', US: '美国',
UK: '英国', JP: '日本', CN: '中国', AU: '澳洲', DE: '德国'
};
const GNACS_MAP = {
real_estate: 'GNACS-01', financial: 'GNACS-02', art: 'GNACS-03',
commodity: 'GNACS-04', ip: 'GNACS-05', equity: 'GNACS-06'
};
const ASSET_NAMES = {
real_estate: '不动产', financial: '金融资产', art: '艺术品',
commodity: '大宗商品', ip: '知识产权', equity: '股权'
};
function setJurisdiction(code, btn) {
state.currentJurisdiction = code;
document.querySelectorAll('.juris-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
document.getElementById('panel-jurisdiction').textContent = code;
document.getElementById('panel-juris-name').textContent = JURISDICTION_NAMES[code] || code;
}
function setAssetType(type, btn) {
state.currentAssetType = type;
document.querySelectorAll('.asset-type-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
document.getElementById('panel-asset-type').textContent = ASSET_NAMES[type] || type;
document.getElementById('panel-gnacs').textContent = GNACS_MAP[type] || 'GNACS-01';
}
function toggleMode(mode, btn) {
state.currentMode = mode;
document.querySelectorAll('.tool-chip').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
}
// ============================================================
// 消息发送与SSE流式输出
// ============================================================
async function sendMessage() {
const input = document.getElementById('chatInput');
const text = input.value.trim();
if (!text || state.isStreaming) return;
// 隐藏欢迎界面
const welcome = document.getElementById('welcome');
if (welcome) welcome.style.display = 'none';
// 添加用户消息
appendUserMessage(text);
input.value = '';
autoResize(input);
// 添加到记忆
state.conversationMemory.push({ role: 'user', content: text });
if (state.conversationMemory.length > CONFIG.maxMemory * 2) {
state.conversationMemory = state.conversationMemory.slice(-CONFIG.maxMemory * 2);
}
// 保存到历史
if (state.messages.length === 1) {
addToHistory(text.substring(0, 40) + (text.length > 40 ? '...' : ''));
}
state.isStreaming = true;
document.getElementById('sendBtn').disabled = true;
// 创建AI消息容器
const aiMsgId = 'ai_' + Date.now();
const aiDiv = appendAIMessage('', aiMsgId);
try {
// 判断是否为估值请求
const isValuationRequest = detectValuationIntent(text);
if (isValuationRequest && state.currentMode === 'valuation') {
// 调用独立估值引擎
await callValuationEngine(text, aiDiv, aiMsgId);
} else {
// 调用推理引擎SSE
await callInferenceSSE(text, aiDiv, aiMsgId);
}
} catch(e) {
updateAIMessage(aiMsgId, `⚠️ 服务暂时不可用,请稍后重试。\n\n错误:${e.message}`);
} finally {
state.isStreaming = false;
document.getElementById('sendBtn').disabled = false;
removeCursor(aiMsgId);
}
}
function detectValuationIntent(text) {
// 纯问答类关键词(优先排除,不触发估值引擎)
const qaKeywords = ['解释', '什么是', '如何', '为什么', '原理', '机制', '流程', '步骤',
'介绍', '说明', '讲解', '定义', '区别', '对比', '比较', '方法论',
'定价机制', '如何计算', '如何保障', '怎么', '怎样',
'explain', 'what is', 'how', 'why', 'process', 'methodology'];
const isQA = qaKeywords.some(k => text.toLowerCase().includes(k.toLowerCase()));
if (isQA) return false;
// 必须包含具体数值或资产描述才触发估值
const valuationKeywords = ['万元', '万港元', '万美元', '万新元', '万迪拉姆', '万英镑', '万欧元',
'平米', '平方米', '㎡', '亩', '英亩',
'市值', '售价', '成交价', '评估价',
'HKD', 'SGD', 'AED', 'USD', 'GBP', 'EUR', 'JPY', 'CNY',
'请估算', '请估值', '请评估', '帮我估'];
return valuationKeywords.some(k => text.toLowerCase().includes(k.toLowerCase()));
}
async function callValuationEngine(text, aiDiv, msgId) {
// 显示打字动画
showTyping(msgId);
try {
const res = await fetch(CONFIG.valuationApiBase + '/valuate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
description: text,
jurisdiction: state.currentJurisdiction,
assetType: state.currentAssetType,
conversationMemory: state.conversationMemory.slice(-6),
language: CONFIG.lang
})
});
if (!res.ok) throw new Error('估值引擎返回 HTTP ' + res.status);
const data = await res.json();
removeTyping(msgId);
if (data.valuation && data.valuation.finalXTZH > 0) {
// 渲染估值结果
renderValuationResult(data, aiDiv, msgId);
// 添加到记忆
state.conversationMemory.push({
role: 'assistant',
content: `估值完成:${data.valuation.finalXTZH.toFixed(2)} XTZH置信度 ${(data.valuation.confidence*100).toFixed(0)}%`
});
} else {
// 降级到推理引擎
await callInferenceSSE(text, aiDiv, msgId);
}
} catch(e) {
removeTyping(msgId);
// 降级到推理引擎
await callInferenceSSE(text, aiDiv, msgId);
}
}
async function callInferenceSSE(text, aiDiv, msgId) {
const lang = CONFIG.lang;
const systemPrompt = getSystemPrompt(lang);
const res = await fetch(CONFIG.inferenceApiBase + '/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
question: text,
language: lang,
mode: state.currentMode,
jurisdiction: state.currentJurisdiction,
assetType: state.currentAssetType,
conversationMemory: state.conversationMemory.slice(-CONFIG.maxMemory * 2),
systemPrompt: systemPrompt,
sessionId: state.sessionId
})
});
if (!res.ok) throw new Error('推理引擎返回 HTTP ' + res.status);
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let fullContent = '';
showCursor(msgId);
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (!line.startsWith('data: ')) continue;
const raw = line.slice(6).trim();
if (!raw || raw === '[DONE]') continue;
try {
const parsed = JSON.parse(raw);
if (parsed.type === 'chunk' && parsed.content) {
fullContent += parsed.content;
updateAIMessage(msgId, fullContent, true);
scrollToBottom();
} else if (parsed.type === 'done') {
break;
} else if (parsed.type === 'error') {
fullContent += '\n\n⚠ ' + parsed.message;
updateAIMessage(msgId, fullContent, false);
}
} catch(e) {
// 非JSON数据直接追加
if (raw && raw !== '[DONE]') {
fullContent += raw;
updateAIMessage(msgId, fullContent, true);
}
}
}
}
state.conversationMemory.push({ role: 'assistant', content: fullContent });
}
function getSystemPrompt(lang) {
const prompts = {
zh: `你是NAC NewAssetChain的AI资产估值专家。专注于
1. 多资产多辖区估值(住宅/商业/艺术品/金融资产等)
2. XTZH统一定价SDR锚定当前约$4.39/XTZH
3. 估值方法论(市场法/收益法/成本法/DCF
4. 一键上链流程估值→合规→TOKEN→权证→代币
5. 60+司法辖区合规要求
当前辖区:${state.currentJurisdiction},资产类型:${ASSET_NAMES[state.currentAssetType]}`,
en: `You are NAC NewAssetChain's AI Asset Valuation Expert. Focus on:
1. Multi-asset multi-jurisdiction valuation
2. XTZH unified pricing (SDR-pegged, ~$4.39/XTZH)
3. Valuation methods (Market/Income/Cost/DCF)
4. One-click on-chain process
Current jurisdiction: ${state.currentJurisdiction}, Asset type: ${state.currentAssetType}`
};
return prompts[lang] || prompts.zh;
}
// ============================================================
// 渲染估值结果
// ============================================================
function renderValuationResult(data, aiDiv, msgId) {
const v = data.valuation;
const input = data.input || {};
const lang = CONFIG.lang;
const t = I18N[lang] || I18N.zh;
const xtzhPrice = v.xtzhPrice ? v.xtzhPrice.usd : 4.3944;
const confidence = v.confidence || 0;
const finalXTZH = v.finalXTZH || 0;
const finalUSD = v.finalUSD || 0;
const stakeXTZH = finalXTZH * 0.8;
const methods = v.methodsUsed || [];
const rules = v.appliedRules || [];
// 构建方法摘要
let methodSummary = '';
if (methods.length > 0) {
methodSummary = `\n\n**估值方法融合:** ${methods.join(' + ')}`;
}
if (rules.length > 0) {
methodSummary += `\n**适用规则:** ${rules.join('、')}`;
}
// 估值摘要文本
const summaryText = `**资产估值完成** ✅
根据您描述的资产AI估值引擎已完成多方法融合分析${methodSummary}
> 以上估值基于NAC自研推理引擎GNACS分类 + 辖区适配 + 规则引擎采用SDR锚定XTZH统一定价机制。`;
updateAIMessage(msgId, summaryText, false);
// 添加估值结果卡片
const contentEl = document.querySelector(`#${msgId} .message-content`);
if (contentEl) {
const card = document.createElement('div');
card.className = 'valuation-card';
card.innerHTML = `
<div class="vc-header">
<span class="vc-title">💎 ${t.finalXTZH} 估值结果</span>
<span class="vc-badge">置信度 ${(confidence*100).toFixed(0)}%</span>
<span class="vc-badge" style="background:rgba(59,130,246,0.15);color:#60a5fa;border-color:rgba(59,130,246,0.3)">
${data.engineMode || 'SELF_INFERENCE_V4'}
</span>
</div>
<div class="valuation-grid">
<div class="valuation-item">
<div class="vi-label">${t.finalXTZH}</div>
<div class="vi-value gold">${finalXTZH.toLocaleString('zh-CN', {maximumFractionDigits:2})} XTZH</div>
<div class="vi-sub">≈ $${finalUSD.toLocaleString('en-US', {maximumFractionDigits:0})} USD</div>
</div>
<div class="valuation-item">
<div class="vi-label">${t.xtzhPrice}</div>
<div class="vi-value">$${parseFloat(xtzhPrice).toFixed(4)}</div>
<div class="vi-sub">${v.xtzhPrice ? v.xtzhPrice.source : 'NAC_SDR_MODEL_V4'}</div>
</div>
<div class="valuation-item">
<div class="vi-label">${t.stakeRequired}</div>
<div class="vi-value green">${stakeXTZH.toLocaleString('zh-CN', {maximumFractionDigits:2})} XTZH</div>
<div class="vi-sub">可发行权益代币数量</div>
</div>
<div class="valuation-item">
<div class="vi-label">辖区 / GNACS</div>
<div class="vi-value">${state.currentJurisdiction}</div>
<div class="vi-sub">${GNACS_MAP[state.currentAssetType] || 'GNACS-01'}</div>
</div>
</div>
<div class="confidence-bar">
<div class="confidence-fill" style="width:${confidence*100}%"></div>
</div>
<div class="chain-actions">
<button class="chain-btn primary" onclick="applyOnChain(${JSON.stringify({finalXTZH, finalUSD, jurisdiction: state.currentJurisdiction}).replace(/"/g,'&quot;')})">
⛓️ ${t.chainBtn}
</button>
<button class="chain-btn secondary" onclick="viewValuationDetail()">
📄 ${t.detailBtn}
</button>
<button class="chain-btn secondary" onclick="compareJurisdictions()">
🌍 辖区对比
</button>
</div>
`;
contentEl.appendChild(card);
}
scrollToBottom();
}
function applyOnChain(data) {
const msg = `我希望将这个资产上链,估值为 ${data.finalXTZH.toFixed(2)} XTZH$${data.finalUSD.toFixed(0)} USD辖区 ${data.jurisdiction}。请告诉我上链流程和需要准备的材料。`;
document.getElementById('chatInput').value = msg;
toggleMode('chain', document.getElementById('chip-chain'));
sendMessage();
}
function viewValuationDetail() {
quickAsk('请详细解释刚才的估值计算过程,包括使用的方法、权重分配和辖区调整系数');
}
function compareJurisdictions() {
quickAsk(`请对比香港、新加坡、阿联酋三个辖区对于${ASSET_NAMES[state.currentAssetType]}的估值差异和辖区乘数`);
}
// ============================================================
// DOM操作
// ============================================================
function appendUserMessage(text) {
const messages = document.getElementById('messages');
const div = document.createElement('div');
div.className = 'message user';
div.innerHTML = `
<div class="message-avatar">👤</div>
<div class="message-content">${escapeHtml(text)}</div>
`;
messages.appendChild(div);
state.messages.push({ role: 'user', content: text });
scrollToBottom();
}
function appendAIMessage(content, id) {
const messages = document.getElementById('messages');
const div = document.createElement('div');
div.className = 'message ai';
div.id = id;
div.innerHTML = `
<div class="message-avatar">V</div>
<div class="message-content">${content ? renderMarkdown(content) : '<div class="typing-indicator"><div class="typing-dot"></div><div class="typing-dot"></div><div class="typing-dot"></div></div>'}</div>
`;
messages.appendChild(div);
scrollToBottom();
return div;
}
function updateAIMessage(id, content, streaming) {
const el = document.querySelector(`#${id} .message-content`);
if (!el) return;
el.innerHTML = renderMarkdown(content) + (streaming ? '<span class="stream-cursor"></span>' : '');
}
function showTyping(id) {
const el = document.querySelector(`#${id} .message-content`);
if (el) el.innerHTML = '<div class="typing-indicator"><div class="typing-dot"></div><div class="typing-dot"></div><div class="typing-dot"></div></div>';
}
function removeTyping(id) {
const el = document.querySelector(`#${id} .message-content`);
if (el) el.innerHTML = '';
}
function showCursor(id) {
const el = document.querySelector(`#${id} .message-content`);
if (el && !el.querySelector('.stream-cursor')) {
el.innerHTML += '<span class="stream-cursor"></span>';
}
}
function removeCursor(id) {
const cursor = document.querySelector(`#${id} .stream-cursor`);
if (cursor) cursor.remove();
}
function scrollToBottom() {
const messages = document.getElementById('messages');
messages.scrollTop = messages.scrollHeight;
}
// ============================================================
// Markdown渲染
// ============================================================
function escapeInline(text) {
return text.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
function renderMarkdown(text) {
if (!text) return '';
const lines = text.split('\n');
const output = [];
let inTable = false;
let tableRows = [];
let isFirstRow = true;
function flushTable() {
if (tableRows.length) {
output.push('<table style="border-collapse:collapse;width:100%;margin:8px 0;font-size:13px">' + tableRows.join('') + '</tbody></table>');
}
inTable = false; tableRows = []; isFirstRow = true;
}
function processLine(line) {
const e = escapeInline(line);
if (/^### /.test(e)) return e.replace(/^### (.+)$/, '<h3 style="color:var(--accent-gold-light);margin:10px 0 5px;font-size:14px">$1</h3>');
if (/^## /.test(e)) return e.replace(/^## (.+)$/, '<h2 style="color:var(--accent-gold);margin:12px 0 6px;font-size:15px">$1</h2>');
if (/^# /.test(e)) return e.replace(/^# (.+)$/, '<h1 style="color:var(--accent-gold);margin:14px 0 8px;font-size:16px">$1</h1>');
if (/^&gt; /.test(e)) return e.replace(/^&gt; (.+)$/, '<blockquote style="border-left:3px solid var(--accent-gold);padding-left:12px;color:var(--text-secondary);margin:8px 0">$1</blockquote>');
if (/^[-*] /.test(e)) return e.replace(/^[-*] (.+)$/, (_, content) => {
const inlined = content
.replace(/\*\*(.+?)\*\*/g, '<strong style="color:var(--accent-gold-light)">$1</strong>')
.replace(/\*(.+?)\*/g, '<em>$1</em>')
.replace(/`([^`]+)`/g, '<code style="background:rgba(0,0,0,0.3);padding:2px 6px;border-radius:4px;font-size:12px">$1</code>');
return `<li style="margin:3px 0 3px 16px">${inlined}</li>`;
});
return e
.replace(/\*\*(.+?)\*\*/g, '<strong style="color:var(--accent-gold-light)">$1</strong>')
.replace(/\*(.+?)\*/g, '<em>$1</em>')
.replace(/`([^`]+)`/g, '<code style="background:rgba(0,0,0,0.3);padding:2px 6px;border-radius:4px;font-size:12px">$1</code>');
}
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const trimmed = line.trim();
const isTableRow = /^\|.+\|$/.test(trimmed);
const isSeparator = /^\|[-|: ]+\|$/.test(trimmed);
if (isTableRow && !isSeparator) {
if (!inTable) { inTable = true; isFirstRow = true; tableRows = []; }
const cells = trimmed.split('|').filter(c => c !== '').map(c => escapeInline(c.trim())
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>'));
if (isFirstRow) {
tableRows.push('<thead><tr>' + cells.map(c => `<th style="background:rgba(212,168,67,0.1);color:var(--accent-gold);border:1px solid var(--border);padding:6px 10px">${c}</th>`).join('') + '</tr></thead><tbody>');
isFirstRow = false;
} else {
tableRows.push('<tr>' + cells.map(c => `<td style="border:1px solid var(--border);padding:6px 10px">${c}</td>`).join('') + '</tr>');
}
} else if (isSeparator) {
// skip separator
} else {
if (inTable) flushTable();
if (trimmed === '') {
output.push('<br>');
} else {
output.push(processLine(line));
}
}
}
if (inTable) flushTable();
// 将连续的 li 包裹在 ul 中
return output.join('\n')
.replace(/(<li[^>]*>[\s\S]*?<\/li>\n?)+/g, m => `<ul style="margin:6px 0">${m}</ul>`)
.replace(/\n/g, '<br>');
}
function escapeHtml(text) {
return text.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
// ============================================================
// 快捷问题
// ============================================================
function quickAsk(text) {
document.getElementById('chatInput').value = text;
sendMessage();
}
// ============================================================
// 新建对话 / 清除记忆
// ============================================================
function newChat() {
state.messages = [];
state.conversationMemory = [];
state.sessionId = generateSessionId();
state.isStreaming = false;
const messages = document.getElementById('messages');
messages.innerHTML = `
<div class="welcome" id="welcome">
<div class="welcome-icon">💎</div>
<h1>NAC AI 资产估值引擎</h1>
<p>基于白皮书自研推理引擎支持20大类资产、60+司法辖区。采用SDR锚定的XTZH统一定价机制一键上链生成TOKEN权证。</p>
<div class="quick-cards">
<div class="quick-card" onclick="quickAsk('香港九龙区120平米住宅公寓市值800万港元请估算XTZH价值')">
<div class="card-icon">🏠</div>
<div class="card-title">住宅不动产估值</div>
<div class="card-desc">香港/新加坡/阿联酋住宅资产XTZH换算</div>
</div>
<div class="quick-card" onclick="quickAsk('新加坡CBD商业写字楼1200平米年租金收入180万SGD请用收益法估值')">
<div class="card-icon">🏢</div>
<div class="card-title">商业地产收益法</div>
<div class="card-desc">基于年租金收益的资本化率估值</div>
</div>
<div class="quick-card" onclick="quickAsk('请解释XTZH的SDR锚定定价机制当前价格如何计算')">
<div class="card-icon">💰</div>
<div class="card-title">XTZH定价机制</div>
<div class="card-desc">SDR五货币篮子+黄金储备保障原理</div>
</div>
<div class="quick-card" onclick="quickAsk('资产上链流程是什么从估值到TOKEN生成需要哪些步骤')">
<div class="card-icon">⛓️</div>
<div class="card-title">一键上链流程</div>
<div class="card-desc">估值→合规→TOKEN→权证→代币发行</div>
</div>
</div>
</div>
`;
document.getElementById('sendBtn').disabled = false;
}
function clearMemory() {
state.conversationMemory = [];
newChat();
}
// ============================================================
// 历史记录
// ============================================================
function addToHistory(title) {
const item = { id: state.sessionId, title, time: Date.now() };
state.history.unshift(item);
if (state.history.length > 20) state.history = state.history.slice(0, 20);
localStorage.setItem('nac_valuation_history', JSON.stringify(state.history));
renderHistory();
}
function renderHistory() {
const list = document.getElementById('historyList');
list.innerHTML = state.history.map(item => `
<div class="history-item ${item.id === state.sessionId ? 'active' : ''}" onclick="loadHistory('${item.id}')">
💎 ${item.title}
</div>
`).join('');
}
function loadHistory(id) {
// 简单切换激活状态(完整实现需要持久化消息)
document.querySelectorAll('.history-item').forEach(el => el.classList.remove('active'));
event.target.classList.add('active');
}
// ============================================================
// 语言切换
// ============================================================
function switchLanguage(lang) {
CONFIG.lang = lang;
document.querySelectorAll('[data-i18n]').forEach(el => {
const key = el.getAttribute('data-i18n');
const t = I18N[lang] || I18N.zh;
if (t[key]) el.textContent = t[key];
});
const input = document.getElementById('chatInput');
const t = I18N[lang] || I18N.zh;
if (t.placeholder) input.placeholder = t.placeholder;
}
// ============================================================
// 键盘事件
// ============================================================
function handleKeyDown(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
}
function autoResize(el) {
el.style.height = 'auto';
el.style.height = Math.min(el.scrollHeight, 120) + 'px';
}
// ============================================================
// 初始化
// ============================================================
document.addEventListener('DOMContentLoaded', () => {
fetchXTZHPrice();
setInterval(fetchXTZHPrice, 60000); // 每分钟刷新
renderHistory();
document.getElementById('chatInput').focus();
});
</script>
</body>
</html>