198 lines
8.3 KiB
PHP
198 lines
8.3 KiB
PHP
{{-- OneBot 界面内 AI 助手:右下角悬浮聊天窗 --}}
|
||
@if(session()->has('userdata'))
|
||
<style>
|
||
#onebotAiFab {
|
||
position: fixed; right: 22px; bottom: 22px; z-index: 9998;
|
||
width: 56px; height: 56px; border-radius: 50%;
|
||
background: #4f46e5; color: #fff; border: none; cursor: pointer;
|
||
box-shadow: 0 6px 20px rgba(0,0,0,0.25);
|
||
font-size: 24px; display: flex; align-items: center; justify-content: center;
|
||
}
|
||
#onebotAiFab:hover { background: #4338ca; }
|
||
#onebotAiPanel {
|
||
position: fixed; right: 22px; bottom: 90px; z-index: 9999;
|
||
width: 420px; min-width: 320px; max-width: 94vw; height: 560px; min-height: 320px; max-height: 90vh;
|
||
background: #fff; border-radius: 12px; box-shadow: 0 12px 40px rgba(0,0,0,0.28);
|
||
display: none; flex-direction: column; overflow: hidden;
|
||
}
|
||
#onebotAiPanel.open { display: flex; }
|
||
#onebotAiHeader {
|
||
padding: 12px 16px; background: #4f46e5; color: #fff;
|
||
display: flex; justify-content: space-between; align-items: center; flex: none;
|
||
}
|
||
#onebotAiHeader .title { font-weight: 600; font-size: 15px; }
|
||
#onebotAiHeader .cfg { cursor: pointer; opacity: .85; font-size: 13px; }
|
||
#onebotAiHeader .cfg:hover { opacity: 1; }
|
||
#onebotAiHeader .close { cursor: pointer; font-size: 18px; line-height: 1; opacity: .85; }
|
||
#onebotAiHeader .close:hover { opacity: 1; }
|
||
#onebotAiMessages {
|
||
flex: 1; overflow-y: auto; padding: 14px; background: #f8fafc;
|
||
display: flex; flex-direction: column; gap: 10px;
|
||
}
|
||
#onebotAiMessages .msg { max-width: 82%; padding: 9px 12px; border-radius: 10px; font-size: 13px; line-height: 1.5; white-space: pre-wrap; word-break: break-word; }
|
||
#onebotAiMessages .msg.user { align-self: flex-end; background: #4f46e5; color: #fff; border-bottom-right-radius: 3px; }
|
||
#onebotAiMessages .msg.ai { align-self: flex-start; background: #fff; border: 1px solid #e2e8f0; color: #0f172a; border-bottom-left-radius: 3px; }
|
||
#onebotAiMessages .msg.err { align-self: flex-start; background: #fef2f2; border: 1px solid #fecaca; color: #b91c1c; }
|
||
#onebotAiInput {
|
||
flex: none; border-top: 1px solid #e2e8f0; padding: 10px; background: #fff;
|
||
display: flex; gap: 8px; align-items: flex-end;
|
||
}
|
||
#onebotAiInput textarea {
|
||
flex: 1; resize: vertical; min-height: 42px; max-height: 260px;
|
||
border: 1px solid #cbd5e1; border-radius: 8px;
|
||
padding: 8px 10px; font-size: 13px; height: 42px; outline: none;
|
||
line-height: 1.5;
|
||
}
|
||
#onebotAiInput textarea:focus { border-color: #4f46e5; }
|
||
#onebotAiInput button {
|
||
background: #4f46e5; color: #fff; border: none; border-radius: 8px;
|
||
padding: 0 16px; font-size: 13px; cursor: pointer; flex: none; height: 42px;
|
||
}
|
||
#onebotAiInput button:hover { background: #4338ca; }
|
||
#onebotAiInput button:disabled { background: #a5b4fc; cursor: not-allowed; }
|
||
#onebotAiResize {
|
||
position: absolute; right: 0; bottom: 0; width: 16px; height: 16px;
|
||
cursor: nwse-resize; z-index: 10000;
|
||
background: linear-gradient(135deg, transparent 0 45%, #cbd5e1 45% 55%, transparent 55% 100%);
|
||
}
|
||
</style>
|
||
|
||
<button id="onebotAiFab" title="AI 助手"><span class="fa fa-robot"></span></button>
|
||
|
||
<div id="onebotAiPanel">
|
||
<div id="onebotAiHeader">
|
||
<span class="title"><span class="fa fa-robot"></span> AI 助手</span>
|
||
<span>
|
||
<span class="cfg" id="onebotAiCfg" title="AI 设置"><span class="fa fa-gear"></span></span>
|
||
<span class="close" id="onebotAiClose" title="关闭">×</span>
|
||
</span>
|
||
</div>
|
||
<div id="onebotAiMessages"></div>
|
||
<div id="onebotAiInput">
|
||
<textarea id="onebotAiText" placeholder="输入指令,例如:列出所有 BOM" rows="1"></textarea>
|
||
<button id="onebotAiSend">发送</button>
|
||
</div>
|
||
<div id="onebotAiResize" title="拖动调整大小"></div>
|
||
</div>
|
||
|
||
<script>
|
||
jQuery(document).ready(function () {
|
||
var BASE = '{{ BASE_URL }}';
|
||
var csrf = function () { return jQuery('meta[name="csrf-token"]').attr('content') || ''; };
|
||
var history = []; // [{role, content}]
|
||
|
||
var $fab = jQuery('#onebotAiFab');
|
||
var $panel = jQuery('#onebotAiPanel');
|
||
var $msgs = jQuery('#onebotAiMessages');
|
||
var $text = jQuery('#onebotAiText');
|
||
var $send = jQuery('#onebotAiSend');
|
||
|
||
function addMsg(role, content) {
|
||
var cls = role === 'user' ? 'user' : (role === 'err' ? 'err' : 'ai');
|
||
jQuery('<div class="msg ' + cls + '"></div>').text(content).appendTo($msgs);
|
||
$msgs.scrollTop($msgs[0].scrollHeight);
|
||
}
|
||
|
||
function send() {
|
||
var text = $text.val().trim();
|
||
if (!text) { return; }
|
||
addMsg('user', text);
|
||
history.push({ role: 'user', content: text });
|
||
$text.val('');
|
||
$send.prop('disabled', true);
|
||
addMsg('ai', '思考中…');
|
||
|
||
jQuery.ajax({
|
||
url: BASE + '/ai/chat',
|
||
method: 'POST',
|
||
headers: { 'X-CSRF-TOKEN': csrf() },
|
||
contentType: 'application/json',
|
||
dataType: 'json',
|
||
data: JSON.stringify({ messages: history })
|
||
}).done(function (res) {
|
||
jQuery('#onebotAiMessages .msg:last').remove();
|
||
var reply = res.content || '';
|
||
addMsg('ai', reply);
|
||
history.push({ role: 'assistant', content: reply });
|
||
}).fail(function (xhr) {
|
||
jQuery('#onebotAiMessages .msg:last').remove();
|
||
var m = xhr.responseJSON && xhr.responseJSON.message ? xhr.responseJSON.message : ('HTTP ' + xhr.status);
|
||
addMsg('err', m);
|
||
}).always(function () {
|
||
$send.prop('disabled', false);
|
||
$text.trigger('focus');
|
||
});
|
||
}
|
||
|
||
$fab.on('click', function () { $panel.toggleClass('open'); if ($panel.hasClass('open')) $text.trigger('focus'); });
|
||
jQuery('#onebotAiClose').on('click', function () { $panel.removeClass('open'); });
|
||
$send.on('click', send);
|
||
$text.on('keydown', function (e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); } });
|
||
|
||
// 拖动调整面板大小
|
||
(function () {
|
||
var $resize = jQuery('#onebotAiResize');
|
||
var startX, startY, startW, startH;
|
||
|
||
$resize.on('mousedown', function (e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
startX = e.clientX;
|
||
startY = e.clientY;
|
||
startW = $panel.width();
|
||
startH = $panel.height();
|
||
|
||
jQuery(document).on('mousemove.aiResize', function (e) {
|
||
var w = startW + (e.clientX - startX);
|
||
var h = startH + (e.clientY - startY);
|
||
// 应用 min/max 约束(与 CSS 一致)
|
||
w = Math.max(320, Math.min(w, window.innerWidth * 0.94));
|
||
h = Math.max(320, Math.min(h, window.innerHeight * 0.9));
|
||
$panel.css({ width: w + 'px', height: h + 'px' });
|
||
});
|
||
|
||
jQuery(document).on('mouseup.aiResize', function () {
|
||
jQuery(document).off('mousemove.aiResize mouseup.aiResize');
|
||
});
|
||
});
|
||
|
||
// 触屏支持
|
||
$resize.on('touchstart', function (e) {
|
||
var t = e.originalEvent.touches[0];
|
||
startX = t.clientX;
|
||
startY = t.clientY;
|
||
startW = $panel.width();
|
||
startH = $panel.height();
|
||
});
|
||
$resize.on('touchmove', function (e) {
|
||
e.preventDefault();
|
||
var t = e.originalEvent.touches[0];
|
||
var w = startW + (t.clientX - startX);
|
||
var h = startH + (t.clientY - startY);
|
||
w = Math.max(320, Math.min(w, window.innerWidth * 0.94));
|
||
h = Math.max(320, Math.min(h, window.innerHeight * 0.9));
|
||
$panel.css({ width: w + 'px', height: h + 'px' });
|
||
});
|
||
})();
|
||
|
||
// AI 设置:弹出配置(预留地址填写)
|
||
jQuery('#onebotAiCfg').on('click', function () {
|
||
jQuery.ajax({
|
||
url: BASE + '/ai/config',
|
||
method: 'GET',
|
||
dataType: 'json'
|
||
}).done(function (res) {
|
||
var d = res.data || {};
|
||
addMsg('ai', '当前 AI 配置:\nProvider: ' + (d.provider || '-') +
|
||
'\nBaseURL: ' + (d.baseUrl || '-') +
|
||
'\nModel: ' + (d.model || '-') +
|
||
'\nAPI Key: ' + (d.apiKeyMasked || '未配置') +
|
||
'\n\n(配置在 .env 的 AI_* 变量,或联系管理员填写)');
|
||
}).fail(function () {
|
||
addMsg('err', '读取 AI 配置失败');
|
||
});
|
||
});
|
||
});
|
||
</script>
|
||
@endif
|