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>
|
||||
<div
|
||||
v-show="visible"
|
||||
ref="floatBtn"
|
||||
class="ai-float-button no-dark-content"
|
||||
:style="btnStyle"
|
||||
@mousedown.stop.prevent="onMouseDown">
|
||||
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/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"/>
|
||||
</svg>
|
||||
</div>
|
||||
<transition name="fade">
|
||||
<div
|
||||
v-if="visible"
|
||||
ref="floatBtn"
|
||||
class="ai-float-button no-dark-content"
|
||||
:style="btnStyle"
|
||||
@mousedown.stop.prevent="onMouseDown">
|
||||
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/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"/>
|
||||
</svg>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import emitter from "../../store/events";
|
||||
import {withLanguagePreferencePrompt} from "../../utils/ai";
|
||||
import {getPageContext} from "./page-context";
|
||||
|
||||
export default {
|
||||
name: 'AIAssistantFloatButton',
|
||||
@ -86,6 +90,7 @@ export default {
|
||||
window.removeEventListener('resize', this.onResize);
|
||||
document.removeEventListener('mousemove', this.onMouseMove);
|
||||
document.removeEventListener('mouseup', this.onMouseUp);
|
||||
document.removeEventListener('contextmenu', this.onContextMenu);
|
||||
},
|
||||
|
||||
methods: {
|
||||
@ -143,6 +148,9 @@ export default {
|
||||
* 鼠标按下
|
||||
*/
|
||||
onMouseDown(e) {
|
||||
// 只响应鼠标左键
|
||||
if (e.button !== 0) return;
|
||||
|
||||
this.record = {
|
||||
time: Date.now(),
|
||||
startLeft: this.left,
|
||||
@ -154,6 +162,16 @@ export default {
|
||||
|
||||
document.addEventListener('mousemove', this.onMouseMove);
|
||||
document.addEventListener('mouseup', this.onMouseUp);
|
||||
document.addEventListener('contextmenu', this.onContextMenu);
|
||||
},
|
||||
|
||||
/**
|
||||
* 右键菜单弹出时取消拖动
|
||||
*/
|
||||
onContextMenu() {
|
||||
if (this.dragging) {
|
||||
this.onMouseUp();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@ -179,6 +197,7 @@ export default {
|
||||
onMouseUp() {
|
||||
document.removeEventListener('mousemove', this.onMouseMove);
|
||||
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 duration = Date.now() - this.record.time;
|
||||
@ -218,10 +237,33 @@ export default {
|
||||
*/
|
||||
onClick() {
|
||||
emitter.emit('openAIAssistant', {
|
||||
displayMode: 'chat',
|
||||
sessionKey: 'global',
|
||||
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>
|
||||
@ -231,8 +273,8 @@ export default {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||||
background: #8bcf70;
|
||||
box-shadow: 0 4px 12px lch(77 53.3 131.54 / 0.4);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -242,7 +284,7 @@ export default {
|
||||
|
||||
&:hover {
|
||||
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 {
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
<template>
|
||||
<Modal
|
||||
<AssistantModal
|
||||
v-model="showModal"
|
||||
:width="shouldCreateNewSession ? '440px' : '600px'"
|
||||
:mask-closable="false"
|
||||
:footer-hide="true"
|
||||
class-name="ai-assistant-modal">
|
||||
:displayMode="displayMode"
|
||||
:shouldCreateNewSession="shouldCreateNewSession">
|
||||
<div slot="header" class="ai-assistant-header">
|
||||
<div class="ai-assistant-header-title">
|
||||
<i class="taskfont"></i>
|
||||
@ -90,6 +88,17 @@
|
||||
</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">
|
||||
<Input
|
||||
v-model="inputValue"
|
||||
@ -131,7 +140,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</AssistantModal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@ -141,14 +150,16 @@ import {SSEClient} from "../../utils";
|
||||
import {AIBotMap, AIModelNames} from "../../utils/ai";
|
||||
import DialogMarkdown from "../../pages/manage/components/DialogMarkdown.vue";
|
||||
import FloatButton from "./float-button.vue";
|
||||
import AssistantModal from "./modal.vue";
|
||||
|
||||
export default {
|
||||
name: 'AIAssistant',
|
||||
components: {DialogMarkdown},
|
||||
components: {AssistantModal, DialogMarkdown},
|
||||
floatButtonInstance: null,
|
||||
data() {
|
||||
return {
|
||||
// 弹窗状态
|
||||
displayMode: 'modal',
|
||||
showModal: false,
|
||||
closing: false,
|
||||
loadIng: 0,
|
||||
@ -232,6 +243,13 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 获取输入框焦点事件
|
||||
*/
|
||||
onFocus() {
|
||||
this.$refs.inputRef?.focus();
|
||||
},
|
||||
|
||||
/**
|
||||
* 挂载浮动按钮到 body
|
||||
*/
|
||||
@ -264,6 +282,25 @@ export default {
|
||||
if (!$A.isJson(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.inputPlaceholder = params.placeholder || null;
|
||||
this.inputRows = params.rows || null;
|
||||
@ -289,7 +326,7 @@ export default {
|
||||
this.$nextTick(() => {
|
||||
this.scheduleAutoSubmit();
|
||||
this.scrollResponsesToBottom();
|
||||
this.$refs.inputRef.focus();
|
||||
this.onFocus();
|
||||
});
|
||||
},
|
||||
|
||||
@ -1103,6 +1140,7 @@ export default {
|
||||
this.currentSessionId = session.id;
|
||||
this.responses = JSON.parse(JSON.stringify(session.responses));
|
||||
this.syncResponseSeed();
|
||||
this.scrollResponsesToBottom();
|
||||
}
|
||||
},
|
||||
|
||||
@ -1174,262 +1212,251 @@ export default {
|
||||
</script>
|
||||
|
||||
<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;
|
||||
align-items: center;
|
||||
margin: -11px 24px -10px 0;
|
||||
height: 38px;
|
||||
color: #303133;
|
||||
padding-right: 12px;
|
||||
gap: 8px;
|
||||
|
||||
.ai-assistant-header-title {
|
||||
> i {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
> span {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #303133;
|
||||
padding-right: 12px;
|
||||
gap: 8px;
|
||||
|
||||
> 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;
|
||||
}
|
||||
}
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.ai-assistant-content {
|
||||
.ai-assistant-header-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
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);
|
||||
}
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
.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 {
|
||||
.ai-assistant-header-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;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
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;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
&:hover {
|
||||
border-color: transparent;
|
||||
background-color: rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
> i {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.ivu-select-selection {
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
padding: 0 0 0 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.ai-assistant-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
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 {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
.ai-assistant-footer-models {
|
||||
text-align: left;
|
||||
.ivu-select-disabled {
|
||||
.ivu-select-selection {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
.ivu-select-selection {
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
padding: 0 0 0 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.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 {
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
.ivu-select-placeholder,
|
||||
.ivu-select-selected-value {
|
||||
padding-left: 0;
|
||||
opacity: 0.8;
|
||||
}
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
.ai-assistant-footer-btns {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
.ivu-select-selection {
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
.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 {
|
||||
@ -1507,6 +1534,93 @@ export default {
|
||||
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 {
|
||||
.ai-assistant-modal {
|
||||
--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