diff --git a/ui/src/assets/iconfont.js b/ui/src/assets/iconfont.js
index b23687ab52f..debca930b4e 100644
--- a/ui/src/assets/iconfont.js
+++ b/ui/src/assets/iconfont.js
@@ -1 +1 @@
-window._iconfont_svg_string_5200172='',(h=>{var a=(l=(l=document.getElementsByTagName("script"))[l.length-1]).getAttribute("data-injectcss"),l=l.getAttribute("data-disable-injectsvg");if(!l){var o,v,t,i,c,d=function(a,l){l.parentNode.insertBefore(a,l)};if(a&&!h.__iconfont__svg__cssinject__){h.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(a){console&&console.log(a)}}o=function(){var a,l=document.createElement("div");l.innerHTML=h._iconfont_svg_string_5200172,(l=l.getElementsByTagName("svg")[0])&&(l.setAttribute("aria-hidden","true"),l.style.position="absolute",l.style.width=0,l.style.height=0,l.style.overflow="hidden",l=l,(a=document.body).firstChild?d(l,a.firstChild):a.appendChild(l))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(o,0):(v=function(){document.removeEventListener("DOMContentLoaded",v,!1),o()},document.addEventListener("DOMContentLoaded",v,!1)):document.attachEvent&&(t=o,i=h.document,c=!1,m(),i.onreadystatechange=function(){"complete"==i.readyState&&(i.onreadystatechange=null,e())})}function e(){c||(c=!0,t())}function m(){try{i.documentElement.doScroll("left")}catch(a){return void setTimeout(m,50)}e()}})(window);
\ No newline at end of file
+window._iconfont_svg_string_5200172='',(h=>{var a=(l=(l=document.getElementsByTagName("script"))[l.length-1]).getAttribute("data-injectcss"),l=l.getAttribute("data-disable-injectsvg");if(!l){var o,v,t,i,c,d=function(a,l){l.parentNode.insertBefore(a,l)};if(a&&!h.__iconfont__svg__cssinject__){h.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(a){console&&console.log(a)}}o=function(){var a,l=document.createElement("div");l.innerHTML=h._iconfont_svg_string_5200172,(l=l.getElementsByTagName("svg")[0])&&(l.setAttribute("aria-hidden","true"),l.style.position="absolute",l.style.width=0,l.style.height=0,l.style.overflow="hidden",l=l,(a=document.body).firstChild?d(l,a.firstChild):a.appendChild(l))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(o,0):(v=function(){document.removeEventListener("DOMContentLoaded",v,!1),o()},document.addEventListener("DOMContentLoaded",v,!1)):document.attachEvent&&(t=o,i=h.document,c=!1,e(),i.onreadystatechange=function(){"complete"==i.readyState&&(i.onreadystatechange=null,m())})}function m(){c||(c=!0,t())}function e(){try{i.documentElement.doScroll("left")}catch(a){return void setTimeout(e,50)}m()}})(window);
\ No newline at end of file
diff --git a/ui/src/components/COMPONENT_README.md b/ui/src/components/COMPONENT_README.md
index 1b6566d1dca..a53b010b6ce 100644
--- a/ui/src/components/COMPONENT_README.md
+++ b/ui/src/components/COMPONENT_README.md
@@ -77,13 +77,14 @@ import { MkDynamicsForm, MkDynamicsFormConstructor } from '@/components/mk-dynam
### MkTooltip
-普通提示统一使用 `MkTooltip`,业务代码不直接使用 `el-tooltip`。默认悬停 500ms 后显示,
+普通提示统一使用 `MkTooltip`,业务代码不直接使用 `el-tooltip`。默认悬停 300ms 后显示,
延迟由 `global/mk-tooltip/constants.ts` 的 `TOOLTIP_SHOW_DELAY` 维护;提前移开时由 Element Plus 取消显示。
-`showAfter` 可按需覆盖,其他 Tooltip 属性、事件(包括 `update:visible`)通过 `$attrs` 透传,
+项目中所有显式 `show-after` / `showAfter` 配置(包括 Popover)统一引用 `TOOLTIP_SHOW_DELAY`,不写死延迟数值。
+`MkTooltip` 的 `showAfter` 默认值使用该常量;其他 Tooltip 属性、事件(包括 `update:visible`)通过 `$attrs` 透传,
支持默认触发插槽和 `content` 插槽,保持原有关闭延迟及可移入行为。
显式控制 `visible` 时,显示时机由调用方负责,`showAfter` 不参与受控显示。
Admin、Chat 共用的 `App.vue` 通过 Config Provider 的 `table.tooltipOptions` 为表格溢出提示
-配置同一延迟,不额外开启溢出提示。原生 `title`、Popover、下拉菜单及图表提示保持各自行为。
+配置同一延迟,不额外开启溢出提示。原生 `title`、未显式配置延迟的浮层、下拉菜单及图表提示保持各自行为。
### MdEditor、MdEditorMagnify、MdPreview
@@ -655,6 +656,8 @@ Model 配置器的默认模型也使用 `SelectModel`,仅展示已选的可选
重新生成追加用户消息 `Re generate` 后请求;停止调用当前流的 `cancel()`,保留已生成内容。
输入支持 Enter 发送、Ctrl/Shift/Alt/Meta + Enter 在光标处换行、原生文本粘贴,拒绝纯空白,
输入法组合期间不提交;键盘和粘贴事件停止冒泡,输入上限 100000 字符。
+回车与组合键换行复用 `conversation-panel/core/shortcuts.ts` 的 `inputShortcut`,
+发送条件由组件保留;该工具不接管原生文本粘贴或附件上传。
### FolderTree
diff --git a/ui/src/components/business/generate-content/index.vue b/ui/src/components/business/generate-content/index.vue
index 735459bc25b..e9d1c7c1839 100644
--- a/ui/src/components/business/generate-content/index.vue
+++ b/ui/src/components/business/generate-content/index.vue
@@ -3,6 +3,7 @@ import { computed, nextTick, onBeforeUnmount, reactive, ref, useTemplateRef, wat
import type { ScrollbarInstance } from 'element-plus'
import type { PromptGenerateMessage } from '@/api/types'
import { ConversationStream } from '@/conversation-panel/core/stream'
+import { inputShortcut } from '@/conversation-panel/core/shortcuts'
import { MsgError } from '@/utils/message'
defineOptions({ name: 'GenerateContent' })
@@ -107,21 +108,8 @@ function replaceContent() {
}
// 需求输入:回车发送,组合键在光标处换行,输入法确认时不提交。
-function handleInputKeydown(event: KeyboardEvent) {
- if (event.isComposing || event.key !== 'Enter') return
- event.preventDefault()
- if (event.ctrlKey || event.shiftKey || event.altKey || event.metaKey) {
- const textarea = event.target
- if (!(textarea instanceof HTMLTextAreaElement)) return
- const startPos = textarea.selectionStart
- const endPos = textarea.selectionEnd
- const content = inputValue.value.slice(0, startPos) + '\n' + inputValue.value.slice(endPos)
- if (textarea.maxLength >= 0 && content.length > textarea.maxLength) return
- inputValue.value = content
- nextTick(() => textarea.setSelectionRange(startPos + 1, startPos + 1))
- return
- }
- generateContent()
+function handleKeydown(event: KeyboardEvent) {
+ inputShortcut(event, inputValue, () => generateContent())
}
// 关闭或卸载时停止读取,关闭动画结束后清理会话。
@@ -162,7 +150,7 @@ onBeforeUnmount(stopGenerate)