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"
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 { getCurrentWebview } from "@tauri-apps/api/webview"
import { open } from "@tauri-apps/plugin-dialog"
@@ -1079,6 +1086,9 @@ export function MessageInput({
const hasImageAttachments = imageAttachments.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 =
hasImageAttachments && hasResourceAttachments
? "pt-[6.25rem]"
@@ -1087,9 +1097,45 @@ export function MessageInput({
: hasResourceAttachments
? "pt-10"
: "pt-3"
const bottomPaddingClass = "pb-10"
const [bottomPaddingPx, setBottomPaddingPx] = useState(40)
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 = (
<>
{showConfigLoading && (
@@ -1141,11 +1187,11 @@ export function MessageInput({
onPaste={handlePaste}
onFocus={onFocus}
placeholder={resolvedPlaceholder}
style={{ paddingBottom: bottomPaddingPx }}
className={cn(
"text-sm pr-12 resize-none bg-transparent",
showDragActive && "ring-1 ring-primary/40",
topPaddingClass,
bottomPaddingClass,
className
)}
autoFocus={autoFocus}
@@ -1211,7 +1257,15 @@ export function MessageInput({
{t("dropFilesToAttach")}
</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">
<Button
onClick={handlePickFiles}
@@ -1248,7 +1302,10 @@ export function MessageInput({
</div>
</div>
{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
onClick={onCancelQueueEdit}
variant="ghost"
@@ -1268,7 +1325,10 @@ export function MessageInput({
</Button>
</div>
) : 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
onClick={handleSend}
disabled={!hasSendableContent}
@@ -1290,10 +1350,11 @@ export function MessageInput({
</div>
) : (
<Button
ref={actionButtonRef}
onClick={handleSend}
disabled={disabled || !hasSendableContent}
size="icon"
className="absolute right-2 bottom-2"
className="absolute right-2 bottom-2 z-20"
title={t("send")}
>
<Send className="h-4 w-4" />