feat: 添加 AIaToolbox深色主题页面
- 新增 AIaToolbox_dark.html 文件,实现深色主题的智能助手页面 - 优化页面样式,适配暗黑模式下的视觉效果 - 引入高亮代码和数学公式渲染功能 - 实现消息流式加载和动态渲染 - 添加复制代码和折叠长消息的功能
This commit is contained in:
867
src/main/resources/javascript/AIaToolbox_dark.html
Normal file
867
src/main/resources/javascript/AIaToolbox_dark.html
Normal file
@@ -0,0 +1,867 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<title>DeepSeek - 智能助手</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<!-- 动态加载主题样式 -->
|
||||
<link id="hljs-light" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/intellij-light.min.css" media="(prefers-color-scheme: light)">
|
||||
<link id="hljs-dark" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css" media="(prefers-color-scheme: dark)">
|
||||
<!-- KaTeX 核心样式 -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
|
||||
<!-- KaTeX 核心库 -->
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
|
||||
<!-- 自动渲染扩展 -->
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
/* 色彩体系 */
|
||||
--primary-color: #58a6ff;
|
||||
--primary-hover: #79b8ff;
|
||||
--bg-gradient: linear-gradient(158deg, #0a0d12 0%, #141920 100%);
|
||||
--card-bg: rgba(22, 26, 34, 0.98);
|
||||
--text-primary: #e3e6eb;
|
||||
--text-secondary: #8b949e;
|
||||
--border-color: rgba(255, 255, 255, 0.12);
|
||||
--shadow: 0 12px 32px -8px rgba(0, 0, 0, 0.4);
|
||||
--code-bg: rgba(255, 255, 255, 0.08);
|
||||
|
||||
/* 状态色 */
|
||||
--success: #3ba776;
|
||||
--warning: #d29922;
|
||||
--error: #da3633;
|
||||
|
||||
/* 特殊效果 */
|
||||
--glow-effect: radial-gradient(circle at 50% 0%, rgba(88,166,255,0.15) 0%, transparent 70%);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--bg-gradient);
|
||||
color: var(--text-primary);
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 容器样式 */
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 20px auto;
|
||||
flex: 1;
|
||||
width: 95%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.chat-container {
|
||||
background: transparent !important;
|
||||
backdrop-filter: none;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* 消息区域 */
|
||||
.messages {
|
||||
flex: 1;
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
scroll-behavior: smooth;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
/* 消息气泡 */
|
||||
.message {
|
||||
max-width: 85%;
|
||||
opacity: 0;
|
||||
animation: messageIn 0.3s ease-out forwards;
|
||||
position: relative;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.message:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px -6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
@keyframes messageIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* 用户消息 */
|
||||
.message.user {
|
||||
align-self: flex-end;
|
||||
background: var(--primary-color);
|
||||
color: #ffffff;
|
||||
border-radius: 20px 20px 4px 20px;
|
||||
box-shadow: 0 4px 12px -2px rgba(88, 166, 255, 0.3);
|
||||
}
|
||||
|
||||
/* AI消息 */
|
||||
.message.ai {
|
||||
align-self: flex-start;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px 20px 20px 20px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.bubble {
|
||||
padding: 18px 24px;
|
||||
line-height: 1.8;
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* 流式光标 */
|
||||
.streaming-cursor {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 1em;
|
||||
background: var(--text-primary);
|
||||
margin-left: 4px;
|
||||
vertical-align: middle;
|
||||
animation: cursorPulse 1.2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes cursorPulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.3; }
|
||||
}
|
||||
|
||||
/* 输入区域 */
|
||||
.input-area {
|
||||
padding: 24px;
|
||||
background: rgba(22, 26, 34, 0.95);
|
||||
border-top: 1px solid var(--border-color);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
padding: 16px 24px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 30px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: var(--text-primary);
|
||||
font-size: 16px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px rgba(88, 166, 255, 0.2);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
button {
|
||||
padding: 16px 32px;
|
||||
border: none;
|
||||
border-radius: 30px;
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: var(--primary-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
button::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(120deg,
|
||||
rgba(255,255,255,0.1) 0%,
|
||||
rgba(255,255,255,0.05) 50%,
|
||||
rgba(255,255,255,0.1) 100%
|
||||
);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
button:hover::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 代码块样式 */
|
||||
pre {
|
||||
background: var(--code-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
position: relative;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.9em;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* 复制按钮 */
|
||||
.copy-btn {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 12px;
|
||||
padding: 6px 12px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: rgba(255,255,255,0.1);
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.copy-btn:hover {
|
||||
background: rgba(255,255,255,0.15);
|
||||
}
|
||||
|
||||
/* 折叠按钮 */
|
||||
.fold-btn {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
bottom: 10px;
|
||||
background: linear-gradient(180deg,
|
||||
rgba(22,26,34,0) 0%,
|
||||
rgba(22,26,34,0.95) 60%
|
||||
);
|
||||
padding: 6px 12px;
|
||||
border: none;
|
||||
color: var(--primary-color);
|
||||
font-size: 0.85em;
|
||||
cursor: pointer;
|
||||
display: none;
|
||||
z-index: 2;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
background: var(--card-bg);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(255,255,255,0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 其他组件 */
|
||||
.thinking-content {
|
||||
color: var(--text-secondary);
|
||||
font-style: italic;
|
||||
margin-bottom: 12px;
|
||||
padding: 8px 12px;
|
||||
background: rgba(255,255,255,0.05);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
color: white;
|
||||
padding: 12px 24px;
|
||||
border-radius: 30px;
|
||||
font-size: 0.9em;
|
||||
animation: toastIn 0.3s ease-out;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
/* 数学公式样式 */
|
||||
.katex {
|
||||
font-size: 1.1em;
|
||||
padding: 0 0.2em;
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
.math-block {
|
||||
margin: 1em 0;
|
||||
padding: 1em;
|
||||
background: var(--code-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/java.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/python.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/javascript.min.js"></script>
|
||||
|
||||
<div class="container">
|
||||
<div class="chat-container">
|
||||
<div class="messages" id="messages">
|
||||
<div class="typing-indicator" id="typing">
|
||||
<div class="dot-flashing"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-area">
|
||||
<div class="input-wrapper">
|
||||
<input type="text"
|
||||
id="input"
|
||||
placeholder="输入您的问题..."
|
||||
autocomplete="off">
|
||||
<button onclick="sendMessage()">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z"/>
|
||||
</svg>
|
||||
发送
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
class EffectSystem {
|
||||
constructor() {
|
||||
this.initCanvas();
|
||||
this.initParticleSystem();
|
||||
this.initMouseEffects();
|
||||
this.createStarryBackground();
|
||||
this.lastMeteorTime = 0;
|
||||
}
|
||||
|
||||
initCanvas() {
|
||||
this.canvas = document.createElement('canvas');
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
this.canvas.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
`;
|
||||
document.body.prepend(this.canvas);
|
||||
this.resize();
|
||||
window.addEventListener('resize', () => this.resize());
|
||||
}
|
||||
|
||||
resize() {
|
||||
this.canvas.width = window.innerWidth;
|
||||
this.canvas.height = window.innerHeight;
|
||||
}
|
||||
|
||||
initParticleSystem() {
|
||||
this.particles = [];
|
||||
this.animate();
|
||||
}
|
||||
|
||||
animate() {
|
||||
this.ctx.fillStyle = 'rgba(0,0,0,0.05)';
|
||||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
this.particles.forEach((particle, index) => {
|
||||
particle.update();
|
||||
particle.draw();
|
||||
if (particle.alpha <= 0) this.particles.splice(index, 1);
|
||||
});
|
||||
|
||||
requestAnimationFrame(() => this.animate());
|
||||
}
|
||||
|
||||
initMouseEffects() {
|
||||
// 点击特效
|
||||
document.addEventListener('click', (e) => {
|
||||
for(let i = 0; i < 15; i++) {
|
||||
this.particles.push(new SparkParticle(e.clientX, e.clientY, this.ctx));
|
||||
}
|
||||
});
|
||||
|
||||
// 流星特效(每3秒随机生成)
|
||||
setInterval(() => {
|
||||
if (Math.random() > 0.7) { // 30%概率生成
|
||||
this.createMeteor();
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
createMeteor() {
|
||||
const startX = Math.random() * this.canvas.width;
|
||||
const meteor = new MeteorParticle(
|
||||
startX,
|
||||
-50,
|
||||
this.ctx,
|
||||
this.canvas.width,
|
||||
this.canvas.height
|
||||
);
|
||||
this.particles.push(meteor);
|
||||
}
|
||||
|
||||
createStarryBackground() {
|
||||
// 绘制静态星空
|
||||
this.ctx.fillStyle = 'rgba(255,255,255,0.8)';
|
||||
for(let i = 0; i < 150; i++) {
|
||||
const x = Math.random() * this.canvas.width;
|
||||
const y = Math.random() * this.canvas.height;
|
||||
const r = Math.random() * 1.2;
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(x, y, r, 0, Math.PI * 2);
|
||||
this.ctx.fill();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SparkParticle {
|
||||
constructor(x, y, ctx) {
|
||||
this.ctx = ctx;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.alpha = 1;
|
||||
this.velocity = {
|
||||
x: (Math.random() - 0.5) * 4,
|
||||
y: (Math.random() - 0.5) * 4
|
||||
};
|
||||
this.size = Math.random() * 2 + 1;
|
||||
this.color = `hsla(${190 + Math.random() * 40}, 70%, 60%, ${this.alpha})`;
|
||||
}
|
||||
|
||||
update() {
|
||||
this.x += this.velocity.x;
|
||||
this.y += this.velocity.y;
|
||||
this.alpha -= 0.02;
|
||||
this.velocity.x *= 0.95;
|
||||
this.velocity.y *= 0.95;
|
||||
}
|
||||
|
||||
draw() {
|
||||
this.ctx.fillStyle = this.color;
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
|
||||
this.ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
class MeteorParticle {
|
||||
constructor(x, y, ctx, maxWidth, maxHeight) {
|
||||
this.ctx = ctx;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.alpha = 1;
|
||||
this.size = 2;
|
||||
this.speed = 15;
|
||||
this.angle = Math.PI / 4 + Math.random() * Math.PI / 2;
|
||||
this.color = `hsla(180, 100%, 70%, ${this.alpha})`;
|
||||
this.trail = [];
|
||||
this.maxWidth = maxWidth;
|
||||
this.maxHeight = maxHeight;
|
||||
}
|
||||
|
||||
update() {
|
||||
this.x += Math.cos(this.angle) * this.speed;
|
||||
this.y += Math.sin(this.angle) * this.speed;
|
||||
|
||||
// 添加拖尾
|
||||
this.trail.push({x: this.x, y: this.y});
|
||||
if(this.trail.length > 15) this.trail.shift();
|
||||
|
||||
// 渐隐
|
||||
this.alpha -= 0.008;
|
||||
this.color = `hsla(180, 100%, 70%, ${this.alpha})`;
|
||||
}
|
||||
|
||||
draw() {
|
||||
// 绘制拖尾
|
||||
this.trail.forEach((pos, i) => {
|
||||
const alpha = this.alpha * (i / this.trail.length);
|
||||
const gradient = this.ctx.createLinearGradient(
|
||||
pos.x, pos.y,
|
||||
pos.x + Math.cos(this.angle)*20,
|
||||
pos.y + Math.sin(this.angle)*20
|
||||
);
|
||||
|
||||
gradient.addColorStop(0, `hsla(180, 100%, 70%, ${alpha})`);
|
||||
gradient.addColorStop(1, `hsla(180, 100%, 70%, 0)`);
|
||||
|
||||
this.ctx.strokeStyle = gradient;
|
||||
this.ctx.lineWidth = this.size;
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(pos.x, pos.y);
|
||||
this.ctx.lineTo(
|
||||
pos.x + Math.cos(this.angle)*20,
|
||||
pos.y + Math.sin(this.angle)*20
|
||||
);
|
||||
this.ctx.stroke();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化特效
|
||||
//if(document.documentElement.getAttribute('data-theme') === 'dark') {
|
||||
new EffectSystem();
|
||||
//}
|
||||
|
||||
// 增强毛玻璃效果
|
||||
document.querySelectorAll('.chat-container, .message.ai .bubble').forEach(el => {
|
||||
el.style.cssText += `
|
||||
background: rgba(30, 35, 45, 0.85) !important;
|
||||
backdrop-filter: blur(16px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(16px) saturate(180%);
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
`;
|
||||
});
|
||||
|
||||
// 层级调整
|
||||
document.querySelector('.container').style.cssText += `
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
`;
|
||||
|
||||
// 修改原有createMessageElement函数
|
||||
function createMessageElement(requestId) {
|
||||
const element = document.createElement('div');
|
||||
element.className = 'message ai response';
|
||||
element.innerHTML = `
|
||||
<div class="bubble" style="
|
||||
background: rgba(255,255,255,0.05);
|
||||
backdrop-filter: blur(8px);
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
">
|
||||
<div class="content"></div>
|
||||
<div class="streaming-cursor"></div>
|
||||
<button class="fold-btn" onclick="toggleFold(event)">展开</button>
|
||||
</div>
|
||||
`;
|
||||
messages.insertBefore(element, typing);
|
||||
return element;
|
||||
}
|
||||
|
||||
|
||||
// 初始化配置
|
||||
marked.setOptions({
|
||||
breaks: true,
|
||||
highlight: code => hljs.highlightAuto(code).value
|
||||
});
|
||||
|
||||
marked.use({
|
||||
extensions: []
|
||||
});
|
||||
|
||||
hljs.configure({
|
||||
languages: ['java', 'python', 'javascript', 'typescript'],
|
||||
cssSelector: 'pre code',
|
||||
ignoreUnescapedHTML: true
|
||||
});
|
||||
|
||||
// CEF通信桥接
|
||||
window.javaQuery = window.cefQuery ? (request, success, error) => {
|
||||
window.cefQuery({
|
||||
request,
|
||||
onSuccess: success,
|
||||
onFailure: (code, msg) => error?.(msg)
|
||||
});
|
||||
} : console.error;
|
||||
|
||||
// 流式响应处理器
|
||||
const streams = new Map();
|
||||
|
||||
window.updateResponse = (requestId, content) => {
|
||||
if (content === '[end]') {
|
||||
finalizeStream(requestId);
|
||||
return;
|
||||
}
|
||||
|
||||
let stream = streams.get(requestId);
|
||||
if (!stream) {
|
||||
stream = {
|
||||
buffer: "", // 主缓冲区
|
||||
element: createMessageElement(requestId),
|
||||
cursorTimer: null,
|
||||
isCompleted: false
|
||||
};
|
||||
streams.set(requestId, stream);
|
||||
startCursorAnimation(requestId);
|
||||
hideTyping();
|
||||
}
|
||||
|
||||
// 直接累积内容到主缓冲区
|
||||
stream.buffer += content;
|
||||
|
||||
// 实时更新到 DOM(无论公式是否闭合)
|
||||
renderContent(requestId);
|
||||
maintainScroll();
|
||||
};
|
||||
|
||||
function hideTyping() {
|
||||
document.getElementById('typing').style.display = 'none';
|
||||
}
|
||||
|
||||
function handleThinkingContent(stream, content) {
|
||||
const parts = content.split('</think>');
|
||||
if (parts[0]) {
|
||||
stream.hasThinking = true;
|
||||
appendThinkingContent(stream, parts[0]);
|
||||
}
|
||||
if (parts[1]) {
|
||||
stream.buffer += parts[1];
|
||||
}
|
||||
}
|
||||
|
||||
function appendThinkingContent(stream, content) {
|
||||
const thinkingDiv = document.createElement('div');
|
||||
thinkingDiv.className = 'thinking-content';
|
||||
thinkingDiv.textContent = content.replace('<think>', '').trim();
|
||||
stream.element.querySelector('.content').appendChild(thinkingDiv);
|
||||
}
|
||||
|
||||
function startCursorAnimation(requestId) {
|
||||
const stream = streams.get(requestId);
|
||||
if (!stream) return;
|
||||
|
||||
stream.cursorTimer = setInterval(() => {
|
||||
if (stream.isCompleted) {
|
||||
clearInterval(stream.cursorTimer);
|
||||
return;
|
||||
}
|
||||
const cursor = stream.element.querySelector('.streaming-cursor');
|
||||
if (cursor) {
|
||||
cursor.style.opacity = cursor.style.opacity === '1' ? '0.3' : '1';
|
||||
}
|
||||
}, 600);
|
||||
}
|
||||
|
||||
function renderContent(requestId) {
|
||||
const stream = streams.get(requestId);
|
||||
if (!stream) return;
|
||||
|
||||
const contentDiv = stream.element.querySelector('.content');
|
||||
const rawContent = stream.buffer;
|
||||
|
||||
// 使用 Marked 解析内容
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = marked.parse(rawContent);
|
||||
|
||||
// 高亮代码块
|
||||
tempDiv.querySelectorAll('pre code').forEach(block => {
|
||||
hljs.highlightElement(block);
|
||||
});
|
||||
|
||||
// 增量更新 DOM
|
||||
contentDiv.innerHTML = tempDiv.innerHTML;
|
||||
|
||||
// 触发 KaTeX 渲染(兼容未闭合公式)
|
||||
if (window.renderMathInElement) {
|
||||
renderMathInElement(contentDiv, {
|
||||
delimiters: [
|
||||
{ left: '$$', right: '$$', display: true },
|
||||
{ left: '$', right: '$', display: false }
|
||||
],
|
||||
throwOnError: false, // 忽略错误,允许未闭合公式临时显示
|
||||
strict: false // 宽松模式,兼容不完整语法
|
||||
});
|
||||
}
|
||||
|
||||
// 显示流式光标
|
||||
if (!stream.isCompleted && !contentDiv.querySelector('.streaming-cursor')) {
|
||||
contentDiv.innerHTML += '<div class="streaming-cursor"></div>';
|
||||
}
|
||||
|
||||
addCopyButtons(contentDiv);
|
||||
}
|
||||
|
||||
///function createMessageElement(requestId) {
|
||||
/// const element = document.createElement('div');
|
||||
/// element.className = 'message ai response'; // 添加response类
|
||||
/// element.innerHTML = `
|
||||
/// <div class="bubble">
|
||||
/// <div class="content"></div>
|
||||
/// <div class="streaming-cursor"></div>
|
||||
/// <button class="fold-btn" onclick="toggleFold(event)">展开</button>
|
||||
/// </div>
|
||||
/// `;
|
||||
/// messages.insertBefore(element, typing);
|
||||
/// return element;
|
||||
///}
|
||||
|
||||
function finalizeStream(requestId) {
|
||||
const stream = streams.get(requestId);
|
||||
if (stream) {
|
||||
clearInterval(stream.cursorTimer);
|
||||
stream.isCompleted = true;
|
||||
|
||||
// 移除光标
|
||||
const cursor = stream.element.querySelector('.streaming-cursor');
|
||||
if (cursor) cursor.remove();
|
||||
|
||||
// 自动折叠逻辑
|
||||
checkCollapsible(stream.element);
|
||||
addFoldButton(stream.element);
|
||||
|
||||
streams.delete(requestId);
|
||||
}
|
||||
}
|
||||
|
||||
function checkCollapsible(element) {
|
||||
const bubble = element.querySelector('.bubble');
|
||||
const lineHeight = parseInt(getComputedStyle(bubble).lineHeight);
|
||||
if (bubble.scrollHeight > lineHeight * 5) {
|
||||
element.classList.add('collapsed');
|
||||
}
|
||||
}
|
||||
|
||||
function addFoldButton(element) {
|
||||
const btn = element.querySelector('.fold-btn');
|
||||
btn.style.display = 'block';
|
||||
btn.textContent = element.classList.contains('collapsed') ? '展开' : '收起';
|
||||
}
|
||||
|
||||
function toggleFold(event) {
|
||||
const btn = event.target;
|
||||
const message = btn.closest('.message');
|
||||
const beforeHeight = messages.scrollHeight;
|
||||
|
||||
message.classList.toggle('collapsed');
|
||||
btn.textContent = message.classList.contains('collapsed') ? '展开' : '收起';
|
||||
|
||||
const heightDiff = messages.scrollHeight - beforeHeight;
|
||||
messages.scrollTop += heightDiff;
|
||||
}
|
||||
|
||||
function maintainScroll() {
|
||||
const threshold = 100;
|
||||
const container = document.getElementById('messages');
|
||||
const isNearBottom = container.scrollHeight - container.scrollTop - container.clientHeight <= threshold;
|
||||
|
||||
if (isNearBottom) {
|
||||
requestAnimationFrame(() => {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function addCopyButtons(container) {
|
||||
container.querySelectorAll('pre').forEach(pre => {
|
||||
if (!pre.querySelector('.copy-btn')) {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'copy-btn';
|
||||
btn.textContent = '复制';
|
||||
btn.onclick = () => copyCode(pre);
|
||||
pre.prepend(btn);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function copyCode(pre) {
|
||||
const code = pre.querySelector('code')?.textContent || '';
|
||||
navigator.clipboard.writeText(code).then(() => {
|
||||
showToast('代码已复制');
|
||||
});
|
||||
}
|
||||
|
||||
function showToast(message) {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = 'toast';
|
||||
toast.textContent = message;
|
||||
document.body.appendChild(toast);
|
||||
setTimeout(() => toast.remove(), 2000);
|
||||
}
|
||||
|
||||
function sendMessage() {
|
||||
const input = document.getElementById('input');
|
||||
const prompt = input.value.trim();
|
||||
if (!prompt) return;
|
||||
|
||||
const requestId = `req_${Date.now()}_${Math.random().toString(36).substr(2, 4)}`;
|
||||
|
||||
const userMsg = document.createElement('div');
|
||||
userMsg.className = 'message user';
|
||||
userMsg.innerHTML = `
|
||||
<div class="bubble">${marked.parse(prompt)}</div>
|
||||
`;
|
||||
messages.insertBefore(userMsg, typing);
|
||||
messages.scrollTop = messages.scrollHeight;
|
||||
|
||||
showTyping(true);
|
||||
input.value = '';
|
||||
|
||||
window.javaQuery(
|
||||
`ai-inference:${requestId}:${prompt}`,
|
||||
response => {
|
||||
if (response.startsWith("COMPLETED:")) {
|
||||
finalizeStream(requestId);
|
||||
}
|
||||
},
|
||||
error => showError(requestId, error)
|
||||
);
|
||||
}
|
||||
|
||||
function showError(requestId, message) {
|
||||
const stream = streams.get(requestId);
|
||||
if (stream) {
|
||||
stream.element.innerHTML = `
|
||||
<div class="bubble error">
|
||||
<strong>⚠️ 请求失败:</strong> ${message}
|
||||
</div>
|
||||
`;
|
||||
streams.delete(requestId);
|
||||
}
|
||||
showTyping(false);
|
||||
}
|
||||
|
||||
function showTyping(show) {
|
||||
typing.style.display = show ? 'block' : 'none';
|
||||
if (show) messages.scrollTop = messages.scrollHeight;
|
||||
}
|
||||
|
||||
document.getElementById('input').addEventListener('keypress', e => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
sendMessage();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
renderMathInElement(document.body, {
|
||||
delimiters: [
|
||||
{ left: '$$', right: '$$', display: true }, // 块级公式
|
||||
{ left: '$', right: '$', display: false }, // 行内公式
|
||||
{ left: '\\[', right: '\\]', display: true }, // LaTeX 环境
|
||||
{ left: '\\(', right: '\\)', display: false }
|
||||
],
|
||||
throwOnError: false // 忽略渲染错误
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
665
src/main/resources/javascript/AIaToolbox_light.html
Normal file
665
src/main/resources/javascript/AIaToolbox_light.html
Normal file
@@ -0,0 +1,665 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<title>DeepSeek - 智能助手</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<!-- 动态加载主题样式 -->
|
||||
<link id="hljs-light" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/intellij-light.min.css" media="(prefers-color-scheme: light)">
|
||||
<link id="hljs-dark" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css" media="(prefers-color-scheme: dark)">
|
||||
<!-- KaTeX 核心样式 -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
|
||||
<!-- KaTeX 核心库 -->
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
|
||||
<!-- 自动渲染扩展 -->
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #2d8cf0;
|
||||
--primary-hover: #57a3f3;
|
||||
--bg-color: #f8f9fa;
|
||||
--card-bg: rgba(255, 255, 255, 0.97);
|
||||
--text-primary: #1f2d3d;
|
||||
--text-secondary: #666;
|
||||
--shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--primary-color: #58a6ff;
|
||||
--primary-hover: #79b8ff;
|
||||
--bg-color: #0d1117;
|
||||
--card-bg: rgba(13, 17, 23, 0.95);
|
||||
--text-primary: #c9d1d9;
|
||||
--text-secondary: #8b949e;
|
||||
--shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.katex {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.message.ai {
|
||||
background: var(--card-bg);
|
||||
}
|
||||
|
||||
pre {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.katex { font-size: 1.1em; padding: 0 0.2em; }
|
||||
.math-block { margin: 1em 0; padding: 1em; background: var(--card-bg); }
|
||||
|
||||
.math-block::-webkit-scrollbar {
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.message.ai.response {
|
||||
background: white; /* 恢复白色背景 */
|
||||
color: var(--text-primary); /* 恢复正常文字颜色 */
|
||||
}
|
||||
|
||||
.message.ai.response.collapsed .bubble {
|
||||
max-height: 6em;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.message.ai.response .fold-btn {
|
||||
display: none;
|
||||
background: linear-gradient(180deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0.95) 60%);
|
||||
padding: 6px 12px;
|
||||
right: 15px;
|
||||
bottom: 10px;
|
||||
}
|
||||
|
||||
.message.ai.response.collapsed .fold-btn {
|
||||
display: block;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 20px auto;
|
||||
flex: 1;
|
||||
width: 95%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chat-container {
|
||||
background: var(--card-bg);
|
||||
backdrop-filter: blur(12px);
|
||||
border-radius: 16px;
|
||||
box-shadow: var(--shadow);
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.messages {
|
||||
flex: 1;
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
scroll-behavior: smooth;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.message {
|
||||
max-width: 85%;
|
||||
opacity: 0;
|
||||
animation: messageIn 0.3s ease-out forwards;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@keyframes messageIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.message.user {
|
||||
align-self: flex-end;
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
border-radius: 20px 20px 4px 20px;
|
||||
}
|
||||
|
||||
.message.ai {
|
||||
align-self: flex-start;
|
||||
background: white;
|
||||
color: var(--text-secondary);
|
||||
border-radius: 4px 20px 20px 20px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.bubble {
|
||||
padding: 16px 24px;
|
||||
line-height: 1.7;
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.streaming-cursor {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 1em;
|
||||
background: var(--text-secondary);
|
||||
margin-left: 4px;
|
||||
vertical-align: middle;
|
||||
animation: cursorPulse 1.2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes cursorPulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.3; }
|
||||
}
|
||||
|
||||
.typing-indicator {
|
||||
display: none;
|
||||
padding: 16px 24px;
|
||||
background: white;
|
||||
border-radius: 24px;
|
||||
margin: 12px 0;
|
||||
box-shadow: var(--shadow);
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.dot-flashing {
|
||||
position: relative;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--text-secondary);
|
||||
animation: dotFlashing 1s infinite linear;
|
||||
}
|
||||
|
||||
@keyframes dotFlashing {
|
||||
0% { background-color: var(--text-secondary); }
|
||||
50%, 100% { background-color: rgba(94, 108, 130, 0.2); }
|
||||
}
|
||||
|
||||
.input-area {
|
||||
padding: 24px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
padding: 16px 24px;
|
||||
border: none;
|
||||
border-radius: 30px;
|
||||
background: white;
|
||||
font-size: 16px;
|
||||
box-shadow: var(--shadow);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px rgba(45, 140, 240, 0.2);
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 16px 32px;
|
||||
border: none;
|
||||
border-radius: 30px;
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: var(--primary-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
pre {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
position: relative;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 12px;
|
||||
padding: 6px 12px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.copy-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.fold-btn {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
bottom: 10px;
|
||||
background: linear-gradient(180deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0.95) 60%);
|
||||
padding: 6px 12px;
|
||||
border: none;
|
||||
color: var(--primary-color);
|
||||
font-size: 0.85em;
|
||||
cursor: pointer;
|
||||
display: none;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.message.collapsed .fold-btn,
|
||||
.message:not(.streaming):hover .fold-btn {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.message.collapsed .bubble {
|
||||
max-height: 6em;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.message.collapsed .fold-btn::after {
|
||||
content: "展开";
|
||||
}
|
||||
|
||||
.message:not(.collapsed) .fold-btn::after {
|
||||
content: "收起";
|
||||
}
|
||||
|
||||
.thinking-content {
|
||||
color: #888;
|
||||
font-style: italic;
|
||||
margin-bottom: 12px;
|
||||
padding: 8px 12px;
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
color: white;
|
||||
padding: 12px 24px;
|
||||
border-radius: 30px;
|
||||
font-size: 0.9em;
|
||||
animation: toastIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes toastIn {
|
||||
from { opacity: 0; transform: translate(-50%, 10px); }
|
||||
to { opacity: 1; transform: translate(-50%, 0); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/java.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/python.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/javascript.min.js"></script>
|
||||
|
||||
<div class="container">
|
||||
<div class="chat-container">
|
||||
<div class="messages" id="messages">
|
||||
<div class="typing-indicator" id="typing">
|
||||
<div class="dot-flashing"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-area">
|
||||
<div class="input-wrapper">
|
||||
<input type="text"
|
||||
id="input"
|
||||
placeholder="输入您的问题..."
|
||||
autocomplete="off">
|
||||
<button onclick="sendMessage()">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z"/>
|
||||
</svg>
|
||||
发送
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 初始化配置
|
||||
marked.setOptions({
|
||||
breaks: true,
|
||||
highlight: code => hljs.highlightAuto(code).value
|
||||
});
|
||||
|
||||
marked.use({
|
||||
extensions: []
|
||||
});
|
||||
|
||||
hljs.configure({
|
||||
languages: ['java', 'python', 'javascript', 'typescript'],
|
||||
cssSelector: 'pre code',
|
||||
ignoreUnescapedHTML: true
|
||||
});
|
||||
|
||||
// CEF通信桥接
|
||||
window.javaQuery = window.cefQuery ? (request, success, error) => {
|
||||
window.cefQuery({
|
||||
request,
|
||||
onSuccess: success,
|
||||
onFailure: (code, msg) => error?.(msg)
|
||||
});
|
||||
} : console.error;
|
||||
|
||||
// 流式响应处理器
|
||||
const streams = new Map();
|
||||
|
||||
window.updateResponse = (requestId, content) => {
|
||||
if (content === '[end]') {
|
||||
finalizeStream(requestId);
|
||||
return;
|
||||
}
|
||||
|
||||
let stream = streams.get(requestId);
|
||||
if (!stream) {
|
||||
stream = {
|
||||
buffer: "", // 主缓冲区
|
||||
element: createMessageElement(requestId),
|
||||
cursorTimer: null,
|
||||
isCompleted: false
|
||||
};
|
||||
streams.set(requestId, stream);
|
||||
startCursorAnimation(requestId);
|
||||
hideTyping();
|
||||
}
|
||||
|
||||
// 直接累积内容到主缓冲区
|
||||
stream.buffer += content;
|
||||
|
||||
// 实时更新到 DOM(无论公式是否闭合)
|
||||
renderContent(requestId);
|
||||
maintainScroll();
|
||||
};
|
||||
|
||||
function hideTyping() {
|
||||
document.getElementById('typing').style.display = 'none';
|
||||
}
|
||||
|
||||
function handleThinkingContent(stream, content) {
|
||||
const parts = content.split('</think>');
|
||||
if (parts[0]) {
|
||||
stream.hasThinking = true;
|
||||
appendThinkingContent(stream, parts[0]);
|
||||
}
|
||||
if (parts[1]) {
|
||||
stream.buffer += parts[1];
|
||||
}
|
||||
}
|
||||
|
||||
function appendThinkingContent(stream, content) {
|
||||
const thinkingDiv = document.createElement('div');
|
||||
thinkingDiv.className = 'thinking-content';
|
||||
thinkingDiv.textContent = content.replace('<think>', '').trim();
|
||||
stream.element.querySelector('.content').appendChild(thinkingDiv);
|
||||
}
|
||||
|
||||
function startCursorAnimation(requestId) {
|
||||
const stream = streams.get(requestId);
|
||||
if (!stream) return;
|
||||
|
||||
stream.cursorTimer = setInterval(() => {
|
||||
if (stream.isCompleted) {
|
||||
clearInterval(stream.cursorTimer);
|
||||
return;
|
||||
}
|
||||
const cursor = stream.element.querySelector('.streaming-cursor');
|
||||
if (cursor) {
|
||||
cursor.style.opacity = cursor.style.opacity === '1' ? '0.3' : '1';
|
||||
}
|
||||
}, 600);
|
||||
}
|
||||
|
||||
function renderContent(requestId) {
|
||||
const stream = streams.get(requestId);
|
||||
if (!stream) return;
|
||||
|
||||
const contentDiv = stream.element.querySelector('.content');
|
||||
const rawContent = stream.buffer;
|
||||
|
||||
// 使用 Marked 解析内容
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = marked.parse(rawContent);
|
||||
|
||||
// 高亮代码块
|
||||
tempDiv.querySelectorAll('pre code').forEach(block => {
|
||||
hljs.highlightElement(block);
|
||||
});
|
||||
|
||||
// 增量更新 DOM
|
||||
contentDiv.innerHTML = tempDiv.innerHTML;
|
||||
|
||||
// 触发 KaTeX 渲染(兼容未闭合公式)
|
||||
if (window.renderMathInElement) {
|
||||
renderMathInElement(contentDiv, {
|
||||
delimiters: [
|
||||
{ left: '$$', right: '$$', display: true },
|
||||
{ left: '$', right: '$', display: false }
|
||||
],
|
||||
throwOnError: false, // 忽略错误,允许未闭合公式临时显示
|
||||
strict: false // 宽松模式,兼容不完整语法
|
||||
});
|
||||
}
|
||||
|
||||
// 显示流式光标
|
||||
if (!stream.isCompleted && !contentDiv.querySelector('.streaming-cursor')) {
|
||||
contentDiv.innerHTML += '<div class="streaming-cursor"></div>';
|
||||
}
|
||||
|
||||
addCopyButtons(contentDiv);
|
||||
}
|
||||
|
||||
function createMessageElement(requestId) {
|
||||
const element = document.createElement('div');
|
||||
element.className = 'message ai response'; // 添加response类
|
||||
element.innerHTML = `
|
||||
<div class="bubble">
|
||||
<div class="content"></div>
|
||||
<div class="streaming-cursor"></div>
|
||||
<button class="fold-btn" onclick="toggleFold(event)">展开</button>
|
||||
</div>
|
||||
`;
|
||||
messages.insertBefore(element, typing);
|
||||
return element;
|
||||
}
|
||||
|
||||
function finalizeStream(requestId) {
|
||||
const stream = streams.get(requestId);
|
||||
if (stream) {
|
||||
clearInterval(stream.cursorTimer);
|
||||
stream.isCompleted = true;
|
||||
|
||||
// 移除光标
|
||||
const cursor = stream.element.querySelector('.streaming-cursor');
|
||||
if (cursor) cursor.remove();
|
||||
|
||||
// 自动折叠逻辑
|
||||
checkCollapsible(stream.element);
|
||||
addFoldButton(stream.element);
|
||||
|
||||
streams.delete(requestId);
|
||||
}
|
||||
}
|
||||
|
||||
function checkCollapsible(element) {
|
||||
const bubble = element.querySelector('.bubble');
|
||||
const lineHeight = parseInt(getComputedStyle(bubble).lineHeight);
|
||||
if (bubble.scrollHeight > lineHeight * 5) {
|
||||
element.classList.add('collapsed');
|
||||
}
|
||||
}
|
||||
|
||||
function addFoldButton(element) {
|
||||
const btn = element.querySelector('.fold-btn');
|
||||
btn.style.display = 'block';
|
||||
btn.textContent = element.classList.contains('collapsed') ? '展开' : '收起';
|
||||
}
|
||||
|
||||
function toggleFold(event) {
|
||||
const btn = event.target;
|
||||
const message = btn.closest('.message');
|
||||
const beforeHeight = messages.scrollHeight;
|
||||
|
||||
message.classList.toggle('collapsed');
|
||||
btn.textContent = message.classList.contains('collapsed') ? '展开' : '收起';
|
||||
|
||||
const heightDiff = messages.scrollHeight - beforeHeight;
|
||||
messages.scrollTop += heightDiff;
|
||||
}
|
||||
|
||||
function maintainScroll() {
|
||||
const threshold = 100;
|
||||
const container = document.getElementById('messages');
|
||||
const isNearBottom = container.scrollHeight - container.scrollTop - container.clientHeight <= threshold;
|
||||
|
||||
if (isNearBottom) {
|
||||
requestAnimationFrame(() => {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function addCopyButtons(container) {
|
||||
container.querySelectorAll('pre').forEach(pre => {
|
||||
if (!pre.querySelector('.copy-btn')) {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'copy-btn';
|
||||
btn.textContent = '复制';
|
||||
btn.onclick = () => copyCode(pre);
|
||||
pre.prepend(btn);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function copyCode(pre) {
|
||||
const code = pre.querySelector('code')?.textContent || '';
|
||||
navigator.clipboard.writeText(code).then(() => {
|
||||
showToast('代码已复制');
|
||||
});
|
||||
}
|
||||
|
||||
function showToast(message) {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = 'toast';
|
||||
toast.textContent = message;
|
||||
document.body.appendChild(toast);
|
||||
setTimeout(() => toast.remove(), 2000);
|
||||
}
|
||||
|
||||
function sendMessage() {
|
||||
const input = document.getElementById('input');
|
||||
const prompt = input.value.trim();
|
||||
if (!prompt) return;
|
||||
|
||||
const requestId = `req_${Date.now()}_${Math.random().toString(36).substr(2, 4)}`;
|
||||
|
||||
const userMsg = document.createElement('div');
|
||||
userMsg.className = 'message user';
|
||||
userMsg.innerHTML = `
|
||||
<div class="bubble">${marked.parse(prompt)}</div>
|
||||
`;
|
||||
messages.insertBefore(userMsg, typing);
|
||||
messages.scrollTop = messages.scrollHeight;
|
||||
|
||||
showTyping(true);
|
||||
input.value = '';
|
||||
|
||||
window.javaQuery(
|
||||
`ai-inference:${requestId}:${prompt}`,
|
||||
response => {
|
||||
if (response.startsWith("COMPLETED:")) {
|
||||
finalizeStream(requestId);
|
||||
}
|
||||
},
|
||||
error => showError(requestId, error)
|
||||
);
|
||||
}
|
||||
|
||||
function showError(requestId, message) {
|
||||
const stream = streams.get(requestId);
|
||||
if (stream) {
|
||||
stream.element.innerHTML = `
|
||||
<div class="bubble error">
|
||||
<strong>⚠️ 请求失败:</strong> ${message}
|
||||
</div>
|
||||
`;
|
||||
streams.delete(requestId);
|
||||
}
|
||||
showTyping(false);
|
||||
}
|
||||
|
||||
function showTyping(show) {
|
||||
typing.style.display = show ? 'block' : 'none';
|
||||
if (show) messages.scrollTop = messages.scrollHeight;
|
||||
}
|
||||
|
||||
document.getElementById('input').addEventListener('keypress', e => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
sendMessage();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
renderMathInElement(document.body, {
|
||||
delimiters: [
|
||||
{ left: '$$', right: '$$', display: true }, // 块级公式
|
||||
{ left: '$', right: '$', display: false }, // 行内公式
|
||||
{ left: '\\[', right: '\\]', display: true }, // LaTeX 环境
|
||||
{ left: '\\(', right: '\\)', display: false }
|
||||
],
|
||||
throwOnError: false // 忽略渲染错误
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user