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 && ( +
+ )}