Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/_locales/de/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/_locales/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/_locales/es/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/_locales/fr/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/_locales/in/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/_locales/it/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/_locales/ja/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -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ペイ",
Expand Down
1 change: 1 addition & 0 deletions src/_locales/ko/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "위챗 페이",
Expand Down
1 change: 1 addition & 0 deletions src/_locales/pt/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/_locales/ru/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -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-платеж",
Expand Down
1 change: 1 addition & 0 deletions src/_locales/tr/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/_locales/zh-hans/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "微信打赏",
Expand Down
1 change: 1 addition & 0 deletions src/_locales/zh-hant/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "微信支付贊助",
Expand Down
2 changes: 1 addition & 1 deletion src/components/ConversationCard/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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' }
}
>
Expand Down
171 changes: 135 additions & 36 deletions src/components/InputBox/index.jsx
Original file line number Diff line number Diff line change
@@ -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])
Comment thread
PeterDaveHello marked this conversation as resolved.

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`
}
})
Comment thread
PeterDaveHello marked this conversation as resolved.
Comment thread
PeterDaveHello marked this conversation as resolved.

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])
Comment thread
PeterDaveHello marked this conversation as resolved.

useEffect(() => {
if (enabled)
getUserConfig().then((config) => {
Comment thread
PeterDaveHello marked this conversation as resolved.
Expand All @@ -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,
}
Comment thread
PeterDaveHello marked this conversation as resolved.
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),
)
}
Comment thread
PeterDaveHello marked this conversation as resolved.

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()
}
Comment thread
qodo-code-review[bot] marked this conversation as resolved.

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,
)
}
Comment thread
qodo-code-review[bot] marked this conversation as resolved.

return (
<div className="input-box">
{hasTopResizeHandle && (
<div
ref={resizeHandleRef}
className="input-resize-handle"
role="separator"
aria-controls="chatgptbox-independent-input"
aria-label={t('Resize input box')}
aria-orientation="horizontal"
aria-valuemax={maxInputHeight}
aria-valuemin={MIN_INPUT_HEIGHT}
aria-valuenow={inputHeight}
tabIndex={0}
onKeyDown={handleResizeKeyDown}
onLostPointerCapture={stopResizing}
onPointerCancel={stopResizing}
onPointerDown={handleResizePointerDown}
onPointerMove={handleResizePointerMove}
onPointerUp={stopResizing}
/>
)}
<div
ref={reverseDivRef}
style={
internalReverseResizeDir && {
transform: 'rotateX(180deg)',
resize: 'vertical',
overflow: 'hidden',
minHeight: '160px',
}
}
className={hasTopResizeHandle ? 'input-resize-content' : undefined}
style={hasTopResizeHandle ? { height: `${inputHeight}px` } : undefined}
>
Comment thread
PeterDaveHello marked this conversation as resolved.
<textarea
id={hasTopResizeHandle ? 'chatgptbox-independent-input' : undefined}
dir="auto"
ref={inputRef}
disabled={false}
className="interact-input"
style={
internalReverseResizeDir
? { transform: 'rotateX(180deg)', resize: 'none' }
: { resize: 'vertical', minHeight: '70px' }
}
style={{
resize: hasTopResizeHandle ? 'none' : 'vertical',
minHeight: `${MIN_INPUT_HEIGHT}px`,
}}
placeholder={
enabled
? t('Type your question here\nEnter to send, shift + enter to break line')
Expand Down
32 changes: 32 additions & 0 deletions src/components/InputBox/resize.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
export const DEFAULT_INPUT_HEIGHT = 160
export const MIN_INPUT_HEIGHT = 70
export const MIN_CONVERSATION_HEIGHT = 80

const INPUT_RESIZE_KEY_STEP = 10
const INPUT_RESIZE_KEY_LARGE_STEP = 40

export function clampInputHeight(height, maxHeight) {
const safeMaxHeight = Math.max(MIN_INPUT_HEIGHT, maxHeight)
return Math.min(Math.max(MIN_INPUT_HEIGHT, height), safeMaxHeight)
}

export function getPointerInputHeight(startHeight, startY, currentY, maxHeight) {
return clampInputHeight(startHeight + startY - currentY, maxHeight)
}

export function getKeyboardInputHeight(currentHeight, key, maxHeight, useLargeStep = false) {
const step = useLargeStep ? INPUT_RESIZE_KEY_LARGE_STEP : INPUT_RESIZE_KEY_STEP

switch (key) {
case 'ArrowUp':
return clampInputHeight(currentHeight + step, maxHeight)
case 'ArrowDown':
return clampInputHeight(currentHeight - step, maxHeight)
case 'Home':
return MIN_INPUT_HEIGHT
case 'End':
return Math.max(MIN_INPUT_HEIGHT, maxHeight)
default:
return null
}
}
Loading