fix(chat): prevent textarea content under bottom toolbar

This commit is contained in:
han
2026-03-13 10:31:05 +08:00
parent bdbfa9ce97
commit cde5be5958

View File

@@ -1,6 +1,13 @@
"use client" "use client"
import { useCallback, useEffect, useMemo, useRef, useState } from "react" import {
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from "react"
import { TauriEvent } from "@tauri-apps/api/event" import { TauriEvent } from "@tauri-apps/api/event"
import { getCurrentWebview } from "@tauri-apps/api/webview" import { getCurrentWebview } from "@tauri-apps/api/webview"
import { open } from "@tauri-apps/plugin-dialog" import { open } from "@tauri-apps/plugin-dialog"
@@ -1079,6 +1086,9 @@ export function MessageInput({
const hasImageAttachments = imageAttachments.length > 0 const hasImageAttachments = imageAttachments.length > 0
const hasResourceAttachments = resourceAttachments.length > 0 const hasResourceAttachments = resourceAttachments.length > 0
const bottomBarRef = useRef<HTMLDivElement | null>(null)
const actionAreaRef = useRef<HTMLDivElement | null>(null)
const actionButtonRef = useRef<HTMLButtonElement | null>(null)
const topPaddingClass = const topPaddingClass =
hasImageAttachments && hasResourceAttachments hasImageAttachments && hasResourceAttachments
? "pt-[6.25rem]" ? "pt-[6.25rem]"
@@ -1087,9 +1097,45 @@ export function MessageInput({
: hasResourceAttachments : hasResourceAttachments
? "pt-10" ? "pt-10"
: "pt-3" : "pt-3"
const bottomPaddingClass = "pb-10" const [bottomPaddingPx, setBottomPaddingPx] = useState(40)
const showDragActive = isDragActive && !disabled const showDragActive = isDragActive && !disabled
useLayoutEffect(() => {
const bottomOffsetPx = 8 // Tailwind `bottom-2`
const bufferPx = 6
const bottomBar = bottomBarRef.current
const actionArea = actionAreaRef.current
const actionButton = actionButtonRef.current
if (!bottomBar && !actionArea && !actionButton) return
const measure = () => {
const bottomBarHeight = bottomBar?.getBoundingClientRect().height ?? 0
const actionAreaHeight = actionArea?.getBoundingClientRect().height ?? 0
const actionButtonHeight =
actionButton?.getBoundingClientRect().height ?? 0
const next = Math.ceil(
Math.max(bottomBarHeight, actionAreaHeight, actionButtonHeight) +
bottomOffsetPx +
bufferPx
)
setBottomPaddingPx((prev) => (Math.abs(prev - next) < 1 ? prev : next))
}
measure()
const observer = new ResizeObserver(() => {
measure()
})
if (bottomBar) observer.observe(bottomBar)
if (actionArea) observer.observe(actionArea)
if (actionButton) observer.observe(actionButton)
return () => {
observer.disconnect()
}
}, [hasAnySelector, isEditingQueueItem, isPrompting])
const selectorItems = ( const selectorItems = (
<> <>
{showConfigLoading && ( {showConfigLoading && (
@@ -1141,11 +1187,11 @@ export function MessageInput({
onPaste={handlePaste} onPaste={handlePaste}
onFocus={onFocus} onFocus={onFocus}
placeholder={resolvedPlaceholder} placeholder={resolvedPlaceholder}
style={{ paddingBottom: bottomPaddingPx }}
className={cn( className={cn(
"text-sm pr-12 resize-none bg-transparent", "text-sm pr-12 resize-none bg-transparent",
showDragActive && "ring-1 ring-primary/40", showDragActive && "ring-1 ring-primary/40",
topPaddingClass, topPaddingClass,
bottomPaddingClass,
className className
)} )}
autoFocus={autoFocus} autoFocus={autoFocus}
@@ -1211,7 +1257,15 @@ export function MessageInput({
{t("dropFilesToAttach")} {t("dropFilesToAttach")}
</div> </div>
)} )}
<div className="@container absolute left-2 right-24 bottom-2"> <div
className="pointer-events-none absolute left-px right-px bottom-px z-10 rounded-b-xl bg-background"
style={{ height: bottomPaddingPx }}
aria-hidden="true"
/>
<div
ref={bottomBarRef}
className="@container absolute left-2 right-24 bottom-2 z-20"
>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Button <Button
onClick={handlePickFiles} onClick={handlePickFiles}
@@ -1248,7 +1302,10 @@ export function MessageInput({
</div> </div>
</div> </div>
{isEditingQueueItem ? ( {isEditingQueueItem ? (
<div className="absolute right-2 bottom-2 flex items-center gap-1"> <div
ref={actionAreaRef}
className="absolute right-2 bottom-2 z-20 flex items-center gap-1"
>
<Button <Button
onClick={onCancelQueueEdit} onClick={onCancelQueueEdit}
variant="ghost" variant="ghost"
@@ -1268,7 +1325,10 @@ export function MessageInput({
</Button> </Button>
</div> </div>
) : isPrompting && onCancel ? ( ) : isPrompting && onCancel ? (
<div className="absolute right-2 bottom-2 flex items-center gap-1"> <div
ref={actionAreaRef}
className="absolute right-2 bottom-2 z-20 flex items-center gap-1"
>
<Button <Button
onClick={handleSend} onClick={handleSend}
disabled={!hasSendableContent} disabled={!hasSendableContent}
@@ -1290,10 +1350,11 @@ export function MessageInput({
</div> </div>
) : ( ) : (
<Button <Button
ref={actionButtonRef}
onClick={handleSend} onClick={handleSend}
disabled={disabled || !hasSendableContent} disabled={disabled || !hasSendableContent}
size="icon" size="icon"
className="absolute right-2 bottom-2" className="absolute right-2 bottom-2 z-20"
title={t("send")} title={t("send")}
> >
<Send className="h-4 w-4" /> <Send className="h-4 w-4" />