mirror of
https://github.com/kuaifan/dootask.git
synced 2026-01-21 08:28:12 +00:00
feat(ai-assistant): 添加聊天窗口模式和页面上下文感知
- 新增 chat 显示模式,支持可拖拽的悬浮聊天窗口 - 新增 page-context.js,根据当前路由提供针对性系统提示词 - 优化浮动按钮:添加淡入淡出动画、修复右键菜单拖动问题、更新配色 - 重构 Modal 为独立组件,支持 modal/chat 双模式切换 - 恢复会话时自动滚动到底部
This commit is contained in:
parent
32ffecb905
commit
70ad8c394a
@ -1,18 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<transition name="fade">
|
||||||
v-show="visible"
|
<div
|
||||||
ref="floatBtn"
|
v-if="visible"
|
||||||
class="ai-float-button no-dark-content"
|
ref="floatBtn"
|
||||||
:style="btnStyle"
|
class="ai-float-button no-dark-content"
|
||||||
@mousedown.stop.prevent="onMouseDown">
|
:style="btnStyle"
|
||||||
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
@mousedown.stop.prevent="onMouseDown">
|
||||||
<path d="M385.80516777 713.87417358c-12.76971517 0-24.13100586-7.79328205-28.82575409-19.62404756l-48.91927648-123.9413531c-18.40341303-46.75969229-55.77360888-84.0359932-102.53330118-102.53330117l-123.94135309-48.91927649c-11.83076552-4.69474822-19.62404757-16.05603892-19.62404757-28.8257541s7.79328205-24.13100586 19.62404757-28.82575407l123.94135309-48.91927649c46.75969229-18.40341303 84.0359932-55.77360888 102.53330118-102.53330119l48.91927648-123.94135308c4.69474822-11.83076552 16.05603892-19.62404757 28.8257541-19.62404757s24.13100586 7.79328205 28.82575408 19.62404757l48.91927648 123.94135308c18.40341303 46.75969229 55.77360888 84.0359932 102.53330118 102.53330119l123.94135309 48.91927649c11.83076552 4.69474822 19.62404757 16.05603892 19.62404757 28.82575407 0 12.76971517-7.79328205 24.13100586-19.62404757 28.8257541l-123.94135309 48.91927649c-46.75969229 18.40341303-84.0359932 55.77360888-102.53330118 102.53330117l-48.91927648 123.9413531c-4.69474822 11.83076552-16.14993388 19.62404757-28.82575408 19.62404756zM177.45224165 390.12433614l50.89107073 20.0935224c62.62794129 24.69437565 112.67395736 74.74039171 137.368333 137.36833299l20.09352239 50.89107073 20.0935224-50.89107073c24.69437565-62.62794129 74.74039171-112.67395736 137.368333-137.36833299l50.89107072-20.0935224-50.89107073-20.09352239c-62.62794129-24.69437565-112.67395736-74.74039171-137.36833299-137.36833301l-20.09352239-50.89107074-20.0935224 50.89107074c-24.69437565 62.62794129-74.74039171 112.67395736-137.368333 137.36833301l-50.89107073 20.09352239zM771.33789183 957.62550131c-12.76971517 0-24.13100586-7.79328205-28.82575409-19.62404758l-26.6661699-67.6043744c-8.63833672-21.87752672-26.10280012-39.34199011-47.98032684-47.98032684l-67.60437441-26.6661699c-11.83076552-4.69474822-19.62404757-16.05603892-19.62404757-28.82575409s7.79328205-24.13100586 19.62404757-28.82575409l67.60437441-26.6661699c21.87752672-8.63833672 39.34199011-26.10280012 47.98032684-47.98032685l26.6661699-67.6043744c4.69474822-11.83076552 16.05603892-19.62404757 28.82575409-19.62404757s24.13100586 7.79328205 28.82575409 19.62404757l26.66616991 67.6043744c8.63833672 21.87752672 26.10280012 39.34199011 47.98032684 47.98032685l67.6043744 26.6661699c11.83076552 4.69474822 19.62404757 16.05603892 19.62404757 28.82575409s-7.79328205 24.13100586-19.62404757 28.82575409l-67.6043744 26.6661699c-21.87752672 8.63833672-39.34199011 26.10280012-47.98032684 47.98032684l-26.66616991 67.6043744c-4.69474822 11.83076552-16.14993388 19.62404757-28.82575409 19.62404758z m-75.58544639-190.70067281c33.61439727 14.83540438 60.75004201 41.87715415 75.49155143 75.49155143 14.83540438-33.61439727 41.87715415-60.75004201 75.49155142-75.49155143-33.61439727-14.83540438-60.75004201-41.87715415-75.49155142-75.49155143-14.74150942 33.61439727-41.87715415 60.75004201-75.49155143 75.49155143z"/>
|
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||||
</svg>
|
<path d="M385.80516777 713.87417358c-12.76971517 0-24.13100586-7.79328205-28.82575409-19.62404756l-48.91927648-123.9413531c-18.40341303-46.75969229-55.77360888-84.0359932-102.53330118-102.53330117l-123.94135309-48.91927649c-11.83076552-4.69474822-19.62404757-16.05603892-19.62404757-28.8257541s7.79328205-24.13100586 19.62404757-28.82575407l123.94135309-48.91927649c46.75969229-18.40341303 84.0359932-55.77360888 102.53330118-102.53330119l48.91927648-123.94135308c4.69474822-11.83076552 16.05603892-19.62404757 28.8257541-19.62404757s24.13100586 7.79328205 28.82575408 19.62404757l48.91927648 123.94135308c18.40341303 46.75969229 55.77360888 84.0359932 102.53330118 102.53330119l123.94135309 48.91927649c11.83076552 4.69474822 19.62404757 16.05603892 19.62404757 28.82575407 0 12.76971517-7.79328205 24.13100586-19.62404757 28.8257541l-123.94135309 48.91927649c-46.75969229 18.40341303-84.0359932 55.77360888-102.53330118 102.53330117l-48.91927648 123.9413531c-4.69474822 11.83076552-16.14993388 19.62404757-28.82575408 19.62404756zM177.45224165 390.12433614l50.89107073 20.0935224c62.62794129 24.69437565 112.67395736 74.74039171 137.368333 137.36833299l20.09352239 50.89107073 20.0935224-50.89107073c24.69437565-62.62794129 74.74039171-112.67395736 137.368333-137.36833299l50.89107072-20.0935224-50.89107073-20.09352239c-62.62794129-24.69437565-112.67395736-74.74039171-137.36833299-137.36833301l-20.09352239-50.89107074-20.0935224 50.89107074c-24.69437565 62.62794129-74.74039171 112.67395736-137.368333 137.36833301l-50.89107073 20.09352239zM771.33789183 957.62550131c-12.76971517 0-24.13100586-7.79328205-28.82575409-19.62404758l-26.6661699-67.6043744c-8.63833672-21.87752672-26.10280012-39.34199011-47.98032684-47.98032684l-67.60437441-26.6661699c-11.83076552-4.69474822-19.62404757-16.05603892-19.62404757-28.82575409s7.79328205-24.13100586 19.62404757-28.82575409l67.60437441-26.6661699c21.87752672-8.63833672 39.34199011-26.10280012 47.98032684-47.98032685l26.6661699-67.6043744c4.69474822-11.83076552 16.05603892-19.62404757 28.82575409-19.62404757s24.13100586 7.79328205 28.82575409 19.62404757l26.66616991 67.6043744c8.63833672 21.87752672 26.10280012 39.34199011 47.98032684 47.98032685l67.6043744 26.6661699c11.83076552 4.69474822 19.62404757 16.05603892 19.62404757 28.82575409s-7.79328205 24.13100586-19.62404757 28.82575409l-67.6043744 26.6661699c-21.87752672 8.63833672-39.34199011 26.10280012-47.98032684 47.98032684l-26.66616991 67.6043744c-4.69474822 11.83076552-16.14993388 19.62404757-28.82575409 19.62404758z m-75.58544639-190.70067281c33.61439727 14.83540438 60.75004201 41.87715415 75.49155143 75.49155143 14.83540438-33.61439727 41.87715415-60.75004201 75.49155142-75.49155143-33.61439727-14.83540438-60.75004201-41.87715415-75.49155142-75.49155143-14.74150942 33.61439727-41.87715415 60.75004201-75.49155143 75.49155143z"/>
|
||||||
</div>
|
</svg>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import emitter from "../../store/events";
|
import emitter from "../../store/events";
|
||||||
|
import {withLanguagePreferencePrompt} from "../../utils/ai";
|
||||||
|
import {getPageContext} from "./page-context";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'AIAssistantFloatButton',
|
name: 'AIAssistantFloatButton',
|
||||||
@ -86,6 +90,7 @@ export default {
|
|||||||
window.removeEventListener('resize', this.onResize);
|
window.removeEventListener('resize', this.onResize);
|
||||||
document.removeEventListener('mousemove', this.onMouseMove);
|
document.removeEventListener('mousemove', this.onMouseMove);
|
||||||
document.removeEventListener('mouseup', this.onMouseUp);
|
document.removeEventListener('mouseup', this.onMouseUp);
|
||||||
|
document.removeEventListener('contextmenu', this.onContextMenu);
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
@ -143,6 +148,9 @@ export default {
|
|||||||
* 鼠标按下
|
* 鼠标按下
|
||||||
*/
|
*/
|
||||||
onMouseDown(e) {
|
onMouseDown(e) {
|
||||||
|
// 只响应鼠标左键
|
||||||
|
if (e.button !== 0) return;
|
||||||
|
|
||||||
this.record = {
|
this.record = {
|
||||||
time: Date.now(),
|
time: Date.now(),
|
||||||
startLeft: this.left,
|
startLeft: this.left,
|
||||||
@ -154,6 +162,16 @@ export default {
|
|||||||
|
|
||||||
document.addEventListener('mousemove', this.onMouseMove);
|
document.addEventListener('mousemove', this.onMouseMove);
|
||||||
document.addEventListener('mouseup', this.onMouseUp);
|
document.addEventListener('mouseup', this.onMouseUp);
|
||||||
|
document.addEventListener('contextmenu', this.onContextMenu);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 右键菜单弹出时取消拖动
|
||||||
|
*/
|
||||||
|
onContextMenu() {
|
||||||
|
if (this.dragging) {
|
||||||
|
this.onMouseUp();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -179,6 +197,7 @@ export default {
|
|||||||
onMouseUp() {
|
onMouseUp() {
|
||||||
document.removeEventListener('mousemove', this.onMouseMove);
|
document.removeEventListener('mousemove', this.onMouseMove);
|
||||||
document.removeEventListener('mouseup', this.onMouseUp);
|
document.removeEventListener('mouseup', this.onMouseUp);
|
||||||
|
document.removeEventListener('contextmenu', this.onContextMenu);
|
||||||
|
|
||||||
const moveDistance = Math.abs(this.left - this.record.startLeft) + Math.abs(this.top - this.record.startTop);
|
const moveDistance = Math.abs(this.left - this.record.startLeft) + Math.abs(this.top - this.record.startTop);
|
||||||
const duration = Date.now() - this.record.time;
|
const duration = Date.now() - this.record.time;
|
||||||
@ -218,10 +237,33 @@ export default {
|
|||||||
*/
|
*/
|
||||||
onClick() {
|
onClick() {
|
||||||
emitter.emit('openAIAssistant', {
|
emitter.emit('openAIAssistant', {
|
||||||
|
displayMode: 'chat',
|
||||||
sessionKey: 'global',
|
sessionKey: 'global',
|
||||||
resumeSession: 300,
|
resumeSession: 300,
|
||||||
|
showApplyButton: false,
|
||||||
|
onBeforeSend: this.handleBeforeSend,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理发送前的上下文准备
|
||||||
|
* 在发送时动态获取当前页面上下文,确保上下文与用户当前所在页面一致
|
||||||
|
* @param context - 对话历史上下文
|
||||||
|
* @returns {(string|*)[][]}
|
||||||
|
*/
|
||||||
|
handleBeforeSend(context = []) {
|
||||||
|
const {systemPrompt} = getPageContext(this.$store);
|
||||||
|
|
||||||
|
const prepared = [
|
||||||
|
['system', withLanguagePreferencePrompt(systemPrompt)],
|
||||||
|
];
|
||||||
|
|
||||||
|
if (context.length > 0) {
|
||||||
|
prepared.push(...context);
|
||||||
|
}
|
||||||
|
|
||||||
|
return prepared;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@ -231,8 +273,8 @@ export default {
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: #8bcf70;
|
||||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
box-shadow: 0 4px 12px lch(77 53.3 131.54 / 0.4);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -242,7 +284,7 @@ export default {
|
|||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
transform: scale(1.08);
|
transform: scale(1.08);
|
||||||
box-shadow: 0 6px 16px rgba(102, 126, 234, 0.5);
|
box-shadow: 0 6px 16px lch(77 53.3 131.54 / 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<Modal
|
<AssistantModal
|
||||||
v-model="showModal"
|
v-model="showModal"
|
||||||
:width="shouldCreateNewSession ? '440px' : '600px'"
|
:displayMode="displayMode"
|
||||||
:mask-closable="false"
|
:shouldCreateNewSession="shouldCreateNewSession">
|
||||||
:footer-hide="true"
|
|
||||||
class-name="ai-assistant-modal">
|
|
||||||
<div slot="header" class="ai-assistant-header">
|
<div slot="header" class="ai-assistant-header">
|
||||||
<div class="ai-assistant-header-title">
|
<div class="ai-assistant-header-title">
|
||||||
<i class="taskfont"></i>
|
<i class="taskfont"></i>
|
||||||
@ -90,6 +88,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else-if="displayMode === 'chat'" class="ai-assistant-welcome" @click="onFocus">
|
||||||
|
<div class="ai-assistant-welcome-icon">
|
||||||
|
<i class="taskfont"></i>
|
||||||
|
</div>
|
||||||
|
<div class="ai-assistant-welcome-title">
|
||||||
|
欢迎使用 AI 助手
|
||||||
|
</div>
|
||||||
|
<div class="ai-assistant-welcome-swiper">
|
||||||
|
<!-- Swiper 容器 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="ai-assistant-input">
|
<div class="ai-assistant-input">
|
||||||
<Input
|
<Input
|
||||||
v-model="inputValue"
|
v-model="inputValue"
|
||||||
@ -131,7 +140,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</AssistantModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@ -141,14 +150,16 @@ import {SSEClient} from "../../utils";
|
|||||||
import {AIBotMap, AIModelNames} from "../../utils/ai";
|
import {AIBotMap, AIModelNames} from "../../utils/ai";
|
||||||
import DialogMarkdown from "../../pages/manage/components/DialogMarkdown.vue";
|
import DialogMarkdown from "../../pages/manage/components/DialogMarkdown.vue";
|
||||||
import FloatButton from "./float-button.vue";
|
import FloatButton from "./float-button.vue";
|
||||||
|
import AssistantModal from "./modal.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'AIAssistant',
|
name: 'AIAssistant',
|
||||||
components: {DialogMarkdown},
|
components: {AssistantModal, DialogMarkdown},
|
||||||
floatButtonInstance: null,
|
floatButtonInstance: null,
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
// 弹窗状态
|
// 弹窗状态
|
||||||
|
displayMode: 'modal',
|
||||||
showModal: false,
|
showModal: false,
|
||||||
closing: false,
|
closing: false,
|
||||||
loadIng: 0,
|
loadIng: 0,
|
||||||
@ -232,6 +243,13 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
/**
|
||||||
|
* 获取输入框焦点事件
|
||||||
|
*/
|
||||||
|
onFocus() {
|
||||||
|
this.$refs.inputRef?.focus();
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 挂载浮动按钮到 body
|
* 挂载浮动按钮到 body
|
||||||
*/
|
*/
|
||||||
@ -264,6 +282,25 @@ export default {
|
|||||||
if (!$A.isJson(params)) {
|
if (!$A.isJson(params)) {
|
||||||
params = {};
|
params = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const newDisplayMode = params.displayMode === 'chat' ? 'chat' : 'modal';
|
||||||
|
let timeout = 0;
|
||||||
|
if (this.showModal && this.displayMode === 'chat' && newDisplayMode === 'modal') {
|
||||||
|
this.showModal = false;
|
||||||
|
timeout = 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.doOpenAssistant(params, newDisplayMode);
|
||||||
|
}, timeout);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 实际执行打开助手的逻辑
|
||||||
|
*/
|
||||||
|
doOpenAssistant(params, displayMode) {
|
||||||
|
// 应用参数
|
||||||
|
this.displayMode = displayMode;
|
||||||
this.inputValue = params.value || '';
|
this.inputValue = params.value || '';
|
||||||
this.inputPlaceholder = params.placeholder || null;
|
this.inputPlaceholder = params.placeholder || null;
|
||||||
this.inputRows = params.rows || null;
|
this.inputRows = params.rows || null;
|
||||||
@ -289,7 +326,7 @@ export default {
|
|||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.scheduleAutoSubmit();
|
this.scheduleAutoSubmit();
|
||||||
this.scrollResponsesToBottom();
|
this.scrollResponsesToBottom();
|
||||||
this.$refs.inputRef.focus();
|
this.onFocus();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -1103,6 +1140,7 @@ export default {
|
|||||||
this.currentSessionId = session.id;
|
this.currentSessionId = session.id;
|
||||||
this.responses = JSON.parse(JSON.stringify(session.responses));
|
this.responses = JSON.parse(JSON.stringify(session.responses));
|
||||||
this.syncResponseSeed();
|
this.syncResponseSeed();
|
||||||
|
this.scrollResponsesToBottom();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -1174,262 +1212,251 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.ai-assistant-modal {
|
|
||||||
--apply-reasoning-before-bg: #e1e1e1;
|
|
||||||
.ivu-modal {
|
|
||||||
transition: width 0.3s, max-width 0.3s;
|
|
||||||
.ivu-modal-header {
|
|
||||||
border-bottom: none !important;
|
|
||||||
}
|
|
||||||
.ivu-modal-body {
|
|
||||||
padding: 0 !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-assistant-header {
|
.ai-assistant-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: -11px 24px -10px 0;
|
||||||
|
height: 38px;
|
||||||
|
|
||||||
|
.ai-assistant-header-title {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin: -11px 24px -10px 0;
|
color: #303133;
|
||||||
height: 38px;
|
padding-right: 12px;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
.ai-assistant-header-title {
|
> i {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> span {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
display: flex;
|
overflow: hidden;
|
||||||
align-items: center;
|
text-overflow: ellipsis;
|
||||||
color: #303133;
|
white-space: nowrap;
|
||||||
padding-right: 12px;
|
font-size: 18px;
|
||||||
gap: 8px;
|
font-weight: 500;
|
||||||
|
|
||||||
> i {
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> span {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.ai-assistant-header-actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
|
|
||||||
.ai-assistant-header-btn {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.2s;
|
|
||||||
&:hover {
|
|
||||||
background-color: rgba(0, 0, 0, 0.06);
|
|
||||||
}
|
|
||||||
> i {
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.ai-assistant-header-actions {
|
||||||
.ai-assistant-content {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
align-items: center;
|
||||||
max-height: calc(var(--window-height) - var(--status-bar-height) - var(--navigation-bar-height) - 266px);
|
gap: 4px;
|
||||||
@media (height <= 900px) {
|
|
||||||
max-height: calc(var(--window-height) - var(--status-bar-height) - var(--navigation-bar-height) - 136px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-assistant-output {
|
.ai-assistant-header-btn {
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
padding: 12px 24px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
border-radius: 0;
|
|
||||||
background: #f8f9fb;
|
|
||||||
border: 0;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-assistant-output-item + .ai-assistant-output-item {
|
|
||||||
margin-top: 12px;
|
|
||||||
padding-top: 12px;
|
|
||||||
border-top: 1px solid rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-assistant-output-apply {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: 1;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
align-items: center;
|
|
||||||
height: 26px;
|
|
||||||
color: #999;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-assistant-output-icon {
|
|
||||||
font-size: 16px;
|
|
||||||
color: #52c41a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-assistant-apply-btn {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 13px;
|
width: 28px;
|
||||||
border-radius: 4px;
|
height: 28px;
|
||||||
height: 26px;
|
|
||||||
padding: 0 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-assistant-output-status {
|
|
||||||
color: #52c41a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-assistant-output-error {
|
|
||||||
color: #ff4d4f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-assistant-output-meta {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
height: 24px;
|
|
||||||
margin-top: -24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-assistant-output-model {
|
|
||||||
max-width: 50%;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #2f54eb;
|
|
||||||
background: rgba(47, 84, 235, 0.08);
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 2px 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-assistant-output-question {
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
overflow: hidden;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #666;
|
|
||||||
line-height: 1.4;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-assistant-output-placeholder {
|
|
||||||
margin-top: 12px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #999;
|
|
||||||
padding: 8px;
|
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background: rgba(0, 0, 0, 0.02);
|
cursor: pointer;
|
||||||
}
|
transition: background-color 0.2s;
|
||||||
|
|
||||||
.ai-assistant-output-markdown {
|
|
||||||
margin-top: 12px;
|
|
||||||
font-size: 13px;
|
|
||||||
|
|
||||||
.apply-reasoning {
|
|
||||||
margin: 0 0 12px 0;
|
|
||||||
padding: 0 0 0 13px;
|
|
||||||
line-height: 26px;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&:before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
bottom: 0;
|
|
||||||
width: 2px;
|
|
||||||
background-color: var(--apply-reasoning-before-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.reasoning-label {
|
|
||||||
margin-bottom: 4px;
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reasoning-content {
|
|
||||||
opacity: 0.5;
|
|
||||||
> p:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-assistant-input {
|
|
||||||
padding: 4px 16px 16px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
|
|
||||||
.ivu-input {
|
|
||||||
background-color: transparent;
|
|
||||||
border: 0;
|
|
||||||
border-radius: 0;
|
|
||||||
box-shadow: none;
|
|
||||||
padding: 0 8px;
|
|
||||||
resize: none;
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: transparent;
|
background-color: rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
> i {
|
||||||
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.ivu-select-selection {
|
}
|
||||||
background-color: transparent;
|
|
||||||
border: 0;
|
.ai-assistant-content {
|
||||||
border-radius: 0;
|
display: flex;
|
||||||
box-shadow: none;
|
flex-direction: column;
|
||||||
padding: 0 0 0 8px;
|
max-height: calc(var(--window-height) - var(--status-bar-height) - var(--navigation-bar-height) - 266px);
|
||||||
|
@media (height <= 900px) {
|
||||||
|
max-height: calc(var(--window-height) - var(--status-bar-height) - var(--navigation-bar-height) - 136px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-assistant-welcome,
|
||||||
|
.ai-assistant-output {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 12px 24px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
border-radius: 0;
|
||||||
|
background: #f8f9fb;
|
||||||
|
border: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-assistant-output-item + .ai-assistant-output-item {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-assistant-output-apply {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
height: 26px;
|
||||||
|
color: #999;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-assistant-output-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #52c41a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-assistant-apply-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 13px;
|
||||||
|
border-radius: 4px;
|
||||||
|
height: 26px;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-assistant-output-status {
|
||||||
|
color: #52c41a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-assistant-output-error {
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-assistant-output-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 24px;
|
||||||
|
margin-top: -24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-assistant-output-model {
|
||||||
|
max-width: 50%;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2f54eb;
|
||||||
|
background: rgba(47, 84, 235, 0.08);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-assistant-output-question {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-assistant-output-placeholder {
|
||||||
|
margin-top: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #999;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: rgba(0, 0, 0, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-assistant-output-markdown {
|
||||||
|
margin-top: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
|
||||||
|
.apply-reasoning {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
padding: 0 0 0 13px;
|
||||||
|
line-height: 26px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 2px;
|
||||||
|
background-color: var(--apply-reasoning-before-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reasoning-label {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reasoning-content {
|
||||||
|
opacity: 0.5;
|
||||||
|
> p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-assistant-input {
|
||||||
|
padding: 4px 16px 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.ivu-input {
|
||||||
|
background-color: transparent;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
padding: 0 8px;
|
||||||
|
resize: none;
|
||||||
|
&:hover {
|
||||||
|
border-color: transparent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ai-assistant-footer {
|
.ivu-select-selection {
|
||||||
display: flex;
|
background-color: transparent;
|
||||||
justify-content: space-between;
|
border: 0;
|
||||||
flex-wrap: wrap;
|
border-radius: 0;
|
||||||
gap: 12px;
|
box-shadow: none;
|
||||||
.ai-assistant-footer-models {
|
padding: 0 0 0 8px;
|
||||||
text-align: left;
|
}
|
||||||
.ivu-select-disabled {
|
}
|
||||||
.ivu-select-selection {
|
|
||||||
background-color: transparent;
|
.ai-assistant-footer {
|
||||||
}
|
display: flex;
|
||||||
}
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
.ai-assistant-footer-models {
|
||||||
|
text-align: left;
|
||||||
|
.ivu-select-disabled {
|
||||||
.ivu-select-selection {
|
.ivu-select-selection {
|
||||||
border: 0;
|
background-color: transparent;
|
||||||
box-shadow: none;
|
|
||||||
.ivu-select-placeholder,
|
|
||||||
.ivu-select-selected-value {
|
|
||||||
padding-left: 0;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.ai-assistant-footer-btns {
|
.ivu-select-selection {
|
||||||
flex: 1;
|
border: 0;
|
||||||
display: flex;
|
box-shadow: none;
|
||||||
justify-content: flex-end;
|
.ivu-select-placeholder,
|
||||||
|
.ivu-select-selected-value {
|
||||||
|
padding-left: 0;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.ai-assistant-footer-btns {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ai-assistant-history-menu {
|
.ai-assistant-history-menu {
|
||||||
@ -1507,6 +1534,93 @@ export default {
|
|||||||
color: #F56C6C;
|
color: #F56C6C;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ai-assistant-chat {
|
||||||
|
position: fixed;
|
||||||
|
right: 24px;
|
||||||
|
bottom: 24px;
|
||||||
|
width: 460px;
|
||||||
|
height: 80vh;
|
||||||
|
min-width: 380px;
|
||||||
|
max-width: 600px;
|
||||||
|
max-height: 640px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
box-shadow: 0 10px 30px 0 rgba(0, 0, 0, 0.12);
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.ai-assistant-close {
|
||||||
|
position: absolute;
|
||||||
|
top: 6px;
|
||||||
|
right: 10px;
|
||||||
|
z-index: 1;
|
||||||
|
font-size: 38px;
|
||||||
|
color: #999;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
&:hover {
|
||||||
|
color: #444;
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-assistant-drag-handle {
|
||||||
|
cursor: move;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-assistant-header {
|
||||||
|
margin: 6px 48px 6px 16px;
|
||||||
|
|
||||||
|
.ai-assistant-header-title {
|
||||||
|
> span {
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-assistant-content {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.ai-assistant-welcome {
|
||||||
|
.ai-assistant-welcome-icon {
|
||||||
|
margin-top: 12px;
|
||||||
|
i {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ai-assistant-welcome-title {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
.ai-assistant-welcome-swiper {
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-assistant-input {
|
||||||
|
padding: 4px 12px 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-assistant-modal {
|
||||||
|
--apply-reasoning-before-bg: #e1e1e1;
|
||||||
|
.ivu-modal {
|
||||||
|
transition: width 0.3s, max-width 0.3s;
|
||||||
|
.ivu-modal-header {
|
||||||
|
border-bottom: none !important;
|
||||||
|
}
|
||||||
|
.ivu-modal-body {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
body.dark-mode-reverse {
|
body.dark-mode-reverse {
|
||||||
.ai-assistant-modal {
|
.ai-assistant-modal {
|
||||||
--apply-reasoning-before-bg: #4e4e56;
|
--apply-reasoning-before-bg: #4e4e56;
|
||||||
|
|||||||
295
resources/assets/js/components/AIAssistant/modal.vue
Normal file
295
resources/assets/js/components/AIAssistant/modal.vue
Normal file
@ -0,0 +1,295 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="displayMode === 'chat'" v-transfer-dom :data-transfer="true">
|
||||||
|
<transition name="fade">
|
||||||
|
<div
|
||||||
|
v-if="visible"
|
||||||
|
ref="chatWindow"
|
||||||
|
class="ai-assistant-chat"
|
||||||
|
:style="chatStyle">
|
||||||
|
<Icon class="ai-assistant-close" type="ios-close" @click="onClose"/>
|
||||||
|
<div
|
||||||
|
class="ai-assistant-drag-handle"
|
||||||
|
@mousedown.stop.prevent="onMouseDown">
|
||||||
|
<slot name="header"></slot>
|
||||||
|
</div>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
<Modal
|
||||||
|
v-else
|
||||||
|
v-model="visible"
|
||||||
|
:width="shouldCreateNewSession ? '440px' : '600px'"
|
||||||
|
:mask-closable="false"
|
||||||
|
:footer-hide="true"
|
||||||
|
class-name="ai-assistant-modal">
|
||||||
|
<template #header>
|
||||||
|
<slot name="header"></slot>
|
||||||
|
</template>
|
||||||
|
<slot></slot>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import TransferDom from "../../directives/transfer-dom";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'AssistantModal',
|
||||||
|
directives: {TransferDom},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
displayMode: {
|
||||||
|
type: String,
|
||||||
|
default: 'modal'
|
||||||
|
},
|
||||||
|
shouldCreateNewSession: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
// 位置存储:只保存两个距离(水平一个、垂直一个)
|
||||||
|
position: {
|
||||||
|
x: 24, // 水平距离值
|
||||||
|
y: 24, // 垂直距离值
|
||||||
|
fromRight: true, // true: 距右边, false: 距左边
|
||||||
|
fromBottom: true, // true: 距底部, false: 距顶部
|
||||||
|
},
|
||||||
|
dragging: false,
|
||||||
|
positionLoaded: false,
|
||||||
|
cacheKey: 'aiAssistant.chatPosition',
|
||||||
|
windowSize: {
|
||||||
|
width: 460,
|
||||||
|
height: 640,
|
||||||
|
},
|
||||||
|
record: {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
visible: {
|
||||||
|
get() {
|
||||||
|
return this.value;
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$emit('input', val);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clientWidth() {
|
||||||
|
return this.windowWidth || document.documentElement.clientWidth;
|
||||||
|
},
|
||||||
|
|
||||||
|
clientHeight() {
|
||||||
|
return this.windowHeight || document.documentElement.clientHeight;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 计算实际的 left 值
|
||||||
|
left() {
|
||||||
|
if (this.position.fromRight) {
|
||||||
|
return this.clientWidth - this.windowSize.width - this.position.x;
|
||||||
|
}
|
||||||
|
return this.position.x;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 计算实际的 top 值
|
||||||
|
top() {
|
||||||
|
if (this.position.fromBottom) {
|
||||||
|
return this.clientHeight - this.windowSize.height - this.position.y;
|
||||||
|
}
|
||||||
|
return this.position.y;
|
||||||
|
},
|
||||||
|
|
||||||
|
chatStyle() {
|
||||||
|
if (!this.positionLoaded) {
|
||||||
|
return {
|
||||||
|
opacity: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
left: `${this.left}px`,
|
||||||
|
top: `${this.top}px`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
visible(val) {
|
||||||
|
if (val && this.displayMode === 'chat') {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.updateWindowSize();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.loadPosition();
|
||||||
|
window.addEventListener('resize', this.onResize);
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeDestroy() {
|
||||||
|
window.removeEventListener('resize', this.onResize);
|
||||||
|
document.removeEventListener('mousemove', this.onMouseMove);
|
||||||
|
document.removeEventListener('mouseup', this.onMouseUp);
|
||||||
|
document.removeEventListener('contextmenu', this.onContextMenu);
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
/**
|
||||||
|
* 更新窗口实际尺寸
|
||||||
|
*/
|
||||||
|
updateWindowSize() {
|
||||||
|
const el = this.$refs.chatWindow;
|
||||||
|
if (el) {
|
||||||
|
this.windowSize = {
|
||||||
|
width: el.offsetWidth,
|
||||||
|
height: el.offsetHeight,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载保存的位置
|
||||||
|
*/
|
||||||
|
async loadPosition() {
|
||||||
|
try {
|
||||||
|
const saved = await $A.IDBString(this.cacheKey);
|
||||||
|
if (saved) {
|
||||||
|
const pos = JSON.parse(saved);
|
||||||
|
if (pos && typeof pos.x === 'number' && typeof pos.y === 'number') {
|
||||||
|
this.position = pos;
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.checkBounds();
|
||||||
|
this.positionLoaded = true;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
// 默认位置:右下角
|
||||||
|
this.position = {x: 24, y: 24, fromRight: true, fromBottom: true};
|
||||||
|
this.positionLoaded = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存位置
|
||||||
|
*/
|
||||||
|
savePosition() {
|
||||||
|
$A.IDBSave(this.cacheKey, JSON.stringify(this.position));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据当前 left/top 更新 position 对象
|
||||||
|
*/
|
||||||
|
updatePositionFromCoords(left, top) {
|
||||||
|
const centerX = left + this.windowSize.width / 2;
|
||||||
|
const centerY = top + this.windowSize.height / 2;
|
||||||
|
|
||||||
|
// 判断在哪个半区
|
||||||
|
const fromRight = centerX >= this.clientWidth / 2;
|
||||||
|
const fromBottom = centerY >= this.clientHeight / 2;
|
||||||
|
|
||||||
|
// 计算距离
|
||||||
|
const x = fromRight ? (this.clientWidth - this.windowSize.width - left) : left;
|
||||||
|
const y = fromBottom ? (this.clientHeight - this.windowSize.height - top) : top;
|
||||||
|
|
||||||
|
this.position = {x, y, fromRight, fromBottom};
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 鼠标按下
|
||||||
|
*/
|
||||||
|
onMouseDown(e) {
|
||||||
|
// 只响应鼠标左键
|
||||||
|
if (e.button !== 0) return;
|
||||||
|
|
||||||
|
this.updateWindowSize();
|
||||||
|
this.record = {
|
||||||
|
time: Date.now(),
|
||||||
|
startLeft: this.left,
|
||||||
|
startTop: this.top,
|
||||||
|
offsetX: e.clientX - this.left,
|
||||||
|
offsetY: e.clientY - this.top,
|
||||||
|
};
|
||||||
|
this.dragging = true;
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', this.onMouseMove);
|
||||||
|
document.addEventListener('mouseup', this.onMouseUp);
|
||||||
|
document.addEventListener('contextmenu', this.onContextMenu);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 右键菜单弹出时取消拖动
|
||||||
|
*/
|
||||||
|
onContextMenu() {
|
||||||
|
if (this.dragging) {
|
||||||
|
this.onMouseUp();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 鼠标移动
|
||||||
|
*/
|
||||||
|
onMouseMove(e) {
|
||||||
|
if (!this.dragging) return;
|
||||||
|
|
||||||
|
const minMargin = 12;
|
||||||
|
let newLeft = e.clientX - this.record.offsetX;
|
||||||
|
let newTop = e.clientY - this.record.offsetY;
|
||||||
|
|
||||||
|
// 边界限制(最小边距12px)
|
||||||
|
newLeft = Math.max(minMargin, Math.min(newLeft, this.clientWidth - this.windowSize.width - minMargin));
|
||||||
|
newTop = Math.max(minMargin, Math.min(newTop, this.clientHeight - this.windowSize.height - minMargin));
|
||||||
|
|
||||||
|
this.updatePositionFromCoords(newLeft, newTop);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 鼠标松开
|
||||||
|
*/
|
||||||
|
onMouseUp() {
|
||||||
|
document.removeEventListener('mousemove', this.onMouseMove);
|
||||||
|
document.removeEventListener('mouseup', this.onMouseUp);
|
||||||
|
document.removeEventListener('contextmenu', this.onContextMenu);
|
||||||
|
|
||||||
|
this.savePosition();
|
||||||
|
this.dragging = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查边界(仅在加载和窗口变化时调用)
|
||||||
|
*/
|
||||||
|
checkBounds() {
|
||||||
|
const minMargin = 12;
|
||||||
|
// 确保距离在有效范围内(最小12px,最大不超出屏幕)
|
||||||
|
const maxX = this.clientWidth - this.windowSize.width - minMargin;
|
||||||
|
const maxY = this.clientHeight - this.windowSize.height - minMargin;
|
||||||
|
this.position.x = Math.max(minMargin, Math.min(this.position.x, maxX));
|
||||||
|
this.position.y = Math.max(minMargin, Math.min(this.position.y, maxY));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 窗口大小改变
|
||||||
|
*/
|
||||||
|
onResize() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.updateWindowSize();
|
||||||
|
this.checkBounds();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onClose() {
|
||||||
|
this.$emit('input', false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
226
resources/assets/js/components/AIAssistant/page-context.js
vendored
Normal file
226
resources/assets/js/components/AIAssistant/page-context.js
vendored
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
/**
|
||||||
|
* AI 助手页面上下文配置
|
||||||
|
*
|
||||||
|
* 根据当前路由和应用状态,为 AI 助手提供针对性的系统提示词
|
||||||
|
* 原则:诚实描述 AI 能力边界,不承诺做不到的事情
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前页面的 AI 上下文
|
||||||
|
* @param {Object} store - Vuex store 实例
|
||||||
|
* @returns {Object} { systemPrompt, title }
|
||||||
|
*/
|
||||||
|
export function getPageContext(store) {
|
||||||
|
const routeName = store.state.routeName;
|
||||||
|
|
||||||
|
const contextMap = {
|
||||||
|
'manage-dashboard': getDashboardContext,
|
||||||
|
'manage-project': getProjectContext,
|
||||||
|
'manage-messenger': getMessengerContext,
|
||||||
|
'manage-calendar': getCalendarContext,
|
||||||
|
'manage-file': getFileContext,
|
||||||
|
};
|
||||||
|
|
||||||
|
const getContext = contextMap[routeName];
|
||||||
|
if (getContext) {
|
||||||
|
return getContext(store);
|
||||||
|
}
|
||||||
|
|
||||||
|
return getDefaultContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仪表盘上下文
|
||||||
|
*/
|
||||||
|
function getDashboardContext(store) {
|
||||||
|
const dashboardTask = store.getters.dashboardTask || {};
|
||||||
|
const assistTask = store.getters.assistTask || [];
|
||||||
|
|
||||||
|
const overdueCount = dashboardTask.overdue_count || 0;
|
||||||
|
const todayCount = dashboardTask.today_count || 0;
|
||||||
|
const todoCount = dashboardTask.todo_count || 0;
|
||||||
|
const assistCount = assistTask.length || 0;
|
||||||
|
|
||||||
|
const lines = [
|
||||||
|
'你是一个工作效率助手。用户正在查看仪表盘。',
|
||||||
|
'',
|
||||||
|
'当前任务概况:',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (overdueCount > 0 || todayCount > 0 || todoCount > 0 || assistCount > 0) {
|
||||||
|
if (overdueCount > 0) lines.push(`- 逾期任务:${overdueCount} 个`);
|
||||||
|
if (todayCount > 0) lines.push(`- 今日到期:${todayCount} 个`);
|
||||||
|
if (todoCount > 0) lines.push(`- 待办任务:${todoCount} 个`);
|
||||||
|
if (assistCount > 0) lines.push(`- 协助任务:${assistCount} 个`);
|
||||||
|
} else {
|
||||||
|
lines.push('- 暂无待办任务');
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(
|
||||||
|
'',
|
||||||
|
'你可以帮助用户:',
|
||||||
|
'- 回答任务管理和时间规划相关问题',
|
||||||
|
'- 根据用户描述的任务情况给出优先级建议',
|
||||||
|
'- 协助用户整理和规划工作思路',
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: '工作助手',
|
||||||
|
systemPrompt: lines.join('\n'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目详情上下文
|
||||||
|
*/
|
||||||
|
function getProjectContext(store) {
|
||||||
|
const project = store.getters.projectData || {};
|
||||||
|
const columns = store.state.cacheColumns || [];
|
||||||
|
const tasks = store.state.cacheTasks || [];
|
||||||
|
|
||||||
|
const lines = [
|
||||||
|
'你是一个项目管理助手。用户正在查看项目详情页面。',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (project.id) {
|
||||||
|
// 项目基本信息
|
||||||
|
lines.push('', '当前项目:');
|
||||||
|
lines.push(`- project_id:${project.id}`);
|
||||||
|
if (project.name) {
|
||||||
|
lines.push(`- 名称:${project.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (project.desc) {
|
||||||
|
const desc = project.desc.length > 200 ? project.desc.substring(0, 200) + '...' : project.desc;
|
||||||
|
lines.push(`- 描述:${desc}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 任务统计
|
||||||
|
const projectTasks = tasks.filter(t => t.project_id === project.id);
|
||||||
|
if (projectTasks.length > 0) {
|
||||||
|
const completedCount = projectTasks.filter(t => t.complete_at).length;
|
||||||
|
const overdueCount = projectTasks.filter(t => !t.complete_at && t.end_at && new Date(t.end_at) < new Date()).length;
|
||||||
|
|
||||||
|
lines.push('', '任务统计:');
|
||||||
|
lines.push(`- 总任务:${projectTasks.length} 个`);
|
||||||
|
lines.push(`- 已完成:${completedCount} 个`);
|
||||||
|
if (overdueCount > 0) {
|
||||||
|
lines.push(`- 已逾期:${overdueCount} 个`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 看板列
|
||||||
|
const projectColumns = columns.filter(c => c.project_id === project.id);
|
||||||
|
if (projectColumns.length > 0) {
|
||||||
|
const columnNames = projectColumns.map(c => c.name).join('、');
|
||||||
|
lines.push('', `看板列:${columnNames}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(
|
||||||
|
'',
|
||||||
|
'你可以帮助用户:',
|
||||||
|
'- 协助拆解用户描述的需求为具体任务',
|
||||||
|
'- 回答项目管理方法和最佳实践问题',
|
||||||
|
'- 根据用户提供的信息给出排期建议',
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: '项目助手',
|
||||||
|
systemPrompt: lines.join('\n'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息对话上下文
|
||||||
|
*/
|
||||||
|
function getMessengerContext(store) {
|
||||||
|
const dialogId = store.state.dialogId;
|
||||||
|
const dialogs = store.state.cacheDialogs || [];
|
||||||
|
const dialog = dialogs.find(d => d.id === dialogId);
|
||||||
|
|
||||||
|
const lines = [
|
||||||
|
'你是一个沟通协作助手。用户正在使用消息功能。',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (dialog) {
|
||||||
|
const dialogType = dialog.type === 'group' ? '群聊' : '私聊';
|
||||||
|
lines.push('', '当前对话:');
|
||||||
|
lines.push(`- dialog_id:${dialog.id}`);
|
||||||
|
lines.push(`- 类型:${dialogType}`);
|
||||||
|
if (dialog.name) {
|
||||||
|
lines.push(`- 名称:${dialog.name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(
|
||||||
|
'',
|
||||||
|
'你可以帮助用户:',
|
||||||
|
'- 润色和优化用户输入的消息内容',
|
||||||
|
'- 根据用户描述的场景生成得体的回复',
|
||||||
|
'- 翻译消息内容',
|
||||||
|
'',
|
||||||
|
'请保持回复简洁、专业、得体。',
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: '沟通助手',
|
||||||
|
systemPrompt: lines.join('\n'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日历上下文
|
||||||
|
*/
|
||||||
|
function getCalendarContext() {
|
||||||
|
const lines = [
|
||||||
|
'你是一个时间管理助手。用户正在查看日历。',
|
||||||
|
'',
|
||||||
|
'你可以帮助用户:',
|
||||||
|
'- 回答时间管理和日程规划相关问题',
|
||||||
|
'- 根据用户描述的安排给出优化建议',
|
||||||
|
'- 协助用户合理安排会议和任务时间',
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: '日程助手',
|
||||||
|
systemPrompt: lines.join('\n'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件管理上下文
|
||||||
|
*/
|
||||||
|
function getFileContext() {
|
||||||
|
const lines = [
|
||||||
|
'你是一个文件管理助手。用户正在查看文件管理页面。',
|
||||||
|
'',
|
||||||
|
'你可以帮助用户:',
|
||||||
|
'- 回答文件整理和分类相关问题',
|
||||||
|
'- 根据用户描述建议文件命名和组织方式',
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: '文件助手',
|
||||||
|
systemPrompt: lines.join('\n'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认上下文(通用)
|
||||||
|
*/
|
||||||
|
function getDefaultContext() {
|
||||||
|
const lines = [
|
||||||
|
'你是 DooTask 的 AI 助手。',
|
||||||
|
'',
|
||||||
|
'你可以帮助用户:',
|
||||||
|
'- 回答任务管理和项目协作相关问题',
|
||||||
|
'- 提供工作效率和方法论建议',
|
||||||
|
'- 协助处理各种工作事务',
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: 'AI 助手',
|
||||||
|
systemPrompt: lines.join('\n'),
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user