fix(chat): prevent textarea content under bottom toolbar
This commit is contained in:
@@ -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" />
|
||||||
|
|||||||
Reference in New Issue
Block a user