mirror of
https://github.com/kuaifan/dootask.git
synced 2026-09-25 21:36:25 +00:00
feat(ai-assistant): AI 回复增加复制/时间/反馈取消,历史删除二次确认
- 反馈行新增复制按钮(去推理段落)与回复时间(规则参考 formatMessageTime) - 点赞/点踩支持再点取消;修复 feedbackLoading 持久化导致重载后无法再点 - 历史删除改为二次确认(垃圾桶→红勾),离开/3秒/下拉关闭自动复位,触摸端常显 - 回复完成后贴底用户自动滚动到底部,露出反馈按钮 - 后端 feedback__save 支持空 feedback 取消(删除记录),同步 ai-kb Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
97bd58312e
commit
b0a4fe6646
@ -244,7 +244,7 @@ class AssistantController extends AbstractController
|
|||||||
/**
|
/**
|
||||||
* @api {post} api/assistant/feedback/save 保存回复反馈
|
* @api {post} api/assistant/feedback/save 保存回复反馈
|
||||||
*
|
*
|
||||||
* @apiDescription 需要token身份。保存用户对一条 AI 回复的 👍/👎 反馈,同一条回复可改票(覆盖更新)
|
* @apiDescription 需要token身份。保存用户对一条 AI 回复的 👍/👎 反馈,同一条回复可改票(覆盖更新);传空 feedback 表示取消反馈(删除记录)
|
||||||
* @apiVersion 1.0.0
|
* @apiVersion 1.0.0
|
||||||
* @apiGroup assistant
|
* @apiGroup assistant
|
||||||
* @apiName feedback__save
|
* @apiName feedback__save
|
||||||
@ -252,7 +252,7 @@ class AssistantController extends AbstractController
|
|||||||
* @apiParam {String} session_key 场景分类key
|
* @apiParam {String} session_key 场景分类key
|
||||||
* @apiParam {String} session_id 前端会话ID
|
* @apiParam {String} session_id 前端会话ID
|
||||||
* @apiParam {Number} local_id 回复条目localId
|
* @apiParam {Number} local_id 回复条目localId
|
||||||
* @apiParam {String} feedback like|dislike
|
* @apiParam {String} feedback like|dislike,空字符串表示取消反馈
|
||||||
* @apiParam {String} [prompt] 用户问题
|
* @apiParam {String} [prompt] 用户问题
|
||||||
* @apiParam {String} [answer] 回复摘录
|
* @apiParam {String} [answer] 回复摘录
|
||||||
* @apiParam {Array} [source_ids] 回复引用的kb source id列表
|
* @apiParam {Array} [source_ids] 回复引用的kb source id列表
|
||||||
@ -279,7 +279,7 @@ class AssistantController extends AbstractController
|
|||||||
if (empty($sessionId) || $localId <= 0) {
|
if (empty($sessionId) || $localId <= 0) {
|
||||||
return Base::retError('参数错误');
|
return Base::retError('参数错误');
|
||||||
}
|
}
|
||||||
if (!in_array($feedback, ['like', 'dislike'])) {
|
if (!in_array($feedback, ['', 'like', 'dislike'])) {
|
||||||
return Base::retError('反馈类型错误');
|
return Base::retError('反馈类型错误');
|
||||||
}
|
}
|
||||||
if (!is_array($sourceIds)) {
|
if (!is_array($sourceIds)) {
|
||||||
@ -292,6 +292,14 @@ class AssistantController extends AbstractController
|
|||||||
->where('local_id', $localId)
|
->where('local_id', $localId)
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
|
// 空反馈表示取消:删除已有记录
|
||||||
|
if ($feedback === '') {
|
||||||
|
$exist?->delete();
|
||||||
|
return Base::retSuccess('success', [
|
||||||
|
'feedback' => '',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
$row = AiAssistantFeedback::createInstance([
|
$row = AiAssistantFeedback::createInstance([
|
||||||
'userid' => $user->userid,
|
'userid' => $user->userid,
|
||||||
'session_key' => $sessionKey,
|
'session_key' => $sessionKey,
|
||||||
|
|||||||
@ -11,6 +11,8 @@ aliases:
|
|||||||
- AI 回答不好怎么反馈
|
- AI 回答不好怎么反馈
|
||||||
- 反馈 AI 回答
|
- 反馈 AI 回答
|
||||||
- 有帮助 没帮助
|
- 有帮助 没帮助
|
||||||
|
- 复制 AI 回答
|
||||||
|
- 取消反馈
|
||||||
related_tools: []
|
related_tools: []
|
||||||
related_pages: []
|
related_pages: []
|
||||||
prerequisites:
|
prerequisites:
|
||||||
@ -18,18 +20,20 @@ prerequisites:
|
|||||||
negative:
|
negative:
|
||||||
- 反馈只针对 AI 助手浮窗里的回复,聊天对话里 @AI 机器人的消息暂不支持
|
- 反馈只针对 AI 助手浮窗里的回复,聊天对话里 @AI 机器人的消息暂不支持
|
||||||
- 点踩不会让 AI 立即重新回答,需要自己追问或重新提问
|
- 点踩不会让 AI 立即重新回答,需要自己追问或重新提问
|
||||||
last_verified: v1.7.90
|
last_verified: v1.7.91
|
||||||
---
|
---
|
||||||
|
|
||||||
# 给 AI 回答点赞或点踩
|
# 给 AI 回答点赞或点踩
|
||||||
|
|
||||||
## 这是什么
|
## 这是什么
|
||||||
AI 助手浮窗中,每条 AI 回复完成后下方会出现「有帮助 / 没帮助」两个图标按钮(👍/👎)。点击即提交反馈,用于帮助官方改进 AI 回答质量和帮助文档内容。
|
AI 助手浮窗中,每条 AI 回复完成后下方右侧会出现「复制 / 有帮助 / 没帮助」三个图标按钮(📋/👍/👎)。复制用于把回复内容复制到剪贴板,👍/👎 用于提交反馈,帮助官方改进 AI 回答质量和帮助文档内容。
|
||||||
|
|
||||||
## 怎么操作
|
## 怎么操作
|
||||||
1. 在 AI 助手浮窗中提问,等待回复完成(流式输出结束后按钮才出现)
|
1. 在 AI 助手浮窗中提问,等待回复完成(流式输出结束后按钮才出现)
|
||||||
2. 回复下方右侧点击 👍(有帮助)或 👎(没帮助)
|
2. 回复下方右侧点击 📋(复制)可复制该条回复正文到剪贴板(不含推理过程)
|
||||||
3. 按钮高亮表示已提交;再点另一个按钮可以改票,同一条回复只记最新一次
|
3. 点击 👍(有帮助)或 👎(没帮助)提交反馈
|
||||||
|
4. 按钮高亮表示已提交;再点另一个按钮可以改票,同一条回复只记最新一次
|
||||||
|
5. 再次点击当前已高亮的按钮可取消反馈
|
||||||
|
|
||||||
## 反馈会被怎么用
|
## 反馈会被怎么用
|
||||||
- 反馈与该回复引用的帮助文档关联,被频繁点踩的文档会被优先修订
|
- 反馈与该回复引用的帮助文档关联,被频繁点踩的文档会被优先修订
|
||||||
@ -37,7 +41,6 @@ AI 助手浮窗中,每条 AI 回复完成后下方会出现「有帮助 / 没
|
|||||||
|
|
||||||
## 不支持
|
## 不支持
|
||||||
- 不支持填写文字原因,只有 👍/👎 两档
|
- 不支持填写文字原因,只有 👍/👎 两档
|
||||||
- 不支持取消反馈(可改票,不可清除)
|
|
||||||
|
|
||||||
## 相关
|
## 相关
|
||||||
- AI 查帮助文档:[[ai-assistant.search-help-docs.howto]]
|
- AI 查帮助文档:[[ai-assistant.search-help-docs.howto]]
|
||||||
|
|||||||
@ -18,7 +18,8 @@
|
|||||||
trigger="click"
|
trigger="click"
|
||||||
placement="bottom-end"
|
placement="bottom-end"
|
||||||
:transfer="true"
|
:transfer="true"
|
||||||
:z-index="topZIndex + 1">
|
:z-index="topZIndex + 1"
|
||||||
|
@on-visible-change="onHistoryVisibleChange">
|
||||||
<div class="ai-assistant-header-btn" :title="$L('历史会话')">
|
<div class="ai-assistant-header-btn" :title="$L('历史会话')">
|
||||||
<i class="taskfont"></i>
|
<i class="taskfont"></i>
|
||||||
</div>
|
</div>
|
||||||
@ -28,10 +29,21 @@
|
|||||||
:key="session.id"
|
:key="session.id"
|
||||||
:class="{'active': session.id === currentSessionId}"
|
:class="{'active': session.id === currentSessionId}"
|
||||||
@click.native="loadSession(session.id)">
|
@click.native="loadSession(session.id)">
|
||||||
<div class="history-item">
|
<div class="history-item" @mouseleave="resetDeleteConfirm">
|
||||||
<div class="history-item-content">
|
<div class="history-item-content">
|
||||||
<div class="history-item-title">{{ session.title }}</div>
|
<div class="history-item-title">{{ session.title }}</div>
|
||||||
<div class="history-item-delete" @click.stop="deleteSession(session.id)">
|
<div
|
||||||
|
v-if="confirmingDeleteId === session.id"
|
||||||
|
class="history-item-delete history-item-delete-confirm"
|
||||||
|
:title="$L('确认删除')"
|
||||||
|
@click.stop="deleteSession(session.id)">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="history-item-delete"
|
||||||
|
:title="$L('删除')"
|
||||||
|
@click.stop="askDeleteSession(session.id)">
|
||||||
<i class="taskfont"></i>
|
<i class="taskfont"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -142,18 +154,25 @@
|
|||||||
<div
|
<div
|
||||||
v-if="response.rawOutput && response.status === 'completed'"
|
v-if="response.rawOutput && response.status === 'completed'"
|
||||||
class="ai-assistant-output-feedback">
|
class="ai-assistant-output-feedback">
|
||||||
|
<span
|
||||||
|
class="ai-assistant-feedback-btn"
|
||||||
|
:title="$L('复制')"
|
||||||
|
@click="copyResponse(response)">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.0"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>
|
||||||
|
</span>
|
||||||
<span
|
<span
|
||||||
:class="['ai-assistant-feedback-btn', {active: response.feedback === 'like'}]"
|
:class="['ai-assistant-feedback-btn', {active: response.feedback === 'like'}]"
|
||||||
:title="$L('有帮助')"
|
:title="$L('有帮助')"
|
||||||
@click="submitFeedback(response, 'like')">
|
@click="submitFeedback(response, 'like')">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><path stroke-linecap="round" stroke-linejoin="round" d="M6.633 10.25c.806 0 1.533-.446 2.031-1.08a9.041 9.041 0 0 1 2.861-2.4c.723-.384 1.35-.956 1.653-1.715a4.498 4.498 0 0 0 .322-1.672V3a.75.75 0 0 1 .75-.75 2.25 2.25 0 0 1 2.25 2.25c0 1.152-.26 2.243-.723 3.218-.266.558.107 1.282.725 1.282h3.126c1.026 0 1.945.694 2.054 1.715.045.422.068.85.068 1.285a11.95 11.95 0 0 1-2.649 7.521c-.388.482-.987.729-1.605.729H13.48c-.483 0-.964-.078-1.423-.23l-3.114-1.04a4.501 4.501 0 0 0-1.423-.23H5.904m1.729-7.5a8.97 8.97 0 0 0-.621 4.72c.063.504.123 1.012.182 1.52.04.35.05.703.05 1.06v.27c0 .415-.336.75-.75.75h-2.25a.75.75 0 0 1-.75-.75v-7.5a.75.75 0 0 1 .75-.75h2.25c.414 0 .75.335.75.75v.198Z"/></svg>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.0"><path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z"/><path d="M7 10v12"/></svg>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
:class="['ai-assistant-feedback-btn ai-assistant-feedback-btn-down', {active: response.feedback === 'dislike'}]"
|
:class="['ai-assistant-feedback-btn ai-assistant-feedback-btn-down', {active: response.feedback === 'dislike'}]"
|
||||||
:title="$L('没帮助')"
|
:title="$L('没帮助')"
|
||||||
@click="submitFeedback(response, 'dislike')">
|
@click="submitFeedback(response, 'dislike')">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><path stroke-linecap="round" stroke-linejoin="round" d="M6.633 10.25c.806 0 1.533-.446 2.031-1.08a9.041 9.041 0 0 1 2.861-2.4c.723-.384 1.35-.956 1.653-1.715a4.498 4.498 0 0 0 .322-1.672V3a.75.75 0 0 1 .75-.75 2.25 2.25 0 0 1 2.25 2.25c0 1.152-.26 2.243-.723 3.218-.266.558.107 1.282.725 1.282h3.126c1.026 0 1.945.694 2.054 1.715.045.422.068.85.068 1.285a11.95 11.95 0 0 1-2.649 7.521c-.388.482-.987.729-1.605.729H13.48c-.483 0-.964-.078-1.423-.23l-3.114-1.04a4.501 4.501 0 0 0-1.423-.23H5.904m1.729-7.5a8.97 8.97 0 0 0-.621 4.72c.063.504.123 1.012.182 1.52.04.35.05.703.05 1.06v.27c0 .415-.336.75-.75.75h-2.25a.75.75 0 0 1-.75-.75v-7.5a.75.75 0 0 1 .75-.75h2.25c.414 0 .75.335.75.75v.198Z"/></svg>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.0"><path d="M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H20a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.76a2 2 0 0 0-1.79 1.11L12 22a3.13 3.13 0 0 1-3-3.88Z"/><path d="M17 14V2"/></svg>
|
||||||
</span>
|
</span>
|
||||||
|
<span v-if="response.createdAt" class="ai-assistant-output-time" :title="$A.dayjs(response.createdAt).format('YYYY-MM-DD HH:mm:ss')">{{ formatResponseTime(response.createdAt) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -273,6 +292,8 @@ export default {
|
|||||||
loadIng: 0,
|
loadIng: 0,
|
||||||
pendingAutoSubmit: false,
|
pendingAutoSubmit: false,
|
||||||
autoSubmitTimer: null,
|
autoSubmitTimer: null,
|
||||||
|
confirmingDeleteId: null,
|
||||||
|
confirmingDeleteTimer: null,
|
||||||
modalTitle: null,
|
modalTitle: null,
|
||||||
applyButtonText: null,
|
applyButtonText: null,
|
||||||
submitButtonText: null,
|
submitButtonText: null,
|
||||||
@ -368,6 +389,7 @@ export default {
|
|||||||
emitter.off('openAIAssistant', this.onOpenAIAssistant);
|
emitter.off('openAIAssistant', this.onOpenAIAssistant);
|
||||||
this.clearActiveSSEClients();
|
this.clearActiveSSEClients();
|
||||||
this.clearAutoSubmitTimer();
|
this.clearAutoSubmitTimer();
|
||||||
|
this.resetDeleteConfirm();
|
||||||
this.unmountFloatButton();
|
this.unmountFloatButton();
|
||||||
this.refreshWelcomePromptsDebounced?.cancel();
|
this.refreshWelcomePromptsDebounced?.cancel();
|
||||||
this.stopZIndexTimer();
|
this.stopZIndexTimer();
|
||||||
@ -1105,6 +1127,8 @@ export default {
|
|||||||
handleStreamDone(owner, sse, event) {
|
handleStreamDone(owner, sse, event) {
|
||||||
const donePayload = this.parseStreamPayload(event);
|
const donePayload = this.parseStreamPayload(event);
|
||||||
const target = this.locateStreamTarget(owner);
|
const target = this.locateStreamTarget(owner);
|
||||||
|
// 完成前先判断是否仍贴底(此时反馈行尚未渲染)
|
||||||
|
const stickToBottom = target.isCurrent && this.shouldStickToBottom();
|
||||||
if (target.entry) {
|
if (target.entry) {
|
||||||
if (donePayload && donePayload.error) {
|
if (donePayload && donePayload.error) {
|
||||||
this.markResponseError(target.entry, donePayload.error);
|
this.markResponseError(target.entry, donePayload.error);
|
||||||
@ -1114,6 +1138,10 @@ export default {
|
|||||||
}
|
}
|
||||||
this.releaseSSEClient(sse);
|
this.releaseSSEClient(sse);
|
||||||
this.persistStreamTarget(target);
|
this.persistStreamTarget(target);
|
||||||
|
// 完成后反馈/时间等按钮新增了高度,贴底用户需要再滚到底部
|
||||||
|
if (stickToBottom) {
|
||||||
|
this.scrollResponsesToBottom();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1325,6 +1353,7 @@ export default {
|
|||||||
error: '',
|
error: '',
|
||||||
applyLoading: false,
|
applyLoading: false,
|
||||||
feedback: '',
|
feedback: '',
|
||||||
|
createdAt: Date.now(),
|
||||||
};
|
};
|
||||||
this.responses.push(entry);
|
this.responses.push(entry);
|
||||||
if (this.responses.length > this.maxResponses) {
|
if (this.responses.length > this.maxResponses) {
|
||||||
@ -1427,14 +1456,27 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 提交 👍/👎 反馈(可改票:点另一个值覆盖更新)
|
* 复制回复内容(去除推理段落)
|
||||||
*/
|
*/
|
||||||
async submitFeedback(response, value) {
|
copyResponse(response) {
|
||||||
if (!response || response.feedbackLoading || response.feedback === value) {
|
const text = this.removeReasoningSections(response?.displayOutput || response?.rawOutput) || '';
|
||||||
|
if (!text) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this.copyText(text);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交 👍/👎 反馈(可改票:点另一个值覆盖更新;再点当前已选项则取消)
|
||||||
|
*/
|
||||||
|
async submitFeedback(response, value) {
|
||||||
|
if (!response || response.feedbackLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 点击当前已选中的反馈表示取消
|
||||||
|
const next = response.feedback === value ? '' : value;
|
||||||
const prev = response.feedback;
|
const prev = response.feedback;
|
||||||
this.$set(response, 'feedback', value);
|
this.$set(response, 'feedback', next);
|
||||||
this.$set(response, 'feedbackLoading', true);
|
this.$set(response, 'feedbackLoading', true);
|
||||||
try {
|
try {
|
||||||
await this.$store.dispatch("call", {
|
await this.$store.dispatch("call", {
|
||||||
@ -1444,19 +1486,20 @@ export default {
|
|||||||
session_key: this.currentSessionKey,
|
session_key: this.currentSessionKey,
|
||||||
session_id: this.currentSessionId || '',
|
session_id: this.currentSessionId || '',
|
||||||
local_id: response.localId,
|
local_id: response.localId,
|
||||||
feedback: value,
|
feedback: next,
|
||||||
prompt: this.parsePromptContent(response.prompt).text.substring(0, 1000),
|
prompt: this.parsePromptContent(response.prompt).text.substring(0, 1000),
|
||||||
answer: (this.removeReasoningSections(response.rawOutput) || '').substring(0, 2000),
|
answer: (this.removeReasoningSections(response.rawOutput) || '').substring(0, 2000),
|
||||||
source_ids: this.extractSourceIds(response.rawOutput),
|
source_ids: this.extractSourceIds(response.rawOutput),
|
||||||
model: response.model,
|
model: response.model,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
// 先复位 loading 再保存,避免把 feedbackLoading=true 持久化导致重载后无法再点
|
||||||
|
this.$set(response, 'feedbackLoading', false);
|
||||||
this.saveCurrentSession();
|
this.saveCurrentSession();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.$set(response, 'feedback', prev);
|
this.$set(response, 'feedback', prev);
|
||||||
$A.messageError(e?.msg || '反馈失败,请重试');
|
|
||||||
} finally {
|
|
||||||
this.$set(response, 'feedbackLoading', false);
|
this.$set(response, 'feedbackLoading', false);
|
||||||
|
$A.messageError(e?.msg || '反馈失败,请重试');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -1554,6 +1597,8 @@ export default {
|
|||||||
r.status = 'error';
|
r.status = 'error';
|
||||||
r.error = r.error || this.$L('会话中断');
|
r.error = r.error || this.$L('会话中断');
|
||||||
}
|
}
|
||||||
|
// feedbackLoading 为瞬时态,恢复时强制复位,避免历史脏数据卡死反馈按钮
|
||||||
|
r.feedbackLoading = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// 缓存服务端返回的图片URL映射
|
// 缓存服务端返回的图片URL映射
|
||||||
@ -1577,10 +1622,12 @@ export default {
|
|||||||
*/
|
*/
|
||||||
sanitizeResponsesForPersist(responses) {
|
sanitizeResponsesForPersist(responses) {
|
||||||
return (responses || []).map(r => {
|
return (responses || []).map(r => {
|
||||||
if (r.status === 'streaming' || r.status === 'waiting') {
|
// feedbackLoading 为瞬时 UI 态,不持久化,避免重载后卡死反馈按钮
|
||||||
return {...r, status: 'error', error: r.error || this.$L('会话中断')};
|
const {feedbackLoading, ...rest} = r;
|
||||||
|
if (rest.status === 'streaming' || rest.status === 'waiting') {
|
||||||
|
return {...rest, status: 'error', error: rest.error || this.$L('会话中断')};
|
||||||
}
|
}
|
||||||
return r;
|
return rest;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -1803,7 +1850,35 @@ export default {
|
|||||||
/**
|
/**
|
||||||
* 删除指定会话
|
* 删除指定会话
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* 第一次点击删除:进入「确认删除」态(不立即删除),3 秒后自动复位
|
||||||
|
*/
|
||||||
|
askDeleteSession(sessionId) {
|
||||||
|
this.confirmingDeleteId = sessionId;
|
||||||
|
clearTimeout(this.confirmingDeleteTimer);
|
||||||
|
this.confirmingDeleteTimer = setTimeout(() => {
|
||||||
|
this.resetDeleteConfirm();
|
||||||
|
}, 3000);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 复位「确认删除」态
|
||||||
|
*/
|
||||||
|
resetDeleteConfirm() {
|
||||||
|
this.confirmingDeleteId = null;
|
||||||
|
clearTimeout(this.confirmingDeleteTimer);
|
||||||
|
this.confirmingDeleteTimer = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 历史下拉显隐变化时复位「确认删除」态
|
||||||
|
*/
|
||||||
|
onHistoryVisibleChange() {
|
||||||
|
this.resetDeleteConfirm();
|
||||||
|
},
|
||||||
|
|
||||||
deleteSession(sessionId) {
|
deleteSession(sessionId) {
|
||||||
|
this.resetDeleteConfirm();
|
||||||
const index = this.sessionStore.findIndex(s => s.id === sessionId);
|
const index = this.sessionStore.findIndex(s => s.id === sessionId);
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
const session = this.sessionStore[index];
|
const session = this.sessionStore[index];
|
||||||
@ -1850,6 +1925,40 @@ export default {
|
|||||||
/**
|
/**
|
||||||
* 格式化会话时间显示
|
* 格式化会话时间显示
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* 格式化单条回复时间(规则参考 happy-next formatMessageTime)
|
||||||
|
* - 今天:HH:mm
|
||||||
|
* - 本周内(周一为起点):周几 HH:mm
|
||||||
|
* - 今年内:MM-DD HH:mm
|
||||||
|
* - 更早:YYYY-MM-DD
|
||||||
|
*/
|
||||||
|
formatResponseTime(timestamp) {
|
||||||
|
if (!timestamp) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const now = $A.daytz();
|
||||||
|
const time = $A.dayjs(timestamp);
|
||||||
|
const hm = time.format('HH:mm');
|
||||||
|
// 今天
|
||||||
|
if (now.format('YYYY-MM-DD') === time.format('YYYY-MM-DD')) {
|
||||||
|
return hm;
|
||||||
|
}
|
||||||
|
// 本周内(周一 00:00 起点,且不晚于当前)
|
||||||
|
const weekStart = now.subtract((now.day() + 6) % 7, 'day').startOf('day');
|
||||||
|
if (time.valueOf() >= weekStart.valueOf() && time.valueOf() <= now.valueOf()) {
|
||||||
|
const map = {zh: 'zh-CN', 'zh-CHT': 'zh-TW'};
|
||||||
|
const lang = getLanguage();
|
||||||
|
const locale = map[lang] || lang || 'en';
|
||||||
|
const weekday = new Intl.DateTimeFormat(locale, {weekday: 'short'}).format(time.toDate());
|
||||||
|
return `${weekday} ${hm}`;
|
||||||
|
}
|
||||||
|
// 今年内
|
||||||
|
if (now.year() === time.year()) {
|
||||||
|
return time.format('MM-DD HH:mm');
|
||||||
|
}
|
||||||
|
return time.format('YYYY-MM-DD');
|
||||||
|
},
|
||||||
|
|
||||||
formatSessionTime(timestamp) {
|
formatSessionTime(timestamp) {
|
||||||
const now = $A.daytz();
|
const now = $A.daytz();
|
||||||
const time = $A.dayjs(timestamp);
|
const time = $A.dayjs(timestamp);
|
||||||
@ -2698,19 +2807,21 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ai-assistant-output-placeholder {
|
.ai-assistant-output-placeholder {
|
||||||
|
height: 34px;
|
||||||
|
line-height: 34px;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #999;
|
color: #999;
|
||||||
padding: 8px;
|
padding: 0 10px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background: rgba(0, 0, 0, 0.02);
|
background: rgba(0, 0, 0, 0.02);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ai-assistant-output-feedback {
|
.ai-assistant-output-feedback {
|
||||||
margin-top: 6px;
|
margin-top: 8px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 4px;
|
gap: 3px;
|
||||||
|
|
||||||
.ai-assistant-feedback-btn {
|
.ai-assistant-feedback-btn {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@ -2724,8 +2835,8 @@ export default {
|
|||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
width: 15px;
|
width: 14px;
|
||||||
height: 15px;
|
height: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@ -2734,26 +2845,32 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
color: var(--primary-color, #1677ff);
|
color: var(--primary-color, #8bcf70);
|
||||||
background: rgba(22, 119, 255, 0.08);
|
background: rgba(139, 207, 112, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.ai-assistant-feedback-btn-down {
|
&.ai-assistant-feedback-btn-down {
|
||||||
svg {
|
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
color: #f56c6c;
|
color: #f56c6c;
|
||||||
background: rgba(245, 108, 108, 0.08);
|
background: rgba(245, 108, 108, 0.08);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ai-assistant-output-time {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #bbb;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ai-assistant-output-markdown {
|
.ai-assistant-output-markdown {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
min-height: 34px;
|
||||||
|
|
||||||
.apply-reasoning {
|
.apply-reasoning {
|
||||||
margin: 0 0 12px 0;
|
margin: 0 0 12px 0;
|
||||||
@ -2966,6 +3083,22 @@ export default {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #909399;
|
color: #909399;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.history-item-delete-confirm {
|
||||||
|
display: flex;
|
||||||
|
color: #fff;
|
||||||
|
background-color: #f56c6c;
|
||||||
|
opacity: 1;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #e15a5a;
|
||||||
|
}
|
||||||
|
|
||||||
|
> svg {
|
||||||
|
width: 13px;
|
||||||
|
height: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2981,6 +3114,16 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 触摸设备无 hover,删除按钮常显
|
||||||
|
@media (pointer: coarse) {
|
||||||
|
.history-item-content {
|
||||||
|
.history-item-delete {
|
||||||
|
display: flex;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.history-clear {
|
.history-clear {
|
||||||
@ -3317,6 +3460,21 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.window-portrait {
|
||||||
|
.ai-assistant-content {
|
||||||
|
.ai-assistant-output-feedback {
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
.ai-assistant-feedback-btn {
|
||||||
|
svg {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
body.dark-mode-reverse {
|
body.dark-mode-reverse {
|
||||||
.ai-assistant-content {
|
.ai-assistant-content {
|
||||||
.ai-assistant-welcome,
|
.ai-assistant-welcome,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user