From 0d5c5f8d9f5db74a449752bfefe653946831e383 Mon Sep 17 00:00:00 2001 From: Peter Dave Hello Date: Mon, 13 Jul 2026 23:43:13 +0800 Subject: [PATCH] Fix independent panel input resizing Replace the browser-specific transformed resize handle with an accessible pointer-driven separator so Firefox can adjust the input area from its top edge. Clamp the input height to keep the conversation pane usable, preserve native resizing outside the independent panel, and cover the shared resize calculations with unit tests. --- src/_locales/de/main.json | 1 + src/_locales/en/main.json | 1 + src/_locales/es/main.json | 1 + src/_locales/fr/main.json | 1 + src/_locales/in/main.json | 1 + src/_locales/it/main.json | 1 + src/_locales/ja/main.json | 1 + src/_locales/ko/main.json | 1 + src/_locales/pt/main.json | 1 + src/_locales/ru/main.json | 1 + src/_locales/tr/main.json | 1 + src/_locales/zh-hans/main.json | 1 + src/_locales/zh-hant/main.json | 1 + src/components/ConversationCard/index.jsx | 2 +- src/components/InputBox/index.jsx | 171 ++++++++++++++---- src/components/InputBox/resize.mjs | 32 ++++ src/content-script/styles.scss | 34 ++++ .../unit/components/input-box-resize.test.mjs | 42 +++++ 18 files changed, 257 insertions(+), 37 deletions(-) create mode 100644 src/components/InputBox/resize.mjs create mode 100644 tests/unit/components/input-box-resize.test.mjs diff --git a/src/_locales/de/main.json b/src/_locales/de/main.json index e09c196aa..0d6a37e08 100644 --- a/src/_locales/de/main.json +++ b/src/_locales/de/main.json @@ -20,6 +20,7 @@ "Custom Site Regex": "Benutzerdefinierter Website-Regex", "Exclusively use Custom Site Regex for website matching, ignoring built-in rules": "Nur benutzerdefinierten Website-Regex verwenden, um Website-Übereinstimmungen zu finden und interne Regeln ignorieren", "Input Query": "Eingabeaufforderung", + "Resize input box": "Größe des Eingabefelds ändern", "Append Query": "Am Ende anhängen", "Prepend Query": "Am Anfang hinzufügen", "Wechat Pay": "WeChat-Pay", diff --git a/src/_locales/en/main.json b/src/_locales/en/main.json index fd34aa442..a913425b9 100644 --- a/src/_locales/en/main.json +++ b/src/_locales/en/main.json @@ -20,6 +20,7 @@ "Custom Site Regex": "Custom Site Regex", "Exclusively use Custom Site Regex for website matching, ignoring built-in rules": "Exclusively use Custom Site Regex for website matching, ignoring built-in rules", "Input Query": "Input Query", + "Resize input box": "Resize input box", "Append Query": "Append Query", "Prepend Query": "Prepend Query", "Wechat Pay": "Wechat Pay", diff --git a/src/_locales/es/main.json b/src/_locales/es/main.json index 5e0f6e553..f8038a126 100644 --- a/src/_locales/es/main.json +++ b/src/_locales/es/main.json @@ -20,6 +20,7 @@ "Custom Site Regex": "Expresión regular personalizada del sitio", "Exclusively use Custom Site Regex for website matching, ignoring built-in rules": "Utilice exclusivamente expesiones regulares personalizadas para la coincidencia de sitios web, ignorando las reglas integradas", "Input Query": "Consulta de entrada", + "Resize input box": "Cambiar el tamaño del cuadro de entrada", "Append Query": "Añadir consulta", "Prepend Query": "Insertar consulta", "Wechat Pay": "Pago de Wechat", diff --git a/src/_locales/fr/main.json b/src/_locales/fr/main.json index b125b3975..c9f76e4e3 100644 --- a/src/_locales/fr/main.json +++ b/src/_locales/fr/main.json @@ -20,6 +20,7 @@ "Custom Site Regex": "Expression régulière de site personnalisée", "Exclusively use Custom Site Regex for website matching, ignoring built-in rules": "Utiliser exclusivement l'expression régulière de site personnalisée pour la correspondance de site Web, en ignorant les règles intégrées", "Input Query": "Sélecteur d'entrée", + "Resize input box": "Redimensionner le champ de saisie", "Append Query": "Sélecteur à ajouter", "Prepend Query": "Sélecteur à insérer", "Wechat Pay": "Paiement Wechat", diff --git a/src/_locales/in/main.json b/src/_locales/in/main.json index 99619e5bf..1e3ca93e7 100644 --- a/src/_locales/in/main.json +++ b/src/_locales/in/main.json @@ -20,6 +20,7 @@ "Custom Site Regex": "Regex Situs Kustom", "Exclusively use Custom Site Regex for website matching, ignoring built-in rules": "Gunakan secara eksklusif Regex Situs Kustom untuk pencocokan situs web, mengabaikan aturan bawaan", "Input Query": "Query Masukan", + "Resize input box": "Ubah ukuran kotak input", "Append Query": "Tambahkan Query", "Prepend Query": "Tambahkan Query di Awal", "Wechat Pay": "Pembayaran Wechat", diff --git a/src/_locales/it/main.json b/src/_locales/it/main.json index a7146afad..c45a81df1 100644 --- a/src/_locales/it/main.json +++ b/src/_locales/it/main.json @@ -20,6 +20,7 @@ "Custom Site Regex": "Espressione regolare del sito personalizzata", "Exclusively use Custom Site Regex for website matching, ignoring built-in rules": "Usa esclusivamente l'espressione regolare del sito personalizzata per la corrispondenza dei siti Web, ignorando le regole incorporate", "Input Query": "Query di input", + "Resize input box": "Ridimensiona la casella di input", "Append Query": "Query di appendice", "Prepend Query": "Query di aggiunta in testa", "Wechat Pay": "Paga con Wechat", diff --git a/src/_locales/ja/main.json b/src/_locales/ja/main.json index 38d91a4ec..453cae577 100644 --- a/src/_locales/ja/main.json +++ b/src/_locales/ja/main.json @@ -20,6 +20,7 @@ "Custom Site Regex": "カスタムサイトの正規表現", "Exclusively use Custom Site Regex for website matching, ignoring built-in rules": "内蔵ルールを無視して、カスタムサイト用の正規表現のみを使用", "Input Query": "入力クエリ", + "Resize input box": "入力欄のサイズを変更", "Append Query": "末尾に追加するクエリ", "Prepend Query": "先頭に挿入するクエリ", "Wechat Pay": "Wechatペイ", diff --git a/src/_locales/ko/main.json b/src/_locales/ko/main.json index 595559dcb..333e29777 100644 --- a/src/_locales/ko/main.json +++ b/src/_locales/ko/main.json @@ -20,6 +20,7 @@ "Custom Site Regex": "사용자 정의 사이트 Regex", "Exclusively use Custom Site Regex for website matching, ignoring built-in rules": "사이트 일치에 독점적으로 사용자 정의 사이트 Regex를 사용하며, 내장 규칙을 무시합니다.", "Input Query": "입력 쿼리 선택기", + "Resize input box": "입력 상자 크기 조절", "Append Query": "끝에 추가할 쿼리 선택기", "Prepend Query": "앞에 추가할 쿼리 선택기", "Wechat Pay": "위챗 페이", diff --git a/src/_locales/pt/main.json b/src/_locales/pt/main.json index 88db48125..e0b5cd26a 100644 --- a/src/_locales/pt/main.json +++ b/src/_locales/pt/main.json @@ -20,6 +20,7 @@ "Custom Site Regex": "Expressão Regular do Site Personalizada", "Exclusively use Custom Site Regex for website matching, ignoring built-in rules": "Usar exclusivamente a Expressão Regular do Site Personalizada para combinação de sites, ignorando regras incorporadas", "Input Query": "Consulta de Entrada", + "Resize input box": "Redimensionar a caixa de entrada", "Append Query": "Consulta Anexada", "Prepend Query": "Consulta Prependida", "Wechat Pay": "Pagamento Wechat", diff --git a/src/_locales/ru/main.json b/src/_locales/ru/main.json index f0cdbcbad..4394b3c76 100644 --- a/src/_locales/ru/main.json +++ b/src/_locales/ru/main.json @@ -20,6 +20,7 @@ "Custom Site Regex": "Пользовательское регулярное выражение сайта", "Exclusively use Custom Site Regex for website matching, ignoring built-in rules": "Использовать только пользовательское регулярное выражение сайта для сопоставления сайта, игнорируя встроенные правила.", "Input Query": "Входной запрос", + "Resize input box": "Изменить размер поля ввода", "Append Query": "Добавить запрос", "Prepend Query": "Вставить запрос", "Wechat Pay": "Wechat-платеж", diff --git a/src/_locales/tr/main.json b/src/_locales/tr/main.json index 438f017c1..dd62ccc52 100644 --- a/src/_locales/tr/main.json +++ b/src/_locales/tr/main.json @@ -20,6 +20,7 @@ "Custom Site Regex": "Özel Site Regex'i", "Exclusively use Custom Site Regex for website matching, ignoring built-in rules": "Yerleşik kuralları yok sayarak web sitesi eşleştirme için yalnızca Özel Site Regex'i kullan", "Input Query": "Girdi Sorgusu", + "Resize input box": "Giriş kutusunu yeniden boyutlandır", "Append Query": "Sorgu Ekle", "Prepend Query": "Sorgu Ön Eki", "Wechat Pay": "Wechat Pay", diff --git a/src/_locales/zh-hans/main.json b/src/_locales/zh-hans/main.json index 8a02d2d3f..d2b9ae705 100644 --- a/src/_locales/zh-hans/main.json +++ b/src/_locales/zh-hans/main.json @@ -20,6 +20,7 @@ "Custom Site Regex": "自定义站点正则匹配", "Exclusively use Custom Site Regex for website matching, ignoring built-in rules": "只使用自定义站点正则匹配, 忽略内置站点规则", "Input Query": "输入的查询选择器", + "Resize input box": "调整输入框大小", "Append Query": "挂载到末尾的查询选择器", "Prepend Query": "插入到开头的查询选择器", "Wechat Pay": "微信打赏", diff --git a/src/_locales/zh-hant/main.json b/src/_locales/zh-hant/main.json index 8b1c632d8..d2c41a721 100644 --- a/src/_locales/zh-hant/main.json +++ b/src/_locales/zh-hant/main.json @@ -20,6 +20,7 @@ "Custom Site Regex": "自訂網站正規表達式", "Exclusively use Custom Site Regex for website matching, ignoring built-in rules": "僅使用自訂網站正規表達式進行網站配對,忽略內建規則", "Input Query": "輸入查詢選擇器", + "Resize input box": "調整輸入框大小", "Append Query": "附加查詢選擇器", "Prepend Query": "插入查詢選擇器", "Wechat Pay": "微信支付贊助", diff --git a/src/components/ConversationCard/index.jsx b/src/components/ConversationCard/index.jsx index f7137506e..ca8765af4 100644 --- a/src/components/ConversationCard/index.jsx +++ b/src/components/ConversationCard/index.jsx @@ -572,7 +572,7 @@ function ConversationCard(props) { className="markdown-body" style={ props.notClampSize - ? { flexGrow: 1 } + ? { flexGrow: 1, minHeight: 0 } : { maxHeight: windowSize[1] * 0.55 + 'px', resize: 'vertical' } } > diff --git a/src/components/InputBox/index.jsx b/src/components/InputBox/index.jsx index c0adcc9ed..d50f95f2f 100644 --- a/src/components/InputBox/index.jsx +++ b/src/components/InputBox/index.jsx @@ -1,51 +1,89 @@ -import { useEffect, useRef, useState } from 'react' +import { useEffect, useLayoutEffect, useRef, useState } from 'react' import PropTypes from 'prop-types' -import { isFirefox, isMobile, isSafari, updateRefHeight } from '../../utils' +import { isMobile, updateRefHeight } from '../../utils' import { useTranslation } from 'react-i18next' import { getUserConfig } from '../../config/index.mjs' +import { + clampInputHeight, + DEFAULT_INPUT_HEIGHT, + getKeyboardInputHeight, + getPointerInputHeight, + MIN_CONVERSATION_HEIGHT, + MIN_INPUT_HEIGHT, +} from './resize.mjs' export function InputBox({ onSubmit, enabled, postMessage, reverseResizeDir }) { const { t } = useTranslation() const [value, setValue] = useState('') - const reverseDivRef = useRef(null) const inputRef = useRef(null) const resizedRef = useRef(false) - const [internalReverseResizeDir, setInternalReverseResizeDir] = useState(reverseResizeDir) + const resizeHandleRef = useRef(null) + const resizeStartRef = useRef(null) + const hasTopResizeHandle = Boolean(reverseResizeDir && !isMobile()) + const [inputHeight, setInputHeight] = useState(DEFAULT_INPUT_HEIGHT) + const [maxInputHeight, setMaxInputHeight] = useState(DEFAULT_INPUT_HEIGHT) useEffect(() => { - setInternalReverseResizeDir( - !isSafari() && !isFirefox() && !isMobile() ? internalReverseResizeDir : false, - ) + inputRef.current.focus() }, []) - const virtualInputRef = internalReverseResizeDir ? reverseDivRef : inputRef - useEffect(() => { - inputRef.current.focus() + if (hasTopResizeHandle) return + const input = inputRef.current const onResizeY = () => { - if (virtualInputRef.current.h !== virtualInputRef.current.offsetHeight) { - virtualInputRef.current.h = virtualInputRef.current.offsetHeight + if (input.h !== input.offsetHeight) { + input.h = input.offsetHeight if (!resizedRef.current) { resizedRef.current = true - virtualInputRef.current.style.maxHeight = '' + input.style.maxHeight = '' } } } - virtualInputRef.current.h = virtualInputRef.current.offsetHeight - virtualInputRef.current.addEventListener('mousemove', onResizeY) - }, []) + input.h = input.offsetHeight + input.addEventListener('mousemove', onResizeY) + return () => input.removeEventListener('mousemove', onResizeY) + }, [hasTopResizeHandle]) useEffect(() => { - if (!resizedRef.current) { - if (!internalReverseResizeDir) { - updateRefHeight(inputRef) - virtualInputRef.current.h = virtualInputRef.current.offsetHeight - virtualInputRef.current.style.maxHeight = '160px' - } + if (!resizedRef.current && !hasTopResizeHandle) { + updateRefHeight(inputRef) + inputRef.current.h = inputRef.current.offsetHeight + inputRef.current.style.maxHeight = `${DEFAULT_INPUT_HEIGHT}px` } }) + const getMaxInputHeight = () => { + const container = inputRef.current?.closest('.gpt-inner') + const conversation = container?.querySelector('.markdown-body') + const resizeHandle = resizeHandleRef.current + + if (!container || !conversation || !resizeHandle) return DEFAULT_INPUT_HEIGHT + + return Math.max( + MIN_INPUT_HEIGHT, + container.clientHeight - + conversation.offsetTop - + resizeHandle.offsetHeight - + MIN_CONVERSATION_HEIGHT, + ) + } + + useLayoutEffect(() => { + if (!hasTopResizeHandle) return + + const updateResizeBounds = () => { + const maxHeight = getMaxInputHeight() + if (resizeStartRef.current) resizeStartRef.current.maxHeight = maxHeight + setMaxInputHeight(maxHeight) + setInputHeight((height) => clampInputHeight(height, maxHeight)) + } + + updateResizeBounds() + window.addEventListener('resize', updateResizeBounds) + return () => window.removeEventListener('resize', updateResizeBounds) + }, [hasTopResizeHandle]) + useEffect(() => { if (enabled) getUserConfig().then((config) => { @@ -67,29 +105,90 @@ export function InputBox({ onSubmit, enabled, postMessage, reverseResizeDir }) { } } + const handleResizePointerDown = (e) => { + if (!e.isPrimary || e.button !== 0) return + + e.currentTarget.focus() + e.preventDefault() + const maxHeight = getMaxInputHeight() + setMaxInputHeight(maxHeight) + resizeStartRef.current = { + pointerId: e.pointerId, + height: inputRef.current.offsetHeight, + y: e.clientY, + maxHeight, + } + e.currentTarget.setPointerCapture(e.pointerId) + } + + const handleResizePointerMove = (e) => { + const resizeStart = resizeStartRef.current + if (!resizeStart || resizeStart.pointerId !== e.pointerId) return + + e.preventDefault() + setInputHeight( + getPointerInputHeight(resizeStart.height, resizeStart.y, e.clientY, resizeStart.maxHeight), + ) + } + + const stopResizing = (e) => { + if (resizeStartRef.current?.pointerId !== e.pointerId) return + + const restoreInputFocus = document.activeElement === e.currentTarget + resizeStartRef.current = null + if (e.currentTarget.hasPointerCapture(e.pointerId)) { + e.currentTarget.releasePointerCapture(e.pointerId) + } + if (restoreInputFocus) inputRef.current.focus() + } + + const handleResizeKeyDown = (e) => { + if (!['ArrowUp', 'ArrowDown', 'Home', 'End'].includes(e.key)) return + + const maxHeight = getMaxInputHeight() + e.preventDefault() + setMaxInputHeight(maxHeight) + setInputHeight( + (height) => getKeyboardInputHeight(height, e.key, maxHeight, e.shiftKey) ?? height, + ) + } + return (
+ {hasTopResizeHandle && ( +
+ )}