mirror of
https://github.com/kuaifan/dootask.git
synced 2026-02-20 07:20:37 +00:00
feat: Enhance AI Assistant with session management and improved UI
- Added session management capabilities to the AI Assistant, allowing users to create, load, and delete sessions. - Improved modal UI with a new header for session actions and a footer for model selection. - Updated input handling to support dynamic loading of session data and improved response formatting. - Enhanced search functionality in various components to utilize the AI Assistant for generating content based on user input.
This commit is contained in:
parent
fe7a2a0e73
commit
986c4871df
@ -1,11 +1,52 @@
|
||||
<template>
|
||||
<Modal
|
||||
v-model="showModal"
|
||||
:title="$L('AI 助手')"
|
||||
:mask-closable="false"
|
||||
:closable="false"
|
||||
:width="shouldCreateNewSession ? '420px' : '600px'"
|
||||
:mask-closable="false"
|
||||
:footer-hide="true"
|
||||
class-name="ai-assistant-modal">
|
||||
<div slot="header" class="ai-assistant-header">
|
||||
<div class="ai-assistant-header-title">
|
||||
<i class="taskfont"></i>
|
||||
<span>{{ modalTitle || $L('AI 助手') }}</span>
|
||||
</div>
|
||||
<div class="ai-assistant-header-actions">
|
||||
<div v-if="sessionEnabled && (responses.length > 0 || hasSessionHistory)" class="ai-assistant-header-btn" :title="$L('新建会话')" @click="createNewSession()">
|
||||
<i class="taskfont"></i>
|
||||
</div>
|
||||
<Dropdown
|
||||
v-if="sessionEnabled && hasSessionHistory"
|
||||
trigger="click"
|
||||
placement="bottom-end"
|
||||
:transfer="true">
|
||||
<div class="ai-assistant-header-btn" :title="$L('历史会话')">
|
||||
<i class="taskfont"></i>
|
||||
</div>
|
||||
<DropdownMenu slot="list" class="ai-assistant-history-menu">
|
||||
<DropdownItem
|
||||
v-for="session in currentSessionList"
|
||||
:key="session.id"
|
||||
:class="{'active': session.id === currentSessionId}"
|
||||
@click.native="loadSession(session.id)">
|
||||
<div class="history-item">
|
||||
<div class="history-item-content">
|
||||
<span class="history-item-title">{{ session.title }}</span>
|
||||
<span class="history-item-time">{{ formatSessionTime(session.updatedAt) }}</span>
|
||||
</div>
|
||||
<div class="history-item-delete" @click.stop="deleteSession(session.id)">
|
||||
<i class="taskfont"></i>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownItem>
|
||||
<DropdownItem divided @click.native="clearSessionHistory">
|
||||
<div class="history-clear">
|
||||
{{ $L('清空历史记录') }}
|
||||
</div>
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ai-assistant-content">
|
||||
<div
|
||||
v-if="responses.length"
|
||||
@ -21,6 +62,7 @@
|
||||
</template>
|
||||
<template v-else-if="response.rawOutput">
|
||||
<Button
|
||||
v-if="showApplyButton"
|
||||
type="primary"
|
||||
size="small"
|
||||
:loading="response.applyLoading"
|
||||
@ -31,7 +73,7 @@
|
||||
</template>
|
||||
<template v-else>
|
||||
<Icon type="ios-sync" class="ai-assistant-output-icon icon-loading"/>
|
||||
<span class="ai-assistant-output-status">{{ $L('生成中...') }}</span>
|
||||
<span class="ai-assistant-output-status">{{ loadingText || $L('生成中...') }}</span>
|
||||
</template>
|
||||
</div>
|
||||
<div class="ai-assistant-output-meta">
|
||||
@ -41,7 +83,8 @@
|
||||
<DialogMarkdown
|
||||
v-if="response.rawOutput"
|
||||
class="ai-assistant-output-markdown no-dark-content"
|
||||
:text="response.displayOutput || response.rawOutput"/>
|
||||
:text="response.displayOutput || response.rawOutput"
|
||||
@click="onContentClick"/>
|
||||
<div v-else class="ai-assistant-output-placeholder">
|
||||
{{ response.status === 'error' ? (response.error || $L('发送失败')) : $L('等待 AI 回复...') }}
|
||||
</div>
|
||||
@ -52,36 +95,36 @@
|
||||
v-model="inputValue"
|
||||
type="textarea"
|
||||
:placeholder="inputPlaceholder || $L('请输入你的问题...')"
|
||||
:rows="inputRows || 2"
|
||||
:autosize="inputAutosize || {minRows:2, maxRows:6}"
|
||||
:rows="inputRows || 1"
|
||||
:autosize="inputAutosize || {minRows:1, maxRows:6}"
|
||||
:maxlength="inputMaxlength || 500" />
|
||||
</div>
|
||||
</div>
|
||||
<div slot="footer" class="ai-assistant-footer">
|
||||
<div class="ai-assistant-footer-models">
|
||||
<Select
|
||||
v-model="inputModel"
|
||||
:placeholder="$L('选择模型')"
|
||||
:loading="modelsLoading"
|
||||
:disabled="modelsLoading || modelGroups.length === 0"
|
||||
:not-found-text="$L('暂无可用模型')"
|
||||
transfer>
|
||||
<OptionGroup
|
||||
v-for="group in modelGroups"
|
||||
:key="group.type"
|
||||
:label="group.label">
|
||||
<Option
|
||||
v-for="option in group.options"
|
||||
:key="option.id"
|
||||
:value="option.id">
|
||||
{{ option.label }}
|
||||
</Option>
|
||||
</OptionGroup>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="ai-assistant-footer-btns">
|
||||
<Button type="text" @click="showModal=false">{{ $L('取消') }}</Button>
|
||||
<Button type="primary" :loading="loadIng > 0" @click="onSubmit">{{ submitButtonText || $L('确定') }}</Button>
|
||||
<div class="ai-assistant-footer">
|
||||
<div class="ai-assistant-footer-models">
|
||||
<Select
|
||||
v-model="inputModel"
|
||||
:placeholder="$L('选择模型')"
|
||||
:loading="modelsLoading"
|
||||
:disabled="modelsLoading || modelGroups.length === 0"
|
||||
:not-found-text="$L('暂无可用模型')"
|
||||
transfer>
|
||||
<OptionGroup
|
||||
v-for="group in modelGroups"
|
||||
:key="group.type"
|
||||
:label="group.label">
|
||||
<Option
|
||||
v-for="option in group.options"
|
||||
:key="option.id"
|
||||
:value="option.id">
|
||||
{{ option.label }}
|
||||
</Option>
|
||||
</OptionGroup>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="ai-assistant-footer-btns">
|
||||
<Button v-if="submitButtonText" type="primary" shape="circle" icon="md-arrow-up" :loading="loadIng > 0" @click="onSubmit">{{ submitButtonText }}</Button>
|
||||
<Button v-else type="primary" shape="circle" icon="md-arrow-up" :loading="loadIng > 0" @click="onSubmit"></Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
@ -104,8 +147,11 @@ export default {
|
||||
loadIng: 0,
|
||||
pendingAutoSubmit: false,
|
||||
autoSubmitTimer: null,
|
||||
modalTitle: null,
|
||||
applyButtonText: null,
|
||||
submitButtonText: null,
|
||||
showApplyButton: true,
|
||||
loadingText: null,
|
||||
|
||||
// 输入配置
|
||||
inputValue: '',
|
||||
@ -134,11 +180,20 @@ export default {
|
||||
maxResponses: 50,
|
||||
contextWindowSize: 10,
|
||||
activeSSEClients: [],
|
||||
|
||||
// 会话管理
|
||||
sessionEnabled: false,
|
||||
sessionStore: {},
|
||||
currentSessionKey: 'default',
|
||||
currentSessionId: null,
|
||||
sessionCacheKey: 'aiAssistant.sessions',
|
||||
maxSessionsPerKey: 20,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
emitter.on('openAIAssistant', this.onOpenAIAssistant);
|
||||
this.loadCachedModel();
|
||||
this.loadSessionStore();
|
||||
},
|
||||
beforeDestroy() {
|
||||
emitter.off('openAIAssistant', this.onOpenAIAssistant);
|
||||
@ -152,6 +207,12 @@ export default {
|
||||
shouldCreateNewSession() {
|
||||
return this.responses.length === 0;
|
||||
},
|
||||
currentSessionList() {
|
||||
return this.sessionStore[this.currentSessionKey] || [];
|
||||
},
|
||||
hasSessionHistory() {
|
||||
return this.currentSessionList.length > 0;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
inputModel(value) {
|
||||
@ -173,18 +234,24 @@ export default {
|
||||
this.inputMaxlength = params.maxlength || null;
|
||||
this.applyHook = params.onApply || null;
|
||||
this.beforeSendHook = params.onBeforeSend || null;
|
||||
this.modalTitle = params.title || null;
|
||||
this.applyButtonText = params.applyButtonText || null;
|
||||
this.submitButtonText = params.submitButtonText || null;
|
||||
this.showApplyButton = params.showApplyButton !== false;
|
||||
this.loadingText = params.loadingText || null;
|
||||
this.renderHook = params.onRender || null;
|
||||
this.pendingAutoSubmit = !!params.autoSubmit;
|
||||
//
|
||||
this.responses = [];
|
||||
|
||||
// 会话管理
|
||||
this.initSession(params.sessionKey, params.resumeSession);
|
||||
|
||||
this.showModal = true;
|
||||
this.fetchModelOptions();
|
||||
this.clearActiveSSEClients();
|
||||
this.clearAutoSubmitTimer();
|
||||
this.$nextTick(() => {
|
||||
this.scheduleAutoSubmit();
|
||||
this.scrollResponsesToBottom();
|
||||
});
|
||||
},
|
||||
|
||||
@ -512,6 +579,8 @@ export default {
|
||||
responseEntry.status = 'completed';
|
||||
}
|
||||
this.releaseSSEClient(sse);
|
||||
// 响应完成后保存会话
|
||||
this.saveCurrentSession();
|
||||
break;
|
||||
}
|
||||
});
|
||||
@ -817,6 +886,275 @@ export default {
|
||||
}
|
||||
return distance <= threshold;
|
||||
},
|
||||
|
||||
/**
|
||||
* 处理内容区域的点击事件
|
||||
*/
|
||||
onContentClick(e) {
|
||||
const target = e.target;
|
||||
if (target.tagName !== 'A') {
|
||||
return;
|
||||
}
|
||||
|
||||
const href = target.getAttribute('href');
|
||||
if (!href || !href.startsWith('dootask://')) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// 解析 dootask:// 协议链接
|
||||
// 格式: dootask://type/id 例如 dootask://task/123
|
||||
const match = href.match(/^dootask:\/\/(\w+)\/(\d+)$/);
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [, type, id] = match;
|
||||
const numId = parseInt(id, 10);
|
||||
|
||||
switch (type) {
|
||||
case 'task':
|
||||
this.$store.dispatch('openTask', {id: numId});
|
||||
break;
|
||||
|
||||
case 'project':
|
||||
this.goForward({name: 'manage-project', params: {projectId: numId}});
|
||||
break;
|
||||
|
||||
case 'file':
|
||||
this.goForward({name: 'manage-file', params: {folderId: 0, fileId: null, shakeId: numId}});
|
||||
this.$store.state.fileShakeId = numId;
|
||||
setTimeout(() => {
|
||||
this.$store.state.fileShakeId = 0;
|
||||
}, 600);
|
||||
break;
|
||||
|
||||
case 'contact':
|
||||
this.$store.dispatch('openDialogUserid', numId).catch(({msg}) => {
|
||||
$A.modalError(msg || this.$L('打开会话失败'));
|
||||
});
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
// ==================== 会话管理方法 ====================
|
||||
|
||||
/**
|
||||
* 加载持久化的会话数据
|
||||
*/
|
||||
async loadSessionStore() {
|
||||
try {
|
||||
const stored = await $A.IDBString(this.sessionCacheKey);
|
||||
if (stored) {
|
||||
this.sessionStore = JSON.parse(stored);
|
||||
}
|
||||
} catch (e) {
|
||||
this.sessionStore = {};
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 持久化会话数据
|
||||
*/
|
||||
saveSessionStore() {
|
||||
try {
|
||||
$A.IDBSave(this.sessionCacheKey, JSON.stringify(this.sessionStore));
|
||||
} catch (e) {
|
||||
console.warn('[AIAssistant] Failed to save session store:', e);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 生成会话 ID
|
||||
*/
|
||||
generateSessionId() {
|
||||
return `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
},
|
||||
|
||||
/**
|
||||
* 根据首次输入生成会话标题
|
||||
*/
|
||||
generateSessionTitle(responses) {
|
||||
if (!responses || responses.length === 0) {
|
||||
return this.$L('新会话');
|
||||
}
|
||||
const firstPrompt = responses.find(r => r.prompt)?.prompt || '';
|
||||
if (!firstPrompt) {
|
||||
return this.$L('新会话');
|
||||
}
|
||||
// 截取前20个字符作为标题
|
||||
const title = firstPrompt.trim().substring(0, 20);
|
||||
return title.length < firstPrompt.trim().length ? `${title}...` : title;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取指定场景的会话列表
|
||||
*/
|
||||
getSessionList(sessionKey) {
|
||||
return this.sessionStore[sessionKey] || [];
|
||||
},
|
||||
|
||||
/**
|
||||
* 初始化会话
|
||||
* @param {string} sessionKey - 会话场景标识,不传则不启用会话管理
|
||||
* @param {boolean|number} resumeSession - 恢复上次会话:true 总是恢复,数字表示秒数阈值(上次会话在该时间内则恢复)
|
||||
*/
|
||||
initSession(sessionKey, resumeSession = false) {
|
||||
// 保存当前会话
|
||||
if (this.responses.length > 0) {
|
||||
this.saveCurrentSession();
|
||||
}
|
||||
|
||||
this.sessionEnabled = !!sessionKey;
|
||||
|
||||
if (this.sessionEnabled) {
|
||||
this.currentSessionKey = sessionKey;
|
||||
|
||||
if (resumeSession) {
|
||||
const sessions = this.getSessionList(sessionKey);
|
||||
if (sessions.length > 0) {
|
||||
const lastSession = sessions[0];
|
||||
// 如果是数字,检查时间阈值
|
||||
if (typeof resumeSession === 'number') {
|
||||
const elapsed = (Date.now() - lastSession.updatedAt) / 1000;
|
||||
if (elapsed > resumeSession) {
|
||||
// 超过阈值,创建新会话
|
||||
this.currentSessionId = this.generateSessionId();
|
||||
this.responses = [];
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.currentSessionId = lastSession.id;
|
||||
this.responses = JSON.parse(JSON.stringify(lastSession.responses));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.currentSessionId = this.generateSessionId();
|
||||
this.responses = [];
|
||||
} else {
|
||||
this.currentSessionKey = 'default';
|
||||
this.currentSessionId = null;
|
||||
this.responses = [];
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 创建新会话
|
||||
*/
|
||||
createNewSession(autoSaveCurrent = true) {
|
||||
// 保存当前会话
|
||||
if (autoSaveCurrent && this.responses.length > 0) {
|
||||
this.saveCurrentSession();
|
||||
}
|
||||
// 创建新会话
|
||||
this.currentSessionId = this.generateSessionId();
|
||||
this.responses = [];
|
||||
},
|
||||
|
||||
/**
|
||||
* 保存当前会话到存储
|
||||
*/
|
||||
saveCurrentSession() {
|
||||
if (!this.sessionEnabled || !this.currentSessionId || this.responses.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sessionKey = this.currentSessionKey;
|
||||
if (!this.sessionStore[sessionKey]) {
|
||||
this.$set(this.sessionStore, sessionKey, []);
|
||||
}
|
||||
|
||||
const sessions = this.sessionStore[sessionKey];
|
||||
const existingIndex = sessions.findIndex(s => s.id === this.currentSessionId);
|
||||
const sessionData = {
|
||||
id: this.currentSessionId,
|
||||
title: this.generateSessionTitle(this.responses),
|
||||
responses: JSON.parse(JSON.stringify(this.responses)),
|
||||
createdAt: existingIndex > -1 ? sessions[existingIndex].createdAt : Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
|
||||
if (existingIndex > -1) {
|
||||
sessions[existingIndex] = sessionData;
|
||||
} else {
|
||||
sessions.unshift(sessionData);
|
||||
}
|
||||
|
||||
// 限制每个场景的会话数量
|
||||
if (sessions.length > this.maxSessionsPerKey) {
|
||||
sessions.splice(this.maxSessionsPerKey);
|
||||
}
|
||||
|
||||
this.saveSessionStore();
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载指定会话
|
||||
*/
|
||||
loadSession(sessionId) {
|
||||
const sessions = this.getSessionList(this.currentSessionKey);
|
||||
const session = sessions.find(s => s.id === sessionId);
|
||||
if (session) {
|
||||
// 先保存当前会话
|
||||
if (this.currentSessionId !== sessionId && this.responses.length > 0) {
|
||||
this.saveCurrentSession();
|
||||
}
|
||||
this.currentSessionId = session.id;
|
||||
this.responses = JSON.parse(JSON.stringify(session.responses));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除指定会话
|
||||
*/
|
||||
deleteSession(sessionId) {
|
||||
const sessions = this.getSessionList(this.currentSessionKey);
|
||||
const index = sessions.findIndex(s => s.id === sessionId);
|
||||
if (index > -1) {
|
||||
sessions.splice(index, 1);
|
||||
this.saveSessionStore();
|
||||
// 如果删除的是当前会话,创建新会话
|
||||
if (this.currentSessionId === sessionId) {
|
||||
this.createNewSession(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 清空当前场景的所有历史会话
|
||||
*/
|
||||
clearSessionHistory() {
|
||||
$A.modalConfirm({
|
||||
title: this.$L('清空历史会话'),
|
||||
content: this.$L('确定要清空当前场景的所有历史会话吗?'),
|
||||
onOk: () => {
|
||||
this.$set(this.sessionStore, this.currentSessionKey, []);
|
||||
this.saveSessionStore();
|
||||
this.createNewSession(false);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 格式化会话时间显示
|
||||
*/
|
||||
formatSessionTime(timestamp) {
|
||||
const now = $A.daytz();
|
||||
const time = $A.dayjs(timestamp);
|
||||
if (now.format('YYYY-MM-DD') === time.format('YYYY-MM-DD')) {
|
||||
return this.$L('今天') + ' ' + time.format('HH:mm');
|
||||
}
|
||||
if (now.subtract(1, 'day').format('YYYY-MM-DD') === time.format('YYYY-MM-DD')) {
|
||||
return this.$L('昨天') + ' ' + time.format('HH:mm');
|
||||
}
|
||||
if (now.year() === time.year()) {
|
||||
return time.format('MM-DD HH:mm');
|
||||
}
|
||||
return time.format('YYYY-MM-DD HH:mm');
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -825,18 +1163,64 @@ export default {
|
||||
.ai-assistant-modal {
|
||||
--apply-reasoning-before-bg: #e1e1e1;
|
||||
.ivu-modal {
|
||||
transition: max-width 0.3s ease;
|
||||
transition: width 0.3s, max-width 0.3s;
|
||||
.ivu-modal-header {
|
||||
padding-left: 30px !important;
|
||||
padding-right: 30px !important;
|
||||
border-bottom: none !important;
|
||||
}
|
||||
.ivu-modal-body {
|
||||
padding-top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
.ivu-modal-footer {
|
||||
.ivu-btn {
|
||||
min-width: auto !important;
|
||||
}
|
||||
|
||||
.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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -844,19 +1228,19 @@ export default {
|
||||
.ai-assistant-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
max-height: calc(var(--window-height) - var(--status-bar-height) - var(--navigation-bar-height) - 344px);
|
||||
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) - 214px);
|
||||
max-height: calc(var(--window-height) - var(--status-bar-height) - var(--navigation-bar-height) - 136px);
|
||||
}
|
||||
|
||||
.ai-assistant-output {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
padding: 12px 24px;
|
||||
margin-bottom: 12px;
|
||||
border-radius: 0;
|
||||
background: #f8f9fb;
|
||||
border: 1px solid rgba(0, 0, 0, 0.04);
|
||||
border: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@ -874,7 +1258,7 @@ export default {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
height: 24px;
|
||||
height: 26px;
|
||||
color: #999;
|
||||
gap: 4px;
|
||||
}
|
||||
@ -885,10 +1269,13 @@ export default {
|
||||
}
|
||||
|
||||
.ai-assistant-apply-btn {
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
border-radius: 4px;
|
||||
height: 26px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.ai-assistant-output-status {
|
||||
@ -974,6 +1361,33 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
@ -991,7 +1405,8 @@ export default {
|
||||
box-shadow: none;
|
||||
.ivu-select-placeholder,
|
||||
.ivu-select-selected-value {
|
||||
padding-left: 4px;
|
||||
padding-left: 0;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -999,10 +1414,85 @@ export default {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ai-assistant-history-menu {
|
||||
min-width: 240px;
|
||||
max-width: 300px;
|
||||
max-height: 320px;
|
||||
overflow-y: auto;
|
||||
|
||||
.ivu-dropdown-item {
|
||||
&.active {
|
||||
background-color: rgba(45, 140, 240, 0.1);
|
||||
}
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
}
|
||||
|
||||
.history-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.history-item-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
|
||||
.history-item-title {
|
||||
font-size: 13px;
|
||||
color: #303133;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.history-item-time {
|
||||
font-size: 11px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
.history-item-delete {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 4px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s, background-color 0.2s;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
> i {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .history-item-delete {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.history-clear {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
color: #F56C6C;
|
||||
}
|
||||
}
|
||||
body.dark-mode-reverse {
|
||||
.ai-assistant-modal {
|
||||
--apply-reasoning-before-bg: #4e4e56;
|
||||
|
||||
@ -647,21 +647,53 @@ export default {
|
||||
},
|
||||
|
||||
toggleAiSearch() {
|
||||
this.aiSearch = !this.aiSearch;
|
||||
// 如果有搜索内容,重新搜索
|
||||
if (this.searchKey.trim()) {
|
||||
// 清除所有支持 AI 搜索的类型的结果
|
||||
const aiSearchTypes = ['file', 'task', 'project', 'contact'];
|
||||
this.searchResults = this.searchResults.filter(item => !aiSearchTypes.includes(item.type));
|
||||
// 重新搜索
|
||||
if (this.action) {
|
||||
if (aiSearchTypes.includes(this.action)) {
|
||||
this.distSearch(this.action);
|
||||
}
|
||||
} else {
|
||||
aiSearchTypes.forEach(type => this.distSearch(type));
|
||||
}
|
||||
const keyword = this.searchKey.trim();
|
||||
emitter.emit('openAIAssistant', {
|
||||
sessionKey: 'ai-search',
|
||||
resumeSession: 300,
|
||||
title: this.$L('AI 搜索'),
|
||||
value: keyword,
|
||||
placeholder: this.$L('请描述你想搜索的内容...'),
|
||||
loadingText: this.$L('搜索中...'),
|
||||
showApplyButton: false,
|
||||
autoSubmit: !!keyword,
|
||||
onBeforeSend: this.handleAISearchBeforeSend,
|
||||
});
|
||||
this.onHide();
|
||||
},
|
||||
|
||||
handleAISearchBeforeSend(context = []) {
|
||||
const systemPrompt = [
|
||||
'你是一个智能搜索助手,负责帮助用户在 DooTask 系统中搜索和整理信息。',
|
||||
'你可以使用 intelligent_search 工具来搜索任务、项目、文件和联系人。',
|
||||
'',
|
||||
'请根据用户的搜索需求:',
|
||||
'1. 调用搜索工具获取相关结果',
|
||||
'2. 对搜索结果进行分类整理',
|
||||
'3. 以清晰的格式呈现给用户',
|
||||
'4. 如有需要,可以进行多次搜索以获取更全面的结果',
|
||||
'',
|
||||
'## 链接格式要求',
|
||||
'在返回结果时,请使用以下格式创建可点击的链接:',
|
||||
'- 任务: [任务名称](dootask://task/任务ID)',
|
||||
'- 项目: [项目名称](dootask://project/项目ID)',
|
||||
'- 文件: [文件名称](dootask://file/文件ID)',
|
||||
'- 联系人: [联系人名称](dootask://contact/用户ID)',
|
||||
'',
|
||||
'示例:',
|
||||
'- [完成项目报告](dootask://task/123)',
|
||||
'- [产品开发项目](dootask://project/456)',
|
||||
].join('\n');
|
||||
|
||||
const prepared = [
|
||||
['system', systemPrompt]
|
||||
];
|
||||
|
||||
if (context.length > 0) {
|
||||
prepared.push(...context);
|
||||
}
|
||||
|
||||
return prepared;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -1063,6 +1063,7 @@ export default {
|
||||
|
||||
onProjectAI() {
|
||||
emitter.emit('openAIAssistant', {
|
||||
sessionKey: 'project-create',
|
||||
placeholder: this.$L('请简要描述项目目标、范围或关键里程碑,AI 将生成名称和任务列表'),
|
||||
onBeforeSend: this.handleProjectAIBeforeSend,
|
||||
onRender: this.handleProjectAIRender,
|
||||
|
||||
@ -152,7 +152,7 @@
|
||||
<em>{{$L('上传文件')}}</em>
|
||||
</div>
|
||||
</template>
|
||||
<div class="chat-input-popover-item" @click="onToolbar('ai')">
|
||||
<div v-if="dialogId > 0" class="chat-input-popover-item" @click="onToolbar('ai')">
|
||||
<i class="taskfont"></i>
|
||||
<em>{{$L('AI 生成')}}</em>
|
||||
</div>
|
||||
@ -1941,6 +1941,7 @@ export default {
|
||||
return;
|
||||
}
|
||||
emitter.emit('openAIAssistant', {
|
||||
sessionKey: 'chat-message',
|
||||
placeholder: this.$L('请简要描述消息的主题、语气或要点,AI 将生成完整消息'),
|
||||
onBeforeSend: this.handleMessageAIBeforeSend,
|
||||
onApply: this.handleMessageAIApply,
|
||||
|
||||
@ -174,6 +174,7 @@ export default {
|
||||
return;
|
||||
}
|
||||
emitter.emit('openAIAssistant', {
|
||||
sessionKey: 'report-analysis',
|
||||
placeholder: this.$L('补充你想聚焦的风险、成果或建议,留空直接生成分析'),
|
||||
onBeforeSend: this.handleReportAnalysisBeforeSend,
|
||||
onApply: this.handleReportAnalysisApply,
|
||||
|
||||
@ -265,6 +265,7 @@ export default {
|
||||
return;
|
||||
}
|
||||
emitter.emit('openAIAssistant', {
|
||||
sessionKey: 'report-edit',
|
||||
placeholder: this.$L('补充你想强调的重点或特殊说明,AI 将在此基础上整理汇报'),
|
||||
onBeforeSend: this.handleReportAIBeforeSend,
|
||||
onApply: this.handleReportAIApply,
|
||||
|
||||
@ -631,6 +631,7 @@ export default {
|
||||
|
||||
onAI() {
|
||||
emitter.emit('openAIAssistant', {
|
||||
sessionKey: 'task-create',
|
||||
placeholder: this.$L('请简要描述任务目标、背景或预期交付,AI 将生成标题、详细说明和子任务'),
|
||||
onBeforeSend: this.handleTaskAIBeforeSend,
|
||||
onRender: this.handleTaskAIRender,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user