mirror of
https://gitcode.com/JianFeeeee/TrulyMEM-TrueHumanMEM.git
synced 2026-09-20 17:08:18 +00:00
feat: add markdown rendering, API settings, star graph auto-view toggle
- index.html: markdown rendering with marked.js + DOMPurify XSS protection - settings.html: API Key/Base URL/Model config form with save - graph.html: auto camera transition toggle for add/read/delete nodes
This commit is contained in:
@ -300,7 +300,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
.chat-msg .markdown-content p { margin: 4px 0; }
|
||||
.chat-msg .markdown-content code { background: rgba(255,255,255,0.1); padding: 1px 4px; border-radius: 3px; font-size: 12px; }
|
||||
.chat-msg .markdown-content pre { background: rgba(0,0,0,0.3); padding: 8px; border-radius: 4px; overflow-x: auto; margin: 6px 0; }
|
||||
.chat-msg .markdown-content pre code { background: none; padding: 0; }
|
||||
.chat-msg .markdown-content ul, .chat-msg .markdown-content ol { padding-left: 20px; margin: 4px 0; }
|
||||
.chat-msg .markdown-content h1, .chat-msg .markdown-content h2, .chat-msg .markdown-content h3, .chat-msg .markdown-content h4 { margin: 8px 0 4px; color: #aaccff; }
|
||||
.chat-msg .markdown-content blockquote { border-left: 3px solid #4488ff; padding-left: 10px; margin: 4px 0; color: #8899bb; }
|
||||
.chat-msg .markdown-content a { color: #4488ff; text-decoration: underline; }
|
||||
.chat-msg .markdown-content strong { color: #fff; }
|
||||
.chat-msg .markdown-content table { border-collapse: collapse; margin: 6px 0; width: 100%; }
|
||||
.chat-msg .markdown-content th, .chat-msg .markdown-content td { border: 1px solid rgba(100,100,255,0.3); padding: 4px 8px; text-align: left; }
|
||||
.chat-msg .markdown-content th { background: rgba(68,136,255,0.2); }
|
||||
.chat-msg .markdown-content hr { border: none; border-top: 1px solid rgba(100,100,255,0.2); margin: 8px 0; }
|
||||
|
||||
</style>
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.0.6/purify.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app-container">
|
||||
@ -312,6 +328,14 @@
|
||||
<p>节点: <span id="node-count">0</span></p>
|
||||
<p>边: <span id="edge-count">0</span></p>
|
||||
<p>状态: <span id="status">初始化中...</span></p>
|
||||
<p style="margin-top:8px;display:flex;align-items:center;gap:6px">
|
||||
<span style="color:#8888aa;font-size:12px">自动视角</span>
|
||||
<label style="position:relative;width:36px;height:20px;cursor:pointer;flex-shrink:0">
|
||||
<input type="checkbox" id="autoViewToggle" checked style="display:none">
|
||||
<span style="position:absolute;inset:0;background:rgba(60,60,80,0.8);border-radius:10px;transition:all 0.3s;border:1px solid rgba(100,100,255,0.2)"></span>
|
||||
<span style="position:absolute;width:16px;height:16px;left:2px;bottom:2px;background:#6666aa;border-radius:50%;transition:all 0.3s" class="auto-slider"></span>
|
||||
</label>
|
||||
</p>
|
||||
</div>
|
||||
<div id="node-info">
|
||||
<h3 id="info-name"></h3>
|
||||
@ -348,6 +372,17 @@
|
||||
<script>
|
||||
// Check authentication
|
||||
fetch('/api/check-auth').then(r=>r.json()).then(d=>{if(!d.authenticated)window.location.href='/login'}).catch(()=>window.location.href='/login')
|
||||
|
||||
function renderMarkdown(text) {
|
||||
if (!text) return '';
|
||||
try {
|
||||
const html = marked.parse(text);
|
||||
return DOMPurify.sanitize(html);
|
||||
} catch(e) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
// 全局变量
|
||||
let scene, camera, renderer, controls;
|
||||
let nodes = [], edges = [];
|
||||
@ -356,6 +391,7 @@
|
||||
let raycaster, mouse;
|
||||
let hoveredNode = null, selectedNode = null;
|
||||
let animationId;
|
||||
let autoViewEnabled = true;
|
||||
let highlightPulse = 0;
|
||||
let lastHighlightCheck = 0;
|
||||
let currentHighlightIds = [];
|
||||
@ -450,6 +486,12 @@ function init() {
|
||||
}
|
||||
|
||||
// 创建星空背景
|
||||
|
||||
// 自动视角开关事件
|
||||
document.getElementById("autoViewToggle").addEventListener("change", function() {
|
||||
autoViewEnabled = this.checked;
|
||||
});
|
||||
|
||||
function createStarField() {
|
||||
const starCount = 3000;
|
||||
const positions = new Float32Array(starCount * 3);
|
||||
@ -847,7 +889,14 @@ function init() {
|
||||
});
|
||||
// 相机距离:节点范围加 20u 余量,但不超过 120u,不低于 25u
|
||||
const targetDist = Math.min(Math.max(maxDist + 15, 25), 120);
|
||||
camera.position.set(targetDist * 0.9, targetDist * 0.6, targetDist * 0.9);
|
||||
// 如果有待聚焦节点且自动视角开启,飞过去;否则全局定位
|
||||
if (_pendingFocusNodeId) {
|
||||
const focusId = _pendingFocusNodeId;
|
||||
_pendingFocusNodeId = null;
|
||||
setTimeout(() => flyToNode(focusId, 800), 100);
|
||||
} else {
|
||||
camera.position.set(targetDist * 0.9, targetDist * 0.6, targetDist * 0.9);
|
||||
}
|
||||
controls.target.set(0, 0, 0);
|
||||
controls.update();
|
||||
}
|
||||
@ -972,6 +1021,7 @@ function init() {
|
||||
}
|
||||
|
||||
// 已消费的高亮变更ID(防止重复触发)
|
||||
let _pendingFocusNodeId = null;
|
||||
let _consumedHighlightIds = new Set();
|
||||
|
||||
// 平滑重新定位剩余的节点
|
||||
@ -1082,6 +1132,20 @@ function smoothReposition() {
|
||||
// 自适应相机
|
||||
function fitCameraToGraph() {
|
||||
if (nodeMeshes.length === 0) return;
|
||||
if (!autoViewEnabled) {
|
||||
// 关闭自动视角时直接跳转
|
||||
let maxDist = 0;
|
||||
nodeMeshes.forEach(m => {
|
||||
const d = m.position.length();
|
||||
if (d > maxDist) maxDist = d;
|
||||
});
|
||||
if (maxDist < 1) maxDist = 30;
|
||||
const targetDist = Math.min(Math.max(maxDist + 15, 25), 120);
|
||||
camera.position.set(targetDist * 0.9, targetDist * 0.6, targetDist * 0.9);
|
||||
controls.target.set(0, 0, 0);
|
||||
controls.update();
|
||||
return;
|
||||
}
|
||||
let maxDist = 0;
|
||||
nodeMeshes.forEach(m => {
|
||||
const d = m.position.length();
|
||||
@ -1105,6 +1169,28 @@ function smoothReposition() {
|
||||
lerpCamera();
|
||||
}
|
||||
|
||||
// 飞向指定节点(自动视角)
|
||||
function flyToNode(nodeId, duration = 600) {
|
||||
if (!autoViewEnabled) return;
|
||||
const mesh = nodeMeshes.find(m => m.userData.nodeId === nodeId);
|
||||
if (!mesh) return;
|
||||
const targetPos = mesh.position.clone();
|
||||
const startPos = camera.position.clone();
|
||||
const startTarget = controls.target.clone();
|
||||
const distance = targetPos.length() + 25;
|
||||
const endCamPos = new THREE.Vector3(targetPos.x, targetPos.y + distance * 0.4, targetPos.z + distance * 0.8);
|
||||
const startTime = Date.now();
|
||||
function lerp() {
|
||||
const elapsed = Date.now() - startTime;
|
||||
const t = Math.min(elapsed / duration, 1);
|
||||
const ease = 1 - Math.pow(1 - t, 3);
|
||||
camera.position.lerpVectors(startPos, endCamPos, ease);
|
||||
controls.target.lerpVectors(startTarget, targetPos, ease);
|
||||
if (t < 1) requestAnimationFrame(lerp);
|
||||
}
|
||||
lerp();
|
||||
}
|
||||
|
||||
// 检查高亮 — 增量更新,不再全量 reload
|
||||
async function checkHighlight() {
|
||||
try {
|
||||
@ -1132,6 +1218,7 @@ function smoothReposition() {
|
||||
if (data.new_node_id && !_consumedHighlightIds.has('new:' + data.new_node_id)) {
|
||||
_consumedHighlightIds.add('new:' + data.new_node_id);
|
||||
// 新节点不在当前场景中,重新加载完整图
|
||||
_pendingFocusNodeId = data.new_node_id;
|
||||
loadGraphData();
|
||||
setTimeout(showDebugInfo, 500);
|
||||
return;
|
||||
@ -1573,7 +1660,10 @@ function smoothReposition() {
|
||||
if (parsed.content) displayText = parsed.content;
|
||||
} catch(e) {}
|
||||
|
||||
div.textContent = displayText;
|
||||
const contentDiv = document.createElement('div');
|
||||
contentDiv.className = 'markdown-content';
|
||||
contentDiv.innerHTML = renderMarkdown(displayText);
|
||||
div.appendChild(contentDiv);
|
||||
const time = document.createElement('div');
|
||||
time.className = 'chat-msg-time';
|
||||
time.textContent = new Date().toLocaleTimeString();
|
||||
@ -1620,7 +1710,7 @@ function smoothReposition() {
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
const reply = data.data?.content || '(无回复)';
|
||||
const reply = data.content || data.data?.content || '(无回复)';
|
||||
addChatMessage('assistant', reply);
|
||||
chatStatus.textContent = '🟢 已连接';
|
||||
chatStatus.className = 'chat-status';
|
||||
|
||||
@ -514,7 +514,24 @@ body {
|
||||
white-space: pre-wrap;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.message-content h1,.message-content h2,.message-content h3{color:#e94560;margin:8px 0 4px}
|
||||
.message-content p{margin:4px 0}
|
||||
.message-content ul,.message-content ol{margin:4px 0 4px 20px}
|
||||
.message-content code{background:#12121f;color:#ffd700;padding:1px 4px;border-radius:3px;font-size:0.9em}
|
||||
.message-content pre{background:#12121f;padding:8px;border-radius:4px;overflow-x:auto;margin:8px 0;border-left:3px solid #e94560}
|
||||
.message-content pre code{background:none;color:#eee;padding:0}
|
||||
.message-content blockquote{border-left:3px solid #0f3460;padding-left:8px;margin:8px 0;color:#888}
|
||||
.message-content a{color:#4169e1;text-decoration:none}
|
||||
.message-content a:hover{text-decoration:underline}
|
||||
.message-content table{border-collapse:collapse;margin:8px 0;width:100%}
|
||||
.message-content th,.message-content td{border:1px solid #0f3460;padding:4px 8px}
|
||||
.message-content th{background:#12121f;color:#e94560}
|
||||
.message-content img{max-width:100%;border-radius:4px}
|
||||
.message-content hr{border:none;border-top:1px solid #0f3460;margin:8px 0}
|
||||
</style>
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.0.6/purify.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@ -654,6 +671,15 @@ async function init() {
|
||||
addWelcomeMessage();
|
||||
}
|
||||
|
||||
// Markdown Rendering
|
||||
function renderMarkdown(text) {
|
||||
if (typeof marked !== 'undefined' && typeof DOMPurify !== 'undefined') {
|
||||
marked.setOptions({ breaks: true, gfm: true });
|
||||
return DOMPurify.sanitize(marked.parse(text));
|
||||
}
|
||||
return text.replace(/\n/g, '<br>');
|
||||
}
|
||||
|
||||
// API Functions
|
||||
async function apiRequest(endpoint, method = 'GET', data = null) {
|
||||
const options = {
|
||||
@ -739,7 +765,7 @@ function addMessageToUI(role, content, toolCalls = null) {
|
||||
|
||||
const contentEl = document.createElement('div');
|
||||
contentEl.className = 'message-content';
|
||||
contentEl.textContent = content;
|
||||
contentEl.innerHTML = marked.parse(content);
|
||||
|
||||
widget.appendChild(header);
|
||||
widget.appendChild(contentEl);
|
||||
@ -790,7 +816,7 @@ function updateLatestMessage(content, toolCalls = null) {
|
||||
const latest = widgets[widgets.length - 1];
|
||||
const contentEl = latest.querySelector('.message-content');
|
||||
if (contentEl) {
|
||||
contentEl.textContent = content;
|
||||
contentEl.innerHTML = marked.parse(content);
|
||||
}
|
||||
|
||||
// Add tool calls if present
|
||||
|
||||
@ -189,6 +189,25 @@
|
||||
<div class="settings-container">
|
||||
<h1 class="settings-title"><i class="fas fa-cog"></i> 设置</h1>
|
||||
|
||||
<!-- API 配置 -->
|
||||
<div class="section-title"><i class="fas fa-plug"></i> API 配置</div>
|
||||
<form id="apiConfigForm">
|
||||
<div class="form-group">
|
||||
<label for="settings_api_key">API Key</label>
|
||||
<input type="password" id="settings_api_key" placeholder="输入 API Key">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="settings_base_url">Base URL</label>
|
||||
<input type="text" id="settings_base_url" placeholder="https://api.deepseek.com">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="settings_model">Model</label>
|
||||
<input type="text" id="settings_model" placeholder="deepseek-chat">
|
||||
</div>
|
||||
<button type="submit" class="btn" id="saveApiConfigBtn">保 存 API 配 置</button>
|
||||
</form>
|
||||
<div id="apiConfigStatus" style="text-align:center;font-size:12px;color:#666688;margin-top:5px;"></div>
|
||||
|
||||
<!-- 修改密码 -->
|
||||
<div class="section-title"><i class="fas fa-key"></i> 修改密码</div>
|
||||
<form id="passwordForm">
|
||||
@ -451,6 +470,65 @@
|
||||
|
||||
init();
|
||||
|
||||
// API 配置加载与保存
|
||||
async function loadApiConfig() {
|
||||
try {
|
||||
const resp = await fetch('/api/settings');
|
||||
const data = await resp.json();
|
||||
if (data.success && data.data) {
|
||||
const cfg = data.data.api_config || {};
|
||||
document.getElementById('settings_api_key').value = cfg.api_key || '';
|
||||
document.getElementById('settings_base_url').value = cfg.base_url || 'https://api.deepseek.com';
|
||||
document.getElementById('settings_model').value = cfg.model || 'deepseek-chat';
|
||||
document.getElementById('apiConfigStatus').textContent = '✅ 配置已加载';
|
||||
}
|
||||
} catch (e) {
|
||||
document.getElementById('apiConfigStatus').textContent = '⚠️ 无法加载配置';
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('apiConfigForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const btn = document.getElementById('saveApiConfigBtn');
|
||||
btn.disabled = true;
|
||||
btn.textContent = '保存中...';
|
||||
const statusEl = document.getElementById('apiConfigStatus');
|
||||
statusEl.textContent = '🔄 保存中...';
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/settings', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
api_config: {
|
||||
api_key: document.getElementById('settings_api_key').value,
|
||||
base_url: document.getElementById('settings_base_url').value,
|
||||
model: document.getElementById('settings_model').value
|
||||
}
|
||||
})
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (data.success) {
|
||||
statusEl.textContent = '✅ API 配置已保存并生效';
|
||||
setTimeout(() => { statusEl.textContent = ''; }, 3000);
|
||||
} else {
|
||||
statusEl.textContent = '⚠️ 保存失败: ' + (data.error || '未知错误');
|
||||
}
|
||||
} catch (e) {
|
||||
statusEl.textContent = '⚠️ 网络错误';
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = '保 存 API 配 置';
|
||||
}
|
||||
});
|
||||
|
||||
// 补充 loadConfig 中调用 loadApiConfig
|
||||
const origLoadConfig = loadConfig;
|
||||
loadConfig = function() {
|
||||
origLoadConfig();
|
||||
loadApiConfig();
|
||||
};
|
||||
|
||||
// TUI 开关
|
||||
document.getElementById('enableTuiToggle').addEventListener('change', async function() {
|
||||
const enable = this.checked;
|
||||
|
||||
Reference in New Issue
Block a user