diff --git a/resources/assets/js/components/AIAssistant/index.vue b/resources/assets/js/components/AIAssistant/index.vue index b65113c2f..d4d2b0fd5 100644 --- a/resources/assets/js/components/AIAssistant/index.vue +++ b/resources/assets/js/components/AIAssistant/index.vue @@ -77,7 +77,32 @@
{{ response.modelLabel || response.model }}
-
{{ response.prompt }}
+ +
+ +
+ +
+ + +
+
+ +
+ {{ response.prompt }} + + + +
+
0) { return; } - const rawValue = this.inputValue || ''; + const prompt = (this.inputValue || '').trim(); + if (!prompt) { + return; + } + const success = await this._doSendQuestion(prompt); + if (success) { + this.inputValue = ''; + } + }, + + /** + * 执行发送问题的核心逻辑 + * @param {string} prompt - 要发送的问题 + * @returns {Promise} - 是否发送成功 + * @private + */ + async _doSendQuestion(prompt) { const modelOption = this.selectedModelOption; if (!modelOption) { $A.messageWarning('请选择模型'); - return; + return false; } this.loadIng++; let responseEntry = null; try { - const baseContext = this.collectBaseContext(rawValue); + const baseContext = this.collectBaseContext(prompt); const context = await this.buildPayloadData(baseContext); responseEntry = this.createResponseEntry({ modelOption, - prompt: rawValue, + prompt, }); this.scrollResponsesToBottom(); @@ -577,14 +622,15 @@ export default { context, }); - this.inputValue = ''; this.startStream(streamKey, responseEntry); + return true; } catch (error) { const msg = error?.msg || '发送失败'; if (responseEntry) { this.markResponseError(responseEntry, msg); } $A.modalError(msg); + return false; } finally { this.loadIng--; } @@ -1291,6 +1337,75 @@ export default { } return time.format('YYYY-MM-DD HH:mm'); }, + + // ==================== 编辑历史问题 ==================== + + /** + * 开始编辑历史问题 + */ + startEditQuestion(index) { + if (index < 0 || index >= this.responses.length) { + return; + } + if (this.loadIng > 0) { + return; + } + this.editingIndex = index; + this.editingValue = this.responses[index].prompt || ''; + this.$nextTick(() => { + // ref 在 v-for 中会变成数组 + const inputRef = this.$refs.editInputRef; + const input = Array.isArray(inputRef) ? inputRef[0] : inputRef; + if (input && typeof input.focus === 'function') { + input.focus(); + } + }); + }, + + /** + * 取消编辑 + */ + cancelEditQuestion() { + this.editingIndex = -1; + this.editingValue = ''; + }, + + /** + * 编辑器键盘事件 + */ + onEditKeydown(e) { + if (e.key === 'Escape') { + e.preventDefault(); + this.cancelEditQuestion(); + } else if (e.key === 'Enter' && !e.shiftKey && !this.isComposing) { + e.preventDefault(); + this.submitEditedQuestion(); + } + }, + + /** + * 提交编辑后的问题 + */ + async submitEditedQuestion() { + if (this.editingIndex < 0 || this.loadIng > 0) { + return; + } + const newPrompt = (this.editingValue || '').trim(); + if (!newPrompt) { + $A.messageWarning('请输入问题'); + return; + } + + // 删除从编辑位置开始的所有响应 + this.responses.splice(this.editingIndex); + + // 重置编辑状态 + this.editingIndex = -1; + this.editingValue = ''; + + // 发送新问题 + await this._doSendQuestion(newPrompt); + }, }, } @@ -1427,15 +1542,97 @@ export default { padding: 2px 8px; } + .ai-assistant-output-question-wrap { + margin-top: 8px; + } + .ai-assistant-output-question { - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; + display: flex; + align-items: flex-start; + gap: 4px; font-size: 12px; color: #666; line-height: 1.4; - margin-top: 8px; + + .ai-assistant-output-question-text { + flex: 1; + min-width: 0; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + } + + .ai-assistant-output-question-edit { + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + color: #777; + border-radius: 4px; + margin-top: -2px; + cursor: pointer; + opacity: 0; + transition: opacity 0.2s, color 0.2s, background-color 0.2s; + + svg { + width: 14px; + height: 14px; + } + + &:hover { + color: #444; + background-color: rgba(0, 0, 0, 0.06); + } + } + + &:hover { + .ai-assistant-output-question-edit { + opacity: 1; + } + } + } + + .ai-assistant-question-editor { + display: flex; + flex-direction: column; + gap: 8px; + padding: 8px; + background: #fff; + border: 1px solid #e8e8e8; + border-radius: 13px; + + .ivu-input { + color: #333; + background-color: transparent; + border: 0; + border-radius: 0; + box-shadow: none; + padding: 0 2px; + resize: none; + font-size: 12px; + + &:hover, + &:focus { + border-color: transparent; + box-shadow: none; + } + } + + .ai-assistant-question-editor-btns { + display: flex; + justify-content: flex-end; + gap: 8px; + + .ivu-btn { + height: 26px; + font-size: 12px; + padding: 0 9px; + border-radius: 13px; + } + } } .ai-assistant-output-placeholder {