diff --git a/client/package.json b/client/package.json index fb4acca60ecc..41f3d2e0cfbe 100644 --- a/client/package.json +++ b/client/package.json @@ -121,7 +121,7 @@ "@babel/plugin-transform-runtime": "^7.22.15", "@babel/preset-env": "^7.22.15", "@babel/preset-react": "^7.22.15", - "@babel/preset-typescript": "^7.22.15", + "@babel/preset-typescript": "^7.28.5", "@happy-dom/jest-environment": "^20.8.9", "@tanstack/react-query-devtools": "^4.29.0", "@testing-library/dom": "^9.3.0", diff --git a/client/src/components/Chat/Messages/MessageNav.tsx b/client/src/components/Chat/Messages/MessageNav.tsx new file mode 100644 index 000000000000..9fa2c420c771 --- /dev/null +++ b/client/src/components/Chat/Messages/MessageNav.tsx @@ -0,0 +1,595 @@ +import { memo, useState, useEffect, useCallback, useRef, useMemo } from 'react'; +import { ChevronUp, ChevronDown } from 'lucide-react'; +import { ContentTypes } from 'librechat-data-provider'; +import { HoverCard, HoverCardTrigger, HoverCardPortal, HoverCardContent } from '@librechat/client'; +import type { TMessage, TMessageContentParts } from 'librechat-data-provider'; +import { useGetMessagesByConvoId } from '~/data-provider'; +import { useMessagesConversation } from '~/Providers'; +import { useLocalize } from '~/hooks'; +import { cn } from '~/utils'; + +type MessageEntry = { + id: string; + isUser: boolean; + preview: string; +}; + +function extractPreviewFromContent(content?: TMessageContentParts[]): string { + if (!content) { + return ''; + } + for (const part of content) { + if (part.type !== ContentTypes.TEXT) { + continue; + } + const textField = part.text; + if (typeof textField === 'string' && textField.trim()) { + return textField; + } + if (textField && typeof textField === 'object' && textField.value?.trim()) { + return textField.value; + } + } + return ''; +} + +function buildEntry(id: string, msg: TMessage): MessageEntry { + const raw = msg.text?.trim() ? msg.text : extractPreviewFromContent(msg.content); + const trimmed = raw.trim(); + return { + id, + isUser: !!msg.isCreatedByUser, + preview: trimmed.slice(0, 80) + (trimmed.length > 80 ? '...' : ''), + }; +} + +const USER_TURN_SELECTOR = '.user-turn'; + +function buildFallbackEntry(node: HTMLElement, id: string): MessageEntry { + const isUser = node.querySelector(USER_TURN_SELECTOR) != null; + const trimmed = (node.textContent ?? '').trim(); + return { + id, + isUser, + preview: trimmed.slice(0, 80) + (trimmed.length > 80 ? '...' : ''), + }; +} + +function getMessageEntries(root: ParentNode, messagesById: Map): MessageEntry[] { + const nodes = root.querySelectorAll('.message-render'); + const entries: MessageEntry[] = []; + for (let i = 0; i < nodes.length; i++) { + const node = nodes[i]; + const id = node.id; + if (!id) { + continue; + } + const msg = messagesById.get(id); + entries.push(msg ? buildEntry(id, msg) : buildFallbackEntry(node, id)); + } + return entries; +} + +const JUMP_EPS = 4; +const SCROLL_DURATION = 400; + +function easeOutCubic(t: number): number { + return 1 - Math.pow(1 - t, 3); +} + +function readScrollMargin(el: HTMLElement | null): number { + if (!el) { + return 0; + } + const value = parseFloat(getComputedStyle(el).scrollMarginTop); + return Number.isFinite(value) ? value : 0; +} + +const indicatorButtonClasses = cn( + 'flex h-[5px] items-center justify-center rounded-sm', + 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-xheavy', +); + +const MessageIndicator = memo(function MessageIndicator({ + entry, + isActive, + label, + onSelect, +}: { + entry: MessageEntry; + isActive: boolean; + label: string; + onSelect: (id: string) => void; +}) { + return ( + + + + + + +

{entry.preview}

+
+
+
+ ); +}); + +const chevronButtonClasses = cn( + 'rounded-md p-0.5 text-text-tertiary transition-colors', + 'group-hover/nav:text-text-secondary group-focus-within/nav:text-text-secondary', + 'group-hover/nav:hover:text-text-primary', + 'group-hover/nav:disabled:opacity-30 group-focus-within/nav:disabled:opacity-30', + 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-xheavy', +); + +function MessageNav({ scrollableRef }: { scrollableRef: React.RefObject }) { + const localize = useLocalize(); + const { conversationId } = useMessagesConversation(); + const { data: messages } = useGetMessagesByConvoId(conversationId ?? '', { + enabled: !!conversationId, + }); + const messagesById = useMemo(() => { + const map = new Map(); + if (messages) { + for (let i = 0; i < messages.length; i++) { + const m = messages[i]; + if (m.messageId) { + map.set(m.messageId, m); + } + } + } + return map; + }, [messages]); + + const [entries, setEntries] = useState([]); + const [activeIds, setActiveIds] = useState>(new Set()); + const [canGoUp, setCanGoUp] = useState(false); + const [canGoDown, setCanGoDown] = useState(false); + + const observerRef = useRef(null); + const observedRef = useRef(new Map()); + const columnRef = useRef(null); + const refreshTimerRef = useRef | null>(null); + const visibleSetRef = useRef(new Set()); + const messagesByIdRef = useRef(messagesById); + const scrollTokenRef = useRef(0); + const scrollMarginRef = useRef(0); + + useEffect(() => { + messagesByIdRef.current = messagesById; + }, [messagesById]); + + const refreshEntries = useCallback(() => { + if (refreshTimerRef.current) { + clearTimeout(refreshTimerRef.current); + } + refreshTimerRef.current = setTimeout(() => { + const root = scrollableRef.current ?? document; + const next = getMessageEntries(root, messagesByIdRef.current); + setEntries((prev) => { + if ( + prev.length === next.length && + prev.every((e, i) => e.id === next[i].id && e.preview === next[i].preview) + ) { + return prev; + } + return next; + }); + }, 200); + }, [scrollableRef]); + + useEffect(() => { + refreshEntries(); + }, [messagesById, refreshEntries]); + + const scrollToStart = useCallback((id: string) => { + const el = document.getElementById(id); + if (!el) { + return; + } + const container = el.closest('.scrollbar-gutter-stable'); + if (!container) { + el.scrollIntoView({ behavior: 'smooth', block: 'start' }); + return; + } + const token = ++scrollTokenRef.current; + const scrollMargin = scrollMarginRef.current || readScrollMargin(el); + const startScroll = container.scrollTop; + const start = performance.now(); + + const step = (now: number) => { + if (token !== scrollTokenRef.current) { + return; + } + const progress = Math.min(1, (now - start) / SCROLL_DURATION); + const current = document.getElementById(id); + if (!current) { + return; + } + const cRect = container.getBoundingClientRect(); + const elRect = current.getBoundingClientRect(); + const targetScroll = container.scrollTop + (elRect.top - cRect.top) - scrollMargin; + const max = container.scrollHeight - container.clientHeight; + const clamped = Math.max(0, Math.min(targetScroll, max)); + container.scrollTop = startScroll + (clamped - startScroll) * easeOutCubic(progress); + if (progress < 1) { + requestAnimationFrame(step); + } + }; + + requestAnimationFrame(step); + }, []); + + useEffect(() => { + refreshEntries(); + + const container = scrollableRef.current; + if (!container) { + return; + } + + const mutationObserver = new MutationObserver((mutations) => { + for (let i = 0; i < mutations.length; i++) { + const m = mutations[i]; + if (m.type === 'attributes') { + const target = m.target as HTMLElement; + if (target.nodeType === 1 && target.classList?.contains('message-render')) { + refreshEntries(); + return; + } + continue; + } + if (m.addedNodes.length || m.removedNodes.length) { + for (let j = 0; j < m.addedNodes.length; j++) { + const n = m.addedNodes[j] as HTMLElement; + if ( + n.nodeType === 1 && + (n.classList?.contains('message-render') || n.querySelector?.('.message-render')) + ) { + refreshEntries(); + return; + } + } + for (let j = 0; j < m.removedNodes.length; j++) { + const n = m.removedNodes[j] as HTMLElement; + if ( + n.nodeType === 1 && + (n.classList?.contains('message-render') || n.querySelector?.('.message-render')) + ) { + refreshEntries(); + return; + } + } + } + } + }); + + mutationObserver.observe(container, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ['id'], + }); + + return () => { + mutationObserver.disconnect(); + if (refreshTimerRef.current) { + clearTimeout(refreshTimerRef.current); + } + }; + }, [scrollableRef, refreshEntries]); + + useEffect(() => { + const container = scrollableRef.current; + if (!container || entries.length === 0) { + setCanGoUp(false); + setCanGoDown(false); + return; + } + + const offsetsTop: number[] = new Array(entries.length); + const offsetsBottom: number[] = new Array(entries.length); + const recomputeOffsets = () => { + for (let i = 0; i < entries.length; i++) { + const el = document.getElementById(entries[i].id); + offsetsTop[i] = el ? el.offsetTop : Number.POSITIVE_INFINITY; + offsetsBottom[i] = el ? el.offsetTop + el.offsetHeight : Number.POSITIVE_INFINITY; + } + }; + recomputeOffsets(); + + const firstEl = document.getElementById(entries[0].id); + const scrollMargin = readScrollMargin(firstEl); + scrollMarginRef.current = scrollMargin; + + let needsRecompute = false; + let frameId: number | null = null; + + const tick = () => { + frameId = null; + if (needsRecompute) { + recomputeOffsets(); + needsRecompute = false; + } + + const scrollTop = container.scrollTop; + let nextCanUp = false; + let nextCanDown = false; + for (let i = 0; i < offsetsTop.length; i++) { + const snap = offsetsTop[i] - scrollMargin; + if (snap < scrollTop - JUMP_EPS) { + nextCanUp = true; + } else if (snap > scrollTop + JUMP_EPS) { + nextCanDown = true; + break; + } + } + setCanGoUp((prev) => (prev === nextCanUp ? prev : nextCanUp)); + setCanGoDown((prev) => (prev === nextCanDown ? prev : nextCanDown)); + + const col = columnRef.current; + if (!col) { + return; + } + const viewBottom = scrollTop + container.clientHeight; + let first = -1; + let last = -1; + for (let i = 0; i < offsetsTop.length; i++) { + if (offsetsBottom[i] <= scrollTop) { + continue; + } + if (offsetsTop[i] >= viewBottom) { + break; + } + if (first === -1) { + first = i; + } + last = i; + } + if (first === -1) { + return; + } + const firstInd = col.children[first] as HTMLElement | undefined; + const lastInd = col.children[last] as HTMLElement | undefined; + if (!firstInd || !lastInd) { + return; + } + const mid = (firstInd.offsetTop + lastInd.offsetTop + lastInd.offsetHeight) / 2; + const target = mid - col.clientHeight / 2; + col.scrollTop = Math.max(0, Math.min(target, col.scrollHeight - col.clientHeight)); + }; + + const scheduleTick = () => { + if (frameId == null) { + frameId = requestAnimationFrame(tick); + } + }; + + const content = container.firstElementChild as HTMLElement | null; + const resizeObserver = new ResizeObserver(() => { + needsRecompute = true; + scheduleTick(); + }); + if (content) { + resizeObserver.observe(content); + } + + tick(); + container.addEventListener('scroll', scheduleTick, { passive: true }); + + return () => { + container.removeEventListener('scroll', scheduleTick); + if (frameId != null) { + cancelAnimationFrame(frameId); + } + resizeObserver.disconnect(); + }; + }, [entries, scrollableRef]); + + useEffect(() => { + const root = scrollableRef.current; + if (!root) { + return; + } + + const visibleSet = visibleSetRef.current; + const observed = observedRef.current; + let pendingFrame: number | null = null; + + const flush = () => { + pendingFrame = null; + setActiveIds((prev) => { + if (prev.size === visibleSet.size) { + let same = true; + for (const id of visibleSet) { + if (!prev.has(id)) { + same = false; + break; + } + } + if (same) { + return prev; + } + } + return new Set(visibleSet); + }); + }; + + const observer = new IntersectionObserver( + (intersections) => { + for (const entry of intersections) { + const id = entry.target.id; + if (entry.isIntersecting) { + visibleSet.add(id); + } else { + visibleSet.delete(id); + } + } + if (pendingFrame == null) { + pendingFrame = requestAnimationFrame(flush); + } + }, + { root, threshold: 0 }, + ); + observerRef.current = observer; + + return () => { + observer.disconnect(); + observerRef.current = null; + observed.clear(); + visibleSet.clear(); + if (pendingFrame != null) { + cancelAnimationFrame(pendingFrame); + } + }; + }, [scrollableRef]); + + useEffect(() => { + const observer = observerRef.current; + if (!observer) { + return; + } + const observed = observedRef.current; + const visibleSet = visibleSetRef.current; + const newIds = new Set(); + for (let i = 0; i < entries.length; i++) { + const entry = entries[i]; + newIds.add(entry.id); + if (!observed.has(entry.id)) { + const el = document.getElementById(entry.id); + if (el) { + observer.observe(el); + observed.set(entry.id, el); + } + } + } + + for (const [id, el] of observed) { + if (!newIds.has(id)) { + observer.unobserve(el); + observed.delete(id); + visibleSet.delete(id); + } + } + }, [entries]); + + const jumpToPrevious = useCallback(() => { + const container = scrollableRef.current; + if (!container || entries.length === 0) { + return; + } + const scrollTop = container.scrollTop; + const scrollMargin = + scrollMarginRef.current !== 0 + ? scrollMarginRef.current + : readScrollMargin(document.getElementById(entries[0].id)); + for (let i = entries.length - 1; i >= 0; i--) { + const el = document.getElementById(entries[i].id); + if (!el) { + continue; + } + if (el.offsetTop - scrollMargin < scrollTop - JUMP_EPS) { + scrollToStart(entries[i].id); + return; + } + } + container.scrollTo({ top: 0, behavior: 'smooth' }); + }, [entries, scrollableRef, scrollToStart]); + + const jumpToNext = useCallback(() => { + const container = scrollableRef.current; + if (!container || entries.length === 0) { + return; + } + const scrollTop = container.scrollTop; + const scrollMargin = + scrollMarginRef.current !== 0 + ? scrollMarginRef.current + : readScrollMargin(document.getElementById(entries[0].id)); + for (let i = 0; i < entries.length; i++) { + const el = document.getElementById(entries[i].id); + if (!el) { + continue; + } + if (el.offsetTop - scrollMargin > scrollTop + JUMP_EPS) { + scrollToStart(entries[i].id); + return; + } + } + }, [entries, scrollableRef, scrollToStart]); + + if (entries.length < 3) { + return null; + } + + return ( + + ); +} + +export default memo(MessageNav); diff --git a/client/src/components/Chat/Messages/MessagesView.tsx b/client/src/components/Chat/Messages/MessagesView.tsx index b4f555d189f7..9c59cab292e8 100644 --- a/client/src/components/Chat/Messages/MessagesView.tsx +++ b/client/src/components/Chat/Messages/MessagesView.tsx @@ -8,6 +8,7 @@ import ScrollToBottom from '~/components/Messages/ScrollToBottom'; import { MessagesViewProvider } from '~/Providers'; import { fontSizeAtom } from '~/store/fontSize'; import MultiMessage from './MultiMessage'; +import MessageNav from './MessageNav'; import { cn } from '~/utils'; import store from '~/store'; @@ -21,7 +22,7 @@ function MessagesViewContent({ const { screenshotTargetRef } = useScreenshot(); const scrollButtonPreference = useRecoilValue(store.showScrollButton); const [currentEditId, setCurrentEditId] = useState(-1); - const scrollToBottomRef = useRef(null); + const scrollToBottomRef = useRef(null); const { conversation, @@ -81,8 +82,8 @@ function MessagesViewContent({ + + diff --git a/client/src/components/Chat/Messages/__tests__/MessageNav.spec.tsx b/client/src/components/Chat/Messages/__tests__/MessageNav.spec.tsx new file mode 100644 index 000000000000..cfdfa29f7048 --- /dev/null +++ b/client/src/components/Chat/Messages/__tests__/MessageNav.spec.tsx @@ -0,0 +1,629 @@ +import React from 'react'; +import { render, act, fireEvent } from '@testing-library/react'; + +type ReactNode = React.ReactNode; +type RefObject = React.RefObject; + +type TestMessage = { + messageId: string; + conversationId?: string; + text?: string; + isCreatedByUser?: boolean; +}; + +const mockUseGetMessagesByConvoId = jest.fn(); +const mockUseMessagesConversation = jest.fn(); + +jest.mock('~/data-provider', () => ({ + useGetMessagesByConvoId: (...args: unknown[]) => mockUseGetMessagesByConvoId(...args), +})); + +jest.mock('~/Providers', () => ({ + useMessagesConversation: (...args: unknown[]) => mockUseMessagesConversation(...args), +})); + +jest.mock('~/hooks', () => ({ + useLocalize: + () => + (key: string, opts?: Record): string => + opts ? `${key}|${JSON.stringify(opts)}` : key, +})); + +jest.mock('@librechat/client', () => ({ + HoverCard: ({ children }: { children: ReactNode }) => <>{children}, + HoverCardTrigger: ({ children, asChild }: { children: ReactNode; asChild?: boolean }) => + asChild ? children :
{children}
, + HoverCardPortal: ({ children }: { children: ReactNode }) => <>{children}, + HoverCardContent: ({ children, className }: { children: ReactNode; className?: string }) => ( +
+ {children} +
+ ), +})); + +type IOEntry = Pick; + +class MockIntersectionObserver { + static instances: MockIntersectionObserver[] = []; + static last(): MockIntersectionObserver | undefined { + return MockIntersectionObserver.instances[MockIntersectionObserver.instances.length - 1]; + } + + static reset() { + MockIntersectionObserver.instances = []; + } + + root: Element | Document | null = null; + rootMargin = '0px'; + thresholds: number[] = [0]; + callback: IntersectionObserverCallback; + observed = new Set(); + observe = jest.fn((el: Element) => { + this.observed.add(el); + }); + + unobserve = jest.fn((el: Element) => { + this.observed.delete(el); + }); + + disconnect = jest.fn(() => { + this.observed.clear(); + }); + + takeRecords = jest.fn(() => []); + constructor(cb: IntersectionObserverCallback, opts?: IntersectionObserverInit) { + this.callback = cb; + if (opts?.root instanceof Element || opts?.root instanceof Document) { + this.root = opts.root; + } + MockIntersectionObserver.instances.push(this); + } + + trigger(entries: IOEntry[]) { + this.callback(entries as IntersectionObserverEntry[], this as unknown as IntersectionObserver); + } +} + +const originalIO = global.IntersectionObserver; + +import MessageNav from '../MessageNav'; + +function buildMessage(overrides: Partial = {}): TestMessage { + return { + messageId: 'm', + conversationId: 'test-convo', + text: 'hello', + isCreatedByUser: false, + ...overrides, + }; +} + +function buildDom(messages: TestMessage[]): { + scrollable: HTMLDivElement; + content: HTMLDivElement; +} { + const scrollable = document.createElement('div'); + scrollable.className = 'scrollbar-gutter-stable'; + Object.defineProperty(scrollable, 'clientHeight', { value: 600, configurable: true }); + Object.defineProperty(scrollable, 'scrollHeight', { value: 3000, configurable: true }); + Object.defineProperty(scrollable, 'scrollTop', { value: 0, writable: true, configurable: true }); + + const content = document.createElement('div'); + content.className = 'flex flex-col'; + scrollable.appendChild(content); + + for (let i = 0; i < messages.length; i++) { + const m = messages[i]; + const div = document.createElement('div'); + div.id = m.messageId; + div.className = 'message-render'; + if (m.isCreatedByUser) { + const turn = document.createElement('div'); + turn.className = 'user-turn'; + turn.textContent = m.text ?? ''; + div.appendChild(turn); + } else { + div.textContent = m.text ?? ''; + } + Object.defineProperty(div, 'offsetTop', { value: 100 + i * 200, configurable: true }); + Object.defineProperty(div, 'offsetHeight', { value: 150, configurable: true }); + content.appendChild(div); + } + + document.body.appendChild(scrollable); + return { scrollable, content }; +} + +function renderNav(messages: TestMessage[]) { + mockUseGetMessagesByConvoId.mockReturnValue({ data: messages }); + const { scrollable, content } = buildDom(messages); + const scrollableRef = { current: scrollable } as RefObject; + const result = render(); + act(() => { + jest.advanceTimersByTime(250); + }); + return { ...result, scrollable, content, scrollableRef }; +} + +function clearDom() { + while (document.body.firstChild) { + document.body.removeChild(document.body.firstChild); + } +} + +beforeEach(() => { + MockIntersectionObserver.reset(); + ( + global as unknown as { IntersectionObserver: typeof MockIntersectionObserver } + ).IntersectionObserver = MockIntersectionObserver; + jest.useFakeTimers(); + mockUseMessagesConversation.mockReturnValue({ conversationId: 'test-convo' }); + mockUseGetMessagesByConvoId.mockReturnValue({ data: [] }); +}); + +afterEach(() => { + jest.useRealTimers(); + ( + global as unknown as { IntersectionObserver: typeof IntersectionObserver } + ).IntersectionObserver = originalIO; + clearDom(); +}); + +describe('MessageNav', () => { + describe('rendering threshold', () => { + it('renders nothing when there are 0 messages', () => { + const { container } = renderNav([]); + expect(container.querySelector('nav')).toBeNull(); + }); + + it('renders nothing with fewer than 3 messages', () => { + const messages = [ + buildMessage({ messageId: 'a', text: 'hi', isCreatedByUser: true }), + buildMessage({ messageId: 'b', text: 'hey' }), + ]; + const { container } = renderNav(messages); + expect(container.querySelector('nav')).toBeNull(); + }); + + it('renders the nav with 3+ messages and an indicator for each', () => { + const messages = [ + buildMessage({ messageId: 'a', text: 'alpha', isCreatedByUser: true }), + buildMessage({ messageId: 'b', text: 'bravo' }), + buildMessage({ messageId: 'c', text: 'charlie', isCreatedByUser: true }), + ]; + const { container } = renderNav(messages); + const nav = container.querySelector('nav'); + expect(nav).not.toBeNull(); + expect(nav).toHaveAttribute('aria-label', 'com_ui_message_nav'); + + const indicators = container.querySelectorAll('[data-msg-id]'); + expect(indicators).toHaveLength(3); + expect(Array.from(indicators).map((el) => el.getAttribute('data-msg-id'))).toEqual([ + 'a', + 'b', + 'c', + ]); + }); + }); + + describe('indicator styling', () => { + it('uses narrower width for user turns and wider for assistant turns', () => { + const messages = [ + buildMessage({ messageId: 'u', text: 'user msg', isCreatedByUser: true }), + buildMessage({ messageId: 'a', text: 'assistant msg' }), + buildMessage({ messageId: 'u2', text: 'more user', isCreatedByUser: true }), + ]; + const { container } = renderNav(messages); + const [userInd, assistantInd] = container.querySelectorAll('[data-msg-id]'); + expect(userInd.className).toContain('w-4'); + expect(userInd.className).not.toContain('w-6'); + expect(assistantInd.className).toContain('w-6'); + expect(assistantInd.className).not.toContain('w-4'); + }); + }); + + describe('preview text', () => { + it('uses message text from React Query data when available', () => { + const messages = [ + buildMessage({ messageId: 'a', text: 'alpha-preview', isCreatedByUser: true }), + buildMessage({ messageId: 'b', text: 'bravo-preview' }), + buildMessage({ messageId: 'c', text: 'charlie-preview', isCreatedByUser: true }), + ]; + const { container } = renderNav(messages); + const previews = container.querySelectorAll('[data-testid="hover-card-content"] p'); + expect(previews).toHaveLength(3); + expect(previews[0]).toHaveTextContent('alpha-preview'); + expect(previews[1]).toHaveTextContent('bravo-preview'); + }); + + it('falls back to DOM text when a message is not in React Query data', () => { + mockUseGetMessagesByConvoId.mockReturnValue({ data: [] }); + const scrollable = document.createElement('div'); + scrollable.className = 'scrollbar-gutter-stable'; + const content = document.createElement('div'); + scrollable.appendChild(content); + for (const [i, id] of ['x', 'y', 'z'].entries()) { + const div = document.createElement('div'); + div.id = id; + div.className = 'message-render'; + div.textContent = `dom-text-${id}`; + Object.defineProperty(div, 'offsetTop', { value: i * 200 }); + Object.defineProperty(div, 'offsetHeight', { value: 150 }); + content.appendChild(div); + } + document.body.appendChild(scrollable); + const scrollableRef = { current: scrollable } as RefObject; + const { container } = render(); + act(() => { + jest.advanceTimersByTime(250); + }); + + const previews = container.querySelectorAll('[data-testid="hover-card-content"] p'); + expect(previews[0]).toHaveTextContent('dom-text-x'); + expect(previews[2]).toHaveTextContent('dom-text-z'); + }); + + it('truncates previews longer than 80 chars with an ellipsis', () => { + const long = 'a'.repeat(120); + const messages = [ + buildMessage({ messageId: 'a', text: long, isCreatedByUser: true }), + buildMessage({ messageId: 'b', text: 'short' }), + buildMessage({ messageId: 'c', text: 'also short', isCreatedByUser: true }), + ]; + const { container } = renderNav(messages); + const preview = container.querySelectorAll('[data-testid="hover-card-content"] p')[0]; + const text = preview?.textContent ?? ''; + expect(text.endsWith('...')).toBe(true); + expect(text.length).toBe(83); + }); + }); + + describe('accessibility', () => { + it('labels the nav and chevron buttons via useLocalize', () => { + const messages = [ + buildMessage({ messageId: 'a', text: 'one', isCreatedByUser: true }), + buildMessage({ messageId: 'b', text: 'two' }), + buildMessage({ messageId: 'c', text: 'three', isCreatedByUser: true }), + ]; + const { container } = renderNav(messages); + + expect(container.querySelector('nav')).toHaveAttribute('aria-label', 'com_ui_message_nav'); + + const prevBtn = container.querySelector('button[aria-label="com_ui_message_nav_previous"]'); + const nextBtn = container.querySelector('button[aria-label="com_ui_message_nav_next"]'); + expect(prevBtn).not.toBeNull(); + expect(nextBtn).not.toBeNull(); + }); + + it('labels each indicator with its role-specific key and localized preview', () => { + const messages = [ + buildMessage({ messageId: 'u', text: 'hi there', isCreatedByUser: true }), + buildMessage({ messageId: 'a', text: 'assistant reply' }), + buildMessage({ messageId: 'u2', text: 'follow-up', isCreatedByUser: true }), + ]; + const { container } = renderNav(messages); + const [userInd, assistantInd] = container.querySelectorAll('[data-msg-id]'); + expect(userInd.getAttribute('aria-label')).toMatch(/^com_ui_message_nav_go_to_user\|/); + expect(userInd.getAttribute('aria-label')).toContain('hi there'); + expect(assistantInd.getAttribute('aria-label')).toMatch( + /^com_ui_message_nav_go_to_assistant\|/, + ); + }); + + it('sets aria-current on the active indicator after IntersectionObserver fires', () => { + const messages = [ + buildMessage({ messageId: 'a', text: 'alpha', isCreatedByUser: true }), + buildMessage({ messageId: 'b', text: 'bravo' }), + buildMessage({ messageId: 'c', text: 'charlie', isCreatedByUser: true }), + ]; + const { container } = renderNav(messages); + const io = MockIntersectionObserver.last(); + expect(io).toBeDefined(); + + const target = document.getElementById('b'); + act(() => { + io!.trigger([{ target: target!, isIntersecting: true }]); + jest.advanceTimersByTime(32); + }); + + const active = container.querySelector('[aria-current="true"]'); + expect(active).not.toBeNull(); + expect(active).toHaveAttribute('data-msg-id', 'b'); + }); + + it('chevron buttons expose a disabled state when there is nothing to navigate to', () => { + const messages = [ + buildMessage({ messageId: 'a', text: 'one', isCreatedByUser: true }), + buildMessage({ messageId: 'b', text: 'two' }), + buildMessage({ messageId: 'c', text: 'three', isCreatedByUser: true }), + ]; + const { container, scrollable } = renderNav(messages); + + Object.defineProperty(scrollable, 'scrollTop', { + value: 0, + configurable: true, + writable: true, + }); + act(() => { + fireEvent.scroll(scrollable); + jest.advanceTimersByTime(32); + }); + + const prev = container.querySelector( + 'button[aria-label="com_ui_message_nav_previous"]', + ) as HTMLButtonElement; + const next = container.querySelector( + 'button[aria-label="com_ui_message_nav_next"]', + ) as HTMLButtonElement; + + expect(prev.disabled).toBe(true); + expect(next.disabled).toBe(false); + }); + }); + + describe('scroll behavior', () => { + it('schedules a rAF when an indicator is clicked', () => { + const messages = [ + buildMessage({ messageId: 'a', text: 'alpha', isCreatedByUser: true }), + buildMessage({ messageId: 'b', text: 'bravo' }), + buildMessage({ messageId: 'c', text: 'charlie', isCreatedByUser: true }), + ]; + const { container } = renderNav(messages); + const rafSpy = jest.spyOn(window, 'requestAnimationFrame'); + + const target = container.querySelectorAll('[data-msg-id]')[2] as HTMLButtonElement; + act(() => { + fireEvent.click(target); + }); + expect(rafSpy).toHaveBeenCalled(); + rafSpy.mockRestore(); + }); + + it('cancels any prior rAF scroll when a new one starts', () => { + const messages = [ + buildMessage({ messageId: 'a', text: 'alpha', isCreatedByUser: true }), + buildMessage({ messageId: 'b', text: 'bravo' }), + buildMessage({ messageId: 'c', text: 'charlie', isCreatedByUser: true }), + ]; + const { container } = renderNav(messages); + const indicators = container.querySelectorAll('[data-msg-id]'); + + const steps: Array<(ts: number) => void> = []; + const rafSpy = jest + .spyOn(window, 'requestAnimationFrame') + .mockImplementation((cb: FrameRequestCallback) => { + steps.push(cb); + return steps.length; + }); + + act(() => { + fireEvent.click(indicators[0] as HTMLElement); + }); + act(() => { + fireEvent.click(indicators[2] as HTMLElement); + }); + + expect(steps.length).toBeGreaterThanOrEqual(2); + expect(() => { + steps[0](performance.now()); + steps[steps.length - 1](performance.now()); + }).not.toThrow(); + + rafSpy.mockRestore(); + }); + + it('keeps per-instance scroll tokens isolated across mounted MessageNav instances', () => { + const messagesA = [ + buildMessage({ messageId: 'a1', text: 'one', isCreatedByUser: true }), + buildMessage({ messageId: 'a2', text: 'two' }), + buildMessage({ messageId: 'a3', text: 'three', isCreatedByUser: true }), + ]; + const messagesB = [ + buildMessage({ messageId: 'b1', text: 'alpha', isCreatedByUser: true }), + buildMessage({ messageId: 'b2', text: 'beta' }), + buildMessage({ messageId: 'b3', text: 'gamma', isCreatedByUser: true }), + ]; + + mockUseGetMessagesByConvoId.mockReturnValue({ data: messagesA }); + const domA = buildDom(messagesA); + const { container: navA } = render( + } />, + ); + act(() => { + jest.advanceTimersByTime(250); + }); + + mockUseGetMessagesByConvoId.mockReturnValue({ data: messagesB }); + const domB = buildDom(messagesB); + const { container: navB } = render( + } />, + ); + act(() => { + jest.advanceTimersByTime(250); + }); + + const steps: Array<(ts: number) => void> = []; + const rafSpy = jest + .spyOn(window, 'requestAnimationFrame') + .mockImplementation((cb: FrameRequestCallback) => { + steps.push(cb); + return steps.length; + }); + + const indA = navA.querySelectorAll('[data-msg-id]')[2] as HTMLButtonElement; + const indB = navB.querySelectorAll('[data-msg-id]')[2] as HTMLButtonElement; + + act(() => { + fireEvent.click(indA); + }); + act(() => { + fireEvent.click(indB); + }); + + expect(steps.length).toBeGreaterThanOrEqual(2); + + let aScrollTouched = false; + Object.defineProperty(domA.scrollable, 'scrollTop', { + get: () => 0, + set: () => { + aScrollTouched = true; + }, + configurable: true, + }); + act(() => { + steps[0](performance.now()); + }); + expect(aScrollTouched).toBe(true); + + rafSpy.mockRestore(); + }); + }); + + describe('observers', () => { + it('observes each message on mount', () => { + const messages = [ + buildMessage({ messageId: 'a', text: 'alpha', isCreatedByUser: true }), + buildMessage({ messageId: 'b', text: 'bravo' }), + buildMessage({ messageId: 'c', text: 'charlie', isCreatedByUser: true }), + ]; + renderNav(messages); + const io = MockIntersectionObserver.last(); + expect(io).toBeDefined(); + expect(io!.observed.size).toBe(3); + expect(io!.observe).toHaveBeenCalledTimes(3); + }); + + it('reuses the same IntersectionObserver when entries change', async () => { + const messages = [ + buildMessage({ messageId: 'a', text: 'alpha', isCreatedByUser: true }), + buildMessage({ messageId: 'b', text: 'bravo' }), + buildMessage({ messageId: 'c', text: 'charlie', isCreatedByUser: true }), + ]; + const { scrollable } = renderNav(messages); + + const instanceCountAfterMount = MockIntersectionObserver.instances.length; + const io = MockIntersectionObserver.last()!; + const initialObserveCalls = io.observe.mock.calls.length; + const initialUnobserveCalls = io.unobserve.mock.calls.length; + + const newMsg = document.createElement('div'); + newMsg.id = 'd'; + newMsg.className = 'message-render'; + newMsg.textContent = 'delta'; + Object.defineProperty(newMsg, 'offsetTop', { value: 800 }); + Object.defineProperty(newMsg, 'offsetHeight', { value: 150 }); + + await act(async () => { + scrollable.firstElementChild!.appendChild(newMsg); + await Promise.resolve(); + }); + mockUseGetMessagesByConvoId.mockReturnValue({ + data: [...messages, buildMessage({ messageId: 'd', text: 'delta' })], + }); + await act(async () => { + jest.advanceTimersByTime(250); + await Promise.resolve(); + }); + + expect(MockIntersectionObserver.instances.length).toBe(instanceCountAfterMount); + expect(io.observe.mock.calls.length).toBe(initialObserveCalls + 1); + expect(io.unobserve.mock.calls.length).toBe(initialUnobserveCalls); + }); + + it('unobserves messages that are removed', async () => { + const messages = [ + buildMessage({ messageId: 'a', text: 'alpha', isCreatedByUser: true }), + buildMessage({ messageId: 'b', text: 'bravo' }), + buildMessage({ messageId: 'c', text: 'charlie', isCreatedByUser: true }), + buildMessage({ messageId: 'd', text: 'delta' }), + ]; + const { scrollable } = renderNav(messages); + const io = MockIntersectionObserver.last()!; + const initialUnobserve = io.unobserve.mock.calls.length; + + const removed = document.getElementById('d'); + await act(async () => { + removed?.remove(); + await Promise.resolve(); + }); + mockUseGetMessagesByConvoId.mockReturnValue({ data: messages.slice(0, 3) }); + await act(async () => { + jest.advanceTimersByTime(250); + await Promise.resolve(); + }); + + expect(io.unobserve.mock.calls.length).toBeGreaterThan(initialUnobserve); + expect(scrollable.contains(removed as Node)).toBe(false); + }); + + it('disconnects the IntersectionObserver on unmount', () => { + const messages = [ + buildMessage({ messageId: 'a', text: 'alpha', isCreatedByUser: true }), + buildMessage({ messageId: 'b', text: 'bravo' }), + buildMessage({ messageId: 'c', text: 'charlie', isCreatedByUser: true }), + ]; + const { unmount } = renderNav(messages); + const io = MockIntersectionObserver.last()!; + expect(io.disconnect).not.toHaveBeenCalled(); + unmount(); + expect(io.disconnect).toHaveBeenCalled(); + }); + }); + + describe('dom-driven refresh', () => { + it('refreshes entries when a .message-render id attribute changes (SSE lifecycle)', async () => { + const messages = [ + buildMessage({ messageId: 'client-uuid', text: 'streaming', isCreatedByUser: false }), + buildMessage({ messageId: 'b', text: 'bravo', isCreatedByUser: true }), + buildMessage({ messageId: 'c', text: 'charlie', isCreatedByUser: true }), + ]; + const { container } = renderNav(messages); + expect(container.querySelector('[data-msg-id="client-uuid"]')).not.toBeNull(); + + const streamingNode = document.getElementById('client-uuid') as HTMLElement; + await act(async () => { + streamingNode.id = 'server-id'; + await Promise.resolve(); + }); + mockUseGetMessagesByConvoId.mockReturnValue({ + data: [buildMessage({ messageId: 'server-id', text: 'streaming' }), ...messages.slice(1)], + }); + await act(async () => { + jest.advanceTimersByTime(250); + await Promise.resolve(); + }); + + expect(container.querySelector('[data-msg-id="client-uuid"]')).toBeNull(); + expect(container.querySelector('[data-msg-id="server-id"]')).not.toBeNull(); + }); + + it('refreshes entries when a .message-render is added via MutationObserver', async () => { + const messages = [ + buildMessage({ messageId: 'a', text: 'alpha', isCreatedByUser: true }), + buildMessage({ messageId: 'b', text: 'bravo' }), + buildMessage({ messageId: 'c', text: 'charlie', isCreatedByUser: true }), + ]; + const { container, scrollable } = renderNav(messages); + expect(container.querySelectorAll('[data-msg-id]')).toHaveLength(3); + + const newMsg = document.createElement('div'); + newMsg.id = 'd'; + newMsg.className = 'message-render'; + newMsg.textContent = 'delta'; + Object.defineProperty(newMsg, 'offsetTop', { value: 800 }); + Object.defineProperty(newMsg, 'offsetHeight', { value: 150 }); + + await act(async () => { + scrollable.firstElementChild!.appendChild(newMsg); + await Promise.resolve(); + }); + await act(async () => { + jest.advanceTimersByTime(250); + await Promise.resolve(); + }); + + expect(container.querySelectorAll('[data-msg-id]')).toHaveLength(4); + expect(container.querySelector('[data-msg-id="d"]')).not.toBeNull(); + }); + }); +}); diff --git a/client/src/components/Messages/ScrollToBottom.tsx b/client/src/components/Messages/ScrollToBottom.tsx index 0b99df0a61a2..b6537a1493a9 100644 --- a/client/src/components/Messages/ScrollToBottom.tsx +++ b/client/src/components/Messages/ScrollToBottom.tsx @@ -1,27 +1,34 @@ import { forwardRef } from 'react'; +import { useRecoilValue } from 'recoil'; +import { ChevronDown } from 'lucide-react'; +import { useLocalize } from '~/hooks'; +import { cn } from '~/utils'; +import store from '~/store'; type Props = { scrollHandler: React.MouseEventHandler; }; -const ScrollToBottom = forwardRef(({ scrollHandler }, ref) => { +const ScrollToBottom = forwardRef(({ scrollHandler }, ref) => { + const localize = useLocalize(); + const maximizeChatSpace = useRecoilValue(store.maximizeChatSpace); + return ( - + + ); }); diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index 4daf5691767e..d4a11997cc55 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -1188,6 +1188,11 @@ "com_ui_mermaid_failed": "Failed to render diagram:", "com_ui_mermaid_source": "Source code:", "com_ui_message_input": "Message input", + "com_ui_message_nav": "Message navigation", + "com_ui_message_nav_go_to_assistant": "Go to assistant message: {{0}}", + "com_ui_message_nav_go_to_user": "Go to user message: {{0}}", + "com_ui_message_nav_next": "Navigate to next message", + "com_ui_message_nav_previous": "Navigate to previous message", "com_ui_microphone_unavailable": "Microphone is not available", "com_ui_min_tags": "Cannot remove more values, a minimum of {{0}} are required.", "com_ui_minimal": "Minimal", @@ -1376,6 +1381,7 @@ "com_ui_saving": "Saving...", "com_ui_schema": "Schema", "com_ui_scope": "Scope", + "com_ui_scroll_to_bottom": "Scroll to bottom", "com_ui_search": "Search", "com_ui_search_above_to_add": "Search above to add users or groups", "com_ui_search_above_to_add_all": "Search above to add users, groups, or roles", diff --git a/client/src/style.css b/client/src/style.css index e934d241c1cd..902860ef525a 100644 --- a/client/src/style.css +++ b/client/src/style.css @@ -2,6 +2,10 @@ @tailwind components; @tailwind utilities; +.message-render { + scroll-margin-top: 4rem; +} + /* Custom Variables */ :root { --white: #fff; @@ -647,127 +651,83 @@ pre { margin: 0; } +/* Scroll-to-bottom button — enter */ .scroll-animation-enter { opacity: 0; - transform: translateY(20px) scale(0.7) rotate(-5deg); + transform: translateY(12px) scale(0.9); pointer-events: none; } +.scroll-animation-enter-active { + animation: scroll-btn-enter 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards; +} + .scroll-animation-enter-done { opacity: 1; transform: translateY(0) scale(1); } -.scroll-animation-exit-done { - display: none; -} - -@keyframes twist-entrance { +@keyframes scroll-btn-enter { 0% { - transform: translateY(20px) scale(0.7) rotate(-5deg); opacity: 0; - } - 60% { - transform: translateY(2px) scale(0.95) rotate(2deg); - opacity: 0.9; + transform: translateY(12px) scale(0.9); } 100% { - transform: translateY(0) scale(1) rotate(0deg); opacity: 1; + transform: translateY(0) scale(1); } } -.scroll-animation-enter-active { - animation: twist-entrance 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; - transition-delay: 50ms; +/* Scroll-to-bottom button — exit */ +.scroll-animation-exit-active { + animation: scroll-btn-exit 0.25s cubic-bezier(0.4, 0, 1, 1) forwards; + pointer-events: none; +} + +.scroll-animation-exit-done { + display: none; } -@keyframes twist-exit { +@keyframes scroll-btn-exit { 0% { - transform: translateY(0) scale(1) rotate(0deg); opacity: 1; - } - 40% { - transform: translateY(5px) scale(0.95) rotate(2deg); - opacity: 0.7; + transform: translateY(0) scale(1); } 100% { - transform: translateY(20px) scale(0.7) rotate(-5deg); opacity: 0; + transform: translateY(8px) scale(0.95); } } -.scroll-animation-exit-active { - animation: twist-exit 0.4s cubic-bezier(0.55, 0.085, 0.68, 0.53) forwards; - pointer-events: none; -} - +/* Scroll-to-bottom button */ .premium-scroll-button { display: flex; align-items: center; justify-content: center; - width: 34px; - height: 34px; + width: 32px; + height: 32px; padding: 0; border-radius: 50%; - box-shadow: - 0 2px 8px rgba(0, 0, 0, 0.05), - 0 4px 12px rgba(0, 0, 0, 0.08), - 0 0 0 1px rgba(255, 255, 255, 0.08); - background-color: rgba(255, 255, 255, 0.9); + border: 1px solid rgba(0, 0, 0, 0.08); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); + background-color: #ffffff; z-index: 10; - transition: - transform 500ms cubic-bezier(0.25, 0.1, 0.25, 1), - box-shadow 500ms cubic-bezier(0.25, 0.1, 0.25, 1); overflow: hidden; + transition: transform 100ms ease; } .dark .premium-scroll-button { - box-shadow: - 0 2px 8px rgba(0, 0, 0, 0.2), - 0 4px 12px rgba(0, 0, 0, 0.25), - 0 0 0 1px rgba(255, 255, 255, 0.06); - background-color: rgba(35, 35, 40, 0.9); + border-color: rgba(255, 255, 255, 0.1); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); + background-color: #2a2a2e; } .scroll-animation-enter-active .premium-scroll-button { pointer-events: none !important; } -.premium-scroll-button:hover:not(:active) { - transform: translateY(-1.5px) scale(1.02); - box-shadow: - 0 5px 10px rgba(0, 0, 0, 0.07), - 0 7px 14px rgba(0, 0, 0, 0.1), - 0 0 0 1px rgba(255, 255, 255, 0.1); -} - -.premium-scroll-button:active { - transform: translateY(1px) scale(0.98); - transition: all 150ms cubic-bezier(0.2, 0, 0.2, 1); - box-shadow: - 0 1px 4px rgba(0, 0, 0, 0.1), - 0 2px 8px rgba(0, 0, 0, 0.08), - 0 0 0 1px rgba(255, 255, 255, 0.08); -} - -@keyframes float { - 0%, - 100% { - transform: translateY(0); - } - 50% { - transform: translateY(-1px); - } -} - -.scroll-animation-enter-done .premium-scroll-button { - animation: float 2s ease-in-out infinite; -} - -.premium-scroll-button:hover, .premium-scroll-button:active { - animation: none; + transform: scale(0.95); } .blink { diff --git a/package-lock.json b/package-lock.json index 04de36277a09..914d58e02965 100644 --- a/package-lock.json +++ b/package-lock.json @@ -487,7 +487,7 @@ "@babel/plugin-transform-runtime": "^7.22.15", "@babel/preset-env": "^7.22.15", "@babel/preset-react": "^7.22.15", - "@babel/preset-typescript": "^7.22.15", + "@babel/preset-typescript": "^7.28.5", "@happy-dom/jest-environment": "^20.8.9", "@tanstack/react-query-devtools": "^4.29.0", "@testing-library/dom": "^9.3.0", @@ -605,28 +605,6 @@ "node": ">=6.9.0" } }, - "client/node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.26.9.tgz", - "integrity": "sha512-ubbUqCofvxPRurw5L8WTsCLSkQiVpov4Qx0WMA+jUN+nXBK8ADPlJO1grkFw5CWKC5+sZSOfuGMdX1aI1iT9Sg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-member-expression-to-functions": "^7.25.9", - "@babel/helper-optimise-call-expression": "^7.25.9", - "@babel/helper-replace-supers": "^7.26.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", - "@babel/traverse": "^7.26.9", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, "client/node_modules/@babel/helper-define-polyfill-provider": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.3.tgz", @@ -644,20 +622,6 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "client/node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz", - "integrity": "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, "client/node_modules/@babel/helper-module-imports": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", @@ -690,19 +654,6 @@ "@babel/core": "^7.0.0" } }, - "client/node_modules/@babel/helper-optimise-call-expression": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz", - "integrity": "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, "client/node_modules/@babel/helper-remap-async-to-generator": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz", @@ -721,38 +672,6 @@ "@babel/core": "^7.0.0" } }, - "client/node_modules/@babel/helper-replace-supers": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.26.5.tgz", - "integrity": "sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.25.9", - "@babel/helper-optimise-call-expression": "^7.25.9", - "@babel/traverse": "^7.26.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "client/node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz", - "integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, "client/node_modules/@babel/helper-wrap-function": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz", @@ -1218,23 +1137,6 @@ "@babel/core": "^7.0.0-0" } }, - "client/node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.26.3.tgz", - "integrity": "sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "client/node_modules/@babel/plugin-transform-modules-systemjs": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz", @@ -4926,13 +4828,13 @@ } }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", - "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.25.9" + "@babel/types": "^7.27.3" }, "engines": { "node": ">=6.9.0" @@ -4975,19 +4877,18 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.23.10", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.23.10.tgz", - "integrity": "sha512-2XpP2XhkXzgxecPNEEK8Vz8Asj9aRxt08oKOqtiZoqV2UGZ5T+EkyP9sXQ9nwMxBIG34a7jmasVqoMop7VdPUw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", + "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-member-expression-to-functions": "^7.23.0", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.20", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.28.6", "semver": "^6.3.1" }, "engines": { @@ -4997,6 +4898,102 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -5092,12 +5089,14 @@ } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz", - "integrity": "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.23.0" + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -5134,21 +5133,22 @@ } }, "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", - "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -5172,14 +5172,15 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz", - "integrity": "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", + "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-member-expression-to-functions": "^7.22.15", - "@babel/helper-optimise-call-expression": "^7.22.5" + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -5188,26 +5189,111 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-simple-access": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", - "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "node_modules/@babel/helper-replace-supers/node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers/node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers/node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-replace-supers/node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers/node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers/node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", - "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.22.5" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -6069,14 +6155,14 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.3.tgz", - "integrity": "sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", + "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-simple-access": "^7.22.5" + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -6085,6 +6171,134 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-modules-commonjs/node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs/node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs/node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs/node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs/node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs/node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs/node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs/node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/plugin-transform-modules-systemjs": { "version": "7.23.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.9.tgz", @@ -6563,15 +6777,33 @@ } }, "node_modules/@babel/plugin-transform-typescript": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.23.6.tgz", - "integrity": "sha512-6cBG5mBvUu4VUD04OHKnYzbuHNP8huDsD3EDqqpIpsswTDoqHCjLoHb6+QgsV1WsT2nipRqCPgxD3LXnEO7XfA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz", + "integrity": "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript/node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.23.6", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-typescript": "^7.23.3" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -6781,16 +7013,17 @@ } }, "node_modules/@babel/preset-typescript": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.23.3.tgz", - "integrity": "sha512-17oIGVlqz6CchO9RFYn5U6ZpWRZIngayYCtrPRSgANSwC2V1Jb+iP74nVxzzXJte8b8BYxrL1yY96xfhTBrNNQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz", + "integrity": "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.15", - "@babel/plugin-syntax-jsx": "^7.23.3", - "@babel/plugin-transform-modules-commonjs": "^7.23.3", - "@babel/plugin-transform-typescript": "^7.23.3" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-typescript": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -44378,41 +44611,6 @@ "dev": true, "license": "MIT" }, - "packages/client/node_modules/@babel/helper-annotate-as-pure": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", - "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.27.3" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "packages/client/node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.5.tgz", - "integrity": "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-member-expression-to-functions": "^7.28.5", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/helper-replace-supers": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/traverse": "^7.28.5", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, "packages/client/node_modules/@babel/helper-create-regexp-features-plugin": { "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", @@ -44448,33 +44646,6 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "packages/client/node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", - "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "packages/client/node_modules/@babel/helper-optimise-call-expression": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", - "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, "packages/client/node_modules/@babel/helper-remap-async-to-generator": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", @@ -44493,38 +44664,6 @@ "@babel/core": "^7.0.0" } }, - "packages/client/node_modules/@babel/helper-replace-supers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", - "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.27.1", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/traverse": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "packages/client/node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", - "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, "packages/client/node_modules/@babel/helper-wrap-function": { "version": "7.28.3", "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.3.tgz", @@ -45010,23 +45149,6 @@ "@babel/core": "^7.0.0-0" } }, - "packages/client/node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", - "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "packages/client/node_modules/@babel/plugin-transform-modules-systemjs": { "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.28.5.tgz", @@ -45464,26 +45586,6 @@ "@babel/core": "^7.0.0-0" } }, - "packages/client/node_modules/@babel/plugin-transform-typescript": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.5.tgz", - "integrity": "sha512-x2Qa+v/CuEoX7Dr31iAfr0IhInrVOWZU/2vJMJ00FOR/2nM0BcBEclpaf9sWCDc+v5e9dMrhSH8/atq/kX7+bA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-create-class-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/plugin-syntax-typescript": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "packages/client/node_modules/@babel/plugin-transform-unicode-escapes": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", @@ -45657,26 +45759,6 @@ "@babel/core": "^7.0.0-0" } }, - "packages/client/node_modules/@babel/preset-typescript": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz", - "integrity": "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-validator-option": "^7.27.1", - "@babel/plugin-syntax-jsx": "^7.27.1", - "@babel/plugin-transform-modules-commonjs": "^7.27.1", - "@babel/plugin-transform-typescript": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "packages/client/node_modules/@tanstack/react-table": { "version": "8.21.3", "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz",