Files
Leantime/app/Views/Templates/components/aiPanel.blade.php

198 lines
8.3 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{{-- 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="关闭">&times;</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