feat: chat panel UE done

This commit is contained in:
butterfly
2024-04-18 12:27:44 +08:00
parent 51a1d9f92a
commit b3559f99a2
39 changed files with 953 additions and 447 deletions

View File

@@ -0,0 +1,103 @@
import { RefObject, useState } from "react";
import { useDebouncedCallback } from "use-debounce";
export interface Options {
containerRef: RefObject<HTMLDivElement | null>;
delay?: number;
offsetDistance?: number;
}
export enum Orientation {
left,
right,
bottom,
top,
}
export type X = Orientation.left | Orientation.right;
export type Y = Orientation.top | Orientation.bottom;
interface Position {
id: string;
poi: {
targetH: number;
targetW: number;
distanceToRightBoundary: number;
distanceToLeftBoundary: number;
distanceToTopBoundary: number;
distanceToBottomBoundary: number;
overlapPositions: Record<Orientation, boolean>;
relativePosition: [X, Y];
};
}
export default function useRelativePosition({
containerRef,
delay = 100,
offsetDistance = 0,
}: Options) {
const [position, setPosition] = useState<Position | undefined>();
const getRelativePosition = useDebouncedCallback(
(target: HTMLDivElement, id: string) => {
if (!containerRef.current) {
return;
}
const {
x: targetX,
y: targetY,
width: targetW,
height: targetH,
} = target.getBoundingClientRect();
const {
x: containerX,
y: containerY,
width: containerWidth,
height: containerHeight,
} = containerRef.current.getBoundingClientRect();
const distanceToRightBoundary =
containerX + containerWidth - (targetX + targetW) - offsetDistance;
const distanceToLeftBoundary = targetX - containerX - offsetDistance;
const distanceToTopBoundary = targetY - containerY - offsetDistance;
const distanceToBottomBoundary =
containerY + containerHeight - (targetY + targetH) - offsetDistance;
setPosition({
id,
poi: {
targetW: targetW + 2 * offsetDistance,
targetH: targetH + 2 * offsetDistance,
distanceToRightBoundary,
distanceToLeftBoundary,
distanceToTopBoundary,
distanceToBottomBoundary,
overlapPositions: {
[Orientation.left]: distanceToLeftBoundary <= 0,
[Orientation.top]: distanceToTopBoundary <= 0,
[Orientation.right]: distanceToRightBoundary <= 0,
[Orientation.bottom]: distanceToBottomBoundary <= 0,
},
relativePosition: [
distanceToLeftBoundary <= distanceToRightBoundary
? Orientation.left
: Orientation.right,
distanceToTopBoundary <= distanceToBottomBoundary
? Orientation.top
: Orientation.bottom,
],
},
});
},
delay,
{
leading: true,
trailing: true,
},
);
return {
getRelativePosition,
position,
};
}

34
app/hooks/useRows.ts Normal file
View File

@@ -0,0 +1,34 @@
import { useState } from "react";
import { useDebouncedCallback } from "use-debounce";
import { autoGrowTextArea } from "../utils";
import useMobileScreen from "./useMobileScreen";
export default function useRows({
inputRef,
}: {
inputRef: React.RefObject<HTMLTextAreaElement>;
}) {
const [inputRows, setInputRows] = useState(2);
const isMobileScreen = useMobileScreen();
const measure = useDebouncedCallback(
() => {
const rows = inputRef.current ? autoGrowTextArea(inputRef.current) : 1;
const inputRows = Math.min(
20,
Math.max(2 + (isMobileScreen ? -1 : 1), rows),
);
setInputRows(inputRows);
},
100,
{
leading: true,
trailing: true,
},
);
return {
inputRows,
measure,
};
}

View File

@@ -1,11 +1,17 @@
import { RefObject, useEffect, useState } from "react";
import { RefObject, useEffect, useRef, useState } from "react";
export default function useScrollToBottom(
scrollRef: RefObject<HTMLDivElement>,
detach: boolean = false,
) {
// for auto-scroll
const detach = scrollRef?.current
? Math.abs(
scrollRef.current.scrollHeight -
(scrollRef.current.scrollTop + scrollRef.current.clientHeight),
) <= 1
: false;
const initScrolled = useRef(false);
// for auto-scroll
const [autoScroll, setAutoScroll] = useState(true);
function scrollDomToBottom() {
const dom = scrollRef.current;
@@ -19,10 +25,11 @@ export default function useScrollToBottom(
// auto scroll
useEffect(() => {
if (autoScroll && !detach) {
if (autoScroll && !detach && !initScrolled.current) {
scrollDomToBottom();
initScrolled.current = true;
}
});
}, [autoScroll, detach]);
return {
scrollRef,

View File

@@ -0,0 +1,29 @@
import { useEffect, useState } from "react";
export default function useShowPromptHint<RenderPompt>(props: {
prompts: RenderPompt[];
}) {
const [internalPrompts, setInternalPrompts] = useState<RenderPompt[]>([]);
const [notShowPrompt, setNotShowPrompt] = useState(true);
useEffect(() => {
if (props.prompts.length !== 0) {
setInternalPrompts(props.prompts);
window.setTimeout(() => {
setNotShowPrompt(false);
}, 50);
return;
}
setNotShowPrompt(true);
window.setTimeout(() => {
setInternalPrompts(props.prompts);
}, 300);
}, [props.prompts]);
return {
notShowPrompt,
internalPrompts,
};
}

View File

@@ -1,4 +1,4 @@
import { useLayoutEffect, useMemo, useRef } from "react";
import { useLayoutEffect, useRef } from "react";
type Size = {
width: number;
@@ -7,15 +7,6 @@ type Size = {
export function useWindowSize(callback: (size: Size) => void) {
const callbackRef = useRef<typeof callback>();
const hascalled = useRef(false);
if (typeof window !== "undefined" && !hascalled.current) {
callback({
width: window.innerWidth,
height: window.innerHeight,
});
hascalled.current = true;
}
callbackRef.current = callback;
@@ -29,6 +20,11 @@ export function useWindowSize(callback: (size: Size) => void) {
window.addEventListener("resize", onResize);
callback({
width: window.innerWidth,
height: window.innerHeight,
});
return () => {
window.removeEventListener("resize", onResize);
};