Merge branch 'develop' of https://gitea.vvzvlad.xyz/vvzvlad/gitmost into develop
This commit is contained in:
@@ -1,4 +1,22 @@
|
||||
import { atom } from "jotai";
|
||||
import { atomWithStorage } from "jotai/utils";
|
||||
|
||||
/**
|
||||
* Persisted floating AI chat window geometry (position + size). Held in
|
||||
* localStorage so a drag/resize survives a full page reload. `null` means
|
||||
* "never placed yet" — the window then computes an initial top-right placement.
|
||||
* On restore the value is clamped to the current viewport (see AiChatWindow).
|
||||
*/
|
||||
export type AiChatWindowGeom = {
|
||||
left: number;
|
||||
top: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
export const aiChatWindowGeomAtom = atomWithStorage<AiChatWindowGeom | null>(
|
||||
"ai-chat-window-geom",
|
||||
null,
|
||||
);
|
||||
|
||||
/**
|
||||
* The currently selected chat id. `null` means a fresh (not-yet-created) chat:
|
||||
|
||||
@@ -26,6 +26,7 @@ import { useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
activeAiChatIdAtom,
|
||||
aiChatWindowOpenAtom,
|
||||
aiChatWindowGeomAtom,
|
||||
aiChatDraftAtom,
|
||||
selectedAiRoleIdAtom,
|
||||
} from "@/features/ai-chat/atoms/ai-chat-atom.ts";
|
||||
@@ -123,15 +124,13 @@ export default function AiChatWindow() {
|
||||
minimizedRef.current = minimized;
|
||||
|
||||
const winRef = useRef<HTMLDivElement>(null);
|
||||
// Live window geometry (position + size); initialized lazily on first open so
|
||||
// it is anchored to the current viewport (top-right corner). Kept in state so
|
||||
// a user resize survives close/reopen and can be re-clamped to the viewport.
|
||||
const [geom, setGeom] = useState<{
|
||||
left: number;
|
||||
top: number;
|
||||
width: number;
|
||||
height: number;
|
||||
} | null>(null);
|
||||
// Live window geometry (position + size); persisted to localStorage so a
|
||||
// drag/resize survives a full page reload (and close/reopen). `null` means
|
||||
// "never placed yet" — the layout effect below then computes an initial
|
||||
// top-right placement anchored to the current viewport, and on restore it is
|
||||
// re-clamped to the viewport (so a placement saved on a larger screen is not
|
||||
// left partly off-screen).
|
||||
const [geom, setGeom] = useAtom(aiChatWindowGeomAtom);
|
||||
|
||||
// Track whether we are awaiting the id of a just-created (new) chat, so we
|
||||
// can adopt it once the chat list refreshes after the first turn finishes.
|
||||
@@ -416,6 +415,10 @@ export default function AiChatWindow() {
|
||||
useEffect(() => {
|
||||
if (!windowOpen || minimized) return;
|
||||
const el = winRef.current;
|
||||
// `geom` is in the deps so this re-runs once geometry is settled and the
|
||||
// window is actually rendered (on the first open `geom` is still null on the
|
||||
// render that flips windowOpen, so winRef.current is null then — without the
|
||||
// geom dep the observer would never attach and resizes would not persist).
|
||||
if (!el) return;
|
||||
const ro = new ResizeObserver(() => {
|
||||
const width = el.offsetWidth;
|
||||
@@ -427,7 +430,7 @@ export default function AiChatWindow() {
|
||||
});
|
||||
ro.observe(el);
|
||||
return () => ro.disconnect();
|
||||
}, [windowOpen, minimized]);
|
||||
}, [windowOpen, minimized, geom !== null]);
|
||||
|
||||
const startDrag = useCallback((e: React.MouseEvent): void => {
|
||||
// Ignore drags that originate on a button (minimize/close/new chat).
|
||||
|
||||
Reference in New Issue
Block a user