feat(ai-assistant): 支持编辑历史问题并重新发送

- 鼠标悬停历史问题时显示编辑图标
  - 点击编辑后在原位置显示内联编辑器
  - 支持 Enter 发送、Shift+Enter 换行、Esc 取消
  - 发送后删除该问题及之后的对话历史,重新发送编辑后的问题
  - 正确处理中文输入法组合状态,避免误触发提交
This commit is contained in:
kuaifan 2026-01-18 12:56:16 +00:00
parent f8b335a003
commit c65f0276bd

View File

@ -77,7 +77,32 @@
<div class="ai-assistant-output-meta">
<span class="ai-assistant-output-model">{{ response.modelLabel || response.model }}</span>
</div>
<div v-if="response.prompt" class="ai-assistant-output-question">{{ response.prompt }}</div>
<!-- 问题区域正常显示或编辑模式 -->
<div v-if="response.prompt" class="ai-assistant-output-question-wrap">
<!-- 编辑模式 -->
<div v-if="editingIndex === responses.indexOf(response)" class="ai-assistant-question-editor">
<Input
v-model="editingValue"
ref="editInputRef"
type="textarea"
:autosize="{minRows: 1, maxRows: 6}"
:maxlength="inputMaxlength || 500"
@on-keydown="onEditKeydown"
@compositionstart.native="isComposing = true"
@compositionend.native="isComposing = false" />
<div class="ai-assistant-question-editor-btns">
<Button size="small" @click="cancelEditQuestion">{{ $L('取消') }}</Button>
<Button type="primary" size="small" :loading="loadIng > 0" @click="submitEditedQuestion">{{ $L('发送') }}</Button>
</div>
</div>
<!-- 正常显示模式 -->
<div v-else class="ai-assistant-output-question">
<span class="ai-assistant-output-question-text">{{ response.prompt }}</span>
<span class="ai-assistant-output-question-edit" :title="$L('编辑问题')" @click="startEditQuestion(responses.indexOf(response))">
<svg viewBox="0 0 20 20" fill="currentColor"><path d="M11.331 3.568a3.61 3.61 0 0 1 4.973.128l.128.135a3.61 3.61 0 0 1 0 4.838l-.128.135-6.292 6.29c-.324.324-.558.561-.79.752l-.235.177q-.309.21-.65.36l-.23.093c-.181.066-.369.114-.585.159l-.765.135-2.394.399c-.142.024-.294.05-.422.06-.1.007-.233.01-.378-.026l-.149-.049a1.1 1.1 0 0 1-.522-.474l-.046-.094a1.1 1.1 0 0 1-.074-.526c.01-.129.035-.28.06-.423l.398-2.394.134-.764a4 4 0 0 1 .16-.586l.093-.23q.15-.342.36-.65l.176-.235c.19-.232.429-.466.752-.79l6.291-6.292zm-5.485 7.36c-.35.35-.533.535-.66.688l-.11.147a2.7 2.7 0 0 0-.24.433l-.062.155c-.04.11-.072.225-.106.394l-.127.717-.398 2.393-.001.002h.003l2.393-.399.717-.126c.169-.034.284-.065.395-.105l.153-.062q.228-.1.433-.241l.148-.11c.153-.126.338-.31.687-.66l4.988-4.988-3.226-3.226zm9.517-6.291a2.28 2.28 0 0 0-3.053-.157l-.173.157-.364.363L15 8.226l.363-.363.157-.174a2.28 2.28 0 0 0 0-2.878z"/></svg>
</span>
</div>
</div>
<DialogMarkdown
v-if="response.rawOutput"
class="ai-assistant-output-markdown no-dark-content"
@ -225,6 +250,10 @@ export default {
//
displayWelcomePrompts: [],
//
editingIndex: -1, // -1
editingValue: '', //
}
},
created() {
@ -552,22 +581,38 @@ export default {
if (this.loadIng > 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<boolean>} - 是否发送成功
* @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);
},
},
}
</script>
@ -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 {