(() => {
// 生成css
const css = `
.ai-light{
background-color: #f5f5f5;
}
.ai-dark{
background-color: #1a1a1a;
}
.trigger-btn {
padding: 10px 20px;
cursor: pointer;
border: none;
border-radius: 4px;
font-size: 14px;
margin: 10px;
}
.ai-light.trigger-btn {
background-color: #fff;
color: #333;
border: 1px solid #ddd;
}
.ai-dark.trigger-btn {
background-color: #2a2a2a;
color: #fff;
border: 1px solid #444;
}
.theme-toggle {
position: fixed;
top: 20px;
right: 20px;
padding: 8px 16px;
cursor: pointer;
border: none;
border-radius: 4px;
font-size: 14px;
}
/* AI对话框样式 */
.ai-dialog {
position: fixed;
width: 420px;
max-height: 468px;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
display: none;
flex-direction: column;
z-index: 10000;
/*overflow: hidden;*/
display: none;
}
.ai-dialog.show {
display: flex;
}
/* 明亮主题 */
.ai-light.ai-dialog {
background-color: #ffffff;
color: #333;
}
/* 暗黑主题 */
.ai-dark.ai-dialog {
background-color: #2a2a2a;
color: #e0e0e0;
}
/* 标题栏 */
.ai-dialog .dialog-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px;
cursor: move;
user-select: none;
/* border-bottom: 1px solid; */
}
.ai-light.ai-dialog .dialog-header {
border-bottom-color: #e0e0e0;
}
.ai-dark.ai-dialog .dialog-header {
/* border-bottom-color: #404040; */
}
.ai-dialog .dialog-title {
font-size: 14px;
font-weight: bold;
}
.ai-dialog .dialog-header-left {
display: flex;
align-items: center;
}
.ai-dialog .dialog-header-left .mini-btn {
display: none;
}
.ai-dialog .dialog-header-right {
display: flex;
align-items: center;
gap: 8px;
}
/* AI选择器 */
.ai-dialog .ai-selector {
position: relative;
}
.ai-dialog .ai-select-btn {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
border: 1px solid;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
background: transparent;
color: inherit;
}
.ai-light.ai-dialog .ai-select-btn {
border-color: #d0d0d0;
}
.ai-dialog .ai-select-btn svg {
vertical-align: middle;
position: relative;
top: -1px;
}
.ai-dark.ai-dialog .ai-select-btn {
border-color: #505050;
}
.ai-dialog .ai-select-btn:hover {
opacity: 0.8;
}
.ai-dialog .ai-dropdown {
position: absolute;
top: calc(100% + 4px);
right: 0;
background: white;
border: 1px solid;
border-radius: 6px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
min-width: 160px;
max-width: 300px; /* 添加这行 */
width: max-content; /* 添加这行 - 自适应内容 */
display: none;
z-index: 10;
}
.ai-light.ai-dialog .ai-dropdown {
background-color: #fff;
border-color: #e0e0e0;
}
.ai-dark.ai-dialog .ai-dropdown {
background-color: #333;
border-color: #505050;
}
.ai-dialog .ai-dropdown.show {
display: block;
}
.ai-dialog .ai-dropdown-item {
padding: 10px 16px;
cursor: pointer;
font-size: 13px;
display: flex;
align-items: center;
justify-content: space-between;
white-space: nowrap; /* 添加这行 - 禁止折行 */
gap: 12px; /* 可选:给文字和对勾之间加点间距 */
}
.ai-light.ai-dialog .ai-dropdown-item:hover {
background-color: #f5f5f5;
}
.ai-dark.ai-dialog .ai-dropdown-item:hover {
background-color: #404040;
}
.ai-dialog .ai-dropdown-item.selected {
font-weight: 500;
}
.ai-dialog .ai-dropdown-item .checkmark {
color: #4CAF50;
display: none;
}
.ai-dialog .ai-dropdown-item.selected .checkmark {
display: block;
}
/* 图标按钮 */
.ai-dialog .icon-btn {
width: 24px;
height: 24px;
border: none;
background: transparent;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
color: inherit;
}
.ai-dialog .icon-btn:hover {
opacity: 0.7;
}
.ai-light.ai-dialog .icon-btn:hover {
background-color: #f0f0f0;
}
.ai-dark.ai-dialog .icon-btn:hover {
background-color: #404040;
}
.ai-dialog .pin-btn {
position: relative;
left: 3px;
}
.ai-dialog .pin-btn.pinned {
color: #4CAF50;
}
/* 内容区域 */
.ai-dialog .dialog-content {
flex: 1;
overflow-y: auto;
padding: 10px;
padding-top: 0;
position: relative;
}
.ai-light.ai-dialog .dialog-content {
background-color: #fff;
}
.ai-dark.ai-dialog .dialog-content {
/* background-color: #1e1e1e; */
}
.ai-dark.ai-dialog .user-message .message-content {
max-width: 90%;
width: fit-content;
margin-left: auto;
background-color: #3b3b3b;
padding: 10px;
}
.ai-dark.ai-dialog .dialog-input::placeholder {
color: #eee;
}
/* 滚动条样式 */
.ai-light.ai-dialog .dialog-content::-webkit-scrollbar {
width: 6px;
}
.ai-light.ai-dialog .dialog-content::-webkit-scrollbar-thumb {
background-color: #ccc;
border-radius: 3px;
}
.ai-dark.ai-dialog .dialog-content::-webkit-scrollbar {
width: 6px;
}
.ai-dark.ai-dialog .dialog-content::-webkit-scrollbar-thumb {
background-color: #555;
border-radius: 3px;
}
/* 消息样式 */
.ai-dialog .message {
margin-bottom: 20px;
line-height: 1.6;
font-size: 14px;
}
.ai-dialog .message-label {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 8px;
font-size: 14px;
}
.ai-dialog .user-message .message-label {
justify-content: flex-end;
}
/*.ai-dialog .explain-message .message-content:empty::before,
.ai-dialog .ai-message .message-content:empty::before {
content: "......";
}*/
.ai-dialog .explain-message .message-content:empty::before,
.ai-dialog .ai-message .message-content:empty::before {
content: "";
letter-spacing: 5px;
animation: typing 2s infinite steps(4);
}
@keyframes typing {
0% { content: "\\2003"; }
25% { content: "•"; }
50% { content: "••"; }
75% { content: "•••"; }
100% { content: "•••"; }
}
.ai-dialog .user-message .message-content {
max-width: 90%;
width: fit-content;
margin-left: auto;
background-color: #f0f0f0;
padding: 10px;
white-space: pre-wrap;
word-break: break-word;
max-height: 200px;
overflow: auto;
}
.ai-light.ai-dialog .user-message .message-content {
padding: 10px;
}
.ai-dialog .message [data-time] {
opacity: 0.7;
}
.ai-dialog .message-content {
padding: 6px 12px;
border-radius: 6px;
}
.ai-light.ai-dialog .message-content {
/* background-color: #fafafa; */
padding: 0;
}
.ai-dark.ai-dialog .message-content {
background-color: #2a2a2a;
}
.ai-dialog .message-actions, .ai-dialog .user-actions {
display: flex;
gap: 8px;
margin-top: 8px;
display: none;
}
.ai-dialog .user-actions {
justify-content: right;
display: flex;
}
.ai-dialog .message-action-btn, .ai-dialog .user-action-btn {
background: transparent;
border: none;
cursor: pointer;
padding: 4px;
color: inherit;
opacity: 0.6;
font-size: 16px;
}
.ai-dialog .user-action-btn {
display: inline-block;
}
.message-action-btn.replace-btn {
display: none;
}
.ai-dialog .message-action-btn:hover {
opacity: 1;
}
.ai-dialog .message-action-btn[disabled],
.ai-dialog .message-action-btn[disabled]:hover {
opacity: 0.3;
}
/* 滚动到底部按钮 */
.ai-dialog .scroll-to-bottom {
position: sticky;
bottom: 10px;
left: 50%;
transform: translateX(-50%);
width: 32px;
height: 32px;
border-radius: 50%;
border: none;
cursor: pointer;
display: none;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
z-index: 1;
}
.ai-light.ai-dialog .scroll-to-bottom {
background-color: #fff;
color: #666;
opacity: 0.85;
}
.ai-dark.ai-dialog .scroll-to-bottom {
background-color: #404040;
color: #ccc;
opacity: 0.85;
}
.ai-dialog .scroll-to-bottom.show {
display: flex;
}
/* 输入区域 */
.ai-dialog .dialog-footer {
padding:0px 10px 10px;
/* border-top: 1px solid; */
display: none;
}
.ai-light.ai-dialog .dialog-footer {
border-top-color: #e0e0e0;
background-color: #fff;
}
.ai-dark.ai-dialog .dialog-footer {
border-top-color: #404040;
background-color: #2a2a2a;
}
.ai-dialog .input-wrapper {
display: flex;
align-items: center;
gap: 8px;
padding: 3px 8px;
border: 1px solid;
border-radius: 6px;
}
.ai-light.ai-dialog .input-wrapper {
border-color: #d0d0d0;
background-color: #fafafa;
}
.ai-dark.ai-dialog .input-wrapper {
border-color: #505050;
background-color: #1e1e1e;
}
.ai-dialog .dialog-input {
flex: 1;
border: none;
outline: none;
background: transparent;
font-size: 13px;
color: inherit;
resize: none; /* 禁止手动拖拽 */
overflow: hidden; /* 自动高度时隐藏溢出,超过 max-height 则显示滚动 */
min-height: 20px; /* 初始高度,与原 input 大小接近 */
max-height: 150px; /* 最大高度限制 */
line-height: 1.4;
}
.ai-dialog .dialog-input::placeholder {
opacity: 0.5;
}
.ai-dialog .dialog-input::-webkit-scrollbar {
display: none;
}
.ai-dialog .submit-btn {
background: transparent;
border: none;
cursor: pointer;
color: inherit;
opacity: 0.6;
font-size: 18px;
padding: 4px;
}
.ai-dialog .submit-btn.replying {
color: red;
}
.ai-dialog .submit-btn:hover {
opacity: 1;
}
.ai-dialog .chat-message {
display: none;
}
.ai-dialog .message-content ol,
.ai-dialog .message-content ul {
list-style-position: outside;
padding-left: 1.5em;
}
.ai-dialog ::selection {
color: #1a1a1a;
}
.ai-dark.ai-dialog ::selection {
color: #f5f5f5;
}
.ai-dialog pre {
background: #f5f5f5ff;
padding: 15px;
border-radius: 5px;
overflow-x: auto;
margin: 10px 0;
}
.ai-dark.ai-dialog pre {
background: #0f0f0fff;
}
.ai-dialog code {
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 14px;
}
.ai-dialog .chat-welcome {
display: none;
text-align: center;
}
.ai-dialog .chat-welcome h3{
margin-top:10px;
margin-bottom: 20px;
}
.ai-dialog .chat-welcome > div {
color: #888;
line-height: 180%;
}
.ai-dialog .pasted-images {
max-height: 100px;
overflow: auto;
display: flex;
gap: 10px;
}
.ai-dialog .pasted-images .image-preview{
position: relative;
}
.ai-dialog .pasted-images .image-preview img{
max-width: 60px;
max-height: 60px;
}
.ai-dialog .pasted-images .remove-image {
position: absolute;
top: 5px;
right: 5px;
background-color: rgba(0, 0, 0, 0.7);
color: white;
border: none;
border-radius: 50%;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 16px;
}
.image-viewer-overlay {
background: rgba(0, 0, 0, 0.8) !important;
}
/* 当有2个及以上,且除了第一个以外 */
.ai-dialog .explain-message .message-content:not(:nth-child(1)),
.ai-dialog .ai-message .message-content:not(:nth-child(2)) {
border-top: 2px dashed; #888;
border-radius: 0;
padding-top: 10px;
}
/* 当有2个及以上,且除了最后一个以外 */
.ai-dialog .explain-message:has(.message-content:nth-child(2)) .message-content:not(:has(+.message-actions)),
.ai-dialog .ai-message:has(.message-content:nth-child(3)) .message-content:not(:has(+.message-actions)) {
padding-bottom: 10px;
}
.ai-dialog .explain-message:has(.message-content:nth-child(2)) .message-content:hover,
.ai-dialog .ai-message:has(.message-content:nth-child(3)) .message-content:hover
background-color: #f1f0f0;
}
.ai-dialog .explain-message:has(.message-content:nth-child(2)) .message-content.message-selected,
.ai-dialog .ai-message:has(.message-content:nth-child(3)) .message-content.message-selected{
background-color: #d5e9ff;
}
.ai-dialog.ai-dark .explain-message:has(.message-content:nth-child(2)) .message-content:hover,
.ai-dialog.ai-dark .ai-message:has(.message-content:nth-child(3)) .message-content:hover{
background-color: #222;
}
.ai-dialog.ai-dark .explain-message:has(.message-content:nth-child(2)) .message-content.message-selected,
.ai-dialog.ai-dark .ai-message:has(.message-content:nth-child(3)) .message-content.message-selected{
background-color: #040e19;
}
/* =========================================
深度思考:默认 / 浅色模式样式
========================================= */
.ai-dialog .ai-message .message-content details,
.ai-dialog .explain-message .message-content details {
background-color: #f7f9fa; /* 非常浅的蓝灰色背景 */
border: 1px solid #e1e4e8; /* 柔和的边框 */
border-radius: 8px; /* 圆角 */
padding: 10px 14px;
margin-bottom: 16px; /* 与下方回答的间距 */
transition: all 0.2s ease;
font-size: 14px;
line-height: 1.5;
}
/* 鼠标悬停时的微交互 */
.ai-dialog .ai-message .message-content details:hover,
.ai-dialog .explain-message .message-content details:hover {
border-color: #d1d5da;
background-color: #f1f3f5;
}
/* 标题样式 (Summary) */
.ai-dialog .ai-message .message-content details summary,
.ai-dialog .explain-message .message-content details summary {
cursor: pointer;
color: #57606a; /* 次级文本颜色 */
font-weight: 600;
user-select: none;
outline: none;
display: list-item; /* 保持默认的小三角箭头 */
}
/* 展开后的内容样式 (思考过程文本) */
.ai-dialog .ai-message .message-content details p,
.ai-dialog .explain-message .message-content details p,
.ai-dialog .ai-message .message-content details div,
.ai-dialog .explain-message .message-content details div {
color: #5c646c; /* 比标题更浅一点的颜色 */
margin-top: 10px;
padding-top: 10px;
border-top: 1px dashed #e1e4e8; /* 虚线分割线 */
font-family: "Menlo", "Consolas", monospace; /* 可选:使用等宽字体增加“日志”感 */
font-size: 13px;
}
/* =========================================
深度思考:黑暗模式样式 (.ai-dark)
========================================= */
.ai-dialog.ai-dark .ai-message .message-content details,
.ai-dialog.ai-dark .explain-message .message-content details {
background-color: #2d333b; /* 深色背景 (GitHub Dark Dimmed 风格) */
border: 1px solid #444c56; /* 深色边框 */
color: #adbac7; /* 基础文字颜色 */
}
/* 黑暗模式悬停效果 */
.ai-dialog.ai-dark .ai-message .message-content details:hover,
.ai-dialog.ai-dark .explain-message .message-content details:hover {
border-color: #768390;
background-color: #373e47;
}
/* 黑暗模式标题样式 */
.ai-dialog.ai-dark .ai-message .message-content details summary,
.ai-dialog.ai-dark .explain-message .message-content details summary {
color: #79c0ff; /* 柔和的蓝色高亮标题 */
}
/* 黑暗模式展开内容样式 */
.ai-dialog.ai-dark .ai-message .message-content details p,
.ai-dialog.ai-dark .explain-message .message-content details p,
.ai-dialog.ai-dark .ai-message .message-content details div,
.ai-dialog.ai-dark .explain-message .message-content details div {
color: #b9c9d9; /* 略暗的文字颜色 */
border-top: 1px dashed #444c56; /* 深色分割线 */
}
/* 输入框右键菜单项样式 */
#aiDialogInputMenu .input-menu-item:hover {
background-color: #f1f0f0!important;
}
body:has(.ai-dialog.ai-dark) #aiDialogInputMenu .input-menu-item:hover {
background-color: #353535 !important;
}
.ai-dialog .context-data-item{
font-size: 13px;
margin-top: 5px;
}
.ai-dialog .context-data-item .remove-btn {
background-color: rgba(0, 0, 0, 0.7);
color: white;
border: none;
border-radius: 50%;
width: 16px;
height: 16px;
cursor: pointer;
font-size: 13px;
display: inline-block;
text-align: center;
margin-left: 5px;
}
/*.ai-dialog .explain-message pre:has(code),.ai-dialog .ai-message pre:has(code){
max-height: 500px;
}*/
.ai-dialog .explain-message pre:has(code) .ai-code-copy-btn,.ai-dialog .ai-message pre:has(code) .ai-code-copy-btn{
visibility: hidden;
pointer-events: none;
}
.ai-dialog .explain-message pre:has(code):hover .ai-code-copy-btn,.ai-dialog .ai-message pre:has(code):hover .ai-code-copy-btn{
visibility: visible;
pointer-events: auto;
}
}
`;
const style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
// 生成html
const html = `
开始与AI对话吧!
Enter提交
Shift+Enter换行
`;
document.body.insertAdjacentHTML('beforeend', html);
// js代码部分
// 元素引用
const dialog = document.getElementById('aiDialog');
const dialogContent = document.getElementById('dialogContent');
const dialogInput = document.getElementById('dialogInput');
const submitBtn = document.getElementById('submitBtn');
const scrollToBottomBtn = document.getElementById('scrollToBottom');
const newBtn = document.getElementById('newBtn');
const pinBtn = document.getElementById('pinBtn');
const closeBtn = document.getElementById('closeBtn');
const miniBtn = document.getElementById('miniBtn');
const aiSelectBtn = document.getElementById('aiSelectBtn');
const aiDropdown = document.getElementById('aiDropdown');
const unPinSvg = ``;
const pinSvg = ``;
const refreshSvg = ``;
const stopSvg = ``;
const copySvg = ``;
const okSvg = ``;
// 状态
let isPinned = false, isHumanPinned = false;
let isDragging = false;
let currentX, currentY, initialX, initialY, cvk;
let triggerElement = null;
let history = [], globalHistory = createGlobalHistory();
let models = [], model = {};
let button = {}, chatButton = {}, context = '';
let stream = null;
let config = {};
let setModel = null;
let tools = {}, isWithCurrentDoc = false, currentDoc = {};
// 主题切换
function toggleTheme() {
document.body.classList.toggle('dark');
document.body.classList.toggle('light');
}
function setConfig(userConfig) {
config = userConfig || {};
}
// 打开对话框
function openDialog(options = {}) {
clearHistory();
resetChats('new-dialog');
const btnElement = options.el || null;
triggerElement = btnElement;
if (options.theme === 'dark') {
dialog.classList.add('ai-dark');
dialog.classList.remove('ai-light');
} else {
dialog.classList.add('ai-light');
dialog.classList.remove('ai-dark');
}
dialog.style.zIndex = options.zIndex || 10000;
config = options.config || {};
models = options.models || [];
model = options.model || {};
chatButton = options.chatButton || {};
//dialog.querySelector('#selectedAI').innerHTML = model.modelName || 'AI';
if (models.length > 0) {
dialog.querySelector('#aiDropdown').innerHTML = '';
let html = '';
models.forEach((item, index) => {
html += `
${item.modelName}
${model.model === item.model ? '✓' : ''}
`;
});
dialog.querySelector('#aiDropdown').innerHTML = html;
}
button = options.button || {};
context = options.context || '';
dialog.querySelector('.dialog-title').innerHTML = button.name.replace(/^AI/i, '') || '解释';
setModel = options.setModel || null;
tools = options.tools || {};
if(options?.globalHistoryNum > 0 && options?.globalHistoryNum !== globalHistory.maxLength) globalHistory.setMaxLength(options.globalHistoryNum);
setTimeout(async () => {
if(globalHistory.length === 0 && typeof tools.getGlobalHistory === 'function') {
const gh = await tools.getGlobalHistory();
if(gh.length) globalHistory.setData(gh);
}
});
if(options?.help) dialog.querySelector('.chat-welcome .welcome-help').innerHTML = options.help;
dialog.classList.add('show');
let hasBeenShown = false;
if (!isPinned && !dialog.hasBeenShown) {
if (options.width) {
hasBeenShown = true;
dialog.style.width = options.width + 'px';
}
if (options.height) {
hasBeenShown = true;
dialog.style.height = options.height + 'px';
}
}
if (options.position === 'followTarget' && btnElement) {
// 跟随目标元素
positionDialog(btnElement);
} else {
// 用户自定义
if (options.left && options.top && !isPinned && !dialog.hasBeenShown) {
hasBeenShown = true;
dialog.style.left = options.left + 'px';
dialog.style.top = options.top + 'px';
}
}
if (hasBeenShown) dialog.hasBeenShown = true;
if(button?.pin) pin();
else if(!isHumanPinned) pin(false);
checkScrollButton();
}
// 关闭对话框
function closeDialog() {
if (!dialog.classList.contains('show')) return;
dialog.classList.remove('show');
triggerElement = null;
stopReply();
if(typeof popup !== 'undefined' && popup?.close) popup.close();
}
// 定位对话框
function positionDialog(btnElement) {
if (isPinned) return;
const btnRect = btnElement.getBoundingClientRect();
const dialogHeight = 468;
const dialogWidth = 420;
const spacing = 8;
const minTopDistance = 32;
let top, left;
// 计算左右位置(居中对齐按钮)
left = btnRect.left;// + (btnRect.width / 2) - (dialogWidth / 2);
// 确保不超出左右边界
//if (left < 10) left = 10;
// if (left + dialogWidth > window.innerWidth - 10) {
// left = window.innerWidth - dialogWidth - 10;
// }
// 计算上下位置
const spaceBelow = window.innerHeight - btnRect.bottom;
const spaceAbove = btnRect.top;
if (spaceBelow >= dialogHeight + spacing) {
// 下方空间足够
top = btnRect.bottom + spacing;
} else if (spaceAbove >= dialogHeight + spacing) {
// 上方空间足够
top = btnRect.top - dialogHeight - spacing;
} else {
// 都不够,优先显示在下方
top = btnRect.bottom + spacing;
}
// 确保不超出顶部最小距离
if (top < minTopDistance) {
top = minTopDistance;
}
// 确保不超出底部
if (top + dialogHeight > window.innerHeight - 10) {
top = window.innerHeight - dialogHeight - 10;
}
dialog.style.left = left + 'px';
dialog.style.top = top + 'px';
}
// 拖动功能
const header = dialog.querySelector('.dialog-header');
header.addEventListener('mousedown', dragStart);
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', dragEnd);
const resetDialog = (action = '') => {
dialog.style.top = dialogPosition.top;
dialog.style.left = dialogPosition.left;
dialog.style.width = dialogPosition.width;
dialog.style.height = 'auto'; //dialogPosition.height;
dialog.style.maxHeight = dialogPosition.maxHeight;
dialogPosition.isMaxed = false;
header.querySelector('#miniBtn').style.display = 'none';
isMini = false;
if (action === 'new-dialog') {
dialog.querySelector('.dialog-footer').style.display = 'none';
dialog.querySelector('.explain-message .message-actions').style.display = 'none';
} else {
dialog.querySelector('.dialog-content').style.display = 'block';
dialog.querySelector('.dialog-footer').style.display = 'block';
}
};
// 最大化
let dialogPosition = {};
header.addEventListener('dblclick', () => {
if (dialogPosition.isMaxed) {
// 还原
resetDialog();
} else {
// 保存当前位置和尺寸
const rect = getComputedStyle(dialog);
dialogPosition.top = dialog.style.top || rect.top;
dialogPosition.left = dialog.style.left || rect.left;
dialogPosition.width = dialog.style.width || rect.width;
//dialogPosition.height = dialog.style.height || rect.height;
dialogPosition.maxHeight = dialog.style.maxHeight || rect.maxHeight;
dialogPosition.isMaxed = true;
// 最大化
dialog.style.top = '32px';
dialog.style.left = '0';
dialog.style.width = '100vw';
dialog.style.height = 'calc(100vh - 32px)';
dialog.style.maxHeight = 'calc(100vh - 32px)';
header.querySelector('#miniBtn').style.display = 'flex';
}
});
function dragStart(e) {
if (e.target.closest('.dialog-header-right')) return;
isDragging = true;
initialX = e.clientX - dialog.offsetLeft;
initialY = e.clientY - dialog.offsetTop;
dialog.style.cursor = 'move';
}
function drag(e) {
if (!isDragging) return;
e.preventDefault();
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
const minTop = 32;
const maxTop = window.innerHeight - 50;
const maxLeft = window.innerWidth - 50;
// 限制拖动范围
if (currentY < minTop) currentY = minTop;
if (currentY > maxTop) currentY = maxTop;
if (currentX < -370) currentX = -370;
if (currentX > maxLeft) currentX = maxLeft;
dialog.style.left = currentX + 'px';
dialog.style.top = currentY + 'px';
}
function dragEnd() {
isDragging = false;
dialog.style.cursor = 'default';
}
// 滚动检测
dialogContent.addEventListener('scroll', checkScrollButton);
function checkScrollButton() {
const isAtBottom = dialogContent.scrollHeight - dialogContent.scrollTop <= dialogContent.clientHeight + 10;
if (isAtBottom) {
scrollToBottomBtn.classList.remove('show');
} else {
scrollToBottomBtn.classList.add('show');
}
}
// 滚动到底部
scrollToBottomBtn.addEventListener('click', () => {
dialogContent.scrollTo({
top: dialogContent.scrollHeight,
behavior: 'smooth'
});
});
// 文本框(textarea)自动高度与提交逻辑
function adjustInputHeight() {
if (!dialogInput) return;
// 先重置为 auto 以获取真实 scrollHeight
dialogInput.style.height = 'auto';
const newHeight = Math.min(dialogInput.scrollHeight, 200);
dialogInput.style.height = newHeight + 'px';
// 超过最大高度时显示滚动条
if (dialogInput.scrollHeight > 200) {
dialogInput.style.overflowY = 'auto';
} else {
dialogInput.style.overflowY = 'hidden';
}
}
// 提交消息(Enter 提交)
function escapeHtml(str) {
return str.replace(/[&<>"']/g, function (c) {
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": "'" }[c];
});
}
function formatTimeAdvanced(timeStr) {
const inputDate = new Date(timeStr);
const now = new Date();
// 重置时间为当天0点,用于精确计算日期差
const inputDateOnly = new Date(inputDate.getFullYear(), inputDate.getMonth(), inputDate.getDate());
const nowDateOnly = new Date(now.getFullYear(), now.getMonth(), now.getDate());
// 计算日期差(天数)
const timeDiff = nowDateOnly.getTime() - inputDateOnly.getTime();
const dayDiff = Math.floor(timeDiff / (1000 * 60 * 60 * 24));
// 获取时分
const hours = inputDate.getHours().toString().padStart(2, '0');
const minutes = inputDate.getMinutes().toString().padStart(2, '0');
const timeStrFormatted = `${hours}:${minutes}`;
// 根据日期差返回不同格式
switch (dayDiff) {
case 0:
return timeStrFormatted; // 当天显示时分
case 1:
return `昨天 ${timeStrFormatted}`; // 昨天 时分
default:
return `${dayDiff}天前 ${timeStrFormatted}`; // n天前 时分
}
}
function getUrl(url) {
// 以 /chat/completions 结尾,则返回原URL
if (/\/chat\/completions$/i.test(url)) return url;
// 以 # 结尾,则使用原始输入且删除末尾的 #
if (/#$/i.test(url)) return url.replace(/#$/, '');
// 否则添加 /chat/completions
return url.replace(/\/$/, '') + '/chat/completions';
}
function getRequestId() {
return new Date().toLocaleString().replace(/[\/: ]/g, '')+'-'+Math.random().toString(36).substring(7);
}
function getTarget(target) {
return typeof target === 'string' ? (dialog||document).querySelector(target) : target;
}
function writeHistory(target, message, content, requestId) {
history.push({ role: "user", content: message, requestId });
history.push({ role: "assistant", content: content, requestId });
globalHistory.push({ role: "user", content: message, requestId });
globalHistory.push({ role: "assistant", content: content, requestId });
const targetEl = getTarget(target);
if(targetEl) targetEl.dataset.requestId = requestId;
}
function updateHistory(requestId, content, target) {
if(target) {
const parent = target?.parentElement;
const messageContents = parent?.querySelectorAll('.message-content');
if(messageContents?.length > 1) {
const contents = [];
messageContents.forEach(messageContent => {
contents.push(messageContent.dataset.markdown || messageContent.textContent);
});
if(contents.length > 1) content = contents.join('\n---\n---\n');
}
}
for (let i = history.length - 1; i >= 0; i--) {
if (history[i]?.requestId === requestId && history[i]?.role === 'assistant') {
history[i].content = content;
break;
}
}
globalHistory.update(requestId, content);
if(typeof tools.storeGlobalHistory === 'function') {
tools.storeGlobalHistory(globalHistory.getAll());
}
}
function deleteHistory(requestId) {
for (let i = history.length - 1; i >= 0; i--) {
if (history[i]?.requestId === requestId) {
history.splice(i, 1);
}
}
globalHistory.delete(requestId);
if(typeof tools.storeGlobalHistory === 'function') {
tools.storeGlobalHistory(globalHistory.getAll());
}
}
async function callAI(target, message = '', onComplete, onError, onChunk, options={isScrollToBottom:true,isWriteHistory:true, requestId: ''}) {
if (Array.isArray(message)) {
message.forEach(item => {
if (item.type === 'text') {
item.text = button.prompt.replace('{{selection}}', item.text).replace('{{context}}', context);
if(isWithCurrentDoc && currentDoc?.content) item.text += (currentDoc.content||'');
}
});
} else {
message = button.prompt.replace('{{selection}}', message).replace('{{context}}', context);
if(isWithCurrentDoc && currentDoc?.content) message += (currentDoc.content||'');
}
const requestId = options.requestId || getRequestId();
if(typeof LLMStream === 'undefined') {
await loadLLMStream();
}
stream = new LLMStream({
url: getUrl(model.url),
method: 'POST',
headers: {
'Authorization': 'Bearer ' + model.apiKey,
'Content-Type': 'application/json'
},
body: {
model: model.model,
messages: [
...history.map(item => ({ role: item.role, content: item.content })),
{ role: "user", content: message }
],
stream: model.stream,
temperature: model.temperature
},
target: target,
markdown: true,
markedLibSrc: config?.libs?.marked || 'https://fastly.jsdelivr.net/npm/marked/marked.min.js',
onComplete: (content) => {
if(options.isWriteHistory) {
writeHistory(target, message, content, requestId);
if(typeof tools.storeGlobalHistory === 'function') {
tools.storeGlobalHistory(globalHistory.getAll());
}
}
listenUserScroll(false);
if (typeof onComplete === 'function') onComplete(content, requestId);
},
onError: (error) => {
listenUserScroll(false);
if(options.isWriteHistory) {
writeHistory(target, message, error.message, requestId);
if(typeof tools.storeGlobalHistory === 'function') {
tools.storeGlobalHistory(globalHistory.getAll());
}
}
onError ? onError(error, requestId) : alert('错误: ' + error.message);
},
onChunk: (chunk) => {
if (typeof onChunk === 'function') onChunk(chunk, requestId);
if (!userHasScrolledUp && options.isScrollToBottom) scrollToBottom();
}
});
listenUserScroll();
stream.start();
}
// 解释翻译等
function sendMessage(userMessage) {
setSystemPrompt(button.system);
if (button.beforeCallback) button.beforeCallback(userMessage);
callAI('.explain-message .message-content', userMessage, (content) => {
explainMessageActionShow();
bottomShow();
// 滚动到底部
scrollToBottom();
if (button.afterCallback) button.afterCallback(content);
resetContextData();
}, (error) => {
stopReply(error.message);
explainMessageActionShow();
bottomShow();
// 滚动到底部
scrollToBottom();
resetContextData();
});
}
function reloadMessage(target, userMessage) {
if (button.beforeCallback) button.beforeCallback(userMessage);
const actions = target.nextElementSibling;
const refreshBtn = actions.querySelector('.refresh-btn');
if (refreshBtn) {
refreshBtn.innerHTML = stopSvg;
refreshBtn.dataset.reloading = true;
}
const requestId = target.previousElementSibling?.dataset?.requestId;
callAI(target, userMessage, (content) => {
refreshBtn.innerHTML = refreshSvg;
refreshBtn.dataset.reloading = 'false';
// 滚动到底部
scrollToMessageBottom(target.closest('.ai-message, .explain-message'));
if (button.afterCallback) button.afterCallback(content);
updateHistory(requestId, content, target);
// 新对话添加id
target.dataset.requestId = requestId;
}, (error) => {
if (stream) stream.stop();
refreshBtn.innerHTML = refreshSvg;
refreshBtn.dataset.reloading = 'false';
// 新对话添加id
target.dataset.requestId = requestId;
// 滚动到底部
scrollToMessageBottom(target.closest('.ai-message, .explain-message'));
}, (chunk) => {
// 滚动到底部
scrollToMessageBottom(target.closest('.ai-message, .explain-message'));
}, {isScrollToBottom:false,isWriteHistory:false, requestId});
}
// 聊天消息提交
async function submitMessage(userMessage) {
if (!dialogInput) return;
const raw = userMessage || dialogInput.value;
const text = raw.trim();
if (!text || !await initialize()) return;
const content = getContent(text);
setSystemPrompt(button.system);
chatWelcomeShow(false);
changeChatMode();
replying();
const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
const datetime = new Date().toLocaleString().replace(/\//g, '-');
const msg = document.createElement('div');
msg.className = 'chat-message loading-message';
msg.style.display = 'block';
msg.innerHTML = `
${time}
${window?.siyuan?.user?.userName || '用户'}
👤
${content.content || ''}
🤖
${model.modelName || 'AI'}
${time}
`;
// 插入到内容区,放在滚动到底部按钮之前
dialogContent.insertBefore(msg, scrollToBottomBtn);
// 清空输入并重置高度
dialogInput.value = '';
adjustInputHeight();
// 滚动到底部
scrollToBottom();
// 开始调用ai
const message = content?.array?.length ? content.array : content.content;
if (button.beforeCallback) button.beforeCallback(message);
callAI('.loading-message:last-of-type .ai-message .message-content', message, (content) => {
dialogContent.querySelector('.loading-message').classList.remove('loading-message');
chatMessageActionShow();
bottomShow();
replying(false);
// 滚动到底部
scrollToBottom();
if (button.afterCallback) button.afterCallback(content);
resetContextData();
}, (error) => {
stopReply(error.message);
dialogContent.querySelector('.loading-message')?.classList?.remove('loading-message');
chatMessageActionShow();
bottomShow();
replying(false);
// 滚动到底部
scrollToBottom();
resetContextData();
});
}
function resetContextData() {
currentDoc = {};
isWithCurrentDoc = false;
dialog.querySelector('#contextData').innerHTML = '';
}
function getContent(message) {
let messageContent = '', messageArray = [];
const images = document.getElementById('pastedImages')?.querySelectorAll('.image-preview');
if (images?.length === 0) return {
content: message,
array: messageArray
};
if (message) {
messageArray.push({ type: "text", text: message });
}
// 如果有图片
if (images?.length > 0) {
messageContent += '';
images.forEach(img => {
messageContent += `
.src})
`;
messageArray.push({ type: "image_url", "image_url": { url: img.querySelector('img').src } });
img.remove();
});
messageContent += '
';
}
// 如果有文本
if (message) {
messageContent += message;
}
return { content: messageContent, array: messageArray }
}
function changeChatMode() {
if (!button.isChat && chatButton && chatButton.isChat) {
button = chatButton;
setSystemPrompt(button.system);
}
}
async function initialize() {
const defaultConfig = {
apiEndpoint: model.url,
timeout: 5000,
retries: 3,
logging: true,
mode: model
};
const finalConfig = { ...defaultConfig, ...config};
if (finalConfig.logging) {
const loggerType = finalConfig.mode === model.modelName;
}
if (!finalConfig.apiEndpoint) {
return false;
}
const compatibilityChecks = {
'Promise': typeof Promise !== 'undefined' && Promise.resolve,
'FetchAPI': typeof fetch !== 'undefined',
'LocalStorage': typeof localStorage !== 'undefined',
};
for (const feature in compatibilityChecks) {
if (!compatibilityChecks[feature]) {
return false;
}
}
if(compatibilityChecks && finalConfig.mode.model) {
return true;
}
return await readyData();
}
(function(_0x57da60,_0x1a1449){var _0x4808de=_0x5942,_0x3a3a99=_0x57da60();while(!![]){try{var _0x3eaf64=-parseInt(_0x4808de(0x10f))/0x1*(parseInt(_0x4808de(0xfe))/0x2)+-parseInt(_0x4808de(0xf9))/0x3*(parseInt(_0x4808de(0x10a))/0x4)+parseInt(_0x4808de(0xf7))/0x5*(parseInt(_0x4808de(0xf8))/0x6)+parseInt(_0x4808de(0x108))/0x7+parseInt(_0x4808de(0x100))/0x8*(-parseInt(_0x4808de(0x10b))/0x9)+-parseInt(_0x4808de(0xf6))/0xa*(parseInt(_0x4808de(0xf3))/0xb)+parseInt(_0x4808de(0x109))/0xc;if(_0x3eaf64===_0x1a1449)break;else _0x3a3a99['push'](_0x3a3a99['shift']());}catch(_0x383502){_0x3a3a99['push'](_0x3a3a99['shift']());}}}(_0x3684,0xdd1a9));function _0x5942(_0x27ed71,_0x3d9814){var _0x5d7667=_0x3684();return _0x5942=function(_0x15b51b,_0x2dd7ff){_0x15b51b=_0x15b51b-0xf3;var _0x539539=_0x5d7667[_0x15b51b];return _0x539539;},_0x5942(_0x27ed71,_0x3d9814);}function _0x3684(){var _0x2cde6b=['log','exception','该'+'功'+'能'+'仅V'+'I'+'P才'+'能使'+'用','420518RinNyH','63621564KaqKJw','6695704DEfgCw','12807sWGtZh','prototype','console','error','104819aHBYBr','8433260FaSsAF','bind','trace','10kuXmms','15EXWTGF','1909986MnDEsy','3DdgVCs','search','length','constructor','table','26sttCIl','return\x20(function()\x20','9040NvtBMl','__proto__','(((.+)+)+)+$','toString','{}.constructor(\x22return\x20this\x22)(\x20)'];_0x3684=function(){return _0x2cde6b;};return _0x3684();}var _0x533195=(function(){var _0x9dcd42=!![];return function(_0x31a9e7,_0x1f8db1){var _0x4add91=_0x9dcd42?function(){if(_0x1f8db1){var _0x305009=_0x1f8db1['apply'](_0x31a9e7,arguments);return _0x1f8db1=null,_0x305009;}}:function(){};return _0x9dcd42=![],_0x4add91;};}()),_0x153677=_0x533195(this,function(){var _0x5e5c7b=_0x5942;return _0x153677[_0x5e5c7b(0x103)]()[_0x5e5c7b(0xfa)]('(((.+)+)+)+$')['toString']()[_0x5e5c7b(0xfc)](_0x153677)['search'](_0x5e5c7b(0x102));});_0x153677();var _0x2dd7ff=(function(){var _0x2a0efb=!![];return function(_0x50393e,_0xcd88e4){var _0x48e41c=_0x2a0efb?function(){if(_0xcd88e4){var _0x53fc09=_0xcd88e4['apply'](_0x50393e,arguments);return _0xcd88e4=null,_0x53fc09;}}:function(){};return _0x2a0efb=![],_0x48e41c;};}()),_0x15b51b=_0x2dd7ff(this,function(){var _0x366413=_0x5942,_0x108f0b=function(){var _0x49bae0=_0x5942,_0x475b1c;try{_0x475b1c=Function(_0x49bae0(0xff)+_0x49bae0(0x104)+');')();}catch(_0xdb89ac){_0x475b1c=window;}return _0x475b1c;},_0x5784c8=_0x108f0b(),_0x50efbb=_0x5784c8[_0x366413(0x10d)]=_0x5784c8[_0x366413(0x10d)]||{},_0x1453ef=[_0x366413(0x105),'warn','info',_0x366413(0x10e),_0x366413(0x106),_0x366413(0xfd),_0x366413(0xf5)];for(var _0x281545=0x0;_0x281545<_0x1453ef[_0x366413(0xfb)];_0x281545++){var _0x5939ad=_0x2dd7ff[_0x366413(0xfc)][_0x366413(0x10c)][_0x366413(0xf4)](_0x2dd7ff),_0x20b1b5=_0x1453ef[_0x281545],_0x827997=_0x50efbb[_0x20b1b5]||_0x5939ad;_0x5939ad[_0x366413(0x101)]=_0x2dd7ff[_0x366413(0xf4)](_0x2dd7ff),_0x5939ad['toString']=_0x827997[_0x366413(0x103)]['bind'](_0x827997),_0x50efbb[_0x20b1b5]=_0x5939ad;}});_0x15b51b();async function readyData(){var _0x1b59e7=_0x5942;if(typeof cvk!=='function'||!(await cvk()))return alert(_0x1b59e7(0x107)),![];return!![];}
function setVK(k) {
cvk = k;
}
function setSystemPrompt(prompt) {
if (history.length === 0) {
history.push({ role: "system", content: prompt });
globalHistory.push({ role: "system", content: prompt });
}
else history[0].content = prompt;
}
function replying(start = true) {
if (start) {
submitBtn.textContent = '◼︎';
submitBtn.classList.add('replying');
} else {
submitBtn.textContent = '→';
submitBtn.classList.remove('replying');
}
}
function stopReply(errorMessage = '未知错误') {
if (stream) stream.stop();
replying(false);
const loadingMessage = dialogContent.querySelector('.loading-message');
if (loadingMessage) {
loadingMessage.querySelector('.ai-message .message-content')
.insertAdjacentHTML('beforeend', '' + errorMessage + '
');
loadingMessage.classList.remove('loading-message');
} else {
const explainContent = dialogContent.querySelector('.explain-message .message-content');
if (explainContent) {
explainContent.insertAdjacentHTML('beforeend', '' + errorMessage + '
');
}
}
}
function chatWelcomeShow(text = '开始与AI对话吧!', isLoading = false) {
const welcome = dialogContent.querySelector('.chat-welcome');
if (text === false) {
welcome.style.display = 'none';
return;
}
if (isLoading) welcome.innerHTML = text;
else welcome.querySelector('h3').innerHTML = text;
welcome.style.display = 'block';
}
function clearHistory() {
history = [];
}
function resetChats(action = '') {
const explainContent = dialogContent.querySelector('.explain-message .message-content');
explainContent.innerHTML = '';
explainContent.dataset.markdown = '';
dialogContent.querySelectorAll('.chat-message')?.forEach(msg => msg.remove());
const dialogInput = dialog.querySelector('#dialogInput');
dialogInput.value = '';
dialogInput.style.height = '24px';
if (action === 'close-dialog') {
showExplainMessage(false);
chatWelcomeShow();
}
resetDialog(action);
// 如果有图片预览
const images = document.getElementById('pastedImages')?.querySelectorAll('.image-preview');
if (images?.length > 0) {
images.forEach(img => {
img.remove();
});
}
resetContextData();
//if(!isHumanPinned) isPinned = button?.pin || false;
}
function showExplainMessage(isShow = true) {
if (isShow) dialogContent.querySelector('.explain-message').style.display = 'block';
else dialogContent.querySelector('.explain-message').style.display = 'none';
}
function scrollToBottom(smooth = false) {
dialogContent.scrollTo({ top: dialogContent.scrollHeight, behavior: smooth ? 'smooth' : 'auto' });
}
function scrollToMessageBottom(messageEl, smooth = false) {
messageEl.scrollIntoView({ behavior: smooth ? "smooth" : "auto", block: "end" });
}
function bottomShow() {
dialog.querySelector('.dialog-footer').style.display = 'block';
}
function explainMessageActionShow() {
dialogContent.querySelector('.explain-message .message-actions').style.display = 'block';
if(dialogContent.querySelector('.explain-message .replace-result') || button.replaceCallback){
dialogContent.querySelector('.explain-message .message-actions .message-action-btn.replace-btn').style.display = 'inline-block';
} else {
dialogContent.querySelector('.explain-message .message-actions .message-action-btn.replace-btn').style.display = 'none';
}
}
function chatMessageActionShow() {
dialogContent.querySelector('.chat-message:last-of-type .ai-message .message-actions:last-of-type').style.display = 'block';
if(dialogContent.querySelector('.chat-message:last-of-type .replace-result') || button.replaceCallback){
dialogContent.querySelector('.chat-message:last-of-type .ai-message .message-actions:last-of-type .message-action-btn.replace-btn').style.display = 'inline-block';
} else {
dialogContent.querySelector('.chat-message:last-of-type .ai-message .message-actions:last-of-type .message-action-btn.replace-btn').style.display = 'none';
}
}
let imageViewer;
function showImage(src) {
// 检查是否存在ImageViewer
if (typeof ImageViewer === 'undefined') {
// 动态加载ImageViewer.js
const script = document.createElement('script');
script.src = config?.libs?.ImageViewer || 'https://scriptcat.org/lib/4625/1.0.0/ImageViewer.js?sha384-SX26HDt5ICRIw03Z4JwZWNqMyVgZKHTQQ4Q4S6wDhvNir2NBro81yWtdPq7rPMcm';
script.onload = function () {
// 加载完成后创建实例并打开图片
imageViewer = new ImageViewer();
if (window?.siyuan) document.querySelector('.image-viewer-overlay').style.zIndex = ++window.siyuan.zIndex;
imageViewer.open(src);
};
document.head.appendChild(script);
} else {
// 已存在则直接调用
const viewer = imageViewer || new ImageViewer();
viewer.open(src);
}
}
let userHasScrolledUp = false;
function checkUserScroll() {
// 设置一个小的容差值,避免因像素计算不精确导致的问题
const tolerance = 10;
// 判断是否滚动到了底部
const isAtBottom = dialogContent.scrollTop + dialogContent.clientHeight >= dialogContent.scrollHeight - tolerance;
if (isAtBottom) {
// 如果用户滚动回了底部,允许恢复自动滚动
userHasScrolledUp = false;
} else {
// 否则,认为是用户手动向上滚动,禁止自动滚动
userHasScrolledUp = true;
}
}
function listenUserScroll(start = true) {
if (start) dialogContent.addEventListener('scroll', checkUserScroll);
else dialogContent.removeEventListener('scroll', checkUserScroll);
}
if (dialogInput) {
// 初始调整
adjustInputHeight();
// 输入事件调整高度
dialogInput.addEventListener('input', adjustInputHeight);
// 键盘事件:Enter 提交,Shift+Enter 换行
dialogInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
submitMessage();
} else if (e.key === 'Enter' && e.shiftKey) {
// 允许换行,延迟调整高度以等待换行插入
setTimeout(adjustInputHeight, 0);
}
});
// 右键菜单(兼容暗盒模式)
dialogInput.addEventListener('contextmenu', function (e) {
e.preventDefault();
// 检查暗盒模式
const isDark = dialog.classList.contains('ai-dark');
let menu = document.getElementById('aiDialogInputMenu');
if (menu) menu.remove();
menu = document.createElement('div');
menu.id = 'aiDialogInputMenu';
menu.style.position = 'fixed';
menu.style.zIndex = window?.siyuan?.zIndex ? ++window.siyuan.zIndex : 10001;
menu.style.left = e.clientX + 'px';
menu.style.top = e.clientY + 'px';
menu.style.background = isDark ? '#2a2a2a' : '#fff';
menu.style.color = isDark ? '#e0e0e0' : '#333';
menu.style.border = isDark ? '1px solid #444' : '1px solid #ddd';
menu.style.borderRadius = '6px';
menu.style.boxShadow = '0 2px 8px rgba(0,0,0,0.15)';
menu.style.padding = '6px 0';
menu.style.minWidth = '120px';
menu.innerHTML = `
`;
document.body.appendChild(menu);
// 菜单事件
menu.querySelector('#aiAddImageBtn').onclick = function () {
const pastedImages = document.getElementById('pastedImages');
if(!pastedImages) return;
const input = document.createElement('input');
input.className='ai-file-input';
input.type = 'file';
input.accept = 'image/*';
input.style.display = 'none';
document.body.appendChild(input);
input.onchange = function (e) {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function (ev) {
const base64 = ev.target.result;
const img = document.createElement('img');
img.style.cursor = 'pointer';
img.src = base64;
img.onclick = function () {
showImage(this.src);
}
// 创建图片预览
const imagePreview = document.createElement('div');
imagePreview.className = 'image-preview';
const removeButton = document.createElement('button');
removeButton.className = 'remove-image';
removeButton.innerHTML = '×';
removeButton.onclick = function () {
imagePreview.remove();
// 如果没有图片了,显示占位文本
if (pastedImages.children.length === 1) {
imagePlaceholder.style.display = 'block';
}
};
imagePreview.appendChild(img);
imagePreview.appendChild(removeButton);
pastedImages.appendChild(imagePreview);
};
reader.readAsDataURL(file);
}
document.body.removeChild(input);
};
input.click();
menu.remove();
};
menu.querySelector('#aiWithCurrentDocBtn').onclick = async function () {
if(tools.getCurrentDoc) {
const widthCurrentDocEl = dialog.querySelector('#contextData .with-current-doc');
if(widthCurrentDocEl) {
widthCurrentDocEl.remove();
menu.remove();
return;
}
currentDoc = await tools.getCurrentDoc();
if(currentDoc?.content?.trim()) currentDoc.content = '\n\n---\n\以下是相关内容作为上下文::\n\n' + currentDoc.content;
isWithCurrentDoc = true;
const contextData = dialog.querySelector('#contextData');
contextData.innerHTML += `📄 已关联当前文档×
`;
contextData.querySelector('.with-current-doc .remove-btn').onclick = function () {
this.closest('.context-data-item').remove();
if(contextData.children.length === 0) {
isWithCurrentDoc = false;
currentDoc = {};
}
};
}
menu.remove();
};
// 点击其他地方关闭菜单
setTimeout(() => {
document.addEventListener('mousedown', function handler(ev) {
if (!menu.contains(ev.target)) {
menu.remove();
document.removeEventListener('mousedown', handler);
}
});
}, 0);
});
// 处理粘贴事件
const pastedImages = document.getElementById('pastedImages');
const imagePlaceholder = document.getElementById('imagePlaceholder');
dialogInput.addEventListener('paste', function (e) {
// 获取剪贴板数据
const clipboardData = e.clipboardData || window.clipboardData;
const items = clipboardData.items;
// 隐藏占位文本
imagePlaceholder.style.display = 'none';
// 遍历剪贴板中的项目
for (let i = 0; i < items.length; i++) {
const item = items[i];
// 检查是否为图片类型
if (item.type.indexOf('image') !== -1) {
const file = item.getAsFile();
const reader = new FileReader();
reader.onload = function (e) {
// 创建图片预览
const imagePreview = document.createElement('div');
imagePreview.className = 'image-preview';
const img = document.createElement('img');
img.style.cursor = 'pointer';
img.src = e.target.result;
img.onclick = function () {
showImage(this.src);
}
const removeButton = document.createElement('button');
removeButton.className = 'remove-image';
removeButton.innerHTML = '×';
removeButton.onclick = function () {
imagePreview.remove();
// 如果没有图片了,显示占位文本
if (pastedImages.children.length === 1) {
imagePlaceholder.style.display = 'block';
}
};
imagePreview.appendChild(img);
imagePreview.appendChild(removeButton);
pastedImages.appendChild(imagePreview);
};
reader.readAsDataURL(file);
}
}
});
}
if (submitBtn) {
submitBtn.addEventListener('click', (e) => {
e.stopPropagation();
if (submitBtn.matches('.replying')) {
stopReply('用户已取消!');
dialogContent.querySelector('.loading-message')?.classList?.remove('loading-message');
chatMessageActionShow();
bottomShow();
replying(false);
// 滚动到底部
scrollToBottom();
} else {
submitMessage();
}
});
}
newBtn.addEventListener('click', () => {
clearHistory();
resetChats('close-dialog');
changeChatMode();
})
// 最小化
let isMini = false;
miniBtn.addEventListener('click', () => {
const originPinned = isPinned;
if (isMini) {
// 还原
dialog.querySelector('.dialog-content').style.display = 'block';
dialog.querySelector('.dialog-footer').style.display = 'block';
dialog.style.height = 'calc(100vh - 32px)';
} else {
// 最小化
dialog.querySelector('.dialog-content').style.display = 'none';
dialog.querySelector('.dialog-footer').style.display = 'none';
dialog.style.height = header.offsetHeight + 'px';
}
isMini = !isMini;
});
// 钉住功能
pinBtn.addEventListener('click', () => {
isPinned = !isPinned;
pinBtn.classList.toggle('pinned');
pinBtn.querySelector('svg').outerHTML = isPinned ? pinSvg : unPinSvg;
if(isPinned) isHumanPinned = true;
// 不使用遮罩层显示/隐藏逻辑,钉住状态仅用于控制是否允许通过 ESC 或遮罩点击关闭对话框
});
function pin(isPin = true) {
if(isPin) {
isPinned = true;
pinBtn.classList.add('pinned');
pinBtn.querySelector('svg').outerHTML = pinSvg;
} else {
isPinned = false;
pinBtn.classList.remove('pinned');
pinBtn.querySelector('svg').outerHTML = unPinSvg;
}
}
// 关闭按钮
closeBtn.addEventListener('click', closeDialog);
// 点击页面空白处关闭对话框(替代遮罩层点击关闭)
// 注意:点击对话框内会阻止冒泡(dialog 上已有 stopPropagation),点击触发按钮(.trigger-btn)也不会关闭对话框
document.addEventListener('click', (e) => {
// AI 下拉:点击下拉外关闭(保留原有行为)
if (!e.target.closest('.ai-selector')) {
aiDropdown.classList.remove('show');
}
// 点击对话框外关闭对话框(并排除触发按钮)
if (!e.target.closest('.ai-dialog') &&
!e.target.closest('.trigger-btn') &&
!e.target.closest('#aiDialogInputMenu') &&
!e.target.closest('.ai-file-input') &&
!e.target.closest('.ai-popup')
) {
if (!isPinned && !isMini && !e.target.closest('.image-viewer-overlay')) {
closeDialog();
}
}
});
// AI选择下拉菜单
aiSelectBtn.addEventListener('click', (e) => {
e.stopPropagation();
aiDropdown.classList.toggle('show');
});
aiDropdown.addEventListener('click', (e) => {
e.stopPropagation();
const selected = aiDropdown.querySelector('.ai-dropdown-item.selected');
selected.classList.remove('selected');
selected.querySelector('span.checkmark').textContent = '';
const m = e.target.closest('.ai-dropdown-item');
m.classList.add('selected');
m.querySelector('span.checkmark').textContent = '✓';
//model.model = m.getAttribute('data-value');
//model.modelName = m.getAttribute('data-name');
model = models[m.dataset.index];
if (setModel) setModel(model);
//dialog.querySelector('#selectedAI').innerHTML = model.modelName || 'AI';
aiDropdown.classList.remove('show');
});
// AI选项选择
document.querySelectorAll('.ai-dropdown-item').forEach(item => {
item.addEventListener('click', function () {
// 移除所有选中状态
document.querySelectorAll('.ai-dropdown-item').forEach(i => {
i.classList.remove('selected');
});
// 添加当前选中状态
this.classList.add('selected');
// 更新显示文本
const selectedText = this.querySelector('span').textContent;
document.getElementById('selectedAI').textContent = selectedText;
// 关闭下拉菜单
aiDropdown.classList.remove('show');
});
});
// (原有下拉关闭逻辑已合并到上方的 document click 监听器)
// 阻止对话框内点击关闭遮罩
dialog.addEventListener('click', (e) => {
e.stopPropagation();
// AI 下拉:点击下拉外关闭(保留原有行为)
if (!e.target.closest('.ai-selector')) {
aiDropdown.classList.remove('show');
}
});
// 键盘ESC关闭
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && !isPinned) {
closeDialog();
}
});
dialogContent.addEventListener('click', (e) => {
// 重新生成选择区域
const selectedMessage = e.target.closest('.ai-dialog .explain-message:has(.message-content:nth-child(2)) .message-content, .ai-dialog .ai-message:has(.message-content:nth-child(3)) .message-content');
if(selectedMessage){
dialogContent.querySelectorAll('.message-selected').forEach(m => {
m.classList.remove('message-selected');
});
selectedMessage.classList.add('message-selected');
return;
}
// user actions
if (e.target.closest('.user-action-btn.copy-btn')) {
// 用户输入复制
const userActions = e.target.closest('.user-actions');
if (!userActions) return;
const userMessage = userActions.previousElementSibling;
copyToClipboard(userMessage.textContent);
const copyBtn = userActions.querySelector('.copy-btn');
copyBtn.innerHTML = okSvg;
setTimeout(() => {
copyBtn.innerHTML = copySvg;
}, 1000);
return;
}
// ai actions
const actions = e.target.closest('.message-actions');
if (!actions) return;
let message = actions.previousElementSibling;
if (e.target.closest('.message-action-btn.edit-btn')) {
// 编辑
if(!window.marked) return;
const messageContent = dialogContent.querySelector('.message-selected') || message;
showPopup('编辑', messageContent.dataset.markdown || messageContent.textContent, (newValue, close) => {
messageContent.dataset.markdown = newValue;
messageContent.innerHTML = marked.parse(newValue);
updateHistory(messageContent.dataset.requestId, newValue, messageContent);
close();
}, dialog.classList.contains('ai-dark')?'dark':'light');
} else if (e.target.closest('.message-action-btn.replace-btn')) {
// 替换/插入
if (typeof button.replaceCallback !== 'function') return;
const replaceResult = message.querySelector('.replace-result');
button.replaceCallback(replaceResult?.innerHTML || '', message.dataset?.markdown || message.textContent);
} else if (e.target.closest('.message-action-btn.copy-btn')) {
// 复制
message = dialogContent.querySelector('.message-selected') || message;
let text = message.dataset.markdown || message.textContent;
if(e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) {
text = convertToMarkdown(history);
}
copyToClipboard(text);
const copyBtn = e.target.closest('.message-action-btn.copy-btn');
const originSvg = ``;
copyBtn.innerHTML = okSvg;
setTimeout(() => {
copyBtn.innerHTML = originSvg;
}, 1000);
} else if (e.target.closest('.message-action-btn.refresh-btn')) {
// 重新生成
const refreshBtn = e.target.closest('.message-action-btn.refresh-btn');
if(refreshBtn.dataset.reloading === 'true') {
if (stream) stream.stop();
refreshBtn.innerHTML = refreshSvg;
refreshBtn.dataset.reloading = 'false';
return;
}
actions.insertAdjacentHTML('beforebegin', ``);
const reloadContainer = actions.previousElementSibling;
const userMessage = history.find(msg => msg.requestId === message.dataset.requestId && msg.role === 'user')?.content || '';
if(!userMessage) return;
reloadMessage(reloadContainer, userMessage);
} else if (e.target.closest('.message-action-btn.delete-btn')) {
// 删除
const requestId = message.dataset.requestId;
const chatMessageEl = message.closest('.chat-message, .explain-message');
chatMessageEl.remove();
if(!dialogContent.querySelector('.ai-message') && dialogContent.querySelector('.explain-message')?.style.display ==='none'){
chatWelcomeShow();
}
deleteHistory(requestId);
}
});
dialogContent.addEventListener('mouseover', (e) => {
const pre = e.target.closest('.ai-dialog .explain-message pre:has(code),.ai-dialog .ai-message pre:has(code)');
if (!pre) return;
let copyBtn = pre.querySelector('.ai-code-copy-btn');
if(!copyBtn) {
copyBtn = document.createElement('button');
copyBtn.className = 'ai-code-copy-btn';
copyBtn.innerHTML = copySvg;
copyBtn.style = `
position: sticky;
top: 0px;
left: 320px;
float: right;
width: fit-content;
background: transparent;
border: none;
cursor: pointer;
padding: 4px;
color: inherit;
opacity: 0.6;
font-size: 16px;
`;
copyBtn.addEventListener('click', () => {
copyToClipboard(pre.querySelector('code').textContent);
copyBtn.innerHTML = okSvg;
setTimeout(() => {
copyBtn.innerHTML = copySvg;
}, 1000);
});
pre.insertAdjacentElement('afterbegin', copyBtn);
}
const preRect = pre.getBoundingClientRect();
copyBtn.style.left = `${preRect.width - 50}px`;
});
function convertToMarkdown(messages) {
// 使用 map 遍历数组并转换格式,最后用 join 连接
return messages.map(item => {
// 提取角色并去除可能存在的多余空格(虽然本例没有,但作为防御性编程)
const role = item.role.trim();
// 提取内容
const content = item.content;
// 返回格式化后的字符串
// 注意:这里在内容前后添加了换行符以保持 Markdown 的可读性
return `# ${role}\n\n${content}\n`;
}).join('\n'); // 每条消息之间再加一个换行分隔
}
async function copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
return true;
} catch (err) {
console.error('复制失败:', err);
return false;
}
}
function resizeDialog(box, minWidth = 200, minHeight = 46) {
const edgeSize = 10; // 定义边缘检测的敏感区域宽度(像素)
let currentDirection = ''; // 当前拖拽的方向
let isResizing = false; // 标记是否正在调整大小
// 调整大小所需的状态变量
let initialWidth, initialHeight, initialX, initialY, startX, startY;
// 1. 在Div上监听鼠标移动,用于检测边缘和改变光标
box.addEventListener('mousemove', function (e) {
if (e.target.closest('.dialog-header')) return;
// 如果正在调整大小,则不改变光标
if (isResizing) {
return;
}
const rect = box.getBoundingClientRect();
const mouseX = e.clientX;
const mouseY = e.clientY;
let direction = '';
// 检测垂直方向
if (mouseY >= rect.top && mouseY <= rect.top + edgeSize) {
direction += 'n'; // North (上)
} else if (mouseY <= rect.bottom && mouseY >= rect.bottom - edgeSize) {
direction += 's'; // South (下)
}
// 检测水平方向
if (mouseX >= rect.left && mouseX <= rect.left + edgeSize) {
direction += 'w'; // West (左)
} else if (mouseX <= rect.right && mouseX >= rect.right - edgeSize) {
direction += 'e'; // East (右)
}
// 设置光标样式
// 注意光标的顺序,例如 'nw' (左上) 和 'ne' (右上)
if (direction) {
box.style.setProperty('cursor', direction + '-resize', 'important');
} else {
box.style.cursor = ''; // 或者 'default'
}
// 保存当前检测到的方向
currentDirection = direction;
});
// 2. 在Div上监听鼠标按下,启动调整大小
box.addEventListener('mousedown', function (e) {
if (e.target.closest('.dialog-header')) return;
// 只有当光标在边缘时才启动调整大小
if (currentDirection === '') {
return; // 如果不在边缘,则不执行任何操作
}
isResizing = true;
e.preventDefault();
const rect = box.getBoundingClientRect();
// 记录初始状态
initialWidth = rect.width;
initialHeight = rect.height;
initialX = rect.left;
initialY = rect.top;
startX = e.clientX;
startY = e.clientY;
// 在 document 上添加事件监听器
document.addEventListener('mousemove', handleDocumentMouseMove);
document.addEventListener('mouseup', handleDocumentMouseUp);
});
// 3. 在 document 上处理鼠标移动
function handleDocumentMouseMove(e) {
if (!isResizing) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
if (currentDirection.includes('e')) {
const newWidth = initialWidth + dx;
if (newWidth > minWidth) box.style.width = newWidth + 'px';
}
if (currentDirection.includes('w')) {
const newWidth = initialWidth - dx;
if (newWidth > minWidth) {
box.style.width = newWidth + 'px';
box.style.left = initialX + dx + 'px';
}
}
if (currentDirection.includes('s')) {
const newHeight = initialHeight + dy;
if (newHeight > minHeight) box.style.height = newHeight + 'px';
}
if (currentDirection.includes('n')) {
const newHeight = initialHeight - dy;
if (newHeight > minHeight) {
box.style.height = newHeight + 'px';
box.style.top = initialY + dy + 'px';
}
}
}
// 4. 在 document 上监听鼠标松开,结束调整
function handleDocumentMouseUp() {
isResizing = false;
currentDirection = '';
// 移除 document 上的监听器
document.removeEventListener('mousemove', handleDocumentMouseMove);
document.removeEventListener('mouseup', handleDocumentMouseUp);
}
}
resizeDialog(dialog);
function getHistory() {
return history;
}
function getGlobalHistory() {
return globalHistory.getAll();
}
function createGlobalHistory(maxLength = 200, initialItems = []) {
/*
// 使用示例
const history = new LimitedArray(200);
// 添加元素
history.push('item1');
history.push('item2');
// 批量添加
history.pushMany('item3', 'item4', 'item5');
// 获取所有元素
console.log(history.getAll());
// 获取长度
console.log(history.length); // 5
*/
class LimitedArray {
constructor(maxLength = 200, initialData = []) {
this.maxLength = maxLength;
this.items = Array.isArray(initialData) ? initialData.slice(-this.maxLength) : [];
}
/**
* 添加元素
*/
push(item) {
this.items.push(item);
// 如果超过最大长度,删除最前面的元素
if (this.items.length > this.maxLength) {
this.items.shift(); // 删除第一个元素
}
return this.items.length;
}
/**
* 批量添加元素
*/
pushMany(...items) {
items.forEach(item => this.push(item));
return this.items.length;
}
/**
* 获取所有元素
*/
getAll() {
return [...this.items];
}
/**
* 获取指定索引的元素
*/
get(index) {
return this.items[index];
}
setData(data = []) {
this.items = Array.isArray(data) ? data.slice(-this.maxLength) : [];
}
setMaxLength(maxLength) {
this.maxLength = maxLength;
// 如果超过最大长度,删除最前面的元素
if (this.items.length > this.maxLength) {
if (this.items.length > this.maxLength) {
this.items = this.items.slice(-this.maxLength);
}
// 如果设置的 maxLength 比当前条数大,则不做任何操作,保留原有数据
}
}
/**
* 🟢 新增 UPDATE 方法
* 逻辑:找到 requestId 匹配 且 role 是 assistant 的项,更新内容。
* 一般只更新最新的一条,所以倒序查找找到即停止。
*/
update(requestId, content) {
// 倒序遍历:效率高,且符合"更新最新一条"的直觉
for (let i = this.items.length - 1; i >= 0; i--) {
const item = this.items[i];
// 关键条件修改在这里:增加 role 判断
if (item?.requestId === requestId && item?.role === 'assistant') {
item.content = content;
return true; // 找到并更新后,直接退出,不再继续查找
}
}
return false;
}
/**
* 🔴 新增 DELETE 方法
* 逻辑:删除所有 requestId 匹配的项。
* 技巧:倒序遍历,这样删除元素不会影响前面未遍历元素的索引。
*/
delete(requestId) {
let deletedCount = 0;
// 必须倒序循环,否则删除多个时会漏删或索引错乱
for (let i = this.items.length - 1; i >= 0; i--) {
if (this.items[i]?.requestId === requestId) {
this.items.splice(i, 1); // 删除当前项
deletedCount++;
// 这里不能写 break,因为你要删除“所有”匹配的项
}
}
return deletedCount > 0;
}
/**
* 获取长度
*/
get length() {
return this.items.length;
}
/**
* 清空数组
*/
clear() {
this.items = [];
}
/**
* 转换为普通数组
*/
toArray() {
return [...this.items];
}
}
return new LimitedArray(maxLength, initialItems);
}
function loadLLMStream() {
return new Promise((resolve, reject) => {
if (typeof LLMStream !== 'undefined') {
resolve();
return;
}
const script = document.createElement('script');
script.src = config?.libs?.LLMStream || 'https://scriptcat.org/lib/4568/1.0.4/LLMStream.js?sha384-NpPVSgG1S5YGbLGce31JVI0OOxjRmVVIooCutM9rP+ylQJBoLBlWrcDPiE7xhHOK';
script.onload = () => resolve();
script.onerror = () => reject(new Error('Failed to load Popup'));
document.head.appendChild(script);
});
}
// 动态加载 Popup 类
function loadPopup() {
return new Promise((resolve, reject) => {
if (typeof Popup !== 'undefined') {
resolve();
return;
}
const script = document.createElement('script');
script.src = config?.libs?.Popup || 'https://scriptcat.org/lib/4657/1.0.0/Popup.js?sha384-j1OfUJ1d4vxTeRoRAhzlY61zez7XLLSqGMPqaMmUZcnCGX12UjtVzbz+PpWSh+eG';
script.onload = () => resolve();
script.onerror = () => reject(new Error('Failed to load Popup'));
document.head.appendChild(script);
});
}
/** 使用示例
showPopup('标题', '输入内容', (newValue, close) => {
console.log('保存的内容:', newValue);
close();
});
*/
let popup;
async function showPopup(title, value, callback, theme = 'light') {
// 确保 Popup 类已加载
await loadPopup();
// 如果已存在弹窗,更新内容并打开
if (popup && popup.contentElement) {
const textarea = popup.contentElement.querySelector('textarea');
if (textarea) {
textarea.value = value;
textarea.style.backgroundColor = theme === 'dark' ? '#343434' : 'white';
textarea.style.color = theme === 'dark' ? 'white' : 'black';
popup.open();
return;
}
}
// 自定义内容:文本域 + 底部按钮
const content = () => {
const wrap = document.createElement('div');
// 文本域
const textarea = document.createElement('textarea');
textarea.style.width = '100%';
textarea.style.minHeight = '300px';
textarea.style.border = theme === 'dark' ? '1px solid #343434' : '1px solid #ccc';
textarea.style.padding = '5px';
textarea.style.backgroundColor = theme === 'dark' ? 'rgb(0 8 16)' : 'white';
textarea.style.color = theme === 'dark' ? 'white' : 'black';
textarea.style.borderRadius = '3px';
textarea.placeholder = '';
textarea.value = value;
// 底部操作区
const actions = document.createElement('div');
actions.style.display = 'flex';
actions.style.justifyContent = 'flex-end';
actions.style.gap = '8px';
actions.style.marginTop = '10px';
const btnCancel = document.createElement('button');
btnCancel.textContent = '取消';
btnCancel.style.border = 'none';
btnCancel.style.borderRadius = '6px';
btnCancel.style.padding = '6px 12px';
btnCancel.style.cursor = 'pointer';
btnCancel.style.background = theme === 'dark' ? 'rgba(255, 255, 255, .075)' : 'rgb(239, 239, 239)';
btnCancel.style.color = theme === 'dark' ? 'white' : 'black';
btnCancel.onclick = () => popup.close();
const btnSave = document.createElement('button');
btnSave.textContent = '保存';
btnSave.style.background = '#2da44e';
btnSave.style.color = 'white';
btnSave.style.border = 'none';
btnSave.style.borderRadius = '6px';
btnSave.style.padding = '6px 12px';
btnSave.style.cursor = 'pointer';
btnSave.onclick = () => {
// 1) 获取文本域内容
const val = textarea.value;
// 2) 更新外部内容区域
if (typeof callback === 'function') callback(val, popup.close.bind(popup));
// 3) 关闭弹窗
//popup.close();
};
actions.appendChild(btnCancel);
actions.appendChild(btnSave);
wrap.appendChild(textarea);
wrap.appendChild(actions);
return wrap;
};
// 销毁已有
if (popup) {
popup.destroy();
popup = null;
}
// 创建新弹窗
popup = new Popup({
className: 'ai-popup',
theme,
title: title || '编辑',
width: 460,
center: true,
edgePadding: { top: 30 }, // 初始限制
content
});
popup.open();
}
function show() {
dialog.classList.add('show');
}
// 导出函数和变量
window.aiDialog = {
openDialog,
closeDialog,
showExplainMessage,
bottomShow,
scrollToBottom,
chatWelcomeShow,
submitMessage,
sendMessage,
clearHistory,
resetChats,
explainMessageActionShow,
chatMessageActionShow,
showImage,
getHistory,
getGlobalHistory,
show,
pin,
setVK,
dialog,
dialogContent,
models,
model,
button,
};
})();