136 lines
4.3 KiB
TypeScript
136 lines
4.3 KiB
TypeScript
"use client";
|
|
|
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
import { getChatInputHeight, setChatInputHeight } from "@/lib/local-storage";
|
|
|
|
const MIN_HEIGHT = 110;
|
|
const DEFAULT_HEIGHT = 110;
|
|
const MAX_ABSOLUTE = 400;
|
|
|
|
function clampHeight(value: number): number {
|
|
const maxHeight = Math.min(window.innerHeight * 0.6, MAX_ABSOLUTE);
|
|
return Math.min(Math.max(value, MIN_HEIGHT), maxHeight);
|
|
}
|
|
|
|
/** Measure the natural content height of an editor inside its container. */
|
|
function measureContentHeight(container: HTMLElement, content: HTMLElement): number {
|
|
const chrome = container.offsetHeight - content.offsetHeight;
|
|
const prev = content.style.height;
|
|
content.style.height = "0px";
|
|
const naturalHeight = content.scrollHeight;
|
|
content.style.height = prev;
|
|
return clampHeight(naturalHeight + chrome);
|
|
}
|
|
|
|
/** When the input grows, scroll the chat down so messages don't shift up. */
|
|
function compensateChatScroll(delta: number) {
|
|
if (delta <= 0) return;
|
|
const el = document.querySelector<HTMLElement>(".chat-message-list");
|
|
if (el) el.scrollTop += delta;
|
|
}
|
|
|
|
export function useResizableInput(
|
|
sessionId: string | undefined,
|
|
getContentElement?: () => HTMLElement | null,
|
|
) {
|
|
const [height, setHeight] = useState(() => {
|
|
if (typeof window === "undefined" || !sessionId) return DEFAULT_HEIGHT;
|
|
const saved = getChatInputHeight(sessionId);
|
|
return saved ? clampHeight(saved) : DEFAULT_HEIGHT;
|
|
});
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
const isDragging = useRef(false);
|
|
const startY = useRef(0);
|
|
const startHeight = useRef(0);
|
|
// Restore height when session changes
|
|
const prevSessionRef = useRef(sessionId);
|
|
useEffect(() => {
|
|
if (sessionId === prevSessionRef.current) return;
|
|
prevSessionRef.current = sessionId;
|
|
/* eslint-disable react-hooks/set-state-in-effect -- syncing from localStorage on session switch */
|
|
const saved = sessionId ? getChatInputHeight(sessionId) : null;
|
|
setHeight(saved ? clampHeight(saved) : DEFAULT_HEIGHT);
|
|
/* eslint-enable react-hooks/set-state-in-effect */
|
|
}, [sessionId]);
|
|
|
|
// Persist height after drag ends
|
|
const persistHeight = useCallback(
|
|
(h: number) => {
|
|
if (sessionId) {
|
|
setChatInputHeight(sessionId, h);
|
|
}
|
|
},
|
|
[sessionId],
|
|
);
|
|
|
|
const handleMouseDown = useCallback(
|
|
(e: React.MouseEvent) => {
|
|
e.preventDefault();
|
|
isDragging.current = true;
|
|
startY.current = e.clientY;
|
|
startHeight.current = height;
|
|
document.body.style.cursor = "ns-resize";
|
|
document.body.style.userSelect = "none";
|
|
},
|
|
[height],
|
|
);
|
|
|
|
const resetHeight = useCallback(() => {
|
|
setHeight(DEFAULT_HEIGHT);
|
|
persistHeight(DEFAULT_HEIGHT);
|
|
}, [persistHeight]);
|
|
|
|
useEffect(() => {
|
|
const handleMouseMove = (e: MouseEvent) => {
|
|
if (!isDragging.current) return;
|
|
const delta = startY.current - e.clientY;
|
|
setHeight(clampHeight(startHeight.current + delta));
|
|
};
|
|
|
|
const handleMouseUp = () => {
|
|
if (!isDragging.current) return;
|
|
isDragging.current = false;
|
|
document.body.style.cursor = "";
|
|
document.body.style.userSelect = "";
|
|
// Persist the final height after drag
|
|
setHeight((h) => {
|
|
persistHeight(h);
|
|
return h;
|
|
});
|
|
};
|
|
|
|
document.addEventListener("mousemove", handleMouseMove);
|
|
document.addEventListener("mouseup", handleMouseUp);
|
|
return () => {
|
|
document.removeEventListener("mousemove", handleMouseMove);
|
|
document.removeEventListener("mouseup", handleMouseUp);
|
|
};
|
|
}, [persistHeight]);
|
|
|
|
const autoExpand = useCallback(() => {
|
|
const container = containerRef.current;
|
|
const content = getContentElement?.();
|
|
if (!container || !content || isDragging.current) return;
|
|
const raw = measureContentHeight(container, content);
|
|
// Never shrink below DEFAULT_HEIGHT during auto-expand;
|
|
// MIN_HEIGHT is only for manual drag-resize.
|
|
const needed = Math.max(raw, DEFAULT_HEIGHT);
|
|
if (needed !== height) {
|
|
compensateChatScroll(needed - height);
|
|
setHeight(needed);
|
|
persistHeight(needed);
|
|
}
|
|
}, [height, persistHeight, getContentElement]);
|
|
|
|
return {
|
|
height,
|
|
resetHeight,
|
|
autoExpand,
|
|
containerRef,
|
|
resizeHandleProps: {
|
|
onMouseDown: handleMouseDown,
|
|
onDoubleClick: resetHeight,
|
|
},
|
|
};
|
|
}
|