mirror of
https://github.com/open-webui/open-webui.git
synced 2026-10-05 02:41:34 +00:00
fix: typing with an input method breaks in the empty chat input while a follow-up suggestion is shown (#31393)
After a reply, the first suggested follow-up question sits as grey ghost text in the empty chat input. Typing into it with an input method (Chinese, Japanese or Korean) broke the word being composed: on iOS the input lost focus after the first character, and in Chromium the first letter was left behind, so typing "ni" and picking "你" gave "n你". Clearing the ghost text rebuilt the input's line of text while the keyboard was still composing the word. The ghost text is now drawn over the empty input without being written into it, so input methods work again, and Tab to accept it and the follow-up buttons under the reply behave as before. Fixes #31372
This commit is contained in:
parent
a5176f4cda
commit
d09ab78fce
2 changed files with 47 additions and 19 deletions
|
|
@ -143,7 +143,7 @@
|
|||
import { Decoration, DecorationSet } from 'prosemirror-view';
|
||||
import { Editor, Extension, markInputRule, mergeAttributes } from '@tiptap/core';
|
||||
|
||||
import { AIAutocompletion } from './RichTextInput/AutoCompletion.js';
|
||||
import { AIAutocompletion, setFollowUpSuggestion } from './RichTextInput/AutoCompletion.js';
|
||||
|
||||
import StarterKit from '@tiptap/starter-kit';
|
||||
|
||||
|
|
@ -322,23 +322,7 @@
|
|||
export let followUpSuggestion = '';
|
||||
|
||||
$: if (editor && !editor.isDestroyed) {
|
||||
const { doc } = editor.state;
|
||||
const node = doc.firstChild;
|
||||
if (node?.type.name === 'paragraph' && !node.attrs['data-prompt']) {
|
||||
const suggestion = doc.childCount === 1 && node.content.size === 0 ? followUpSuggestion : '';
|
||||
if ((node.attrs['data-suggestion'] ?? '') !== suggestion) {
|
||||
editor.view.dispatch(
|
||||
editor.state.tr
|
||||
.setNodeMarkup(0, null, {
|
||||
...node.attrs,
|
||||
class: suggestion ? 'ai-autocompletion' : null,
|
||||
'data-prompt': suggestion ? '' : null,
|
||||
'data-suggestion': suggestion || null
|
||||
})
|
||||
.setMeta('addToHistory', false)
|
||||
);
|
||||
}
|
||||
}
|
||||
setFollowUpSuggestion(editor.view, followUpSuggestion);
|
||||
}
|
||||
|
||||
export let messageInput = false;
|
||||
|
|
|
|||
|
|
@ -13,6 +13,31 @@ Intelligently reset suggestions on new input.
|
|||
|
||||
import { Extension } from '@tiptap/core';
|
||||
import { Plugin, PluginKey } from 'prosemirror-state';
|
||||
import { Decoration, DecorationSet } from 'prosemirror-view';
|
||||
|
||||
const aiAutocompletionKey = new PluginKey('aiAutocompletion');
|
||||
|
||||
const getFollowUpSuggestion = (state) => {
|
||||
const { doc } = state;
|
||||
const isEmpty =
|
||||
doc.childCount === 1 &&
|
||||
doc.firstChild.type.name === 'paragraph' &&
|
||||
doc.firstChild.content.size === 0;
|
||||
return isEmpty ? aiAutocompletionKey.getState(state) : '';
|
||||
};
|
||||
|
||||
export const setFollowUpSuggestion = (view, suggestion) => {
|
||||
const current = aiAutocompletionKey.getState(view.state);
|
||||
if (current === undefined || current === suggestion) return;
|
||||
view.dispatch(view.state.tr.setMeta(aiAutocompletionKey, suggestion));
|
||||
};
|
||||
|
||||
const acceptFollowUpSuggestion = (view) => {
|
||||
const suggestion = getFollowUpSuggestion(view.state);
|
||||
if (!suggestion) return false;
|
||||
view.dispatch(view.state.tr.insertText(suggestion));
|
||||
return true;
|
||||
};
|
||||
|
||||
export const AIAutocompletion = Extension.create({
|
||||
name: 'aiAutocompletion',
|
||||
|
|
@ -131,8 +156,23 @@ export const AIAutocompletion = Extension.create({
|
|||
|
||||
return [
|
||||
new Plugin({
|
||||
key: new PluginKey('aiAutocompletion'),
|
||||
key: aiAutocompletionKey,
|
||||
state: {
|
||||
init: () => '',
|
||||
apply: (tr, suggestion) => tr.getMeta(aiAutocompletionKey) ?? suggestion
|
||||
},
|
||||
props: {
|
||||
// A decoration is patched in place, so removing it cannot break an IME composition
|
||||
decorations: (state) => {
|
||||
const suggestion = getFollowUpSuggestion(state);
|
||||
if (!suggestion) return null;
|
||||
return DecorationSet.create(state.doc, [
|
||||
Decoration.node(0, state.doc.firstChild.nodeSize, {
|
||||
class: 'ai-autocompletion',
|
||||
'data-suggestion': suggestion
|
||||
})
|
||||
]);
|
||||
},
|
||||
handleKeyDown: (view, event) => {
|
||||
if (isComposing || event.isComposing || (event.key === 'Tab' && event.shiftKey))
|
||||
return false;
|
||||
|
|
@ -145,6 +185,8 @@ export const AIAutocompletion = Extension.create({
|
|||
const node = $head.parent;
|
||||
|
||||
if (event.key === 'Tab') {
|
||||
if (acceptFollowUpSuggestion(view)) return true;
|
||||
|
||||
// if (!node.attrs['data-suggestion']) {
|
||||
// // Generate completion
|
||||
// if (loading) return true
|
||||
|
|
@ -219,6 +261,8 @@ export const AIAutocompletion = Extension.create({
|
|||
|
||||
// Check if the swipe was primarily horizontal and to the right
|
||||
if (Math.abs(deltaX) > Math.abs(deltaY) && deltaX > 50) {
|
||||
if (acceptFollowUpSuggestion(view)) return true;
|
||||
|
||||
const { state, dispatch } = view;
|
||||
const { selection } = state;
|
||||
const { $head } = selection;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue