feat(browser): 实现 AI 推理功能并优化聊天界面
- 新增 AI 推理相关功能,包括模型加载、上下文创建和消息处理 - 设计并实现聊天界面的前端逻辑,支持流式响应和消息折叠 - 集成 KaTeX、highlight.js 和 marked 库,支持数学公式和代码高亮显示 - 添加错误处理和友好的用户交互提示
This commit is contained in:
10
.idea/encodings.xml
generated
Normal file
10
.idea/encodings.xml
generated
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Encoding" native2AsciiForPropertiesFiles="true" defaultCharsetForPropertiesFiles="GBK">
|
||||||
|
<file url="file://$PROJECT_DIR$/language" charset="UTF-8" />
|
||||||
|
<file url="file://$PROJECT_DIR$/plug-in/python/Testing/main.py" charset="UTF-8" />
|
||||||
|
<file url="file://$PROJECT_DIR$/src/main/java/com/axis/innovators/box/gui/FridaWindow.java" charset="UTF-8" />
|
||||||
|
<file url="file://$PROJECT_DIR$/src/main/java/org/tzd/lm/LM.java" charset="UTF-8" />
|
||||||
|
<file url="file://$PROJECT_DIR$/src/main/java/org/tzd/lm/LMApi.java" charset="UTF-8" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/jsLibraryMappings.xml
generated
Normal file
6
.idea/jsLibraryMappings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="JavaScriptLibraryMappings">
|
||||||
|
<file url="file://$PROJECT_DIR$/src/main" libraries="{highlight.js, katex, marked, mathjax}" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,688 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>DeepSeek - 智能助手</title>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/intellij-light.min.css">
|
||||||
|
<!-- 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.katex {
|
||||||
|
font-size: 1.1em;
|
||||||
|
padding: 0 0.2em;
|
||||||
|
}
|
||||||
|
.math-block {
|
||||||
|
margin: 1em 0;
|
||||||
|
padding: 1em;
|
||||||
|
background: rgba(0, 0, 0, 0.03);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
.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: [
|
||||||
|
{
|
||||||
|
name: 'math',
|
||||||
|
level: 'block',
|
||||||
|
start(src) { return src.indexOf('$$') !== -1; },
|
||||||
|
tokenizer(src) {
|
||||||
|
const match = src.match(/^\$\$((?:\\\$|[\s\S])+?)\$\$/m); // 修正正则
|
||||||
|
if (match) {
|
||||||
|
return {
|
||||||
|
type: 'math',
|
||||||
|
raw: match[0],
|
||||||
|
text: match[1].trim()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
renderer(token) {
|
||||||
|
return `<div class="math-block">${token.text}</div>`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'inlineMath',
|
||||||
|
level: 'inline',
|
||||||
|
start(src) { return src.indexOf('$') !== -1; },
|
||||||
|
tokenizer(src) {
|
||||||
|
const match = src.match(/^\$((?:\\\$|[^$])+?)\$/);
|
||||||
|
if (match) {
|
||||||
|
return {
|
||||||
|
type: 'inlineMath',
|
||||||
|
raw: match[0],
|
||||||
|
text: match[1].trim()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
renderer(token) {
|
||||||
|
return `<span class="math-inline">${token.text}</span>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
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,
|
||||||
|
isResponse: true
|
||||||
|
};
|
||||||
|
streams.set(requestId, stream);
|
||||||
|
stream.element.classList.add('response');
|
||||||
|
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 // 宽松模式,兼容不完整语法
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error('KaTeX 自动渲染扩展未加载');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示流式光标
|
||||||
|
if (!stream.isCompleted) {
|
||||||
|
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>
|
||||||
@@ -1,34 +1,168 @@
|
|||||||
package com.axis.innovators.box.browser;
|
package com.axis.innovators.box.browser;
|
||||||
|
|
||||||
|
import org.cef.browser.CefBrowser;
|
||||||
|
import org.cef.browser.CefFrame;
|
||||||
|
import org.cef.browser.CefMessageRouter;
|
||||||
|
import org.cef.callback.CefQueryCallback;
|
||||||
|
import org.cef.handler.CefMessageRouterHandlerAdapter;
|
||||||
|
import org.tzd.lm.LM;
|
||||||
|
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 这是一个简单的示例程序,用于展示如何使用JCEF来创建一个简单的浏览器窗口。
|
* 这是一个简单的示例程序,用于展示如何使用JCEF来创建一个简单的浏览器窗口。
|
||||||
*/
|
*/
|
||||||
public class MainApplication {
|
public class MainApplication {
|
||||||
|
private static final ExecutorService executor = Executors.newCachedThreadPool();
|
||||||
|
private static long modelHandle;
|
||||||
|
private static long ctxHandle;
|
||||||
|
private static boolean isSystem = true;
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SwingUtilities.invokeLater(() -> {
|
LM.loadLibrary(LM.CUDA);
|
||||||
//if (!CefApp.startup(args)) {
|
modelHandle = LM.llamaLoadModelFromFile(LM.DEEP_SEEK);
|
||||||
// System.out.println("Startup initialization failed!");
|
ctxHandle = LM.createContext(modelHandle);
|
||||||
// return;
|
|
||||||
//}
|
|
||||||
|
|
||||||
|
AtomicReference<BrowserWindow> window = new AtomicReference<>();
|
||||||
|
SwingUtilities.invokeLater(() -> {
|
||||||
WindowRegistry.getInstance().createNewWindow("main", builder ->
|
WindowRegistry.getInstance().createNewWindow("main", builder ->
|
||||||
builder.title("Axis Innovators Box")
|
window.set(builder.title("Axis Innovators Box")
|
||||||
.size(1280, 720)
|
.size(1280, 720)
|
||||||
.htmlPath("C:\\Users\\Administrator\\MCreatorWorkspaces\\AxisInnovatorsBox\\src\\main\\java\\com\\axis\\innovators\\box\\browser\\main.html")
|
.htmlPath("C:\\Users\\Administrator\\MCreatorWorkspaces\\AxisInnovatorsBox\\src\\main\\java\\com\\axis\\innovators\\box\\browser\\DeepSeek - 探索未至之境.html")
|
||||||
.operationHandler(createOperationHandler())
|
.operationHandler(createOperationHandler())
|
||||||
.build()
|
.build())
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CefMessageRouter msgRouter = window.get().getMsgRouter();
|
||||||
|
if (msgRouter != null) {
|
||||||
|
msgRouter.addHandler(new CefMessageRouterHandlerAdapter() {
|
||||||
|
@Override
|
||||||
|
public boolean onQuery(CefBrowser browser, CefFrame frame, long queryId,
|
||||||
|
String request, boolean persistent, CefQueryCallback callback) {
|
||||||
|
// 处理浏览器请求
|
||||||
|
handleBrowserQuery(browser, request, callback);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onQueryCanceled(CefBrowser browser, CefFrame frame, long queryId) {
|
||||||
|
// 处理请求取消
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 关闭钩子
|
||||||
|
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||||
|
LM.llamaFreeContext(ctxHandle);
|
||||||
|
LM.llamaFreeModel(modelHandle);
|
||||||
|
executor.shutdown();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void handleBrowserQuery(CefBrowser browser, String request, CefQueryCallback callback) {
|
||||||
|
try {
|
||||||
|
String[] parts = request.split(":", 3);
|
||||||
|
if (parts.length < 3) {
|
||||||
|
callback.failure(400, "请求格式错误");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String operation = parts[0];
|
||||||
|
String requestId = parts[1];
|
||||||
|
String prompt = parts[2];
|
||||||
|
|
||||||
|
if ("ai-inference".equals(operation)) {
|
||||||
|
executor.execute(() -> {
|
||||||
|
//String system;
|
||||||
|
if (isSystem) {
|
||||||
|
//system = ;
|
||||||
|
isSystem = false;
|
||||||
|
} //else {
|
||||||
|
//system = null;
|
||||||
|
//}
|
||||||
|
List<String> messageList = new java.util.ArrayList<>(List.of());
|
||||||
|
// 修改后的推理回调处理
|
||||||
|
String jsCode = String.format(
|
||||||
|
"if (typeof updateResponse === 'function') {" +
|
||||||
|
" updateResponse('%s', '%s');" +
|
||||||
|
"}",
|
||||||
|
requestId, "<details open style=background: rgba(0,0,0,0.03);border: 1px solid #eee;border-radius: 4px;padding: 8px;margin: 8px 0;>" +
|
||||||
|
"\\n" +
|
||||||
|
"<summary>推理内容</summary><font face=\\\"黑体\\\" color=grey size=3>"
|
||||||
|
);
|
||||||
|
browser.executeJavaScript(jsCode, null, 0);
|
||||||
|
LM.inference(modelHandle, ctxHandle, 0.6f, prompt + "<think>\n",
|
||||||
|
"""
|
||||||
|
# 角色设定
|
||||||
|
你是一个严格遵循规则的AI助手,
|
||||||
|
当遇到简单的问题时你可以直接回答(回答先添加</think>),在遇到复杂问题时请你思考后再给出答案,并且需满足以下要求:
|
||||||
|
|
||||||
|
## 核心指令
|
||||||
|
1. **强制推理标记**:无论是否推理,回答**必须**以`<think>`开头,推理结束时闭合`</think>`。
|
||||||
|
2. **推理流程**:
|
||||||
|
- 使用固定句式(如“好的,用户现在需要...”)启动分析
|
||||||
|
3. **回答规范**:
|
||||||
|
- 使用Markdown标题、列表、加粗突出重点
|
||||||
|
- LaTeX公式严格包裹在`$$...$$`中(示例:`$$E=mc^2$$`)
|
||||||
|
- 不同可能性答案用列表呈现
|
||||||
|
|
||||||
|
## 违规惩罚
|
||||||
|
- 若未包含`<think>`标签,需在回答开头添加 </think>
|
||||||
|
""",
|
||||||
|
new LM.MessageCallback() {
|
||||||
|
private boolean thinkingClosed = false;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMessage(String message) {
|
||||||
|
messageList.add(message);
|
||||||
|
SwingUtilities.invokeLater(() -> {
|
||||||
|
// 统一转义处理
|
||||||
|
String escaped = message
|
||||||
|
.replace("\\", "\\\\")
|
||||||
|
.replace("'", "\\'")
|
||||||
|
.replace("\"", "\\\"")
|
||||||
|
.replace("\n", "\\n")
|
||||||
|
.replace("\r", "\\r");
|
||||||
|
|
||||||
|
if (messageList.contains("</think>") && !thinkingClosed) {
|
||||||
|
String endJs = String.format(
|
||||||
|
"if (typeof updateResponse === 'function') {" +
|
||||||
|
" updateResponse('%s', '%s');" +
|
||||||
|
"}",
|
||||||
|
requestId, "</font></details>"
|
||||||
|
);
|
||||||
|
browser.executeJavaScript(endJs, null, 0);
|
||||||
|
thinkingClosed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 实时更新内容
|
||||||
|
String jsCode = String.format(
|
||||||
|
"if (typeof updateResponse === 'function') {" +
|
||||||
|
" updateResponse('%s', '%s');" +
|
||||||
|
"}",
|
||||||
|
requestId, escaped
|
||||||
|
);
|
||||||
|
browser.executeJavaScript(jsCode, null, 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},isSystem);
|
||||||
|
messageList.clear();
|
||||||
|
callback.success("COMPLETED:" + requestId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
callback.failure(500, "服务器错误: " + e.getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static WindowOperationHandler createOperationHandler() {
|
private static WindowOperationHandler createOperationHandler() {
|
||||||
return new WindowOperationHandler.Builder()
|
return new WindowOperationHandler.Builder()
|
||||||
.withDefaultOperations()
|
.withDefaultOperations()
|
||||||
.onOperation("我是注册的指令", s -> {
|
|
||||||
|
|
||||||
})
|
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,18 @@ package com.axis.innovators.box.browser;
|
|||||||
|
|
||||||
import org.cef.callback.CefQueryCallback;
|
import org.cef.callback.CefQueryCallback;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author tzdwindows 7
|
||||||
|
*/
|
||||||
public class WindowOperation {
|
public class WindowOperation {
|
||||||
private final String type;
|
private final String type;
|
||||||
private final String targetWindow;
|
private final String targetWindow;
|
||||||
private final CefQueryCallback callback;
|
private final CefQueryCallback callback;
|
||||||
|
|
||||||
public WindowOperation(String type, String targetWindow, CefQueryCallback callback) { // [!code ++]
|
public WindowOperation(String type, String targetWindow, CefQueryCallback callback) {
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.targetWindow = targetWindow;
|
this.targetWindow = targetWindow;
|
||||||
this.callback = callback; // [!code ++]
|
this.callback = callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getType() {
|
public String getType() {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ public class WindowOperationHandler {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Builder onOperation(String operation, Consumer<String> handler) {
|
public Builder onOperation(String operation, Consumer<String> handler) {
|
||||||
this.operations.put(operation, handler);
|
this.operations.put(operation, handler);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,6 +130,44 @@ public class LM {
|
|||||||
*/
|
*/
|
||||||
public static native void llamaFreeContext(long ctxHandle);
|
public static native void llamaFreeContext(long ctxHandle);
|
||||||
|
|
||||||
|
public static String inference(long modelHandle ,
|
||||||
|
long ctxHandle,
|
||||||
|
float temperature,
|
||||||
|
String prompt,
|
||||||
|
String system,
|
||||||
|
MessageCallback messageCallback, boolean isContinue){
|
||||||
|
//if (isContinue){
|
||||||
|
// return inference(modelHandle,
|
||||||
|
// ctxHandle,
|
||||||
|
// temperature,
|
||||||
|
// 0.1f,
|
||||||
|
// 100,
|
||||||
|
// 0.9f,
|
||||||
|
// 0,
|
||||||
|
// 64,
|
||||||
|
// 1.1f,
|
||||||
|
// 0.0f,
|
||||||
|
// 0.0f,
|
||||||
|
// system + "用户:" + prompt + "\n请继续回答:",
|
||||||
|
// messageCallback
|
||||||
|
// );
|
||||||
|
//}
|
||||||
|
return inference(modelHandle,
|
||||||
|
ctxHandle,
|
||||||
|
temperature,
|
||||||
|
0.1f,
|
||||||
|
100,
|
||||||
|
0.9f,
|
||||||
|
0,
|
||||||
|
64,
|
||||||
|
1.1f,
|
||||||
|
0.0f,
|
||||||
|
0.0f,
|
||||||
|
"{问题}" + prompt,
|
||||||
|
messageCallback
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public static String inference(long modelHandle ,
|
public static String inference(long modelHandle ,
|
||||||
long ctxHandle,
|
long ctxHandle,
|
||||||
float temperature,
|
float temperature,
|
||||||
@@ -147,7 +185,7 @@ public class LM {
|
|||||||
1.1f,
|
1.1f,
|
||||||
0.0f,
|
0.0f,
|
||||||
0.0f,
|
0.0f,
|
||||||
system + "\n用户:" + prompt + "\n助手:",
|
system + "\n用户:" + prompt + "\n请开始回答:",
|
||||||
messageCallback
|
messageCallback
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user