fix(desktop): keep ai chat input within viewport
This commit is contained in:
parent
99081f8e7f
commit
14c77295ac
|
|
@ -0,0 +1,38 @@
|
|||
import { describe, expect, it } from "vitest"
|
||||
|
||||
import { getBottomPanelContainerStyle } from "./ChatBottomPanel.styles"
|
||||
|
||||
describe("getBottomPanelContainerStyle", () => {
|
||||
it("returns no extra transform after the chat has messages", () => {
|
||||
expect(
|
||||
getBottomPanelContainerStyle({
|
||||
centerInputOnEmpty: true,
|
||||
hasMessages: true,
|
||||
visualOffsetY: "clamp(-10vh, -8vh, -6vh)",
|
||||
}),
|
||||
).toBeUndefined()
|
||||
})
|
||||
|
||||
it("uses the base centered transform when no visual offset is provided", () => {
|
||||
expect(
|
||||
getBottomPanelContainerStyle({
|
||||
centerInputOnEmpty: true,
|
||||
hasMessages: false,
|
||||
}),
|
||||
).toEqual({
|
||||
transform: "translateY(calc(100% + 1rem))",
|
||||
})
|
||||
})
|
||||
|
||||
it("merges the visual offset into the centered transform", () => {
|
||||
expect(
|
||||
getBottomPanelContainerStyle({
|
||||
centerInputOnEmpty: true,
|
||||
hasMessages: false,
|
||||
visualOffsetY: "clamp(-10vh, -8vh, -6vh)",
|
||||
}),
|
||||
).toEqual({
|
||||
transform: "translateY(calc(100% + 1rem + clamp(-10vh, -8vh, -6vh)))",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import type { CSSProperties } from "react"
|
||||
|
||||
const EMPTY_BOTTOM_PANEL_BASE_OFFSET = "100% + 1rem"
|
||||
|
||||
const formatVisualOffsetY = (visualOffsetY: string | number) =>
|
||||
typeof visualOffsetY === "number" ? `${visualOffsetY}px` : visualOffsetY
|
||||
|
||||
interface BottomPanelContainerStyleOptions {
|
||||
centerInputOnEmpty?: boolean
|
||||
hasMessages: boolean
|
||||
visualOffsetY?: string | number
|
||||
}
|
||||
|
||||
export const getBottomPanelContainerStyle = ({
|
||||
centerInputOnEmpty,
|
||||
hasMessages,
|
||||
visualOffsetY,
|
||||
}: BottomPanelContainerStyleOptions): CSSProperties | undefined => {
|
||||
if (!centerInputOnEmpty || hasMessages) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (visualOffsetY == null) {
|
||||
return {
|
||||
transform: `translateY(calc(${EMPTY_BOTTOM_PANEL_BASE_OFFSET}))`,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
transform: `translateY(calc(${EMPTY_BOTTOM_PANEL_BASE_OFFSET} + ${formatVisualOffsetY(visualOffsetY)}))`,
|
||||
}
|
||||
}
|
||||
|
|
@ -10,10 +10,12 @@ import { RateLimitNotice } from "~/modules/ai-chat/components/layouts/RateLimitN
|
|||
|
||||
import type { ShortcutData } from "../../editor"
|
||||
import { useSendAIShortcut } from "../../hooks/useSendAIShortcut"
|
||||
import { getBottomPanelContainerStyle } from "./ChatBottomPanel.styles"
|
||||
|
||||
interface ChatBottomPanelProps {
|
||||
hasMessages: boolean
|
||||
centerInputOnEmpty?: boolean
|
||||
visualOffsetY?: string | number
|
||||
shouldShowInterruptionNotice: boolean
|
||||
rateLimitMessage: string | null
|
||||
isRateLimited: boolean
|
||||
|
|
@ -27,6 +29,7 @@ interface ChatBottomPanelProps {
|
|||
export const ChatBottomPanel = ({
|
||||
hasMessages,
|
||||
centerInputOnEmpty,
|
||||
visualOffsetY,
|
||||
shouldShowInterruptionNotice,
|
||||
rateLimitMessage,
|
||||
isRateLimited,
|
||||
|
|
@ -39,6 +42,11 @@ export const ChatBottomPanel = ({
|
|||
const panelRef = useRef<HTMLDivElement | null>(null)
|
||||
const t = useI18n()
|
||||
const { sendAIShortcut } = useSendAIShortcut()
|
||||
const containerStyle = getBottomPanelContainerStyle({
|
||||
centerInputOnEmpty,
|
||||
hasMessages,
|
||||
visualOffsetY,
|
||||
})
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const element = panelRef.current
|
||||
|
|
@ -83,10 +91,9 @@ export const ChatBottomPanel = ({
|
|||
className={cn(
|
||||
"absolute z-10 mx-auto duration-500 ease-in-out",
|
||||
"inset-x-0 bottom-0 max-w-4xl px-4 pb-4",
|
||||
centerInputOnEmpty &&
|
||||
!hasMessages &&
|
||||
"bottom-1/2 translate-y-[calc(100%+1rem)] duration-200",
|
||||
centerInputOnEmpty && !hasMessages && "bottom-1/2 duration-200",
|
||||
)}
|
||||
style={containerStyle}
|
||||
>
|
||||
{shouldShowInterruptionNotice && (
|
||||
<m.div
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ import { ChatBottomPanel } from "./ChatBottomPanel"
|
|||
import { ChatMessageContainer } from "./ChatMessageContainer"
|
||||
|
||||
const draftMessages = new Map<string, EditorState>()
|
||||
const ChatInterfaceContent = ({ centerInputOnEmpty }: ChatInterfaceProps) => {
|
||||
const ChatInterfaceContent = ({ centerInputOnEmpty, visualOffsetY }: ChatInterfaceProps) => {
|
||||
const hasMessages = useHasMessages()
|
||||
const status = useChatStatus()
|
||||
const chatActions = useChatActions()
|
||||
|
|
@ -288,6 +288,7 @@ const ChatInterfaceContent = ({ centerInputOnEmpty }: ChatInterfaceProps) => {
|
|||
<ChatBottomPanel
|
||||
hasMessages={hasMessages}
|
||||
centerInputOnEmpty={centerInputOnEmpty}
|
||||
visualOffsetY={visualOffsetY}
|
||||
shouldShowInterruptionNotice={shouldShowInterruptionNotice}
|
||||
rateLimitMessage={rateLimitMessage}
|
||||
isRateLimited={isRateLimited}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { cn } from "@follow/utils"
|
||||
|
||||
import { AIChatRoot } from "~/modules/ai-chat/components/layouts/AIChatRoot"
|
||||
import { ChatPageHeader } from "~/modules/ai-chat/components/layouts/ChatHeader"
|
||||
import { ChatInterface } from "~/modules/ai-chat/components/layouts/ChatInterface"
|
||||
|
|
@ -7,10 +5,7 @@ import { ChatInterface } from "~/modules/ai-chat/components/layouts/ChatInterfac
|
|||
export const Component = () => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative flex h-screen w-full flex-col",
|
||||
"[&_[data-testid=chat-input-container]]:translate-y-32 [&_[data-testid=welcome-screen-header]]:-translate-y-24",
|
||||
)}
|
||||
className="relative flex h-screen w-full flex-col [&_[data-testid=welcome-screen-header]]:-translate-y-24"
|
||||
style={{ "--ai-chat-layout-width": "65rem" } as React.CSSProperties}
|
||||
>
|
||||
<AIChatRoot>
|
||||
|
|
|
|||
Loading…
Reference in New Issue