diff --git a/__mocks__/@gorhom/bottom-sheet.tsx b/__mocks__/@gorhom/bottom-sheet.tsx index fad1e0e1..cf6e0062 100644 --- a/__mocks__/@gorhom/bottom-sheet.tsx +++ b/__mocks__/@gorhom/bottom-sheet.tsx @@ -1,14 +1,53 @@ -import React from 'react'; -import { View, FlatList } from 'react-native'; +import React, { forwardRef, type PropsWithChildren } from 'react'; +import { + View, + FlatList, + type FlatListProps, + type ScrollViewProps, + type ViewProps, +} from 'react-native'; -export const BottomSheetModal = React.forwardRef( - ({ children }: any, _ref: any) => <>{children} -); -export const BottomSheetView = ({ children, style }: any) => ( +export interface BottomSheetModalRef { + present: () => void; + dismiss: () => void; +} + +export type SheetAnimationConfig = Record; + +export const BottomSheetModal = forwardRef< + BottomSheetModalRef, + PropsWithChildren +>(({ children }, _ref) => <>{children}); +BottomSheetModal.displayName = 'BottomSheetModal'; + +export const BottomSheetView = ({ children, style }: ViewProps) => ( {children} ); -export const BottomSheetFlatList = (props: any) => ; + +export const BottomSheetScrollView = ({ + children, + contentContainerStyle, + testID, +}: ScrollViewProps) => ( + + {children} + +); + +export const BottomSheetFlatList = (props: FlatListProps) => ( + +); + export const BottomSheetBackdrop = () => null; -export const BottomSheetModalProvider = ({ children }: any) => <>{children}; -export const useBottomSheetTimingConfigs = (_config: any) => ({}); -export const useBottomSheetSpringConfigs = (_config: any) => ({}); + +export const BottomSheetModalProvider = ({ children }: PropsWithChildren) => ( + <>{children} +); + +export const useBottomSheet = () => ({ close: () => {} }); + +export const useBottomSheetTimingConfigs = (config: SheetAnimationConfig) => + config; + +export const useBottomSheetSpringConfigs = (config: SheetAnimationConfig) => + config; diff --git a/__mocks__/react-native-executorch-expo-resource-fetcher.ts b/__mocks__/react-native-executorch-expo-resource-fetcher.ts index 0dc7c1be..5cdcc098 100644 --- a/__mocks__/react-native-executorch-expo-resource-fetcher.ts +++ b/__mocks__/react-native-executorch-expo-resource-fetcher.ts @@ -1,4 +1,6 @@ export const ExpoResourceFetcher = { cancelFetching: jest.fn(), deleteResources: jest.fn(), + listDownloadedFiles: jest.fn(async () => [] as string[]), + getFilesTotalSize: jest.fn(async () => 0), }; diff --git a/__mocks__/react-native-reanimated.ts b/__mocks__/react-native-reanimated.ts index 956a3ac8..d0cc00ea 100644 --- a/__mocks__/react-native-reanimated.ts +++ b/__mocks__/react-native-reanimated.ts @@ -76,6 +76,7 @@ module.exports = { inOut: (fn: any) => fn, out: (fn: any) => fn, in: (fn: any) => fn, + bezier: () => (t: any) => t, }, interpolate: (val: any, inputRange: any, outputRange: any) => { if (val <= inputRange[0]) return outputRange[0]; diff --git a/__tests__/ChatBar.test.tsx b/__tests__/ChatBar.test.tsx index dc02b337..0aedee1a 100644 --- a/__tests__/ChatBar.test.tsx +++ b/__tests__/ChatBar.test.tsx @@ -1,5 +1,8 @@ import React from 'react'; import { render, screen, fireEvent, act } from '@testing-library/react-native'; +import type { LLMStore } from '../store/llmStore'; +import type { Attachment } from '../hooks/useAttachment'; +import type { PermissionStatus } from 'react-native-audio-api'; // ── mocks ───────────────────────────────────────────────────────────────────── @@ -13,17 +16,20 @@ jest.mock('../context/ThemeContext', () => ({ })); jest.mock('../store/llmStore', () => ({ - useLLMStore: jest.fn(() => ({ - isGenerating: false, - isProcessingPrompt: false, - interrupt: jest.fn(), - loadModel: jest.fn(), - model: null, - })), + useLLMStore: jest.fn((selector?: (state: Partial) => unknown) => { + const state = { + isGenerating: false, + isProcessingPrompt: false, + interrupt: jest.fn(), + loadModel: jest.fn(), + model: null, + }; + return selector ? selector(state) : state; + }), })); const mockUseAttachment = { - attachments: [] as any[], + attachments: [] as Attachment[], sheetRef: { current: null }, pickFromLibrary: jest.fn(), pickFromCamera: jest.fn(), @@ -45,7 +51,12 @@ jest.mock('../components/bottomSheets/AttachmentSheet', () => { onPickFromCamera, onPickDocument, isVisionModel, - }: any) => ( + }: { + onPickFromLibrary: () => void; + onPickFromCamera: () => void; + onPickDocument: () => void; + isVisionModel: boolean; + }) => ( {`vision:${isVisionModel}`} @@ -63,7 +74,13 @@ jest.mock('../components/bottomSheets/AttachmentSheet', () => { jest.mock('../components/chat-screen/AttachmentThumbnail', () => { const { View, TouchableOpacity, Text } = require('react-native'); - return ({ attachment, onRemove }: any) => ( + return ({ + attachment, + onRemove, + }: { + attachment: Attachment; + onRemove: () => void; + }) => ( {attachment.name || attachment.uri} { }); jest.mock('../components/chat-screen/ChatSpeechInput', () => { - const { View, TouchableOpacity, Text } = require('react-native'); - return ({ onSubmit, onCancel }: any) => ( + const { View, TouchableOpacity } = require('react-native'); + return ({ + onSubmit, + onCancel, + }: { + onSubmit: (transcript: string) => void; + onCancel: () => void; + }) => ( { jest.mock('../components/chat-screen/PromptSuggestions', () => { const { TouchableOpacity, Text } = require('react-native'); - return ({ onSelectPrompt }: any) => ( + return ({ onSelectPrompt }: { onSelectPrompt: (prompt: string) => void }) => ( onSelectPrompt('Suggested prompt')} @@ -114,7 +137,18 @@ jest.mock('../components/chat-screen/ChatBarActions', () => { onThinkingToggle, thinkingEnabled, onAttach, - }: any) => ( + }: { + userInput: string; + hasAttachments: boolean; + onSend: () => void; + isGenerating: boolean; + isProcessingPrompt: boolean; + onInterrupt: () => void; + onSpeechInput: () => void; + onThinkingToggle: () => void; + thinkingEnabled: boolean; + onAttach: () => void; + }) => ( + @@ -175,7 +209,7 @@ const defaultProps = { onSelectModel: jest.fn(), onSelectPrompt: jest.fn(), model: downloadedModel, - scrollRef: { current: null } as any, + scrollRef: { current: null }, isAtBottom: true, isVisionModel: false, thinkingEnabled: false, @@ -187,13 +221,18 @@ const renderBar = (props: Partial = {}) => render(); beforeEach(() => { - mockUseLLMStore.mockReturnValue({ - isGenerating: false, - isProcessingPrompt: false, - interrupt: jest.fn(), - loadModel: jest.fn(), - model: null, - }); + mockUseLLMStore.mockImplementation( + (selector?: (state: Partial) => unknown) => { + const state = { + isGenerating: false, + isProcessingPrompt: false, + interrupt: jest.fn(), + loadModel: jest.fn(), + model: null, + }; + return selector ? selector(state) : state; + } + ); mockUseAttachment.attachments = []; mockUseAttachment.openSheet.mockClear(); mockUseAttachment.clearAll.mockClear(); @@ -203,7 +242,7 @@ beforeEach(() => { jest.spyOn(console, 'warn').mockImplementation(() => {}); // Default: permission granted mockAudioManager.requestRecordingPermissions.mockResolvedValue( - 'Granted' as any + 'Granted' as PermissionStatus ); }); @@ -293,26 +332,36 @@ describe('downloaded model — text input', () => { describe('generating state', () => { it('shows interrupt button when isGenerating', () => { - mockUseLLMStore.mockReturnValue({ - isGenerating: true, - isProcessingPrompt: false, - interrupt: jest.fn(), - loadModel: jest.fn(), - model: null, - }); + mockUseLLMStore.mockImplementation( + (selector?: (state: Partial) => unknown) => { + const state = { + isGenerating: true, + isProcessingPrompt: false, + interrupt: jest.fn(), + loadModel: jest.fn(), + model: null, + }; + return selector ? selector(state) : state; + } + ); renderBar(); expect(screen.getByTestId('interrupt-btn')).toBeTruthy(); }); it('calls interrupt when interrupt button is pressed', () => { const interrupt = jest.fn(); - mockUseLLMStore.mockReturnValue({ - isGenerating: true, - isProcessingPrompt: false, - interrupt, - loadModel: jest.fn(), - model: null, - }); + mockUseLLMStore.mockImplementation( + (selector?: (state: Partial) => unknown) => { + const state = { + isGenerating: true, + isProcessingPrompt: false, + interrupt, + loadModel: jest.fn(), + model: null, + }; + return selector ? selector(state) : state; + } + ); renderBar(); fireEvent.press(screen.getByTestId('interrupt-btn')); expect(interrupt).toHaveBeenCalled(); @@ -332,7 +381,7 @@ describe('speech input', () => { it('shows toast and stays in text mode when microphone permission is denied', async () => { mockAudioManager.requestRecordingPermissions.mockResolvedValue( - 'Denied' as any + 'Denied' as PermissionStatus ); renderBar(); await act(async () => { @@ -405,7 +454,7 @@ describe('speech input', () => { // Override speech mock to submit empty string jest.mock('../components/chat-screen/ChatSpeechInput', () => { const { View, TouchableOpacity } = require('react-native'); - return ({ onSubmit }: any) => ( + return ({ onSubmit }: { onSubmit: (transcript: string) => void }) => ( = (state: MockLLMState) => T; + +type ThinkingBlockMockProps = { + content: string; + isComplete: boolean; + inProgress: boolean; +}; + +type BottomSheetModalHandle = { + present: jest.Mock; + dismiss: jest.Mock; +}; + +type BottomSheetModalMockProps = { + children?: React.ReactNode; +}; jest.mock('../context/ThemeContext', () => ({ useTheme: () => ({ @@ -14,7 +34,10 @@ jest.mock('../context/ThemeContext', () => ({ })); jest.mock('../store/llmStore', () => ({ - useLLMStore: jest.fn(() => ({ isGenerating: false })), + useLLMStore: jest.fn((selector?: MockLLMSelector) => { + const state = { isGenerating: false, isProcessingPrompt: false }; + return selector ? selector(state) : state; + }), })); jest.mock('../components/chat-screen/MarkdownComponent', () => { @@ -24,7 +47,7 @@ jest.mock('../components/chat-screen/MarkdownComponent', () => { jest.mock('../components/chat-screen/ThinkingBlock', () => { const { Text } = require('react-native'); - return ({ content, isComplete, inProgress }: any) => ( + return ({ content, isComplete, inProgress }: ThinkingBlockMockProps) => ( { jest.mock('../components/chat-screen/AnimatedChatLoading', () => () => null); +jest.mock('@gorhom/bottom-sheet', () => { + const MockReact = require('react') as typeof import('react'); + const { View } = require('react-native'); + + const BottomSheetModal = MockReact.forwardRef< + BottomSheetModalHandle, + BottomSheetModalMockProps + >(({ children }, ref) => { + MockReact.useImperativeHandle(ref, () => ({ + present: jest.fn(), + dismiss: jest.fn(), + })); + return {children}; + }); + + return { + BottomSheetBackdrop: (props: ViewProps) => , + BottomSheetModal, + BottomSheetView: View, + BottomSheetScrollView: View, + useBottomSheet: () => ({ close: jest.fn() }), + useBottomSheetSpringConfigs: (config: unknown) => config, + }; +}); + // ── helpers ─────────────────────────────────────────────────────────────────── import MessageItem from '../components/chat-screen/MessageItem'; @@ -43,14 +91,9 @@ import { useLLMStore } from '../store/llmStore'; const mockUseLLMStore = useLLMStore as unknown as jest.Mock; -// The component reads the store via selectors; make the mock honor them. -const mockLLMState = (state: { - isGenerating?: boolean; - isProcessingPrompt?: boolean; -}) => - mockUseLLMStore.mockImplementation( - (selector?: (s: typeof state) => unknown) => - selector ? selector(state) : state +const setLLMState = (state: MockLLMState) => + mockUseLLMStore.mockImplementation((selector?: MockLLMSelector) => + selector ? selector(state) : state ); const baseMessage = { @@ -75,7 +118,7 @@ const renderItem = ( ); beforeEach(() => { - mockLLMState({ isGenerating: false }); + setLLMState({ isGenerating: false, isProcessingPrompt: false }); jest.spyOn(console, 'error').mockImplementation(() => {}); }); @@ -133,6 +176,67 @@ describe('assistant messages', () => { renderItem({ role: 'assistant', content: 'Hi' }); expect(screen.queryByText(/tps:/)).toBeNull(); }); + + it('hands deduplicated sources to onShowSources when the sources button is pressed', () => { + const onShowSources = jest.fn(); + renderItem({ + role: 'assistant', + content: 'The answer is in the report.', + userQuestion: 'What was the revenue?', + sourceDocuments: [ + { documentId: 1, name: 'financial_report.pdf' }, + { documentId: 1, name: 'financial_report.pdf' }, + ], + onShowSources, + }); + + expect(screen.getByTestId('source-action-button')).toBeTruthy(); + expect(screen.getByLabelText('Sources')).toBeTruthy(); + + fireEvent.press(screen.getByTestId('source-action-button')); + + expect(onShowSources).toHaveBeenCalledWith( + [{ documentId: 1, name: 'financial_report.pdf' }], + 'What was the revenue?' + ); + }); + + it('strips inline [n] citation markers from the rendered answer', () => { + renderItem({ + role: 'assistant', + content: 'The total was 100 [1].', + sourceDocuments: [{ documentId: 1, name: 'financial_report.pdf' }], + }); + + const markdown = screen.getByTestId('markdown'); + expect(markdown.props.children).toBe('The total was 100.'); + }); + + it('does not render source actions for user messages', () => { + renderItem({ + role: 'user', + content: 'Question', + sourceDocuments: [{ documentId: 1, name: 'notes.txt' }], + }); + + expect(screen.queryByTestId('source-action-button')).toBeNull(); + }); + + it('does not render source actions while the last assistant message is generating', () => { + setLLMState({ + isGenerating: true, + isProcessingPrompt: false, + }); + + renderItem({ + role: 'assistant', + content: 'Streaming answer', + isLastMessage: true, + sourceDocuments: [{ documentId: 1, name: 'report.pdf' }], + }); + + expect(screen.queryByTestId('source-action-button')).toBeNull(); + }); }); // ─── user messages ──────────────────────────────────────────────────────────── @@ -267,14 +371,14 @@ describe('thinking block parsing', () => { }); it('marks ThinkingBlock as inProgress when last message and isGenerating and thinking is incomplete', () => { - mockLLMState({ isGenerating: true }); + setLLMState({ isGenerating: true, isProcessingPrompt: false }); renderItem({ content: 'working...', isLastMessage: true }); const block = screen.getByTestId('thinking-block'); expect(block.props.accessibilityLabel).toContain('inProgress:true'); }); it('does not mark ThinkingBlock as inProgress when not isLastMessage', () => { - mockLLMState({ isGenerating: true }); + setLLMState({ isGenerating: true, isProcessingPrompt: false }); renderItem({ content: 'working...', isLastMessage: false }); const block = screen.getByTestId('thinking-block'); expect(block.props.accessibilityLabel).toContain('inProgress:false'); diff --git a/__tests__/SourcesSheet.test.tsx b/__tests__/SourcesSheet.test.tsx new file mode 100644 index 00000000..5fe88fd2 --- /dev/null +++ b/__tests__/SourcesSheet.test.tsx @@ -0,0 +1,131 @@ +import React from 'react'; +import type { ViewProps } from 'react-native'; +import { act, fireEvent, render, screen } from '@testing-library/react-native'; +import type { SourceDocument } from '../database/chatRepository'; + +type BottomSheetModalHandle = { + present: jest.Mock; + dismiss: jest.Mock; +}; + +type BottomSheetModalMockProps = { + children?: React.ReactNode; +}; + +jest.mock('../context/ThemeContext', () => ({ + useTheme: () => ({ + theme: { + ...require('../styles/colors').lightTheme, + insets: { top: 0, bottom: 0, left: 0, right: 0 }, + }, + }), +})); + +jest.mock('@gorhom/bottom-sheet', () => { + const MockReact = require('react') as typeof import('react'); + const { View } = require('react-native'); + + const BottomSheetModal = MockReact.forwardRef< + BottomSheetModalHandle, + BottomSheetModalMockProps + >(({ children }, ref) => { + MockReact.useImperativeHandle(ref, () => ({ + present: jest.fn(), + dismiss: jest.fn(), + })); + return {children}; + }); + + return { + BottomSheetBackdrop: (props: ViewProps) => , + BottomSheetModal, + BottomSheetView: View, + BottomSheetScrollView: View, + useBottomSheet: () => ({ close: jest.fn() }), + useBottomSheetSpringConfigs: (config: unknown) => config, + }; +}); + +import SourcesSheet, { + type SourcesSheetHandle, +} from '../components/chat-screen/SourcesSheet'; + +const presentWith = (sources: SourceDocument[], userQuestion?: string) => { + const ref = React.createRef(); + render(); + act(() => { + ref.current?.present(sources, userQuestion); + }); + return ref; +}; + +describe('SourcesSheet', () => { + it('renders nothing until present() supplies sources', () => { + render(()} />); + + expect(screen.queryByTestId('source-item')).toBeNull(); + }); + + it('renders the sources handed to present()', () => { + presentWith([{ documentId: 1, name: 'financial_report.pdf' }]); + + expect(screen.getByText('financial_report.pdf')).toBeTruthy(); + expect(screen.getByText('PDF')).toBeTruthy(); + expect(screen.queryByText('Source document')).toBeNull(); + }); + + it('ignores a second present() while the sheet is still open', () => { + const ref = presentWith([{ documentId: 1, name: 'first.pdf' }]); + + act(() => { + ref.current?.present([{ documentId: 2, name: 'second.pdf' }]); + }); + + expect(screen.getByText('first.pdf')).toBeTruthy(); + expect(screen.queryByText('second.pdf')).toBeNull(); + }); + + it('keeps the cited passage collapsed until the source row is tapped', () => { + presentWith([ + { + documentId: 1, + name: 'financial_report.pdf', + passage: 'Net revenue grew 12% year over year.', + }, + ]); + + expect(screen.queryByTestId('source-passage')).toBeNull(); + + fireEvent.press(screen.getByTestId('source-item')); + + expect(screen.getByTestId('source-passage')).toBeTruthy(); + expect( + screen.getByText('Net revenue grew 12% year over year.') + ).toBeTruthy(); + }); + + it('emphasises the passage span relevant to the user question', () => { + const passage = + 'Intro sentence. Net revenue grew 12% year over year. Outro.'; + presentWith( + [{ documentId: 1, name: 'financial_report.pdf', passage }], + 'What was the net revenue growth?' + ); + + fireEvent.press(screen.getByTestId('source-item')); + + const cited = screen.getByText('Net revenue grew 12% year over year.'); + expect(cited).toBeTruthy(); + expect(cited.props.style).toEqual( + expect.objectContaining({ fontFamily: expect.any(String) }) + ); + }); + + it('does not render a passage block when the source has no passage', () => { + presentWith([{ documentId: 1, name: 'financial_report.pdf' }]); + + fireEvent.press(screen.getByTestId('source-item')); + + expect(screen.queryByTestId('source-passage')).toBeNull(); + }); +}); diff --git a/__tests__/VectorStoreContext.test.tsx b/__tests__/VectorStoreContext.test.tsx new file mode 100644 index 00000000..7e4ca0ce --- /dev/null +++ b/__tests__/VectorStoreContext.test.tsx @@ -0,0 +1,154 @@ +import React from 'react'; +import { render, act } from '@testing-library/react-native'; + +const mockStoreInstances: Array<{ + name: string; + db: unknown; + load: jest.Mock; + unload: jest.Mock; +}> = []; + +jest.mock('@react-native-rag/op-sqlite', () => ({ + OPSQLiteVectorStore: jest + .fn() + .mockImplementation((opts: { name: string }) => { + const instance = { + name: opts.name, + db: { __fakeDb: true }, + load: jest.fn().mockResolvedValue(undefined), + unload: jest.fn().mockResolvedValue(undefined), + }; + mockStoreInstances.push(instance); + return instance; + }), +})); + +jest.mock('expo-sqlite', () => { + const db = { __db: true }; + return { useSQLiteContext: jest.fn(() => db) }; +}); + +jest.mock('../utils/embeddingModelMigration', () => ({ + migrateEmbeddingModelIfNeeded: jest.fn().mockResolvedValue(undefined), +})); + +jest.mock('../utils/embeddingModel', () => ({ + isEmbeddingModelDownloaded: jest.fn().mockResolvedValue(true), +})); + +jest.mock('../utils/lfmEmbeddings', () => ({ + LFMEmbeddings: jest.fn().mockImplementation(() => ({})), +})); + +jest.mock('../store/embeddingModelStore', () => ({ + useEmbeddingModelStore: { + getState: jest.fn(() => ({ + setProgress: jest.fn(), + markReady: jest.fn(), + setStatus: jest.fn(), + })), + }, +})); + +import { VectorStoreProvider } from '../context/VectorStoreContext'; +import { migrateEmbeddingModelIfNeeded } from '../utils/embeddingModelMigration'; + +const mockMigrate = migrateEmbeddingModelIfNeeded as jest.Mock; + +const createDeferred = () => { + let resolve!: (value: T) => void; + const promise = new Promise((promiseResolve) => { + resolve = promiseResolve; + }); + return { promise, resolve }; +}; + +const flush = async () => { + await act(async () => { + await new Promise((resolve) => setImmediate(resolve)); + await new Promise((resolve) => setImmediate(resolve)); + }); +}; + +const renderProvider = () => + render({null}); + +beforeEach(() => { + jest.clearAllMocks(); + mockStoreInstances.length = 0; + mockMigrate.mockResolvedValue(undefined); +}); + +afterEach(async () => { + await flush(); +}); + +describe('VectorStoreProvider init/teardown chain', () => { + it('mount → unmount → mount does a single unload per store', async () => { + const first = renderProvider(); + await flush(); + expect(mockStoreInstances).toHaveLength(1); + expect(mockStoreInstances[0].unload).not.toHaveBeenCalled(); + + first.unmount(); + await flush(); + expect(mockStoreInstances[0].unload).toHaveBeenCalledTimes(1); + + renderProvider(); + await flush(); + expect(mockStoreInstances).toHaveLength(2); + expect(mockStoreInstances[0].unload).toHaveBeenCalledTimes(1); + expect(mockStoreInstances[1].unload).not.toHaveBeenCalled(); + }); + + it('unmount during init unloads the store exactly once', async () => { + const deferred = createDeferred(); + mockMigrate.mockReturnValueOnce(deferred.promise); + + const { unmount } = renderProvider(); + await flush(); + expect(mockStoreInstances).toHaveLength(1); + const store = mockStoreInstances[0]; + expect(store.unload).not.toHaveBeenCalled(); + + unmount(); + await flush(); + + await act(async () => { + deferred.resolve(); + }); + await flush(); + + expect(store.unload).toHaveBeenCalledTimes(1); + }); + + it('cancel during migrate skips load and does not publish the store', async () => { + const deferred = createDeferred(); + mockMigrate.mockReturnValueOnce(deferred.promise); + + const { unmount } = renderProvider(); + await flush(); + const store = mockStoreInstances[0]; + + unmount(); + await flush(); + + await act(async () => { + deferred.resolve(); + }); + await flush(); + + expect(mockMigrate).toHaveBeenCalledTimes(1); + expect(store.load).not.toHaveBeenCalled(); + expect(store.unload).toHaveBeenCalledTimes(1); + }); + + it('cancel before init reaches the store creates and unloads nothing', async () => { + const { unmount } = renderProvider(); + unmount(); + await flush(); + + expect(mockStoreInstances).toHaveLength(0); + expect(mockMigrate).not.toHaveBeenCalled(); + }); +}); diff --git a/__tests__/chatRepository.test.ts b/__tests__/chatRepository.test.ts index 467a0172..58c6f8da 100644 --- a/__tests__/chatRepository.test.ts +++ b/__tests__/chatRepository.test.ts @@ -15,7 +15,7 @@ jest.mock('@react-native-async-storage/async-storage', () => ({ describe('persistMessage with imagePath', () => { it('includes imagePath in INSERT when provided', async () => { const runAsync = jest.fn().mockResolvedValue({ lastInsertRowId: 1 }); - const mockDb = { runAsync } as SQLiteDatabase; + const mockDb = { runAsync } as Partial as SQLiteDatabase; await persistMessage(mockDb, { role: 'user', @@ -32,7 +32,7 @@ describe('persistMessage with imagePath', () => { it('passes null imagePath when not provided', async () => { const runAsync = jest.fn().mockResolvedValue({ lastInsertRowId: 2 }); - const mockDb = { runAsync } as SQLiteDatabase; + const mockDb = { runAsync } as Partial as SQLiteDatabase; await persistMessage(mockDb, { role: 'user', @@ -45,6 +45,31 @@ describe('persistMessage with imagePath', () => { expect.arrayContaining([null]) ); }); + + it('serializes sourceDocuments when provided', async () => { + const runAsync = jest.fn().mockResolvedValue({ lastInsertRowId: 3 }); + const mockDb = { runAsync } as Partial as SQLiteDatabase; + const sourceDocuments = [ + { + documentId: 7, + name: 'financial_report.pdf', + passage: 'Revenue increased.', + similarity: 0.82, + }, + ]; + + await persistMessage(mockDb, { + role: 'assistant', + content: 'Revenue increased.', + chatId: 1, + sourceDocuments, + }); + + expect(runAsync).toHaveBeenCalledWith( + expect.stringContaining('sourceDocuments'), + expect.arrayContaining([JSON.stringify(sourceDocuments)]) + ); + }); }); describe('forkChat', () => { diff --git a/__tests__/chatStore.test.ts b/__tests__/chatStore.test.ts index beabca5e..a8df898f 100644 --- a/__tests__/chatStore.test.ts +++ b/__tests__/chatStore.test.ts @@ -1,12 +1,14 @@ import { useChatStore } from '../store/chatStore'; import * as chatRepository from '../database/chatRepository'; import * as sourcesRepository from '../database/sourcesRepository'; +import type { SQLiteDatabase } from 'expo-sqlite'; +import type { Model } from '../database/modelRepository'; // Mock all DB interactions jest.mock('../database/chatRepository'); jest.mock('../database/sourcesRepository'); -const mockDb = {} as any; +const mockDb = {} as Partial as SQLiteDatabase; const mockChat = (id: number, lastUsed = Date.now()) => ({ id, @@ -233,9 +235,7 @@ describe('enableSource', () => { }); it('calls activateSource and updates state for a real chat', async () => { - (sourcesRepository.activateSource as jest.Mock).mockResolvedValue( - undefined - ); + (sourcesRepository.activateSource as jest.Mock).mockResolvedValue(true); useChatStore.setState({ chats: [{ ...mockChat(1), enabledSources: [2] }], phantomChat: null, @@ -246,6 +246,18 @@ describe('enableSource', () => { expect(sourcesRepository.activateSource).toHaveBeenCalledWith(mockDb, 1, 7); expect(useChatStore.getState().chats[0].enabledSources).toEqual([2, 7]); }); + + it('does not update state when source activation is skipped', async () => { + (sourcesRepository.activateSource as jest.Mock).mockResolvedValue(false); + useChatStore.setState({ + chats: [{ ...mockChat(1), enabledSources: [2] }], + phantomChat: null, + }); + + await useChatStore.getState().enableSource(1, 7); + + expect(useChatStore.getState().chats[0].enabledSources).toEqual([2]); + }); }); describe('setPhantomChatSettings', () => { @@ -281,9 +293,9 @@ describe('initPhantomChat with model system prompt', () => { systemPrompt: 'global default', }); - await useChatStore - .getState() - .initPhantomChat(99, { systemPrompt: modelPrompt } as any); + await useChatStore.getState().initPhantomChat(99, { + systemPrompt: modelPrompt, + } as Partial as Model); const phantom = useChatStore.getState().phantomChat; expect(phantom?.settings?.systemPrompt).toBe(modelPrompt); @@ -296,7 +308,7 @@ describe('initPhantomChat with model system prompt', () => { await useChatStore .getState() - .initPhantomChat(99, { systemPrompt: null } as any); + .initPhantomChat(99, { systemPrompt: null } as Partial as Model); const phantom = useChatStore.getState().phantomChat; expect(phantom?.settings?.systemPrompt).toBe('global default'); diff --git a/__tests__/citationHighlight.test.ts b/__tests__/citationHighlight.test.ts new file mode 100644 index 00000000..55b69462 --- /dev/null +++ b/__tests__/citationHighlight.test.ts @@ -0,0 +1,117 @@ +import { findCitedSpan, queryNamesDocument } from '../utils/citationHighlight'; + +describe('queryNamesDocument', () => { + it('matches when every filename token appears in the query', () => { + expect( + queryNamesDocument( + 'Co jest w pliku polityka_urlopowa_2026.txt', + 'polityka_urlopowa_2026.txt' + ) + ).toBe(true); + }); + + it('does not match an unrelated document sharing only an incidental term', () => { + expect( + queryNamesDocument( + 'Co jest w pliku polityka_urlopowa_2026.txt', + 'sample.pdf' + ) + ).toBe(false); + }); + + it('returns false when the query names no document', () => { + expect( + queryNamesDocument('O czym jest ten plik?', 'raport_finansowy.pdf') + ).toBe(false); + }); +}); + +describe('findCitedSpan', () => { + it('returns the span of the sentence most relevant to the query', () => { + const passage = + 'The company was founded in 1998. Total revenue reached 2455 PLN last year. Employees enjoy free coffee.'; + const span = findCitedSpan(passage, 'What was the total revenue?'); + + expect(span).not.toBeNull(); + const cited = passage.slice(span!.start, span!.end); + expect(cited).toBe('Total revenue reached 2455 PLN last year.'); + }); + + it('matches identifiers/numbers even when short', () => { + const passage = + 'Ogólne warunki umowy. Faktura FS-219039 na kwotę 2455,01 PLN. Dziękujemy za współpracę.'; + const span = findCitedSpan(passage, 'Ile wynosi faktura FS-219039?'); + + expect(span).not.toBeNull(); + const cited = passage.slice(span!.start, span!.end); + expect(cited).toContain('FS-219039'); + }); + + it('picks the narrowest (densest) sentence when scores tie', () => { + const passage = + 'Revenue. This long sentence also mentions revenue but pads it with a great many additional unrelated words.'; + const span = findCitedSpan(passage, 'revenue'); + + expect(span).not.toBeNull(); + expect(passage.slice(span!.start, span!.end)).toBe('Revenue.'); + }); + + it('matches an inflected passage word to its query stem (Polish)', () => { + const passage = + 'Czy moje dane sa bezpieczne? Wszystkie modele dzialaja lokalnie, a dane nigdy nie opuszczaja urzadzenia.'; + const span = findCitedSpan(passage, 'Czy moje dane opuszczaja urzadzenie?'); + + expect(span).not.toBeNull(); + const cited = passage.slice(span!.start, span!.end); + expect(cited).toBe( + 'Wszystkie modele dzialaja lokalnie, a dane nigdy nie opuszczaja urzadzenia.' + ); + }); + + it('does not stem-match short tokens or identifiers', () => { + const span = findCitedSpan('Zupa dania obiadowe.', 'gdzie sa dane'); + expect(span).toBeNull(); + }); + + it('returns null when nothing overlaps', () => { + expect( + findCitedSpan('Completely unrelated content here.', 'quarterly revenue') + ).toBeNull(); + }); + + it('ignores stopword-only queries', () => { + expect(findCitedSpan('Some real content.', 'what is the')).toBeNull(); + }); + + it('handles empty/undefined input safely', () => { + expect(findCitedSpan(undefined, 'revenue')).toBeNull(); + expect(findCitedSpan('', 'revenue')).toBeNull(); + expect(findCitedSpan('Content.', '')).toBeNull(); + }); + + it('produces offsets that map back onto the original passage', () => { + const passage = 'Intro line.\nThe invoice number is 12345.\nOutro.'; + const span = findCitedSpan(passage, 'invoice 12345'); + + expect(span).not.toBeNull(); + expect(passage.slice(span!.start, span!.end)).toBe( + 'The invoice number is 12345.' + ); + }); + + it('does not highlight on a single weak (stem-only) match', () => { + const span = findCitedSpan( + 'Skanowanie kodu QR ulatwia pobranie dokumentu z systemu KSeF.', + 'Co jest kupowane w dokumencie' + ); + expect(span).toBeNull(); + }); + + it('still highlights when two terms match by stem', () => { + const span = findCitedSpan( + 'Instrukcja obslugi urzadzenia oraz dokumentacji technicznej.', + 'urzadzenie i dokumentacja' + ); + expect(span).not.toBeNull(); + }); +}); diff --git a/__tests__/citations.test.ts b/__tests__/citations.test.ts new file mode 100644 index 00000000..4886004a --- /dev/null +++ b/__tests__/citations.test.ts @@ -0,0 +1,29 @@ +import { stripCitations } from '../utils/citations'; + +describe('stripCitations', () => { + it('removes a single citation marker and tidies punctuation spacing', () => { + expect(stripCitations('The total was 100 [1].')).toBe('The total was 100.'); + }); + + it('removes grouped citation markers', () => { + expect(stripCitations('Backed by [1][3] as noted.')).toBe( + 'Backed by as noted.' + ); + }); + + it('collapses doubled spaces left behind mid-sentence', () => { + expect(stripCitations('See [2] the report.')).toBe('See the report.'); + }); + + it('leaves text without citations untouched', () => { + expect(stripCitations('No markers here.')).toBe('No markers here.'); + }); + + it('does not strip long bracketed numbers (e.g. array indices)', () => { + expect(stripCitations('arr[1234] value')).toBe('arr[1234] value'); + }); + + it('returns empty/falsy text unchanged', () => { + expect(stripCitations('')).toBe(''); + }); +}); diff --git a/__tests__/dbMigration.test.ts b/__tests__/dbMigration.test.ts new file mode 100644 index 00000000..e4dd15be --- /dev/null +++ b/__tests__/dbMigration.test.ts @@ -0,0 +1,74 @@ +import type { SQLiteDatabase } from 'expo-sqlite'; +import { migrateLegacyVectorStore } from '../database/vectorStoreMigration'; + +const makeDb = (opts: { + hasLegacyVectorsTable: boolean; + documentColumnMissing?: boolean; +}) => { + const getFirstAsync: jest.Mock = jest.fn(async (sql: string) => { + if (sql.includes('sqlite_master')) { + return opts.hasLegacyVectorsTable ? { 1: 1 } : null; + } + return null; + }); + const execAsync: jest.Mock = jest.fn(async (sql: string) => { + if ( + sql.includes('SELECT document FROM vectors') && + opts.documentColumnMissing + ) { + throw new Error('no such column: document'); + } + }); + const runAsync: jest.Mock = jest.fn(async () => ({})); + + return { + db: { + getFirstAsync, + execAsync, + runAsync, + } as Partial as SQLiteDatabase, + getFirstAsync, + execAsync, + runAsync, + }; +}; + +const deleteCalls = (runAsync: jest.Mock): string[] => + runAsync.mock.calls.map((call) => call[0] as string); + +describe('migrateLegacyVectorStore', () => { + it('does NOT wipe sources when no legacy vectors table exists (current dual-db setup)', async () => { + const { db, execAsync, runAsync } = makeDb({ + hasLegacyVectorsTable: false, + }); + + await migrateLegacyVectorStore(db); + + expect(execAsync).not.toHaveBeenCalled(); + expect(runAsync).not.toHaveBeenCalled(); + }); + + it('wipes the legacy vectors table and orphaned sources when the table lacks a document column', async () => { + const { db, runAsync } = makeDb({ + hasLegacyVectorsTable: true, + documentColumnMissing: true, + }); + + await migrateLegacyVectorStore(db); + + const calls = deleteCalls(runAsync); + expect(calls.some((sql) => /DELETE FROM chatSources/.test(sql))).toBe(true); + expect(calls.some((sql) => /DELETE FROM sources/.test(sql))).toBe(true); + }); + + it('leaves data intact when a legacy vectors table already has the document column', async () => { + const { db, runAsync } = makeDb({ + hasLegacyVectorsTable: true, + documentColumnMissing: false, + }); + + await migrateLegacyVectorStore(db); + + expect(runAsync).not.toHaveBeenCalled(); + }); +}); diff --git a/__tests__/dbSchemaMigration.test.ts b/__tests__/dbSchemaMigration.test.ts new file mode 100644 index 00000000..49336a6a --- /dev/null +++ b/__tests__/dbSchemaMigration.test.ts @@ -0,0 +1,187 @@ +import type { SQLiteDatabase } from 'expo-sqlite'; + +jest.mock('../store/chatStore', () => ({ + useChatStore: { getState: () => ({}) }, +})); +jest.mock('../store/llmStore', () => ({ + useLLMStore: { getState: () => ({}) }, +})); +jest.mock('../store/modelStore', () => ({ + useModelStore: { getState: () => ({}) }, +})); +jest.mock('../store/sourceStore', () => ({ + useSourceStore: { getState: () => ({}) }, +})); +jest.mock('../database/modelRepository', () => ({ addModel: jest.fn() })); +jest.mock('../utils/sourceLinkingBoundary', () => ({ + initSourceLinkingBoundary: jest.fn(), +})); +jest.mock('../constants/default-models', () => ({ DEFAULT_MODELS: [] })); + +import { runMigrations } from '../database/db'; + +type Schema = Record; + +const oldSchema = (): Schema => ({ + models: ['id', 'name', 'source'], + messages: ['id', 'chatId', 'content', 'role'], + chatSettings: ['id', 'chatId', 'contextWindow'], + sources: ['id', 'name'], + chats: ['id', 'title'], + benchmarks: ['id'], + chatSources: ['chatId', 'sourceId'], +}); + +class FakeDb { + tables: Schema; + alterLog: string[] = []; + failOn: ((sql: string) => boolean) | null = null; + + constructor(schema: Schema) { + this.tables = schema; + } + + getAllAsync = async (sql: string) => { + const match = /PRAGMA table_info\((\w+)\)/.exec(sql); + if (match) return (this.tables[match[1]] ?? []).map((name) => ({ name })); + return []; + }; + + execAsync = async (sql: string) => { + if (this.failOn?.(sql)) throw new Error('simulated migration failure'); + + const add = /ALTER TABLE (\w+) ADD COLUMN (\w+)/.exec(sql); + if (add) { + const [, table, column] = add; + this.alterLog.push(sql); + if ((this.tables[table] ?? []).includes(column)) { + throw new Error(`duplicate column name: ${column}`); + } + this.tables[table] = [...(this.tables[table] ?? []), column]; + return; + } + + const drop = /ALTER TABLE (\w+) DROP COLUMN (\w+)/.exec(sql); + if (drop) { + const [, table, column] = drop; + this.alterLog.push(sql); + this.tables[table] = (this.tables[table] ?? []).filter( + (name) => name !== column + ); + } + }; + + getFirstAsync = async () => null; + runAsync = async () => ({}); + withTransactionAsync = async (fn: () => Promise) => fn(); + + asDb() { + return this as unknown as SQLiteDatabase; + } +} + +const has = (db: FakeDb, table: string, column: string) => + (db.tables[table] ?? []).includes(column); + +describe('runMigrations from a real old (pre-RAG) schema', () => { + it('adds every missing column onto the old schema', async () => { + const db = new FakeDb(oldSchema()); + + await runMigrations(db.asDb()); + + for (const column of [ + 'featured', + 'experimental', + 'family', + 'thinking', + 'labels', + 'vision', + 'systemPrompt', + ]) { + expect(has(db, 'models', column)).toBe(true); + } + expect(has(db, 'messages', 'imagePath')).toBe(true); + expect(has(db, 'messages', 'documentName')).toBe(true); + expect(has(db, 'messages', 'sourceDocuments')).toBe(true); + expect(has(db, 'chatSettings', 'thinkingEnabled')).toBe(true); + expect(has(db, 'sources', 'firstChunk')).toBe(true); + }); + + it('drops contextWindow only when it is present', async () => { + const withColumn = new FakeDb(oldSchema()); + await runMigrations(withColumn.asDb()); + expect(has(withColumn, 'chatSettings', 'contextWindow')).toBe(false); + expect(withColumn.alterLog.some((sql) => sql.includes('DROP COLUMN'))).toBe( + true + ); + + const withoutColumn = new FakeDb({ + ...oldSchema(), + chatSettings: ['id', 'chatId'], + }); + await runMigrations(withoutColumn.asDb()); + expect( + withoutColumn.alterLog.some((sql) => sql.includes('DROP COLUMN')) + ).toBe(false); + }); + + it('is idempotent: a second run alters nothing and does not throw', async () => { + const db = new FakeDb(oldSchema()); + await runMigrations(db.asDb()); + db.alterLog.length = 0; + + await expect(runMigrations(db.asDb())).resolves.toBeUndefined(); + expect(db.alterLog).toEqual([]); + }); + + it('is already-migrated safe: running on a fresh full schema is a no-op', async () => { + const db = new FakeDb({ + models: [ + 'id', + 'name', + 'source', + 'featured', + 'experimental', + 'family', + 'thinking', + 'labels', + 'vision', + 'systemPrompt', + ], + messages: [ + 'id', + 'chatId', + 'content', + 'role', + 'imagePath', + 'documentName', + 'sourceDocuments', + ], + chatSettings: ['id', 'chatId', 'thinkingEnabled'], + sources: ['id', 'name', 'firstChunk'], + }); + + await runMigrations(db.asDb()); + expect(db.alterLog).toEqual([]); + }); + + it('is not atomic but recovers: a mid-migration failure is completed by a re-run', async () => { + const db = new FakeDb(oldSchema()); + db.failOn = (sql) => sql.includes('ADD COLUMN sourceDocuments'); + + await expect(runMigrations(db.asDb())).rejects.toThrow( + 'simulated migration failure' + ); + + expect(has(db, 'models', 'featured')).toBe(true); + expect(has(db, 'messages', 'imagePath')).toBe(true); + expect(has(db, 'messages', 'documentName')).toBe(true); + expect(has(db, 'messages', 'sourceDocuments')).toBe(false); + expect(has(db, 'sources', 'firstChunk')).toBe(false); + + db.failOn = null; + await expect(runMigrations(db.asDb())).resolves.toBeUndefined(); + expect(has(db, 'messages', 'sourceDocuments')).toBe(true); + expect(has(db, 'sources', 'firstChunk')).toBe(true); + }); +}); diff --git a/__tests__/embeddingModelMigration.test.ts b/__tests__/embeddingModelMigration.test.ts new file mode 100644 index 00000000..46e68383 --- /dev/null +++ b/__tests__/embeddingModelMigration.test.ts @@ -0,0 +1,178 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { migrateEmbeddingModelIfNeeded } from '../utils/embeddingModelMigration'; +import { ACTIVE_EMBEDDING_MODEL_KEY } from '../constants/embedding-model'; +import type { OPSQLiteVectorStore } from '@react-native-rag/op-sqlite'; +import type { SQLiteDatabase } from 'expo-sqlite'; + +const CURRENT_DIM = 1024; + +const makeVectorStore = (persistedDim?: number) => { + const deleteVectorStore = jest.fn().mockResolvedValue(undefined); + const execute = jest.fn().mockResolvedValue({ + rows: + persistedDim === undefined + ? [] + : [ + { + sql: `CREATE TABLE vectors (id TEXT, embedding F32_BLOB(${persistedDim}) NOT NULL)`, + }, + ], + }); + return { + store: { + deleteVectorStore, + db: { execute }, + } as unknown as OPSQLiteVectorStore, + deleteVectorStore, + execute, + }; +}; + +const makeDb = () => { + const runAsync = jest.fn().mockResolvedValue(undefined); + return { db: { runAsync } as unknown as SQLiteDatabase, runAsync }; +}; + +beforeEach(async () => { + jest.clearAllMocks(); + await AsyncStorage.clear(); + jest.spyOn(console, 'warn').mockImplementation(() => {}); +}); + +afterEach(() => { + jest.restoreAllMocks(); +}); + +describe('migrateEmbeddingModelIfNeeded', () => { + it('no-ops when the stored model matches the current one', async () => { + await AsyncStorage.setItem(ACTIVE_EMBEDDING_MODEL_KEY, 'model-a'); + const { store, deleteVectorStore } = makeVectorStore(); + const { db, runAsync } = makeDb(); + + const migrated = await migrateEmbeddingModelIfNeeded( + store, + db, + 'model-a', + CURRENT_DIM + ); + + expect(migrated).toBe(false); + expect(deleteVectorStore).not.toHaveBeenCalled(); + expect(runAsync).not.toHaveBeenCalled(); + }); + + it('adopts the current model without wiping on a fresh install (no vectors table)', async () => { + const { store, deleteVectorStore } = makeVectorStore(); + const { db, runAsync } = makeDb(); + + const migrated = await migrateEmbeddingModelIfNeeded( + store, + db, + 'model-a', + CURRENT_DIM + ); + + expect(migrated).toBe(false); + expect(deleteVectorStore).not.toHaveBeenCalled(); + expect(runAsync).not.toHaveBeenCalled(); + expect(await AsyncStorage.getItem(ACTIVE_EMBEDDING_MODEL_KEY)).toBe( + 'model-a' + ); + }); + + it('adopts without wiping when a lost key sits over dimension-compatible vectors', async () => { + const { store, deleteVectorStore } = makeVectorStore(CURRENT_DIM); + const { db, runAsync } = makeDb(); + + const migrated = await migrateEmbeddingModelIfNeeded( + store, + db, + 'model-a', + CURRENT_DIM + ); + + expect(migrated).toBe(false); + expect(deleteVectorStore).not.toHaveBeenCalled(); + expect(runAsync).not.toHaveBeenCalled(); + expect(await AsyncStorage.getItem(ACTIVE_EMBEDDING_MODEL_KEY)).toBe( + 'model-a' + ); + }); + + it('wipes legacy pre-key sources when the persisted dimension is incompatible (384 to 1024)', async () => { + const { store, deleteVectorStore } = makeVectorStore(384); + const { db, runAsync } = makeDb(); + + const migrated = await migrateEmbeddingModelIfNeeded( + store, + db, + 'lfm-2-5', + CURRENT_DIM + ); + + expect(migrated).toBe(true); + expect(deleteVectorStore).toHaveBeenCalledTimes(1); + expect(runAsync).toHaveBeenCalledWith('DELETE FROM chatSources'); + expect(runAsync).toHaveBeenCalledWith('DELETE FROM sources'); + expect(await AsyncStorage.getItem(ACTIVE_EMBEDDING_MODEL_KEY)).toBe( + 'lfm-2-5' + ); + }); + + it('wipes the vector store and source metadata on a genuine model change', async () => { + await AsyncStorage.setItem(ACTIVE_EMBEDDING_MODEL_KEY, 'model-old'); + const { store, deleteVectorStore } = makeVectorStore(CURRENT_DIM); + const { db, runAsync } = makeDb(); + + const migrated = await migrateEmbeddingModelIfNeeded( + store, + db, + 'model-new', + CURRENT_DIM + ); + + expect(migrated).toBe(true); + expect(deleteVectorStore).toHaveBeenCalledTimes(1); + expect(runAsync).toHaveBeenCalledWith('DELETE FROM chatSources'); + expect(runAsync).toHaveBeenCalledWith('DELETE FROM sources'); + expect(await AsyncStorage.getItem(ACTIVE_EMBEDDING_MODEL_KEY)).toBe( + 'model-new' + ); + }); + + it('leaves the key unset when clearing source metadata fails, so the next launch retries', async () => { + await AsyncStorage.setItem(ACTIVE_EMBEDDING_MODEL_KEY, 'model-old'); + const { store, deleteVectorStore } = makeVectorStore(CURRENT_DIM); + const { db, runAsync } = makeDb(); + runAsync.mockRejectedValueOnce(new Error('database is locked')); + + const migrated = await migrateEmbeddingModelIfNeeded( + store, + db, + 'model-new', + CURRENT_DIM + ); + + expect(migrated).toBe(true); + expect(deleteVectorStore).toHaveBeenCalledTimes(1); + expect(await AsyncStorage.getItem(ACTIVE_EMBEDDING_MODEL_KEY)).toBe( + 'model-old' + ); + }); + + it('leaves a lost key unset when a dimension-incompatible wipe fails', async () => { + const { store } = makeVectorStore(384); + const { db, runAsync } = makeDb(); + runAsync.mockRejectedValueOnce(new Error('database is locked')); + + const migrated = await migrateEmbeddingModelIfNeeded( + store, + db, + 'model-new', + CURRENT_DIM + ); + + expect(migrated).toBe(true); + expect(await AsyncStorage.getItem(ACTIVE_EMBEDDING_MODEL_KEY)).toBeNull(); + }); +}); diff --git a/__tests__/embeddingModelStore.test.ts b/__tests__/embeddingModelStore.test.ts new file mode 100644 index 00000000..70dde28d --- /dev/null +++ b/__tests__/embeddingModelStore.test.ts @@ -0,0 +1,115 @@ +import { useEmbeddingModelStore } from '../store/embeddingModelStore'; +import type { OPSQLiteVectorStore } from '@react-native-rag/op-sqlite'; + +const makeStore = (load: jest.Mock) => + ({ load }) as unknown as OPSQLiteVectorStore; + +const createDeferred = () => { + let resolve!: (value: T) => void; + let reject!: (reason?: unknown) => void; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { promise, resolve, reject }; +}; + +beforeEach(() => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + useEmbeddingModelStore.setState({ status: 'unknown', progress: 0 }); +}); + +afterEach(() => { + jest.restoreAllMocks(); +}); + +describe('embeddingModelStore.ensureReady', () => { + it('returns true immediately without loading when already ready', async () => { + useEmbeddingModelStore.setState({ status: 'ready', progress: 1 }); + const load = jest.fn(); + + const ready = await useEmbeddingModelStore + .getState() + .ensureReady(makeStore(load)); + + expect(ready).toBe(true); + expect(load).not.toHaveBeenCalled(); + }); + + it('downloads then marks the model ready on success', async () => { + const load = jest.fn().mockResolvedValue(undefined); + + const ready = await useEmbeddingModelStore + .getState() + .ensureReady(makeStore(load)); + + expect(ready).toBe(true); + expect(load).toHaveBeenCalledTimes(1); + expect(useEmbeddingModelStore.getState().status).toBe('ready'); + expect(useEmbeddingModelStore.getState().progress).toBe(1); + }); + + it('sets an error status when the download fails (no network / no space / interrupted)', async () => { + const load = jest + .fn() + .mockRejectedValue(new Error('Network request failed')); + + const ready = await useEmbeddingModelStore + .getState() + .ensureReady(makeStore(load)); + + expect(ready).toBe(false); + expect(useEmbeddingModelStore.getState().status).toBe('error'); + expect(useEmbeddingModelStore.getState().progress).toBe(0); + expect(console.error).toHaveBeenCalled(); + }); + + it('reports downloading status while the download is in flight', async () => { + const deferred = createDeferred(); + const load = jest.fn().mockReturnValue(deferred.promise); + + const pending = useEmbeddingModelStore + .getState() + .ensureReady(makeStore(load)); + + expect(useEmbeddingModelStore.getState().status).toBe('downloading'); + expect(useEmbeddingModelStore.getState().progress).toBe(0); + + deferred.resolve(); + await pending; + expect(useEmbeddingModelStore.getState().status).toBe('ready'); + }); + + it('single-flights concurrent calls so the model downloads only once', async () => { + const deferred = createDeferred(); + const load = jest.fn().mockReturnValue(deferred.promise); + const store = makeStore(load); + + const first = useEmbeddingModelStore.getState().ensureReady(store); + const second = useEmbeddingModelStore.getState().ensureReady(store); + + deferred.resolve(); + const [a, b] = await Promise.all([first, second]); + + expect(a).toBe(true); + expect(b).toBe(true); + expect(load).toHaveBeenCalledTimes(1); + }); + + it('allows a fresh retry after a failed download', async () => { + const load = jest + .fn() + .mockRejectedValueOnce(new Error('No space left on device')) + .mockResolvedValueOnce(undefined); + const store = makeStore(load); + + const first = await useEmbeddingModelStore.getState().ensureReady(store); + expect(first).toBe(false); + expect(useEmbeddingModelStore.getState().status).toBe('error'); + + const second = await useEmbeddingModelStore.getState().ensureReady(store); + expect(second).toBe(true); + expect(load).toHaveBeenCalledTimes(2); + expect(useEmbeddingModelStore.getState().status).toBe('ready'); + }); +}); diff --git a/__tests__/fileReaders.test.ts b/__tests__/fileReaders.test.ts index 2ad41d83..ea21cd05 100644 --- a/__tests__/fileReaders.test.ts +++ b/__tests__/fileReaders.test.ts @@ -1,4 +1,4 @@ -import { readDocumentText } from '../utils/fileReaders'; +import { readDocumentText, normalizePdfText } from '../utils/fileReaders'; import { readPDF } from 'react-native-pdfium'; import { File } from 'expo-file-system'; @@ -64,8 +64,6 @@ describe('readDocumentText — TXT / MD', () => { }); describe('readDocumentText — HTML', () => { - const html = (body: string) => body; - it('strips basic HTML tags', async () => { const mockText = jest.fn().mockResolvedValue('

Hello world

'); MockFile.mockImplementation(() => ({ text: mockText })); @@ -132,10 +130,54 @@ describe('readDocumentText — unsupported types', () => { 'Unsupported file type: docx' ); }); +}); + +describe('readDocumentText — CSV', () => { + it('reads csv files as text via File.text()', async () => { + const mockText = jest.fn().mockResolvedValue('a,b,c\n1,2,3'); + MockFile.mockImplementation(() => ({ text: mockText })); + + const result = await readDocumentText('/data.csv', 'csv'); + expect(MockFile).toHaveBeenCalledWith('/data.csv'); + expect(result).toBe('a,b,c\n1,2,3'); + }); +}); + +describe('normalizePdfText', () => { + it('collapses single soft-wrap line breaks into spaces', () => { + expect(normalizePdfText('Sta\nwka\nVAT')).toBe('Sta wka VAT'); + }); + + it('rejoins wrapped multi-word headers', () => { + expect(normalizePdfText('Cena brutto\n[zł]')).toBe('Cena brutto [zł]'); + }); - it('throws for csv', async () => { - await expect(readDocumentText('/data.csv', 'csv')).rejects.toThrow( - 'Unsupported file type: csv' + it('preserves paragraph breaks between blocks', () => { + expect(normalizePdfText('Block one.\n\n\nBlock two.')).toBe( + 'Block one.\n\nBlock two.' ); }); + + it('rejoins a word split by a hyphen at a line break', () => { + expect(normalizePdfText('structured e-\ninvoice')).toBe( + 'structured einvoice' + ); + }); + + it('strips control and format characters (soft hyphen, zero-width space)', () => { + const softHyphen = String.fromCharCode(0x00ad); + const zeroWidth = String.fromCharCode(0x200b); + const input = `e${softHyphen}invoice a${zeroWidth}b`; + expect(normalizePdfText(input)).toBe('einvoice ab'); + }); + + it('leaves already-clean single-line text unchanged', () => { + expect(normalizePdfText('extracted text')).toBe('extracted text'); + }); + + it('applies to text read from a PDF', async () => { + mockReadPDF.mockResolvedValue('Cena brutto\n[zł]\nWartość'); + const result = await readDocumentText('file:///doc.pdf', 'pdf'); + expect(result).toBe('Cena brutto [zł] Wartość'); + }); }); diff --git a/__tests__/legacyChat.test.ts b/__tests__/legacyChat.test.ts new file mode 100644 index 00000000..73cd868a --- /dev/null +++ b/__tests__/legacyChat.test.ts @@ -0,0 +1,124 @@ +import { chatPredatesSourceLinking } from '../utils/legacyChat'; +import { setSourceLinkingBoundary } from '../utils/sourceLinkingBoundary'; +import { Message } from '../database/chatRepository'; + +const BOUNDARY = 200; + +const message = (overrides: Partial): Message => ({ + id: 1, + chatId: 7, + role: 'user', + content: 'hi', + timestamp: 0, + ...overrides, +}); + +describe('chatPredatesSourceLinking', () => { + it('returns false for a chat with no attached documents', () => { + expect( + chatPredatesSourceLinking( + [ + message({ role: 'user', content: 'hello' }), + message({ id: 2, role: 'assistant', content: 'hi there' }), + ], + BOUNDARY + ) + ).toBe(false); + }); + + it('flags a legacy chat that attached a document but has no source linking', () => { + expect( + chatPredatesSourceLinking( + [ + message({ id: 10, role: 'user', documentName: 'report.pdf' }), + message({ id: 11, role: 'assistant', content: 'summary' }), + ], + BOUNDARY + ) + ).toBe(true); + }); + + it('does not flag a chat where the document is linked via sourceDocuments', () => { + expect( + chatPredatesSourceLinking( + [ + message({ id: 10, role: 'user', documentName: 'report.pdf' }), + message({ + id: 11, + role: 'assistant', + content: 'summary [1]', + sourceDocuments: [{ name: 'report.pdf', documentId: 3 }], + }), + ], + BOUNDARY + ) + ).toBe(false); + }); + + it('keeps flagging a legacy chat after a new-era turn retrieves a source', () => { + expect( + chatPredatesSourceLinking( + [ + message({ id: 10, role: 'user', documentName: 'report.pdf' }), + message({ id: 11, role: 'assistant', content: 'summary' }), + message({ id: 204, role: 'user', documentName: 'other.pdf' }), + message({ + id: 205, + role: 'assistant', + content: 'answer [1]', + sourceDocuments: [{ name: 'other.pdf', documentId: 9 }], + }), + ], + BOUNDARY + ) + ).toBe(true); + }); + + it('does NOT flag a new-era chat whose upload was interrupted before sourceDocuments', () => { + expect( + chatPredatesSourceLinking( + [ + message({ id: 201, role: 'user', documentName: 'report.pdf' }), + message({ id: 202, role: 'assistant', content: '' }), + ], + BOUNDARY + ) + ).toBe(false); + }); + + it('treats an empty sourceDocuments array as no linking (legacy id)', () => { + expect( + chatPredatesSourceLinking( + [ + message({ id: 10, role: 'user', documentName: 'report.pdf' }), + message({ id: 11, role: 'assistant', sourceDocuments: [] }), + ], + BOUNDARY + ) + ).toBe(true); + }); + + it('returns false for an empty conversation', () => { + expect(chatPredatesSourceLinking([], BOUNDARY)).toBe(false); + }); + + it('reads the module boundary when none is passed', () => { + setSourceLinkingBoundary(BOUNDARY); + expect( + chatPredatesSourceLinking([ + message({ id: 10, role: 'user', documentName: 'report.pdf' }), + message({ id: 11, role: 'assistant', content: 'summary' }), + ]) + ).toBe(true); + setSourceLinkingBoundary(0); + }); + + it('flags nothing when the boundary is 0 (fresh install / not yet loaded)', () => { + expect( + chatPredatesSourceLinking( + [message({ id: 10, role: 'user', documentName: 'report.pdf' })], + 0 + ) + ).toBe(false); + }); +}); diff --git a/__tests__/llmStore.test.ts b/__tests__/llmStore.test.ts index b3fa2934..235e43f2 100644 --- a/__tests__/llmStore.test.ts +++ b/__tests__/llmStore.test.ts @@ -1,6 +1,9 @@ import { useLLMStore } from '../store/llmStore'; import { LLMModule } from 'react-native-executorch'; import * as chatRepository from '../database/chatRepository'; +import type { Message } from '../database/chatRepository'; +import type { Model } from '../database/modelRepository'; +import type { SQLiteDatabase } from 'expo-sqlite'; import * as Feedback from '../utils/Feedback'; import { prepareMessagesForLLM } from '../utils/promptUtils'; import { useSettingsStore } from '../store/settingsStore'; @@ -24,7 +27,13 @@ const mockLLMModule = LLMModule as jest.Mocked; const mockPersistMessage = chatRepository.persistMessage as jest.Mock; const mockGetChatMessages = chatRepository.getChatMessages as jest.Mock; -const mockDb = {} as any; +const noSources = async () => ({ + context: [] as string[], + sourceDocuments: [], + preferredSourceDocuments: [], +}); + +const mockDb = {} as unknown as SQLiteDatabase; const baseModel = { id: 1, @@ -38,7 +47,7 @@ const baseModel = { }; // Captures the token callback registered during loadModel so tests can fire tokens -let capturedTokenCallback: ((token: string) => void) | null = null; +let capturedTokenCallback: ((token: string) => void) | null | undefined = null; const makeMockInstance = () => ({ generate: jest.fn(), @@ -59,7 +68,7 @@ beforeEach(() => { mockLLMModule.fromModelName.mockImplementation( async (_namedSources, _onProgress, onToken) => { capturedTokenCallback = onToken; - return mockInstance as any; + return mockInstance as unknown as LLMModule; } ); @@ -88,7 +97,7 @@ beforeEach(() => { mockLLMModule.fromModelName.mockImplementation( async (_namedSources, _onProgress, onToken) => { capturedTokenCallback = onToken; - return mockInstance as any; + return mockInstance as unknown as LLMModule; } ); }); @@ -114,8 +123,8 @@ describe('loadModel', () => { let wasLoading = false; mockLLMModule.fromModelName.mockImplementation(async (...args) => { wasLoading = useLLMStore.getState().isLoading; - capturedTokenCallback = args[4]; - return mockInstance as any; + capturedTokenCallback = args[2]; + return mockInstance as unknown as LLMModule; }); await useLLMStore.getState().loadModel(baseModel); @@ -144,8 +153,8 @@ describe('loadModel', () => { // Load a different model mockInstance = makeMockInstance(); mockLLMModule.fromModelName.mockImplementation(async (...args) => { - capturedTokenCallback = args[4]; - return mockInstance as any; + capturedTokenCallback = args[2]; + return mockInstance as unknown as LLMModule; }); await useLLMStore.getState().loadModel({ ...baseModel, id: 2 }); @@ -324,13 +333,13 @@ describe('sendChatMessage', () => { it('returns early when db is not set', async () => { useLLMStore.setState({ db: null }); - await useLLMStore.getState().sendChatMessage('hi', 1, [], settings); + await useLLMStore.getState().sendChatMessage('hi', 1, noSources, settings); expect(mockPersistMessage).not.toHaveBeenCalled(); }); it('returns early when model is not loaded', async () => { useLLMStore.setState({ model: null }); - await useLLMStore.getState().sendChatMessage('hi', 1, [], settings); + await useLLMStore.getState().sendChatMessage('hi', 1, noSources, settings); expect(mockPersistMessage).not.toHaveBeenCalled(); }); @@ -341,7 +350,9 @@ describe('sendChatMessage', () => { activeChatMessages: [], }); - await useLLMStore.getState().sendChatMessage('hello', 1, [], settings); + await useLLMStore + .getState() + .sendChatMessage('hello', 1, noSources, settings); expect(mockPersistMessage).toHaveBeenCalledTimes(2); expect(mockPersistMessage).toHaveBeenCalledWith( @@ -364,14 +375,16 @@ describe('sendChatMessage', () => { activeChatMessages: [], }); - await useLLMStore.getState().sendChatMessage('hello', 1, [], settings); + await useLLMStore + .getState() + .sendChatMessage('hello', 1, noSources, settings); expect(useLLMStore.getState().isProcessingPrompt).toBe(false); expect(useLLMStore.getState().isGenerating).toBe(false); }); it('adds user message and assistant placeholder to activeChatMessages before generating', async () => { - let messagesBeforeGenerate: any[] = []; + let messagesBeforeGenerate: Message[] = []; mockInstance.generate.mockImplementation(async () => { messagesBeforeGenerate = useLLMStore.getState().activeChatMessages; return 'response'; @@ -382,7 +395,9 @@ describe('sendChatMessage', () => { activeChatMessages: [], }); - await useLLMStore.getState().sendChatMessage('ping', 1, [], settings); + await useLLMStore + .getState() + .sendChatMessage('ping', 1, noSources, settings); expect(messagesBeforeGenerate).toHaveLength(2); expect(messagesBeforeGenerate[0].role).toBe('user'); @@ -398,7 +413,9 @@ describe('sendChatMessage', () => { activeChatMessages: [], }); - await useLLMStore.getState().sendChatMessage('ping', 1, [], settings); + await useLLMStore + .getState() + .sendChatMessage('ping', 1, noSources, settings); const messages = useLLMStore.getState().activeChatMessages; expect(messages).toHaveLength(2); @@ -420,7 +437,9 @@ describe('sendChatMessage', () => { activeChatMessages: [], }); - await useLLMStore.getState().sendChatMessage('hello', 1, [], settings); + await useLLMStore + .getState() + .sendChatMessage('hello', 1, noSources, settings); expect(useLLMStore.getState().isGenerating).toBe(false); expect(useLLMStore.getState().isProcessingPrompt).toBe(false); @@ -436,21 +455,28 @@ describe('sendChatMessage', () => { activeChatMessages: [], }); - await useLLMStore.getState().sendChatMessage('hello', 1, [], settings); + await useLLMStore + .getState() + .sendChatMessage('hello', 1, noSources, settings); expect(useLLMStore.getState().isGenerating).toBe(false); expect(useLLMStore.getState().isProcessingPrompt).toBe(false); }); it('does not update performance metrics on last message when user navigated away', async () => { - mockInstance.generate.mockResolvedValue('response'); + mockInstance.generate.mockImplementation(async () => { + useLLMStore.setState({ activeChatId: 99 }); + return 'response'; + }); useLLMStore.setState({ model: baseModel, - activeChatId: 99, // different from chatId=1 + activeChatId: 1, activeChatMessages: [], }); - await useLLMStore.getState().sendChatMessage('hello', 1, [], settings); + await useLLMStore + .getState() + .sendChatMessage('hello', 1, noSources, settings); // complete called without perf data — last message should not have timeToFirstToken const messages = useLLMStore.getState().activeChatMessages; @@ -478,7 +504,7 @@ describe('sendChatMessage — settings hydration barrier', () => { const sendPromise = useLLMStore .getState() - .sendChatMessage('hello', 1, [], settings); + .sendChatMessage('hello', 1, async () => ({ context: [] }), settings); await new Promise((resolve) => setTimeout(resolve, 10)); @@ -504,7 +530,9 @@ describe('sendChatMessage — settings hydration barrier', () => { customSystemPrompt: 'Be concise.', }); - await useLLMStore.getState().sendChatMessage('hi', 1, [], settings); + await useLLMStore + .getState() + .sendChatMessage('hi', 1, async () => ({ context: [] }), settings); expect(prepareMessagesForLLM).toHaveBeenCalledTimes(1); expect((prepareMessagesForLLM as jest.Mock).mock.calls[0][4]).toBe( @@ -605,7 +633,7 @@ describe('sendChatMessage imagePath', () => { modelName: 'LFM VL', vision: true, featured: true, - } as any, + } as Model, activeChatId: 1, activeChatMessages: [], }); @@ -614,7 +642,13 @@ describe('sendChatMessage imagePath', () => { it('passes imagePath to persistMessage for user message when provided', async () => { await useLLMStore .getState() - .sendChatMessage('What is this?', 1, [], settings, '/local/image.jpg'); + .sendChatMessage( + 'What is this?', + 1, + noSources, + settings, + '/local/image.jpg' + ); expect(mockPersistMessage).toHaveBeenCalledWith( expect.anything(), @@ -623,7 +657,9 @@ describe('sendChatMessage imagePath', () => { }); it('passes undefined imagePath to persistMessage when not provided', async () => { - await useLLMStore.getState().sendChatMessage('Hello', 1, [], settings); + await useLLMStore + .getState() + .sendChatMessage('Hello', 1, noSources, settings); expect(mockPersistMessage).toHaveBeenCalledWith( expect.anything(), @@ -642,7 +678,13 @@ describe('sendChatMessage imagePath', () => { await useLLMStore .getState() - .sendChatMessage('What is this?', 1, [], settings, '/local/image.jpg'); + .sendChatMessage( + 'What is this?', + 1, + noSources, + settings, + '/local/image.jpg' + ); expect(mockInstance.generate).toHaveBeenCalledTimes(1); const calledMessages = mockInstance.generate.mock.calls[0][0]; @@ -719,6 +761,12 @@ describe('runBenchmark', () => { await loadModel(); useLLMStore.setState({ model: baseModel }); + // The RN jest preset aliases performance.now to Date.now (1 ms resolution), + // so on a fast machine startTime and the first token can share a millisecond + // and the measured delta collapses to 0. Advance a virtual clock instead. + let now = 0; + jest.spyOn(performance, 'now').mockImplementation(() => (now += 10)); + mockInstance.generate.mockImplementation(async () => { await flushFrame(); capturedTokenCallback!('tok'); diff --git a/__tests__/messageSources.test.ts b/__tests__/messageSources.test.ts new file mode 100644 index 00000000..a85dc3ae --- /dev/null +++ b/__tests__/messageSources.test.ts @@ -0,0 +1,366 @@ +import { + assembleSourceDocuments, + looksLikeNoAnswer, + mergeAttachmentFirst, + pickCitationsByAnswer, + restrictCitationsToContext, + visibleAnswer, + type SourceRow, +} from '../utils/messageSources'; +import { SourceDocument } from '../database/chatRepository'; +import { formatContextChunks } from '../utils/contextUtils'; + +const doc = (documentId: number | undefined, name: string): SourceDocument => ({ + documentId, + name, +}); + +describe('mergeAttachmentFirst', () => { + it('leads with retrieved attachment docs, then the rest', () => { + const retrieved = [doc(1, 'old.pdf'), doc(2, 'attachment.txt')]; + const result = mergeAttachmentFirst( + retrieved, + [doc(2, 'attachment.txt')], + [2] + ); + + expect(result.map((d) => d.documentId)).toEqual([2, 1]); + }); + + it('cites an attachment that produced no retrieved chunk, using its overview', () => { + const retrieved = [doc(1, 'old.pdf')]; + const preferred = [doc(2, 'attachment.txt')]; + const result = mergeAttachmentFirst(retrieved, preferred, [2]); + + expect(result.map((d) => d.documentId)).toEqual([2, 1]); + expect(result).toHaveLength(2); + }); + + it('does not duplicate an attachment that was both retrieved and preferred', () => { + const retrieved = [doc(2, 'attachment.txt')]; + const preferred = [doc(2, 'attachment.txt')]; + const result = mergeAttachmentFirst(retrieved, preferred, [2]); + + expect(result).toHaveLength(1); + expect(result[0].documentId).toBe(2); + }); + + it('does not collide two undefined-id sources onto one slot', () => { + const retrieved = [doc(undefined, 'a.pdf'), doc(undefined, 'b.pdf')]; + const result = mergeAttachmentFirst(retrieved, [], [7]); + + expect(result).toHaveLength(2); + expect(result.map((d) => d.name)).toEqual(['a.pdf', 'b.pdf']); + }); +}); + +describe('assembleSourceDocuments', () => { + const source = ( + id: number, + name: string, + firstChunk?: string + ): SourceRow => ({ + id, + name, + firstChunk, + }); + + it('returns the merged citations when retrieval produced sources', () => { + const retrieved = [doc(1, 'old.pdf'), doc(2, 'attachment.txt')]; + const result = assembleSourceDocuments( + retrieved, + [doc(2, 'attachment.txt')], + [2], + [source(1, 'old.pdf'), source(2, 'attachment.txt')], + true + ); + + expect(result.map((d) => d.documentId)).toEqual([2, 1]); + }); + + it('links the active document when context was sent but nothing was cited', () => { + const result = assembleSourceDocuments( + [], + [], + [], + [source(9, 'report.pdf', 'overview text')], + true + ); + + expect(result).toEqual([ + { documentId: 9, name: 'report.pdf', passage: 'overview text' }, + ]); + }); + + it('stays empty when no context reached the model', () => { + const result = assembleSourceDocuments( + [], + [], + [], + [source(9, 'report.pdf', 'overview text')], + false + ); + + expect(result).toEqual([]); + }); +}); + +describe('restrictCitationsToContext', () => { + const cite = (documentId: number, name: string): SourceDocument => ({ + documentId, + name, + }); + const block = (documentId: number, name: string, document: string) => ({ + document, + similarity: 0.8, + metadata: { documentId, name }, + }); + + it('drops documents whose block was truncated out of the prompt', () => { + const cited = [ + cite(21, 'polityka_urlopowa_2026.pdf'), + cite(20, 'sample.htm'), + cite(19, '_10-K-2025-As-Filed.pdf'), + ]; + const prompt = formatContextChunks([ + block(21, 'polityka_urlopowa_2026.pdf', 'vacation'), + ]).join(' '); + + const result = restrictCitationsToContext(cited, prompt, [ + doc(21, 'polityka_urlopowa_2026.pdf'), + ]); + + expect(result.map((d) => d.documentId)).toEqual([21]); + }); + + it('keeps every document whose block survived', () => { + const cited = [cite(1, 'a.pdf'), cite(2, 'b.pdf')]; + const prompt = formatContextChunks([ + block(1, 'a.pdf', 'x'), + block(2, 'b.pdf', 'y'), + ]).join(' '); + + const result = restrictCitationsToContext(cited, prompt, []); + + expect(result.map((d) => d.documentId)).toEqual([1, 2]); + }); + + it('keeps the leading citation when nothing matched the prompt', () => { + const cited = [cite(1, 'a.pdf'), cite(2, 'b.pdf')]; + + const result = restrictCitationsToContext(cited, 'no headers here', []); + + expect(result.map((d) => d.documentId)).toEqual([1]); + }); + + it('passes through a single citation untouched', () => { + const cited = [cite(1, 'a.pdf')]; + + expect(restrictCitationsToContext(cited, '', [])).toEqual(cited); + }); +}); + +describe('pickCitationsByAnswer', () => { + const withPassage = ( + documentId: number, + name: string, + passage: string + ): SourceDocument => ({ documentId, name, passage }); + + it('cites only the source the answer actually echoes', () => { + const cited = [ + withPassage( + 22, + 'sample.html', + 'The quarterly revenue report and profit summary.' + ), + withPassage(24, 'sample.csv', 'employee,vacation,days\nAnna,urlop,26'), + ]; + const answer = 'Anna ma 26 dni urlopu według danych o pracownikach.'; + + const result = pickCitationsByAnswer(cited, answer, []); + + expect(result.map((d) => d.documentId)).toEqual([24]); + }); + + it('does not cite a source for a fact the answer says it lacks', () => { + const cited = [ + withPassage( + 1, + 'revenue.txt', + 'Total revenue grew to five million dollars.' + ), + withPassage(2, 'headcount.txt', 'The company hired forty new engineers.'), + ]; + const answer = 'The report does not mention revenue.'; + + const result = pickCitationsByAnswer(cited, answer, []); + + expect(result).toEqual([]); + }); + + it('keeps the asserted half of a sentence and drops the negated half', () => { + const cited = [ + withPassage( + 1, + 'revenue.txt', + 'Total revenue grew to five million dollars.' + ), + withPassage(2, 'headcount.txt', 'The company hired forty new engineers.'), + ]; + const answer = + 'The company hired forty engineers, but the report does not mention total revenue or the five million dollars figure.'; + + const result = pickCitationsByAnswer(cited, answer, []); + + expect(result.map((d) => d.documentId)).toEqual([2]); + }); + + it('keeps both sources when the answer draws on each', () => { + const cited = [ + withPassage( + 1, + 'revenue.txt', + 'Total revenue grew to five million dollars.' + ), + withPassage(2, 'headcount.txt', 'The company hired forty new engineers.'), + ]; + const answer = + 'Revenue grew to five million dollars while the company hired forty engineers.'; + + const result = pickCitationsByAnswer(cited, answer, []); + + expect(result.map((d) => d.documentId).sort()).toEqual([1, 2]); + }); + + it('never drops the freshly-attached source the answer does not echo', () => { + const cited = [ + withPassage(1, 'other.txt', 'Revenue grew to five million dollars.'), + withPassage( + 2, + 'attachment.txt', + 'Completely unrelated attached content.' + ), + ]; + const answer = 'Revenue grew to five million dollars.'; + + const result = pickCitationsByAnswer(cited, answer, [ + doc(2, 'attachment.txt'), + ]); + + expect(result.map((d) => d.documentId).sort()).toEqual([1, 2]); + }); + + it('drops all citations when the answer echoes no passage (refusal)', () => { + const cited = [ + withPassage(1, 'sample.pdf', 'alpha beta gamma'), + withPassage(2, 'misja_ares_trzy.pdf', 'delta epsilon zeta'), + ]; + + const result = pickCitationsByAnswer( + cited, + 'W dokumentach nie ma informacji o L4.', + [] + ); + + expect(result).toEqual([]); + }); + + it('keeps only the fresh attachment when a refusal echoes no passage', () => { + const cited = [ + withPassage(1, 'library.pdf', 'alpha beta gamma'), + withPassage(2, 'attachment.pdf', 'delta epsilon zeta'), + ]; + + const result = pickCitationsByAnswer( + cited, + 'There is no information about L4 here.', + [doc(2, 'attachment.pdf')] + ); + + expect(result.map((d) => d.documentId)).toEqual([2]); + }); + + it('passes through a single citation untouched', () => { + const cited = [withPassage(1, 'a.txt', 'alpha beta gamma')]; + + expect(pickCitationsByAnswer(cited, 'anything at all', [])).toEqual(cited); + }); + + it('ignores the block and attributes only the visible reply', () => { + const cited = [ + withPassage(1, 'sample.pdf', 'alpha beta gamma'), + withPassage(2, 'misja_ares_trzy.pdf', 'delta epsilon zeta'), + ]; + const answer = + 'The alpha beta gamma file and the delta epsilon zeta file both ' + + 'need checking for L4.W dokumentach nie ma informacji o L4.'; + + const result = pickCitationsByAnswer(cited, answer, []); + + expect(result).toEqual([]); + }); + + it('attributes to the source the visible reply echoes, not the reasoning', () => { + const cited = [ + withPassage(1, 'sample.pdf', 'alpha beta gamma'), + withPassage(2, 'misja_ares_trzy.pdf', 'delta epsilon zeta'), + ]; + const answer = + 'Compare alpha beta gamma against delta epsilon zeta.' + + 'The mission file covers delta, epsilon and zeta in detail.'; + + const result = pickCitationsByAnswer(cited, answer, []); + + expect(result.map((d) => d.documentId)).toEqual([2]); + }); +}); + +describe('visibleAnswer', () => { + it('drops a complete think block, keeping text before and after', () => { + expect(visibleAnswer('beforehidden reasoningafter')).toBe( + 'before after' + ); + }); + + it('drops an unterminated think block (streaming) entirely', () => { + expect(visibleAnswer('visiblestill reasoning')).toBe('visible '); + }); + + it('returns the text unchanged when there is no think block', () => { + expect(visibleAnswer('plain answer')).toBe('plain answer'); + }); + + it('drops every think block, not just the first', () => { + expect( + visibleAnswer( + 'onehiddentwoalso hiddenthree' + ) + ).toBe('one two three'); + }); +}); + +describe('looksLikeNoAnswer', () => { + it.each([ + 'There is no information about L4 in the documents.', + 'There is no mention of sick leave anywhere.', + 'Sick leave is not mentioned in the provided documents.', + 'The file does not contain any information about L4.', + "I don't know — the context does not cover this.", + 'That detail is not found in the provided sources.', + ])('flags the refusal: %s', (reply) => { + expect(looksLikeNoAnswer(reply)).toBe(true); + }); + + it.each([ + 'The company has no debt and reported a five million profit.', + 'Firma nie ma zadłużenia, a zysk wyniósł pięć milionów.', + 'Nie ma limitu urlopu — polityka pozwala na 30 dni w roku.', + 'Polityka nie zawiera kar umownych za zwłokę.', + 'Document A covers revenue; it does not mention costs, which are in B.', + 'Anna ma 26 dni urlopu zgodnie z regulaminem.', + 'The mission launches on Tuesday with a crew of three.', + ])('does not flag a real answer: %s', (reply) => { + expect(looksLikeNoAnswer(reply)).toBe(false); + }); +}); diff --git a/__tests__/prepareContext.test.ts b/__tests__/prepareContext.test.ts index a92cc64a..304d4549 100644 --- a/__tests__/prepareContext.test.ts +++ b/__tests__/prepareContext.test.ts @@ -1,65 +1,88 @@ import { - filterAndFormatContext, + formatContextChunks, formatFirstChunks, + getSourceDocumentsFromChunks, + sourcesPresentInContext, } from '../utils/contextUtils'; -describe('filterAndFormatContext', () => { +describe('formatContextChunks / getSourceDocumentsFromChunks', () => { const makeChunk = ( document: string, similarity: number, - documentId: number + documentId: number, + name?: string ) => ({ document, similarity, - metadata: { documentId }, + metadata: { documentId, ...(name ? { name } : {}) }, }); - it('includes chunks above 0.3 similarity threshold', () => { - const chunks = [ - makeChunk('Relevant', 0.5, 1), - makeChunk('Irrelevant', 0.2, 1), - ]; - const result = filterAndFormatContext(chunks); - expect(result).toHaveLength(1); - expect(result[0]).toContain('Relevant'); + it('returns empty output for no chunks', () => { + expect(formatContextChunks([])).toEqual([]); + expect(getSourceDocumentsFromChunks([])).toEqual([]); }); - it('limits to max 3 chunks', () => { + it('does not leak the relevance score into the LLM context', () => { + const chunks = [makeChunk('Content', 0.85, 1)]; + const result = formatContextChunks(chunks); + expect(result[0]).toContain('Content'); + expect(result[0]).not.toMatch(/%|Relevance/); + }); + + it('stitches adjacent passages without re-printing their shared overlap', () => { const chunks = [ - makeChunk('High 1', 0.9, 1), - makeChunk('High 2', 0.8, 1), - makeChunk('High 3', 0.7, 1), - makeChunk('High 4', 0.6, 1), + makeChunk( + 'The quarterly revenue report shows a total of 1200 units sold in Q3.', + 0.9, + 1, + 'report.pdf' + ), + makeChunk( + 'a total of 1200 units sold in Q3. The following section covers Q4 projections.', + 0.85, + 1, + 'report.pdf' + ), ]; - const result = filterAndFormatContext(chunks); - expect(result).toHaveLength(3); - }); + const [source] = getSourceDocumentsFromChunks(chunks); + const passage = source.passage!; - it('returns empty array for no chunks', () => { - expect(filterAndFormatContext([])).toEqual([]); + expect(passage.split('a total of 1200 units sold in Q3')).toHaveLength(2); + expect(passage).toContain('The following section covers Q4 projections'); }); - it('returns empty when all below threshold', () => { - const chunks = [makeChunk('Low 1', 0.2, 1), makeChunk('Low 2', 0.1, 1)]; - expect(filterAndFormatContext(chunks)).toEqual([]); - }); + it('leaves non-overlapping passages fully intact', () => { + const chunks = [ + makeChunk('completely distinct first passage', 0.9, 1, 'doc.pdf'), + makeChunk('an entirely separate second passage', 0.8, 1, 'doc.pdf'), + ]; + const [source] = getSourceDocumentsFromChunks(chunks); - it('includes relevance score in formatted output', () => { - const chunks = [makeChunk('Content', 0.85, 1)]; - const result = filterAndFormatContext(chunks); - expect(result[0]).toContain('85.0%'); + expect(source.passage).toContain('completely distinct first passage'); + expect(source.passage).toContain('an entirely separate second passage'); }); - it('sorts by similarity descending', () => { + it('groups chunks of one document into a single source, preserving input order', () => { const chunks = [ - makeChunk('Low', 0.6, 1), - makeChunk('High', 0.9, 1), - makeChunk('Mid', 0.75, 1), + makeChunk('a-1', 0.9, 1, 'doc-a.pdf'), + makeChunk('a-2', 0.85, 1, 'doc-a.pdf'), + makeChunk('b-1', 0.8, 2, 'doc-b.pdf'), ]; - const result = filterAndFormatContext(chunks); - expect(result[0]).toContain('High'); - expect(result[1]).toContain('Mid'); - expect(result[2]).toContain('Low'); + const context = formatContextChunks(chunks); + const sources = getSourceDocumentsFromChunks(chunks); + + expect(sources).toHaveLength(2); + expect(sources[0].name).toBe('doc-a.pdf'); + expect(sources[1].name).toBe('doc-b.pdf'); + expect(context[0]).toContain('Source 1'); + expect(context[0]).toContain(sources[0].name); + expect(context[1]).toContain('Source 2'); + expect(context[1]).toContain(sources[1].name); + expect(context[0]).toContain('a-1'); + expect(context[0]).toContain('a-2'); + expect(sources[0].passage).toContain('a-1'); + expect(sources[0].passage).toContain('a-2'); + expect(sources[0].similarity).toBe(0.9); }); }); @@ -97,4 +120,55 @@ describe('formatFirstChunks', () => { it('returns empty for empty input', () => { expect(formatFirstChunks([])).toEqual([]); }); + + it('supports a custom source label for current attachments', () => { + const result = formatFirstChunks( + [{ id: 1, name: 'latest.pdf', firstChunk: 'Fresh context' }], + 'Current Attachment Source' + ); + + expect(result[0]).toContain('Current Attachment Source: latest.pdf'); + expect(result[0]).toContain('End of Current Attachment Source'); + }); +}); + +describe('sourcesPresentInContext', () => { + const chunk = (document: string, documentId: number, name: string) => ({ + document, + similarity: 0.9, + metadata: { documentId, name }, + }); + + it('extracts the names from Source blocks it is given', () => { + const context = formatContextChunks([ + chunk('vacation rules', 21, 'polityka_urlopowa_2026.pdf'), + chunk('unrelated', 20, 'sample.htm'), + ]).join(' '); + + expect(sourcesPresentInContext(context)).toEqual( + new Set(['polityka_urlopowa_2026.pdf', 'sample.htm']) + ); + }); + + it('strips the (Overview) marker from attachment headers', () => { + const context = formatFirstChunks( + [{ id: 21, name: 'polityka_urlopowa_2026.pdf', firstChunk: 'intro' }], + 'Current Attachment Source' + ).join(' '); + + expect(sourcesPresentInContext(context)).toEqual( + new Set(['polityka_urlopowa_2026.pdf']) + ); + }); + + it('reports only the blocks that survived truncation', () => { + const blocks = formatContextChunks([ + chunk('kept', 21, 'polityka_urlopowa_2026.pdf'), + chunk('dropped', 20, 'sample.htm'), + ]); + + expect(sourcesPresentInContext(blocks[0])).toEqual( + new Set(['polityka_urlopowa_2026.pdf']) + ); + }); }); diff --git a/__tests__/promptUtils.test.ts b/__tests__/promptUtils.test.ts index 72cac5cc..fa90021f 100644 --- a/__tests__/promptUtils.test.ts +++ b/__tests__/promptUtils.test.ts @@ -1,6 +1,7 @@ import { prepareMessagesForLLM } from '../utils/promptUtils'; import { Message, ChatSettings } from '../database/chatRepository'; import { Model } from '../database/modelRepository'; +import { getPromptCharBudget } from '../constants/context-window'; const baseSettings = { systemPrompt: 'You are a helpful assistant.', @@ -75,6 +76,24 @@ describe('prepareMessagesForLLM', () => { expect(result[0].content).toBe(baseSettings.systemPrompt); expect(result[0].content).not.toContain('IMPORTANT CONTEXT INFORMATION'); }); + + it('adds current attachment priority without making it exclusive', () => { + const messages = makeMessages(2); + const result = prepareMessagesForLLM( + messages, + ['some context'], + baseSettings, + baseModel, + '', + [{ documentId: 2, name: 'current.pdf' }] + ); + + expect(result[0].content).toContain('CURRENT ATTACHMENT PRIORITY'); + expect(result[0].content).toContain('current.pdf'); + expect(result[0].content).toContain( + 'You may still use earlier conversation' + ); + }); }); describe('global custom system prompt', () => { @@ -183,8 +202,9 @@ describe('prepareMessagesForLLM', () => { ); const roles = result.map((m) => m.role); expect(roles).not.toContain('event'); - // system + user + assistant + placeholder - expect(result).toHaveLength(4); + // system + user + assistant; trailing empty assistant placeholder is not + // sent to the model. + expect(result).toHaveLength(3); }); }); @@ -197,8 +217,8 @@ describe('prepareMessagesForLLM', () => { baseSettings, baseModel ); - // system + 20 messages + 1 trailing placeholder - expect(result).toHaveLength(22); + // system + 20 messages; trailing empty assistant placeholder is not sent. + expect(result).toHaveLength(21); expect(result[0].role).toBe('system'); }); @@ -261,7 +281,7 @@ describe('prepareMessagesForLLM', () => { }); describe('context injection', () => { - it('wraps context in tags on the last message', () => { + it('wraps context in tags on the latest user message', () => { const messages = makeMessages(3); const result = prepareMessagesForLLM( messages, @@ -270,10 +290,12 @@ describe('prepareMessagesForLLM', () => { baseModel ); const last = result[result.length - 1]; + expect(last.role).toBe('user'); expect(last.content).toContain('chunk one chunk two'); + expect(last.content).toContain('message 3'); }); - it('context is added to the last message (placeholder)', () => { + it('removes the assistant placeholder before adding context', () => { const messages: Message[] = [ { id: 1, @@ -305,9 +327,40 @@ describe('prepareMessagesForLLM', () => { baseModel ); const last = result[result.length - 1]; + expect(result).toHaveLength(4); + expect(last.role).toBe('user'); + expect(last.content).toContain('Tell me more'); expect(last.content).toContain('some context'); }); + it('adds a grounding reminder next to the question when an attachment is present', () => { + const messages = makeMessages(3); + const result = prepareMessagesForLLM( + messages, + ['some context'], + baseSettings, + baseModel, + '', + [{ documentId: 2, name: 'current.pdf' }] + ); + const last = result[result.length - 1]; + expect(last.content).toMatch(/Ignore any document mentioned earlier/i); + }); + + it('omits the grounding reminder when there is no attachment', () => { + const messages = makeMessages(3); + const result = prepareMessagesForLLM( + messages, + ['some context'], + baseSettings, + baseModel + ); + const last = result[result.length - 1]; + expect(last.content).not.toMatch( + /Ignore any document mentioned earlier/i + ); + }); + it('combines context and /think token', () => { const messages = makeMessages(3); const settings = { ...baseSettings, thinkingEnabled: true }; @@ -319,8 +372,125 @@ describe('prepareMessagesForLLM', () => { ); const last = result[result.length - 1]; // /think is appended before context wrapping + expect(last.role).toBe('user'); expect(last.content).toContain('/think'); expect(last.content).toContain(''); }); }); + + describe('context window budget', () => { + const bigMessage = (id: number, role: Message['role']): Message => ({ + id, + chatId: 1, + role, + content: 'x'.repeat(getPromptCharBudget(baseModel)), + timestamp: 0, + }); + + it('drops the oldest history messages when the prompt overflows', () => { + const history: Message[] = Array.from({ length: 10 }, (_, i) => + bigMessage(i + 1, i % 2 === 0 ? 'user' : 'assistant') + ); + const messages: Message[] = [ + ...history, + { + id: 11, + chatId: 1, + role: 'user', + content: 'latest question', + timestamp: 0, + }, + { id: 12, chatId: 1, role: 'assistant', content: '', timestamp: 0 }, + ]; + + const result = prepareMessagesForLLM( + messages, + [], + baseSettings, + baseModel + ); + + expect(result[0].role).toBe('system'); + expect(result[result.length - 1].content).toContain('latest question'); + expect(result.length).toBeLessThan(messages.length); + expect(result.length).toBeGreaterThanOrEqual(2); + }); + + it('always keeps the system prompt and the latest question', () => { + const history: Message[] = Array.from({ length: 20 }, (_, i) => + bigMessage(i + 1, i % 2 === 0 ? 'user' : 'assistant') + ); + const messages: Message[] = [ + ...history, + { id: 21, chatId: 1, role: 'user', content: 'keep me', timestamp: 0 }, + { id: 22, chatId: 1, role: 'assistant', content: '', timestamp: 0 }, + ]; + + const result = prepareMessagesForLLM( + messages, + [], + baseSettings, + baseModel + ); + + expect(result[0].role).toBe('system'); + const last = result[result.length - 1]; + expect(last.role).toBe('user'); + expect(last.content).toContain('keep me'); + }); + + it('truncates the RAG context when it alone overflows the budget', () => { + const messages: Message[] = [ + { id: 1, chatId: 1, role: 'user', content: 'question', timestamp: 0 }, + { id: 2, chatId: 1, role: 'assistant', content: '', timestamp: 0 }, + ]; + const hugeContext = 'y'.repeat( + getPromptCharBudget(baseModel) * 2 + 10000 + ); + + const result = prepareMessagesForLLM( + messages, + [hugeContext], + baseSettings, + baseModel + ); + + const last = result[result.length - 1]; + expect(last.content).toContain('question'); + expect(last.content).toContain(''); + expect(last.content.length).toBeLessThan(hugeContext.length); + }); + + it('cuts an over-budget context at a chunk boundary, keeping the leading section', () => { + const messages: Message[] = [ + { id: 1, chatId: 1, role: 'user', content: 'question', timestamp: 0 }, + { id: 2, chatId: 1, role: 'assistant', content: '', timestamp: 0 }, + ]; + const answer = 'NEEDLE_ANSWER_XYZ is the answer.'; + const filler = 'FILLERBLOCK'.repeat(3000); + const context = [`${answer}\n\n${filler}`]; + + const result = prepareMessagesForLLM( + messages, + context, + baseSettings, + baseModel + ); + + const last = result[result.length - 1]; + expect(last.content).toContain('NEEDLE_ANSWER_XYZ'); + expect(last.content).not.toContain('FILLERBLOCK'); + }); + + it('does not trim when everything comfortably fits', () => { + const messages = makeMessages(6); + const result = prepareMessagesForLLM( + messages, + ['small context'], + baseSettings, + baseModel + ); + expect(result).toHaveLength(7); + }); + }); }); diff --git a/__tests__/queryTerms.test.ts b/__tests__/queryTerms.test.ts new file mode 100644 index 00000000..47556d59 --- /dev/null +++ b/__tests__/queryTerms.test.ts @@ -0,0 +1,47 @@ +import { extractQueryTerms, stemPrefix } from '../utils/queryTerms'; + +describe('extractQueryTerms', () => { + it('drops short bare numbers and codes that caused false highlights', () => { + const terms = extractQueryTerms('L4 100% for the first 5 of 30 days'); + expect(terms.has('l4')).toBe(false); + expect(terms.has('5')).toBe(false); + expect(terms.has('30')).toBe(false); + expect(terms.has('100')).toBe(true); + expect(terms.has('first')).toBe(true); + expect(terms.has('days')).toBe(true); + }); + + it('keeps longer identifiers and years', () => { + const terms = extractQueryTerms( + 'What changed in invoice FS-219039 during 2020?' + ); + expect(terms.has('219039')).toBe(true); + expect(terms.has('2020')).toBe(true); + expect(terms.has('invoice')).toBe(true); + expect(terms.has('fs')).toBe(false); + }); + + it('ignores stopwords and empty input', () => { + expect(extractQueryTerms('what is the').size).toBe(0); + expect(extractQueryTerms('').size).toBe(0); + }); +}); + +describe('stemPrefix', () => { + it('reduces inflected Polish words to a shared stem', () => { + expect(stemPrefix('pliku')).toBe('plik'); + expect(stemPrefix('plików')).toBe('plik'); + expect(stemPrefix('linijce')).toBe('linij'); + }); + + it('never truncates below 4 characters', () => { + expect(stemPrefix('kotek')).toBe('kote'); + expect(stemPrefix('rok')).toBe('rok'); + }); + + it('leaves identifiers, codes and years untouched', () => { + expect(stemPrefix('219039')).toBe('219039'); + expect(stemPrefix('e-4021')).toBe('e-4021'); + expect(stemPrefix('2026')).toBe('2026'); + }); +}); diff --git a/__tests__/ragPipeline.integration.test.ts b/__tests__/ragPipeline.integration.test.ts new file mode 100644 index 00000000..0cfa7426 --- /dev/null +++ b/__tests__/ragPipeline.integration.test.ts @@ -0,0 +1,327 @@ +import { + buildMessageSources, + pickCitationsByAnswer, + restrictCitationsToContext, + type SourceRow, +} from '../utils/messageSources'; +import { sourcesPresentInContext } from '../utils/contextUtils'; +import type { OPSQLiteVectorStore } from '@react-native-rag/op-sqlite'; + +type VectorRow = { + id: string; + document: string; + embedding: number[]; + similarity: number; + metadata: { documentId: number; name?: string }; +}; + +const makeVectorStore = (queryResults: VectorRow[]) => { + const byId = new Map(queryResults.map((r) => [r.id, r])); + return { + query: jest.fn().mockResolvedValue(queryResults), + db: { + execute: jest + .fn() + .mockImplementation(async (_sql: string, ids: string[]) => ({ + rows: ids + .map((id) => byId.get(id)) + .filter(Boolean) + .map((r) => ({ + id: r!.id, + document: r!.document, + embedding: r!.embedding, + metadata: JSON.stringify(r!.metadata), + })), + })), + }, + } as unknown as OPSQLiteVectorStore; +}; + +const source = (id: number, name: string, firstChunk?: string): SourceRow => ({ + id, + name, + firstChunk, +}); + +const presentNames = (context: string[]): Set => + sourcesPresentInContext(context.join('\n')); + +beforeEach(() => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + jest.spyOn(console, 'warn').mockImplementation(() => {}); +}); + +afterEach(() => { + jest.restoreAllMocks(); +}); + +describe('buildMessageSources — retrieval → context → citation pipeline', () => { + it('keeps context "Source N" headers and the citation set in lockstep across two documents', async () => { + const vectorStore = makeVectorStore([ + { + id: '1:0', + document: 'vacation policy grants twenty six days of paid leave', + embedding: [1, 0], + similarity: 0.85, + metadata: { documentId: 1, name: 'handbook.pdf' }, + }, + { + id: '2:0', + document: 'quarterly revenue reached five million dollars', + embedding: [0, 1], + similarity: 0.82, + metadata: { documentId: 2, name: 'q4_report.pdf' }, + }, + ]); + + const { context, sourceDocuments, preferredSourceDocuments } = + await buildMessageSources({ + userInput: 'how many vacation days and what was the revenue', + attachmentSourceIds: [], + enabledSources: [1, 2], + sources: [source(1, 'handbook.pdf'), source(2, 'q4_report.pdf')], + vectorStore, + embeddings: null, + }); + + expect(context.join('\n')).toContain('--- Source 1:'); + expect(context.join('\n')).toContain('--- Source 2:'); + + const citedNames = new Set(sourceDocuments.map((d) => d.name)); + expect(citedNames).toEqual(presentNames(context)); + expect(citedNames).toEqual(new Set(['handbook.pdf', 'q4_report.pdf'])); + expect(preferredSourceDocuments).toEqual([]); + }); + + it('prepends the attachment overview and orders the attachment first in the citations', async () => { + const vectorStore = makeVectorStore([ + { + id: '1:0', + document: 'older library document about vacation policy', + embedding: [1, 0], + similarity: 0.8, + metadata: { documentId: 1, name: 'library.pdf' }, + }, + { + id: '2:0', + document: 'freshly attached note with low semantic overlap', + embedding: [0, 1], + similarity: 0.05, + metadata: { documentId: 2, name: 'attachment.txt' }, + }, + ]); + + const { context, sourceDocuments, preferredSourceDocuments } = + await buildMessageSources({ + userInput: 'what does the attachment say', + attachmentSourceIds: [2], + enabledSources: [1], + sources: [ + source(1, 'library.pdf'), + source(2, 'attachment.txt', 'attached overview snippet'), + ], + vectorStore, + embeddings: null, + }); + + expect(context[0]).toContain( + 'Current Attachment Source: attachment.txt (Overview)' + ); + expect(sourceDocuments[0].documentId).toBe(2); + expect(preferredSourceDocuments.map((d) => d.documentId)).toEqual([2]); + expect(new Set(sourceDocuments.map((d) => d.name))).toEqual( + new Set(['attachment.txt', 'library.pdf']) + ); + }); + + it('still cites a freshly attached source that produced no retrieved chunk', async () => { + const vectorStore = makeVectorStore([ + { + id: '1:0', + document: 'the only retrievable content is in the library file', + embedding: [1, 0], + similarity: 0.8, + metadata: { documentId: 1, name: 'library.pdf' }, + }, + ]); + + const { context, sourceDocuments } = await buildMessageSources({ + userInput: 'summarize everything', + attachmentSourceIds: [2], + enabledSources: [1], + sources: [ + source(1, 'library.pdf'), + source(2, 'attachment.pdf', 'attachment overview only'), + ], + vectorStore, + embeddings: null, + }); + + expect(sourceDocuments.map((d) => d.documentId)).toEqual([2, 1]); + expect(context[0]).toContain('attachment.pdf (Overview)'); + }); + + it('takes the attachment-only path when there is no user query', async () => { + const vectorStore = makeVectorStore([]); + + const { context, sourceDocuments } = await buildMessageSources({ + userInput: ' ', + attachmentSourceIds: [5], + enabledSources: [], + sources: [source(5, 'dropped.pdf', 'just attached, no question yet')], + vectorStore, + embeddings: null, + }); + + expect(vectorStore.query).not.toHaveBeenCalled(); + expect(sourceDocuments).toEqual([ + { + documentId: 5, + name: 'dropped.pdf', + passage: 'just attached, no question yet', + }, + ]); + expect(context[0]).toContain('dropped.pdf (Overview)'); + }); + + it('returns nothing and never touches retrieval when no sources are active', async () => { + const vectorStore = makeVectorStore([]); + + const result = await buildMessageSources({ + userInput: 'anything', + attachmentSourceIds: [], + enabledSources: [], + sources: [source(1, 'unused.pdf')], + vectorStore, + embeddings: null, + }); + + expect(result).toEqual({ + context: [], + sourceDocuments: [], + preferredSourceDocuments: [], + }); + expect(vectorStore.query).not.toHaveBeenCalled(); + }); + + it('never emits a citation whose block is absent from the context sent to the model', async () => { + const vectorStore = makeVectorStore([ + { + id: '1:0', + document: 'vacation policy grants twenty six days of paid leave', + embedding: [1, 0], + similarity: 0.85, + metadata: { documentId: 1, name: 'handbook.pdf' }, + }, + { + id: '2:0', + document: 'quarterly revenue reached five million dollars', + embedding: [0, 1], + similarity: 0.82, + metadata: { documentId: 2, name: 'q4_report.pdf' }, + }, + ]); + + const { context, sourceDocuments, preferredSourceDocuments } = + await buildMessageSources({ + userInput: 'how many vacation days do i get', + attachmentSourceIds: [], + enabledSources: [1, 2], + sources: [source(1, 'handbook.pdf'), source(2, 'q4_report.pdf')], + vectorStore, + embeddings: null, + }); + + const answer = + 'You are granted twenty six days of paid vacation leave each year.'; + + const byAnswer = pickCitationsByAnswer( + sourceDocuments, + answer, + preferredSourceDocuments + ); + const finalCitations = restrictCitationsToContext( + byAnswer, + context.join('\n'), + preferredSourceDocuments + ); + + expect(finalCitations.map((d) => d.documentId)).toEqual([1]); + const present = presentNames(context); + for (const cited of finalCitations) { + expect(present.has(cited.name)).toBe(true); + } + }); + + it('surfaces a paraphrase match that clears the lowered semantic threshold', async () => { + const vectorStore = makeVectorStore([ + { + id: '1:0', + document: 'the summit of Everest is the highest point on Earth', + embedding: [1, 0], + similarity: 0.45, + metadata: { documentId: 1, name: 'everest.txt' }, + }, + ]); + + const { context, sourceDocuments } = await buildMessageSources({ + userInput: 'which mountain is the tallest', + attachmentSourceIds: [], + enabledSources: [1], + sources: [source(1, 'everest.txt')], + vectorStore, + embeddings: null, + }); + + expect(context.join('\n')).toContain('--- Source 1:'); + expect(sourceDocuments.map((d) => d.name)).toEqual(['everest.txt']); + }); + + it('keeps the single best chunk above the top-keep floor even below the main threshold', async () => { + const vectorStore = makeVectorStore([ + { + id: '1:0', + document: 'a loosely related passage about alpine geography', + embedding: [1, 0], + similarity: 0.3, + metadata: { documentId: 1, name: 'geography.txt' }, + }, + ]); + + const { context, sourceDocuments } = await buildMessageSources({ + userInput: 'which mountain is the tallest', + attachmentSourceIds: [], + enabledSources: [1], + sources: [source(1, 'geography.txt')], + vectorStore, + embeddings: null, + }); + + expect(context.join('\n')).toContain('--- Source 1:'); + expect(sourceDocuments.map((d) => d.name)).toEqual(['geography.txt']); + }); + + it('returns nothing when even the best chunk is below the top-keep floor', async () => { + const vectorStore = makeVectorStore([ + { + id: '1:0', + document: 'entirely unrelated noise passage', + embedding: [1, 0], + similarity: 0.2, + metadata: { documentId: 1, name: 'noise.txt' }, + }, + ]); + + const { context, sourceDocuments } = await buildMessageSources({ + userInput: 'which mountain is the tallest', + attachmentSourceIds: [], + enabledSources: [1], + sources: [source(1, 'noise.txt')], + vectorStore, + embeddings: null, + }); + + expect(context).toEqual([]); + expect(sourceDocuments).toEqual([]); + }); +}); diff --git a/__tests__/retrieval.test.ts b/__tests__/retrieval.test.ts new file mode 100644 index 00000000..a67704b0 --- /dev/null +++ b/__tests__/retrieval.test.ts @@ -0,0 +1,62 @@ +import { retrieve } from '../utils/retrieval'; +import type { OPSQLiteVectorStore } from '@react-native-rag/op-sqlite'; + +const embeddingBuffer = () => new Float32Array([1, 0]).buffer; + +const seed = (chunkIndex: number, similarity: number) => ({ + id: `1:${chunkIndex}`, + document: `chunk-${chunkIndex}`, + embedding: [1, 0], + similarity, + metadata: { documentId: 1, name: 'report.txt' }, +}); + +const neighborRow = (chunkIndex: number) => ({ + id: `1:${chunkIndex}`, + document: `chunk-${chunkIndex}`, + embedding: embeddingBuffer(), + metadata: JSON.stringify({ documentId: 1, name: 'report.txt' }), +}); + +const makeVectorStore = ( + queryResults: unknown[], + rowsById: Record +) => + ({ + query: jest.fn().mockResolvedValue(queryResults), + db: { + execute: jest + .fn() + .mockImplementation(async (_sql: string, ids: string[]) => ({ + rows: ids.map((id) => rowsById[id]).filter(Boolean), + })), + }, + }) as unknown as OPSQLiteVectorStore; + +describe('retrieve — neighbor expansion', () => { + it('emits one document in reading order even when a later passage ranks higher', async () => { + const vectorStore = makeVectorStore([seed(10, 0.9), seed(2, 0.5)], { + '1:9': neighborRow(9), + '1:11': neighborRow(11), + '1:1': neighborRow(1), + '1:3': neighborRow(3), + }); + + const result = await retrieve({ + prompt: 'anything', + enabledSourceIds: [1], + vectorStore, + sourceNamesById: new Map([[1, 'report.txt']]), + embeddings: null, + }); + + expect(result.map((chunk) => chunk.document)).toEqual([ + 'chunk-1', + 'chunk-2', + 'chunk-3', + 'chunk-9', + 'chunk-10', + 'chunk-11', + ]); + }); +}); diff --git a/__tests__/sourceStore.test.ts b/__tests__/sourceStore.test.ts index b0c71b5c..4c27cc10 100644 --- a/__tests__/sourceStore.test.ts +++ b/__tests__/sourceStore.test.ts @@ -1,7 +1,15 @@ import { useSourceStore } from '../store/sourceStore'; import * as sourcesRepository from '../database/sourcesRepository'; +import type { Source } from '../database/sourcesRepository'; import * as fileReaders from '../utils/fileReaders'; import { useLLMStore } from '../store/llmStore'; +import type { SQLiteDatabase } from 'expo-sqlite'; +import type { OPSQLiteVectorStore } from '@react-native-rag/op-sqlite'; +import type { LFMEmbeddings } from '../utils/lfmEmbeddings'; +import { + MAX_SOURCE_CHUNKS, + MAX_SOURCE_TEXT_CHARS, +} from '../constants/retrieval'; jest.mock('../database/sourcesRepository'); jest.mock('../utils/fileReaders'); @@ -19,8 +27,13 @@ jest.mock('react-native-rag', () => ({ })); jest.mock('@react-native-rag/op-sqlite', () => ({})); -const mockDb = {} as any; -const mockVectorStore = { add: jest.fn() } as any; +const mockDb = {} as Partial as SQLiteDatabase; +const vectorStoreAdd = jest.fn(); +const vectorStoreDelete = jest.fn(); +const mockVectorStore = { + add: vectorStoreAdd, + delete: vectorStoreDelete, +} as Partial as OPSQLiteVectorStore; const mockReadDocumentText = fileReaders.readDocumentText as jest.Mock; const mockInsertSource = sourcesRepository.insertSource as jest.Mock; @@ -71,6 +84,22 @@ describe('addSource', () => { expect(useSourceStore.getState().isReading).toBe(false); }); + it('flags a PDF with no extractable text as scanned', async () => { + mockReadDocumentText.mockResolvedValue(''); + const result = await useSourceStore + .getState() + .addSource( + { name: 'scan.pdf', type: 'pdf', size: 100 }, + '/path/scan.pdf', + mockVectorStore + ); + expect(result).toEqual({ + success: false, + isEmpty: true, + reason: 'scanned_pdf', + }); + }); + it('sets isReading to true during processing then false on success', async () => { mockReadDocumentText.mockResolvedValue('some content'); mockInsertSource.mockResolvedValue(42); @@ -87,7 +116,7 @@ describe('addSource', () => { }); it('adds a temp source with negative id and isProcessing=true before DB insert', async () => { - let capturedSources: any[] = []; + let capturedSources: Source[] = []; mockReadDocumentText.mockResolvedValue('content'); mockInsertSource.mockImplementation(async () => { capturedSources = useSourceStore.getState().sources; @@ -118,12 +147,73 @@ describe('addSource', () => { .addSource(baseSource, '/path/doc.txt', mockVectorStore); const sources = useSourceStore.getState().sources; - expect(result).toEqual({ success: true, sourceId: 99 }); + expect(result).toEqual({ success: true, sourceId: 99, truncated: false }); expect(sources).toHaveLength(1); expect(sources[0].id).toBe(99); expect(sources[0].isProcessing).toBe(false); }); + it('caps embedded chunks at MAX_SOURCE_CHUNKS and flags the result truncated', async () => { + mockReadDocumentText.mockResolvedValue('content'); + mockInsertSource.mockResolvedValue(99); + const manyChunks = Array.from( + { length: MAX_SOURCE_CHUNKS + 1 }, + (_, i) => `chunk-${i}` + ); + MockSplitter.mockImplementation(() => ({ + splitText: jest.fn().mockResolvedValue(manyChunks), + })); + + const result = await useSourceStore + .getState() + .addSource(baseSource, '/path/doc.txt', mockVectorStore); + + expect(result).toEqual({ success: true, sourceId: 99, truncated: true }); + expect(vectorStoreAdd).toHaveBeenCalledTimes(MAX_SOURCE_CHUNKS); + }); + + it('caps oversized extracted text before splitting and flags it truncated', async () => { + const oversized = 'a'.repeat(MAX_SOURCE_TEXT_CHARS + 500); + mockReadDocumentText.mockResolvedValue(oversized); + mockInsertSource.mockResolvedValue(99); + const splitText = jest.fn().mockResolvedValue(['chunk']); + MockSplitter.mockImplementation(() => ({ splitText })); + + const result = await useSourceStore + .getState() + .addSource(baseSource, '/path/doc.txt', mockVectorStore); + + expect(result).toEqual({ success: true, sourceId: 99, truncated: true }); + expect(splitText.mock.calls[0][0]).toHaveLength(MAX_SOURCE_TEXT_CHARS); + }); + + it('aborts embedding and rolls back the partial source when the signal is aborted', async () => { + mockReadDocumentText.mockResolvedValue('content'); + mockInsertSource.mockResolvedValue(99); + MockSplitter.mockImplementation(() => ({ + splitText: jest.fn().mockResolvedValue(['chunk-a', 'chunk-b']), + })); + const controller = new AbortController(); + controller.abort(); + + const result = await useSourceStore + .getState() + .addSource( + baseSource, + '/path/doc.txt', + mockVectorStore, + undefined, + undefined, + controller.signal + ); + + expect(result).toEqual({ success: false, cancelled: true }); + expect(vectorStoreAdd).not.toHaveBeenCalled(); + expect(vectorStoreDelete).toHaveBeenCalledTimes(1); + expect(mockDeleteSource).toHaveBeenCalledWith(mockDb, 99); + expect(useSourceStore.getState().sources).toHaveLength(0); + }); + it('passes firstChunk to insertSource', async () => { mockReadDocumentText.mockResolvedValue('content'); mockInsertSource.mockResolvedValue(1); @@ -154,17 +244,54 @@ describe('addSource', () => { .getState() .addSource(baseSource, '/path/doc.txt', mockVectorStore); - expect(mockVectorStore.add).toHaveBeenCalledTimes(3); - expect(mockVectorStore.add).toHaveBeenCalledWith({ + expect(vectorStoreAdd).toHaveBeenCalledTimes(3); + expect(vectorStoreAdd).toHaveBeenCalledWith({ + id: '1:0', document: 'a', - metadata: { documentId: 1, isFirstChunk: true }, + embedding: undefined, + metadata: { + documentId: 1, + name: 'doc.txt', + chunkIndex: 0, + isFirstChunk: true, + }, }); - expect(mockVectorStore.add).toHaveBeenCalledWith({ + expect(vectorStoreAdd).toHaveBeenCalledWith({ + id: '1:1', document: 'b', - metadata: { documentId: 1, isFirstChunk: false }, + embedding: undefined, + metadata: { + documentId: 1, + name: 'doc.txt', + chunkIndex: 1, + isFirstChunk: false, + }, }); }); + it('embeds each chunk with the document prefix when embeddings are provided', async () => { + mockReadDocumentText.mockResolvedValue('content'); + mockInsertSource.mockResolvedValue(1); + MockSplitter.mockImplementation(() => ({ + splitText: jest.fn().mockResolvedValue(['a', 'b']), + })); + const embedDocument = jest + .fn() + .mockImplementation(async (text: string) => [text.length]); + + await useSourceStore + .getState() + .addSource(baseSource, '/path/doc.txt', mockVectorStore, { + embedDocument, + } as Partial as LFMEmbeddings); + + expect(embedDocument).toHaveBeenCalledWith('a'); + expect(embedDocument).toHaveBeenCalledWith('b'); + expect(vectorStoreAdd).toHaveBeenCalledWith( + expect.objectContaining({ document: 'a', embedding: [1] }) + ); + }); + it('removes temp source and resets isReading when DB insert fails', async () => { mockReadDocumentText.mockResolvedValue('content'); mockInsertSource.mockResolvedValue(null); // insert failure @@ -256,10 +383,11 @@ describe('cleanupOrphanedSources', () => { mockGetOrphanedSources.mockResolvedValue(orphaned); mockDeleteSource.mockResolvedValue(undefined); + const orphanVectorStoreDelete = jest.fn(); const mockVectorStoreWithDelete = { - ...mockVectorStore, - delete: jest.fn(), - }; + add: vectorStoreAdd, + delete: orphanVectorStoreDelete, + } as Partial as OPSQLiteVectorStore; await useSourceStore .getState() @@ -267,7 +395,7 @@ describe('cleanupOrphanedSources', () => { expect(mockGetOrphanedSources).toHaveBeenCalledWith(mockDb); expect(mockDeleteSource).toHaveBeenCalledWith(mockDb, 5); - expect(mockVectorStoreWithDelete.delete).toHaveBeenCalledWith({ + expect(orphanVectorStoreDelete).toHaveBeenCalledWith({ predicate: expect.any(Function), }); }); @@ -288,9 +416,9 @@ describe('cleanupOrphanedSources', () => { mockGetAllSources.mockResolvedValue(updated); const mockVectorStoreWithDelete = { - ...mockVectorStore, + add: vectorStoreAdd, delete: jest.fn(), - }; + } as Partial as OPSQLiteVectorStore; await useSourceStore .getState() diff --git a/__tests__/useAttachment.test.ts b/__tests__/useAttachment.test.ts index ec0bd05b..a08b2898 100644 --- a/__tests__/useAttachment.test.ts +++ b/__tests__/useAttachment.test.ts @@ -7,10 +7,12 @@ jest.mock('react-native-image-picker', () => ({ jest.mock('expo-document-picker', () => ({ getDocumentAsync: jest.fn(), })); +const mockCleanupOrphanedSources = jest.fn(); jest.mock('../store/sourceStore', () => ({ useSourceStore: { getState: jest.fn(() => ({ addSource: jest.fn(), + cleanupOrphanedSources: mockCleanupOrphanedSources, })), }, })); @@ -27,12 +29,25 @@ jest.mock('react-native-toast-message', () => ({ import { launchImageLibrary, launchCamera } from 'react-native-image-picker'; import * as DocumentPicker from 'expo-document-picker'; import { useAttachment } from '../hooks/useAttachment'; +import { useEmbeddingModelStore } from '../store/embeddingModelStore'; const mockLaunchImageLibrary = launchImageLibrary as jest.Mock; const mockLaunchCamera = launchCamera as jest.Mock; const mockGetDocumentAsync = DocumentPicker.getDocumentAsync as jest.Mock; -beforeEach(() => jest.clearAllMocks()); +beforeEach(() => { + jest.clearAllMocks(); + useEmbeddingModelStore.setState({ status: 'ready', progress: 1 }); +}); + +const createDeferred = () => { + let resolve!: (value: T) => void; + const promise = new Promise((promiseResolve) => { + resolve = promiseResolve; + }); + + return { promise, resolve }; +}; describe('useAttachment', () => { it('initializes with empty attachments', () => { @@ -85,7 +100,10 @@ describe('useAttachment', () => { .fn() .mockResolvedValue({ success: true, sourceId: 42 }); const { useSourceStore } = require('../store/sourceStore'); - useSourceStore.getState.mockReturnValue({ addSource: mockAddSource }); + useSourceStore.getState.mockReturnValue({ + addSource: mockAddSource, + cleanupOrphanedSources: mockCleanupOrphanedSources, + }); const { result } = renderHook(() => useAttachment()); await act(async () => { @@ -99,6 +117,125 @@ describe('useAttachment', () => { expect(att.status).toBe('ready'); }); + describe('abandoned source cleanup', () => { + const pickReadyDocument = async () => { + mockGetDocumentAsync.mockResolvedValue({ + canceled: false, + assets: [{ uri: 'file://doc.txt', name: 'doc.txt', size: 100 }], + }); + const { useSourceStore } = require('../store/sourceStore'); + useSourceStore.getState.mockReturnValue({ + addSource: jest.fn().mockResolvedValue({ success: true, sourceId: 42 }), + cleanupOrphanedSources: mockCleanupOrphanedSources, + }); + + const view = renderHook(() => useAttachment()); + await act(async () => { + await view.result.current.pickDocument(); + }); + mockCleanupOrphanedSources.mockClear(); + return view; + }; + + it('sweeps when an embedded document is removed before it is ever sent', async () => { + const { result } = await pickReadyDocument(); + + act(() => { + result.current.removeAttachment(result.current.attachments[0].id); + }); + + expect(mockCleanupOrphanedSources).toHaveBeenCalledTimes(1); + }); + + it('sweeps when the screen unmounts with the document still attached', async () => { + const { unmount } = await pickReadyDocument(); + + unmount(); + + expect(mockCleanupOrphanedSources).toHaveBeenCalledTimes(1); + }); + + it('does not sweep on send, when the source is about to be linked to the chat', async () => { + const { result } = await pickReadyDocument(); + + act(() => { + result.current.clearAll({ cleanupSources: false }); + }); + + expect(mockCleanupOrphanedSources).not.toHaveBeenCalled(); + }); + + it('does not sweep when a plain image is removed', async () => { + mockLaunchImageLibrary.mockResolvedValue({ + assets: [{ uri: 'file://photo.jpg' }], + }); + const { result } = renderHook(() => useAttachment()); + await act(async () => { + await result.current.pickFromLibrary(); + }); + mockCleanupOrphanedSources.mockClear(); + + act(() => { + result.current.removeAttachment(result.current.attachments[0].id); + }); + + expect(mockCleanupOrphanedSources).not.toHaveBeenCalled(); + }); + }); + + it('ignores a stale document result when a second document replaces it', async () => { + const firstSource = createDeferred<{ + success: boolean; + sourceId: number; + }>(); + const secondSource = createDeferred<{ + success: boolean; + sourceId: number; + }>(); + const mockAddSource = jest + .fn() + .mockReturnValueOnce(firstSource.promise) + .mockReturnValueOnce(secondSource.promise); + const { useSourceStore } = require('../store/sourceStore'); + useSourceStore.getState.mockReturnValue({ + addSource: mockAddSource, + cleanupOrphanedSources: mockCleanupOrphanedSources, + }); + mockGetDocumentAsync + .mockResolvedValueOnce({ + canceled: false, + assets: [{ uri: 'file://first.pdf', name: 'first.pdf', size: 100 }], + }) + .mockResolvedValueOnce({ + canceled: false, + assets: [{ uri: 'file://second.pdf', name: 'second.pdf', size: 100 }], + }); + jest.spyOn(console, 'warn').mockImplementation(() => {}); + + const { result } = renderHook(() => useAttachment()); + let firstPick!: Promise; + let secondPick!: Promise; + + await act(async () => { + firstPick = result.current.pickDocument(); + }); + await act(async () => { + secondPick = result.current.pickDocument(); + }); + await act(async () => { + secondSource.resolve({ success: true, sourceId: 2 }); + await secondPick; + }); + await act(async () => { + firstSource.resolve({ success: true, sourceId: 1 }); + await firstPick; + }); + + expect(result.current.attachments).toHaveLength(1); + expect(result.current.attachments[0].name).toBe('second.pdf'); + expect(result.current.attachments[0].sourceId).toBe(2); + }); + it('removeAttachment removes by id', async () => { mockLaunchImageLibrary.mockResolvedValue({ assets: [{ uri: 'file://photo.jpg' }], @@ -178,7 +315,10 @@ describe('useAttachment', () => { .fn() .mockResolvedValue({ success: true, sourceId: 7 }); const { useSourceStore } = require('../store/sourceStore'); - useSourceStore.getState.mockReturnValue({ addSource: mockAddSource }); + useSourceStore.getState.mockReturnValue({ + addSource: mockAddSource, + cleanupOrphanedSources: mockCleanupOrphanedSources, + }); const { result } = renderHook(() => useAttachment()); await act(async () => { @@ -192,7 +332,7 @@ describe('useAttachment', () => { expect(result.current.attachments[0].type).toBe('document'); }); - it('picking an image after a document cleans up the orphaned source', async () => { + it('picking an image after a document does not clean up sources during attachment replacement', async () => { mockGetDocumentAsync.mockResolvedValue({ canceled: false, assets: [{ uri: 'file://doc.txt', name: 'doc.txt', size: 100 }], @@ -222,7 +362,7 @@ describe('useAttachment', () => { expect(result.current.attachments).toHaveLength(1); expect(result.current.attachments[0].type).toBe('image'); - expect(mockCleanup).toHaveBeenCalled(); + expect(mockCleanup).not.toHaveBeenCalled(); }); it('addPastedAttachment replaces an existing image', () => { diff --git a/assets/icons/book-open.svg b/assets/icons/book-open.svg new file mode 100644 index 00000000..d30fae05 --- /dev/null +++ b/assets/icons/book-open.svg @@ -0,0 +1,4 @@ + + + + diff --git a/components/bottomSheets/EmbeddingDownloadSheet.tsx b/components/bottomSheets/EmbeddingDownloadSheet.tsx new file mode 100644 index 00000000..2a7fb8de --- /dev/null +++ b/components/bottomSheets/EmbeddingDownloadSheet.tsx @@ -0,0 +1,162 @@ +import React, { RefObject, useCallback, useMemo } from 'react'; +import { + BottomSheetModal, + BottomSheetView, + BottomSheetBackdrop, + type BottomSheetBackdropProps, +} from '@gorhom/bottom-sheet'; +import { StyleSheet, Text, View } from 'react-native'; +import { fontFamily, fontSizes } from '../../styles/fontStyles'; +import { useTheme } from '../../context/ThemeContext'; +import { Theme } from '../../styles/colors'; +import PrimaryButton from '../PrimaryButton'; +import SecondaryButton from '../SecondaryButton'; +import { useEmbeddingModelStore } from '../../store/embeddingModelStore'; +import { embeddingModelDownloadSizeLabel } from '../../utils/embeddingModel'; + +type Props = { + bottomSheetModalRef: RefObject; + onDownload: () => void; + onDismiss?: () => void; +}; + +const EmbeddingDownloadSheet = ({ + bottomSheetModalRef, + onDownload, + onDismiss, +}: Props) => { + const { theme } = useTheme(); + const styles = useMemo(() => createStyles(theme), [theme]); + const status = useEmbeddingModelStore((state) => state.status); + const progress = useEmbeddingModelStore((state) => state.progress); + + const isDownloading = status === 'downloading'; + const isError = status === 'error'; + + const renderBackdrop = useCallback( + (props: BottomSheetBackdropProps) => ( + + ), + [styles.backdrop] + ); + + const handleCancel = useCallback( + () => bottomSheetModalRef.current?.dismiss(), + [bottomSheetModalRef] + ); + + return ( + + + Download document model + + {isError + ? 'The document model could not be downloaded. Check your connection and try again.' + : isDownloading + ? 'You can close this sheet — the download keeps going in the background and resumes when you reopen it.' + : `To attach documents, Private Mind needs to download the on-device embedding model once (~${embeddingModelDownloadSizeLabel()}). It is then reused for every future document.`} + + + {isDownloading ? ( + + + + + + {Math.floor(progress * 100)}% + + + ) : ( + + + + + )} + + + ); +}; + +export default EmbeddingDownloadSheet; + +const createStyles = (theme: Theme) => + StyleSheet.create({ + sheet: { + paddingVertical: 24, + paddingHorizontal: 16, + paddingBottom: theme.insets.bottom + 16, + gap: 24, + backgroundColor: theme.bg.softPrimary, + }, + backdrop: { + backgroundColor: theme.bg.overlay, + }, + handleStyle: { + backgroundColor: theme.bg.softPrimary, + borderRadius: 18, + }, + handleIndicator: { + width: 64, + height: 4, + borderRadius: 9999, + backgroundColor: theme.text.primary, + }, + background: { + backgroundColor: theme.bg.softPrimary, + }, + title: { + fontSize: fontSizes.lg, + fontFamily: fontFamily.medium, + color: theme.text.primary, + }, + subText: { + fontSize: fontSizes.md, + fontFamily: fontFamily.regular, + color: theme.text.defaultSecondary, + }, + buttonGroup: { + gap: 8, + }, + progressRow: { + flexDirection: 'row', + alignItems: 'center', + gap: 12, + }, + progressBarContainer: { + flex: 1, + height: 8, + borderRadius: 9999, + overflow: 'hidden', + backgroundColor: theme.bg.softSecondary, + }, + progressBar: { + height: '100%', + borderRadius: 9999, + backgroundColor: theme.bg.strongPrimary, + }, + progressText: { + fontSize: fontSizes.sm, + fontFamily: fontFamily.medium, + color: theme.text.primary, + minWidth: 40, + textAlign: 'right', + }, + }); diff --git a/components/bottomSheets/SheetBackdrop.tsx b/components/bottomSheets/SheetBackdrop.tsx new file mode 100644 index 00000000..d67a11bb --- /dev/null +++ b/components/bottomSheets/SheetBackdrop.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { Pressable, useWindowDimensions } from 'react-native'; +import { + useBottomSheet, + type BottomSheetBackdropProps, +} from '@gorhom/bottom-sheet'; +import Animated, { + Extrapolation, + interpolate, + useAnimatedStyle, +} from 'react-native-reanimated'; +import { useTheme } from '../../context/ThemeContext'; +import { BACKDROP_CLOSE_FADE } from '../../constants/bottom-sheet'; + +const AnimatedPressable = Animated.createAnimatedComponent(Pressable); + +const SheetBackdrop = ({ + animatedPosition, + style, +}: BottomSheetBackdropProps) => { + const { close } = useBottomSheet(); + const { theme } = useTheme(); + const { height: screenHeight } = useWindowDimensions(); + + const animatedStyle = useAnimatedStyle(() => ({ + opacity: interpolate( + animatedPosition.value, + [screenHeight - BACKDROP_CLOSE_FADE, screenHeight], + [1, 0], + Extrapolation.CLAMP + ), + })); + + return ( + close()} + style={[style, { backgroundColor: theme.bg.overlay }, animatedStyle]} + /> + ); +}; + +export default SheetBackdrop; diff --git a/components/chat-screen/AttachmentThumbnail.tsx b/components/chat-screen/AttachmentThumbnail.tsx index 5e1c3001..e126c12c 100644 --- a/components/chat-screen/AttachmentThumbnail.tsx +++ b/components/chat-screen/AttachmentThumbnail.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { View, Image, @@ -7,12 +7,19 @@ import { ActivityIndicator, StyleSheet, } from 'react-native'; +import Animated, { + Easing, + useAnimatedStyle, + useSharedValue, + withTiming, +} from 'react-native-reanimated'; import { useTheme } from '../../context/ThemeContext'; import { Theme } from '../../styles/colors'; import { fontFamily, fontSizes } from '../../styles/fontStyles'; import CloseIcon from '../../assets/icons/close.svg'; import AttachmentIcon from '../../assets/icons/attachment.svg'; import { Attachment } from '../../hooks/useAttachment'; +import { ATTACHMENT_PROGRESS_TRACK_WIDTH } from '../../constants/chat'; interface Props { attachment: Attachment; @@ -23,11 +30,42 @@ const AttachmentThumbnail = ({ attachment, onRemove }: Props) => { const { theme } = useTheme(); const styles = useMemo(() => createStyles(theme), [theme]); + const fill = useSharedValue(0); + useEffect(() => { + if (attachment.progress == null) return; + fill.set( + withTiming(attachment.progress * ATTACHMENT_PROGRESS_TRACK_WIDTH, { + duration: 250, + easing: Easing.out(Easing.cubic), + }) + ); + }, [attachment.progress, fill]); + + const fillStyle = useAnimatedStyle(() => ({ width: fill.get() })); + const renderContent = () => { if (attachment.status === 'loading') { + if (attachment.progress == null) { + return ( + + + + ); + } + return ( - - + + + + {Math.round(attachment.progress * 100)}% + + + + ); } @@ -107,6 +145,24 @@ const createStyles = (theme: Theme) => maxWidth: 60, textAlign: 'center', }, + percentText: { + fontSize: fontSizes.sm, + fontFamily: fontFamily.medium, + color: theme.text.primary, + fontVariant: ['tabular-nums'], + }, + progressTrack: { + width: ATTACHMENT_PROGRESS_TRACK_WIDTH, + height: 4, + borderRadius: 2, + backgroundColor: theme.bg.softSecondary, + overflow: 'hidden', + }, + progressFill: { + height: 4, + borderRadius: 2, + backgroundColor: theme.bg.strongPrimary, + }, dismissButton: { position: 'absolute', top: -6, diff --git a/components/chat-screen/ChatBar.tsx b/components/chat-screen/ChatBar.tsx index d2a294de..45273cda 100644 --- a/components/chat-screen/ChatBar.tsx +++ b/components/chat-screen/ChatBar.tsx @@ -23,6 +23,7 @@ import Animated, { } from 'react-native-reanimated'; import { type PasteEventPayload, TextInputWrapper } from 'expo-paste-input'; import AttachmentSheet from '../bottomSheets/AttachmentSheet'; +import EmbeddingDownloadSheet from '../bottomSheets/EmbeddingDownloadSheet'; import { useAttachment, Attachment } from '../../hooks/useAttachment'; import { Model } from '../../database/modelRepository'; import { fontFamily, fontSizes, lineHeights } from '../../styles/fontStyles'; @@ -53,7 +54,6 @@ interface Props { onSelectModel: () => void; onSelectPrompt: (prompt: string) => void; ref: Ref<{ - clear: () => void; setInput: (text: string) => void; }>; model: Model | undefined; @@ -94,9 +94,12 @@ const ChatBar = ({ const { attachments, sheetRef, + embeddingDownloadSheetRef, pickFromLibrary, pickFromCamera, pickDocument, + downloadModelAndContinue, + markDownloadSheetClosed, removeAttachment, clearAll, openSheet, @@ -116,15 +119,6 @@ const ChatBar = ({ useImperativeHandle( ref, () => ({ - clear: () => { - setUserInput(''); - clearAll(); - extraContentPadding.set(0); - if (Platform.OS === 'ios') { - textInputRef.current?.setNativeProps({ text: ' ' }); - textInputRef.current?.setNativeProps({ text: '' }); - } - }, setInput: (text: string) => { setUserInput(text); if (Platform.OS === 'ios') { @@ -132,7 +126,7 @@ const ChatBar = ({ } }, }), - [clearAll, extraContentPadding] + [] ); const handleBarLayoutForPadding = useCallback( @@ -365,6 +359,11 @@ const ChatBar = ({ onPickDocument={pickDocument} onSheetStateChange={onAttachmentSheetStateChange} /> + )} diff --git a/components/chat-screen/ChatScreen.tsx b/components/chat-screen/ChatScreen.tsx index afbaad5f..ed7c188e 100644 --- a/components/chat-screen/ChatScreen.tsx +++ b/components/chat-screen/ChatScreen.tsx @@ -26,6 +26,7 @@ import { checkIfChatExists, setChatSettings, type Message, + type SourceDocument, } from '../../database/chatRepository'; import { Model } from '../../database/modelRepository'; import Messages from './Messages'; @@ -35,12 +36,9 @@ import ModelSelectSheet from '../bottomSheets/ModelSelectSheet'; import { Theme } from '../../styles/colors'; import { useSQLiteContext } from 'expo-sqlite'; import { useVectorStore } from '../../context/VectorStoreContext'; -import { OPSQLiteVectorStore } from '@react-native-rag/op-sqlite'; import { Attachment } from '../../hooks/useAttachment'; -import { - filterAndFormatContext, - formatFirstChunks, -} from '../../utils/contextUtils'; +import { buildMessageSources } from '../../utils/messageSources'; +import { useLegacyChatNotice } from '../../hooks/useLegacyChatNotice'; import { useSourceStore } from '../../store/sourceStore'; import useChatSettings from '../../hooks/useChatSettings'; import Toast from 'react-native-toast-message'; @@ -64,22 +62,6 @@ interface Props { revealFromTop?: boolean; } -const prepareContext = async ( - prompt: string, - enabledSources: number[], - vectorStore: OPSQLiteVectorStore -) => { - try { - const context = await vectorStore.query({ - queryText: prompt, - predicate: (r) => enabledSources.includes(r.metadata?.documentId), - }); - return filterAndFormatContext(context); - } catch { - return []; - } -}; - export default function ChatScreen({ chatId, chat, @@ -91,7 +73,6 @@ export default function ChatScreen({ revealFromTop = false, }: Props) { const inputRef = useRef<{ - clear: () => void; setInput: (text: string) => void; }>(null); const messagesRef = useRef(null); @@ -99,12 +80,11 @@ export default function ChatScreen({ const modelBottomSheetModalRef = useRef(null); const db = useSQLiteContext(); - const { vectorStore } = useVectorStore(); + const { vectorStore, embeddings } = useVectorStore(); const { isGenerating, sendChatMessage, loadModel, - setActiveChatId, model: loadedModel, } = useLLMStore(); const { getModelById } = useModelStore(); @@ -195,7 +175,8 @@ export default function ChatScreen({ return; let targetChatId = chatId!; - if (!(await checkIfChatExists(db, targetChatId))) { + const isNewChat = !(await checkIfChatExists(db, targetChatId)); + if (isNewChat) { const docName = attachments?.find((a) => a.type === 'document')?.name; const titleSource = userInput.trim() || docName || 'New chat'; const newChatTitle = @@ -204,10 +185,7 @@ export default function ChatScreen({ : titleSource; const newChatId = await addChat(newChatTitle, model!.id); if (!newChatId) return; - targetChatId = newChatId; - await setActiveChatId(targetChatId); - router.replace(`/chat/${targetChatId}`); } let persistedImagePath: string | undefined = imagePath; @@ -234,48 +212,10 @@ export default function ChatScreen({ // https://vercel.com/blog/how-we-built-the-v0-ios-app messagesRef.current?.onMessageSent(); - // Build context from attachments + persisted sources - const context: string[] = []; - - // Collect all source IDs: from attachments + already enabled on chat - const attachmentSourceIds = (attachments || []) - .filter((a) => a.type === 'document' && a.sourceId) - .map((a) => a.sourceId!); - const allSourceIds = [ - ...new Set([...enabledSources, ...attachmentSourceIds]), - ]; - - // RAG context from all sources (persisted + newly attached) - if (allSourceIds.length > 0 && vectorStore) { - const allSources = useSourceStore.getState().sources; - const activeSources = allSources.filter((s) => - allSourceIds.includes(s.id) - ); - const firstChunkContext = formatFirstChunks(activeSources); - context.push(...firstChunkContext); - - if (userInput.trim()) { - const ragContext = await prepareContext( - userInput, - allSourceIds, - vectorStore - ); - context.push(...ragContext); - } - } - - // Enable new sources for this chat (persists for future messages) - for (const sourceId of attachmentSourceIds) { - if (!enabledSources.includes(sourceId)) { - await enableSource(targetChatId, sourceId); - } - } - const settings: ChatSettings = { systemPrompt: chatSettings.systemPrompt, thinkingEnabled: chatSettings.thinkingEnabled, }; - const docAttachments = attachments?.filter((a) => a.type === 'document') || []; const docName = @@ -283,14 +223,64 @@ export default function ChatScreen({ .map((a) => a.name) .filter(Boolean) .join(', ') || undefined; - await sendChatMessage( + + // Deferred so retrieval runs only after the optimistic message is on screen. + const buildSources = async () => { + const allSources = useSourceStore.getState().sources; + const existingSourceIds = new Set(allSources.map((source) => source.id)); + const attachmentSourceIds = (attachments || []) + .filter((a) => a.type === 'document' && a.sourceId) + .map((a) => a.sourceId!) + .filter((sourceId) => { + const exists = existingSourceIds.has(sourceId); + if (!exists) { + console.warn('Skipping missing attachment source before send', { + chatId: targetChatId, + sourceId, + }); + } + return exists; + }); + + let context: string[] = []; + let sourceDocuments: SourceDocument[] = []; + let preferredSourceDocuments: SourceDocument[] = []; + if (vectorStore) { + ({ context, sourceDocuments, preferredSourceDocuments } = + await buildMessageSources({ + userInput, + attachmentSourceIds, + enabledSources, + sources: allSources, + vectorStore, + embeddings, + })); + } + + // Enable new sources for this chat (persists for future messages) + for (const sourceId of attachmentSourceIds) { + if (!enabledSources.includes(sourceId)) { + await enableSource(targetChatId, sourceId); + } + } + + return { context, sourceDocuments, preferredSourceDocuments }; + }; + + const generation = sendChatMessage( userInput, targetChatId, - context, + buildSources, settings, persistedImagePath, docName ); + + if (isNewChat) { + router.replace(`/chat/${targetChatId}`); + } + + await generation; }; const handleSelectModel = async (selectedModel: Model) => { @@ -366,6 +356,8 @@ export default function ChatScreen({ const hasMessages = isLoading || messageHistory.length > 0; const scrollBottomOffset = theme.insets.bottom; + useLegacyChatNotice(messageHistory); + const { height: windowHeight, width: windowWidth } = useWindowDimensions(); const gradientProgress = useSharedValue(isEmpty ? 1 : 0); useEffect(() => { diff --git a/components/chat-screen/MarkdownComponent.tsx b/components/chat-screen/MarkdownComponent.tsx index 18a185b4..9aae515e 100644 --- a/components/chat-screen/MarkdownComponent.tsx +++ b/components/chat-screen/MarkdownComponent.tsx @@ -13,10 +13,17 @@ interface Props { isUser?: boolean; isThinking?: boolean; streaming?: boolean; + onLinkPress?: (event: { url: string }) => void; } const MarkdownComponent = memo( - ({ text, isUser = false, isThinking = false, streaming = false }: Props) => { + ({ + text, + isUser = false, + isThinking = false, + streaming = false, + onLinkPress, + }: Props) => { const { theme } = useTheme(); const baseColor = theme.text.primary; const baseFontSize = isThinking ? fontSizes.sm : fontSizes.md; @@ -138,6 +145,7 @@ const MarkdownComponent = memo( markdown={text} markdownStyle={markdownStyle} selectable={true} + onLinkPress={onLinkPress} /> ); } @@ -147,6 +155,7 @@ const MarkdownComponent = memo( markdown={text} markdownStyle={markdownStyle} selectable={true} + onLinkPress={onLinkPress} /> ); } diff --git a/components/chat-screen/MessageItem.tsx b/components/chat-screen/MessageItem.tsx index 955d8622..1c4105c5 100644 --- a/components/chat-screen/MessageItem.tsx +++ b/components/chat-screen/MessageItem.tsx @@ -1,5 +1,12 @@ -import React, { memo, useMemo, useState } from 'react'; -import { View, StyleSheet, Text, Pressable, Image } from 'react-native'; +import React, { memo, useCallback, useMemo, useState } from 'react'; +import { + View, + StyleSheet, + Text, + Image, + Pressable, + Linking, +} from 'react-native'; import MarkdownComponent from './MarkdownComponent'; import ThinkingBlock from './ThinkingBlock'; import AnimatedChatLoading from './AnimatedChatLoading'; @@ -9,6 +16,7 @@ import { useLLMStore } from '../../store/llmStore'; import { Theme } from '../../styles/colors'; import ImageLightbox from './ImageLightbox'; import AttachmentIcon from '../../assets/icons/attachment.svg'; +import BookIcon from '../../assets/icons/book-open.svg'; import CopyIcon from '../../assets/icons/copy.svg'; import ForkIcon from '../../assets/icons/fork.svg'; import MessageActionButton from './MessageActionButton'; @@ -16,7 +24,9 @@ import { MESSAGE_ACTION_ROW_HEIGHT, SUPPORTS_USER_ACTION_MENU, } from '../../constants/chat-screen'; -import { Message } from '../../database/chatRepository'; +import { Message, type SourceDocument } from '../../database/chatRepository'; +import { stripCitations } from '../../utils/citations'; +import { sourceKey } from '../../utils/contextUtils'; interface MessageItemProps { message: Message; @@ -28,6 +38,9 @@ interface MessageItemProps { isLastMessage: boolean; imagePath?: string; documentName?: string; + sourceDocuments?: SourceDocument[]; + userQuestion?: string; + onShowSources?: (sources: SourceDocument[], userQuestion?: string) => void; showActions?: boolean; showForkAction?: boolean; onCopy?: (message: Message) => void; @@ -77,6 +90,9 @@ const MessageItem = memo( isLastMessage = false, imagePath, documentName, + sourceDocuments, + userQuestion, + onShowSources, showActions = false, showForkAction = false, onCopy, @@ -89,6 +105,43 @@ const MessageItem = memo( const [lightboxVisible, setLightboxVisible] = useState(false); const contentParts = parseThinkingContent(content); + const hasSources = !!sourceDocuments?.length; + const displayedSources = useMemo(() => { + if (!sourceDocuments?.length) return []; + + const seen = new Set(); + return sourceDocuments.filter((source) => { + const key = sourceKey(source.documentId, source.name); + if (seen.has(key)) return false; + seen.add(key); + return true; + }); + }, [sourceDocuments]); + + const handleLinkPress = useCallback(({ url }: { url: string }) => { + Linking.openURL(url).catch(() => {}); + }, []); + + const normalContent = useMemo( + () => + hasSources + ? stripCitations(contentParts.normalContent) + : contentParts.normalContent, + [contentParts.normalContent, hasSources] + ); + const normalAfterThink = useMemo( + () => + hasSources + ? stripCitations(contentParts.normalAfterThink ?? '') + : (contentParts.normalAfterThink ?? ''), + [contentParts.normalAfterThink, hasSources] + ); + + const canShowSourcesAction = + !!content.trim() && + displayedSources.length > 0 && + !(isLastMessage && (isGenerating || isProcessingPrompt)); + const actions = showActions ? ( )} {contentParts.hasThinking && @@ -195,8 +249,9 @@ const MessageItem = memo( {contentParts.normalAfterThink && contentParts.normalAfterThink.trim() && ( )} {tokensPerSecond !== undefined && tokensPerSecond !== 0 && ( @@ -205,6 +260,30 @@ const MessageItem = memo( {tokensPerSecond?.toFixed(2)} tok/s
)} + {canShowSourcesAction && ( + + [ + styles.sourcesButton, + pressed && styles.sourcesButtonPressed, + ]} + onPress={() => + onShowSources?.(displayedSources, userQuestion) + } + hitSlop={8} + accessibilityRole="button" + accessibilityLabel="Sources" + testID="source-action-button" + > + + Sources + + + )} {actions}
@@ -302,6 +381,29 @@ const createStyles = (theme: Theme) => fontFamily: fontFamily.regular, color: theme.text.defaultTertiary, }, + messageActions: { + flexDirection: 'row', + alignItems: 'center', + marginTop: 6, + }, + sourcesButton: { + flexDirection: 'row', + alignItems: 'center', + gap: 6, + alignSelf: 'flex-start', + paddingVertical: 4, + }, + sourcesButtonPressed: { + opacity: 0.6, + }, + sourcesButtonIcon: { + color: theme.text.primary, + }, + sourcesButtonLabel: { + fontSize: fontSizes.sm, + fontFamily: fontFamily.medium, + color: theme.text.primary, + }, actionRow: { flexDirection: 'row', alignItems: 'center', diff --git a/components/chat-screen/Messages.tsx b/components/chat-screen/Messages.tsx index 0d54e227..8235b463 100644 --- a/components/chat-screen/Messages.tsx +++ b/components/chat-screen/Messages.tsx @@ -34,7 +34,12 @@ import Reanimated, { } from 'react-native-reanimated'; import type { SharedValue } from 'react-native-reanimated'; import MessageItem from './MessageItem'; -import { Message, type ChatBranchMarker } from '../../database/chatRepository'; +import SourcesSheet, { type SourcesSheetHandle } from './SourcesSheet'; +import { + Message, + SourceDocument, + type ChatBranchMarker, +} from '../../database/chatRepository'; import { useTheme } from '../../context/ThemeContext'; import { Theme } from '../../styles/colors'; import { Feedback } from '../../utils/Feedback'; @@ -150,6 +155,13 @@ const Messages = ({ ); const lastScrollOffset = useRef(0); const lastLayoutHeight = useRef(0); + const sourcesSheetRef = useRef(null); + + const handleShowSources = useCallback( + (sources: SourceDocument[], question?: string) => + sourcesSheetRef.current?.present(sources, question), + [] + ); // v0-style initial scroll: hide the view until we've snapped to // the bottom, then fade in so the user never sees content flying by. @@ -552,6 +564,18 @@ const Messages = ({ [blankSpace, closeUserActionMenu, scheduleInitialScrollToEnd] ); + // Citations are highlighted against the preceding user question. + const questionForAssistantAt = useMemo(() => { + const questions: (string | undefined)[] = new Array(chatHistory.length); + let lastUserContent: string | undefined; + for (let i = 0; i < chatHistory.length; i += 1) { + const message = chatHistory[i]; + if (message.role === 'user') lastUserContent = message.content; + questions[i] = message.role === 'assistant' ? lastUserContent : undefined; + } + return questions; + }, [chatHistory]); + // Identify the last user and last assistant indices so we can wrap // those specific rows in onLayout measurement Views. let lastUserIndex = -1; @@ -587,6 +611,7 @@ const Messages = ({ > {chatHistory.map((message, index) => { const isLastMessage = index === chatHistory.length - 1; + const userQuestion = questionForAssistantAt[index]; // Streaming assistant placeholder has id: -1 until persisted; fall // back to role+index for that single in-flight row. const key = @@ -618,6 +643,9 @@ const Messages = ({ isLastMessage={isLastMessage} imagePath={message.imagePath} documentName={message.documentName} + sourceDocuments={message.sourceDocuments} + userQuestion={userQuestion} + onShowSources={handleShowSources} showActions={showActions} showForkAction={showForkAction} onCopy={handleCopyMessage} @@ -699,6 +727,8 @@ const Messages = ({ )} + + ); }; diff --git a/components/chat-screen/RowChevron.tsx b/components/chat-screen/RowChevron.tsx new file mode 100644 index 00000000..ec3cc7fd --- /dev/null +++ b/components/chat-screen/RowChevron.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import Animated, { + useAnimatedStyle, + useDerivedValue, + withTiming, +} from 'react-native-reanimated'; +import ChevronDownIcon from '../../assets/icons/chevron-down.svg'; +import { space } from '../../constants/design-system'; + +interface RowChevronProps { + expanded: boolean; + color: string; +} + +const RowChevron = ({ expanded, color }: RowChevronProps) => { + const progress = useDerivedValue(() => withTiming(expanded ? 1 : 0)); + + const animatedStyle = useAnimatedStyle(() => ({ + transform: [{ rotate: `${progress.value * 180}deg` }], + })); + + return ( + + + + ); +}; + +export default RowChevron; diff --git a/components/chat-screen/SourcesSheet.tsx b/components/chat-screen/SourcesSheet.tsx new file mode 100644 index 00000000..d566c0f8 --- /dev/null +++ b/components/chat-screen/SourcesSheet.tsx @@ -0,0 +1,384 @@ +import React, { + forwardRef, + memo, + useCallback, + useEffect, + useImperativeHandle, + useMemo, + useRef, + useState, +} from 'react'; +import { + View, + StyleSheet, + Text, + Pressable, + useWindowDimensions, + type LayoutChangeEvent, +} from 'react-native'; +import { + BottomSheetModal, + BottomSheetScrollView, + useBottomSheetSpringConfigs, + type BottomSheetBackdropProps, + type BottomSheetScrollViewMethods, +} from '@gorhom/bottom-sheet'; +import { useTheme } from '../../context/ThemeContext'; +import { Theme } from '../../styles/colors'; +import { + radius, + space, + stroke, + textStyles, +} from '../../constants/design-system'; +import { + EST_ROW_GAP, + EST_ROW_HEIGHT, + EST_SHEET_CHROME, + MAX_SHEET_HEIGHT_RATIO, + ROW_EXPAND_SCROLL_DELAY, + SHEET_HANDLE_HEIGHT, + SHEET_SPRING_CONFIG, +} from '../../constants/sources-sheet'; +import SheetBackdrop from '../bottomSheets/SheetBackdrop'; +import RowChevron from './RowChevron'; +import SourceIcon from '../../assets/icons/source.svg'; +import { type SourceDocument } from '../../database/chatRepository'; +import { getDocumentType, isSpreadsheetType } from '../../utils/documentType'; +import { + findCitedSpan, + buildCitationExcerpt, + queryNamesDocument, + type CitationExcerpt, +} from '../../utils/citationHighlight'; + +type SheetStyles = ReturnType; + +const renderPassage = (excerpt: CitationExcerpt, styles: SheetStyles) => { + const { text, span } = excerpt; + + if ( + !span || + span.start < 0 || + span.end > text.length || + span.start >= span.end + ) { + return text; + } + + return ( + <> + {text.slice(0, span.start)} + + {text.slice(span.start, span.end)} + + {text.slice(span.end)} + + ); +}; + +interface SourceRowProps { + source: SourceDocument; + isHighlighted: boolean; + isExpanded: boolean; + excerpt: CitationExcerpt | null; + chevronColor: string; + styles: SheetStyles; + onToggle: () => void; + onLayout: (event: LayoutChangeEvent) => void; +} + +const SourceRow = ({ + source, + isHighlighted, + isExpanded, + excerpt, + chevronColor, + styles, + onToggle, + onLayout, +}: SourceRowProps) => { + const docType = getDocumentType(source.name); + const hasPassage = !!source.passage && !isSpreadsheetType(docType); + + return ( + + + + + {docType ? {docType} : null} + + + {source.name} + + {hasPassage ? ( + + ) : null} + + {hasPassage && isExpanded && excerpt ? ( + + {renderPassage(excerpt, styles)} + + ) : null} + + ); +}; + +export interface SourcesSheetHandle { + present: ( + sources: SourceDocument[], + userQuestion?: string, + highlightIndex?: number | null + ) => void; +} + +interface SourcesSheetPayload { + sources: SourceDocument[]; + userQuestion?: string; +} + +const SourcesSheet = forwardRef((_props, ref) => { + const { theme } = useTheme(); + const styles = useMemo(() => createStyles(theme), [theme]); + const { height: screenHeight } = useWindowDimensions(); + + const sheetRef = useRef(null); + const isOpenRef = useRef(false); + const scrollRef = useRef(null); + const rowYRef = useRef>({}); + const listYRef = useRef(0); + const scrollTimerRef = useRef | null>(null); + const [highlightedIndex, setHighlightedIndex] = useState(null); + const [expandedIndex, setExpandedIndex] = useState(null); + const [contentHeight, setContentHeight] = useState(0); + const [payload, setPayload] = useState({ + sources: [], + }); + const { sources, userQuestion } = payload; + + const clearScrollTimer = useCallback(() => { + if (scrollTimerRef.current) clearTimeout(scrollTimerRef.current); + scrollTimerRef.current = null; + }, []); + + useEffect(() => clearScrollTimer, [clearScrollTimer]); + + useImperativeHandle( + ref, + () => ({ + present: ( + nextSources: SourceDocument[], + nextUserQuestion?: string, + highlightIndex: number | null = null + ) => { + if (isOpenRef.current) return; + isOpenRef.current = true; + setPayload({ sources: nextSources, userQuestion: nextUserQuestion }); + setHighlightedIndex(highlightIndex); + setExpandedIndex(highlightIndex); + sheetRef.current?.present(); + }, + }), + [] + ); + + const renderBackdrop = useCallback( + (props: BottomSheetBackdropProps) => , + [] + ); + + const toggleExpanded = useCallback( + (index: number) => { + const willExpand = expandedIndex !== index; + setExpandedIndex(willExpand ? index : null); + clearScrollTimer(); + if (!willExpand) return; + + scrollTimerRef.current = setTimeout(() => { + const y = listYRef.current + (rowYRef.current[index] ?? 0); + scrollRef.current?.scrollTo({ + y: Math.max(space.none, y - space.two), + animated: true, + }); + }, ROW_EXPAND_SCROLL_DELAY); + }, + [expandedIndex, clearScrollTimer] + ); + + const anyNamedSource = useMemo( + () => + sources.some((source) => + queryNamesDocument(userQuestion ?? '', source.name) + ), + [sources, userQuestion] + ); + + const expandedExcerpt = useMemo(() => { + if (expandedIndex === null) return null; + const source = sources[expandedIndex]; + const passage = source?.passage; + const suppressHighlight = + anyNamedSource && + !queryNamesDocument(userQuestion ?? '', source?.name ?? ''); + const span = suppressHighlight + ? null + : findCitedSpan(passage, userQuestion ?? ''); + return buildCitationExcerpt(passage, span); + }, [expandedIndex, sources, userQuestion, anyNamedSource]); + + const onContentSizeChange = useCallback((width: number, height: number) => { + const next = Math.round(height); + setContentHeight((prev) => (Math.abs(prev - next) > 1 ? next : prev)); + }, []); + + const snapPoints = useMemo(() => { + const seed = + EST_SHEET_CHROME + + theme.insets.bottom + + sources.length * EST_ROW_HEIGHT + + Math.max(0, sources.length - 1) * EST_ROW_GAP; + const target = contentHeight ? contentHeight + SHEET_HANDLE_HEIGHT : seed; + return [Math.min(target, screenHeight * MAX_SHEET_HEIGHT_RATIO)]; + }, [contentHeight, sources.length, theme.insets.bottom, screenHeight]); + + const animationConfigs = useBottomSheetSpringConfigs(SHEET_SPRING_CONFIG); + + return ( + { + isOpenRef.current = index >= 0; + }} + onDismiss={() => { + isOpenRef.current = false; + clearScrollTimer(); + setHighlightedIndex(null); + setExpandedIndex(null); + }} + > + + Sources + { + listYRef.current = e.nativeEvent.layout.y; + }} + > + {sources.map((source, index) => ( + toggleExpanded(index)} + onLayout={(e) => { + rowYRef.current[index] = e.nativeEvent.layout.y; + }} + /> + ))} + + + + ); +}); + +SourcesSheet.displayName = 'SourcesSheet'; + +export default memo(SourcesSheet); + +const createStyles = (theme: Theme) => + StyleSheet.create({ + sourcesSheetBackground: { + backgroundColor: theme.bg.softPrimary, + }, + sourcesSheetHandle: { + backgroundColor: theme.border.soft, + }, + sourcesSheet: { + paddingHorizontal: space.four, + paddingTop: space.two, + paddingBottom: theme.insets.bottom + space.eight, + gap: space.three, + backgroundColor: theme.bg.softPrimary, + }, + sourcesSheetTitle: { + ...textStyles.titleH3, + color: theme.text.primary, + }, + sourcesList: { + gap: space.two, + }, + sourceRow: { + flexDirection: 'column', + gap: space.two, + paddingVertical: space.three, + paddingHorizontal: space.three, + borderRadius: radius.twelve, + borderWidth: stroke.soft, + borderColor: theme.border.soft, + }, + sourceRowHighlighted: { + backgroundColor: theme.bg.softSecondary, + }, + sourceRowHeader: { + flexDirection: 'row', + alignItems: 'center', + gap: space.two, + }, + typeBadge: { + flexDirection: 'row', + alignItems: 'center', + gap: space.half, + paddingVertical: space.one, + paddingHorizontal: space.two, + borderWidth: stroke.soft, + borderColor: theme.border.soft, + borderRadius: radius.full, + }, + typeBadgeIcon: { + color: theme.text.defaultSecondary, + }, + typeBadgeText: { + ...textStyles.bodyTertiaryRegular, + color: theme.text.defaultSecondary, + }, + sourceRowName: { + flex: 1, + ...textStyles.bodySecondaryMedium, + color: theme.text.primary, + }, + sourcePassageText: { + ...textStyles.bodyTertiaryRegular, + color: theme.text.defaultTertiary, + }, + sourcePassageCited: { + ...textStyles.bodyTertiaryMedium, + color: theme.text.primary, + }, + }); diff --git a/constants/bottom-sheet.ts b/constants/bottom-sheet.ts new file mode 100644 index 00000000..34410904 --- /dev/null +++ b/constants/bottom-sheet.ts @@ -0,0 +1 @@ +export const BACKDROP_CLOSE_FADE = 120; diff --git a/constants/chat.ts b/constants/chat.ts new file mode 100644 index 00000000..b757db66 --- /dev/null +++ b/constants/chat.ts @@ -0,0 +1,3 @@ +export const LEGACY_CHAT_NOTICE_TOP_OFFSET = 64; + +export const ATTACHMENT_PROGRESS_TRACK_WIDTH = 52; diff --git a/constants/citations.ts b/constants/citations.ts new file mode 100644 index 00000000..a38f2bae --- /dev/null +++ b/constants/citations.ts @@ -0,0 +1,36 @@ +export const CITATION_PATTERN = /(^|[^\w$)\]])\[\d{1,3}\]/g; +export const CITATION_SENTENCE_PATTERN = /[^.!?\n]*[.!?]+|\n+|[^.!?\n]+$/g; +export const CITATION_STEM_PREFIX_LENGTH = 5; +export const CITATION_ALPHA_TERM_PATTERN = /^[a-ząćęłńóśźż]+$/; +export const CITATION_MIN_MATCH_SCORE = 2; +export const CITATION_EXCERPT_MAX_CHARS = 300; +export const CITATION_DOCUMENT_NAME_TOKEN_PATTERN = /[^a-z0-9ąćęłńóśźż]+/i; +export const THINK_OPEN = ''; +export const THINK_CLOSE = ''; + +export const NEGATION_CUE_EN = + /\b(no|not|n['’]t|never|none|neither|nor|without|lacks?|lacking)\b/i; + +export const CLAUSE_SPLIT_PATTERN = + /[,;]|\b(?:but|however|although|though|whereas|while)\b/i; + +// Coverage nouns (does a source address the topic); a refusal negates one, a negative-fact answer does not. +const NO_ANSWER_META_EN = + 'information|info|mention|reference|data|details?|indication|records?'; + +// English "no information" refusal patterns; each negation is tied to a coverage noun. +export const NO_ANSWER_PATTERNS_EN: RegExp[] = [ + new RegExp(`\\bno (${NO_ANSWER_META_EN})\\b`, 'i'), + new RegExp(`\\bthere (is|are|'s) no (${NO_ANSWER_META_EN})\\b`, 'i'), + new RegExp( + `\\b(does|do|did|could|can) ?n['o]?t (contain|mention|include|provide|specify|cover|have|state|say)( any| any relevant)? (${NO_ANSWER_META_EN})\\b`, + 'i' + ), + new RegExp( + `\\b(${NO_ANSWER_META_EN}) (is|are|'s|was|were)?\\s?(not|n['o]?t) (mentioned|found|provided|specified|stated|included|present|available|given)\\b`, + 'i' + ), + /\bnot (found|available|mentioned|provided|present|specified|stated) in (the|these|any|this|your|provided|given)\b/i, + /\bi (do ?n['o]?t|cannot|can ?not|can['o]?t) (know|find|see|answer|tell|determine|locate)\b/i, + /\bunable to (find|answer|determine|locate|provide)\b/i, +]; diff --git a/constants/context-window.ts b/constants/context-window.ts new file mode 100644 index 00000000..305921d0 --- /dev/null +++ b/constants/context-window.ts @@ -0,0 +1,31 @@ +import { type Model } from '../database/modelRepository'; + +const CHARS_PER_TOKEN = 3; + +const DEFAULT_CONTEXT_WINDOW_TOKENS = 2048; + +const GENERATION_RESERVE_TOKENS = 512; + +// TODO: replace with each export's real window. Every family here is ≥32k +// upstream, but the ExecuTorch .pte bakes in a smaller one, so these are +// conservative floors. Unknown/imported models keep the 2048 default. +const CONTEXT_WINDOW_TOKENS_BY_FAMILY: Record = { + 'Qwen 3': 4096, + 'Qwen 2.5': 4096, + 'LLaMA 3.2': 4096, + 'LFM 2.5': 4096, + 'Bielik': 4096, + 'Gemma 4': 4096, +}; + +export const getContextWindowTokens = (model: Model): number => + (model.family ? CONTEXT_WINDOW_TOKENS_BY_FAMILY[model.family] : undefined) ?? + DEFAULT_CONTEXT_WINDOW_TOKENS; + +export const getPromptCharBudget = (model: Model): number => { + const promptTokenBudget = Math.max( + 0, + getContextWindowTokens(model) - GENERATION_RESERVE_TOKENS + ); + return promptTokenBudget * CHARS_PER_TOKEN; +}; diff --git a/constants/design-system.ts b/constants/design-system.ts new file mode 100644 index 00000000..97366e48 --- /dev/null +++ b/constants/design-system.ts @@ -0,0 +1,69 @@ +import { fontFamily } from '../styles/fontStyles'; + +export const space = { + none: 0, + half: 2, + one: 4, + two: 8, + twoHalf: 10, + three: 12, + four: 16, + five: 20, + six: 24, + eight: 32, + ten: 40, + twelve: 48, + fourteen: 56, + sixteen: 64, + twenty: 80, +} as const; + +export const radius = { + six: 6, + twelve: 12, + eighteen: 18, + full: 9999, +} as const; + +export const stroke = { + soft: 1, + strong: 2, +} as const; + +export const textStyles = { + titleH1: { + fontFamily: fontFamily.bold, + fontSize: 28, + lineHeight: 34, + }, + titleH2: { + fontFamily: fontFamily.medium, + fontSize: 22, + lineHeight: 28, + }, + titleH3: { + fontFamily: fontFamily.regular, + fontSize: 18, + lineHeight: 24, + }, + bodySecondaryMedium: { + fontFamily: fontFamily.medium, + fontSize: 14, + lineHeight: 20, + }, + bodyTertiaryRegular: { + fontFamily: fontFamily.regular, + fontSize: 12, + lineHeight: 16, + }, + bodyTertiaryMedium: { + fontFamily: fontFamily.medium, + fontSize: 12, + lineHeight: 16, + }, + bodyQuaternaryMedium: { + fontFamily: fontFamily.medium, + fontSize: 10, + lineHeight: 14, + }, +} as const; diff --git a/constants/documents.ts b/constants/documents.ts new file mode 100644 index 00000000..3ff6ae4d --- /dev/null +++ b/constants/documents.ts @@ -0,0 +1 @@ +export const SPREADSHEET_DOC_TYPES = new Set(['XLSX', 'XLS', 'XLSM', 'CSV']); diff --git a/constants/embedding-model.ts b/constants/embedding-model.ts new file mode 100644 index 00000000..d3cf646e --- /dev/null +++ b/constants/embedding-model.ts @@ -0,0 +1,32 @@ +export interface ModelSourceConfig { + decoderSource?: string; + encoderSource?: string; + tokenizerSource?: string; + modelSource?: string; +} + +const LFM_2_5_EMBEDDING_HF_BASE = + 'https://huggingface.co/software-mansion/react-native-executorch-lfm2.5-embedding-350m/resolve/main'; + +export const LFM_2_5_EMBEDDING_MODEL_FILE = + 'lfm_2_5_embedding_350m_xnnpack_8da4w.pte'; + +export const LFM_2_5_EMBEDDING_MODEL_ID = 'lfm-2-5'; + +export const LFM_2_5_EMBEDDING_SOURCES: ModelSourceConfig = { + modelSource: `${LFM_2_5_EMBEDDING_HF_BASE}/xnnpack/${LFM_2_5_EMBEDDING_MODEL_FILE}`, + tokenizerSource: `${LFM_2_5_EMBEDDING_HF_BASE}/tokenizer.json`, +}; + +const LFM_2_5_EMBEDDING_MODEL_BYTES = 430_609_152; +const LFM_2_5_EMBEDDING_TOKENIZER_BYTES = 4_733_275; + +export const LFM_2_5_EMBEDDING_DOWNLOAD_SIZE_BYTES = + LFM_2_5_EMBEDDING_MODEL_BYTES + LFM_2_5_EMBEDDING_TOKENIZER_BYTES; + +export const LFM_2_5_EMBEDDING_DIM = 1024; + +export const EMBEDDING_QUERY_PREFIX = 'query: '; +export const EMBEDDING_DOCUMENT_PREFIX = 'document: '; + +export const ACTIVE_EMBEDDING_MODEL_KEY = 'active_embedding_model_id'; diff --git a/constants/retrieval.ts b/constants/retrieval.ts new file mode 100644 index 00000000..03fa6a22 --- /dev/null +++ b/constants/retrieval.ts @@ -0,0 +1,35 @@ +/** Candidates pulled from the vector store before gating and re-ranking. */ +export const CANDIDATE_POOL = 20; + +/** Final chunks kept after gating and per-file capping. */ +export const MAX_RELEVANT_CHUNKS = 5; + +/** Bonus that floats a freshly-attached chunk past the gate to the pool's front. */ +export const ATTACHMENT_RELEVANCE_BONUS = 10; + +/** Max chunks kept from one document in the final selection, applied only when ≥2 documents qualify. */ +export const MAX_CHUNKS_PER_FILE = 3; + +/** Cosine floor to qualify on semantics alone. Measured on-device: LFM2.5 true paraphrase matches land ~0.28–0.54, so 0.40 admits clear matches while most unrelated passages stay below. */ +export const STRONG_SEMANTIC_THRESHOLD = 0.4; + +/** The single highest-similarity candidate always qualifies above this floor, so the best semantic match is never fully gated out (empty result) on a paraphrase that falls just short of the main threshold. */ +export const SEMANTIC_TOP_KEEP_FLOOR = 0.25; + +/** After generation, cite a non-attachment document only if its answer↔passage term overlap is at least this fraction of the strongest cited document's — attributes the reply to the source(s) it was actually based on. */ +export const ANSWER_CITATION_OVERLAP_RATIO = 0.5; + +export const TEXT_SPLITTER_CHUNK_SIZE = 1000; +export const TEXT_SPLITTER_CHUNK_OVERLAP = 200; + +/** Safety backstop on chunks embedded per source; high enough that large real documents (~1.6 MB of text) index in full, low enough to stop a pathological multi-MB file from embedding for tens of minutes. */ +export const MAX_SOURCE_CHUNKS = 2000; + +/** Hard cap on extracted text fed to the splitter, so a pathological multi-MB document can't blow up the chunk array (and memory) before the chunk backstop applies. Derived from the chunk backstop and chunk size. */ +export const MAX_SOURCE_TEXT_CHARS = + MAX_SOURCE_CHUNKS * TEXT_SPLITTER_CHUNK_SIZE; + +/** Min matched run to treat as overlap when stitching passages — below the splitter overlap, above coincidental repetition. */ +export const MIN_STITCH_OVERLAP = 24; + +export const SOURCE_HEADER = /--- [^:]+: (.+?) ---/g; diff --git a/constants/sources-sheet.ts b/constants/sources-sheet.ts new file mode 100644 index 00000000..d76fb86f --- /dev/null +++ b/constants/sources-sheet.ts @@ -0,0 +1,29 @@ +import { space, textStyles } from './design-system'; + +export const MAX_SHEET_HEIGHT_RATIO = 0.9; +export const SHEET_HANDLE_HEIGHT = 24; + +const SHEET_CONTENT_PADDING_TOP = space.two; +const SHEET_CONTENT_PADDING_BOTTOM = space.eight; +const SHEET_TITLE_HEIGHT = textStyles.titleH3.lineHeight; +const SHEET_TITLE_GAP = space.three; + +export const EST_ROW_HEIGHT = 50; +export const EST_ROW_GAP = space.two; +export const EST_SHEET_CHROME = + SHEET_HANDLE_HEIGHT + + SHEET_CONTENT_PADDING_TOP + + SHEET_TITLE_HEIGHT + + SHEET_TITLE_GAP + + SHEET_CONTENT_PADDING_BOTTOM; + +export const ROW_EXPAND_SCROLL_DELAY = 260; + +export const SHEET_SPRING_CONFIG = { + damping: 50, + stiffness: 300, + mass: 1, + overshootClamping: true, + restDisplacementThreshold: 0.5, + restSpeedThreshold: 2, +}; diff --git a/context/VectorStoreContext.tsx b/context/VectorStoreContext.tsx index c9453913..9a1d3d0b 100644 --- a/context/VectorStoreContext.tsx +++ b/context/VectorStoreContext.tsx @@ -1,23 +1,26 @@ import React, { createContext, useState, useEffect, useContext } from 'react'; +import { useSQLiteContext } from 'expo-sqlite'; import { OPSQLiteVectorStore } from '@react-native-rag/op-sqlite'; -import { ExecuTorchEmbeddings } from '@react-native-rag/executorch'; -import { getModelConfig, ALL_MINI_LM_MODEL } from '../utils/modelConfig'; - -const getAllMiniLMAssets = async () => { - const config = await getModelConfig(ALL_MINI_LM_MODEL); - - return { - modelSource: config.modelSource!, - tokenizerSource: config.tokenizerSource!, - }; -}; +import { + LFM_2_5_EMBEDDING_DIM, + LFM_2_5_EMBEDDING_MODEL_ID, + LFM_2_5_EMBEDDING_SOURCES, +} from '../constants/embedding-model'; +import { migrateEmbeddingModelIfNeeded } from '../utils/embeddingModelMigration'; +import { LFMEmbeddings } from '../utils/lfmEmbeddings'; +import { isEmbeddingModelDownloaded } from '../utils/embeddingModel'; +import { useEmbeddingModelStore } from '../store/embeddingModelStore'; const VectorStoreContext = createContext<{ vectorStore: OPSQLiteVectorStore | null; + embeddings: LFMEmbeddings | null; }>({ vectorStore: null, + embeddings: null, }); +let vectorStoreInitChain: Promise = Promise.resolve(); + export const VectorStoreProvider = ({ children, }: { @@ -26,35 +29,85 @@ export const VectorStoreProvider = ({ const [vectorStore, setVectorStore] = useState( null ); + const [embeddings, setEmbeddings] = useState(null); + const db = useSQLiteContext(); useEffect(() => { + let cancelled = false; + let localStore: OPSQLiteVectorStore | null = null; + let unloaded = false; + + const unloadStore = async () => { + if (unloaded || !localStore) return; + unloaded = true; + try { + await localStore.unload(); + } catch (error) { + console.error('Failed to unload vector store:', error); + } + }; + const initialize = async () => { + if (cancelled) return; try { - const assets = await getAllMiniLMAssets(); - const embeddings = new ExecuTorchEmbeddings(assets); + const lfmEmbeddings = new LFMEmbeddings({ + modelSource: LFM_2_5_EMBEDDING_SOURCES.modelSource!, + tokenizerSource: LFM_2_5_EMBEDDING_SOURCES.tokenizerSource!, + onDownloadProgress: (progress) => + useEmbeddingModelStore.getState().setProgress(progress), + }); const store = new OPSQLiteVectorStore({ name: 'private-mind-rag', - embeddings, + embeddings: lfmEmbeddings, }); + localStore = store; + + if (cancelled) return; + await migrateEmbeddingModelIfNeeded( + store, + db, + LFM_2_5_EMBEDDING_MODEL_ID, + LFM_2_5_EMBEDDING_DIM + ); + + if (cancelled) return; + const downloaded = await isEmbeddingModelDownloaded(); + + if (cancelled) return; + if (downloaded) { + await store.load(); + } - await store.load(); + if (cancelled) return; setVectorStore(store); + setEmbeddings(lfmEmbeddings); + if (downloaded) { + useEmbeddingModelStore.getState().markReady(); + } else { + useEmbeddingModelStore.getState().setStatus('not_downloaded'); + } } catch (error) { - console.error('Failed to initialize vector store:', error); + if (!cancelled) { + console.error('Failed to initialize vector store:', error); + } } }; - initialize(); + vectorStoreInitChain = vectorStoreInitChain.then(initialize); return () => { + cancelled = true; setVectorStore(null); + setEmbeddings(null); + useEmbeddingModelStore.getState().setStatus('unknown'); + vectorStoreInitChain = vectorStoreInitChain.then(unloadStore); }; - }, []); + }, [db]); return ( - + {children} ); diff --git a/database/chatRepository.ts b/database/chatRepository.ts index 300b703d..14c5a3cd 100644 --- a/database/chatRepository.ts +++ b/database/chatRepository.ts @@ -42,11 +42,53 @@ export type Message = { content: string; imagePath?: string; documentName?: string; + sourceDocuments?: SourceDocument[]; tokensPerSecond?: number; timeToFirstToken?: number; timestamp: number; }; +export type SourceDocument = { + documentId?: number; + name: string; + passage?: string; + similarity?: number; +}; + +type RawMessage = Omit & { + sourceDocuments?: string | null; +}; + +const parseSourceDocuments = ( + sourceDocuments?: string | null +): SourceDocument[] | undefined => { + if (!sourceDocuments) return undefined; + + try { + const parsed = JSON.parse(sourceDocuments); + if (!Array.isArray(parsed)) return undefined; + + return parsed + .filter( + (source): source is SourceDocument => + !!source && + typeof source === 'object' && + typeof source.name === 'string' + ) + .map((source) => ({ + documentId: + typeof source.documentId === 'number' ? source.documentId : undefined, + name: source.name, + passage: + typeof source.passage === 'string' ? source.passage : undefined, + similarity: + typeof source.similarity === 'number' ? source.similarity : undefined, + })); + } catch { + return undefined; + } +}; + export type ChatBranchMarker = { id: number; chatId: number; @@ -107,10 +149,15 @@ export const getChatMessages = async ( db: SQLiteDatabase, chatId: number ): Promise => { - return db.getAllAsync( + const messages = await db.getAllAsync( `SELECT * FROM messages WHERE chatId = ? ORDER BY id ASC`, [chatId] ); + + return messages.map((message) => ({ + ...message, + sourceDocuments: parseSourceDocuments(message.sourceDocuments), + })); }; export const persistMessage = async ( @@ -118,7 +165,7 @@ export const persistMessage = async ( message: Omit ): Promise => { const result = await db.runAsync( - `INSERT INTO messages (chatId, role, content, modelName, tokensPerSecond, timeToFirstToken, imagePath, documentName) VALUES (?, ?, ?, ?, ?, ?, ?, ?);`, + `INSERT INTO messages (chatId, role, content, modelName, tokensPerSecond, timeToFirstToken, imagePath, documentName, sourceDocuments) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);`, [ message.chatId, message.role, @@ -128,6 +175,9 @@ export const persistMessage = async ( message.timeToFirstToken ?? 0, message.imagePath || null, message.documentName || null, + message.sourceDocuments?.length + ? JSON.stringify(message.sourceDocuments) + : null, ] ); @@ -144,8 +194,8 @@ export const persistMessage = async ( }; // SQLite's default SQLITE_MAX_VARIABLE_NUMBER is 999 on older builds. -// 9 params per row, so 100 rows per batch keeps us well under the limit. -const IMPORT_BATCH_SIZE = 100; +// 10 params per row, so 90 rows per batch keeps us well under the limit. +const IMPORT_BATCH_SIZE = 90; const MAX_PREVIEW_LENGTH = 72; export const importMessages = async ( @@ -158,7 +208,7 @@ export const importMessages = async ( for (let i = 0; i < messages.length; i += IMPORT_BATCH_SIZE) { const batch = messages.slice(i, i + IMPORT_BATCH_SIZE); const placeholders = batch - .map(() => '(?, ?, ?, ?, ?, ?, ?, ?, ?)') + .map(() => '(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)') .join(', '); const flattenedValues = batch.flatMap((msg) => [ chatId, @@ -170,9 +220,10 @@ export const importMessages = async ( msg.timeToFirstToken ?? 0, msg.imagePath ?? null, msg.documentName ?? null, + msg.sourceDocuments?.length ? JSON.stringify(msg.sourceDocuments) : null, ]); await db.runAsync( - `INSERT INTO messages (chatId, role, content, timestamp, modelName, tokensPerSecond, timeToFirstToken, imagePath, documentName) VALUES ${placeholders}`, + `INSERT INTO messages (chatId, role, content, timestamp, modelName, tokensPerSecond, timeToFirstToken, imagePath, documentName, sourceDocuments) VALUES ${placeholders}`, flattenedValues ); } diff --git a/database/db.ts b/database/db.ts index cb83a434..960c1c1e 100644 --- a/database/db.ts +++ b/database/db.ts @@ -6,8 +6,10 @@ import { useModelStore } from '../store/modelStore'; import { addModel } from './modelRepository'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { useSourceStore } from '../store/sourceStore'; +import { initSourceLinkingBoundary } from '../utils/sourceLinkingBoundary'; +import { migrateLegacyVectorStore } from './vectorStoreMigration'; -const runMigrations = async (db: SQLiteDatabase) => { +export const runMigrations = async (db: SQLiteDatabase) => { const modelsTableInfo = await db.getAllAsync<{ name: string }>( `PRAGMA table_info(models)` ); @@ -86,6 +88,15 @@ const runMigrations = async (db: SQLiteDatabase) => { ); } + const hasSourceDocuments = messagesTableInfo.some( + (col) => col.name === 'sourceDocuments' + ); + if (!hasSourceDocuments) { + await db.execAsync( + `ALTER TABLE messages ADD COLUMN sourceDocuments TEXT DEFAULT NULL` + ); + } + // Check and add thinkingEnabled to chatSettings const chatSettingsTableInfo = await db.getAllAsync<{ name: string }>( `PRAGMA table_info(chatSettings)` @@ -121,16 +132,7 @@ const runMigrations = async (db: SQLiteDatabase) => { ); } - // Migrate: if the vector store's vectors table lacks the `document` column, - // drop it so it gets recreated on next load. Clear sources since their - // backing vector data is gone and they can no longer be queried against. - try { - await db.execAsync(`SELECT document FROM vectors LIMIT 0`); - } catch { - await db.execAsync(`DROP TABLE IF EXISTS vectors`); - await db.runAsync(`DELETE FROM chatSources`); - await db.runAsync(`DELETE FROM sources`); - } + await migrateLegacyVectorStore(db); // One-time cleanup of orphan rows from before FK enforcement was enabled. await db.runAsync( @@ -146,6 +148,8 @@ const runMigrations = async (db: SQLiteDatabase) => { `DELETE FROM chatBranches WHERE chatId NOT IN (SELECT id FROM chats) OR afterMessageId NOT IN (SELECT id FROM messages)` ); + await initSourceLinkingBoundary(db); + await db.runAsync( `DELETE FROM models WHERE source = 'built-in' @@ -232,6 +236,7 @@ export const initDatabase = async (db: SQLiteDatabase) => { timeToFirstToken INTEGER DEFAULT 0, imagePath TEXT DEFAULT NULL, documentName TEXT DEFAULT NULL, + sourceDocuments TEXT DEFAULT NULL, FOREIGN KEY (chatId) REFERENCES chats (id) ON DELETE CASCADE ); `); diff --git a/database/sourcesRepository.ts b/database/sourcesRepository.ts index 22f4c732..58d04a55 100644 --- a/database/sourcesRepository.ts +++ b/database/sourcesRepository.ts @@ -71,11 +71,38 @@ export const activateSource = async ( db: SQLiteDatabase, chatId: number, sourceId: number -) => { +): Promise => { + const relationState = await db.getFirstAsync<{ + chatExists: number; + sourceExists: number; + alreadyEnabled: number; + }>( + `SELECT + EXISTS(SELECT 1 FROM chats WHERE id = ?) AS chatExists, + EXISTS(SELECT 1 FROM sources WHERE id = ?) AS sourceExists, + EXISTS( + SELECT 1 FROM chatSources WHERE chatId = ? AND sourceId = ? + ) AS alreadyEnabled`, + [chatId, sourceId, chatId, sourceId] + ); + + if (!relationState?.chatExists || !relationState?.sourceExists) { + console.warn('Skipping source activation because FK target is missing', { + chatId, + sourceId, + chatExists: !!relationState?.chatExists, + sourceExists: !!relationState?.sourceExists, + }); + return false; + } + + if (relationState.alreadyEnabled) return true; + await db.runAsync( `INSERT INTO chatSources (chatId, sourceId) VALUES (?, ?)`, [chatId, sourceId] ); + return true; }; export const deleteSourceFromChats = async ( diff --git a/database/vectorStoreMigration.ts b/database/vectorStoreMigration.ts new file mode 100644 index 00000000..76aea6dd --- /dev/null +++ b/database/vectorStoreMigration.ts @@ -0,0 +1,21 @@ +import type { SQLiteDatabase } from 'expo-sqlite'; + +// Drops the legacy pre-RAG `vectors` table and its orphaned sources. The +// sqlite_master guard is load-bearing: vectors moved to a separate op-sqlite db, +// so without it the catch below would wipe sources on every launch. +export const migrateLegacyVectorStore = async ( + db: SQLiteDatabase +): Promise => { + const hasLegacyVectorsTable = await db.getFirstAsync( + `SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'vectors'` + ); + if (!hasLegacyVectorsTable) return; + + try { + await db.execAsync(`SELECT document FROM vectors LIMIT 0`); + } catch { + await db.execAsync(`DROP TABLE IF EXISTS vectors`); + await db.runAsync(`DELETE FROM chatSources`); + await db.runAsync(`DELETE FROM sources`); + } +}; diff --git a/hooks/useAttachment.ts b/hooks/useAttachment.ts index 63f94c28..54de75ab 100644 --- a/hooks/useAttachment.ts +++ b/hooks/useAttachment.ts @@ -1,4 +1,4 @@ -import { useState, useRef, useCallback } from 'react'; +import { useState, useRef, useCallback, useEffect } from 'react'; import { launchImageLibrary, launchCamera } from 'react-native-image-picker'; import * as DocumentPicker from 'expo-document-picker'; import { BottomSheetModal } from '@gorhom/bottom-sheet'; @@ -6,6 +6,8 @@ import { Platform, PermissionsAndroid } from 'react-native'; import Toast from 'react-native-toast-message'; import { useSourceStore } from '../store/sourceStore'; import { useVectorStore } from '../context/VectorStoreContext'; +import { useEmbeddingModelStore } from '../store/embeddingModelStore'; +import { documentErrorMessage } from '../utils/documentErrorMessage'; export interface Attachment { id: string; @@ -14,6 +16,11 @@ export interface Attachment { name?: string; status: 'loading' | 'ready'; sourceId?: number; + progress?: number; +} + +interface ClearAllOptions { + cleanupSources?: boolean; } interface ClearAllOptions { @@ -57,21 +64,36 @@ export const useAttachment = () => { const [attachments, setAttachments] = useState([]); const attachmentsRef = useRef([]); attachmentsRef.current = attachments; + const attachmentRequestRef = useRef(0); + const currentDocumentAttachmentIdRef = useRef(null); + const documentAbortRef = useRef(null); const sheetRef = useRef(null); - const { vectorStore } = useVectorStore(); + const embeddingDownloadSheetRef = useRef(null); + const embeddingDownloadSheetOpenRef = useRef(false); + const { vectorStore, embeddings } = useVectorStore(); + const vectorStoreRef = useRef(vectorStore); + vectorStoreRef.current = vectorStore; - const replaceWithImage = useCallback( - (uri: string) => { - const hadSource = attachmentsRef.current.some((a) => a.sourceId); - setAttachments([ - { id: `img-${Date.now()}`, type: 'image', uri, status: 'ready' }, - ]); - if (hadSource && vectorStore) { - useSourceStore.getState().cleanupOrphanedSources(vectorStore); + const sweepAbandonedSources = useCallback(() => { + const store = vectorStoreRef.current; + if (store) useSourceStore.getState().cleanupOrphanedSources(store); + }, []); + + useEffect(() => { + return () => { + embeddingDownloadSheetOpenRef.current = false; + if (attachmentsRef.current.some((a) => a.sourceId)) { + sweepAbandonedSources(); } - }, - [vectorStore] - ); + }; + }, [sweepAbandonedSources]); + + const replaceWithImage = useCallback((uri: string) => { + currentDocumentAttachmentIdRef.current = null; + setAttachments([ + { id: `img-${Date.now()}`, type: 'image', uri, status: 'ready' }, + ]); + }, []); const pickFromLibrary = useCallback(async () => { const granted = await requestAndroidGalleryPermission(); @@ -101,23 +123,45 @@ export const useAttachment = () => { } }, [replaceWithImage]); - const pickDocument = useCallback(async () => { + const runDocumentPicker = useCallback(async () => { const pickedFileResult = await DocumentPicker.getDocumentAsync({ - type: ['application/pdf', 'text/plain', 'text/markdown', 'text/html'], + type: [ + 'application/pdf', + 'text/plain', + 'text/markdown', + 'text/x-markdown', + 'text/html', + 'text/csv', + 'text/comma-separated-values', + 'application/csv', + ], copyToCacheDirectory: true, }); if (pickedFileResult.canceled || !pickedFileResult.assets[0]) return; const asset = pickedFileResult.assets[0]; - const fileType = asset.uri.split('.').pop() || ''; + const extFromName = asset.name?.includes('.') + ? asset.name.split('.').pop() + : undefined; + const fileType = ( + extFromName || + asset.uri.split('.').pop() || + '' + ).toLowerCase(); const fileName = asset.name?.split('.')[0] || asset.uri.split('/').pop()?.split('.')[0] || 'Unnamed'; const attachmentId = `doc-${Date.now()}`; + const requestId = attachmentRequestRef.current + 1; + attachmentRequestRef.current = requestId; + currentDocumentAttachmentIdRef.current = attachmentId; + + documentAbortRef.current?.abort(); + const abortController = new AbortController(); + documentAbortRef.current = abortController; - const hadSource = attachmentsRef.current.some((a) => a.sourceId); setAttachments([ { id: attachmentId, @@ -127,20 +171,52 @@ export const useAttachment = () => { status: 'loading', }, ]); - if (hadSource && vectorStore) { - useSourceStore.getState().cleanupOrphanedSources(vectorStore); - } try { const newSource = { - name: fileName, + name: asset.name || fileName, type: fileType, size: asset.size || null, }; const { addSource } = useSourceStore.getState(); - const result = await addSource(newSource, asset.uri, vectorStore!); + let lastPercent = -1; + const handleProgress = (progress: number) => { + const percent = Math.round(progress * 100); + if (percent === lastPercent) return; + lastPercent = percent; + if ( + attachmentRequestRef.current !== requestId || + currentDocumentAttachmentIdRef.current !== attachmentId + ) { + return; + } + setAttachments((prev) => + prev.map((a) => (a.id === attachmentId ? { ...a, progress } : a)) + ); + }; + const result = await addSource( + newSource, + asset.uri, + vectorStore!, + embeddings, + handleProgress, + abortController.signal + ); + if (result.cancelled) return; + const isCurrentDocumentRequest = + attachmentRequestRef.current === requestId && + currentDocumentAttachmentIdRef.current === attachmentId; if (result.success) { + if (!isCurrentDocumentRequest) { + console.warn('Ignoring stale document processing result', { + attachmentId, + sourceId: result.sourceId, + name: newSource.name, + }); + return; + } + setAttachments((prev) => prev.map((a) => a.id === attachmentId @@ -148,47 +224,94 @@ export const useAttachment = () => { : a ) ); + if (result.truncated) { + Toast.show({ + type: 'defaultToast', + text1: + 'This document is large — only the first part was indexed for search.', + }); + } } else { + if (!isCurrentDocumentRequest) return; + setAttachments((prev) => prev.filter((a) => a.id !== attachmentId)); Toast.show({ type: 'defaultToast', - text1: result.isEmpty - ? 'Document appears to be empty.' - : 'Failed to process document.', + text1: documentErrorMessage(result), }); } - } catch { + } catch (error) { + console.error('Document attachment processing threw', { + attachmentId, + requestId, + name: asset.name || fileName, + error, + }); + if (attachmentRequestRef.current !== requestId) return; + setAttachments((prev) => prev.filter((a) => a.id !== attachmentId)); Toast.show({ type: 'defaultToast', text1: 'Error reading document.', }); } - }, [vectorStore]); + }, [vectorStore, embeddings]); + + const markDownloadSheetClosed = useCallback(() => { + embeddingDownloadSheetOpenRef.current = false; + }, []); + + const pickDocument = useCallback(async () => { + if (useEmbeddingModelStore.getState().status === 'ready') { + return runDocumentPicker(); + } + embeddingDownloadSheetOpenRef.current = true; + embeddingDownloadSheetRef.current?.present(); + }, [runDocumentPicker]); + + const downloadModelAndContinue = useCallback(async () => { + if (!vectorStore) return; + const ready = await useEmbeddingModelStore + .getState() + .ensureReady(vectorStore); + if (!ready) { + Toast.show({ + type: 'defaultToast', + text1: 'Failed to download the document model.', + }); + return; + } + if (embeddingDownloadSheetOpenRef.current) { + embeddingDownloadSheetRef.current?.dismiss(); + await runDocumentPicker(); + } + }, [vectorStore, runDocumentPicker]); const removeAttachment = useCallback( (id: string) => { - const hadSourceId = attachmentsRef.current.some( - (a) => a.id === id && a.sourceId - ); - setAttachments((prev) => prev.filter((a) => a.id !== id)); - if (hadSourceId && vectorStore) { - useSourceStore.getState().cleanupOrphanedSources(vectorStore); + if (currentDocumentAttachmentIdRef.current === id) { + currentDocumentAttachmentIdRef.current = null; + documentAbortRef.current?.abort(); } + const removed = attachmentsRef.current.find((a) => a.id === id); + setAttachments((prev) => prev.filter((a) => a.id !== id)); + if (removed?.sourceId) sweepAbandonedSources(); }, - [vectorStore] + [sweepAbandonedSources] ); const clearAll = useCallback( (options: ClearAllOptions = {}) => { - const cleanupSources = options.cleanupSources ?? true; + const cleanupSources = options.cleanupSources ?? false; const hadDocuments = attachmentsRef.current.some((a) => a.sourceId); + currentDocumentAttachmentIdRef.current = null; + documentAbortRef.current?.abort(); setAttachments([]); - if (cleanupSources && hadDocuments && vectorStore) { - useSourceStore.getState().cleanupOrphanedSources(vectorStore); + if (cleanupSources && hadDocuments) { + sweepAbandonedSources(); } }, - [vectorStore] + [sweepAbandonedSources] ); const openSheet = useCallback(() => { @@ -217,9 +340,12 @@ export const useAttachment = () => { return { attachments, sheetRef, + embeddingDownloadSheetRef, pickFromLibrary, pickFromCamera, pickDocument, + downloadModelAndContinue, + markDownloadSheetClosed, removeAttachment, clearAll, openSheet, diff --git a/hooks/useLegacyChatNotice.ts b/hooks/useLegacyChatNotice.ts new file mode 100644 index 00000000..36a7d9b8 --- /dev/null +++ b/hooks/useLegacyChatNotice.ts @@ -0,0 +1,29 @@ +import { useEffect, useMemo } from 'react'; +import Toast from 'react-native-toast-message'; +import { useDrawerStatus } from '@react-navigation/drawer'; +import { useTheme } from '../context/ThemeContext'; +import { Message } from '../database/chatRepository'; +import { diagnoseLegacyChat } from '../utils/legacyChat'; +import { LEGACY_CHAT_NOTICE_TOP_OFFSET } from '../constants/chat'; + +export const useLegacyChatNotice = (messageHistory: Message[]): void => { + const { theme } = useTheme(); + const isDrawerOpen = useDrawerStatus() === 'open'; + const isLegacy = useMemo( + () => diagnoseLegacyChat(messageHistory).isLegacy, + [messageHistory] + ); + const show = isLegacy && !isDrawerOpen; + + useEffect(() => { + if (!show) return; + Toast.show({ + type: 'defaultToast', + text1: + 'Note: this conversation predates document linking, so its attached document is no longer available here. Attach it again in a new chat to use it as a source.', + autoHide: false, + topOffset: theme.insets.top + LEGACY_CHAT_NOTICE_TOP_OFFSET, + }); + return () => Toast.hide(); + }, [show, theme.insets.top]); +}; diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 69945c0a..c02a7a0e 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,9 +1,9 @@ PODS: - - EXApplication (55.0.14): + - EXApplication (55.0.16): - ExpoModulesCore - - EXConstants (55.0.15): + - EXConstants (55.0.16): - ExpoModulesCore - - Expo (55.0.20): + - Expo (55.0.27): - ExpoModulesCore - hermes-engine - RCTRequired @@ -28,40 +28,40 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga - - ExpoAsset (55.0.16): + - ExpoAsset (55.0.17): - ExpoModulesCore - - ExpoClipboard (55.0.13): + - ExpoClipboard (55.0.14): - ExpoModulesCore - - ExpoDevice (55.0.15): + - ExpoDevice (55.0.18): - ExpoModulesCore - - ExpoDocumentPicker (55.0.13): + - ExpoDocumentPicker (55.0.14): - ExpoModulesCore - - ExpoDomWebView (55.0.5): + - ExpoDomWebView (55.0.6): - ExpoModulesCore - - ExpoFileSystem (55.0.17): + - ExpoFileSystem (55.0.23): - ExpoModulesCore - - ExpoFont (55.0.6): + - ExpoFont (55.0.8): - ExpoModulesCore - - ExpoGlassEffect (55.0.10): + - ExpoGlassEffect (55.0.11): - ExpoModulesCore - - ExpoImage (55.0.9): + - ExpoImage (55.0.11): - ExpoModulesCore - libavif/libdav1d - SDWebImage (~> 5.21.0) - SDWebImageAVIFCoder (~> 0.11.0) - SDWebImageSVGCoder (~> 1.7.0) - SDWebImageWebPCoder (~> 0.14.6) - - ExpoKeepAwake (55.0.7): + - ExpoKeepAwake (55.0.8): - ExpoModulesCore - - ExpoLinearGradient (55.0.13): + - ExpoLinearGradient (55.0.15): - ExpoModulesCore - - ExpoLinking (55.0.14): + - ExpoLinking (55.0.16): - ExpoModulesCore - - ExpoLocalization (55.0.13): + - ExpoLocalization (55.0.16): - ExpoModulesCore - - ExpoLogBox (55.0.11): + - ExpoLogBox (55.0.12): - React-Core - - ExpoModulesCore (55.0.24): + - ExpoModulesCore (55.0.25): - ExpoModulesJSI - hermes-engine - RCTRequired @@ -86,25 +86,25 @@ PODS: - ReactNativeDependencies - RNWorklets - Yoga - - ExpoModulesJSI (55.0.24): + - ExpoModulesJSI (55.0.25): - hermes-engine - React-Core - React-runtimescheduler - ReactCommon - ExpoPasteInput (1.0.0): - ExpoModulesCore - - ExpoRouter (55.0.13): + - ExpoRouter (55.0.16): - ExpoModulesCore - RNScreens - - ExpoSharing (55.0.18): + - ExpoSharing (55.0.21): - ExpoModulesCore - - ExpoSplashScreen (55.0.19): + - ExpoSplashScreen (55.0.22): - ExpoModulesCore - - ExpoSQLite (55.0.15): + - ExpoSQLite (55.0.17): - ExpoModulesCore - - ExpoStoreReview (55.0.13): + - ExpoStoreReview (55.0.15): - ExpoModulesCore - - ExpoSymbols (55.0.7): + - ExpoSymbols (55.0.9): - ExpoModulesCore - FBLazyVector (0.83.4) - hermes-engine (0.14.1): @@ -128,7 +128,7 @@ PODS: - libwebp/sharpyuv (1.5.0) - libwebp/webp (1.5.0): - libwebp/sharpyuv - - op-sqlite (15.2.12): + - op-sqlite (15.2.14): - hermes-engine - RCTRequired - RCTTypeSafety @@ -173,8 +173,9 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga - - Pulsar (1.4.0): + - Pulsar (1.6.1): - hermes-engine + - Pulsar-haptics (= 1.1.4) - RCTRequired - RCTTypeSafety - React-Core @@ -195,6 +196,7 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga + - Pulsar-haptics (1.1.4) - RCTDeprecation (0.83.4) - RCTRequired (0.83.4) - RCTSwiftUI (0.83.4) @@ -2176,7 +2178,7 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga - - ReactNativeFs (2.38.2): + - ReactNativeFs (2.39.1): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2735,6 +2737,7 @@ SPEC REPOS: - libdav1d - libwebp - opencv-rne + - Pulsar-haptics - SDWebImage - SDWebImageAVIFCoder - SDWebImageSVGCoder @@ -2978,42 +2981,43 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/yoga" SPEC CHECKSUMS: - EXApplication: ab5a9ca485adce8be81309a5172d1565e9d7467f - EXConstants: 5caf448ff399f60be9d1053e77aed32e39411900 - Expo: 1bf84a82787de952fb2fb1270214e3f15d166f05 - ExpoAsset: f56ed740b6a127da2bfd90832cf688ae309d4410 - ExpoClipboard: 5d1b0cd2686406f21e616f2d9b3431259dee2e6a - ExpoDevice: 3a4a361d685333e4139da5d56bb2740c86132beb - ExpoDocumentPicker: be59b82799ae30811e3f37a7521d6622baa63a19 - ExpoDomWebView: 2b2fbd9a07de8790569257cbf9dfdaa31cf95c70 - ExpoFileSystem: a0e4a5a09c9c4f84338289b273a172609452dec9 - ExpoFont: cdd7a1d574a376fa003c713eff49e0a4df8672c7 - ExpoGlassEffect: 72bcb9dc634262c59897ff7c53c16e2ff03990d0 - ExpoImage: bd9e9c5bc4eff7a6883339be305f8b0bb5a0cf1c - ExpoKeepAwake: 861c66deb0f9f28f87882610195f26c4b88fba6f - ExpoLinearGradient: c654e92d726a6d64c588a0988bb22bea331d5e79 - ExpoLinking: f9e01f5182f2653ea9d90f18fb47f1913e2f99ef - ExpoLocalization: c5cd7fa65c797d3a2f1adbd1fd4c601c524fd677 - ExpoLogBox: 617da77cbc083627692b2ea050a3666808aef2cd - ExpoModulesCore: 088468ce2605ce08b704105945c2232e73a7b9ee - ExpoModulesJSI: 9b60671a31c30bc8e24b25632e7cb475a84b9016 + EXApplication: a1413100c36551bef1d775fc6c64147286962bf8 + EXConstants: dadbeba983acc30f855a919658a2b34fbd86615d + Expo: b0f96de93d2e9ab7c6eabcf88597b13f6fe3f67e + ExpoAsset: 7721c5c4ae2a7c3c8147a046727ac6c020b05648 + ExpoClipboard: db4d0d7371d22999ed506dad5f0115d89fd25998 + ExpoDevice: 2d3050f79f8e0b6d6da7933d4fc5f54908731b34 + ExpoDocumentPicker: f7bb7d99ad78f5d3c526930275b5cd34446276bc + ExpoDomWebView: b2e9d601f9cb77d3c97da73234e175b64be91928 + ExpoFileSystem: 49ab894dee0818cb8b83d489fc51eafade73efb9 + ExpoFont: a7e34a75bdfb703fa07a0e87807f836236608c76 + ExpoGlassEffect: 2dedac2fe817f9b02d0dad29ce0aef7afebc5cd4 + ExpoImage: 5854bd51edd7c0a775755473598f9daeb3fde4e3 + ExpoKeepAwake: d0eb7a0719500d2a43fbf29b6f79e84d70c75ebf + ExpoLinearGradient: 07d8c721fd9ea91a7aa666753538f80dfda95bf3 + ExpoLinking: 221ab136d976b632e04ff4023660cc9c24233d07 + ExpoLocalization: 880f2337e7dd372f76ef66336a875f24d0330f80 + ExpoLogBox: a678d36477ab9544fe63e0b5328a0716539b6774 + ExpoModulesCore: 5e1ad569ce7cf8d35c0737585f34cc299fc3f0ca + ExpoModulesJSI: 964334271fc77832736f66ee1785761cdc778dbe ExpoPasteInput: e8eba5274ad513ba3e02ceb71ad6524bf2e46ce1 - ExpoRouter: e2c56f80102b153c5f8c887b38094ef936c266a2 - ExpoSharing: 5acc9d78894386d31b862b12ed1cebe7dce74e68 - ExpoSplashScreen: 43517d54a0f90d381c80deda859aa6087e80884b - ExpoSQLite: c6e966c6096f27c2ee33d608e7c40025a3cbc930 - ExpoStoreReview: dab6e25f0641784bd6207f729f893423afbb6574 - ExpoSymbols: 8b63e859ba013df1f2fc666f535fddb3d5270569 + ExpoRouter: e0e2aa4b03843e7abfa3e63e0cdd6c642d74780f + ExpoSharing: 06ec115626db3732d31ca72f580e180e75948b2c + ExpoSplashScreen: 693efd4323f4561d4c87c79050be7a0a4b9bfc51 + ExpoSQLite: 3938a01789399eddb903ea610d0d5d5f558e7909 + ExpoStoreReview: 416b2de497481b0aeb28ef95d3f93bc9ae4bca6a + ExpoSymbols: 896159288ad708e1a32dc12c92fe5f90bff01126 FBLazyVector: 061f518bbd81677ed8a8317e2ae60b8779495808 hermes-engine: b73eefca929bd717e549c62316ddf5e1785d6499 iosMath: f7a6cbadf9d836d2149c2a84c435b1effc244cba libavif: 5f8e715bea24debec477006f21ef9e95432e254d libdav1d: 23581a4d8ec811ff171ed5e2e05cd27bad64c39f libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 - op-sqlite: 8d0462679673b145c995a240ff432e4346718689 + op-sqlite: 10397885c9f2d478a9d099d20e2a7915c5fda944 opencv-rne: 2305807573b6e29c8c87e3416ab096d09047a7a0 Pdfium: 2695c434cbefe1a651d3fee0034571dc2817efd5 - Pulsar: db1cccc0a590d3dae98f2a9a3151bfc9be8785c3 + Pulsar: 9ec19a182a03d08ecefb47877e8de9e976c2a03c + Pulsar-haptics: f795eaf55d8d0290148fd97c747dcc6689c3d079 RCTDeprecation: 5045f20b2cc1239bf422764004338c720684a22f RCTRequired: b6b9724225dd780ac2989d2e575d5513c50fe04b RCTSwiftUI: 395b65655229fa2006415207adcfcb6e35dc78ed @@ -3090,7 +3094,7 @@ SPEC CHECKSUMS: ReactCommon: c6e81cc1ae185fa84863f3ea1d58caac4be741d7 ReactNativeDependencies: f6a49cf945d48640a12c0f24cd404b89c7218273 ReactNativeEnrichedMarkdown: 27a73f8df1bc304c35b7bd922c9885a4dfec3dc5 - ReactNativeFs: 54a8079110e99a52ab4aeb72f16c6785095eb9e8 + ReactNativeFs: f2b571997aa8d6397850a6477e1c8f5823171e50 RNAudioAPI: a2a67b86dbd376f391252f0e249aec34a6e1ae92 RNCAsyncStorage: 3a4f5e2777dae1688b781a487923a08569e27fe4 RNDeviceInfo: 4c852998208b60dc192ae3529e5867817719ad1e diff --git a/store/chatStore.ts b/store/chatStore.ts index 0f0833a0..d76217f4 100644 --- a/store/chatStore.ts +++ b/store/chatStore.ts @@ -217,7 +217,8 @@ export const useChatStore = create((set, get) => ({ return; } - await activateSource(db, chatId, sourceId); + const activated = await activateSource(db, chatId, sourceId); + if (!activated) return; set((state) => ({ chats: state.chats.map((chat) => diff --git a/store/embeddingModelStore.ts b/store/embeddingModelStore.ts new file mode 100644 index 00000000..b3f5cc09 --- /dev/null +++ b/store/embeddingModelStore.ts @@ -0,0 +1,55 @@ +import { create } from 'zustand'; +import { OPSQLiteVectorStore } from '@react-native-rag/op-sqlite'; + +export type EmbeddingModelStatus = + 'unknown' | 'not_downloaded' | 'downloading' | 'ready' | 'error'; + +type EmbeddingModelStore = { + status: EmbeddingModelStatus; + progress: number; + setProgress: (progress: number) => void; + setStatus: (status: EmbeddingModelStatus) => void; + markReady: () => void; + ensureReady: (vectorStore: OPSQLiteVectorStore) => Promise; +}; + +let inFlightLoad: Promise | null = null; + +export const useEmbeddingModelStore = create( + (set, get) => ({ + status: 'unknown', + progress: 0, + + setProgress: (progress) => + set({ + progress: Number.isFinite(progress) + ? Math.min(1, Math.max(0, progress)) + : 0, + }), + setStatus: (status) => set({ status }), + markReady: () => set({ status: 'ready', progress: 1 }), + + ensureReady: async (vectorStore) => { + if (get().status === 'ready') return true; + if (inFlightLoad) return inFlightLoad; + + set({ status: 'downloading', progress: 0 }); + + inFlightLoad = (async () => { + try { + await vectorStore.load(); + set({ status: 'ready', progress: 1 }); + return true; + } catch (error) { + console.error('Failed to download/load embedding model', error); + set({ status: 'error', progress: 0 }); + return false; + } finally { + inFlightLoad = null; + } + })(); + + return inFlightLoad; + }, + }) +); diff --git a/store/llmStore.ts b/store/llmStore.ts index 86fa5120..f9a9ffb7 100644 --- a/store/llmStore.ts +++ b/store/llmStore.ts @@ -7,6 +7,7 @@ import { getChatMessages, Message, persistMessage, + SourceDocument, } from '../database/chatRepository'; import DeviceInfo from 'react-native-device-info'; import { BENCHMARK_PROMPT } from '../constants/default-benchmark'; @@ -15,10 +16,14 @@ import { type Message as ExecutorchMessage } from 'react-native-executorch'; import { Platform } from 'react-native'; import { Feedback } from '../utils/Feedback'; import { prepareMessagesForLLM } from '../utils/promptUtils'; +import { + pickCitationsByAnswer, + restrictCitationsToContext, +} from '../utils/messageSources'; import { useSettingsStore } from './settingsStore'; import { getGenerationConfigForModel } from '../constants/default-models'; -interface LLMStore { +export interface LLMStore { isLoading: boolean; isGenerating: boolean; isProcessingPrompt: boolean; @@ -39,7 +44,11 @@ interface LLMStore { sendChatMessage: ( newMessage: string, chatId: number, - context: string[], + buildSources: () => Promise<{ + context: string[]; + sourceDocuments?: SourceDocument[]; + preferredSourceDocuments?: SourceDocument[]; + }>, settings: ChatSettings, imagePath?: string, documentName?: string @@ -88,7 +97,7 @@ const createMemoryTracker = (onUpdate: (usedMemory: number) => void) => { if (Platform.OS !== 'ios') { return { start: () => {}, stop: () => {} }; } - let trackerId: number; + let trackerId: ReturnType; return { start: () => { trackerId = setInterval(async () => { @@ -148,6 +157,7 @@ const updateChatStateForGeneration = ( set({ isProcessingPrompt: true, generatingForChatId: data?.chatId, + ...(data?.chatId !== undefined ? { activeChatId: data.chatId } : {}), activeChatMessages: data?.activeChatMessages, }); break; @@ -174,6 +184,9 @@ const updateChatStateForGeneration = ( ...msg, id: data.assistantMessage?.id ?? msg.id, content: data.assistantMessage?.content ?? msg.content, + sourceDocuments: + data.assistantMessage?.sourceDocuments ?? + msg.sourceDocuments, timeToFirstToken: data.timeToFirstToken!, tokensPerSecond: data.tokensPerSecond!, } @@ -233,7 +246,7 @@ const generateLLMResponse = async ( content: [ { type: 'image' }, { type: 'text', text: msg.content as string }, - ] as any, + ] as unknown as string, } : msg ); @@ -386,7 +399,7 @@ export const useLLMStore = create((set, get) => ({ sendChatMessage: async ( newMessage, chatId, - context, + buildSources, settings, imagePath, documentName @@ -397,34 +410,50 @@ export const useLLMStore = create((set, get) => ({ return; } + const tempUserId = -Date.now(); + const userMessage: Message = { + id: tempUserId, + role: 'user', + content: newMessage, + chatId, + timestamp: Date.now(), + imagePath, + documentName, + }; + const assistantPlaceholder: Message = { + role: 'assistant', + content: '', + modelName: currentModel.modelName, + chatId: chatId, + timestamp: Date.now(), + id: -1, + }; + + updateChatStateForGeneration(set, 'start', { + chatId, + activeChatMessages: [ + ...activeChatMessages, + userMessage, + assistantPlaceholder, + ], + }); + try { - const userMessage: Omit = { + const userMessageId = await persistMessage(db, { role: 'user', content: newMessage, chatId, - timestamp: Date.now(), imagePath, documentName, - }; - const assistantPlaceholder: Message = { - role: 'assistant', - content: '', - modelName: currentModel.modelName, - chatId: chatId, - timestamp: Date.now(), - id: -1, - }; - const userMessageId = await persistMessage(db, userMessage); - const updatedChatMessages = [ - ...activeChatMessages, - { ...userMessage, id: userMessageId }, - assistantPlaceholder, - ]; - - updateChatStateForGeneration(set, 'start', { - chatId, - activeChatMessages: updatedChatMessages, }); + set((state) => ({ + activeChatMessages: state.activeChatMessages.map((msg) => + msg.id === tempUserId ? { ...msg, id: userMessageId } : msg + ), + })); + + const { context, sourceDocuments, preferredSourceDocuments } = + await buildSources(); await waitForModelLoad(get); @@ -440,7 +469,20 @@ export const useLLMStore = create((set, get) => ({ context, settings, currentModel, - useSettingsStore.getState().customSystemPrompt + useSettingsStore.getState().customSystemPrompt, + preferredSourceDocuments + ); + + const lastPreparedMessage = messagesWithSystemPrompt.at(-1); + const lastPreparedContent = + typeof lastPreparedMessage?.content === 'string' + ? lastPreparedMessage.content + : JSON.stringify(lastPreparedMessage?.content ?? ''); + + const seenSourceDocuments = restrictCitationsToContext( + sourceDocuments ?? [], + lastPreparedContent, + preferredSourceDocuments ?? [] ); // Set generation state and generate response @@ -449,9 +491,15 @@ export const useLLMStore = create((set, get) => ({ await generateLLMResponse(messagesWithSystemPrompt, get); // Handle successful response if (finalResponse) { + const citedSourceDocuments = pickCitationsByAnswer( + seenSourceDocuments, + finalResponse, + preferredSourceDocuments ?? [] + ); const assistantMessageId = await persistMessage(db, { ...assistantPlaceholder, content: finalResponse, + sourceDocuments: citedSourceDocuments, tokensPerSecond: responsePerformance.tokensPerSecond, timeToFirstToken: responsePerformance.timeToFirstToken, }); @@ -462,6 +510,7 @@ export const useLLMStore = create((set, get) => ({ ...assistantPlaceholder, id: assistantMessageId, content: finalResponse, + sourceDocuments: citedSourceDocuments, tokensPerSecond: responsePerformance.tokensPerSecond, timeToFirstToken: responsePerformance.timeToFirstToken, }, @@ -548,7 +597,7 @@ export const useLLMStore = create((set, get) => ({ tokensGenerated: llmInstance.getGeneratedTokenCount(), peakMemory: runPeakMemory, }; - } catch (e) { + } catch { memoryTracker.stop(); } finally { set({ isGenerating: false, isBenchmarking: false }); diff --git a/store/sourceStore.ts b/store/sourceStore.ts index 8db395d5..24f76a39 100644 --- a/store/sourceStore.ts +++ b/store/sourceStore.ts @@ -13,6 +13,13 @@ import { OPSQLiteVectorStore } from '@react-native-rag/op-sqlite'; import { RecursiveCharacterTextSplitter } from 'react-native-rag'; import { readDocumentText } from '../utils/fileReaders'; import { useLLMStore } from './llmStore'; +import { LFMEmbeddings } from '../utils/lfmEmbeddings'; +import { + MAX_SOURCE_CHUNKS, + MAX_SOURCE_TEXT_CHARS, + TEXT_SPLITTER_CHUNK_OVERLAP, + TEXT_SPLITTER_CHUNK_SIZE, +} from '../constants/retrieval'; interface SourceStore { sources: Source[]; @@ -23,17 +30,24 @@ interface SourceStore { addSource: ( source: Omit, sourceUri: string, - vectorStore: OPSQLiteVectorStore - ) => Promise<{ success: boolean; isEmpty?: boolean; sourceId?: number }>; + vectorStore: OPSQLiteVectorStore, + embeddings?: LFMEmbeddings | null, + onProgress?: (progress: number) => void, + signal?: AbortSignal + ) => Promise<{ + success: boolean; + isEmpty?: boolean; + reason?: 'scanned_pdf'; + sourceId?: number; + cancelled?: boolean; + truncated?: boolean; + }>; setSourceProcessing: (id: number, isProcessing: boolean) => void; deleteSource: (source: Source) => Promise; renameSource: (id: number, newName: string) => Promise; cleanupOrphanedSources: (vectorStore: OPSQLiteVectorStore) => Promise; } -const TEXT_SPLITTER_CHUNK_SIZE = 1000; -const TEXT_SPLITTER_CHUNK_OVERLAP = 100; - export const useSourceStore = create((set, get) => ({ sources: [], db: null, @@ -54,7 +68,14 @@ export const useSourceStore = create((set, get) => ({ } }, - addSource: async (source, sourceUri, vectorStore) => { + addSource: async ( + source, + sourceUri, + vectorStore, + embeddings, + onProgress, + signal + ) => { const db = get().db; if (!db) return { success: false }; @@ -65,17 +86,33 @@ export const useSourceStore = create((set, get) => ({ const sourceTextContent = await readDocumentText(sourceUri, source.type); if (!sourceTextContent || sourceTextContent.trim().length === 0) { - return { success: false, isEmpty: true }; + const isScannedPdf = source.type.toLowerCase() === 'pdf'; + return { + success: false, + isEmpty: true, + ...(isScannedPdf ? { reason: 'scanned_pdf' as const } : {}), + }; } const tempSource: Source = { ...source, id: tempId, isProcessing: true }; set((state) => ({ sources: [...state.sources, tempSource] })); + const cappedText = + sourceTextContent.length > MAX_SOURCE_TEXT_CHARS + ? sourceTextContent.slice(0, MAX_SOURCE_TEXT_CHARS) + : sourceTextContent; + const textSplitter = new RecursiveCharacterTextSplitter({ chunkSize: TEXT_SPLITTER_CHUNK_SIZE, chunkOverlap: TEXT_SPLITTER_CHUNK_OVERLAP, }); - const chunks = await textSplitter.splitText(sourceTextContent); + const allChunks = await textSplitter.splitText(cappedText); + const truncated = + cappedText.length < sourceTextContent.length || + allChunks.length > MAX_SOURCE_CHUNKS; + const chunks = truncated + ? allChunks.slice(0, MAX_SOURCE_CHUNKS) + : allChunks; const sourceId = await insertSource(db, { ...source, @@ -88,11 +125,40 @@ export const useSourceStore = create((set, get) => ({ return { success: false }; } + const rollbackPartialSource = async () => { + if (vectorStore) { + await vectorStore.delete({ + predicate: (value) => value.metadata?.documentId === sourceId, + }); + } + await deleteSource(db, sourceId); + set((state) => ({ + sources: state.sources.filter((s) => s.id !== tempId), + })); + }; + + onProgress?.(0); for (let i = 0; i < chunks.length; i++) { + if (signal?.aborted) { + await rollbackPartialSource(); + return { success: false, cancelled: true }; + } + const embedding = embeddings + ? await embeddings.embedDocument(chunks[i]!) + : undefined; + const chunkId = `${sourceId}:${i}`; await vectorStore?.add({ + id: chunkId, document: chunks[i]!, - metadata: { documentId: sourceId, isFirstChunk: i === 0 }, + embedding, + metadata: { + documentId: sourceId, + name: source.name, + chunkIndex: i, + isFirstChunk: i === 0, + }, }); + onProgress?.((i + 1) / chunks.length); } set((state) => ({ @@ -102,7 +168,7 @@ export const useSourceStore = create((set, get) => ({ : s ), })); - return { success: true, sourceId }; + return { success: true, sourceId, truncated }; } catch (e) { console.error(e); set((state) => ({ diff --git a/utils/citationHighlight.ts b/utils/citationHighlight.ts new file mode 100644 index 00000000..2ca1c3a4 --- /dev/null +++ b/utils/citationHighlight.ts @@ -0,0 +1,179 @@ +import { TOKEN_PATTERN, extractQueryTerms } from './queryTerms'; +import { + CITATION_ALPHA_TERM_PATTERN, + CITATION_DOCUMENT_NAME_TOKEN_PATTERN, + CITATION_EXCERPT_MAX_CHARS, + CITATION_MIN_MATCH_SCORE, + CITATION_SENTENCE_PATTERN, + CITATION_STEM_PREFIX_LENGTH, +} from '../constants/citations'; + +export { extractQueryTerms }; + +export interface CitationSpan { + start: number; + end: number; +} + +interface Sentence { + text: string; + start: number; + end: number; +} + +const splitSentences = (passage: string): Sentence[] => { + const sentences: Sentence[] = []; + let match: RegExpExecArray | null; + + CITATION_SENTENCE_PATTERN.lastIndex = 0; + while ((match = CITATION_SENTENCE_PATTERN.exec(passage)) !== null) { + const raw = match[0]; + if (!raw.trim()) continue; + + const leading = raw.length - raw.trimStart().length; + const trailing = raw.length - raw.trimEnd().length; + const start = match.index + leading; + const end = match.index + raw.length - trailing; + if (end > start) { + sentences.push({ text: passage.slice(start, end), start, end }); + } + } + + return sentences; +}; + +type TermMatch = 'exact' | 'stem' | 'none'; + +const matchTermInSentence = ( + lower: string, + words: string[], + term: string +): TermMatch => { + if (lower.includes(term)) return 'exact'; + if ( + term.length < CITATION_STEM_PREFIX_LENGTH || + !CITATION_ALPHA_TERM_PATTERN.test(term) + ) { + return 'none'; + } + const prefix = term.slice(0, CITATION_STEM_PREFIX_LENGTH); + return words.some((word) => word.startsWith(prefix)) ? 'stem' : 'none'; +}; + +export const findCitedSpan = ( + passage: string | undefined, + query: string +): CitationSpan | null => { + if (!passage?.trim() || !query.trim()) return null; + + const terms = extractQueryTerms(query); + if (terms.size === 0) return null; + + const sentences = splitSentences(passage); + if (sentences.length === 0) return null; + + let best: CitationSpan | null = null; + let bestScore = 0; + let bestExact = 0; + let bestDensity = 0; + + for (const sentence of sentences) { + const lower = sentence.text.toLowerCase(); + const words = lower.match(TOKEN_PATTERN) ?? []; + let score = 0; + let exact = 0; + for (const term of terms) { + const match = matchTermInSentence(lower, words, term); + if (match === 'none') continue; + score += 1; + if (match === 'exact') exact += 1; + } + if (score === 0) continue; + + const density = score / sentence.text.length; + if (score > bestScore || (score === bestScore && density > bestDensity)) { + best = { start: sentence.start, end: sentence.end }; + bestScore = score; + bestExact = exact; + bestDensity = density; + } + } + + if (bestExact === 0 && bestScore < CITATION_MIN_MATCH_SCORE) return null; + + return best; +}; + +export const queryNamesDocument = (query: string, name: string): boolean => { + const base = name.replace(/\.[^.]+$/, '').toLowerCase(); + const tokens = base + .split(CITATION_DOCUMENT_NAME_TOKEN_PATTERN) + .filter((token) => token.length >= 3); + if (tokens.length === 0) return false; + + const lowerQuery = query.toLowerCase(); + return tokens.every((token) => lowerQuery.includes(token)); +}; + +export interface CitationExcerpt { + text: string; + span: CitationSpan | null; +} + +const cutAtWordBoundary = (text: string, limit: number): number => { + if (text.length <= limit) return text.length; + const window = text.slice(0, limit); + const lastSpace = window.lastIndexOf(' '); + return lastSpace > limit * 0.6 ? lastSpace : limit; +}; + +export const buildCitationExcerpt = ( + passage: string | undefined, + span: CitationSpan | null, + maxChars = CITATION_EXCERPT_MAX_CHARS +): CitationExcerpt => { + const source = passage ?? ''; + + const hasValidSpan = + span !== null && + span.start >= 0 && + span.end <= source.length && + span.start < span.end; + + if (!hasValidSpan) { + if (source.length <= maxChars) return { text: source, span: null }; + const cut = cutAtWordBoundary(source, maxChars); + return { text: `${source.slice(0, cut).trimEnd()}…`, span: null }; + } + + const { start: citeStart, end: citeEnd } = span!; + const citedLength = citeEnd - citeStart; + + if (citedLength >= maxChars) { + const text = source.slice(citeStart, citeEnd); + return { text, span: { start: 0, end: text.length } }; + } + + const remaining = maxChars - citedLength; + let start = Math.max(0, citeStart - Math.ceil(remaining / 2)); + let end = Math.min( + source.length, + citeEnd + (remaining - (citeStart - start)) + ); + + if (start > 0) { + const nextSpace = source.indexOf(' ', start); + if (nextSpace !== -1 && nextSpace < citeStart) start = nextSpace + 1; + } + if (end < source.length) { + const prevSpace = source.lastIndexOf(' ', end); + if (prevSpace > citeEnd) end = prevSpace; + } + + const prefix = start > 0 ? '…' : ''; + const suffix = end < source.length ? '…' : ''; + const text = `${prefix}${source.slice(start, end)}${suffix}`; + const relStart = prefix.length + (citeStart - start); + + return { text, span: { start: relStart, end: relStart + citedLength } }; +}; diff --git a/utils/citations.ts b/utils/citations.ts new file mode 100644 index 00000000..4349dd86 --- /dev/null +++ b/utils/citations.ts @@ -0,0 +1,14 @@ +import { CITATION_PATTERN } from '../constants/citations'; + +export const stripCitations = (text: string): string => { + if (!text) return text; + + let stripped = text; + let previous: string; + do { + previous = stripped; + stripped = stripped.replace(CITATION_PATTERN, '$1'); + } while (stripped !== previous); + + return stripped.replace(/ +([.,;:!?])/g, '$1').replace(/[ \t]{2,}/g, ' '); +}; diff --git a/utils/contextUtils.ts b/utils/contextUtils.ts index 570d9563..8a043b56 100644 --- a/utils/contextUtils.ts +++ b/utils/contextUtils.ts @@ -1,50 +1,134 @@ -const SIMILARITY_THRESHOLD = 0.3; -const MAX_RELEVANT_CHUNKS = 3; +import { MIN_STITCH_OVERLAP, SOURCE_HEADER } from '../constants/retrieval'; -interface ContextChunk { - document: string; +export type ContextChunk = { + document?: string; similarity: number; metadata?: { documentId?: number; name?: string; }; -} +}; -interface FirstChunkSource { +export type SourceDocument = { + documentId?: number; + name: string; + passage?: string; + similarity?: number; +}; + +type FirstChunkSource = { id: number; name: string; firstChunk?: string; -} +}; + +export const sourceKey = ( + documentId: number | undefined, + name: string +): string => `${documentId ?? 'unknown'}:${name}`; + +type DocumentGroup = { + documentId?: number; + name: string; + chunks: ContextChunk[]; + maxSimilarity: number; +}; + +const chunkDocumentName = (item: ContextChunk): string => + item.metadata?.name || `Document ${item.metadata?.documentId || 'Unknown'}`; -export const filterAndFormatContext = (chunks: ContextChunk[]): string[] => { - if (chunks.length === 0) return []; +const groupChunksByDocument = (chunks: ContextChunk[]): DocumentGroup[] => { + const groups = new Map(); + const order: string[] = []; + + for (const item of chunks) { + const name = chunkDocumentName(item); + const key = sourceKey(item.metadata?.documentId, name); + const existing = groups.get(key); + + if (existing) { + existing.chunks.push(item); + existing.maxSimilarity = Math.max( + existing.maxSimilarity, + item.similarity + ); + } else { + groups.set(key, { + documentId: item.metadata?.documentId, + name, + chunks: [item], + maxSimilarity: item.similarity, + }); + order.push(key); + } + } + + return order.map((key) => groups.get(key)!); +}; + +// Drop the leading part of `next` that repeats the tail of `prev`, longest match first. +const stripLeadingOverlap = (prev: string, next: string): string => { + const max = Math.min(prev.length, next.length); + for (let len = max; len >= MIN_STITCH_OVERLAP; len--) { + if (prev.slice(prev.length - len) === next.slice(0, len)) { + return next.slice(len); + } + } + return next; +}; + +const joinGroupPassages = (group: DocumentGroup): string => { + const passages = group.chunks + .map((chunk) => chunk.document?.trim() ?? '') + .filter(Boolean); + if (passages.length === 0) return ''; + + let stitched = passages[0]!; + for (let i = 1; i < passages.length; i++) { + const deduped = stripLeadingOverlap(stitched, passages[i]!).trimStart(); + if (deduped) stitched += `\n\n${deduped}`; + } + return stitched; +}; - const relevant = chunks - .filter((c) => c.similarity >= SIMILARITY_THRESHOLD) - .sort((a, b) => b.similarity - a.similarity) - .slice(0, MAX_RELEVANT_CHUNKS); +export const formatContextChunks = (chunks: ContextChunk[]): string[] => + groupChunksByDocument(chunks).map( + (group, index) => + `\n --- Source ${index + 1}: ${ + group.name + } --- \n ${joinGroupPassages(group)} \n --- End of Source ${ + index + 1 + } ---` + ); - return relevant.map((item, index) => { - const documentName = - item.metadata?.name || - `Document ${item.metadata?.documentId || 'Unknown'}`; - const relevanceScore = item.similarity - ? `(Relevance: ${(item.similarity * 100).toFixed(1)}%)` - : ''; +export const getSourceDocumentsFromChunks = ( + chunks: ContextChunk[] +): SourceDocument[] => + groupChunksByDocument(chunks).map((group) => ({ + documentId: group.documentId, + name: group.name, + passage: joinGroupPassages(group), + similarity: group.maxSimilarity, + })); - return `\n --- Source ${ - index + 1 - }: ${documentName} ${relevanceScore} --- \n ${item.document?.trim()} \n --- End of Source ${ - index + 1 - } ---`; - }); +export const sourcesPresentInContext = ( + contextContent: string +): Set => { + const names = new Set(); + for (const match of contextContent.matchAll(SOURCE_HEADER)) { + names.add(match[1]!.replace(/ \(Overview\)$/, '').trim()); + } + return names; }; -export const formatFirstChunks = (sources: FirstChunkSource[]): string[] => { +export const formatFirstChunks = ( + sources: FirstChunkSource[], + label = 'Source' +): string[] => { return sources .filter((s) => s.firstChunk) .map( (s) => - `\n --- Source: ${s.name} (Overview) --- \n ${s.firstChunk!.trim()} \n --- End of Source ---` + `\n --- ${label}: ${s.name} (Overview) --- \n ${s.firstChunk!.trim()} \n --- End of ${label} ---` ); }; diff --git a/utils/documentErrorMessage.ts b/utils/documentErrorMessage.ts new file mode 100644 index 00000000..744c311b --- /dev/null +++ b/utils/documentErrorMessage.ts @@ -0,0 +1,12 @@ +export const documentErrorMessage = (result: { + reason?: 'scanned_pdf'; + isEmpty?: boolean; +}): string => { + if (result.reason === 'scanned_pdf') { + return 'This PDF has no selectable text — it looks scanned, so it can’t be read yet. Try a text-based PDF.'; + } + if (result.isEmpty) { + return 'Document appears to be empty.'; + } + return 'Failed to process document.'; +}; diff --git a/utils/documentType.ts b/utils/documentType.ts new file mode 100644 index 00000000..36b7f298 --- /dev/null +++ b/utils/documentType.ts @@ -0,0 +1,10 @@ +import { SPREADSHEET_DOC_TYPES } from '../constants/documents'; + +export const getDocumentType = (name: string): string => { + const lastDot = name.lastIndexOf('.'); + if (lastDot <= 0 || lastDot === name.length - 1) return ''; + return name.slice(lastDot + 1).toUpperCase(); +}; + +export const isSpreadsheetType = (docType: string): boolean => + SPREADSHEET_DOC_TYPES.has(docType); diff --git a/utils/embeddingModel.ts b/utils/embeddingModel.ts new file mode 100644 index 00000000..db296d69 --- /dev/null +++ b/utils/embeddingModel.ts @@ -0,0 +1,20 @@ +import { ExpoResourceFetcher } from 'react-native-executorch-expo-resource-fetcher'; +import { + LFM_2_5_EMBEDDING_DOWNLOAD_SIZE_BYTES, + LFM_2_5_EMBEDDING_MODEL_FILE, +} from '../constants/embedding-model'; + +export const isEmbeddingModelDownloaded = async (): Promise => { + try { + const files = await ExpoResourceFetcher.listDownloadedFiles(); + return files.some((file) => file.endsWith(LFM_2_5_EMBEDDING_MODEL_FILE)); + } catch (error) { + console.warn('Failed to check embedding model download status', error); + return false; + } +}; + +export const embeddingModelDownloadSizeLabel = (): string => { + const megabytes = LFM_2_5_EMBEDDING_DOWNLOAD_SIZE_BYTES / (1024 * 1024); + return `${Math.round(megabytes)} MB`; +}; diff --git a/utils/embeddingModelMigration.ts b/utils/embeddingModelMigration.ts new file mode 100644 index 00000000..8edaa952 --- /dev/null +++ b/utils/embeddingModelMigration.ts @@ -0,0 +1,63 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { type SQLiteDatabase } from 'expo-sqlite'; +import { OPSQLiteVectorStore } from '@react-native-rag/op-sqlite'; +import { ACTIVE_EMBEDDING_MODEL_KEY } from '../constants/embedding-model'; + +const readPersistedVectorDim = async ( + vectorStore: OPSQLiteVectorStore +): Promise => { + try { + const result = await vectorStore.db.execute( + `SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'vectors'` + ); + const createSql = result.rows[0]?.sql as string | undefined; + const match = createSql?.match(/F32_BLOB\((\d+)\)/i); + return match ? Number(match[1]) : null; + } catch { + return null; + } +}; + +const clearImportedSources = async ( + vectorStore: OPSQLiteVectorStore, + db: SQLiteDatabase +): Promise => { + await vectorStore.deleteVectorStore(); + try { + await db.runAsync(`DELETE FROM chatSources`); + await db.runAsync(`DELETE FROM sources`); + return true; + } catch (error) { + console.warn( + 'Failed to clear source metadata during embedding migration', + error + ); + return false; + } +}; + +export const migrateEmbeddingModelIfNeeded = async ( + vectorStore: OPSQLiteVectorStore, + db: SQLiteDatabase, + currentModelId: string, + currentModelDim: number +): Promise => { + const storedModelId = await AsyncStorage.getItem(ACTIVE_EMBEDDING_MODEL_KEY); + + if (storedModelId === currentModelId) return false; + + if (storedModelId === null) { + const persistedDim = await readPersistedVectorDim(vectorStore); + const incompatible = + persistedDim !== null && persistedDim !== currentModelDim; + if (incompatible && !(await clearImportedSources(vectorStore, db))) { + return true; + } + await AsyncStorage.setItem(ACTIVE_EMBEDDING_MODEL_KEY, currentModelId); + return incompatible; + } + + if (!(await clearImportedSources(vectorStore, db))) return true; + await AsyncStorage.setItem(ACTIVE_EMBEDDING_MODEL_KEY, currentModelId); + return true; +}; diff --git a/utils/fileReaders.ts b/utils/fileReaders.ts index 25f34d49..55eb9dac 100644 --- a/utils/fileReaders.ts +++ b/utils/fileReaders.ts @@ -1,28 +1,62 @@ import { readPDF } from 'react-native-pdfium'; import { File } from 'expo-file-system'; -/** - * Reads text content from various file formats - * @param filePath - The path to the file - * @param fileType - The file extension (pdf, txt, md, html, csv, etc.) - * @returns The extracted text content - */ +const stripHtml = (html: string): string => + html + .replace(/)<[^<]*)*<\/script>/gi, '') + .replace(/)<[^<]*)*<\/style>/gi, '') + .replace(/<[^>]+>/g, ' ') + .replace(/ /g, ' ') + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/\s+/g, ' ') + .trim(); + +const stripInvisibleChars = (text: string): string => + text.replace(/[\p{Cc}\p{Cf}\uFFF9-\uFFFF]/gu, (ch) => + ch === '\n' || ch === '\t' ? ch : '' + ); + +const rejoinHyphenatedWords = (text: string): string => + text.replace(/(\p{Ll})[-\u2010\u2011]\n(\p{Ll})/gu, '$1$2'); + +const collapseBlankLines = (text: string): string => + text.replace(/[ \t]*\n(?:[ \t]*\n)+/g, '\n\n'); + +const unwrapSoftLineBreaks = (text: string): string => + text.replace(/([^\n])\n(?!\n)/g, '$1 '); + +const tidyWhitespace = (text: string): string => + text.replace(/[ \t]{2,}/g, ' ').replace(/ +\n/g, '\n'); + +// PDF extraction leaks layout artifacts (hyphen-split words, hard-wrapped +// lines, invisible control characters) that break retrieval and citations. +export const normalizePdfText = (raw: string): string => { + const stripped = stripInvisibleChars(raw); + const rejoined = rejoinHyphenatedWords(stripped); + const collapsed = collapseBlankLines(rejoined); + const unwrapped = unwrapSoftLineBreaks(collapsed); + return tidyWhitespace(unwrapped).trim(); +}; + export async function readDocumentText( filePath: string, - fileType: string + fileExtension: string ): Promise { - const lowerFileType = fileType.toLowerCase(); - - switch (lowerFileType) { + switch (fileExtension.toLowerCase()) { case 'pdf': { - // PDF reader needs path without file:// prefix const normalizedPath = filePath.replace('file://', ''); - return await readPDF(normalizedPath); + const rawText = await readPDF(normalizedPath); + return normalizePdfText(rawText); } case 'txt': case 'md': - case 'markdown': { + case 'markdown': + case 'csv': { const textFile = new File(filePath); return await textFile.text(); } @@ -31,22 +65,10 @@ export async function readDocumentText( case 'htm': { const htmlFile = new File(filePath); const htmlContent = await htmlFile.text(); - // Basic HTML tag stripping - removes all HTML tags - return htmlContent - .replace(/)<[^<]*)*<\/script>/gi, '') // Remove script tags - .replace(/)<[^<]*)*<\/style>/gi, '') // Remove style tags - .replace(/<[^>]+>/g, ' ') // Remove all HTML tags - .replace(/ /g, ' ') - .replace(/&/g, '&') - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/"/g, '"') - .replace(/'/g, "'") - .replace(/\s+/g, ' ') // Normalize whitespace - .trim(); + return stripHtml(htmlContent); } default: - throw new Error(`Unsupported file type: ${fileType}`); + throw new Error(`Unsupported file type: ${fileExtension}`); } } diff --git a/utils/legacyChat.ts b/utils/legacyChat.ts new file mode 100644 index 00000000..4a25f6b4 --- /dev/null +++ b/utils/legacyChat.ts @@ -0,0 +1,36 @@ +import { Message } from '../database/chatRepository'; +import { getSourceLinkingBoundary } from './sourceLinkingBoundary'; + +export type LegacyChatDiagnosis = { + hasLegacyDocument: boolean; + hasSourceLinking: boolean; + isLegacy: boolean; +}; + +// Both checks are scoped to the pre-boundary era (id <= boundary) on purpose: a +// new turn that retrieves a source (id > boundary) must not flip a legacy chat to +// "linked" and drop the notice mid-conversation. +export const diagnoseLegacyChat = ( + messages: Message[], + boundaryMessageId: number = getSourceLinkingBoundary() +): LegacyChatDiagnosis => { + const hasLegacyDocument = messages.some( + (message) => !!message.documentName && message.id <= boundaryMessageId + ); + const hasSourceLinking = messages.some( + (message) => + message.id <= boundaryMessageId && + !!message.sourceDocuments && + message.sourceDocuments.length > 0 + ); + return { + hasLegacyDocument, + hasSourceLinking, + isLegacy: hasLegacyDocument && !hasSourceLinking, + }; +}; + +export const chatPredatesSourceLinking = ( + messages: Message[], + boundaryMessageId: number = getSourceLinkingBoundary() +): boolean => diagnoseLegacyChat(messages, boundaryMessageId).isLegacy; diff --git a/utils/lfmEmbeddings.ts b/utils/lfmEmbeddings.ts new file mode 100644 index 00000000..643e500e --- /dev/null +++ b/utils/lfmEmbeddings.ts @@ -0,0 +1,15 @@ +import { ExecuTorchEmbeddings } from '@react-native-rag/executorch'; +import { + EMBEDDING_DOCUMENT_PREFIX, + EMBEDDING_QUERY_PREFIX, +} from '../constants/embedding-model'; + +export class LFMEmbeddings extends ExecuTorchEmbeddings { + embedQuery(text: string): Promise { + return this.embed(`${EMBEDDING_QUERY_PREFIX}${text}`); + } + + embedDocument(text: string): Promise { + return this.embed(`${EMBEDDING_DOCUMENT_PREFIX}${text}`); + } +} diff --git a/utils/messageSources.ts b/utils/messageSources.ts new file mode 100644 index 00000000..b54929f2 --- /dev/null +++ b/utils/messageSources.ts @@ -0,0 +1,299 @@ +import { OPSQLiteVectorStore } from '@react-native-rag/op-sqlite'; +import { LFMEmbeddings } from './lfmEmbeddings'; +import { SourceDocument } from '../database/chatRepository'; +import { + formatContextChunks, + formatFirstChunks, + getSourceDocumentsFromChunks, + sourceKey, + sourcesPresentInContext, +} from './contextUtils'; +import { retrieve } from './retrieval'; +import { extractQueryTerms, stemPrefix } from './queryTerms'; +import { ANSWER_CITATION_OVERLAP_RATIO } from '../constants/retrieval'; +import { + CITATION_SENTENCE_PATTERN, + CLAUSE_SPLIT_PATTERN, + NEGATION_CUE_EN, + NO_ANSWER_PATTERNS_EN, + THINK_CLOSE, + THINK_OPEN, +} from '../constants/citations'; + +export interface SourceRow { + id: number; + name: string; + type?: string; + firstChunk?: string; +} + +const getAttachmentSourceDocuments = ( + sources: SourceRow[], + attachmentSourceIds: number[] +): SourceDocument[] => + sources + .filter((source) => attachmentSourceIds.includes(source.id)) + .map((source) => ({ + documentId: source.id, + name: source.name, + passage: source.firstChunk, + })); + +export const mergeAttachmentFirst = ( + retrieved: SourceDocument[], + preferred: SourceDocument[], + attachmentSourceIds: number[] +): SourceDocument[] => { + const attachmentIds = new Set(attachmentSourceIds); + const isAttachment = (doc: SourceDocument) => + doc.documentId !== undefined && attachmentIds.has(doc.documentId); + + const attachmentDocs = retrieved.filter(isAttachment); + const otherDocs = retrieved.filter((doc) => !isAttachment(doc)); + + const citedKeys = new Set( + attachmentDocs.map((doc) => sourceKey(doc.documentId, doc.name)) + ); + const missingAttachments = preferred.filter( + (doc) => !citedKeys.has(sourceKey(doc.documentId, doc.name)) + ); + + return [...attachmentDocs, ...missingAttachments, ...otherDocs]; +}; + +export const assembleSourceDocuments = ( + retrieved: SourceDocument[], + preferred: SourceDocument[], + attachmentSourceIds: number[], + activeSources: SourceRow[], + contextPresent: boolean +): SourceDocument[] => { + const merged = mergeAttachmentFirst( + retrieved, + preferred, + attachmentSourceIds + ); + if (merged.length > 0 || !contextPresent) return merged; + + return activeSources.map((source) => ({ + documentId: source.id, + name: source.name, + passage: source.firstChunk, + })); +}; + +export const restrictCitationsToContext = ( + sourceDocuments: SourceDocument[], + promptContext: string, + preferred: SourceDocument[] +): SourceDocument[] => { + if (sourceDocuments.length <= 1) return sourceDocuments; + + const present = sourcesPresentInContext(promptContext); + const preferredNames = new Set(preferred.map((doc) => doc.name)); + + const survived = sourceDocuments.filter( + (doc) => preferredNames.has(doc.name) || present.has(doc.name) + ); + return survived.length > 0 ? survived : sourceDocuments.slice(0, 1); +}; + +const overlapWithAnswer = ( + passage: string, + answerTerms: Set +): number => { + let overlap = 0; + const seen = new Set(); + for (const term of extractQueryTerms(passage)) { + const stem = stemPrefix(term); + if (seen.has(stem)) continue; + seen.add(stem); + if (answerTerms.has(stem)) overlap++; + } + return overlap; +}; + +// Attribute against the visible reply only; the block surveys every source and inflates overlap. +export const visibleAnswer = (answer: string): string => { + const parts: string[] = []; + let cursor = 0; + let open = answer.indexOf(THINK_OPEN); + + while (open !== -1) { + parts.push(answer.slice(cursor, open)); + const close = answer.indexOf(THINK_CLOSE, open + THINK_OPEN.length); + if (close === -1) return `${parts.join(' ')} `; + cursor = close + THINK_CLOSE.length; + open = answer.indexOf(THINK_OPEN, cursor); + } + + parts.push(answer.slice(cursor)); + return parts.join(' '); +}; + +const affirmativeAnswer = (visibleReply: string): string => + (visibleReply.match(CITATION_SENTENCE_PATTERN) ?? [visibleReply]) + .flatMap((sentence) => sentence.split(CLAUSE_SPLIT_PATTERN)) + .filter((clause) => clause && !NEGATION_CUE_EN.test(clause)) + .join(' '); + +const answerTermsOf = (answer: string): Set => + new Set( + [...extractQueryTerms(affirmativeAnswer(visibleAnswer(answer)))].map( + stemPrefix + ) + ); + +// True when the visible reply is an EN/PL "no information" refusal (negation tied to a coverage noun). +export const looksLikeNoAnswer = (visibleReply: string): boolean => + NO_ANSWER_PATTERNS_EN.some((pattern) => pattern.test(visibleReply)); + +// Compact per-document overlap for logs: `name:overlap` per candidate, so a surprising citation set is diagnosable. +export const answerCitationOverlaps = ( + sourceDocuments: SourceDocument[], + answer: string +): string[] => { + const answerTerms = answerTermsOf(answer); + return sourceDocuments.map( + (doc) => + `${doc.name}:${overlapWithAnswer(`${doc.name} ${doc.passage ?? ''}`, answerTerms)}` + ); +}; + +export const pickCitationsByAnswer = ( + sourceDocuments: SourceDocument[], + answer: string, + preferred: SourceDocument[] +): SourceDocument[] => { + if (sourceDocuments.length <= 1) return sourceDocuments; + + const preferredNames = new Set(preferred.map((doc) => doc.name)); + + // A refusal cites nothing but a freshly-attached subject, even when it describes the sources. + if (looksLikeNoAnswer(visibleAnswer(answer))) { + return sourceDocuments.filter((doc) => preferredNames.has(doc.name)); + } + + const answerTerms = answerTermsOf(answer); + const scored = sourceDocuments.map((doc) => ({ + doc, + isPreferred: preferredNames.has(doc.name), + overlap: answerTerms.size + ? overlapWithAnswer(`${doc.name} ${doc.passage ?? ''}`, answerTerms) + : 0, + })); + + const maxOverlap = Math.max(0, ...scored.map((s) => s.overlap)); + + // Reply echoes no candidate → not grounded in any; keep only a freshly-attached source, cite nothing else. + if (maxOverlap === 0) { + return scored.filter((s) => s.isPreferred).map((s) => s.doc); + } + + return scored + .filter( + (s) => + s.isPreferred || s.overlap >= maxOverlap * ANSWER_CITATION_OVERLAP_RATIO + ) + .map((s) => s.doc); +}; + +const retrieveChunks = async ( + userInput: string, + allSourceIds: number[], + activeSources: SourceRow[], + attachmentSourceIds: number[], + vectorStore: OPSQLiteVectorStore, + embeddings?: LFMEmbeddings | null +) => { + try { + const relevantChunks = await retrieve({ + prompt: userInput, + enabledSourceIds: allSourceIds, + vectorStore, + sourceNamesById: new Map(activeSources.map((s) => [s.id, s.name])), + embeddings, + attachmentSourceIds, + }); + return relevantChunks; + } catch (error) { + console.error('Error preparing context:', error); + return []; + } +}; + +export interface BuildMessageSourcesParams { + userInput: string; + attachmentSourceIds: number[]; + enabledSources: number[]; + sources: SourceRow[]; + vectorStore: OPSQLiteVectorStore; + embeddings?: LFMEmbeddings | null; +} + +export interface MessageSources { + context: string[]; + sourceDocuments: SourceDocument[]; + preferredSourceDocuments: SourceDocument[]; +} + +export const buildMessageSources = async ({ + userInput, + attachmentSourceIds, + enabledSources, + sources, + vectorStore, + embeddings, +}: BuildMessageSourcesParams): Promise => { + const empty: MessageSources = { + context: [], + sourceDocuments: [], + preferredSourceDocuments: [], + }; + + const allSourceIds = [ + ...new Set([...enabledSources, ...attachmentSourceIds]), + ]; + if (allSourceIds.length === 0) return empty; + + const activeSources = sources.filter((s) => allSourceIds.includes(s.id)); + const activeAttachmentSources = activeSources.filter((s) => + attachmentSourceIds.includes(s.id) + ); + const preferredSourceDocuments = getAttachmentSourceDocuments( + activeSources, + attachmentSourceIds + ); + const attachmentOverview = () => + formatFirstChunks(activeAttachmentSources, 'Current Attachment Source'); + + const context: string[] = []; + let sourceDocuments: SourceDocument[] = []; + + if (userInput.trim()) { + const relevantChunks = await retrieveChunks( + userInput, + allSourceIds, + activeSources, + attachmentSourceIds, + vectorStore, + embeddings + ); + context.push(...attachmentOverview()); + context.push(...formatContextChunks(relevantChunks)); + + const retrieved = getSourceDocumentsFromChunks(relevantChunks); + sourceDocuments = assembleSourceDocuments( + retrieved, + preferredSourceDocuments, + attachmentSourceIds, + activeSources, + context.length > 0 + ); + } else if (attachmentSourceIds.length > 0) { + sourceDocuments = preferredSourceDocuments; + context.push(...attachmentOverview()); + } + + return { context, sourceDocuments, preferredSourceDocuments }; +}; diff --git a/utils/modelConfig.ts b/utils/modelConfig.ts deleted file mode 100644 index e665b8dd..00000000 --- a/utils/modelConfig.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { Platform } from 'react-native'; -import { - copyFileAssets, - DocumentDirectoryPath, - exists, -} from '@dr.pogodin/react-native-fs'; - -export interface ModelSourceConfig { - decoderSource?: string; - encoderSource?: string; - tokenizerSource?: string; - modelSource?: string; -} - -export interface ModelDefinition { - assetPackName: string; - bundledAssets: ModelSourceConfig; - assetPackFiles: ModelSourceConfig; -} - -export enum ModelSourceStrategy { - BUNDLED_ASSETS = 'bundled_assets', - ASSET_PACK_COPY = 'asset_pack_copy', -} - -export const ALL_MINI_LM_MODEL: ModelDefinition = { - assetPackName: 'all-mini-lm', - bundledAssets: { - modelSource: require('../assets/models/all-mini-lm/all-MiniLM-L6-v2_xnnpack.pte'), - tokenizerSource: require('../assets/models/all-mini-lm/tokenizer.json'), - }, - assetPackFiles: { - modelSource: 'all-MiniLM-L6-v2_xnnpack.pte', - tokenizerSource: 'tokenizer.json', - }, -}; - -export function getModelSourceStrategy(): ModelSourceStrategy { - if (Platform.OS === 'ios') { - return ModelSourceStrategy.BUNDLED_ASSETS; - } - - if (Platform.OS === 'android') { - return __DEV__ - ? ModelSourceStrategy.BUNDLED_ASSETS - : ModelSourceStrategy.ASSET_PACK_COPY; - } - - return ModelSourceStrategy.BUNDLED_ASSETS; -} - -export async function getModelConfig( - modelDef: ModelDefinition -): Promise { - const strategy = getModelSourceStrategy(); - - switch (strategy) { - case ModelSourceStrategy.BUNDLED_ASSETS: - return getBundledAssetConfig(modelDef); - - case ModelSourceStrategy.ASSET_PACK_COPY: - return getAssetPackConfig(modelDef); - - default: - throw new Error(`Unsupported model source strategy: ${strategy}`); - } -} - -function getBundledAssetConfig(modelDef: ModelDefinition): ModelSourceConfig { - const config: ModelSourceConfig = { - tokenizerSource: modelDef.bundledAssets.tokenizerSource, - }; - - if (modelDef.bundledAssets.decoderSource) { - config.decoderSource = modelDef.bundledAssets.decoderSource; - } - - if (modelDef.bundledAssets.encoderSource) { - config.encoderSource = modelDef.bundledAssets.encoderSource; - } - - if (modelDef.bundledAssets.modelSource) { - config.modelSource = modelDef.bundledAssets.modelSource; - } - - return config; -} - -async function getAssetPackConfig( - modelDef: ModelDefinition -): Promise { - const modelDir = `${DocumentDirectoryPath}/${modelDef.assetPackName}_models`; - - const filePaths: Record = {}; - const existsPromises: Array> = []; - - Object.entries(modelDef.assetPackFiles).forEach(([key, filename]) => { - if (filename) { - filePaths[key] = `${modelDir}/${filename}`; - existsPromises.push(exists(filePaths[key])); - } - }); - - const filesExist = await Promise.all(existsPromises); - const allFilesExist = filesExist.every(Boolean); - if (!allFilesExist) { - await copyFileAssets(modelDef.assetPackName, modelDir); - } - - const config: ModelSourceConfig = { - tokenizerSource: `file://${filePaths.tokenizerSource}`, - }; - - if (filePaths.decoderSource) { - config.decoderSource = `file://${filePaths.decoderSource}`; - } - - if (filePaths.encoderSource) { - config.encoderSource = `file://${filePaths.encoderSource}`; - } - - if (filePaths.modelSource) { - config.modelSource = `file://${filePaths.modelSource}`; - } - - return config; -} diff --git a/utils/promptUtils.ts b/utils/promptUtils.ts index 28678a3d..96e744ea 100644 --- a/utils/promptUtils.ts +++ b/utils/promptUtils.ts @@ -1,26 +1,37 @@ -import { ChatSettings, Message } from '../database/chatRepository'; +import { + ChatSettings, + Message, + SourceDocument, +} from '../database/chatRepository'; import { Model } from '../database/modelRepository'; import { CUSTOM_PROMPT_GUARD } from '../constants/prompts'; import { type Message as ExecutorchMessage } from 'react-native-executorch'; +import { getPromptCharBudget } from '../constants/context-window'; const CONTEXT_INSTRUCTION = ` + IMPORTANT CONTEXT INFORMATION: -You have access to relevant information from the user's document sources. Use this context to provide accurate, well-informed responses. Always prioritize information from the provided context when it's relevant to the user's question. +The block below holds excerpts from the user's documents ("Source N: ", or "(Overview)" for a freshly attached file). It is the ONLY authoritative source for this question — answer strictly from it and prefer it over your own knowledge. +Do not answer about any document that is not in the current block, even if it appeared earlier in the chat. +If the block does not contain the answer, say "I don't know".`; + +const getPreferredSourceInstruction = (sources?: SourceDocument[]) => { + if (!sources?.length) return ''; + + const sourceNames = sources.map((source) => source.name).join(', '); + return ` -Instructions for using context: -- The context is delimited by and tags -- Refer to the context information when answering questions -- If the context directly addresses the user's question, use that information as the primary basis for your response -- If information from context conflicts with your general knowledge, prioritize the context -- If the context doesn't contain relevant information say "I don't know" or "The provided context does not contain the information" -- When citing information from context, you can reference it naturally without formal citations`; +CURRENT ATTACHMENT PRIORITY: +The user just attached: ${sourceNames}. Treat these as the subject of the question — "this file", "the document", "it" refer to them. Base the answer on them; bring in another source only if they lack the answer. You may still use earlier conversation for continuity.`; +}; export const prepareMessagesForLLM = ( activeChatMessages: Message[], context: string[], settings: ChatSettings, model: Model, - customSystemPrompt: string = '' + customSystemPrompt: string = '', + preferredSourceDocuments?: SourceDocument[] ): ExecutorchMessage[] => { let systemPrompt = settings.systemPrompt; @@ -34,44 +45,86 @@ export const prepareMessagesForLLM = ( if (context.length > 0) { systemPrompt += CONTEXT_INSTRUCTION; + systemPrompt += getPreferredSourceInstruction(preferredSourceDocuments); } - const filteredMessages: ExecutorchMessage[] = activeChatMessages - .filter((msg) => msg.role !== 'event') - .map((msg) => ({ - role: msg.role, - content: msg.content, - ...(msg.imagePath ? { mediaPath: msg.imagePath } : {}), - })); + const nonEventMessages = activeChatMessages.filter( + (msg): msg is Message & { role: Exclude } => + msg.role !== 'event' + ); + const lastNonEventMessage = nonEventMessages.at(-1); + const messagesForLLM = + lastNonEventMessage?.role === 'assistant' && + lastNonEventMessage.content.trim().length === 0 + ? nonEventMessages.slice(0, -1) + : nonEventMessages; + + const filteredMessages: ExecutorchMessage[] = messagesForLLM.map((msg) => ({ + role: msg.role, + content: msg.content, + ...(msg.imagePath ? { mediaPath: msg.imagePath } : {}), + })); const messagesWithSystemPrompt: ExecutorchMessage[] = [ { role: 'system', content: systemPrompt }, ...filteredMessages, ]; - const lastMessage = messagesWithSystemPrompt.at(-1); - - if (!lastMessage) { + if (messagesWithSystemPrompt.length <= 1) { return messagesWithSystemPrompt; } + const lastMessage = messagesWithSystemPrompt.at(-1)!; + if (settings.thinkingEnabled) { lastMessage.content += ' /think'; } else if (model.thinking) { lastMessage.content += ' /no_think'; } + const budgetChars = getPromptCharBudget(model); + const systemChars = messagesWithSystemPrompt[0].content.length; + if (context.length > 0) { - // Strip any nested in source chunks — otherwise a document - // containing the literal closing tag (e.g. an HTML export) would close - // the delimiter early and the rest would be parsed as user instruction. const safeContext = context .map((c) => c.replace(/<\/context>/gi, '')) .join(' '); - lastMessage.content = `${safeContext} - ${lastMessage.content} + + const userText = lastMessage.content; + const groundingHint = preferredSourceDocuments?.length + ? `\nAnswer only about the document(s) in the above. Ignore any document mentioned earlier in the chat that is not in it.` + : ''; + const wrap = (ctx: string) => `${ctx}${groundingHint} + ${userText} `; + + const availableForLast = Math.max(0, budgetChars - systemChars); + let finalContext = safeContext; + if (wrap(finalContext).length > availableForLast) { + const overhead = wrap('').length; + const room = Math.max(0, availableForLast - overhead); + const hardSlice = safeContext.slice(0, room); + const boundary = Math.max( + hardSlice.lastIndexOf('\n\n'), + hardSlice.lastIndexOf('\n ---') + ); + finalContext = boundary > 0 ? hardSlice.slice(0, boundary) : hardSlice; + } + lastMessage.content = wrap(finalContext); + } + + const mandatoryChars = systemChars + lastMessage.content.length; + let remainingChars = budgetChars - mandatoryChars; + const history = messagesWithSystemPrompt.slice(1, -1); + const keptReversed: ExecutorchMessage[] = []; + for (let i = history.length - 1; i >= 0; i--) { + const cost = history[i].content.length; + if (remainingChars - cost < 0) { + break; + } + remainingChars -= cost; + keptReversed.push(history[i]); } - return messagesWithSystemPrompt; + return [messagesWithSystemPrompt[0], ...keptReversed.reverse(), lastMessage]; }; diff --git a/utils/queryTerms.ts b/utils/queryTerms.ts new file mode 100644 index 00000000..6939c6bb --- /dev/null +++ b/utils/queryTerms.ts @@ -0,0 +1,120 @@ +// Query tokenization shared by hybrid retrieval and citation highlighting so +// both tokenize the prompt the same way. + +// Deliberately narrow per-language stopword lists (function words only, not a +// full corpus): they keep question words from becoming content terms during +// coverage scoring / highlighting. FTS keyword search leaves this to BM25's IDF. +const EN_STOPWORDS = [ + 'the', + 'and', + 'for', + 'are', + 'was', + 'were', + 'this', + 'that', + 'with', + 'from', + 'what', + 'which', + 'who', + 'whom', + 'how', + 'why', + 'when', + 'where', + 'does', + 'did', + 'has', + 'have', + 'had', + 'you', + 'your', + 'about', + 'into', + 'their', + 'they', + 'them', + 'can', + 'could', + 'would', + 'should', + 'will', + 'shall', + 'not', + 'but', + 'all', + 'any', +]; + +const PL_STOPWORDS = [ + 'jak', + 'jest', + 'czy', + 'oraz', + 'lub', + 'albo', + 'dla', + 'nie', + 'tak', + 'sie', + 'się', + 'jego', + 'jej', + 'tego', + 'tej', + 'ten', + 'tym', + 'moze', + 'może', + 'jakie', + 'jaki', + 'jaka', + 'gdzie', + 'kiedy', + 'dlaczego', + 'kto', + 'kogo', + 'ktore', + 'które', + 'ich', + 'przez', + 'przy', + 'aby', + 'zeby', + 'żeby', +]; + +const STOPWORDS = new Set([...EN_STOPWORDS, ...PL_STOPWORDS]); + +// Non-ASCII letters the tokenizer recognises — Polish only today. To add a +// language, extend this and add its stopword group above. +const PL_LETTERS = 'ąćęłńóśźż'; + +// Drop tokens shorter than this so bare numbers/short codes ("5", "L4") can't +// match unrelated passages; real identifiers ("219039") are longer. +const MIN_TERM_LENGTH = 3; + +export const TOKEN_PATTERN = new RegExp(`[a-z0-9${PL_LETTERS}]+`, 'gi'); + +export const extractQueryTerms = (query: string): Set => { + const terms = new Set(); + const matches = query.toLowerCase().match(TOKEN_PATTERN); + if (!matches) return terms; + + for (const token of matches) { + if (token.length >= MIN_TERM_LENGTH && !STOPWORDS.has(token)) { + terms.add(token); + } + } + + return terms; +}; + +const STEM_MIN_TERM_LENGTH = 5; +const WORD_PATTERN = new RegExp(`^[a-z${PL_LETTERS}]+$`, 'i'); + +export const stemPrefix = (term: string): string => + WORD_PATTERN.test(term) && term.length >= STEM_MIN_TERM_LENGTH + ? term.slice(0, Math.max(4, term.length - 2)) + : term; diff --git a/utils/retrieval.ts b/utils/retrieval.ts new file mode 100644 index 00000000..565b806a --- /dev/null +++ b/utils/retrieval.ts @@ -0,0 +1,332 @@ +import { OPSQLiteVectorStore } from '@react-native-rag/op-sqlite'; +import { type Scalar } from '@op-engineering/op-sqlite'; +import { LFMEmbeddings } from './lfmEmbeddings'; +import { type ContextChunk, sourceKey } from './contextUtils'; +import { + ATTACHMENT_RELEVANCE_BONUS, + CANDIDATE_POOL, + MAX_CHUNKS_PER_FILE, + MAX_RELEVANT_CHUNKS, + SEMANTIC_TOP_KEEP_FLOOR, + STRONG_SEMANTIC_THRESHOLD, +} from '../constants/retrieval'; + +type Candidate = { + id: string; + document?: string; + embedding: number[]; + documentId?: number; + name?: string; + similarity: number; +}; + +type HydratedRow = Omit; + +const resolveName = ( + name: string | undefined, + documentId: number | undefined, + sourceNamesById: Map +): string | undefined => + name || + (typeof documentId === 'number' + ? sourceNamesById.get(documentId) + : undefined); + +const hydrateChunksByIds = async ( + vectorStore: OPSQLiteVectorStore, + ids: string[] +): Promise => { + if (ids.length === 0) return []; + + const placeholders = ids.map(() => '?').join(', '); + const result = await vectorStore.db.execute( + `SELECT id, document, embedding, metadata FROM vectors WHERE id IN (${placeholders})`, + ids + ); + + return result.rows.map((row: Record) => { + const metadata = row.metadata + ? JSON.parse(row.metadata as string) + : undefined; + return { + id: String(row.id), + document: (row.document as string | null) ?? undefined, + embedding: Array.from(new Float32Array(row.embedding as ArrayBuffer)), + documentId: metadata?.documentId, + name: metadata?.name, + }; + }); +}; + +const parseChunkId = ( + id: string +): { documentId: number; chunkIndex: number } | null => { + const match = /^(\d+):(\d+)$/.exec(id); + return match + ? { documentId: Number(match[1]), chunkIndex: Number(match[2]) } + : null; +}; + +const NEIGHBOR_RADIUS = 1; + +const expandSelectedWithNeighbors = async ( + selectedIds: string[], + byId: Map, + vectorStore: OPSQLiteVectorStore, + sourceNamesById: Map +): Promise => { + type Group = { + documentId?: number; + name?: string; + indices: Map; + }; + const groupOrder: string[] = []; + const groups = new Map(); + const selectedSimilarity = new Map(); + + for (const id of selectedIds) { + const candidate = byId.get(id); + if (!candidate) continue; + selectedSimilarity.set(id, candidate.similarity); + + const key = sourceKey(candidate.documentId, candidate.name ?? ''); + let group = groups.get(key); + if (!group) { + group = { + documentId: candidate.documentId, + name: candidate.name, + indices: new Map(), + }; + groups.set(key, group); + groupOrder.push(key); + } + + const parsed = parseChunkId(id); + group.indices.set(id, parsed ? parsed.chunkIndex : -1); + if (!parsed) continue; + + for (let offset = -NEIGHBOR_RADIUS; offset <= NEIGHBOR_RADIUS; offset++) { + if (offset === 0) continue; + const neighborIndex = parsed.chunkIndex + offset; + if (neighborIndex < 0) continue; + const neighborId = `${parsed.documentId}:${neighborIndex}`; + if (!group.indices.has(neighborId)) { + group.indices.set(neighborId, neighborIndex); + } + } + } + + const allIds = groupOrder.flatMap((key) => [ + ...groups.get(key)!.indices.keys(), + ]); + const fetched = await hydrateChunksByIds( + vectorStore, + allIds.filter((id) => !byId.has(id)) + ); + + const chunkById = new Map(); + for (const id of allIds) { + const candidate = byId.get(id); + if (candidate) { + chunkById.set(id, { + id, + document: candidate.document, + embedding: candidate.embedding, + documentId: candidate.documentId, + name: candidate.name, + }); + } + } + for (const row of fetched) { + chunkById.set(row.id, { + ...row, + name: resolveName(row.name, row.documentId, sourceNamesById), + }); + } + + const result: ContextChunk[] = []; + for (const key of groupOrder) { + const group = groups.get(key)!; + + const seeds = [...group.indices.entries()] + .filter(([id]) => selectedSimilarity.has(id)) + .sort( + (a, b) => + (selectedSimilarity.get(b[0]) ?? 0) - + (selectedSimilarity.get(a[0]) ?? 0) || a[1] - b[1] + ); + + const emitted = new Set(); + const orderedIds: string[] = []; + for (const [seedId, seedIndex] of seeds) { + const parsed = parseChunkId(seedId); + const documentId = parsed?.documentId ?? group.documentId; + const windowIds = ( + parsed + ? [seedIndex - 1, seedIndex, seedIndex + 1].map( + (idx) => `${documentId}:${idx}` + ) + : [seedId] + ).filter((id) => group.indices.has(id)); + for (const id of windowIds) { + if (emitted.has(id) || !chunkById.get(id)?.document) continue; + emitted.add(id); + orderedIds.push(id); + } + } + + orderedIds.sort( + (a, b) => (group.indices.get(a) ?? 0) - (group.indices.get(b) ?? 0) + ); + + for (const id of orderedIds) { + const info = chunkById.get(id)!; + result.push({ + document: info.document, + similarity: selectedSimilarity.get(id) ?? 0, + metadata: { + documentId: info.documentId ?? group.documentId, + name: info.name ?? group.name, + }, + }); + } + } + + return result; +}; + +export type RetrieveParams = { + prompt: string; + enabledSourceIds: number[]; + vectorStore: OPSQLiteVectorStore; + sourceNamesById: Map; + embeddings?: LFMEmbeddings | null; + attachmentSourceIds?: number[]; +}; + +export const retrieve = async ({ + prompt, + enabledSourceIds, + vectorStore, + sourceNamesById, + embeddings, + attachmentSourceIds = [], +}: RetrieveParams): Promise => { + const attachmentSet = new Set(attachmentSourceIds); + const enabledSet = new Set(enabledSourceIds); + const isAttachment = (documentId?: number): boolean => + typeof documentId === 'number' && attachmentSet.has(documentId); + + let queryEmbedding: number[] | undefined; + if (embeddings) { + try { + queryEmbedding = await embeddings.embedQuery(prompt); + } catch (error) { + console.warn('Query embedding failed; using text-query fallback', error); + } + } + + const vectorResults = await vectorStore + .query({ + ...(queryEmbedding ? { queryEmbedding } : { queryText: prompt }), + predicate: (r) => enabledSet.has(r.metadata?.documentId), + nResults: CANDIDATE_POOL, + }) + .catch((error) => { + console.warn('Vector query failed; returning no chunks', error); + return [] as Awaited>; + }); + + const byId = new Map(); + for (const result of vectorResults) { + byId.set(result.id, { + id: result.id, + document: result.document, + embedding: result.embedding, + documentId: result.metadata?.documentId, + name: resolveName( + result.metadata?.name, + result.metadata?.documentId, + sourceNamesById + ), + similarity: result.similarity, + }); + } + + const candidates = [...byId.values()]; + const topSemantic = candidates.reduce( + (best, candidate) => + candidate.similarity > (best?.similarity ?? -Infinity) ? candidate : best, + null + ); + + const qualified = candidates.filter((candidate) => { + if (isAttachment(candidate.documentId)) return true; + if (candidate.similarity >= STRONG_SEMANTIC_THRESHOLD) return true; + return ( + candidate === topSemantic && + candidate.similarity >= SEMANTIC_TOP_KEEP_FLOOR + ); + }); + + if (qualified.length === 0) return []; + + const scored = qualified + .map((candidate) => ({ + id: candidate.id, + documentId: candidate.documentId, + relevance: + candidate.similarity + + (isAttachment(candidate.documentId) ? ATTACHMENT_RELEVANCE_BONUS : 0), + })) + .sort((a, b) => b.relevance - a.relevance); + + const distinctDocs = new Set(qualified.map((c) => c.documentId)).size; + const perFileCount = new Map(); + const selected: typeof scored = []; + for (const item of scored) { + if (selected.length >= MAX_RELEVANT_CHUNKS) break; + if (distinctDocs > 1) { + const count = perFileCount.get(item.documentId) ?? 0; + if (count >= MAX_CHUNKS_PER_FILE) continue; + perFileCount.set(item.documentId, count + 1); + } + selected.push(item); + } + + const ordered = attachmentSet.size + ? [...selected].sort( + (a, b) => + Number(isAttachment(b.documentId)) - + Number(isAttachment(a.documentId)) + ) + : selected; + + return expandSelectedWithNeighbors( + ordered.map((item) => item.id), + byId, + vectorStore, + sourceNamesById + ); +}; + +export type RetrieveOptions = Omit< + RetrieveParams, + 'prompt' | 'vectorStore' | 'embeddings' +>; + +export class Retriever { + constructor( + private vectorStore: OPSQLiteVectorStore, + private embeddings?: LFMEmbeddings | null + ) {} + + retrieve(query: string, options: RetrieveOptions): Promise { + return retrieve({ + prompt: query, + vectorStore: this.vectorStore, + embeddings: this.embeddings, + ...options, + }); + } +} diff --git a/utils/sourceLinkingBoundary.ts b/utils/sourceLinkingBoundary.ts new file mode 100644 index 00000000..3ebdd116 --- /dev/null +++ b/utils/sourceLinkingBoundary.ts @@ -0,0 +1,37 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; +import type { SQLiteDatabase } from 'expo-sqlite'; + +const SOURCE_LINKING_BOUNDARY_KEY = 'source_linking_boundary_message_id'; + +let cachedBoundary = 0; + +export const getSourceLinkingBoundary = (): number => cachedBoundary; + +export const setSourceLinkingBoundary = (value: number): void => { + cachedBoundary = Number.isFinite(value) ? value : 0; +}; + +export const initSourceLinkingBoundary = async ( + db: SQLiteDatabase +): Promise => { + try { + const stored = await AsyncStorage.getItem(SOURCE_LINKING_BOUNDARY_KEY); + if (stored != null) { + setSourceLinkingBoundary(Number(stored)); + return cachedBoundary; + } + + const row = await db.getFirstAsync<{ maxId: number | null }>( + `SELECT MAX(id) AS maxId FROM messages` + ); + setSourceLinkingBoundary(row?.maxId ?? 0); + await AsyncStorage.setItem( + SOURCE_LINKING_BOUNDARY_KEY, + String(cachedBoundary) + ); + } catch (error) { + console.warn('Failed to initialize source-linking boundary', error); + setSourceLinkingBoundary(0); + } + return cachedBoundary; +}; diff --git a/yarn.lock b/yarn.lock index 38557189..f87afd19 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5,50 +5,59 @@ __metadata: version: 6 cacheKey: 8 -"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.20.0, @babel/code-frame@npm:^7.24.7, @babel/code-frame@npm:^7.28.6, @babel/code-frame@npm:^7.29.0": - version: 7.29.0 - resolution: "@babel/code-frame@npm:7.29.0" +"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.20.0, @babel/code-frame@npm:^7.24.7, @babel/code-frame@npm:^7.29.0, @babel/code-frame@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/code-frame@npm:7.29.7" dependencies: - "@babel/helper-validator-identifier": ^7.28.5 + "@babel/helper-validator-identifier": ^7.29.7 js-tokens: ^4.0.0 picocolors: ^1.1.1 - checksum: 39f5b303757e4d63bbff8133e251094cd4f952b46e3fa9febc7368d907583911d6a1eded6090876dc1feeff5cf6e134fb19b706f8d58d26c5402cd50e5e1aeb2 + checksum: 21b12fe2356e36f6cc3cd8a3721f878bfeea80ce38356979a0518b47b3aafdcc0bd263da75ccc9d51c64d40b1b6df00e768ce2446acb0b7cbec0ae8f905663ad languageName: node linkType: hard -"@babel/compat-data@npm:^7.28.6": - version: 7.29.3 - resolution: "@babel/compat-data@npm:7.29.3" - checksum: 977192bab334f66bc8150026340a33ed318c1a7ce18a9323f4c1b86f8ed1d8645bfe5600242bf682717c05c46a4ff06225242207c1d599f4296914b9b4e3efb5 +"@babel/code-frame@npm:~7.10.4": + version: 7.10.4 + resolution: "@babel/code-frame@npm:7.10.4" + dependencies: + "@babel/highlight": ^7.10.4 + checksum: feb4543c8a509fe30f0f6e8d7aa84f82b41148b963b826cd330e34986f649a85cb63b2f13dd4effdf434ac555d16f14940b8ea5f4433297c2f5ff85486ded019 + languageName: node + linkType: hard + +"@babel/compat-data@npm:^7.28.6, @babel/compat-data@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/compat-data@npm:7.29.7" + checksum: 4424f8fb72f61657c8e811bdf7e5c69af212a15fe362711162b2e00b82f0e0588fb8259ecd9a70c5490562bf51d408b0cb92f277ead71a2390ddddfe7283b35d languageName: node linkType: hard "@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.13.16, @babel/core@npm:^7.20.0, @babel/core@npm:^7.21.3, @babel/core@npm:^7.23.9, @babel/core@npm:^7.24.4, @babel/core@npm:^7.25.2": - version: 7.29.0 - resolution: "@babel/core@npm:7.29.0" - dependencies: - "@babel/code-frame": ^7.29.0 - "@babel/generator": ^7.29.0 - "@babel/helper-compilation-targets": ^7.28.6 - "@babel/helper-module-transforms": ^7.28.6 - "@babel/helpers": ^7.28.6 - "@babel/parser": ^7.29.0 - "@babel/template": ^7.28.6 - "@babel/traverse": ^7.29.0 - "@babel/types": ^7.29.0 + version: 7.29.7 + resolution: "@babel/core@npm:7.29.7" + dependencies: + "@babel/code-frame": ^7.29.7 + "@babel/generator": ^7.29.7 + "@babel/helper-compilation-targets": ^7.29.7 + "@babel/helper-module-transforms": ^7.29.7 + "@babel/helpers": ^7.29.7 + "@babel/parser": ^7.29.7 + "@babel/template": ^7.29.7 + "@babel/traverse": ^7.29.7 + "@babel/types": ^7.29.7 "@jridgewell/remapping": ^2.3.5 convert-source-map: ^2.0.0 debug: ^4.1.0 gensync: ^1.0.0-beta.2 json5: ^2.2.3 semver: ^6.3.1 - checksum: 85e1df6e213382c46dee27bcd07ed9202fa108a85bb74eb37be656308fd949349171ad2aa17cc84cf0720c908dc9ea6309d25e64d2a7fcdaa63721ce0c67c10b + checksum: 95149e98ffde5b9d903459c284fbcf1f9ad8b3a833d69fdfe1aa7185821a102af1925324fe2892caf4b643b151c1dc948510d1e25a5e3c6c6c6f17f6712eb38e languageName: node linkType: hard "@babel/eslint-parser@npm:^7.25.1": - version: 7.28.6 - resolution: "@babel/eslint-parser@npm:7.28.6" + version: 7.29.7 + resolution: "@babel/eslint-parser@npm:7.29.7" dependencies: "@nicolo-ribaudo/eslint-scope-5-internals": 5.1.1-v1 eslint-visitor-keys: ^2.1.0 @@ -56,72 +65,72 @@ __metadata: peerDependencies: "@babel/core": ^7.11.0 eslint: ^7.5.0 || ^8.0.0 || ^9.0.0 - checksum: 6d789f16842c6f47a6a15f8159ef822e4bf75e8d15f85be2a813098ca4ba49703590ff2cdd56c78cc8816f5779b687cd6245ada4049c25e923e8e40132ace501 + checksum: def39180054d2bdf2ab6e119d50e2746a60531a410496e973c08f5f16daa167722d7c8be96d759837a79b3a763d3bf9b188a0c55671e08d0c1e73a81617e9aa1 languageName: node linkType: hard -"@babel/generator@npm:^7.20.5, @babel/generator@npm:^7.25.0, @babel/generator@npm:^7.29.0, @babel/generator@npm:^7.29.1, @babel/generator@npm:^7.7.2": - version: 7.29.1 - resolution: "@babel/generator@npm:7.29.1" +"@babel/generator@npm:^7.20.5, @babel/generator@npm:^7.25.0, @babel/generator@npm:^7.29.1, @babel/generator@npm:^7.29.7, @babel/generator@npm:^7.7.2": + version: 7.29.7 + resolution: "@babel/generator@npm:7.29.7" dependencies: - "@babel/parser": ^7.29.0 - "@babel/types": ^7.29.0 + "@babel/parser": ^7.29.7 + "@babel/types": ^7.29.7 "@jridgewell/gen-mapping": ^0.3.12 "@jridgewell/trace-mapping": ^0.3.28 jsesc: ^3.0.2 - checksum: d8e6863b2d04f684e65ad72731049ac7d754d3a3d1a67cdfc20807b109ba3180ed90d7ccef58ce5d38ded2eaeb71983a76c711eecb9b6266118262378f6c7226 + checksum: 6bb8f4dc0641dca19e81f5daab37ed1a1f5a78e4d702eb79a4275739f773975134e250090c182cf1eea35d9bd65e634b9d9babf66600ffdcf8ac62fa40f3b980 languageName: node linkType: hard -"@babel/helper-annotate-as-pure@npm:^7.27.1, @babel/helper-annotate-as-pure@npm:^7.27.3": - version: 7.27.3 - resolution: "@babel/helper-annotate-as-pure@npm:7.27.3" +"@babel/helper-annotate-as-pure@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-annotate-as-pure@npm:7.29.7" dependencies: - "@babel/types": ^7.27.3 - checksum: 63863a5c936ef82b546ca289c9d1b18fabfc24da5c4ee382830b124e2e79b68d626207febc8d4bffc720f50b2ee65691d7d12cc0308679dee2cd6bdc926b7190 + "@babel/types": ^7.29.7 + checksum: acd9e128de634a5144b5d622357d018fa616de45f64c74e42007c048dd15d0a0be213f4d5a2bf02307bdaddf053791b87900a99d183de828c08dc3b556329009 languageName: node linkType: hard -"@babel/helper-compilation-targets@npm:^7.27.1, @babel/helper-compilation-targets@npm:^7.28.6": - version: 7.28.6 - resolution: "@babel/helper-compilation-targets@npm:7.28.6" +"@babel/helper-compilation-targets@npm:^7.28.6, @babel/helper-compilation-targets@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-compilation-targets@npm:7.29.7" dependencies: - "@babel/compat-data": ^7.28.6 - "@babel/helper-validator-option": ^7.27.1 + "@babel/compat-data": ^7.29.7 + "@babel/helper-validator-option": ^7.29.7 browserslist: ^4.24.0 lru-cache: ^5.1.1 semver: ^6.3.1 - checksum: 8151e36b74eb1c5e414fe945c189436421f7bfa011884de5be3dd7fd77f12f1f733ff7c982581dfa0a49d8af724450243c2409427114b4a6cfeb8333259d001c + checksum: f60a943937f4eba0e671aa28551cb45569fd081c1e30a52ede167860475dc0417f3dbdf2a0fa3f086965595c7070aa76308da60cc0319860de05db4ed2a431f7 languageName: node linkType: hard -"@babel/helper-create-class-features-plugin@npm:^7.18.6, @babel/helper-create-class-features-plugin@npm:^7.28.6": - version: 7.29.3 - resolution: "@babel/helper-create-class-features-plugin@npm:7.29.3" +"@babel/helper-create-class-features-plugin@npm:^7.18.6, @babel/helper-create-class-features-plugin@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-create-class-features-plugin@npm:7.29.7" 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.28.6 - "@babel/helper-skip-transparent-expression-wrappers": ^7.27.1 - "@babel/traverse": ^7.29.0 + "@babel/helper-annotate-as-pure": ^7.29.7 + "@babel/helper-member-expression-to-functions": ^7.29.7 + "@babel/helper-optimise-call-expression": ^7.29.7 + "@babel/helper-replace-supers": ^7.29.7 + "@babel/helper-skip-transparent-expression-wrappers": ^7.29.7 + "@babel/traverse": ^7.29.7 semver: ^6.3.1 peerDependencies: "@babel/core": ^7.0.0 - checksum: ed7e755d83a59679ea9554b733e2f4b41fd1dd68ef4d8d3e5009930d8ff323f7aabdb8577abea0d98e5cc62a7ce5d14aca501e4446ccfe397c6c2fa3d8164f4b + checksum: c954e4bfe423a277cdcbad64344637cf696ddcd80085fce5f284b02a0c700af0d2d7b61468a06d9e296e948de60ece138921b65564aec023a9d9594f5d9fe18d languageName: node linkType: hard -"@babel/helper-create-regexp-features-plugin@npm:^7.27.1, @babel/helper-create-regexp-features-plugin@npm:^7.28.5": - version: 7.28.5 - resolution: "@babel/helper-create-regexp-features-plugin@npm:7.28.5" +"@babel/helper-create-regexp-features-plugin@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-create-regexp-features-plugin@npm:7.29.7" dependencies: - "@babel/helper-annotate-as-pure": ^7.27.3 + "@babel/helper-annotate-as-pure": ^7.29.7 regexpu-core: ^6.3.1 semver: ^6.3.1 peerDependencies: "@babel/core": ^7.0.0 - checksum: de202103e6ff8cd8da0d62eb269fcceb29857f3fa16173f0ff38188fd514e9ad4901aef1d590ff8ba25381644b42eaf70ad9ba91fda59fe7aa6a5e694cdde267 + checksum: 702a34db6c064a2c26675b717b3af88b8acaeb5341e2285792a873a67a16ec4cbe987ba72e055db198b2e03ead05600d8c9c0c1e7436708e95865bbfb3516026 languageName: node linkType: hard @@ -140,148 +149,160 @@ __metadata: languageName: node linkType: hard -"@babel/helper-globals@npm:^7.28.0": - version: 7.28.0 - resolution: "@babel/helper-globals@npm:7.28.0" - checksum: d8d7b91c12dad1ee747968af0cb73baf91053b2bcf78634da2c2c4991fb45ede9bd0c8f9b5f3254881242bc0921218fcb7c28ae885477c25177147e978ce4397 +"@babel/helper-globals@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-globals@npm:7.29.7" + checksum: 6deaf9846a415c7f110ac153e8c3d81e3543c9b685aa9fd9a59b4235f402e2b760129d3df49466daea9162f0cf73ff98a0ec7f180448a3449ca14ae5e1117b42 languageName: node linkType: hard -"@babel/helper-member-expression-to-functions@npm:^7.28.5": - version: 7.28.5 - resolution: "@babel/helper-member-expression-to-functions@npm:7.28.5" +"@babel/helper-member-expression-to-functions@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-member-expression-to-functions@npm:7.29.7" dependencies: - "@babel/traverse": ^7.28.5 - "@babel/types": ^7.28.5 - checksum: 447d385233bae2eea713df1785f819b5a5ca272950740da123c42d23f491045120f0fbbb5609c091f7a9bbd40f289a442846dde0cb1bf0c59440fa093690cf7c + "@babel/traverse": ^7.29.7 + "@babel/types": ^7.29.7 + checksum: 79d5f095b4bafadff3d1ec316d9f17ec85940fece957db62dd523b08e142da73c53180d1bce2fdc4d523e3889bb01b39bea70ad968b6e2f4dbdc66ebd1055b8a languageName: node linkType: hard -"@babel/helper-module-imports@npm:^7.25.9, @babel/helper-module-imports@npm:^7.28.6": - version: 7.28.6 - resolution: "@babel/helper-module-imports@npm:7.28.6" +"@babel/helper-module-imports@npm:^7.25.9, @babel/helper-module-imports@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-module-imports@npm:7.29.7" dependencies: - "@babel/traverse": ^7.28.6 - "@babel/types": ^7.28.6 - checksum: 437513aa029898b588a38f7991d7656c539b22f595207d85d0c407240c9e3f2aff8b9d0d7115fdedc91e7fdce4465100549a052024e2fba6a810bcbb7584296b + "@babel/traverse": ^7.29.7 + "@babel/types": ^7.29.7 + checksum: ad5a768fc9c162620b7f5b7645c6c2efee1e6f9df432bb79651888661a7e4cd91dac7192f3ddcfd6de4778a089e9fb30fafae768156aa73a07eeca425f64e849 languageName: node linkType: hard -"@babel/helper-module-transforms@npm:^7.28.6": - version: 7.28.6 - resolution: "@babel/helper-module-transforms@npm:7.28.6" +"@babel/helper-module-transforms@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-module-transforms@npm:7.29.7" dependencies: - "@babel/helper-module-imports": ^7.28.6 - "@babel/helper-validator-identifier": ^7.28.5 - "@babel/traverse": ^7.28.6 + "@babel/helper-module-imports": ^7.29.7 + "@babel/helper-validator-identifier": ^7.29.7 + "@babel/traverse": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0 - checksum: 522f7d1d08b5e2ccd4ec912aca879bd1506af78d1fb30f46e3e6b4bb69c6ae6ab4e379a879723844230d27dc6d04a55b03f5215cd3141b7a2b40bb4a02f71a9f + checksum: 484f6d02975d304f680d44e331d4b832d67e51917483985eab7b853664452b5a627366e7a9d421200ec772a123213a591e28b40636566afeac189411ba3f45a8 languageName: node linkType: hard -"@babel/helper-optimise-call-expression@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/helper-optimise-call-expression@npm:7.27.1" +"@babel/helper-optimise-call-expression@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-optimise-call-expression@npm:7.29.7" dependencies: - "@babel/types": ^7.27.1 - checksum: 0fb7ee824a384529d6b74f8a58279f9b56bfe3cce332168067dddeab2552d8eeb56dc8eaf86c04a3a09166a316cb92dfc79c4c623cd034ad4c563952c98b464f + "@babel/types": ^7.29.7 + checksum: 6b477e01b403fd48349336cb1d94722bff4fa54af2841b5fa950c557b796f4ecc14724052252ed1362ccfc23d1c09c54dc03e182fea59d3dc5bd69f8c626ba25 languageName: node linkType: hard -"@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.18.6, @babel/helper-plugin-utils@npm:^7.20.2, @babel/helper-plugin-utils@npm:^7.27.1, @babel/helper-plugin-utils@npm:^7.28.6, @babel/helper-plugin-utils@npm:^7.8.0": - version: 7.28.6 - resolution: "@babel/helper-plugin-utils@npm:7.28.6" - checksum: a0b4caab5e2180b215faa4d141ceac9e82fad9d446b8023eaeb8d82a6e62024726675b07fe8e616dd12f34e2bb59747e8d57aa8adab3e0717d1b8d691b118379 +"@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.18.6, @babel/helper-plugin-utils@npm:^7.20.2, @babel/helper-plugin-utils@npm:^7.28.6, @babel/helper-plugin-utils@npm:^7.29.7, @babel/helper-plugin-utils@npm:^7.8.0": + version: 7.29.7 + resolution: "@babel/helper-plugin-utils@npm:7.29.7" + checksum: b0a183abcc6670afa4861425fa428217d8ebadce062d5b43117919e8715f820080fd63bbfcf0e43c6e0e7d21a96b21f635c46dda80bdb0ce7e8a762ebee1d8d9 languageName: node linkType: hard -"@babel/helper-remap-async-to-generator@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/helper-remap-async-to-generator@npm:7.27.1" +"@babel/helper-remap-async-to-generator@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-remap-async-to-generator@npm:7.29.7" dependencies: - "@babel/helper-annotate-as-pure": ^7.27.1 - "@babel/helper-wrap-function": ^7.27.1 - "@babel/traverse": ^7.27.1 + "@babel/helper-annotate-as-pure": ^7.29.7 + "@babel/helper-wrap-function": ^7.29.7 + "@babel/traverse": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0 - checksum: 0747397ba013f87dbf575454a76c18210d61c7c9af0f697546b4bcac670b54ddc156330234407b397f0c948738c304c228e0223039bc45eab4fbf46966a5e8cc + checksum: 98338ad6e34ebb4be2dc23f8d9199d28d6d8ac6a2ce8b90fe9efdf3595b39748321528d9f2540ec0586a6e45f7c84f5f623fbf980c5efa7fa9ba7ce837ea4b20 languageName: node linkType: hard -"@babel/helper-replace-supers@npm:^7.28.6": - version: 7.28.6 - resolution: "@babel/helper-replace-supers@npm:7.28.6" +"@babel/helper-replace-supers@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-replace-supers@npm:7.29.7" dependencies: - "@babel/helper-member-expression-to-functions": ^7.28.5 - "@babel/helper-optimise-call-expression": ^7.27.1 - "@babel/traverse": ^7.28.6 + "@babel/helper-member-expression-to-functions": ^7.29.7 + "@babel/helper-optimise-call-expression": ^7.29.7 + "@babel/traverse": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0 - checksum: aa6530a52010883b6be88465e3b9e789509786a40203650a23a51c315f7442b196e5925fb8e2d66d1e3dc2c604cdc817bd8c5c170dbb322ab5ebc7486fd8a022 + checksum: f7eb9a6b035d9d45250c880eb09605f95998998e828ec90759ed45764fe0abeee583419969de8b8dee551163dff914c9fc6ace90c1d819c56c23146f8df525db languageName: node linkType: hard -"@babel/helper-skip-transparent-expression-wrappers@npm:^7.20.0, @babel/helper-skip-transparent-expression-wrappers@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/helper-skip-transparent-expression-wrappers@npm:7.27.1" +"@babel/helper-skip-transparent-expression-wrappers@npm:^7.20.0, @babel/helper-skip-transparent-expression-wrappers@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-skip-transparent-expression-wrappers@npm:7.29.7" dependencies: - "@babel/traverse": ^7.27.1 - "@babel/types": ^7.27.1 - checksum: 4f380c5d0e0769fa6942a468b0c2d7c8f0c438f941aaa88f785f8752c103631d0904c7b4e76207a3b0e6588b2dec376595370d92ca8f8f1b422c14a69aa146d4 + "@babel/traverse": ^7.29.7 + "@babel/types": ^7.29.7 + checksum: a5800bfcdca6cef7f6fe33ac02a0f05ff33da9746f97806553f249733f7ba8400290a17f3831d7faa5d91656f254ab749931f53c8a29f301d958d7dd00499637 languageName: node linkType: hard -"@babel/helper-string-parser@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/helper-string-parser@npm:7.27.1" - checksum: 0a8464adc4b39b138aedcb443b09f4005d86207d7126e5e079177e05c3116107d856ec08282b365e9a79a9872f40f4092a6127f8d74c8a01c1ef789dacfc25d6 +"@babel/helper-string-parser@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-string-parser@npm:7.29.7" + checksum: 4c229d2c2296b6c94439e87ecddf3a93cee3ffd2d4cee0b4c28079275bed3de4e02cd943e4cb06036d1d9407549c4e3423006b61a46215c482fce902ee02bc0b languageName: node linkType: hard -"@babel/helper-validator-identifier@npm:^7.28.5": - version: 7.28.5 - resolution: "@babel/helper-validator-identifier@npm:7.28.5" - checksum: 5a251a6848e9712aea0338f659a1a3bd334d26219d5511164544ca8ec20774f098c3a6661e9da65a0d085c745c00bb62c8fada38a62f08fa1f8053bc0aeb57e4 +"@babel/helper-validator-identifier@npm:^7.25.9, @babel/helper-validator-identifier@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-validator-identifier@npm:7.29.7" + checksum: cc7779e96fe9c9478c96ca4bf04fc338b95b501aa5abe78178307dea282d4a5dc23d443efb12dde8e8c5636d03dd00e443532e6ed7f15fa7977349af1f87ba4d languageName: node linkType: hard -"@babel/helper-validator-option@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/helper-validator-option@npm:7.27.1" - checksum: db73e6a308092531c629ee5de7f0d04390835b21a263be2644276cb27da2384b64676cab9f22cd8d8dbd854c92b1d7d56fc8517cf0070c35d1c14a8c828b0903 +"@babel/helper-validator-option@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-validator-option@npm:7.29.7" + checksum: aeb6aa966f59300d3cc2fea7c68e1dfd7ad011fc10e535c8e2b2de3094b27c859428dc7220f16420350f8b1cde99da120b673be04bcb0c2f37b56258c96bed58 languageName: node linkType: hard -"@babel/helper-wrap-function@npm:^7.27.1": - version: 7.28.6 - resolution: "@babel/helper-wrap-function@npm:7.28.6" +"@babel/helper-wrap-function@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-wrap-function@npm:7.29.7" dependencies: - "@babel/template": ^7.28.6 - "@babel/traverse": ^7.28.6 - "@babel/types": ^7.28.6 - checksum: 1281f45d55ff291711de7cf05b8132fc28b8d2b30c6c9cf8fce68669bbe318503ed485057d434efa1a4f91ab55d62bf8f3ecb0a889a9f81d357ad4614cd0fa6c + "@babel/template": ^7.29.7 + "@babel/traverse": ^7.29.7 + "@babel/types": ^7.29.7 + checksum: 2e6dfca94a10a3672a6b0ff337c80f27d7c66f3ea110969e833529a2d5bfbb5e53ab40abf6fad4657b8f810fcab2e3d56998f8da89bfe82a58267c5e8bc06e9a languageName: node linkType: hard -"@babel/helpers@npm:^7.28.6": - version: 7.29.2 - resolution: "@babel/helpers@npm:7.29.2" +"@babel/helpers@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helpers@npm:7.29.7" dependencies: - "@babel/template": ^7.28.6 - "@babel/types": ^7.29.0 - checksum: 2c8ce711a639ef334539d3bd48977f57493f71af99e13d3f685fe47b3bc32aa83dbc1380688e19d5df924d958f8f29072f3dcff8110257ba6399524907287189 + "@babel/template": ^7.29.7 + "@babel/types": ^7.29.7 + checksum: b5c4ed0ce5983c5599cd01b3948444b77ba2fa47bf6282a82afdcbe45eb8cc5b7986194e3920ef2760f533c6a775504e6b00a66960c0a3bc52909920b8433908 languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.13.16, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.23.9, @babel/parser@npm:^7.24.4, @babel/parser@npm:^7.25.3, @babel/parser@npm:^7.28.6, @babel/parser@npm:^7.29.0": - version: 7.29.3 - resolution: "@babel/parser@npm:7.29.3" +"@babel/highlight@npm:^7.10.4": + version: 7.25.9 + resolution: "@babel/highlight@npm:7.25.9" dependencies: - "@babel/types": ^7.29.0 + "@babel/helper-validator-identifier": ^7.25.9 + chalk: ^2.4.2 + js-tokens: ^4.0.0 + picocolors: ^1.0.0 + checksum: a6e0ac0a1c4bef7401915ca3442ab2b7ae4adf360262ca96b91396bfb9578abb28c316abf5e34460b780696db833b550238d9256bdaca60fade4ba7a67645064 + languageName: node + linkType: hard + +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.13.16, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.23.9, @babel/parser@npm:^7.24.4, @babel/parser@npm:^7.25.3, @babel/parser@npm:^7.29.0, @babel/parser@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/parser@npm:7.29.7" + dependencies: + "@babel/types": ^7.29.7 bin: parser: ./bin/babel-parser.js - checksum: 046f46996bf4053b6e29f8a7f420f9e0a2878593c1c9a9914a36faca23fc544a307c78a0101ba3ae98936ade68bdde686a83e1ab2b74c2ebb80dc4a9df48476d + checksum: 56f4c32a371004d3becd0a960a85a3bba4ec4df73d5e202aa3fe6473328b243162c6be9a510be1c12ed1825f5ce9ff1ea5cc357298631e8acf2e5b8da9f5a961 languageName: node linkType: hard @@ -298,26 +319,26 @@ __metadata: linkType: hard "@babel/plugin-proposal-decorators@npm:^7.12.9": - version: 7.29.0 - resolution: "@babel/plugin-proposal-decorators@npm:7.29.0" + version: 7.29.7 + resolution: "@babel/plugin-proposal-decorators@npm:7.29.7" dependencies: - "@babel/helper-create-class-features-plugin": ^7.28.6 - "@babel/helper-plugin-utils": ^7.28.6 - "@babel/plugin-syntax-decorators": ^7.28.6 + "@babel/helper-create-class-features-plugin": ^7.29.7 + "@babel/helper-plugin-utils": ^7.29.7 + "@babel/plugin-syntax-decorators": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: e2aa792078a50e49b1fb2c0759863d4dc688aff19441b7f624361bde3f220b7dee7ab0ea73928e0e67ca21c89557c847ffe9f6e2c4bdcc9fbf066c6b5b372c54 + checksum: 4a92b511afc80a6708c42de6e2875bd5b93f18413c6c2ee2e97af61c7a54ea3b04837ec8fcca0d92de2178d6dd0a589690bab9ff38e0053261170d38e332bd5e languageName: node linkType: hard "@babel/plugin-proposal-export-default-from@npm:^7.24.7": - version: 7.27.1 - resolution: "@babel/plugin-proposal-export-default-from@npm:7.27.1" + version: 7.29.7 + resolution: "@babel/plugin-proposal-export-default-from@npm:7.29.7" dependencies: - "@babel/helper-plugin-utils": ^7.27.1 + "@babel/helper-plugin-utils": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: cf9eb3c80bcee3ee82d28f1053db97fa6c6e4dea819f73df5a3cb9155d45efc29914e86353572eab36adfe691ca1573e6e2cddae4edbdd475253044575eb7a24 + checksum: 373ba179cb2a1ece4facd4a984aa43808ec046d3ad75404a0b2fdd92cd6c3ab41548856c055e63b1cc4a895ae23380298e36a219c454eb4c3ac9da1475a6cbd0 languageName: node linkType: hard @@ -390,14 +411,14 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-decorators@npm:^7.28.6": - version: 7.28.6 - resolution: "@babel/plugin-syntax-decorators@npm:7.28.6" +"@babel/plugin-syntax-decorators@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-syntax-decorators@npm:7.29.7" dependencies: - "@babel/helper-plugin-utils": ^7.28.6 + "@babel/helper-plugin-utils": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: f59a229e80398663c99519ab0785df389a802aedd6e0cfb6d37fa99a5802c22b270ff6273db7cf94cd8fcb9024feba11dcdbb0e907c5624e02c7fe5da056a91f + checksum: 3a49c261fb62a00f72626937eed23a0d293d10225362163d61909c83c91cfa759a8d498b6c7a8847592437a7d14ddc28407d7b8781ec9addac6a70980d41bb22 languageName: node linkType: hard @@ -413,35 +434,35 @@ __metadata: linkType: hard "@babel/plugin-syntax-export-default-from@npm:^7.24.7": - version: 7.28.6 - resolution: "@babel/plugin-syntax-export-default-from@npm:7.28.6" + version: 7.29.7 + resolution: "@babel/plugin-syntax-export-default-from@npm:7.29.7" dependencies: - "@babel/helper-plugin-utils": ^7.28.6 + "@babel/helper-plugin-utils": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 06330b90a4baf9edafe8a4e2e6520d548f83e178c1e832c1ad5018532052996331aedc8c3b4e6b0e51acaef75abe76e25ad3465d3d914658d65acec6908f202a + checksum: 33a0e0a629e58036038e51a48cf18e81a78e60c16827f82667dc6500df01bc52ee52e8f2b107200d3811526be0d1a10e5ccd53a803e6e9bdec79eedd0aa8d760 languageName: node linkType: hard -"@babel/plugin-syntax-flow@npm:^7.12.1, @babel/plugin-syntax-flow@npm:^7.27.1": - version: 7.28.6 - resolution: "@babel/plugin-syntax-flow@npm:7.28.6" +"@babel/plugin-syntax-flow@npm:^7.12.1, @babel/plugin-syntax-flow@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-syntax-flow@npm:7.29.7" dependencies: - "@babel/helper-plugin-utils": ^7.28.6 + "@babel/helper-plugin-utils": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 3dfe5d8168e400376e16937c92648142771b9ba0d9937b04ccdaacd06bf9d854170021b466106d4aa39ba6062b8b5b9b53efddae2c64ca133d4d6fafaa472909 + checksum: 3e03792bb20d4a0f2610df5d5af6c2ec8cbb5096a7576b24027eca60ac2b9e3a183d48255e4156fa94768322d82b13e77623f785ef556660de1c0efc5708e52a languageName: node linkType: hard "@babel/plugin-syntax-import-attributes@npm:^7.24.7": - version: 7.28.6 - resolution: "@babel/plugin-syntax-import-attributes@npm:7.28.6" + version: 7.29.7 + resolution: "@babel/plugin-syntax-import-attributes@npm:7.29.7" dependencies: - "@babel/helper-plugin-utils": ^7.28.6 + "@babel/helper-plugin-utils": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 6c8c6a5988dbb9799d6027360d1a5ba64faabf551f2ef11ba4eade0c62253b5c85d44ddc8eb643c74b9acb2bcaa664a950bd5de9a5d4aef291c4f2a48223bb4b + checksum: 9f47345d09aae16b7ab52ecaf541cde3e3ae1e57e3eb2d4088e062b29dfbd67db55d42d529840557583d66121e2a98788df7a455401cc6d635c8b7700a02efc9 languageName: node linkType: hard @@ -467,14 +488,14 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-jsx@npm:^7.27.1, @babel/plugin-syntax-jsx@npm:^7.28.6, @babel/plugin-syntax-jsx@npm:^7.7.2": - version: 7.28.6 - resolution: "@babel/plugin-syntax-jsx@npm:7.28.6" +"@babel/plugin-syntax-jsx@npm:^7.29.7, @babel/plugin-syntax-jsx@npm:^7.7.2": + version: 7.29.7 + resolution: "@babel/plugin-syntax-jsx@npm:7.29.7" dependencies: - "@babel/helper-plugin-utils": ^7.28.6 + "@babel/helper-plugin-utils": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 572e38f5c1bb4b8124300e7e3dd13e82ae84a21f90d3f0786c98cd05e63c78ca1f32d1cfe462dfbaf5e7d5102fa7cd8fd741dfe4f3afc2e01a3b2877dcc8c866 + checksum: 84150d27c553a1d3d921354437f6725ca1d63b49514c25591bfcaaafa6ea4d6c10715b66fe7245e4ad7ab7c6cf4b6e1de7373defd3df00877ab12638170d7772 languageName: node linkType: hard @@ -566,536 +587,536 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-typescript@npm:^7.28.6, @babel/plugin-syntax-typescript@npm:^7.7.2": - version: 7.28.6 - resolution: "@babel/plugin-syntax-typescript@npm:7.28.6" +"@babel/plugin-syntax-typescript@npm:^7.29.7, @babel/plugin-syntax-typescript@npm:^7.7.2": + version: 7.29.7 + resolution: "@babel/plugin-syntax-typescript@npm:7.29.7" dependencies: - "@babel/helper-plugin-utils": ^7.28.6 + "@babel/helper-plugin-utils": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 5c55f9c63bd36cf3d7e8db892294c8f85000f9c1526c3a1cc310d47d1e174f5c6f6605e5cc902c4636d885faba7a9f3d5e5edc6b35e4f3b1fd4c2d58d0304fa5 + checksum: ef454d2a7a6209dd4255361c072c94ab1293e7ad4b06e7e744d08bb308065d4d6544964eae9b2357c3b33d8d939f9e32d4aa95905bc464407cd8f7101dee4443 languageName: node linkType: hard "@babel/plugin-transform-arrow-functions@npm:^7.24.7, @babel/plugin-transform-arrow-functions@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-arrow-functions@npm:7.27.1" + version: 7.29.7 + resolution: "@babel/plugin-transform-arrow-functions@npm:7.29.7" dependencies: - "@babel/helper-plugin-utils": ^7.27.1 + "@babel/helper-plugin-utils": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 62c2cc0ae2093336b1aa1376741c5ed245c0987d9e4b4c5313da4a38155509a7098b5acce582b6781cc0699381420010da2e3086353344abe0a6a0ec38961eb7 + checksum: 0037fd7563c7c91cddb8ce104e270bc260190d29c7a297df65e4306471010b4343366de13fab4602cf8ee6c672d3b313b34a01b62f27a333cad16908c83368d8 languageName: node linkType: hard "@babel/plugin-transform-async-generator-functions@npm:^7.25.4": - version: 7.29.0 - resolution: "@babel/plugin-transform-async-generator-functions@npm:7.29.0" + version: 7.29.7 + resolution: "@babel/plugin-transform-async-generator-functions@npm:7.29.7" dependencies: - "@babel/helper-plugin-utils": ^7.28.6 - "@babel/helper-remap-async-to-generator": ^7.27.1 - "@babel/traverse": ^7.29.0 + "@babel/helper-plugin-utils": ^7.29.7 + "@babel/helper-remap-async-to-generator": ^7.29.7 + "@babel/traverse": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: bd549b54283034dd3e2f6c4b41b99a0caba0ddc8e9418490a611136ddb01e62235f14b233fcc172902fd1d18eec6e029245d22212566ea5cb5e24c7450d6005d + checksum: d8239f43b4051b12cd7a5581367bd2b13ac26235da39d79f182999879f010124341ac500f480140b0961a62ec451a0a963b421c509f9c1a306c313f10fc60451 languageName: node linkType: hard "@babel/plugin-transform-async-to-generator@npm:^7.24.7": - version: 7.28.6 - resolution: "@babel/plugin-transform-async-to-generator@npm:7.28.6" + version: 7.29.7 + resolution: "@babel/plugin-transform-async-to-generator@npm:7.29.7" dependencies: - "@babel/helper-module-imports": ^7.28.6 - "@babel/helper-plugin-utils": ^7.28.6 - "@babel/helper-remap-async-to-generator": ^7.27.1 + "@babel/helper-module-imports": ^7.29.7 + "@babel/helper-plugin-utils": ^7.29.7 + "@babel/helper-remap-async-to-generator": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: bca5774263ec01dd2bf71c74bbaf7baa183bf03576636b7826c3346be70c8c8cb15cff549112f2983c36885131a0afde6c443591278c281f733ee17f455aa9b1 + checksum: e24db9c4c69121daab25883f9a96e6849fa664c78c6bbcfb77fe0a0c6ab29b81ba39e8497985367463d3a88deac3b5bbe15dd1c5d0e5dd492cfccf8efdd27452 languageName: node linkType: hard "@babel/plugin-transform-block-scoping@npm:^7.25.0": - version: 7.28.6 - resolution: "@babel/plugin-transform-block-scoping@npm:7.28.6" + version: 7.29.7 + resolution: "@babel/plugin-transform-block-scoping@npm:7.29.7" dependencies: - "@babel/helper-plugin-utils": ^7.28.6 + "@babel/helper-plugin-utils": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: cb4f71ac4fc7b32c2e3cc167eb9e7a1a11562127d702e3b5093567750e9a4eb11a29ae5a917f62741bf9d5792bfe3022cbcdcc7bb927ddb6f627b6749a38c118 + checksum: a17c02f8dfcaf2c26c0c323aaa8e3a1616f2de3ea0a4947e16789bb64cb0f177deff388eb1390c100c82d7e6fecd4d583646bb9305fb3df3ca824b1bdbebdc8e languageName: node linkType: hard "@babel/plugin-transform-class-properties@npm:^7.25.4, @babel/plugin-transform-class-properties@npm:^7.27.1": - version: 7.28.6 - resolution: "@babel/plugin-transform-class-properties@npm:7.28.6" + version: 7.29.7 + resolution: "@babel/plugin-transform-class-properties@npm:7.29.7" dependencies: - "@babel/helper-create-class-features-plugin": ^7.28.6 - "@babel/helper-plugin-utils": ^7.28.6 + "@babel/helper-create-class-features-plugin": ^7.29.7 + "@babel/helper-plugin-utils": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 200f30d44b36a768fa3a8cf690db9e333996af2ad14d9fa1b4c91a427ed9302907873b219b4ce87517ca1014a810eb2e929a6a66be68473f72b546fc64d04fbc + checksum: 0cc5e7a882e29eead360f02ef79f6b2ec3b3813213b1513d8fdaa931d1d1361fccc92fbacc9b399e42495953d9d6fc722f283b5f3aa272fe016a0b5fe1e6a130 languageName: node linkType: hard "@babel/plugin-transform-class-static-block@npm:^7.27.1": - version: 7.28.6 - resolution: "@babel/plugin-transform-class-static-block@npm:7.28.6" + version: 7.29.7 + resolution: "@babel/plugin-transform-class-static-block@npm:7.29.7" dependencies: - "@babel/helper-create-class-features-plugin": ^7.28.6 - "@babel/helper-plugin-utils": ^7.28.6 + "@babel/helper-create-class-features-plugin": ^7.29.7 + "@babel/helper-plugin-utils": ^7.29.7 peerDependencies: "@babel/core": ^7.12.0 - checksum: 3db326156f73a0c0d1e2ea4d73e082b9ace2f6a9c965db1c2e51f3a186751b8b91bafb184d05e046bf970b50ecfde1f74862dd895f9a5ea0fad328369d74cfc4 + checksum: fe94eb94d8417de753a26f06a3c50780cdfc3f07e10bcd09dde2fe61dcd9a2714b83b1b2d36733328a3ad09b02e84fa06197f757644ffbcca1673e87c64ecb76 languageName: node linkType: hard "@babel/plugin-transform-classes@npm:^7.25.4, @babel/plugin-transform-classes@npm:^7.28.4": - version: 7.28.6 - resolution: "@babel/plugin-transform-classes@npm:7.28.6" + version: 7.29.7 + resolution: "@babel/plugin-transform-classes@npm:7.29.7" dependencies: - "@babel/helper-annotate-as-pure": ^7.27.3 - "@babel/helper-compilation-targets": ^7.28.6 - "@babel/helper-globals": ^7.28.0 - "@babel/helper-plugin-utils": ^7.28.6 - "@babel/helper-replace-supers": ^7.28.6 - "@babel/traverse": ^7.28.6 + "@babel/helper-annotate-as-pure": ^7.29.7 + "@babel/helper-compilation-targets": ^7.29.7 + "@babel/helper-globals": ^7.29.7 + "@babel/helper-plugin-utils": ^7.29.7 + "@babel/helper-replace-supers": ^7.29.7 + "@babel/traverse": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: bddeefbfd1966272e5da6a0844d68369a0f43c286816c8b379dfd576cf835b8bc652089ef337b0334ff3ae6c9652d56d8332b78a7d29176534265c39856e4822 + checksum: cc45ff5b5b063339131cd701266af90506db8640e56494de0c78f882da6317f057951bf6507c5b48a0278cf5dad21691baf2af05e24b8c26b0725d677088edbf languageName: node linkType: hard "@babel/plugin-transform-computed-properties@npm:^7.24.7": - version: 7.28.6 - resolution: "@babel/plugin-transform-computed-properties@npm:7.28.6" + version: 7.29.7 + resolution: "@babel/plugin-transform-computed-properties@npm:7.29.7" dependencies: - "@babel/helper-plugin-utils": ^7.28.6 - "@babel/template": ^7.28.6 + "@babel/helper-plugin-utils": ^7.29.7 + "@babel/template": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: fd1fcc55003a2584c7461bf214ae9e9fce370ad09339319e99e29e5e55a8a3bd485d10805b3d69636a738208761b3a5b0dafdd023534396be45a36409082b014 + checksum: ad945a48e6826c3b34f825e780ab33bf91e18c7924dd263e44bef8d8a6350636305b22af8f00793d3767482004651f4bfb9fed0c92f05c01b99ae80d79956e67 languageName: node linkType: hard -"@babel/plugin-transform-destructuring@npm:^7.24.8, @babel/plugin-transform-destructuring@npm:^7.28.5": - version: 7.28.5 - resolution: "@babel/plugin-transform-destructuring@npm:7.28.5" +"@babel/plugin-transform-destructuring@npm:^7.24.8, @babel/plugin-transform-destructuring@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-destructuring@npm:7.29.7" dependencies: - "@babel/helper-plugin-utils": ^7.27.1 - "@babel/traverse": ^7.28.5 + "@babel/helper-plugin-utils": ^7.29.7 + "@babel/traverse": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 74a06e55e715cfda0fdd8be53d2655d64dfdc28dffaede329d42548fd5b1449ad26a4ce43a24c3fd277b96f8b2010c7b3915afa8297911cda740cc5cc3a81f38 + checksum: 5f56c030beec2ae1640909eb5213fc655f0062046b28699d049cc8b00fb7d9aa1c5ba1dc1c7ee41c8bae97be778066f1f9ea2ecd8fff872a1e8f10fd3addf18c languageName: node linkType: hard "@babel/plugin-transform-export-namespace-from@npm:^7.25.9": - version: 7.27.1 - resolution: "@babel/plugin-transform-export-namespace-from@npm:7.27.1" + version: 7.29.7 + resolution: "@babel/plugin-transform-export-namespace-from@npm:7.29.7" dependencies: - "@babel/helper-plugin-utils": ^7.27.1 + "@babel/helper-plugin-utils": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 85082923eca317094f08f4953d8ea2a6558b3117826c0b740676983902b7236df1f4213ad844cb38c2dae104753dbe8f1cc51f01567835d476d32f5f544a4385 + checksum: d157d62b144d1626b801e557dcede914db33e78f3f4230f487e5709c21efd40648f7f3a47a44a7fce694ad9cd117d2ac3ed796da0102275fd02b4fdb317d906b languageName: node linkType: hard -"@babel/plugin-transform-flow-strip-types@npm:^7.25.2, @babel/plugin-transform-flow-strip-types@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-flow-strip-types@npm:7.27.1" +"@babel/plugin-transform-flow-strip-types@npm:^7.25.2, @babel/plugin-transform-flow-strip-types@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-flow-strip-types@npm:7.29.7" dependencies: - "@babel/helper-plugin-utils": ^7.27.1 - "@babel/plugin-syntax-flow": ^7.27.1 + "@babel/helper-plugin-utils": ^7.29.7 + "@babel/plugin-syntax-flow": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 0885028866fadefef35292d5a27f878d6a12b6f83778f8731481d4503b49c258507882a7de2aafda9b62d5f6350042f1a06355b998d5ed5e85d693bfcb77b939 + checksum: bc6e94f5398bb512b1a1d7a2bfb36dff9dd7d5150a3644e2ff4f361512b6be3c6a164828263fedbbc9eaa0015617b6c4e0e347117a0ae02acc184215cc27a6c2 languageName: node linkType: hard "@babel/plugin-transform-for-of@npm:^7.24.7": - version: 7.27.1 - resolution: "@babel/plugin-transform-for-of@npm:7.27.1" + version: 7.29.7 + resolution: "@babel/plugin-transform-for-of@npm:7.29.7" dependencies: - "@babel/helper-plugin-utils": ^7.27.1 - "@babel/helper-skip-transparent-expression-wrappers": ^7.27.1 + "@babel/helper-plugin-utils": ^7.29.7 + "@babel/helper-skip-transparent-expression-wrappers": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: c9224e08de5d80b2c834383d4359aa9e519db434291711434dd996a4f86b7b664ad67b45d65459b7ec11fa582e3e11a3c769b8a8ca71594bdd4e2f0503f84126 + checksum: 7641d5eb4ef268df2f7d8736691a565646f42c55fdc2c86a65ed027276415a19ac6cafab2fc386621894c5d3202baacc9f222424bda37fa2ab9a20d7fec921f9 languageName: node linkType: hard "@babel/plugin-transform-function-name@npm:^7.25.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-function-name@npm:7.27.1" + version: 7.29.7 + resolution: "@babel/plugin-transform-function-name@npm:7.29.7" dependencies: - "@babel/helper-compilation-targets": ^7.27.1 - "@babel/helper-plugin-utils": ^7.27.1 - "@babel/traverse": ^7.27.1 + "@babel/helper-compilation-targets": ^7.29.7 + "@babel/helper-plugin-utils": ^7.29.7 + "@babel/traverse": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 26a2a183c3c52a96495967420a64afc5a09f743a230272a131668abf23001e393afa6371e6f8e6c60f4182bea210ed31d1caf866452d91009c1daac345a52f23 + checksum: 87a6329442ef8085fbc160e659f64562f0f5b63be65403b8bbb8e18c67dd7d03b82fb2e1371fdde734e2f05b13a72f88ed8d8cb03807150063954b9604d082cf languageName: node linkType: hard "@babel/plugin-transform-literals@npm:^7.25.2": - version: 7.27.1 - resolution: "@babel/plugin-transform-literals@npm:7.27.1" + version: 7.29.7 + resolution: "@babel/plugin-transform-literals@npm:7.29.7" dependencies: - "@babel/helper-plugin-utils": ^7.27.1 + "@babel/helper-plugin-utils": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 0a76d12ab19f32dd139964aea7da48cecdb7de0b75e207e576f0f700121fe92367d788f328bf4fb44b8261a0f605c97b44e62ae61cddbb67b14e94c88b411f95 + checksum: aadb2b3fe85186c274a07d5486aeef9496ce374e534fbc7b54f77985c75513422d9acec4c532f67b027e939644d93a69c00505b8909e259184c3ee5c5c62c46b languageName: node linkType: hard "@babel/plugin-transform-logical-assignment-operators@npm:^7.24.7": - version: 7.28.6 - resolution: "@babel/plugin-transform-logical-assignment-operators@npm:7.28.6" + version: 7.29.7 + resolution: "@babel/plugin-transform-logical-assignment-operators@npm:7.29.7" dependencies: - "@babel/helper-plugin-utils": ^7.28.6 + "@babel/helper-plugin-utils": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 36095d5d1cfc680e95298b5389a16016da800ae3379b130dabf557e94652c47b06610407e9fa44aaa03e9b0a5aa7b4b93348123985d44a45e369bf5f3497d149 + checksum: 374d83dfbb2de5e339d2966487c0f0d766cd67422addbd0172104ff2788b60317858172a7ab2cb6ee7d5bffad72ce07120fec009a2ef3b35551013fce4908686 languageName: node linkType: hard -"@babel/plugin-transform-modules-commonjs@npm:^7.13.8, @babel/plugin-transform-modules-commonjs@npm:^7.24.8, @babel/plugin-transform-modules-commonjs@npm:^7.27.1": - version: 7.28.6 - resolution: "@babel/plugin-transform-modules-commonjs@npm:7.28.6" +"@babel/plugin-transform-modules-commonjs@npm:^7.13.8, @babel/plugin-transform-modules-commonjs@npm:^7.24.8, @babel/plugin-transform-modules-commonjs@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-modules-commonjs@npm:7.29.7" dependencies: - "@babel/helper-module-transforms": ^7.28.6 - "@babel/helper-plugin-utils": ^7.28.6 + "@babel/helper-module-transforms": ^7.29.7 + "@babel/helper-plugin-utils": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: b48cab26fda72894c7002a9c783befbc8a643d827c52bdcc5adf83e418ca93224a15aaf7ed2d1e6284627be55913696cfa2119242686cfa77a473bf79314df26 + checksum: e9d8102deca4c2f06507a8d071ca0e161f01cec0e108151142edb39995bd830e312d55d21fac0ceb390ac79a1fee5fb768b719413b8fc5adda5f06ecd8845cd6 languageName: node linkType: hard "@babel/plugin-transform-named-capturing-groups-regex@npm:^7.24.7": - version: 7.29.0 - resolution: "@babel/plugin-transform-named-capturing-groups-regex@npm:7.29.0" + version: 7.29.7 + resolution: "@babel/plugin-transform-named-capturing-groups-regex@npm:7.29.7" dependencies: - "@babel/helper-create-regexp-features-plugin": ^7.28.5 - "@babel/helper-plugin-utils": ^7.28.6 + "@babel/helper-create-regexp-features-plugin": ^7.29.7 + "@babel/helper-plugin-utils": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0 - checksum: ed8c27699ca82a6c01cbfd39f3de16b90cfea4f8146a358057f76df290d308a66a8bd2e6734e6a87f68c18576e15d2d70548a84cd474d26fdf256c3f5ae44d8c + checksum: 3a481be133e9ca5d25570b5ed62daae323a51663bacf30fed0d1980e912047ebd34d6326533182db625fc0bbd086d1a23ed68ebb6108e7a68a8650c228afc084 languageName: node linkType: hard "@babel/plugin-transform-nullish-coalescing-operator@npm:^7.24.7, @babel/plugin-transform-nullish-coalescing-operator@npm:^7.27.1": - version: 7.28.6 - resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.28.6" + version: 7.29.7 + resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.29.7" dependencies: - "@babel/helper-plugin-utils": ^7.28.6 + "@babel/helper-plugin-utils": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 1cdd3ca48a8fffa13dbb9949748d3dd2183cf24110cd55d702da4549205611fc12978b49886be809ec1929ff6304ac4eecc747a33dca2484f9dc655928ab5a89 + checksum: f486e737ddcec3a88e2e0dc004c28e15156dd255f3b2d944903f3d75666257c96ee7ebf57d80d2cf42eda2bee497db8134a26d657ddc3defcc34b9583ecbd119 languageName: node linkType: hard "@babel/plugin-transform-numeric-separator@npm:^7.24.7": - version: 7.28.6 - resolution: "@babel/plugin-transform-numeric-separator@npm:7.28.6" + version: 7.29.7 + resolution: "@babel/plugin-transform-numeric-separator@npm:7.29.7" dependencies: - "@babel/helper-plugin-utils": ^7.28.6 + "@babel/helper-plugin-utils": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 4b5ca60e481e22f0842761a3badca17376a230b5a7e5482338604eb95836c2d0c9c9bde53bdc5c2de1c6a12ae6c12de7464d098bf74b0943f85905ca358f0b68 + checksum: 6177df9e1a8a190bf4a360f8cbcfa614b70ededba61201bd0a38929770b6d00a8a54a19f8fda82cf60688d9bab938427a9fe5dae0a9e8cd8ed79c88a3b2dbcd7 languageName: node linkType: hard "@babel/plugin-transform-object-rest-spread@npm:^7.24.7": - version: 7.28.6 - resolution: "@babel/plugin-transform-object-rest-spread@npm:7.28.6" + version: 7.29.7 + resolution: "@babel/plugin-transform-object-rest-spread@npm:7.29.7" dependencies: - "@babel/helper-compilation-targets": ^7.28.6 - "@babel/helper-plugin-utils": ^7.28.6 - "@babel/plugin-transform-destructuring": ^7.28.5 - "@babel/plugin-transform-parameters": ^7.27.7 - "@babel/traverse": ^7.28.6 + "@babel/helper-compilation-targets": ^7.29.7 + "@babel/helper-plugin-utils": ^7.29.7 + "@babel/plugin-transform-destructuring": ^7.29.7 + "@babel/plugin-transform-parameters": ^7.29.7 + "@babel/traverse": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: ab85b1321f86db91aba22ad9d8e6ab65448c983214998012229f5302468527d27b908ad6b14755991c317e35d2f54ec8459a2a094a755999651fe0ac9bd2e9a6 + checksum: e09bcc8800cf374962ab98bce5ccceb900266278a3a1e4a68391abec440fdf7371c8059f9091f407a1b167f3acf624c34c9b92188674fdcb7797e3f02509216b languageName: node linkType: hard "@babel/plugin-transform-optional-catch-binding@npm:^7.24.7": - version: 7.28.6 - resolution: "@babel/plugin-transform-optional-catch-binding@npm:7.28.6" + version: 7.29.7 + resolution: "@babel/plugin-transform-optional-catch-binding@npm:7.29.7" dependencies: - "@babel/helper-plugin-utils": ^7.28.6 + "@babel/helper-plugin-utils": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: ee24a17defec056eb9ef01824d7e4a1f65d531af6b4b79acfd0bcb95ce0b47926e80c61897f36f8c01ce733b069c9acdb1c9ce5ec07a729d0dbf9e8d859fe992 + checksum: 4b6e41e1dc5dbd02cfe0b96214130cca5fd3bd879551fc82188bb3d9a2782af9bab50f2140af9ff946a8ee23b9478ee42810641fb99aa3e033884d7c6103d138 languageName: node linkType: hard "@babel/plugin-transform-optional-chaining@npm:^7.24.8, @babel/plugin-transform-optional-chaining@npm:^7.27.1": - version: 7.28.6 - resolution: "@babel/plugin-transform-optional-chaining@npm:7.28.6" + version: 7.29.7 + resolution: "@babel/plugin-transform-optional-chaining@npm:7.29.7" dependencies: - "@babel/helper-plugin-utils": ^7.28.6 - "@babel/helper-skip-transparent-expression-wrappers": ^7.27.1 + "@babel/helper-plugin-utils": ^7.29.7 + "@babel/helper-skip-transparent-expression-wrappers": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: a40dbe709671a436bb69e14524805e10af81b44c422e4fc5dc905cb91adb92d650c9d266c3c2c0da0d410dea89ce784995d4118b7ab6a7544f4923e61590b386 + checksum: 6255d259bb0143f7938278ebb382aba22311c260919d3f08476509c76335a111b479b3ad7363e86de064f87af85ca4d7f03f08195f0646c525ea70fb587c0a2d languageName: node linkType: hard -"@babel/plugin-transform-parameters@npm:^7.24.7, @babel/plugin-transform-parameters@npm:^7.27.7": - version: 7.27.7 - resolution: "@babel/plugin-transform-parameters@npm:7.27.7" +"@babel/plugin-transform-parameters@npm:^7.24.7, @babel/plugin-transform-parameters@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-parameters@npm:7.29.7" dependencies: - "@babel/helper-plugin-utils": ^7.27.1 + "@babel/helper-plugin-utils": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: d51f195e1d6ac5d9fce583e9a70a5bfe403e62386e5eb06db9fbc6533f895a98ff7e7c3dcaa311a8e6fa7a9794466e81cdabcba6af9f59d787fb767bfe7868b4 + checksum: 615cdd72dc2d08051e6d4e7f0539661e40029257a047ce15c5da5f4394a372a53f6f6d6aece72ad15ccf0590e448c9a742f8576564321c9dcc16262ec6197c9b languageName: node linkType: hard "@babel/plugin-transform-private-methods@npm:^7.24.7": - version: 7.28.6 - resolution: "@babel/plugin-transform-private-methods@npm:7.28.6" + version: 7.29.7 + resolution: "@babel/plugin-transform-private-methods@npm:7.29.7" dependencies: - "@babel/helper-create-class-features-plugin": ^7.28.6 - "@babel/helper-plugin-utils": ^7.28.6 + "@babel/helper-create-class-features-plugin": ^7.29.7 + "@babel/helper-plugin-utils": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: b80179b28f6a165674d0b0d6c6349b13a01dd282b18f56933423c0a33c23fc0626c8f011f859fc20737d021fe966eb8474a5233e4596401482e9ee7fb00e2aa2 + checksum: 5055af2b86a95acbf6bd1a14256edf439ba4f214707f6aa9f6a29d1168c882e5709853c4ff225f55575f88d3a58effbed952d25c5cd70f93785106541f992cdd languageName: node linkType: hard "@babel/plugin-transform-private-property-in-object@npm:^7.24.7": - version: 7.28.6 - resolution: "@babel/plugin-transform-private-property-in-object@npm:7.28.6" + version: 7.29.7 + resolution: "@babel/plugin-transform-private-property-in-object@npm:7.29.7" 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-annotate-as-pure": ^7.29.7 + "@babel/helper-create-class-features-plugin": ^7.29.7 + "@babel/helper-plugin-utils": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 32a935e44872e90607851be5bc2cd3365f29c0e0e3853ef3e2b6a7da4d08c647379bf2f2dc4f14a9064d7d72e2cf75da85e55baeeec1ffc25cf6088fe24422f7 + checksum: 068ff9e35e103b269c707d16f50384646295a6613c5abfdebea24f0430bb18ea4ef40a299e1dcc915bb22f65e2217c3428c20bcd4a3fe5f8ac60ec212835646d languageName: node linkType: hard -"@babel/plugin-transform-react-display-name@npm:^7.24.7, @babel/plugin-transform-react-display-name@npm:^7.28.0": - version: 7.28.0 - resolution: "@babel/plugin-transform-react-display-name@npm:7.28.0" +"@babel/plugin-transform-react-display-name@npm:^7.24.7, @babel/plugin-transform-react-display-name@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-react-display-name@npm:7.29.7" dependencies: - "@babel/helper-plugin-utils": ^7.27.1 + "@babel/helper-plugin-utils": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 268b1a9192974439d17949e170b01cac2a2aa003c844e2fe3b8361146f42f66487178cffdfa8ce862aa9e6c814bc37f879a70300cb3f067815d15fa6aad04e6d + checksum: ded95cce1816f800db43e8f4e1f7fbb928091bf036438617b8ca7e9ce776079606045a0ca482904bfeff801c4fc726de633153843c441ef31980b8c41ace04c9 languageName: node linkType: hard -"@babel/plugin-transform-react-jsx-development@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-react-jsx-development@npm:7.27.1" +"@babel/plugin-transform-react-jsx-development@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-react-jsx-development@npm:7.29.7" dependencies: - "@babel/plugin-transform-react-jsx": ^7.27.1 + "@babel/plugin-transform-react-jsx": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: b88865d5b8c018992f2332da939faa15c4d4a864c9435a5937beaff3fe43781432cc42e0a5d5631098e0bd4066fc33f5fa72203b388b074c3545fe7aaa21e474 + checksum: c535bb5ee09e07839a422f7a8e55849cd30525af57021888eceb84d33391290f6250207319bb4fbb4d4cbdcdb894b2a2b963f0769a3e95536370159b4b505855 languageName: node linkType: hard "@babel/plugin-transform-react-jsx-self@npm:^7.24.7": - version: 7.27.1 - resolution: "@babel/plugin-transform-react-jsx-self@npm:7.27.1" + version: 7.29.7 + resolution: "@babel/plugin-transform-react-jsx-self@npm:7.29.7" dependencies: - "@babel/helper-plugin-utils": ^7.27.1 + "@babel/helper-plugin-utils": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 72cbae66a58c6c36f7e12e8ed79f292192d858dd4bb00e9e89d8b695e4c5cb6ef48eec84bffff421a5db93fd10412c581f1cccdb00264065df76f121995bdb68 + checksum: 779cde890f36a0160585a357f0850951d9e18d72e960099e32544420252e983b54bfe4a7c81c39b1668ad588231771c97e6b9e59056b21e5cda0953f26db1286 languageName: node linkType: hard "@babel/plugin-transform-react-jsx-source@npm:^7.24.7": - version: 7.27.1 - resolution: "@babel/plugin-transform-react-jsx-source@npm:7.27.1" + version: 7.29.7 + resolution: "@babel/plugin-transform-react-jsx-source@npm:7.29.7" dependencies: - "@babel/helper-plugin-utils": ^7.27.1 + "@babel/helper-plugin-utils": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: e2843362adb53692be5ee9fa07a386d2d8883daad2063a3575b3c373fc14cdf4ea7978c67a183cb631b4c9c8d77b2f48c24c088f8e65cc3600cb8e97d72a7161 + checksum: 286641d64bfd1d91eb8fcc3a6a5c48cc7b8e04268c79f3ee9902addc723652a4aa1d967278208d3b0ef03db381853d68eb25ae609e5a305421ff3d3fd5f3cb77 languageName: node linkType: hard -"@babel/plugin-transform-react-jsx@npm:^7.25.2, @babel/plugin-transform-react-jsx@npm:^7.27.1": - version: 7.28.6 - resolution: "@babel/plugin-transform-react-jsx@npm:7.28.6" +"@babel/plugin-transform-react-jsx@npm:^7.25.2, @babel/plugin-transform-react-jsx@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-react-jsx@npm:7.29.7" dependencies: - "@babel/helper-annotate-as-pure": ^7.27.3 - "@babel/helper-module-imports": ^7.28.6 - "@babel/helper-plugin-utils": ^7.28.6 - "@babel/plugin-syntax-jsx": ^7.28.6 - "@babel/types": ^7.28.6 + "@babel/helper-annotate-as-pure": ^7.29.7 + "@babel/helper-module-imports": ^7.29.7 + "@babel/helper-plugin-utils": ^7.29.7 + "@babel/plugin-syntax-jsx": ^7.29.7 + "@babel/types": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: e7d093b5ed6c06563e801d44d1212b451445d7600756efd7b8b8e6db4585c27fa8145176dcb3350968c59381af6c566dae9b6dc97ec15d2837493b238904d1c2 + checksum: d50e5d6f12051c688280b118fc0cdc49f617f7c1f2c41b25733b606aa9a14d2dc84bc544163d115226b9d2cde9f147f49568350b9d100cb47988e5e76cf495c7 languageName: node linkType: hard -"@babel/plugin-transform-react-pure-annotations@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-react-pure-annotations@npm:7.27.1" +"@babel/plugin-transform-react-pure-annotations@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-react-pure-annotations@npm:7.29.7" dependencies: - "@babel/helper-annotate-as-pure": ^7.27.1 - "@babel/helper-plugin-utils": ^7.27.1 + "@babel/helper-annotate-as-pure": ^7.29.7 + "@babel/helper-plugin-utils": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: a6f591c5e85a1ab0685d4a25afe591fe8d11dc0b73c677cf9560ff8d540d036a1cce9efcb729fc9092def4d854dc304ffdc063a89a9247900b69c516bf971a4c + checksum: 7b6bc9e9db06f2c40685b4f0a043af17a98a8bee833831aa28f70c89876dc649fdd682ae572445143b53fe091258964107bae9d3583480eb1c4f1d9c22780b38 languageName: node linkType: hard "@babel/plugin-transform-regenerator@npm:^7.24.7": - version: 7.29.0 - resolution: "@babel/plugin-transform-regenerator@npm:7.29.0" + version: 7.29.7 + resolution: "@babel/plugin-transform-regenerator@npm:7.29.7" dependencies: - "@babel/helper-plugin-utils": ^7.28.6 + "@babel/helper-plugin-utils": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: f48bc814f11239f2bfe010a6e29d5ac2443e7b1d8004e7c022effa111b743491127acf8644cfef475edb86b91f123829585867bc13762652aabd9b85ed6ce61e + checksum: 1a136712ba92693402d7fafb96624d1a833e888cd59029a84ed24d8ff083304a3f1d101d8d5013d0400229041c8a1e4ddf08027268f3c876ea226e86734acd25 languageName: node linkType: hard "@babel/plugin-transform-runtime@npm:^7.24.7": - version: 7.29.0 - resolution: "@babel/plugin-transform-runtime@npm:7.29.0" + version: 7.29.7 + resolution: "@babel/plugin-transform-runtime@npm:7.29.7" dependencies: - "@babel/helper-module-imports": ^7.28.6 - "@babel/helper-plugin-utils": ^7.28.6 + "@babel/helper-module-imports": ^7.29.7 + "@babel/helper-plugin-utils": ^7.29.7 babel-plugin-polyfill-corejs2: ^0.4.14 babel-plugin-polyfill-corejs3: ^0.13.0 babel-plugin-polyfill-regenerator: ^0.6.5 semver: ^6.3.1 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 1d3a5951396469372d954538fb188479b86afa8e02ca541da8f123250aaed8df65573b68f67087f4b15a5ccff9abc3a3fdb1d9a07fbb85bfcb807168d7364a37 + checksum: 7256db11b985bf58fdca84c16a742035f75adfbf49c68b14bbb3232843b5af5d8c9695c90d038a00ae22ce4f5b6b95002ba5833ce2bae9893378caa7e60da900 languageName: node linkType: hard "@babel/plugin-transform-shorthand-properties@npm:^7.24.7, @babel/plugin-transform-shorthand-properties@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-shorthand-properties@npm:7.27.1" + version: 7.29.7 + resolution: "@babel/plugin-transform-shorthand-properties@npm:7.29.7" dependencies: - "@babel/helper-plugin-utils": ^7.27.1 + "@babel/helper-plugin-utils": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: fbba6e2aef0b69681acb68202aa249c0598e470cc0853d7ff5bd0171fd6a7ec31d77cfabcce9df6360fc8349eded7e4a65218c32551bd3fc0caaa1ac899ac6d4 + checksum: c57ef27853f334a6147da9aa00f8a8f4c3a1c217eb2efa73cba2e118edda10754fa23cec2c0c7f7408279ad28fef92c1f663dfec137a7503813331569c3e02f9 languageName: node linkType: hard "@babel/plugin-transform-spread@npm:^7.24.7": - version: 7.28.6 - resolution: "@babel/plugin-transform-spread@npm:7.28.6" + version: 7.29.7 + resolution: "@babel/plugin-transform-spread@npm:7.29.7" dependencies: - "@babel/helper-plugin-utils": ^7.28.6 - "@babel/helper-skip-transparent-expression-wrappers": ^7.27.1 + "@babel/helper-plugin-utils": ^7.29.7 + "@babel/helper-skip-transparent-expression-wrappers": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: e4782578904df68f7d2b3e865f20701c71d6aba0027c4794c1dc08a2f805a12892a078dab483714552398a689ad4ff6786cdf4e088b073452aee7db67e37a09c + checksum: eb25f24d9a5cac163fea91b5872ff2ddb892083624284a6e8a1fa8dfc4c4c6f6230f3b478405eb846df895a3f96631ac19e8c169af33012aafaa144e6192d5d3 languageName: node linkType: hard "@babel/plugin-transform-sticky-regex@npm:^7.24.7": - version: 7.27.1 - resolution: "@babel/plugin-transform-sticky-regex@npm:7.27.1" + version: 7.29.7 + resolution: "@babel/plugin-transform-sticky-regex@npm:7.29.7" dependencies: - "@babel/helper-plugin-utils": ^7.27.1 + "@babel/helper-plugin-utils": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: e1414a502efba92c7974681767e365a8cda6c5e9e5f33472a9eaa0ce2e75cea0a9bef881ff8dda37c7810ad902f98d3c00ead92a3ac3b73a79d011df85b5a189 + checksum: 16b570c0270a59c2a29b2118c00e463882e9dda49b51a17dde3ae6b2886995d49f4bf2161a4c743ad1bca39d2aeb3ac963400e095682e105c7f751e6a2156c28 languageName: node linkType: hard "@babel/plugin-transform-template-literals@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-template-literals@npm:7.27.1" + version: 7.29.7 + resolution: "@babel/plugin-transform-template-literals@npm:7.29.7" dependencies: - "@babel/helper-plugin-utils": ^7.27.1 + "@babel/helper-plugin-utils": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 93aad782503b691faef7c0893372d5243df3219b07f1f22cfc32c104af6a2e7acd6102c128439eab15336d048f1b214ca134b87b0630d8cd568bf447f78b25ce + checksum: d1014dab020f0f802089de17bba82d929eda6ac87fde5f58fb9763885b8d645ce63fc1df97055c06a12d95a8788334a93b559fcaf1da6d7777a191e5f9c5646e languageName: node linkType: hard -"@babel/plugin-transform-typescript@npm:^7.25.2, @babel/plugin-transform-typescript@npm:^7.28.5": - version: 7.28.6 - resolution: "@babel/plugin-transform-typescript@npm:7.28.6" +"@babel/plugin-transform-typescript@npm:^7.25.2, @babel/plugin-transform-typescript@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-typescript@npm:7.29.7" 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 + "@babel/helper-annotate-as-pure": ^7.29.7 + "@babel/helper-create-class-features-plugin": ^7.29.7 + "@babel/helper-plugin-utils": ^7.29.7 + "@babel/helper-skip-transparent-expression-wrappers": ^7.29.7 + "@babel/plugin-syntax-typescript": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 029add39a37e4a1960a43c3a109680462f631bc63cc8457ea65add2cce3271c9fd4d6a1782177c65ea5f77731e2f8e2bc65a9aec9cc826346ba540ecd0b97e5a + checksum: e95bce53fa2add836eec5ef5221e260cfa4ab889a146f7ba5e29cbd42bfe3183cb94e40b49bfb0d14a75f233982723903d3efad0f528b835ce771e38bd365440 languageName: node linkType: hard "@babel/plugin-transform-unicode-regex@npm:^7.24.7, @babel/plugin-transform-unicode-regex@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-unicode-regex@npm:7.27.1" + version: 7.29.7 + resolution: "@babel/plugin-transform-unicode-regex@npm:7.29.7" dependencies: - "@babel/helper-create-regexp-features-plugin": ^7.27.1 - "@babel/helper-plugin-utils": ^7.27.1 + "@babel/helper-create-regexp-features-plugin": ^7.29.7 + "@babel/helper-plugin-utils": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: a34d89a2b75fb78e66d97c3dc90d4877f7e31f43316b52176f95a5dee20e9bb56ecf158eafc42a001676ddf7b393d9e67650bad6b32f5405780f25fb83cd68e3 + checksum: 1ade0672ae5bbbf2ec1ea0a8de1b5d804ae414283215620097ab21cf7f05dae8916f5b0548a18c6f080ec17135018f5edd2d38f8fa9ca052af570cab5c712786 languageName: node linkType: hard "@babel/preset-flow@npm:^7.13.13": - version: 7.27.1 - resolution: "@babel/preset-flow@npm:7.27.1" + version: 7.29.7 + resolution: "@babel/preset-flow@npm:7.29.7" dependencies: - "@babel/helper-plugin-utils": ^7.27.1 - "@babel/helper-validator-option": ^7.27.1 - "@babel/plugin-transform-flow-strip-types": ^7.27.1 + "@babel/helper-plugin-utils": ^7.29.7 + "@babel/helper-validator-option": ^7.29.7 + "@babel/plugin-transform-flow-strip-types": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: f3f25b390debf72a6ff0170a2d5198aea344ba96f05eaca0bae2c7072119706fd46321604d89646bda1842527cfc6eab8828a983ec90149218d2120b9cd26596 + checksum: 562fe8494d7e8e3a894cb9b1213acdc418b42d54f0fe316e97192bb235c8dfe7cde58d351057b9f7896bdc5ff42b605863c00cf6df952d5789cf3d865ab723ba languageName: node linkType: hard "@babel/preset-react@npm:^7.22.15": - version: 7.28.5 - resolution: "@babel/preset-react@npm:7.28.5" + version: 7.29.7 + resolution: "@babel/preset-react@npm:7.29.7" dependencies: - "@babel/helper-plugin-utils": ^7.27.1 - "@babel/helper-validator-option": ^7.27.1 - "@babel/plugin-transform-react-display-name": ^7.28.0 - "@babel/plugin-transform-react-jsx": ^7.27.1 - "@babel/plugin-transform-react-jsx-development": ^7.27.1 - "@babel/plugin-transform-react-pure-annotations": ^7.27.1 + "@babel/helper-plugin-utils": ^7.29.7 + "@babel/helper-validator-option": ^7.29.7 + "@babel/plugin-transform-react-display-name": ^7.29.7 + "@babel/plugin-transform-react-jsx": ^7.29.7 + "@babel/plugin-transform-react-jsx-development": ^7.29.7 + "@babel/plugin-transform-react-pure-annotations": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 13bc1fe4dde0a29d00323e46749e5beb457844507cb3afa2fefbd85d283c2d4836f9e4a780be735de58a44c505870476dc2838f1f8faf9d6f056481e65f1a0fb + checksum: ec9d4d418df3825674ef807020b90a93d6bf6b786a723a59e673c3a32e31927b2f8a50071a17a50fff632f41eda8933e4196974130f2a5cdeb1a040fb2b62b76 languageName: node linkType: hard "@babel/preset-typescript@npm:^7.13.0, @babel/preset-typescript@npm:^7.23.0, @babel/preset-typescript@npm:^7.27.1": - version: 7.28.5 - resolution: "@babel/preset-typescript@npm:7.28.5" + version: 7.29.7 + resolution: "@babel/preset-typescript@npm:7.29.7" 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 + "@babel/helper-plugin-utils": ^7.29.7 + "@babel/helper-validator-option": ^7.29.7 + "@babel/plugin-syntax-jsx": ^7.29.7 + "@babel/plugin-transform-modules-commonjs": ^7.29.7 + "@babel/plugin-transform-typescript": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 22f889835d9db1e627846e71ca2f02e2d24e2eb9ebcf9845b3b1d37bd3a53787967bafabbbcb342f06aaf7627399a7102ba6ca18f9a0e17066c865d680d2ceb9 + checksum: f2f58cbbbdb84f6b27e20c7835fbd1e2474e7e6075c97a6609c606139bc782c995f0c5eb5b64f5b613997f8a463f3b4cea10cd1f0d706a66bf1aa41fea666725 languageName: node linkType: hard "@babel/register@npm:^7.13.16": - version: 7.29.3 - resolution: "@babel/register@npm:7.29.3" + version: 7.29.7 + resolution: "@babel/register@npm:7.29.7" dependencies: clone-deep: ^4.0.1 find-cache-dir: ^2.0.0 @@ -1104,50 +1125,50 @@ __metadata: source-map-support: ^0.5.16 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 6f84a5aa5df539dd7801affbf66b2ec8cd3125dac09151c9d309f932798783c3d2f3036ff84f090c47d587fc089dcd3489cfdd8affa1936347f75240c0b41b0a + checksum: 19c4babef8210106f28b9868f007be6d0dae4f16bb2be2f221ff836ede906599516d713de2c9f7ab32dcb4005f8c1d9e5b0eba3fe1840c132c8425c57ff02dbc languageName: node linkType: hard "@babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.25.0": - version: 7.29.2 - resolution: "@babel/runtime@npm:7.29.2" - checksum: d5548d1165de8995f8afc93a5694b8625409be16cd1f2250ac13e331335858ddac3cb9fd278e6c43956a130101a2203f09417938a1a96f9fb70f02b4b4172e1d + version: 7.29.7 + resolution: "@babel/runtime@npm:7.29.7" + checksum: bc311855c6dbf5356030979584ca0f8b6cee39fe058175b02e85a73e246fc76ae30248b0d40e70c2652101faeb43279468ed2d086a670673eb9783c1fee1df2b languageName: node linkType: hard -"@babel/template@npm:^7.25.0, @babel/template@npm:^7.28.6, @babel/template@npm:^7.3.3": - version: 7.28.6 - resolution: "@babel/template@npm:7.28.6" +"@babel/template@npm:^7.25.0, @babel/template@npm:^7.28.6, @babel/template@npm:^7.29.7, @babel/template@npm:^7.3.3": + version: 7.29.7 + resolution: "@babel/template@npm:7.29.7" dependencies: - "@babel/code-frame": ^7.28.6 - "@babel/parser": ^7.28.6 - "@babel/types": ^7.28.6 - checksum: 8ab6383053e226025d9491a6e795293f2140482d14f60c1244bece6bf53610ed1e251d5e164de66adab765629881c7d9416e1e540c716541d2fd0f8f36a013d7 + "@babel/code-frame": ^7.29.7 + "@babel/parser": ^7.29.7 + "@babel/types": ^7.29.7 + checksum: 521eb6a1fd4735074ca8dac0d70810860a80edf3bf78105851571993cd13701a2041987e7398ccc9376eb6235ea1258bf494ccaccf3b67fa98dbe954154a2e93 languageName: node linkType: hard -"@babel/traverse--for-generate-function-map@npm:@babel/traverse@^7.25.3, @babel/traverse@npm:^7.25.3, @babel/traverse@npm:^7.27.1, @babel/traverse@npm:^7.28.5, @babel/traverse@npm:^7.28.6, @babel/traverse@npm:^7.29.0": - version: 7.29.0 - resolution: "@babel/traverse@npm:7.29.0" +"@babel/traverse--for-generate-function-map@npm:@babel/traverse@^7.25.3, @babel/traverse@npm:^7.25.3, @babel/traverse@npm:^7.29.0, @babel/traverse@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/traverse@npm:7.29.7" 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 + "@babel/code-frame": ^7.29.7 + "@babel/generator": ^7.29.7 + "@babel/helper-globals": ^7.29.7 + "@babel/parser": ^7.29.7 + "@babel/template": ^7.29.7 + "@babel/types": ^7.29.7 debug: ^4.3.1 - checksum: fbb5085aa525b5d4ecd9fe2f5885d88413fff6ad9c0fac244c37f96069b6d3af9ce825750cd16af1d97d26fa3d354b38dbbdb5f31430e0d99ed89660ab65430e + checksum: 6c4508fd2a308a6a41fbf40bd2590bccfdc3903de51c640a928c49e810220b9e27323a083cda604d44a27449b57265a701b549de01f479611390863734b4fd38 languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.3, @babel/types@npm:^7.25.2, @babel/types@npm:^7.26.0, @babel/types@npm:^7.27.1, @babel/types@npm:^7.27.3, @babel/types@npm:^7.28.2, @babel/types@npm:^7.28.5, @babel/types@npm:^7.28.6, @babel/types@npm:^7.29.0, @babel/types@npm:^7.3.3": - version: 7.29.0 - resolution: "@babel/types@npm:7.29.0" +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.3, @babel/types@npm:^7.25.2, @babel/types@npm:^7.26.0, @babel/types@npm:^7.28.2, @babel/types@npm:^7.29.0, @babel/types@npm:^7.29.7, @babel/types@npm:^7.3.3": + version: 7.29.7 + resolution: "@babel/types@npm:7.29.7" dependencies: - "@babel/helper-string-parser": ^7.27.1 - "@babel/helper-validator-identifier": ^7.28.5 - checksum: 83f190438e94c22b2574aaeef7501830311ef266eaabfb06523409f64e2fe855e522951607085d71cad286719adef14e1ba37b671f334a7cd25b0f8506a01e0b + "@babel/helper-string-parser": ^7.29.7 + "@babel/helper-validator-identifier": ^7.29.7 + checksum: 71c46837d22c5c63a5ed571f3b68b4261ecabfc3d4be7251b336ccbd26bc52752ae68ae6d16d24ea512311b78b6f54efdfb00dde87a81a4dc3d19e4a45f05b20 languageName: node linkType: hard @@ -1166,15 +1187,15 @@ __metadata: linkType: hard "@dr.pogodin/react-native-fs@npm:^2.35.1": - version: 2.38.2 - resolution: "@dr.pogodin/react-native-fs@npm:2.38.2" + version: 2.39.1 + resolution: "@dr.pogodin/react-native-fs@npm:2.39.1" dependencies: buffer: ^6.0.3 http-status-codes: ^2.3.0 peerDependencies: react: "*" react-native: "*" - checksum: cc8b9b9817effdde062b9e56d12836d666ff78e5907c69ee817af74d960e83936f0aa457ff105d2e4786f743cb7a6ec5179cf3e020ce65994e77350cd9cca615 + checksum: 584c41db4e88c5dbe32bdc9d2948196bb95692d711fa8adc18c4be09da500dfb06ed7de4574856d129e273319485770408dbcf5cbc66fe07e475a3bf5cc9bfbf languageName: node linkType: hard @@ -1295,33 +1316,33 @@ __metadata: linkType: hard "@expo-google-fonts/material-symbols@npm:^0.4.1": - version: 0.4.34 - resolution: "@expo-google-fonts/material-symbols@npm:0.4.34" - checksum: 552de769e3b21e32346ec401ffee8b697aed0b7f1208ebf2be4f82b2993d5a8f69bb7c7f096a471ea06cdbf32558817c37d08fed1e0964efa643699a9590bac9 + version: 0.4.38 + resolution: "@expo-google-fonts/material-symbols@npm:0.4.38" + checksum: 7a32503acd64173f86acc4f9108709b054e6fcb5255951a56621decbc99aae4c3b29f559c0d070957236f53d823bc68dab3be1678364309ae4193778c88627aa languageName: node linkType: hard -"@expo/cli@npm:55.0.28": - version: 55.0.28 - resolution: "@expo/cli@npm:55.0.28" +"@expo/cli@npm:55.0.33": + version: 55.0.33 + resolution: "@expo/cli@npm:55.0.33" dependencies: "@expo/code-signing-certificates": ^0.0.6 - "@expo/config": ~55.0.15 - "@expo/config-plugins": ~55.0.8 + "@expo/config": ~55.0.18 + "@expo/config-plugins": ~55.0.10 "@expo/devcert": ^1.2.1 - "@expo/env": ~2.1.1 - "@expo/image-utils": ^0.8.13 - "@expo/json-file": ^10.0.13 - "@expo/log-box": 55.0.11 + "@expo/env": ~2.1.2 + "@expo/image-utils": ^0.8.14 + "@expo/json-file": ^10.0.15 + "@expo/log-box": 55.0.12 "@expo/metro": ~55.1.1 - "@expo/metro-config": ~55.0.19 - "@expo/osascript": ^2.4.2 - "@expo/package-manager": ^1.10.4 - "@expo/plist": ^0.5.2 - "@expo/prebuild-config": ^55.0.16 - "@expo/require-utils": ^55.0.4 - "@expo/router-server": ^55.0.15 - "@expo/schema-utils": ^55.0.3 + "@expo/metro-config": ~55.0.24 + "@expo/osascript": ^2.4.4 + "@expo/package-manager": ^1.10.6 + "@expo/plist": ^0.5.4 + "@expo/prebuild-config": ^55.0.19 + "@expo/require-utils": ^55.0.5 + "@expo/router-server": ^55.0.18 + "@expo/schema-utils": ^55.0.4 "@expo/spawn-async": ^1.7.2 "@expo/ws-tunnel": ^1.0.1 "@expo/xcpretty": ^4.4.0 @@ -1336,8 +1357,8 @@ __metadata: compression: ^1.7.4 connect: ^3.7.0 debug: ^4.3.4 - dnssd-advertise: ^1.1.4 - expo-server: ^55.0.8 + dnssd-advertise: ^1.1.6 + expo-server: ^55.0.11 fetch-nodeshim: ^0.4.10 getenv: ^2.0.0 glob: ^13.0.0 @@ -1373,7 +1394,7 @@ __metadata: optional: true bin: expo-internal: build/bin/cli - checksum: 109ddf50a639313e96fc01df91383c977b6ae41e1e8b70105a7bf9689b12f7fce77b6f9ba413131437e0cbda40837448fb40ffc7d43814629d793f2bcd16c373 + checksum: 1804798cf9f15ca48a9e20265713a8deb6b0d91409782e4941d245717b05430c0cb72626699bf0f686ece5eecc8e7d57cca22f16d4d572543c6dfb0ab08b82f3 languageName: node linkType: hard @@ -1386,13 +1407,13 @@ __metadata: languageName: node linkType: hard -"@expo/config-plugins@npm:^55.0.8, @expo/config-plugins@npm:~55.0.8": - version: 55.0.8 - resolution: "@expo/config-plugins@npm:55.0.8" +"@expo/config-plugins@npm:^55.0.10, @expo/config-plugins@npm:~55.0.10": + version: 55.0.10 + resolution: "@expo/config-plugins@npm:55.0.10" dependencies: "@expo/config-types": ^55.0.5 - "@expo/json-file": ~10.0.13 - "@expo/plist": ^0.5.2 + "@expo/json-file": ~10.0.15 + "@expo/plist": ^0.5.4 "@expo/sdk-runtime-versions": ^1.0.0 chalk: ^4.1.2 debug: ^4.3.5 @@ -1403,7 +1424,7 @@ __metadata: slugify: ^1.6.6 xcode: ^3.0.1 xml2js: 0.6.0 - checksum: 3a328e288cabcce508967106bdd766e05c4af5adb9bf279be7723d208127b363057a1c2820eddcd43265387dbb794d86c568ca2ff28851195f1974218da8c0c0 + checksum: aba139ca12c357a6e7e50f64af55224189fcf48e1ccfdcdc3143fed98dd30ea2bfa3736f3425018584c35e07065ba1dea31afdf42a97d25804a11ea31a8b5cb7 languageName: node linkType: hard @@ -1414,21 +1435,21 @@ __metadata: languageName: node linkType: hard -"@expo/config@npm:~55.0.15": - version: 55.0.15 - resolution: "@expo/config@npm:55.0.15" +"@expo/config@npm:~55.0.18": + version: 55.0.18 + resolution: "@expo/config@npm:55.0.18" dependencies: - "@expo/config-plugins": ~55.0.8 + "@expo/config-plugins": ~55.0.10 "@expo/config-types": ^55.0.5 - "@expo/json-file": ^10.0.13 - "@expo/require-utils": ^55.0.4 + "@expo/json-file": ^10.0.15 + "@expo/require-utils": ^55.0.5 deepmerge: ^4.3.1 getenv: ^2.0.0 glob: ^13.0.0 resolve-workspace-root: ^2.0.0 semver: ^7.6.0 slugify: ^1.3.4 - checksum: d60d9d262baa7e8e66da4c1d50a90fdb5d63bb68046d70ca155b74f0ac3d62c639fdc5b77abde9bdcb0d9f92cb5a010a84ec738f021c56353b94e2d247af64a3 + checksum: 93fb543509b1c17b005531799413462bbf86891026df97ddb5b6699d3e701b97bd25ec016bc42de2cc629a0ae9a062a939728503a41844f5a9da90a8065369b0 languageName: node linkType: hard @@ -1442,9 +1463,9 @@ __metadata: languageName: node linkType: hard -"@expo/devtools@npm:55.0.2": - version: 55.0.2 - resolution: "@expo/devtools@npm:55.0.2" +"@expo/devtools@npm:55.0.3": + version: 55.0.3 + resolution: "@expo/devtools@npm:55.0.3" dependencies: chalk: ^4.1.2 peerDependencies: @@ -1455,37 +1476,48 @@ __metadata: optional: true react-native: optional: true - checksum: 21e3a6c965b09682b2e223980fe6661ab56767593cc66b28a2815a14574c31296ebce360f5280d6051ca4374b003654d32b64015da8549aa1b355f22e0cda9e1 + checksum: d1fc7ef4934e1fd77d7c7697aaf467c4d8dd4cc1e6753e1d3b7764ad58c8bfd7fd0dc41a845e4a629f7290b917fb6412776bd479ddf17c9d276daf35f9b1941c languageName: node linkType: hard -"@expo/dom-webview@npm:^55.0.5": - version: 55.0.5 - resolution: "@expo/dom-webview@npm:55.0.5" +"@expo/dom-webview@npm:^55.0.6": + version: 55.0.6 + resolution: "@expo/dom-webview@npm:55.0.6" peerDependencies: expo: "*" react: "*" react-native: "*" - checksum: bcb1fba4fe0ea7c36951f1d8eabf28175746993749207b712624f75701384453e881463438562d74ff6e32adc6e2ffcff4a7c382f73f112c01102712d481283f + checksum: 956810d50cb3e647f8d60d93b48c9d1db28598d966eb543d6453e86d5094948d8d58d5af3087df963b2a43fe6dc58062bf702dd6e5d1a5eb883977056ef24b57 languageName: node linkType: hard -"@expo/env@npm:^2.0.11, @expo/env@npm:~2.1.1": - version: 2.1.1 - resolution: "@expo/env@npm:2.1.1" +"@expo/env@npm:^2.1.2": + version: 2.4.1 + resolution: "@expo/env@npm:2.4.1" dependencies: chalk: ^4.0.0 debug: ^4.3.4 getenv: ^2.0.0 - checksum: 03a7bb255880a8c5b71b8a9eff45b6f58630b5c641cbbfb90fb7b8330c88deec5649e1613ee0e8489ad508796065415868e9fe9a95c52e8d279408188f379b61 + checksum: 1fd9b0285bfc67c2314643684da9ca30bf2c539ed71df190c69d980a2d3c3582bb283493246ce89842354a7a1fad395dc60812d87d6d51a592355a71453c8937 languageName: node linkType: hard -"@expo/fingerprint@npm:0.16.6": - version: 0.16.6 - resolution: "@expo/fingerprint@npm:0.16.6" +"@expo/env@npm:~2.1.2": + version: 2.1.2 + resolution: "@expo/env@npm:2.1.2" dependencies: - "@expo/env": ^2.0.11 + chalk: ^4.0.0 + debug: ^4.3.4 + getenv: ^2.0.0 + checksum: 35101fe1cb5c11ef01ad9260787a945cd8ecb98eb3c78ce06ae8b1806f76fe2aeb22b95f0ad364bef5314f59e64dba2750810b5dd2b0fb2350d860f744fa357d + languageName: node + linkType: hard + +"@expo/fingerprint@npm:0.16.7": + version: 0.16.7 + resolution: "@expo/fingerprint@npm:0.16.7" + dependencies: + "@expo/env": ^2.1.2 "@expo/spawn-async": ^1.7.2 arg: ^5.0.2 chalk: ^4.1.2 @@ -1498,71 +1530,91 @@ __metadata: semver: ^7.6.0 bin: fingerprint: bin/cli.js - checksum: e83ecac8b8ee074f0e57b801619c8874445a4320d468f88c65d65e5db09e8cf137d866a0a3c8f142a342c0fdfe9c8c31ae3d9efafb2ce15d2022383d29e8c34d + checksum: 8dfbcc216bfdb47ff605058f9c3b28eacb75677ce9bf0b0adc90915a9a5fd60519780a93bc3bb19198c8b786c9f1593f3d8bddc34037cba6061efbcc25e13d52 languageName: node linkType: hard -"@expo/image-utils@npm:^0.8.13": - version: 0.8.13 - resolution: "@expo/image-utils@npm:0.8.13" +"@expo/image-utils@npm:^0.8.14": + version: 0.8.14 + resolution: "@expo/image-utils@npm:0.8.14" dependencies: - "@expo/require-utils": ^55.0.4 + "@expo/require-utils": ^55.0.5 "@expo/spawn-async": ^1.7.2 chalk: ^4.0.0 getenv: ^2.0.0 jimp-compact: 0.16.1 parse-png: ^2.1.0 semver: ^7.6.0 - checksum: 290641d0075120872e9a32e24f341ab9e0e78f04493e4f430b612d0a6aea885d0aacf18b8e5331a6519eed557bee666540134a069afc8092d7752c7d78f6f767 + checksum: 2c4e5c00c1a8b2d7c8c155c481b8684e5425d15ce2ebbc9503102dc09de27f45c6796b58581435345197932040b94d62311bfabfc5b461a1dd5a65361b7ebd9f languageName: node linkType: hard -"@expo/json-file@npm:^10.0.13, @expo/json-file@npm:~10.0.13": - version: 10.0.13 - resolution: "@expo/json-file@npm:10.0.13" +"@expo/json-file@npm:^10.0.15": + version: 10.2.0 + resolution: "@expo/json-file@npm:10.2.0" dependencies: "@babel/code-frame": ^7.20.0 json5: ^2.2.3 - checksum: 84fe31e4c9b94b978b3f3c9d8a3b3e8e1c5c785dce3f6c151d0a5c0bb3d1ba7337a87fb566ac10bf7c50d4384bd88cd7ed9d738c05115ef523583b5fd38dc49f + checksum: 89a0c8861024dce24f6b3b576e51d993ad89bb8c26bd70f885b190e52afc22f3b57209fab46cf50850edae7d99033f5ed826ed12b869c3606fdd072583546699 languageName: node linkType: hard -"@expo/local-build-cache-provider@npm:55.0.11": - version: 55.0.11 - resolution: "@expo/local-build-cache-provider@npm:55.0.11" +"@expo/json-file@npm:^11.0.0": + version: 11.0.0 + resolution: "@expo/json-file@npm:11.0.0" dependencies: - "@expo/config": ~55.0.15 + "@babel/code-frame": ^7.20.0 + json5: ^2.2.3 + checksum: 0426299412a1581377c33e6c69e47a5e557a8d715f813f5cb14d79836a4fd477973a05a375a010dcbefa5ae30d68c15f7e5d5773f2e2257f3cc6a3cb01b61a7f + languageName: node + linkType: hard + +"@expo/json-file@npm:~10.0.15": + version: 10.0.16 + resolution: "@expo/json-file@npm:10.0.16" + dependencies: + "@babel/code-frame": ~7.10.4 + json5: ^2.2.3 + checksum: d51baaa1427f399a90340a1780d6138e986c5fca2cec06b06b008fc85348ea731b5baa207f08bd12bc2198af57f7541286b4250a9bb62296899c6a619a3b4889 + languageName: node + linkType: hard + +"@expo/local-build-cache-provider@npm:55.0.14": + version: 55.0.14 + resolution: "@expo/local-build-cache-provider@npm:55.0.14" + dependencies: + "@expo/config": ~55.0.18 chalk: ^4.1.2 - checksum: 1fcb73bdfbf429160853de92397e77d03d243bc1c5fd1d70a3a88a1b5b3213d42ef72c59e95662bca304a105b267536b1d97441015f45f1c03afe2643a60a5bb + checksum: 77d94dbf166078dc972896e76f516960d6a77b6e3b415fe8fb75b1463e7660453a3063d1685042b28146b93d0f7e1675329826c966343a9d859615e3ebbf65cf languageName: node linkType: hard -"@expo/log-box@npm:55.0.11": - version: 55.0.11 - resolution: "@expo/log-box@npm:55.0.11" +"@expo/log-box@npm:55.0.12": + version: 55.0.12 + resolution: "@expo/log-box@npm:55.0.12" dependencies: - "@expo/dom-webview": ^55.0.5 + "@expo/dom-webview": ^55.0.6 anser: ^1.4.9 stacktrace-parser: ^0.1.10 peerDependencies: - "@expo/dom-webview": ^55.0.5 + "@expo/dom-webview": ^55.0.6 expo: "*" react: "*" react-native: "*" - checksum: da7cd20c09c97212a2b11e51ec27c204d932f7d7fd32e19f4427cce98424e1d470e3b9ef05eca1c754ffa0f98eb74b1b13af632087452b4578e6105377de0138 + checksum: 17d603749834767487a71e359638466c85f4d62527745e3086fc280cfda465082b063cc415b82c7c33548f6ad801e84acd05075daa27b5e5cba0dbed45f351e2 languageName: node linkType: hard -"@expo/metro-config@npm:55.0.19, @expo/metro-config@npm:~55.0.19": - version: 55.0.19 - resolution: "@expo/metro-config@npm:55.0.19" +"@expo/metro-config@npm:55.0.24, @expo/metro-config@npm:~55.0.24": + version: 55.0.24 + resolution: "@expo/metro-config@npm:55.0.24" dependencies: "@babel/code-frame": ^7.20.0 "@babel/core": ^7.20.0 "@babel/generator": ^7.20.5 - "@expo/config": ~55.0.15 - "@expo/env": ~2.1.1 - "@expo/json-file": ~10.0.13 + "@expo/config": ~55.0.18 + "@expo/env": ~2.1.2 + "@expo/json-file": ~10.0.15 "@expo/metro": ~55.1.1 "@expo/spawn-async": ^1.7.2 browserslist: ^4.25.0 @@ -1574,22 +1626,22 @@ __metadata: jsc-safe-url: ^0.2.4 lightningcss: ^1.30.1 picomatch: ^4.0.3 - postcss: ~8.4.32 + postcss: ^8.5.14 resolve-from: ^5.0.0 peerDependencies: expo: "*" peerDependenciesMeta: expo: optional: true - checksum: 9355cb4f802aff122d2ac8d78cd99da5c6a257775a9f76edecce3873b3851df49783e7fcbb1acc4ecbef8119df5b2563cb0e876df7ab2fac6e8ee7fff5bbe75b + checksum: 701002e5bb2a3829a2ce1b979802347c3f85a78f87c776cc5caefa41e3331c804c9c4b52ec0fcdc08cc0c406bb10c2ccec41ae68f99936423da2fe3de0ce3ec5 languageName: node linkType: hard -"@expo/metro-runtime@npm:^55.0.10": - version: 55.0.10 - resolution: "@expo/metro-runtime@npm:55.0.10" +"@expo/metro-runtime@npm:^55.0.11": + version: 55.0.11 + resolution: "@expo/metro-runtime@npm:55.0.11" dependencies: - "@expo/log-box": 55.0.11 + "@expo/log-box": 55.0.12 anser: ^1.4.9 pretty-format: ^29.7.0 stacktrace-parser: ^0.1.10 @@ -1602,7 +1654,7 @@ __metadata: peerDependenciesMeta: react-dom: optional: true - checksum: c07009dc4870ccfb97b49a9b3fae90fdf67702d126ba6385f81b29836936c0036aa4cf8aa9863e38e523a4d86828e13201cbb23487a6e719a03eca925091c903 + checksum: db65bad98379cb229269c058c386e3535e6aa9327e8da4182a95257945a0d830cfe44330be50fdefcfc53e704d9505c0d89f31352b3397102ea6508eac3254e1 languageName: node linkType: hard @@ -1628,49 +1680,49 @@ __metadata: languageName: node linkType: hard -"@expo/osascript@npm:^2.4.2": - version: 2.4.2 - resolution: "@expo/osascript@npm:2.4.2" +"@expo/osascript@npm:^2.4.4": + version: 2.7.0 + resolution: "@expo/osascript@npm:2.7.0" dependencies: - "@expo/spawn-async": ^1.7.2 - checksum: 5609b926bd68120b6a01edea0c7b14d4fa9fcd454bbcb49b89988f7acdb540f3b9c1c133acbbd3f9cd6a6937ce2a950c9cdde2a98ec8769d8a8b1481666a67d9 + "@expo/spawn-async": ^1.8.0 + checksum: 456ad05358eac001d2be43510b30422bd7d931023587a2211d2285d8b7adac498b76b18a2907a1bd95debdd315ebc9aa7717cdaa270b61b3631cd7c3ce773c50 languageName: node linkType: hard -"@expo/package-manager@npm:^1.10.4": - version: 1.10.4 - resolution: "@expo/package-manager@npm:1.10.4" +"@expo/package-manager@npm:^1.10.6": + version: 1.13.0 + resolution: "@expo/package-manager@npm:1.13.0" dependencies: - "@expo/json-file": ^10.0.13 - "@expo/spawn-async": ^1.7.2 + "@expo/json-file": ^11.0.0 + "@expo/spawn-async": ^1.8.0 chalk: ^4.0.0 npm-package-arg: ^11.0.0 ora: ^3.4.0 resolve-workspace-root: ^2.0.0 - checksum: bbbe93de910a6a06b5ea3d327e15ac16eced8b3c25e3f4b0e7b4186c3a06c7ef23cf1517b301170cfc7a1c629fe30b89d031b42c0469efa740e7aa59c05224b0 + checksum: 26edfda17318f606f7c181090284899e4da329ed4f18053db3fa49008186203ad2c2170a224245e0ee325340b6cdf9cad9bca552fde2fd5ad0d362027d6998ef languageName: node linkType: hard -"@expo/plist@npm:^0.5.2": - version: 0.5.2 - resolution: "@expo/plist@npm:0.5.2" +"@expo/plist@npm:^0.5.4": + version: 0.5.4 + resolution: "@expo/plist@npm:0.5.4" dependencies: "@xmldom/xmldom": ^0.8.8 base64-js: ^1.5.1 xmlbuilder: ^15.1.1 - checksum: 30c06ee9a1375df1d85ef7608c0b15444d6a084330a9769c02bf66e9ed7b79867753a888f1cd80c61867ad09d7c1b34d3ef9e3839a62536ae07a58bc95de5c6b + checksum: a69aa6a16eadc5d2536788aa13f794a1e18035b2c2bde714c35c6035b233aaee0997aa95683f9547b2f9bcdc00af85ed938d7ee3c3df5157220ceb6061549b2e languageName: node linkType: hard -"@expo/prebuild-config@npm:^55.0.16": - version: 55.0.16 - resolution: "@expo/prebuild-config@npm:55.0.16" +"@expo/prebuild-config@npm:^55.0.19": + version: 55.0.19 + resolution: "@expo/prebuild-config@npm:55.0.19" dependencies: - "@expo/config": ~55.0.15 - "@expo/config-plugins": ~55.0.8 + "@expo/config": ~55.0.18 + "@expo/config-plugins": ~55.0.10 "@expo/config-types": ^55.0.5 - "@expo/image-utils": ^0.8.13 - "@expo/json-file": ^10.0.13 + "@expo/image-utils": ^0.8.14 + "@expo/json-file": ^10.0.15 "@react-native/normalize-colors": 0.83.6 debug: ^4.3.1 resolve-from: ^5.0.0 @@ -1678,13 +1730,13 @@ __metadata: xml2js: 0.6.0 peerDependencies: expo: "*" - checksum: 2a908fca545b5e53d5bc6e757f2e38e6e52b06d1deb23449967c22638033e023865db86faa71f919e76d097f1176663b6d4ea949bb163489c4ff658f532d6a77 + checksum: f21135e7976ab9a644ee3462eeaf12b329e65280be524a2674c8f21865bc58d6816e050c580af59654b91886527d259328ec733d8f30d94545cdcac8e5f5b3ef languageName: node linkType: hard -"@expo/require-utils@npm:^55.0.4": - version: 55.0.4 - resolution: "@expo/require-utils@npm:55.0.4" +"@expo/require-utils@npm:^55.0.5": + version: 55.0.5 + resolution: "@expo/require-utils@npm:55.0.5" dependencies: "@babel/code-frame": ^7.20.0 "@babel/core": ^7.25.2 @@ -1694,22 +1746,22 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 4aa0702f2bd82ffc964ac19105e2b9037173808d43c09ab4e57e48cf17d9ed407047a90f99de5323f662724ffe55f0797f39ed574a8f8661b7cd0a49ac6c8588 + checksum: d4fdb0a3b98e25f98051b245fdd296c225562c01f3bf9b92672ce661fe8e486d6e8ba64bc3569bfb41fbf58812ed05fa019657d66c3085030e8ef71d278ae26c languageName: node linkType: hard -"@expo/router-server@npm:^55.0.15": - version: 55.0.15 - resolution: "@expo/router-server@npm:55.0.15" +"@expo/router-server@npm:^55.0.18": + version: 55.0.18 + resolution: "@expo/router-server@npm:55.0.18" dependencies: debug: ^4.3.4 peerDependencies: - "@expo/metro-runtime": ^55.0.10 + "@expo/metro-runtime": ^55.0.11 expo: "*" - expo-constants: ^55.0.15 - expo-font: ^55.0.6 + expo-constants: ^55.0.16 + expo-font: ^55.0.8 expo-router: "*" - expo-server: ^55.0.8 + expo-server: ^55.0.11 react: "*" react-dom: "*" react-server-dom-webpack: ~19.0.1 || ~19.1.2 || ~19.2.1 @@ -1722,14 +1774,14 @@ __metadata: optional: true react-server-dom-webpack: optional: true - checksum: 3abffdeaf41830fa00dfb48aa53986836a2320ed370dba7396cc91eb1f4da08a906980715e8e33b6132406f77d8e47720984799a35aaa6a5120e84a7288d131b + checksum: cf12d96f3a04012565c06a8ebda9e2163cf5fc4cbfc940ec6f983591d14a94574b3bd38db55f4adfae8b43b9b2cb0791c2c95848c796f2e9a5f868766425e4eb languageName: node linkType: hard -"@expo/schema-utils@npm:^55.0.3": - version: 55.0.3 - resolution: "@expo/schema-utils@npm:55.0.3" - checksum: 0af91b7eb5046a367fac92934ea45a233cbef8fa72b8a6a14e7d98c821b0d56e162f800f20eb20fc52d23473f5ef61e312893a19f5efe64a02e80739493ca895 +"@expo/schema-utils@npm:^55.0.4": + version: 55.0.4 + resolution: "@expo/schema-utils@npm:55.0.4" + checksum: 4898207d90324973b73262464226912043bd817c2743a27119090aaf54bdafe9c1590f98f5a64c5b00b2b3c4e7c1611ce43eef5d9076b715571ca68feba0e50e languageName: node linkType: hard @@ -1740,12 +1792,12 @@ __metadata: languageName: node linkType: hard -"@expo/spawn-async@npm:^1.7.2": - version: 1.7.2 - resolution: "@expo/spawn-async@npm:1.7.2" +"@expo/spawn-async@npm:^1.7.2, @expo/spawn-async@npm:^1.8.0": + version: 1.8.0 + resolution: "@expo/spawn-async@npm:1.8.0" dependencies: - cross-spawn: ^7.0.3 - checksum: d99e5ff6d303ec9b0105f97c4fa6c65bca526c7d4d0987997c35cc745fa8224adf009942d01808192ebb9fa30619a53316641958631e85cf17b773d9eeda2597 + cross-spawn: ^7.0.6 + checksum: fc5202df888ee38915a215dfa177b0581358d378274077814f81ba5b3fe5ee2cd93271c091aba7429337a997af20efb7a38cec63cce6bb0b04395786853c1e30 languageName: node linkType: hard @@ -1775,21 +1827,21 @@ __metadata: linkType: hard "@expo/xcpretty@npm:^4.4.0": - version: 4.4.3 - resolution: "@expo/xcpretty@npm:4.4.3" + version: 4.4.4 + resolution: "@expo/xcpretty@npm:4.4.4" dependencies: "@babel/code-frame": ^7.20.0 chalk: ^4.1.0 js-yaml: ^4.1.0 bin: excpretty: build/cli.js - checksum: c86398e73f2aa3d711685f0278798cb7bce074475d92226e92e693e7a092eb6dcceffaff80096806f05696fc6b1af0fa01719938a7b3691f1abdeac3e474ffa0 + checksum: 9e1fb3292acf67787235f0698edcedfa32bed985ddff5863bda1f4e53b11deef07d89a9192e0142d0e5995441dd10b6698ac370ccf73414a10e768778d425201 languageName: node linkType: hard "@gorhom/bottom-sheet@npm:^5.2.9": - version: 5.2.13 - resolution: "@gorhom/bottom-sheet@npm:5.2.13" + version: 5.2.14 + resolution: "@gorhom/bottom-sheet@npm:5.2.14" dependencies: "@gorhom/portal": 1.0.14 invariant: ^2.2.4 @@ -1805,7 +1857,7 @@ __metadata: optional: true "@types/react-native": optional: true - checksum: 2c787e4617cad8abe91470bb25c8fb6193979a20dba0cc64135c7ceba28e0ac593c41465e362ebbca117d192bc5d804fdf6f79b297829f2087f8173f1f83a744 + checksum: c4387fa66b06e9c5dac713af7cdc9f9f5da42743f7dd6a2117d8c956e9410573c24e552fdb20232e251de5890ee83569894312eb5d4531f933f512b673ffeb53 languageName: node linkType: hard @@ -1822,9 +1874,9 @@ __metadata: linkType: hard "@huggingface/jinja@npm:^0.5.0": - version: 0.5.8 - resolution: "@huggingface/jinja@npm:0.5.8" - checksum: 13511762cebd882bf7d4aa0f52bba0cd3aa7b901e245030a07a95dfd7b341517c388aa0e604bc45638bde7965b17f87486cacf4316c9949efd813444c4ba9aa2 + version: 0.5.9 + resolution: "@huggingface/jinja@npm:0.5.9" + checksum: ca6e398a38d0524b6d520396e5cd0101c7572614f168828264a68a316a4e41ff9011dedc336f238adc605df8227b159bb3c8540280b0b6f6e2f1126062b39123 languageName: node linkType: hard @@ -1969,10 +2021,10 @@ __metadata: languageName: node linkType: hard -"@jest/diff-sequences@npm:30.3.0": - version: 30.3.0 - resolution: "@jest/diff-sequences@npm:30.3.0" - checksum: 715325e544f54cf5336b54fbfecd3e7e0e779b96c1f28a2ab42fdd4388f5f3751558a474d173b0c43bc5fb513fbb0464dbf1503cc69dab248515c6ed42feecb3 +"@jest/diff-sequences@npm:30.4.0": + version: 30.4.0 + resolution: "@jest/diff-sequences@npm:30.4.0" + checksum: a391acfbb6b349558c2c84643b4790be70e466d09bdca2eee8e4cb533cb0b5652930f591614fe05d39681e212cf984c790a770e282602c1645c561d85e8b64ee languageName: node linkType: hard @@ -2077,12 +2129,12 @@ __metadata: languageName: node linkType: hard -"@jest/schemas@npm:30.0.5": - version: 30.0.5 - resolution: "@jest/schemas@npm:30.0.5" +"@jest/schemas@npm:30.4.1": + version: 30.4.1 + resolution: "@jest/schemas@npm:30.4.1" dependencies: "@sinclair/typebox": ^0.34.0 - checksum: 7a4fc4166f688947c22d81e61aaf2cb22f178dbf6ee806b0931b75136899d426a72a8330762f27f0cf6f79da0d2a56f49a22fe09f5f80df95a683ed237a0f3b0 + checksum: 25d0db478805adff276e02f9e1b5a90d5962e51020503eede22edee432de3958654edddca0e66988c515fa7bc06461f5220826de9f76fcc89c5824e88d624842 languageName: node linkType: hard @@ -2262,8 +2314,8 @@ __metadata: linkType: hard "@op-engineering/op-sqlite@npm:^15.2.7": - version: 15.2.12 - resolution: "@op-engineering/op-sqlite@npm:15.2.12" + version: 15.2.14 + resolution: "@op-engineering/op-sqlite@npm:15.2.14" peerDependencies: "@sqlite.org/sqlite-wasm": "*" react: "*" @@ -2271,32 +2323,32 @@ __metadata: peerDependenciesMeta: "@sqlite.org/sqlite-wasm": optional: true - checksum: 9e73b4dcca95b7bc351775ec6138a8134d88d82c5abb4e9cd8cbecd55e265ace5afe4c520747dd36d2e9aef2902391fa26a2ff7dba7a738b4df5dc4e45feeea2 + checksum: e6cc55841bfe4556046b1e1bcf15e8fb68c5e6c17e69a01a09c75ccdeca6a973238fc6032eab4433f36bd4114180d6696f68282f47be052cb26b6d78ef36c45b languageName: node linkType: hard -"@pkgr/core@npm:^0.2.9": - version: 0.2.9 - resolution: "@pkgr/core@npm:0.2.9" - checksum: bb2fb86977d63f836f8f5b09015d74e6af6488f7a411dcd2bfdca79d76b5a681a9112f41c45bdf88a9069f049718efc6f3900d7f1de66a2ec966068308ae517f +"@pkgr/core@npm:^0.3.6": + version: 0.3.6 + resolution: "@pkgr/core@npm:0.3.6" + checksum: 29082aa13d36f13fc41cdc64cb1feb36b630de0d6ebde84e2ff68e3d7a7f1dce4462cca91f76176c46e50bbca6b1e7f1fd9cf907af12d5d70da83bc981ca4ccf languageName: node linkType: hard -"@radix-ui/primitive@npm:1.1.3": - version: 1.1.3 - resolution: "@radix-ui/primitive@npm:1.1.3" - checksum: ee27abbff0d6d305816e9314655eb35e72478ba47416bc9d5cb0581728be35e3408cfc0748313837561d635f0cb7dfaae26e61831f0e16c0fd7d669a612f2cb0 +"@radix-ui/primitive@npm:1.1.4": + version: 1.1.4 + resolution: "@radix-ui/primitive@npm:1.1.4" + checksum: 983f49f953b39eca8cc0fa26f429375bd153535371a222255814ee7b9d2ff89c6ab3c73399eb77c027944d76289e7bc4eec03121c1f782a93d27623de11ad135 languageName: node linkType: hard -"@radix-ui/react-collection@npm:1.1.7": - version: 1.1.7 - resolution: "@radix-ui/react-collection@npm:1.1.7" +"@radix-ui/react-collection@npm:1.1.11": + version: 1.1.11 + resolution: "@radix-ui/react-collection@npm:1.1.11" dependencies: - "@radix-ui/react-compose-refs": 1.1.2 - "@radix-ui/react-context": 1.1.2 - "@radix-ui/react-primitive": 2.1.3 - "@radix-ui/react-slot": 1.2.3 + "@radix-ui/react-compose-refs": 1.1.3 + "@radix-ui/react-context": 1.1.4 + "@radix-ui/react-primitive": 2.1.7 + "@radix-ui/react-slot": 1.3.0 peerDependencies: "@types/react": "*" "@types/react-dom": "*" @@ -2307,54 +2359,54 @@ __metadata: optional: true "@types/react-dom": optional: true - checksum: dd9bb015ef86205b4246f55bc84e5ad54519bb89b4825dd83e646fe95205191fe376bb31a9e847f9d66b710d0ef7fc9353c0b0ded7e8997a5c1f5be6addf94ef + checksum: c8ccc12fed57bc77db816369d31db416d68c03a15d7fce04135a2838141e094a85679baa5abd3344c17138377b6e5d9a04373af9f2a5db631a8a07f97bad600a languageName: node linkType: hard -"@radix-ui/react-compose-refs@npm:1.1.2": - version: 1.1.2 - resolution: "@radix-ui/react-compose-refs@npm:1.1.2" +"@radix-ui/react-compose-refs@npm:1.1.3": + version: 1.1.3 + resolution: "@radix-ui/react-compose-refs@npm:1.1.3" peerDependencies: "@types/react": "*" react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: "@types/react": optional: true - checksum: 9a91f0213014ffa40c5b8aae4debb993be5654217e504e35aa7422887eb2d114486d37e53c482d0fffb00cd44f51b5269fcdf397b280c71666fa11b7f32f165d + checksum: e6c296ee4f816f65feb5f8511217c3521ad822c8a995f320792fe5d909266a982ab3d14b62dab705eab0cb36d82eb0a2895886bb259268dc25898ea43d68a224 languageName: node linkType: hard -"@radix-ui/react-context@npm:1.1.2": - version: 1.1.2 - resolution: "@radix-ui/react-context@npm:1.1.2" +"@radix-ui/react-context@npm:1.1.4": + version: 1.1.4 + resolution: "@radix-ui/react-context@npm:1.1.4" peerDependencies: "@types/react": "*" react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: "@types/react": optional: true - checksum: 6d08437f23df362672259e535ae463e70bf7a0069f09bfa06c983a5a90e15250bde19da1d63ef8e3da06df1e1b4f92afa9d28ca6aa0297bb1c8aaf6ca83d28c5 + checksum: 01a4a4e59f868265aab95280c0a4d96433962f00592a0608f88d35e05901feff9adbf2b6926a49c3cae20f22db4630806ce4b8fc56d3e8a453666547a76485d1 languageName: node linkType: hard "@radix-ui/react-dialog@npm:^1.1.1": - version: 1.1.15 - resolution: "@radix-ui/react-dialog@npm:1.1.15" - dependencies: - "@radix-ui/primitive": 1.1.3 - "@radix-ui/react-compose-refs": 1.1.2 - "@radix-ui/react-context": 1.1.2 - "@radix-ui/react-dismissable-layer": 1.1.11 - "@radix-ui/react-focus-guards": 1.1.3 - "@radix-ui/react-focus-scope": 1.1.7 - "@radix-ui/react-id": 1.1.1 - "@radix-ui/react-portal": 1.1.9 - "@radix-ui/react-presence": 1.1.5 - "@radix-ui/react-primitive": 2.1.3 - "@radix-ui/react-slot": 1.2.3 - "@radix-ui/react-use-controllable-state": 1.2.2 + version: 1.1.18 + resolution: "@radix-ui/react-dialog@npm:1.1.18" + dependencies: + "@radix-ui/primitive": 1.1.4 + "@radix-ui/react-compose-refs": 1.1.3 + "@radix-ui/react-context": 1.1.4 + "@radix-ui/react-dismissable-layer": 1.1.14 + "@radix-ui/react-focus-guards": 1.1.4 + "@radix-ui/react-focus-scope": 1.1.11 + "@radix-ui/react-id": 1.1.2 + "@radix-ui/react-portal": 1.1.13 + "@radix-ui/react-presence": 1.1.6 + "@radix-ui/react-primitive": 2.1.7 + "@radix-ui/react-slot": 1.3.0 + "@radix-ui/react-use-controllable-state": 1.2.3 aria-hidden: ^1.2.4 - react-remove-scroll: ^2.6.3 + react-remove-scroll: ^2.7.2 peerDependencies: "@types/react": "*" "@types/react-dom": "*" @@ -2365,32 +2417,32 @@ __metadata: optional: true "@types/react-dom": optional: true - checksum: a0834338ec66866ce301ef46e0dad9d99accf496f03b5021eceec7e2b79d7286b4f2c5e35f2387891e2bf33ef9a11d381dde2c8fe936a2f30cd50ca4e9bf4cb5 + checksum: 4e4516fca2ebc5a316e5e845c57eda6f707a33bd74034c5e0e97c7ce534657ea79e6b7f8bd6eae7e3490d976c1ab3028bf3aeffc0c50291e87cc07cb68af579d languageName: node linkType: hard -"@radix-ui/react-direction@npm:1.1.1": - version: 1.1.1 - resolution: "@radix-ui/react-direction@npm:1.1.1" +"@radix-ui/react-direction@npm:1.1.2": + version: 1.1.2 + resolution: "@radix-ui/react-direction@npm:1.1.2" peerDependencies: "@types/react": "*" react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: "@types/react": optional: true - checksum: 8cc330285f1d06829568042ca9aabd3295be4690ae93683033fc8632b5c4dfc60f5c1312f6e2cae27c196189c719de3cfbcf792ff74800f9ccae0ab4abc1bc92 + checksum: c2c44fcecfb36dd1b4e75f523531185b4aeb4c46bc6b3512411de6f0b7414cdbf14cf6042fb571df81ae3ada722be94c1883520d7e7f12c02a17642335cfdc6a languageName: node linkType: hard -"@radix-ui/react-dismissable-layer@npm:1.1.11": - version: 1.1.11 - resolution: "@radix-ui/react-dismissable-layer@npm:1.1.11" +"@radix-ui/react-dismissable-layer@npm:1.1.14": + version: 1.1.14 + resolution: "@radix-ui/react-dismissable-layer@npm:1.1.14" dependencies: - "@radix-ui/primitive": 1.1.3 - "@radix-ui/react-compose-refs": 1.1.2 - "@radix-ui/react-primitive": 2.1.3 - "@radix-ui/react-use-callback-ref": 1.1.1 - "@radix-ui/react-use-escape-keydown": 1.1.1 + "@radix-ui/primitive": 1.1.4 + "@radix-ui/react-compose-refs": 1.1.3 + "@radix-ui/react-primitive": 2.1.7 + "@radix-ui/react-use-callback-ref": 1.1.2 + "@radix-ui/react-use-effect-event": 0.0.3 peerDependencies: "@types/react": "*" "@types/react-dom": "*" @@ -2401,30 +2453,30 @@ __metadata: optional: true "@types/react-dom": optional: true - checksum: 8fc9f027c9f68940c69c9cc117c43e1313d1a78ae4109cf809868b82837e5e2a7d410adf78e97328d9d5a080a63e399918414985658ab029a8df7d775af23b68 + checksum: 51304bec83e3680e437c1db8555be0f9f76fb943ece0a948ec4b83e340d2e2a6510f8d9e68b77d0c74f0b7e138498060c53adfc607fec1e037f7261dbe14c3d3 languageName: node linkType: hard -"@radix-ui/react-focus-guards@npm:1.1.3": - version: 1.1.3 - resolution: "@radix-ui/react-focus-guards@npm:1.1.3" +"@radix-ui/react-focus-guards@npm:1.1.4": + version: 1.1.4 + resolution: "@radix-ui/react-focus-guards@npm:1.1.4" peerDependencies: "@types/react": "*" react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: "@types/react": optional: true - checksum: b57878f6cf0ebc3e8d7c5c6bbaad44598daac19c921551ca541c104201048a9a902f3d69196e7a09995fd46e998c309aab64dc30fa184b3609d67d187a6a9c24 + checksum: da18237044a8f270fd69f5634fb0d812c0dc5e035e5e6ca9c7c4b5ab4a6f1bce746ed25bacfdf001cb37ce1b362a92b2802d354d18b7fc048d3a9474f84e577c languageName: node linkType: hard -"@radix-ui/react-focus-scope@npm:1.1.7": - version: 1.1.7 - resolution: "@radix-ui/react-focus-scope@npm:1.1.7" +"@radix-ui/react-focus-scope@npm:1.1.11": + version: 1.1.11 + resolution: "@radix-ui/react-focus-scope@npm:1.1.11" dependencies: - "@radix-ui/react-compose-refs": 1.1.2 - "@radix-ui/react-primitive": 2.1.3 - "@radix-ui/react-use-callback-ref": 1.1.1 + "@radix-ui/react-compose-refs": 1.1.3 + "@radix-ui/react-primitive": 2.1.7 + "@radix-ui/react-use-callback-ref": 1.1.2 peerDependencies: "@types/react": "*" "@types/react-dom": "*" @@ -2435,31 +2487,31 @@ __metadata: optional: true "@types/react-dom": optional: true - checksum: bb642d192d3da8431f8b39f64959b493a7ba743af8501b76699ef93357c96507c11fb76d468824b52b0e024eaee130a641f3a213268ac7c9af34883b45610c9b + checksum: 112316e6186e1245bc0eb8d8fa8372442d005478508eaafb33337b200417beeb744812e6788a997c60ce8190da14b0464bb1165444a988ea4f7beab8866a712d languageName: node linkType: hard -"@radix-ui/react-id@npm:1.1.1": - version: 1.1.1 - resolution: "@radix-ui/react-id@npm:1.1.1" +"@radix-ui/react-id@npm:1.1.2": + version: 1.1.2 + resolution: "@radix-ui/react-id@npm:1.1.2" dependencies: - "@radix-ui/react-use-layout-effect": 1.1.1 + "@radix-ui/react-use-layout-effect": 1.1.2 peerDependencies: "@types/react": "*" react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: "@types/react": optional: true - checksum: 8d68e200778eb3038906870fc869b3d881f4a46715fb20cddd9c76cba42fdaaa4810a3365b6ec2daf0f185b9201fc99d009167f59c7921bc3a139722c2e976db + checksum: d58d0cb53238cef22a880c9fa49ad5510022f0b52aa852842c5ff29abc9dd205e547b58a747682f778adceb9d6f2a548d0897bd27964f4edc903a96707b6e472 languageName: node linkType: hard -"@radix-ui/react-portal@npm:1.1.9": - version: 1.1.9 - resolution: "@radix-ui/react-portal@npm:1.1.9" +"@radix-ui/react-portal@npm:1.1.13": + version: 1.1.13 + resolution: "@radix-ui/react-portal@npm:1.1.13" dependencies: - "@radix-ui/react-primitive": 2.1.3 - "@radix-ui/react-use-layout-effect": 1.1.1 + "@radix-ui/react-primitive": 2.1.7 + "@radix-ui/react-use-layout-effect": 1.1.2 peerDependencies: "@types/react": "*" "@types/react-dom": "*" @@ -2470,16 +2522,15 @@ __metadata: optional: true "@types/react-dom": optional: true - checksum: bd6be39bf021d5c917e2474ecba411e2625171f7ef96862b9af04bbd68833bb3662a7f1fbdeb5a7a237111b10e811e76d2cd03e957dadd6e668ef16541bfbd68 + checksum: b896dd705a93c869e27280af25bade70068bf27e504ed27ee93b669a4250169f8628626010dd82502e51da148e19511cf921b42d8ebfb61e61edd0d61794afa6 languageName: node linkType: hard -"@radix-ui/react-presence@npm:1.1.5": - version: 1.1.5 - resolution: "@radix-ui/react-presence@npm:1.1.5" +"@radix-ui/react-presence@npm:1.1.6": + version: 1.1.6 + resolution: "@radix-ui/react-presence@npm:1.1.6" dependencies: - "@radix-ui/react-compose-refs": 1.1.2 - "@radix-ui/react-use-layout-effect": 1.1.1 + "@radix-ui/react-use-layout-effect": 1.1.2 peerDependencies: "@types/react": "*" "@types/react-dom": "*" @@ -2490,15 +2541,15 @@ __metadata: optional: true "@types/react-dom": optional: true - checksum: 05f1b8e80d3d878efab44304ce55d0b9e6c7050e8345f9da95d0597a716121fb2467c3247c847c51a6cb27edd00e86ac36b2635e4c00ea79d91cfc26c930da81 + checksum: a1ad63411854d7e3ef1adcffad776705e79d777ec6c2222f9d1ca31b6bbea0945adf7bfc7c71383e078c7ea439aecad35d7e14cded9273a243b5343ceb39003c languageName: node linkType: hard -"@radix-ui/react-primitive@npm:2.1.3": - version: 2.1.3 - resolution: "@radix-ui/react-primitive@npm:2.1.3" +"@radix-ui/react-primitive@npm:2.1.7": + version: 2.1.7 + resolution: "@radix-ui/react-primitive@npm:2.1.7" dependencies: - "@radix-ui/react-slot": 1.2.3 + "@radix-ui/react-slot": 1.3.0 peerDependencies: "@types/react": "*" "@types/react-dom": "*" @@ -2509,23 +2560,23 @@ __metadata: optional: true "@types/react-dom": optional: true - checksum: 01f82e4bad76b57767198762c905e5bcea04f4f52129749791e31adfcb1b36f6fdc89c73c40017d812b6e25e4ac925d837214bb280cfeaa5dc383457ce6940b0 + checksum: 59d06ef7a6a2ef81b69ca649e2aef085b0e30f31c5746d907cc13cc23ee40164f935b345b0dbb2ca68eb9c4f482b0e66cb1af615950d46bdd66ceb6aa9dcaa8f languageName: node linkType: hard -"@radix-ui/react-roving-focus@npm:1.1.11": - version: 1.1.11 - resolution: "@radix-ui/react-roving-focus@npm:1.1.11" - dependencies: - "@radix-ui/primitive": 1.1.3 - "@radix-ui/react-collection": 1.1.7 - "@radix-ui/react-compose-refs": 1.1.2 - "@radix-ui/react-context": 1.1.2 - "@radix-ui/react-direction": 1.1.1 - "@radix-ui/react-id": 1.1.1 - "@radix-ui/react-primitive": 2.1.3 - "@radix-ui/react-use-callback-ref": 1.1.1 - "@radix-ui/react-use-controllable-state": 1.2.2 +"@radix-ui/react-roving-focus@npm:1.1.14": + version: 1.1.14 + resolution: "@radix-ui/react-roving-focus@npm:1.1.14" + dependencies: + "@radix-ui/primitive": 1.1.4 + "@radix-ui/react-collection": 1.1.11 + "@radix-ui/react-compose-refs": 1.1.3 + "@radix-ui/react-context": 1.1.4 + "@radix-ui/react-direction": 1.1.2 + "@radix-ui/react-id": 1.1.2 + "@radix-ui/react-primitive": 2.1.7 + "@radix-ui/react-use-callback-ref": 1.1.2 + "@radix-ui/react-use-controllable-state": 1.2.3 peerDependencies: "@types/react": "*" "@types/react-dom": "*" @@ -2536,52 +2587,37 @@ __metadata: optional: true "@types/react-dom": optional: true - checksum: 62af05c244803359c36beea278dac89caee37d20c31b84bcba3a20c462df33b7395c2e1b08b3a8ebb471c29cec4b3fb4f97488b6a167b1b275cedf994cf436e6 - languageName: node - linkType: hard - -"@radix-ui/react-slot@npm:1.2.3": - version: 1.2.3 - resolution: "@radix-ui/react-slot@npm:1.2.3" - dependencies: - "@radix-ui/react-compose-refs": 1.1.2 - peerDependencies: - "@types/react": "*" - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - "@types/react": - optional: true - checksum: 2731089e15477dd5eef98a5757c36113dd932d0c52ff05123cd89f05f0412e95e5b205229185d1cd705cda4a674a838479cce2b3b46ed903f82f5d23d9e3f3c2 + checksum: af4ef8dbddfc34d34094233d518fc9545fceb1c01d7332d9e5434ec509b341e3a458a9edb89242fdfede607bc07490b988f2c46d07a746ff91805a6914cc00fc languageName: node linkType: hard -"@radix-ui/react-slot@npm:^1.2.0": - version: 1.2.4 - resolution: "@radix-ui/react-slot@npm:1.2.4" +"@radix-ui/react-slot@npm:1.3.0, @radix-ui/react-slot@npm:^1.2.0": + version: 1.3.0 + resolution: "@radix-ui/react-slot@npm:1.3.0" dependencies: - "@radix-ui/react-compose-refs": 1.1.2 + "@radix-ui/react-compose-refs": 1.1.3 peerDependencies: "@types/react": "*" react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: "@types/react": optional: true - checksum: 6e1cda512f649ca9df8e746a5059c69aa7539b43870ff6def360103590d68d1ea3adf8a216092107b3e6476d55a3d71707e162b5be04574126bcc6fdfffefe6a + checksum: 5a78dff8d88232468e488321b274fd578f39e03f8c2118096f48e76fc999c1ec321bbc29e6a47e76edf0caa5fa152d49d5dba206b9850cc08be910c9ff581679 languageName: node linkType: hard "@radix-ui/react-tabs@npm:^1.1.12": - version: 1.1.13 - resolution: "@radix-ui/react-tabs@npm:1.1.13" - dependencies: - "@radix-ui/primitive": 1.1.3 - "@radix-ui/react-context": 1.1.2 - "@radix-ui/react-direction": 1.1.1 - "@radix-ui/react-id": 1.1.1 - "@radix-ui/react-presence": 1.1.5 - "@radix-ui/react-primitive": 2.1.3 - "@radix-ui/react-roving-focus": 1.1.11 - "@radix-ui/react-use-controllable-state": 1.2.2 + version: 1.1.16 + resolution: "@radix-ui/react-tabs@npm:1.1.16" + dependencies: + "@radix-ui/primitive": 1.1.4 + "@radix-ui/react-context": 1.1.4 + "@radix-ui/react-direction": 1.1.2 + "@radix-ui/react-id": 1.1.2 + "@radix-ui/react-presence": 1.1.6 + "@radix-ui/react-primitive": 2.1.7 + "@radix-ui/react-roving-focus": 1.1.14 + "@radix-ui/react-use-controllable-state": 1.2.3 peerDependencies: "@types/react": "*" "@types/react-dom": "*" @@ -2592,79 +2628,64 @@ __metadata: optional: true "@types/react-dom": optional: true - checksum: 6bb8fa404d65ddb1be12cb03912abff8d924fb9b3435da71b39836585df6b55bd25341bd989324c330724af942c0f0cdf4c51503057b0532359da40c64b08081 + checksum: 29c7996ad25fef7d137070f286eda771df8bd9531faffc70d2de86ffa1ebbf4c8a2fad24891a7bbb6e28b929c3f1d6cd75791037a591e3c513d66716afe509a6 languageName: node linkType: hard -"@radix-ui/react-use-callback-ref@npm:1.1.1": - version: 1.1.1 - resolution: "@radix-ui/react-use-callback-ref@npm:1.1.1" - peerDependencies: - "@types/react": "*" - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - "@types/react": - optional: true - checksum: cde8c40f1d4e79e6e71470218163a746858304bad03758ac84dc1f94247a046478e8e397518350c8d6609c84b7e78565441d7505bb3ed573afce82cfdcd19faf - languageName: node - linkType: hard - -"@radix-ui/react-use-controllable-state@npm:1.2.2": - version: 1.2.2 - resolution: "@radix-ui/react-use-controllable-state@npm:1.2.2" - dependencies: - "@radix-ui/react-use-effect-event": 0.0.2 - "@radix-ui/react-use-layout-effect": 1.1.1 +"@radix-ui/react-use-callback-ref@npm:1.1.2": + version: 1.1.2 + resolution: "@radix-ui/react-use-callback-ref@npm:1.1.2" peerDependencies: "@types/react": "*" react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: "@types/react": optional: true - checksum: b438ee199d0630bf95eaafe8bf4bce219e73b371cfc8465f47548bfa4ee231f1134b5c6696b242890a01a0fd25fa34a7b172346bbfc5ee25cfb28b3881b1dc92 + checksum: d2a06a7e1f5e2778f49c4ae2579d6fb4d10edcaf71368364988159a54fe8ac4f384c6597541c0d37294ec5ebf43ce4613944fa8c9810f7ab5c4bb110dfc69cdf languageName: node linkType: hard -"@radix-ui/react-use-effect-event@npm:0.0.2": - version: 0.0.2 - resolution: "@radix-ui/react-use-effect-event@npm:0.0.2" +"@radix-ui/react-use-controllable-state@npm:1.2.3": + version: 1.2.3 + resolution: "@radix-ui/react-use-controllable-state@npm:1.2.3" dependencies: - "@radix-ui/react-use-layout-effect": 1.1.1 + "@radix-ui/react-use-effect-event": 0.0.3 + "@radix-ui/react-use-layout-effect": 1.1.2 peerDependencies: "@types/react": "*" react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: "@types/react": optional: true - checksum: 5a1950a30a399ea7e4b98154da9f536737a610de80189b7aacd4f064a89a3cd0d2a48571d527435227252e72e872bdb544ff6ffcfbdd02de2efd011be4aaa902 + checksum: 25d6957b7c4c29225da1ca26cd07b56517be8e655dfcef63f5928cb70f52355489257572ea777f15038e0c6de8bbbfc160e9f15e0c2a08e2d5183ad9ff0a951e languageName: node linkType: hard -"@radix-ui/react-use-escape-keydown@npm:1.1.1": - version: 1.1.1 - resolution: "@radix-ui/react-use-escape-keydown@npm:1.1.1" +"@radix-ui/react-use-effect-event@npm:0.0.3": + version: 0.0.3 + resolution: "@radix-ui/react-use-effect-event@npm:0.0.3" dependencies: - "@radix-ui/react-use-callback-ref": 1.1.1 + "@radix-ui/react-use-layout-effect": 1.1.2 peerDependencies: "@types/react": "*" react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: "@types/react": optional: true - checksum: 0eb0756c2c55ddcde9ff01446ab01c085ab2bf799173e97db7ef5f85126f9e8600225570801a1f64740e6d14c39ffe8eed7c14d29737345a5797f4622ac96f6f + checksum: 53b4e813cb68b5c603cb2ccaa5822474f846ee46a7aa5e172f29842bea135521c20d69bf12a14e5522cc385ee3724b9e3bfbf8d2d01f1933e472bd9fa80478ca languageName: node linkType: hard -"@radix-ui/react-use-layout-effect@npm:1.1.1": - version: 1.1.1 - resolution: "@radix-ui/react-use-layout-effect@npm:1.1.1" +"@radix-ui/react-use-layout-effect@npm:1.1.2": + version: 1.1.2 + resolution: "@radix-ui/react-use-layout-effect@npm:1.1.2" peerDependencies: "@types/react": "*" react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: "@types/react": optional: true - checksum: bad2ba4f206e6255263582bedfb7868773c400836f9a1b423c0b464ffe4a17e13d3f306d1ce19cf7a19a492e9d0e49747464f2656451bb7c6a99f5a57bd34de2 + checksum: 4ecffaec7a1b40634ffa90b5e0b5492980532a59c3315e2e57f91fb54736abed6dcb60acddbe5bb01a8a6b43a226e97d7581e816f061c8df619ed0f188308355 languageName: node linkType: hard @@ -3103,27 +3124,27 @@ __metadata: linkType: hard "@react-navigation/bottom-tabs@npm:^7.15.5": - version: 7.15.11 - resolution: "@react-navigation/bottom-tabs@npm:7.15.11" + version: 7.18.6 + resolution: "@react-navigation/bottom-tabs@npm:7.18.6" dependencies: - "@react-navigation/elements": ^2.9.15 + "@react-navigation/elements": ^2.9.28 color: ^4.2.3 sf-symbols-typescript: ^2.1.0 peerDependencies: - "@react-navigation/native": ^7.2.2 + "@react-navigation/native": ^7.3.6 react: ">= 18.2.0" react-native: "*" react-native-safe-area-context: ">= 4.0.0" react-native-screens: ">= 4.0.0" - checksum: 15968ae491fe76a94b98d00ca861ea29875f7d41bc3a631be6042655c746e73ecd9c4572723cdaba141e9191b06843ad212485aa290321891ca12346ccc8a25f + checksum: 89891c0b606897dd7a80a0d5f8009950a33d0bd84824cdf9e507940aaf4bb35fd9e590eb90cf13e436157a10ca7653b7ecfd73ce0330122a67b2c7b7c81f8822 languageName: node linkType: hard -"@react-navigation/core@npm:^7.17.2": - version: 7.17.2 - resolution: "@react-navigation/core@npm:7.17.2" +"@react-navigation/core@npm:^7.21.4": + version: 7.21.4 + resolution: "@react-navigation/core@npm:7.21.4" dependencies: - "@react-navigation/routers": ^7.5.3 + "@react-navigation/routers": ^7.6.0 escape-string-regexp: ^4.0.0 fast-deep-equal: ^3.1.3 nanoid: ^3.3.11 @@ -3133,90 +3154,91 @@ __metadata: use-sync-external-store: ^1.5.0 peerDependencies: react: ">= 18.2.0" - checksum: ee0641481b7e272ebe9aafa4e5985a9de497b719275a153687ecd8cf80b30dce03e53270d9d5606f6321fea801a7030578f0bc731e569f421cfbb76065e8b8e1 + checksum: 0c8b1f0b27c8632358f2bccbad30ecbec48e988dce8d4dd671c3943581537f13a7fe34e46d916d9f53d913f433569f5d44fdf9e596a2cd5698a9c1910837e52b languageName: node linkType: hard "@react-navigation/drawer@npm:^7.9.8": - version: 7.9.9 - resolution: "@react-navigation/drawer@npm:7.9.9" + version: 7.12.5 + resolution: "@react-navigation/drawer@npm:7.12.5" dependencies: - "@react-navigation/elements": ^2.9.15 + "@react-navigation/elements": ^2.9.28 color: ^4.2.3 - react-native-drawer-layout: ^4.2.2 + react-native-drawer-layout: ^4.2.5 use-latest-callback: ^0.2.4 peerDependencies: - "@react-navigation/native": ^7.2.2 + "@react-navigation/native": ^7.3.6 react: ">= 18.2.0" react-native: "*" react-native-gesture-handler: ">= 2.0.0" react-native-reanimated: ">= 2.0.0" react-native-safe-area-context: ">= 4.0.0" react-native-screens: ">= 4.0.0" - checksum: 4f05c555b19056153df8ebcdc99d6ae8c4f2fe584f01fe811da95d3259f91b46d920401fc883548555bc668ffab7a7e8b89a58f5c9fc0916447f46c5f05641fc + checksum: a5b46c432739a7b198b921485f0bfcf12cc3d29ad80f8f653acc2a7eb9bb2236eaeceb3e259f3c838b94df91e1976a75901ec55f4179923379cf71983913e916 languageName: node linkType: hard -"@react-navigation/elements@npm:^2.9.15": - version: 2.9.15 - resolution: "@react-navigation/elements@npm:2.9.15" +"@react-navigation/elements@npm:^2.9.28": + version: 2.9.28 + resolution: "@react-navigation/elements@npm:2.9.28" dependencies: color: ^4.2.3 use-latest-callback: ^0.2.4 use-sync-external-store: ^1.5.0 peerDependencies: "@react-native-masked-view/masked-view": ">= 0.2.0" - "@react-navigation/native": ^7.2.2 + "@react-navigation/native": ^7.3.6 react: ">= 18.2.0" react-native: "*" react-native-safe-area-context: ">= 4.0.0" peerDependenciesMeta: "@react-native-masked-view/masked-view": optional: true - checksum: fa00ffee3fef3e8f15181fa8c87383b9826aee0b4e63ddf6d7c56e73cf2439af0aca7c5eea2dbb32af35ad82ae94ebae1d60b73ef719fa7945d85f611e7f229f + checksum: 0a1a1b7f435910fec621e9f618cc773f7f16a95a2e882ac307d92cb2410f7af31d235442ca42ba0cea540e1ebc47744ea9b10d091d259bfb3f7f8dafdb9a26a1 languageName: node linkType: hard "@react-navigation/native-stack@npm:^7.14.5": - version: 7.14.12 - resolution: "@react-navigation/native-stack@npm:7.14.12" + version: 7.17.8 + resolution: "@react-navigation/native-stack@npm:7.17.8" dependencies: - "@react-navigation/elements": ^2.9.15 + "@react-navigation/elements": ^2.9.28 color: ^4.2.3 sf-symbols-typescript: ^2.1.0 warn-once: ^0.1.1 peerDependencies: - "@react-navigation/native": ^7.2.2 + "@react-navigation/native": ^7.3.6 react: ">= 18.2.0" react-native: "*" react-native-safe-area-context: ">= 4.0.0" react-native-screens: ">= 4.0.0" - checksum: 749b3f01f3e947706e724ba28f4cc36499b10da24588032ef3cb9e8066a26e5b7a41b2c97fb80bd201bf26a6df3dd5bd20225017bbb1122d8d856af97ffcffd3 + checksum: ba2fc1f0cc02e781f92718883005a2d5f480fdabaf134b3a9b9670e28575b261e91c8bcbfdc350dec06d38d9fc3e1d6f1902f37b717c8b8a25d8b864457eb6f8 languageName: node linkType: hard "@react-navigation/native@npm:^7.1.33": - version: 7.2.2 - resolution: "@react-navigation/native@npm:7.2.2" + version: 7.3.6 + resolution: "@react-navigation/native@npm:7.3.6" dependencies: - "@react-navigation/core": ^7.17.2 + "@react-navigation/core": ^7.21.4 escape-string-regexp: ^4.0.0 fast-deep-equal: ^3.1.3 nanoid: ^3.3.11 + standard-navigation: ^0.0.7 use-latest-callback: ^0.2.4 peerDependencies: react: ">= 18.2.0" react-native: "*" - checksum: a7be7b67bbfb18f04f009b64dcfe432690b56dbbe3c03c3ecfb874b8ba6aaebebc312075e5c57eb9d1aa239066e55a3dba4e3650ef2cea20c1550a712a3c2f7b + checksum: 7d73dfc9bbeea6fe04357f8b1ac43ec91393e0b250141bdeb6e50d20dd6cb08f8ddabaf83a6abbac5dc96a1f164aea4a6b01b5ec404df5b85367cc896b00b15f languageName: node linkType: hard -"@react-navigation/routers@npm:^7.5.3": - version: 7.5.3 - resolution: "@react-navigation/routers@npm:7.5.3" +"@react-navigation/routers@npm:^7.6.0": + version: 7.6.0 + resolution: "@react-navigation/routers@npm:7.6.0" dependencies: nanoid: ^3.3.11 - checksum: 1b8397ade6bbab51a60d2671fd88eca2e0cf22b9cd10bee16d3537bc5f05deea7dad8c116a809f580c87c5a6cceae7c4fc9f20644f45076ee8f00524e903fc4b + checksum: 8e2f29cf88d609db888c0888b9c8171e319aad6fc9dbe53cf77e417dc932b808333612c67db3c62775a2152c0d4b296d9db30428ac414b674dcfd474c9d3368f languageName: node linkType: hard @@ -3478,9 +3500,9 @@ __metadata: linkType: hard "@types/estree@npm:^1.0.6": - version: 1.0.8 - resolution: "@types/estree@npm:1.0.8" - checksum: bd93e2e415b6f182ec4da1074e1f36c480f1d26add3e696d54fb30c09bc470897e41361c8fd957bf0985024f8fbf1e6e2aff977d79352ef7eb93a5c6dcff6c11 + version: 1.0.9 + resolution: "@types/estree@npm:1.0.9" + checksum: 752c0afee3ec82b8e24484bf6a27dfa093bbf3de4ef1c20ed0364fb6ad2c0c7971e7504ed9a7aaff103a47e2d945ce7a17f74951743dd944782a0735f53170de languageName: node linkType: hard @@ -3554,20 +3576,20 @@ __metadata: linkType: hard "@types/node@npm:*": - version: 25.6.0 - resolution: "@types/node@npm:25.6.0" + version: 26.1.0 + resolution: "@types/node@npm:26.1.0" dependencies: - undici-types: ~7.19.0 - checksum: 98945eb59909a08868ccac203022f122b5549448ef8628de9eac3fe20481467cd6ec32af819fd432695f67ac21ebbbc69c8a141de6c6455edaf6e717e2cb89c9 + undici-types: ~8.3.0 + checksum: bbced3378635cb48e739e384e53ff4c7d3e18194a2e003fb5240fc4f6c18fdc9f2452539a9f6fa608ca7b510a40f844d483c23f5d8bd2cf2c245b16d43ce4236 languageName: node linkType: hard "@types/react@npm:~19.2.10": - version: 19.2.14 - resolution: "@types/react@npm:19.2.14" + version: 19.2.17 + resolution: "@types/react@npm:19.2.17" dependencies: csstype: ^3.2.2 - checksum: ddd330292abf2dc2cfa65188e1c5f67cc6e90f8d8ffb088f753a38db9d123f942c23d324a6b7e8027ff04f22b395492150f54b9b520b6cbec1e8841e669f2c19 + checksum: 9704dc2001b6bcc32efc6e3fe144e18c411d5ee8a5c8dfe572535e44bd300424c4e7bddc9314299eeec28efb547816368b5c9851c93a3082e8ad1265786f3d31 languageName: node linkType: hard @@ -3615,105 +3637,105 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:8.59.2, @typescript-eslint/eslint-plugin@npm:^8.36.0": - version: 8.59.2 - resolution: "@typescript-eslint/eslint-plugin@npm:8.59.2" +"@typescript-eslint/eslint-plugin@npm:8.62.1, @typescript-eslint/eslint-plugin@npm:^8.36.0": + version: 8.62.1 + resolution: "@typescript-eslint/eslint-plugin@npm:8.62.1" dependencies: "@eslint-community/regexpp": ^4.12.2 - "@typescript-eslint/scope-manager": 8.59.2 - "@typescript-eslint/type-utils": 8.59.2 - "@typescript-eslint/utils": 8.59.2 - "@typescript-eslint/visitor-keys": 8.59.2 + "@typescript-eslint/scope-manager": 8.62.1 + "@typescript-eslint/type-utils": 8.62.1 + "@typescript-eslint/utils": 8.62.1 + "@typescript-eslint/visitor-keys": 8.62.1 ignore: ^7.0.5 natural-compare: ^1.4.0 ts-api-utils: ^2.5.0 peerDependencies: - "@typescript-eslint/parser": ^8.59.2 + "@typescript-eslint/parser": ^8.62.1 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: ">=4.8.4 <6.1.0" - checksum: c18238066dc1c2641bf463be4f21bcf3f03c97557f5b147bf5306293b7fc498890b79ee99aab518c21acd1c5dc1d44485a30dc6a351176304213b1a6110889b0 + checksum: c40f293bd5040ddac92bf5c8fcb0e2a18ab0eb87fec759c7b3a5f47f2957667632313b59746f37cb4b6a00734da05181e85901b491ef86735d8a433210901027 languageName: node linkType: hard -"@typescript-eslint/parser@npm:8.59.2, @typescript-eslint/parser@npm:^8.36.0": - version: 8.59.2 - resolution: "@typescript-eslint/parser@npm:8.59.2" +"@typescript-eslint/parser@npm:8.62.1, @typescript-eslint/parser@npm:^8.36.0": + version: 8.62.1 + resolution: "@typescript-eslint/parser@npm:8.62.1" dependencies: - "@typescript-eslint/scope-manager": 8.59.2 - "@typescript-eslint/types": 8.59.2 - "@typescript-eslint/typescript-estree": 8.59.2 - "@typescript-eslint/visitor-keys": 8.59.2 + "@typescript-eslint/scope-manager": 8.62.1 + "@typescript-eslint/types": 8.62.1 + "@typescript-eslint/typescript-estree": 8.62.1 + "@typescript-eslint/visitor-keys": 8.62.1 debug: ^4.4.3 peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: ">=4.8.4 <6.1.0" - checksum: 03a784319dff60a51514bf786c20a41b3b2118d3d68aef06e5f1fddcb15da745a6dd30ed7066a505f270ef810f21bde391930c3efae6a62ed3d8901b868e42fe + checksum: 5c11f014c5c2ae525e8412babf189e4bdbe0b818440ed899df3e277fba6f36b14600a212f6fddbaecafbb4723725504e267379b2dbac69a0036b4242b314a2e2 languageName: node linkType: hard -"@typescript-eslint/project-service@npm:8.59.2": - version: 8.59.2 - resolution: "@typescript-eslint/project-service@npm:8.59.2" +"@typescript-eslint/project-service@npm:8.62.1": + version: 8.62.1 + resolution: "@typescript-eslint/project-service@npm:8.62.1" dependencies: - "@typescript-eslint/tsconfig-utils": ^8.59.2 - "@typescript-eslint/types": ^8.59.2 + "@typescript-eslint/tsconfig-utils": ^8.62.1 + "@typescript-eslint/types": ^8.62.1 debug: ^4.4.3 peerDependencies: typescript: ">=4.8.4 <6.1.0" - checksum: 2c4d1cc23ef90f17f1054905d936f9471b8d93e8277ba2e33f08f7106b765dff03922432c9878af262ac62d049b28e2d611a740a1a9102f161af566a954758d1 + checksum: 65ac343a55832b693988a77843e0bfce3a44be57f28bae50c7b6cd76c807a96af3df53a035735f6f05a1aef50ffa386ede90f9d64863b7732539937f48898b74 languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:8.59.2": - version: 8.59.2 - resolution: "@typescript-eslint/scope-manager@npm:8.59.2" +"@typescript-eslint/scope-manager@npm:8.62.1": + version: 8.62.1 + resolution: "@typescript-eslint/scope-manager@npm:8.62.1" dependencies: - "@typescript-eslint/types": 8.59.2 - "@typescript-eslint/visitor-keys": 8.59.2 - checksum: 7a55a62a194dda2f91cf182d393c85700e92cb4f9b381b8ed3af06578b606462daa1ce92356e8ced581f0d55afe3e81c743844bc1f99df05846eefa7ec9ced4f + "@typescript-eslint/types": 8.62.1 + "@typescript-eslint/visitor-keys": 8.62.1 + checksum: c11359d59d84ce4a13563a06bde517bdb77fc792042726ecb69e500e4d6f730feb15799662d472f8d5d5cb85c5fb59114a6617e11a6e8873e117d1f9df5007a6 languageName: node linkType: hard -"@typescript-eslint/tsconfig-utils@npm:8.59.2, @typescript-eslint/tsconfig-utils@npm:^8.59.2": - version: 8.59.2 - resolution: "@typescript-eslint/tsconfig-utils@npm:8.59.2" +"@typescript-eslint/tsconfig-utils@npm:8.62.1, @typescript-eslint/tsconfig-utils@npm:^8.62.1": + version: 8.62.1 + resolution: "@typescript-eslint/tsconfig-utils@npm:8.62.1" peerDependencies: typescript: ">=4.8.4 <6.1.0" - checksum: 7bfbe041ce29db2bc08a6895ba2bbbba37259710978987999a401ea072939fb96f2c11b5a9c5ab3089dbbbbd9089f2b3d2f07f4b7d056f741e8db05f9351305a + checksum: d42585127930631284eed4ba136a1a17fb4f298cf2476748c4221ed63dc1bb45113561167b5fed232bfd38b9e7a7a4e2d2c1de23e1f5e9ffd5a9d4e15fb3b3ce languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:8.59.2": - version: 8.59.2 - resolution: "@typescript-eslint/type-utils@npm:8.59.2" +"@typescript-eslint/type-utils@npm:8.62.1": + version: 8.62.1 + resolution: "@typescript-eslint/type-utils@npm:8.62.1" dependencies: - "@typescript-eslint/types": 8.59.2 - "@typescript-eslint/typescript-estree": 8.59.2 - "@typescript-eslint/utils": 8.59.2 + "@typescript-eslint/types": 8.62.1 + "@typescript-eslint/typescript-estree": 8.62.1 + "@typescript-eslint/utils": 8.62.1 debug: ^4.4.3 ts-api-utils: ^2.5.0 peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: ">=4.8.4 <6.1.0" - checksum: 01361634da6cb8b0f3dac39c06c79d58b806c4c3d838456a7b8efc714473461239dd7e45da485092c3f5dd01c559aba80926cb85d2dd0333ab80200e7af8f527 + checksum: 0ded0807fb9f7be448849da8795c531aabce53109572e7f35526d832f770756969af39cc53e47e51057ab322dd06f4e7764cb1da353a64577f05b3027cf8270b languageName: node linkType: hard -"@typescript-eslint/types@npm:8.59.2, @typescript-eslint/types@npm:^8.59.2": - version: 8.59.2 - resolution: "@typescript-eslint/types@npm:8.59.2" - checksum: eb0fa7423293ccfea4b8649258cb30f3911b1d33cb584d6f6897067cf90ac7c4e66533a6f7a922435646b0d5d7f063ab3d3cf164aec95b9ad07d27c069a244e8 +"@typescript-eslint/types@npm:8.62.1, @typescript-eslint/types@npm:^8.62.1": + version: 8.62.1 + resolution: "@typescript-eslint/types@npm:8.62.1" + checksum: a0e7efd615068c3aaa5bd57334e7fb0a04783560c7dbcbd134e45e07968f14e8d2ccc02566be3f0759e854ddfbfcf817061beab360610f0d7f7943fbedf96640 languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:8.59.2": - version: 8.59.2 - resolution: "@typescript-eslint/typescript-estree@npm:8.59.2" +"@typescript-eslint/typescript-estree@npm:8.62.1": + version: 8.62.1 + resolution: "@typescript-eslint/typescript-estree@npm:8.62.1" dependencies: - "@typescript-eslint/project-service": 8.59.2 - "@typescript-eslint/tsconfig-utils": 8.59.2 - "@typescript-eslint/types": 8.59.2 - "@typescript-eslint/visitor-keys": 8.59.2 + "@typescript-eslint/project-service": 8.62.1 + "@typescript-eslint/tsconfig-utils": 8.62.1 + "@typescript-eslint/types": 8.62.1 + "@typescript-eslint/visitor-keys": 8.62.1 debug: ^4.4.3 minimatch: ^10.2.2 semver: ^7.7.3 @@ -3721,39 +3743,39 @@ __metadata: ts-api-utils: ^2.5.0 peerDependencies: typescript: ">=4.8.4 <6.1.0" - checksum: 58639a5013c6622cae960631ca7e41ca8ff44081d3568ff5e7836f3487d63edf086b9b88d92a27de369e2f20876a14597bc17179a64b9be340a7042b744bff23 + checksum: 41e06bac1fc61d5c1dbf1f657a11fd3ee43cd0ed5d62999a9a5cae54b4eff0fbecd3391e88e7f26f72e9a8d63d5bc420dfb505b196f6685a655d7a8f89fe227e languageName: node linkType: hard -"@typescript-eslint/utils@npm:8.59.2, @typescript-eslint/utils@npm:^8.0.0": - version: 8.59.2 - resolution: "@typescript-eslint/utils@npm:8.59.2" +"@typescript-eslint/utils@npm:8.62.1, @typescript-eslint/utils@npm:^8.0.0": + version: 8.62.1 + resolution: "@typescript-eslint/utils@npm:8.62.1" dependencies: "@eslint-community/eslint-utils": ^4.9.1 - "@typescript-eslint/scope-manager": 8.59.2 - "@typescript-eslint/types": 8.59.2 - "@typescript-eslint/typescript-estree": 8.59.2 + "@typescript-eslint/scope-manager": 8.62.1 + "@typescript-eslint/types": 8.62.1 + "@typescript-eslint/typescript-estree": 8.62.1 peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: ">=4.8.4 <6.1.0" - checksum: 2abd76745be94881b0636bb77bf0fe111bcf1f1fffedf822a67f41bd609a466d8c9cb7f57b553e212721eab123229c26b3ad0e603cbe771f1ddaefde08b64f6e + checksum: 1d68ecf102c9235086e97e4a00b2d090e199f94c71b44b7f69b8d62afcf0cdaffb735df47934df0fe64689c80b772772d0dae956b52484922b27d8dcc4cc9c95 languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:8.59.2": - version: 8.59.2 - resolution: "@typescript-eslint/visitor-keys@npm:8.59.2" +"@typescript-eslint/visitor-keys@npm:8.62.1": + version: 8.62.1 + resolution: "@typescript-eslint/visitor-keys@npm:8.62.1" dependencies: - "@typescript-eslint/types": 8.59.2 + "@typescript-eslint/types": 8.62.1 eslint-visitor-keys: ^5.0.0 - checksum: c0fefa9649c932cf364c67ca0077852229b41e79999bae2e869021621d5ab4dadc3d79bc20a64ed470e693876e11bca38e084e2c91d9b00c0980329c871e6a58 + checksum: 231766bad81adb5e785b8ab7a1a4c643d37be9388160eb3ac4ef73d89ef60de03ec252effcaa1f3050a6c53080298d6ca040095df05d3e229d58c162a521d910 languageName: node linkType: hard "@ungap/structured-clone@npm:^1.3.0": - version: 1.3.0 - resolution: "@ungap/structured-clone@npm:1.3.0" - checksum: 64ed518f49c2b31f5b50f8570a1e37bde3b62f2460042c50f132430b2d869c4a6586f13aa33a58a4722715b8158c68cae2827389d6752ac54da2893c83e480fc + version: 1.3.2 + resolution: "@ungap/structured-clone@npm:1.3.2" + checksum: 5502f0ee4bcc839e9cc1ef146b7db8d605fae32925c6e121beaf568bec0affff2db4bea618167891f450527412efc1adf081e6a6935b9257ef75ccc61dfce2e7 languageName: node linkType: hard @@ -3778,10 +3800,10 @@ __metadata: languageName: node linkType: hard -"abbrev@npm:^4.0.0": - version: 4.0.0 - resolution: "abbrev@npm:4.0.0" - checksum: d0344b63d28e763f259b4898c41bdc92c08e9d06d0da5617d0bbe4d78244e46daea88c510a2f9472af59b031d9060ec1a999653144e793fd029a59dae2f56dc8 +"abbrev@npm:^5.0.0": + version: 5.0.0 + resolution: "abbrev@npm:5.0.0" + checksum: 40526a57545197f1ee0a5cb369508acbd33dbb8a19967850d6d60d9bb392d4034fc56d2572ad863b6a8da810f32eb5553cd05d5af265ea829e168697d9963cde languageName: node linkType: hard @@ -3843,11 +3865,11 @@ __metadata: linkType: hard "acorn@npm:^8.1.0, acorn@npm:^8.11.0, acorn@npm:^8.15.0, acorn@npm:^8.8.1": - version: 8.16.0 - resolution: "acorn@npm:8.16.0" + version: 8.17.0 + resolution: "acorn@npm:8.17.0" bin: acorn: bin/acorn - checksum: bbfa466cd0dbd18b4460a85e9d0fc2f35db999380892403c573261beda91f23836db2aa71fd3ae65e94424ad14ff8e2b7bd37c7a2624278fd89137cd6e448c41 + checksum: 98f0800a48a06e3427cf77a10a0ef7b86366d4ba2fdcde7f72e30430d75e3179d3cbcb253bec263f002e2ea537cbb017c007689370deb4c4ffee74b388d66612 languageName: node linkType: hard @@ -4315,9 +4337,9 @@ __metadata: languageName: node linkType: hard -"babel-preset-expo@npm:~55.0.20": - version: 55.0.20 - resolution: "babel-preset-expo@npm:55.0.20" +"babel-preset-expo@npm:~55.0.23": + version: 55.0.23 + resolution: "babel-preset-expo@npm:55.0.23" dependencies: "@babel/generator": ^7.20.5 "@babel/helper-module-imports": ^7.25.9 @@ -4345,7 +4367,7 @@ __metadata: peerDependencies: "@babel/runtime": ^7.20.0 expo: "*" - expo-widgets: ^55.0.16 + expo-widgets: ^55.0.20 react-refresh: ">=0.14.0 <1.0.0" peerDependenciesMeta: "@babel/runtime": @@ -4354,7 +4376,7 @@ __metadata: optional: true expo-widgets: optional: true - checksum: 35c8b24f594836055acf5896bc66de3b3fc61a94cb45463002364daf06a534f23e894e7369038e43212009826253165ae6e5da3856e6289974916bf3a8bd842a + checksum: 24591065db25ac17aa09ebdc13d16f519079e0dc7437abdb245362ed6d7c4e0e9ed3ea6ffe3fdf94e4b2bbd7fe51f0c800df07150b2641e91670655f8ba1cff8 languageName: node linkType: hard @@ -4391,12 +4413,12 @@ __metadata: languageName: node linkType: hard -"baseline-browser-mapping@npm:^2.10.12": - version: 2.10.27 - resolution: "baseline-browser-mapping@npm:2.10.27" +"baseline-browser-mapping@npm:^2.10.38": + version: 2.10.41 + resolution: "baseline-browser-mapping@npm:2.10.41" bin: baseline-browser-mapping: dist/cli.cjs - checksum: 7b89ecfcf34132b11a8f07249e21ede07363f02136f61b9750d953da22e376002a0a8f01163dfeb57ec7cee9eb32569f24113659f692b194125d89c64ea8156d + checksum: 62a73067953b1b72d1a4b8a7e54bffa542471390c1ea04cc1427b61e6420c204e8ec2ad6129ac264b36eaba67bd7f98fd5ec53e7cf60fbbe6f000ea22fc6dc98 languageName: node linkType: hard @@ -4451,21 +4473,21 @@ __metadata: linkType: hard "brace-expansion@npm:^1.1.7": - version: 1.1.14 - resolution: "brace-expansion@npm:1.1.14" + version: 1.1.15 + resolution: "brace-expansion@npm:1.1.15" dependencies: balanced-match: ^1.0.0 concat-map: 0.0.1 - checksum: 2de747a5891ea0d3a1946ea1ae26e056a47f7ea8d42a3009e1736ec3a31a5aa69a3c5da59d998426773553afe4c258e5b12d7953b534fa7f2cf12ce92eed4931 + checksum: f2a950034e670523cc186da61aabe3beab74b1b8a7c74a756bf6b172dad1917312f255d9ec46906c9f0cab530868095d8c143918576930dd0e1323c3803850f1 languageName: node linkType: hard "brace-expansion@npm:^5.0.5": - version: 5.0.5 - resolution: "brace-expansion@npm:5.0.5" + version: 5.0.7 + resolution: "brace-expansion@npm:5.0.7" dependencies: balanced-match: ^4.0.2 - checksum: 4481b7ffa467b34c14e258167dbd8d9485a2d31d03060e8e8b38142dcde32cdc89c8f55b04d3ae7aae9304fa7eac1dfafd602787cf09c019cc45de3bb6950ffc + checksum: 5739c92d984dfb4b8460e46e52e2a75baa7364261f700f739e0925e9ce7414776d5eb0b10eb19bb10427c444c4114875e1ff1e829fe6a6c3fc490ca4294eb3a0 languageName: node linkType: hard @@ -4479,17 +4501,17 @@ __metadata: linkType: hard "browserslist@npm:^4.24.0, browserslist@npm:^4.25.0, browserslist@npm:^4.28.1": - version: 4.28.2 - resolution: "browserslist@npm:4.28.2" + version: 4.28.4 + resolution: "browserslist@npm:4.28.4" dependencies: - baseline-browser-mapping: ^2.10.12 - caniuse-lite: ^1.0.30001782 - electron-to-chromium: ^1.5.328 - node-releases: ^2.0.36 + baseline-browser-mapping: ^2.10.38 + caniuse-lite: ^1.0.30001799 + electron-to-chromium: ^1.5.376 + node-releases: ^2.0.48 update-browserslist-db: ^1.2.3 bin: browserslist: cli.js - checksum: 702cdd3462b5eb6f8a9bb3bf7bdc6d6a4141ced6935bb44edb7f3d40edd66198775f2b4a9178682535391293e04e625ba2b5943546d692f42ea080323cecb25e + checksum: 5e703da6436325b6cc68c59ea4d84dd04b939b08b5e86e88a8885933a02cf8b9e83e8115ad06a13625ae10e88fa67efb351be5ae04be934e8f4b6bfa6532af2e languageName: node linkType: hard @@ -4604,10 +4626,10 @@ __metadata: languageName: node linkType: hard -"caniuse-lite@npm:^1.0.30001782": - version: 1.0.30001791 - resolution: "caniuse-lite@npm:1.0.30001791" - checksum: 9b2f55d51b85abbb270a0d58c28b8e799ccb8fd5ef017179db3d328c8fead4f1738d95a354e75169af31c0424ffb1b691533722522ff280d4aab05d0fe4eddbd +"caniuse-lite@npm:^1.0.30001799": + version: 1.0.30001800 + resolution: "caniuse-lite@npm:1.0.30001800" + checksum: 347e1394aa05a29f06bfcddc27fc90abdece41fc7be56a2c0ff88ef6b53811ac67d27ae58634893a1f15f4de8a020b29e637ae74c627b7b0e12deb345214b9b4 languageName: node linkType: hard @@ -4944,11 +4966,11 @@ __metadata: linkType: hard "console-table-printer@npm:^2.12.1": - version: 2.15.0 - resolution: "console-table-printer@npm:2.15.0" + version: 2.16.1 + resolution: "console-table-printer@npm:2.16.1" dependencies: simple-wcswidth: ^1.1.2 - checksum: a878e446303eabaa86a2fd7f0d956d24252e0837be23249e7ad24daf3f304ce7bf155ca79b25ccd4f380f31fa35e427adc762e49ff29236f82a96c4b1d88551e + checksum: b953f1a73ebe0162b289567d217aaa4f4075ae73440fb782aa2da17a4f0a83c2f44ccc694e553aaf6f10ebdbcc85f7b851e4e708fd5177cba54da2d4c8aa6ac7 languageName: node linkType: hard @@ -5322,10 +5344,10 @@ __metadata: languageName: node linkType: hard -"dnssd-advertise@npm:^1.1.4": - version: 1.1.4 - resolution: "dnssd-advertise@npm:1.1.4" - checksum: f660bdc0c2716a3ff63c31da2fe64376188f113c763985f791298e70d6f557c9bba9e04bda231df6c0ba40e1a49903b95231e45f717e6b521f9c810bcc333cc0 +"dnssd-advertise@npm:^1.1.6": + version: 1.1.6 + resolution: "dnssd-advertise@npm:1.1.6" + checksum: 5bed966305f6e490029a70840deeed9d5a9d364df747f9f8895ccb9e661bf75de2efdc33d6e2c64f4c41b76605ccdf1d47cf2a6ae384ba7811cd7c2bee6065c9 languageName: node linkType: hard @@ -5413,10 +5435,10 @@ __metadata: languageName: node linkType: hard -"electron-to-chromium@npm:^1.5.328": - version: 1.5.349 - resolution: "electron-to-chromium@npm:1.5.349" - checksum: b2e5f52c63789514ab29d27f830bf7f8a4c44cd58e652e201d48531eb49528a3acaa2809e6dfda913eb0b8c314afaa53ac382b970f599d254ba62802cdcc4080 +"electron-to-chromium@npm:^1.5.376": + version: 1.5.385 + resolution: "electron-to-chromium@npm:1.5.385" + checksum: 34c3f97168401fd396b3a281c961c1b14e78b71598d9d8d0732e5b69526842ee4ff332fa67498f92704056f87aa291d07c23b522563fb288b352f6ff2eb56211 languageName: node linkType: hard @@ -5501,6 +5523,18 @@ __metadata: languageName: node linkType: hard +"es-abstract-get@npm:^1.0.0": + version: 1.0.0 + resolution: "es-abstract-get@npm:1.0.0" + dependencies: + es-errors: ^1.3.0 + es-object-atoms: ^1.1.2 + is-callable: ^1.2.7 + object-inspect: ^1.13.4 + checksum: 625bb67d41ebc22e7585875a6ebb90dc9c153998c9866dc7d50ff7b1b9e178e216c0d23914e5dbfd0addb38aab344c142ad27ff4375c6d2acf095432a4d85fe3 + languageName: node + linkType: hard + "es-abstract@npm:^1.17.5, es-abstract@npm:^1.23.2, es-abstract@npm:^1.23.3, es-abstract@npm:^1.23.5, es-abstract@npm:^1.23.6, es-abstract@npm:^1.23.9, es-abstract@npm:^1.24.0, es-abstract@npm:^1.24.2": version: 1.24.2 resolution: "es-abstract@npm:1.24.2" @@ -5578,8 +5612,8 @@ __metadata: linkType: hard "es-iterator-helpers@npm:^1.2.1": - version: 1.3.2 - resolution: "es-iterator-helpers@npm:1.3.2" + version: 1.3.3 + resolution: "es-iterator-helpers@npm:1.3.3" dependencies: call-bind: ^1.0.9 call-bound: ^1.0.4 @@ -5597,16 +5631,16 @@ __metadata: internal-slot: ^1.1.0 iterator.prototype: ^1.1.5 math-intrinsics: ^1.1.0 - checksum: 3e7f4323af19ac11558e36f2a6fa8f6856d6eab09daf12dc43347695976028ac36561de68767d38b093c29255cb81da3e0d5e53d70302a2a543810999e154b3c + checksum: 0429329d6a77850c4b9b4cd546a7c4e3e49888d6f5ee63255e8516726e56394b1ba7e86aca9dd64523867b0dd744d6a70fdf4cd93be28e9a56d9bb4dfb1fcba2 languageName: node linkType: hard -"es-object-atoms@npm:^1.0.0, es-object-atoms@npm:^1.1.1": - version: 1.1.1 - resolution: "es-object-atoms@npm:1.1.1" +"es-object-atoms@npm:^1.0.0, es-object-atoms@npm:^1.1.1, es-object-atoms@npm:^1.1.2": + version: 1.1.2 + resolution: "es-object-atoms@npm:1.1.2" dependencies: es-errors: ^1.3.0 - checksum: 214d3767287b12f36d3d7267ef342bbbe1e89f899cfd67040309fc65032372a8e60201410a99a1645f2f90c1912c8c49c8668066f6bdd954bcd614dda2e3da97 + checksum: b821f39e4f48bd85b13fee80aea9068a674bcb78b95ff01267aa4da1111f83e6944049b3329b2ffdb3acaa266fb5b8b885476fe8cada05034dc7c87c8016c86d languageName: node linkType: hard @@ -5632,13 +5666,16 @@ __metadata: linkType: hard "es-to-primitive@npm:^1.3.0": - version: 1.3.0 - resolution: "es-to-primitive@npm:1.3.0" + version: 1.3.4 + resolution: "es-to-primitive@npm:1.3.4" dependencies: + es-abstract-get: ^1.0.0 + es-define-property: ^1.0.1 + es-errors: ^1.3.0 is-callable: ^1.2.7 - is-date-object: ^1.0.5 - is-symbol: ^1.0.4 - checksum: 966965880356486cd4d1fe9a523deda2084c81b3702d951212c098f5f2ee93605d1b7c1840062efb48a07d892641c7ed1bc194db563645c0dd2b919cb6d65b93 + is-date-object: ^1.1.0 + is-symbol: ^1.1.1 + checksum: b152ec48ee2f962760751c1c181ef09d2c4483af50fbdd08c0d41004d71014c90ad4339b14d0328a14209d9ee638d13b4190aadc066f19466071cf2af5785d05 languageName: node linkType: hard @@ -5743,8 +5780,8 @@ __metadata: linkType: hard "eslint-plugin-jest@npm:^29.0.1": - version: 29.15.2 - resolution: "eslint-plugin-jest@npm:29.15.2" + version: 29.15.4 + resolution: "eslint-plugin-jest@npm:29.15.4" dependencies: "@typescript-eslint/utils": ^8.0.0 peerDependencies: @@ -5759,16 +5796,16 @@ __metadata: optional: true typescript: optional: true - checksum: a19b13afeb90329860a196f1debb35c696723b3e7c1e308b21c5260cbea94c961e885fbb936d29506f41e644e2d386450089e5caef466c9b51f33ff625d72396 + checksum: 9bfd198df80932230b261b7cbce7d74ade5c71bc234a88ef4c14b46b5d65574047c9ab055c32f2d6b82e33539b03df70f5737630421f70b97062f582ca8ff168 languageName: node linkType: hard "eslint-plugin-prettier@npm:^5.5.5": - version: 5.5.5 - resolution: "eslint-plugin-prettier@npm:5.5.5" + version: 5.5.6 + resolution: "eslint-plugin-prettier@npm:5.5.6" dependencies: prettier-linter-helpers: ^1.0.1 - synckit: ^0.11.12 + synckit: ^0.11.13 peerDependencies: "@types/eslint": ">=8.0.0" eslint: ">=8.0.0" @@ -5779,7 +5816,7 @@ __metadata: optional: true eslint-config-prettier: optional: true - checksum: 49b1c25d75ded255a8707d5f06288ae86e8ab4f8e273d4aabdabf73cd0903848916d5a3598ba8be82f2c8dd06769c5e6c172503b3b9cfb2636b6fc23b9c024fb + checksum: 9e2d94d6631af110966733d19aea15699ef684b543de23d1812423b35a02203ac87ef2d8784f236950ecc4026828e98f00e7c2b41879ee9c974a0a0563f1c754 languageName: node linkType: hard @@ -6078,108 +6115,108 @@ __metadata: linkType: hard "expo-application@npm:~55.0.13": - version: 55.0.14 - resolution: "expo-application@npm:55.0.14" + version: 55.0.16 + resolution: "expo-application@npm:55.0.16" peerDependencies: expo: "*" - checksum: ba4f4dc3f35edeaf142ea676875c3bf179bc4755f1a14c8a43022c080978b04125616d5c7ccb07ddb77ef0590fd16b534abbd420458c3ec1c033d171c5e5744a + checksum: b9893f38c5fa412b00f9bd0942f86950e84b300f72528d2e1f872f57c5b32c44103dff872b021d704108fe33bd55c87a69b153dc97081c4b7145d65b0d01c855 languageName: node linkType: hard -"expo-asset@npm:~55.0.13, expo-asset@npm:~55.0.16": - version: 55.0.16 - resolution: "expo-asset@npm:55.0.16" +"expo-asset@npm:~55.0.13, expo-asset@npm:~55.0.17": + version: 55.0.17 + resolution: "expo-asset@npm:55.0.17" dependencies: - "@expo/image-utils": ^0.8.13 - expo-constants: ~55.0.15 + "@expo/image-utils": ^0.8.14 + expo-constants: ~55.0.16 peerDependencies: expo: "*" react: "*" react-native: "*" - checksum: 4cb2e766709e1dfef927ee2f1e2fceb2badb00ca201034c0703e2c7d981938c0e446708c99b6c24494dd48b4ab38e94be3d9997112c20e2da597165b0e35d84b + checksum: 9a6631af85c9d1cd8e8a70739574c51f096cd6d5565ce75ffd42b56f62d902a4f402ebb48296e12d8cde2c74bcc51eaa129831e703a8668305e03b7343b58da2 languageName: node linkType: hard "expo-clipboard@npm:~55.0.12": - version: 55.0.13 - resolution: "expo-clipboard@npm:55.0.13" + version: 55.0.14 + resolution: "expo-clipboard@npm:55.0.14" peerDependencies: expo: "*" react: "*" react-native: "*" - checksum: f808528b6564ef56e162e3226a9c7f9e80c8f38c893825b864c155f7aa6603c33979701a46a3d63e97dc9d3a7597fa2315af558ca8c14d70a6577d0ba61c3d5e + checksum: 99798863654a69c6f24aa903eb04f4c6e8e781089b27545ba2282422f20f258ea0445b062854a6e0543c914dba745316275eccce72446650424257492324d8e9 languageName: node linkType: hard -"expo-constants@npm:~55.0.12, expo-constants@npm:~55.0.15": - version: 55.0.15 - resolution: "expo-constants@npm:55.0.15" +"expo-constants@npm:~55.0.12, expo-constants@npm:~55.0.16": + version: 55.0.16 + resolution: "expo-constants@npm:55.0.16" dependencies: - "@expo/env": ~2.1.1 + "@expo/env": ~2.1.2 peerDependencies: expo: "*" react-native: "*" - checksum: 1dc50dcd2176c5cc28bb6dcc2a9ad69f47027991bde31577490d9c4a3a03574d473e59d59f9d8ff848fbf4f86d3e1f511ffaeb4113ff6241c53b5d41186bbf4e + checksum: 1a63cf32ae5e6cff91e8b0a10aec05b7eac02a7379339d2879abed24ef29fc14ea64cda60e0175c1c771f2393c9faaf66352a4d2acfd8c681f3944ae3a573f13 languageName: node linkType: hard "expo-device@npm:~55.0.13": - version: 55.0.15 - resolution: "expo-device@npm:55.0.15" + version: 55.0.18 + resolution: "expo-device@npm:55.0.18" dependencies: ua-parser-js: ^0.7.33 peerDependencies: expo: "*" - checksum: a8bbc9a098f9539dbc4f55727542d0800fbf0030fd639c03f837d70080d39ab512772d2720a072ee272b1cc81c22b4241b2598a64451282fd33e97d68aa171a3 + checksum: 6d682cec97de8559c374b1d670e7cdc00de93900db4d68ccdd0d46115c2e4a1b7e9be000495ec565ce3283dec21eac79578f9fb4e929df65c729c919e763976f languageName: node linkType: hard "expo-document-picker@npm:~55.0.12": - version: 55.0.13 - resolution: "expo-document-picker@npm:55.0.13" + version: 55.0.14 + resolution: "expo-document-picker@npm:55.0.14" peerDependencies: expo: "*" - checksum: cb4ab011acbd618e81e82f6e78c5a8b4fbe675be0dafca82b86acbc8b2f25b92fa9619863890b6bd5726335032badea38a67b82bb938257ade5f17c51b8f7e53 + checksum: e4ed85f0edb64aaee41a1125d27193b2372683551025e0d9909b63ab4f7c46eb6e280b6315cdb36f6425ff19aa11ee34c4d5ffc6c2c1559edd5ad7c2964773bd languageName: node linkType: hard -"expo-file-system@npm:~55.0.15, expo-file-system@npm:~55.0.17": - version: 55.0.17 - resolution: "expo-file-system@npm:55.0.17" +"expo-file-system@npm:~55.0.15, expo-file-system@npm:~55.0.23": + version: 55.0.23 + resolution: "expo-file-system@npm:55.0.23" peerDependencies: expo: "*" react-native: "*" - checksum: a99a3adada21f28fb4cb79f74ecb9340ae92fb1783c8fe3f3b6e190fd17b1acd383c0fdd7a211cb908b70d91e75cc1d4b256cde15f82dc94a686939c0833fdf1 + checksum: c20f79958336613b4030b963d8f11def801890f4565d621fd95448edbd88adf4f7b499af171f7504e5f743be8bd04e095bd141f99eb64885840437a768e013a5 languageName: node linkType: hard -"expo-font@npm:~55.0.6": - version: 55.0.6 - resolution: "expo-font@npm:55.0.6" +"expo-font@npm:~55.0.6, expo-font@npm:~55.0.8": + version: 55.0.8 + resolution: "expo-font@npm:55.0.8" dependencies: fontfaceobserver: ^2.1.0 peerDependencies: expo: "*" react: "*" react-native: "*" - checksum: 6a2e237e94cd5fd9217fdb2c5e09b53f2ebc5fd50bc60266eb33c3c14df2e235220bd4a33d4c08e3fe0a8d8bcfdd00cd21f527fe3af5c5fe5398442b4ebbbd83 + checksum: d07da9d1903e0809733fbd11d606127b7be45db88be922c423ea0b70b5504934e65517e432a4245cf980ffb93c3e4f27148de3fd1a2bba8107080a51190bf6a6 languageName: node linkType: hard -"expo-glass-effect@npm:^55.0.10": - version: 55.0.10 - resolution: "expo-glass-effect@npm:55.0.10" +"expo-glass-effect@npm:^55.0.11": + version: 55.0.11 + resolution: "expo-glass-effect@npm:55.0.11" peerDependencies: expo: "*" react: "*" react-native: "*" - checksum: 33723ce6393baefb592816848acc36a64f7a6debfc08fb29b493e08733cddc890e10eeca38dca3d9d49c1f93a677d731d018673a370454a02d4dbe55ac54e180 + checksum: c50f669d09ebf77a290dae785f743ad3dde404f805b360245bdae4f5fe34a6e5f54e606ea5b0d4902058f34e949409fbcb5debfd2e67fd2b982f802a3ed7f31f languageName: node linkType: hard -"expo-image@npm:^55.0.9": - version: 55.0.9 - resolution: "expo-image@npm:55.0.9" +"expo-image@npm:^55.0.11": + version: 55.0.11 + resolution: "expo-image@npm:55.0.11" dependencies: sf-symbols-typescript: ^2.2.0 peerDependencies: @@ -6190,73 +6227,73 @@ __metadata: peerDependenciesMeta: react-native-web: optional: true - checksum: 0eff68e47ff8dd97908cdbbd26ea8670b25178269d3e877980e9d9f347da41553c221b10e1d3102a4906ea2764d128138522ab73799ff673256eaf3fa08f5102 + checksum: b16a23f067f28f27d065aefb67e9000b3321739b96f1cca4fba607f51c60999d9aafe0087ed239a80fd337fb942b331d7d6213a0201a34f1bb0f456e9126e5bb languageName: node linkType: hard -"expo-keep-awake@npm:~55.0.7": - version: 55.0.7 - resolution: "expo-keep-awake@npm:55.0.7" +"expo-keep-awake@npm:~55.0.8": + version: 55.0.8 + resolution: "expo-keep-awake@npm:55.0.8" peerDependencies: expo: "*" react: "*" - checksum: ebf0e671cc6e8682694a9bff8b4883014cbf770257658d0b382b24c33bb41b6ad872bc06b801474e4ad52f3be561ed369bc28ee010da65744e49194a62e60256 + checksum: 9887b54578f3b4a00a6160db6a6834df8b56c1a3fee817e0178177c73a0aea12fa1aa8c1fb35f2981497c4954e1388c6467ff7aeed0050d6adda5514606bbb81 languageName: node linkType: hard "expo-linear-gradient@npm:~55.0.13": - version: 55.0.13 - resolution: "expo-linear-gradient@npm:55.0.13" + version: 55.0.15 + resolution: "expo-linear-gradient@npm:55.0.15" peerDependencies: expo: "*" react: "*" react-native: "*" - checksum: 4a77242b3d8d12ad1c3e7254c1e95dc66f8c8552bf0d0cc5f31f716933a60242c5cf269d7958a69a05824a0f185e81a71c5a0a3181328d9029fd57c1d64aef0f + checksum: a83b8afb25d81c48c55b1b8d014da34a83c985eb0d3cdb9b436ca970351ec66c0ad93708511952140f08284577782e29a16a45ca4d3e62cebb7a9dcd9c278628 languageName: node linkType: hard "expo-linking@npm:~55.0.11": - version: 55.0.14 - resolution: "expo-linking@npm:55.0.14" + version: 55.0.16 + resolution: "expo-linking@npm:55.0.16" dependencies: - expo-constants: ~55.0.15 + expo-constants: ~55.0.16 invariant: ^2.2.4 peerDependencies: react: "*" react-native: "*" - checksum: cdd706a133c3a84e4329d2968d46b42a06ab46d2b8db9725013727dfbeeb65587683a6e8be70c8bcb01e61e453ca2141625ef7e02eb0f4cd6e3fb3bc53a10b09 + checksum: 3c43d0e80b0bcfb54d1b34dfc07735ce8920fe0237983c1c5b35362c96c1545f2bd80a7eb3929de333b88299771caa2f1937016bcc36afdcb90bce0c8355ac7d languageName: node linkType: hard "expo-localization@npm:~55.0.12": - version: 55.0.13 - resolution: "expo-localization@npm:55.0.13" + version: 55.0.16 + resolution: "expo-localization@npm:55.0.16" dependencies: rtl-detect: ^1.0.2 peerDependencies: expo: "*" react: "*" - checksum: 0d7dfdcc37a61962563a3a974987b6ae56c1e42cd872bd73e0bc1f533dc98ee29ef5230212ab9b5f2488fc24bba519a3d8041e62d6f827681b6b4ef803816199 + checksum: 6fa5674032ed0367cb6623c7841081e1821d2b707f12a7228987d86f6e47c3ee2ac149bd34d608ff6ae1a5472efdb68b1c75cd8528ffb696c3163b526c47409c languageName: node linkType: hard -"expo-modules-autolinking@npm:55.0.19": - version: 55.0.19 - resolution: "expo-modules-autolinking@npm:55.0.19" +"expo-modules-autolinking@npm:55.0.24": + version: 55.0.24 + resolution: "expo-modules-autolinking@npm:55.0.24" dependencies: - "@expo/require-utils": ^55.0.4 + "@expo/require-utils": ^55.0.5 "@expo/spawn-async": ^1.7.2 chalk: ^4.1.0 commander: ^7.2.0 bin: expo-modules-autolinking: bin/expo-modules-autolinking.js - checksum: 94198ba663500d1004e14236170fbd6ffc5cd914a47fa8ef7932818185204fe4d37c38b30d6e27014a6306bc250c9a57d131ab3fd235be6b66a01248e1e68100 + checksum: 07e19dc23bd2cbd12c14c327b8bca7472d95c3bb97845709d187c884185ca3886057bdaf1c9193fc1d020b75cfafa878d87a8ec244ce061f549f8165ca0e6f47 languageName: node linkType: hard -"expo-modules-core@npm:55.0.24": - version: 55.0.24 - resolution: "expo-modules-core@npm:55.0.24" +"expo-modules-core@npm:55.0.25": + version: 55.0.25 + resolution: "expo-modules-core@npm:55.0.25" dependencies: invariant: ^2.2.4 peerDependencies: @@ -6266,7 +6303,7 @@ __metadata: peerDependenciesMeta: react-native-worklets: optional: true - checksum: b9091a109f8adfa658c189fe2ed84cee47a58c7a7910ec7fcaec127f0a4d72e8e773b7f5e7b1ed77d65056f00ace3725b2889860fbfb42f1be43cfe5bde01287 + checksum: cce87b2db8ad38629dc16378eb2b61d2f05adb6bf9c64f1c5dc946d9affc70b11b4e9fc624219998c2e329a7a0c98bb36422e14fb4db7403b48eb1cd82ffb0b9 languageName: node linkType: hard @@ -6282,11 +6319,11 @@ __metadata: linkType: hard "expo-router@npm:~55.0.11": - version: 55.0.13 - resolution: "expo-router@npm:55.0.13" + version: 55.0.16 + resolution: "expo-router@npm:55.0.16" dependencies: - "@expo/metro-runtime": ^55.0.10 - "@expo/schema-utils": ^55.0.3 + "@expo/metro-runtime": ^55.0.11 + "@expo/schema-utils": ^55.0.4 "@radix-ui/react-slot": ^1.2.0 "@radix-ui/react-tabs": ^1.1.12 "@react-navigation/bottom-tabs": ^7.15.5 @@ -6295,10 +6332,10 @@ __metadata: client-only: ^0.0.1 debug: ^4.3.4 escape-string-regexp: ^4.0.0 - expo-glass-effect: ^55.0.10 - expo-image: ^55.0.9 - expo-server: ^55.0.8 - expo-symbols: ^55.0.7 + expo-glass-effect: ^55.0.11 + expo-image: ^55.0.11 + expo-server: ^55.0.11 + expo-symbols: ^55.0.9 fast-deep-equal: ^3.1.3 invariant: ^2.2.4 nanoid: ^3.3.8 @@ -6312,13 +6349,13 @@ __metadata: use-latest-callback: ^0.2.1 vaul: ^1.1.2 peerDependencies: - "@expo/log-box": 55.0.11 - "@expo/metro-runtime": ^55.0.10 + "@expo/log-box": 55.0.12 + "@expo/metro-runtime": ^55.0.11 "@react-navigation/drawer": ^7.9.4 "@testing-library/react-native": ">= 13.2.0" expo: "*" - expo-constants: ^55.0.15 - expo-linking: ^55.0.14 + expo-constants: ^55.0.16 + expo-linking: ^55.0.15 react: "*" react-dom: "*" react-native: "*" @@ -6343,81 +6380,81 @@ __metadata: optional: true react-server-dom-webpack: optional: true - checksum: 44f480720a04a948c5ed41fbcbe4c82fb4a359fcec797cb37ccb854f08e7ff595ded557234cad49069f46bebdc44eb6ef2fc08732aab41e277efd5d03edc1e6f + checksum: c2168aa0dc34573d5006557ebaa56df61d9bb1e2f329330d97da3b8137de505ade9173361d6cace50e00e96e7efd95c7876ef9c933196ebbc3256aa17da252f4 languageName: node linkType: hard -"expo-server@npm:^55.0.8": - version: 55.0.8 - resolution: "expo-server@npm:55.0.8" - checksum: 7cc0f3178b3a6a7499c19a7d890f1748fea5a46f504a6d96aa9f99b71dd28e3afb1d10427b024c1aae08bab2a660abeffb7bc36a7a188ccdf1432ad4bd3728a6 +"expo-server@npm:^55.0.11": + version: 55.0.11 + resolution: "expo-server@npm:55.0.11" + checksum: 01e3dcf3fd6289c4ffefbc93d9da6ca957ca0b31fe5f4c808c5643a595c8b90c56abc32deb42c00a9ca0e183ae9449af2d5f0b647d3b026900a047c252c8e292 languageName: node linkType: hard "expo-sharing@npm:~55.0.17": - version: 55.0.18 - resolution: "expo-sharing@npm:55.0.18" + version: 55.0.21 + resolution: "expo-sharing@npm:55.0.21" dependencies: - "@expo/config-plugins": ^55.0.8 + "@expo/config-plugins": ^55.0.10 "@expo/config-types": ^55.0.5 - "@expo/plist": ^0.5.2 + "@expo/plist": ^0.5.4 peerDependencies: expo: "*" react: "*" react-native: "*" - checksum: bdb5507b4f45d00324c821df165f5df3f8b1f9b10a8de4ef41732ac406ccf74d6376096c77821250d6a08c3b59291b513bbcb3ee0b0b108378e93eb0c1906a7d + checksum: 01dc173ed0da4b26b71b0444f58eea5bf15c88003226448ab73e046d853028aa329ba3bba54acb47253b049a9ece0261e3f1829c7d626eaab657cdd4dd0dbcf7 languageName: node linkType: hard "expo-splash-screen@npm:~55.0.16": - version: 55.0.19 - resolution: "expo-splash-screen@npm:55.0.19" + version: 55.0.22 + resolution: "expo-splash-screen@npm:55.0.22" dependencies: - "@expo/prebuild-config": ^55.0.16 + "@expo/prebuild-config": ^55.0.19 peerDependencies: expo: "*" - checksum: 2044e2921e55d6a96537f2fa70a10cb16c34521f61cf94cc4f306a2177a771eceb475e1ab8f20643294c99a9dfee06e4586adbfd38e44f54da0ec19ce0d5f290 + checksum: acb75a4807d599363caf63d19edfe7c6d62d5b046c66625edb0edf228fc92c91978df8893d50e1b6c7e96332837d84200415e97fd628bcdf74b760f543e02b68 languageName: node linkType: hard "expo-sqlite@npm:~55.0.14": - version: 55.0.15 - resolution: "expo-sqlite@npm:55.0.15" + version: 55.0.17 + resolution: "expo-sqlite@npm:55.0.17" dependencies: await-lock: ^2.2.2 peerDependencies: expo: "*" react: "*" react-native: "*" - checksum: 540789025301373928fbf6d9af0c7db486e923ac1539993e2cc5658bb6eae4d68a9fa91e7a34dfb0c7bf11d10163edbfb4a3d40b2ca8f7cfcf04fcba8c653d20 + checksum: bab30afecc6f45e30cf69e94599ee2207345fa4c682a96a19dfd8d2e5c33416dad7983398d111f6ab7013172e522bd092e2dfeb5d587b1f36e5e973b975f45fc languageName: node linkType: hard "expo-status-bar@npm:~55.0.5": - version: 55.0.5 - resolution: "expo-status-bar@npm:55.0.5" + version: 55.0.6 + resolution: "expo-status-bar@npm:55.0.6" dependencies: react-native-is-edge-to-edge: ^1.2.1 peerDependencies: react: "*" react-native: "*" - checksum: df1ff626f879432e8064a15f312da255d11520cd6a0a8c1e38ba603388ef7237d745f323a7c23a0ce0b59ee4a444d93269ab0cedd7f5fc0ad3d26e167333b887 + checksum: e380f16cc3475a674c20280f784d2bbaced0292634408d4defdfed50ad7bc029f791e5a143c91bf9e24564c7a21697b337ac96d5ff60acf17f9f0c17ab5a7976 languageName: node linkType: hard "expo-store-review@npm:~55.0.12": - version: 55.0.13 - resolution: "expo-store-review@npm:55.0.13" + version: 55.0.15 + resolution: "expo-store-review@npm:55.0.15" peerDependencies: expo: "*" react-native: "*" - checksum: 73de754f7cc1373bdc660cd702be11f9f06d97a609e237bb8085a1b3983ccb470711c94d4adc1490a37085ef45af3caddcfbb022d877721a7f276c7d82dc9a95 + checksum: 56ac12cd458cdefd1d9c63da8f004890b0f2b872016f6f9d64f1eb732a19f31bd65e658bfd38c238e0c5dff1aeba6a91fddfe09f1442db6bd38f1883b40c4ec2 languageName: node linkType: hard -"expo-symbols@npm:^55.0.7": - version: 55.0.7 - resolution: "expo-symbols@npm:55.0.7" +"expo-symbols@npm:^55.0.9": + version: 55.0.9 + resolution: "expo-symbols@npm:55.0.9" dependencies: "@expo-google-fonts/material-symbols": ^0.4.1 sf-symbols-typescript: ^2.0.0 @@ -6426,37 +6463,37 @@ __metadata: expo-font: "*" react: "*" react-native: "*" - checksum: 9afe72809d6873dbc3bc02ed369abe50ac473f3174278b047ef550af06b0e504a1e53306998efc6c97967d26868f8833666b2b3499e2e6ed267f286632f96eb7 + checksum: 49b9fa1bf75661386e98b40e734792f1e626bf625533e860e47fd15a008fbd3b6c13b56dd398be85ff19fbab09880ff323f57ad1b56cf948e8f9fab5782706c9 languageName: node linkType: hard "expo@npm:^55.0.0": - version: 55.0.20 - resolution: "expo@npm:55.0.20" + version: 55.0.27 + resolution: "expo@npm:55.0.27" dependencies: "@babel/runtime": ^7.20.0 - "@expo/cli": 55.0.28 - "@expo/config": ~55.0.15 - "@expo/config-plugins": ~55.0.8 - "@expo/devtools": 55.0.2 - "@expo/fingerprint": 0.16.6 - "@expo/local-build-cache-provider": 55.0.11 - "@expo/log-box": 55.0.11 + "@expo/cli": 55.0.33 + "@expo/config": ~55.0.18 + "@expo/config-plugins": ~55.0.10 + "@expo/devtools": 55.0.3 + "@expo/fingerprint": 0.16.7 + "@expo/local-build-cache-provider": 55.0.14 + "@expo/log-box": 55.0.12 "@expo/metro": ~55.1.1 - "@expo/metro-config": 55.0.19 + "@expo/metro-config": 55.0.24 "@expo/vector-icons": ^15.0.2 "@ungap/structured-clone": ^1.3.0 - babel-preset-expo: ~55.0.20 - expo-asset: ~55.0.16 - expo-constants: ~55.0.15 - expo-file-system: ~55.0.17 - expo-font: ~55.0.6 - expo-keep-awake: ~55.0.7 - expo-modules-autolinking: 55.0.19 - expo-modules-core: 55.0.24 + babel-preset-expo: ~55.0.23 + expo-asset: ~55.0.17 + expo-constants: ~55.0.16 + expo-file-system: ~55.0.23 + expo-font: ~55.0.8 + expo-keep-awake: ~55.0.8 + expo-modules-autolinking: 55.0.24 + expo-modules-core: 55.0.25 pretty-format: ^29.7.0 react-refresh: ^0.14.2 - whatwg-url-minimum: ^0.1.1 + whatwg-url-minimum: ^0.1.2 peerDependencies: "@expo/dom-webview": "*" "@expo/metro-runtime": "*" @@ -6474,7 +6511,7 @@ __metadata: expo: bin/cli expo-modules-autolinking: bin/autolinking fingerprint: bin/fingerprint - checksum: 820d845d15507af54e845d67c63073a50599f066f80fa21e68f1fa22937987f109ae767f452f1ee9438d36ac1aee7605c788bbc2cb3c5c40a270f7028ffec4ba + checksum: ebfc534488a4fa4296b732135462afcefedddb619976e461b624f6845e21a3fe765e237d2ba8c399733b8606a8163ed26f602911f93f14828f450f68d4d06b7b languageName: node linkType: hard @@ -6654,10 +6691,19 @@ __metadata: languageName: node linkType: hard +"flow-estree@npm:0.321.0": + version: 0.321.0 + resolution: "flow-estree@npm:0.321.0" + checksum: 01e1119b21a8be08e7dad9749eadac7d06cae3a01bf0bdf5bbc3da0f2036cbb3ff2fb5f6bcced280cd2c1142e3c272d7a07c53571e4f7e001b54274485205491 + languageName: node + linkType: hard + "flow-parser@npm:0.*": - version: 0.312.1 - resolution: "flow-parser@npm:0.312.1" - checksum: 9753525da80a7a5342092fbf18f72950664eec28ca56daed384de0dc2a32f983baa8a0a0c7da36bea098d69b920c457a6656cc1e69b61367192aadd2faa061c8 + version: 0.321.0 + resolution: "flow-parser@npm:0.321.0" + dependencies: + flow-estree: 0.321.0 + checksum: a3b6290dbf4125e785920f3603597013de2f1992626d95ee8fdca4fac4dbe5c0fbac343b408dae134696a31be5fd40032beea1f1081d2b578cf4228569f6d70a languageName: node linkType: hard @@ -6678,15 +6724,15 @@ __metadata: linkType: hard "form-data@npm:^4.0.0": - version: 4.0.5 - resolution: "form-data@npm:4.0.5" + version: 4.0.6 + resolution: "form-data@npm:4.0.6" dependencies: asynckit: ^0.4.0 combined-stream: ^1.0.8 es-set-tostringtag: ^2.1.0 - hasown: ^2.0.2 - mime-types: ^2.1.12 - checksum: af8328413c16d0cded5fccc975a44d227c5120fd46a9e81de8acf619d43ed838414cc6d7792195b30b248f76a65246949a129a4dadd148721948f90cd6d4fb69 + hasown: ^2.0.4 + mime-types: ^2.1.35 + checksum: e51b9e97678c250c872cd4ec3e5eaa8fa43bee4b1acf8274c337308aebc6aebb0553091ce0810612826601ffafed9dace12504a63c6ef16c57fffcd7dcfec457 languageName: node linkType: hard @@ -6731,16 +6777,19 @@ __metadata: linkType: hard "function.prototype.name@npm:^1.1.6, function.prototype.name@npm:^1.1.8": - version: 1.1.8 - resolution: "function.prototype.name@npm:1.1.8" + version: 1.2.0 + resolution: "function.prototype.name@npm:1.2.0" dependencies: - call-bind: ^1.0.8 - call-bound: ^1.0.3 - define-properties: ^1.2.1 + call-bind: ^1.0.9 + call-bound: ^1.0.4 + es-define-property: ^1.0.1 + es-errors: ^1.3.0 functions-have-names: ^1.2.3 - hasown: ^2.0.2 + has-property-descriptors: ^1.0.2 + hasown: ^2.0.4 is-callable: ^1.2.7 - checksum: 3a366535dc08b25f40a322efefa83b2da3cd0f6da41db7775f2339679120ef63b6c7e967266182609e655b8f0a8f65596ed21c7fd72ad8bd5621c2340edd4010 + is-document.all: ^1.0.0 + checksum: 297f6966f6fe1ff756ec7f51956420273d55186697769ff8e815578c10bb9cc27de5e4383cc0da703873b988ad634b74b167f1f9f8ef6a04f7f096f8088fde5d languageName: node linkType: hard @@ -6773,9 +6822,9 @@ __metadata: linkType: hard "get-east-asian-width@npm:^1.0.0, get-east-asian-width@npm:^1.3.1, get-east-asian-width@npm:^1.5.0": - version: 1.5.0 - resolution: "get-east-asian-width@npm:1.5.0" - checksum: 60bc34cd1e975055ab99f0f177e31bed3e516ff7cee9c536474383954a976abaa6b94a51d99ad158ef1e372790fa096cab7d07f166bb0778f6587954c0fbe946 + version: 1.6.0 + resolution: "get-east-asian-width@npm:1.6.0" + checksum: 88faf98aaade2b24ad260be369b026d979ff4d0e6201bfd991654da17f2aa720bbddb812ce5612110eef6b469d8370c863f69a2f9818d61733f1eb2a83d779b8 languageName: node linkType: hard @@ -6969,12 +7018,12 @@ __metadata: languageName: node linkType: hard -"hasown@npm:^2.0.2": - version: 2.0.3 - resolution: "hasown@npm:2.0.3" +"hasown@npm:^2.0.2, hasown@npm:^2.0.3, hasown@npm:^2.0.4": + version: 2.0.4 + resolution: "hasown@npm:2.0.4" dependencies: function-bind: ^1.1.2 - checksum: bb06756a13dc4e6d1f45993c86c23f12d167c6c30a7dcc907aec5042300b4eb255615a0e5ed2c65014b93bf8bfcff111d991032c5c01ddefb340aa64b329bd55 + checksum: 4bd8f916b629e06324853593ffbdd45e200022952a85ad0c967f3bd4c2e4c7e1f9a9766fbe6186f60bd394e0afc73e719730caa1da15cd9bd832b7cdf53fd26c languageName: node linkType: hard @@ -7354,12 +7403,12 @@ __metadata: languageName: node linkType: hard -"is-core-module@npm:^2.16.1": - version: 2.16.1 - resolution: "is-core-module@npm:2.16.1" +"is-core-module@npm:^2.16.1, is-core-module@npm:^2.16.2": + version: 2.16.2 + resolution: "is-core-module@npm:2.16.2" dependencies: - hasown: ^2.0.2 - checksum: 6ec5b3c42d9cbf1ac23f164b16b8a140c3cec338bf8f884c076ca89950c7cc04c33e78f02b8cae7ff4751f3247e3174b2330f1fe4de194c7210deb8b1ea316a7 + hasown: ^2.0.3 + checksum: 9317844b4959f8fb268bfc1b4e24033d60058235c2e7273499c2abfd8e4510e7059b1339bd9109766293747daa3e0b5a89095fb2825a866a4093563fe8fdf16f languageName: node linkType: hard @@ -7374,7 +7423,7 @@ __metadata: languageName: node linkType: hard -"is-date-object@npm:^1.0.5, is-date-object@npm:^1.1.0": +"is-date-object@npm:^1.1.0": version: 1.1.0 resolution: "is-date-object@npm:1.1.0" dependencies: @@ -7400,6 +7449,15 @@ __metadata: languageName: node linkType: hard +"is-document.all@npm:^1.0.0": + version: 1.0.0 + resolution: "is-document.all@npm:1.0.0" + dependencies: + call-bound: ^1.0.4 + checksum: 383175789df98503dc0c15d39e80932b21cbec839b8840d7b75dc36db942e9dd2da0f36c464ea1aa2e751f5808c38e97bbf288b76d8114d5e570f49df26ed930 + languageName: node + linkType: hard + "is-extglob@npm:^2.1.1": version: 2.1.1 resolution: "is-extglob@npm:2.1.1" @@ -7560,7 +7618,7 @@ __metadata: languageName: node linkType: hard -"is-symbol@npm:^1.0.4, is-symbol@npm:^1.1.1": +"is-symbol@npm:^1.1.1": version: 1.1.1 resolution: "is-symbol@npm:1.1.1" dependencies: @@ -7825,15 +7883,15 @@ __metadata: languageName: node linkType: hard -"jest-diff@npm:30.3.0": - version: 30.3.0 - resolution: "jest-diff@npm:30.3.0" +"jest-diff@npm:30.4.1": + version: 30.4.1 + resolution: "jest-diff@npm:30.4.1" dependencies: - "@jest/diff-sequences": 30.3.0 + "@jest/diff-sequences": 30.4.0 "@jest/get-type": 30.1.0 chalk: ^4.1.2 - pretty-format: 30.3.0 - checksum: ad49d2c602a8006725cb507143ffa6f19eb355a56ad7dffc10361ce51f74dee103db9233e1a1aa7020d8dae138ec071034ba05391bc1b0e738b69a4a994dbf29 + pretty-format: 30.4.1 + checksum: f3e58eb102992d6cf2ab6737337f2f2ea7fdf28dbdaf2d7ed8ded08ef954a947ee7f641ef5e6e0f8b14b3c93c99c9428084e1d217e4febd2e3683373a6e57323 languageName: node linkType: hard @@ -7907,11 +7965,11 @@ __metadata: linkType: hard "jest-expo@npm:~55.0.14": - version: 55.0.16 - resolution: "jest-expo@npm:55.0.16" + version: 55.0.19 + resolution: "jest-expo@npm:55.0.19" dependencies: - "@expo/config": ~55.0.15 - "@expo/json-file": ^10.0.13 + "@expo/config": ~55.0.18 + "@expo/json-file": ^10.0.15 "@jest/create-cache-key-function": ^29.2.1 "@jest/globals": ^29.2.1 babel-jest: ^29.2.1 @@ -7933,7 +7991,7 @@ __metadata: optional: true bin: jest: bin/jest.js - checksum: 465c1b5fc758cd9499cf3b6a256c064836589b58fa0d63165ca8cd27e0e2ad3d930d322df94aca41a25ffef75a0490de8202919ec724e352e8fbe4df7179e69a + checksum: 74e73f7e78173b285e197a9ced521647336773c91d0fbf418bc1b43be7286ec210610a60d70dadb040a365245c896a445c9fec01a75185a352e4d33784f234f0 languageName: node linkType: hard @@ -7990,14 +8048,14 @@ __metadata: linkType: hard "jest-matcher-utils@npm:^30.0.5": - version: 30.3.0 - resolution: "jest-matcher-utils@npm:30.3.0" + version: 30.4.1 + resolution: "jest-matcher-utils@npm:30.4.1" dependencies: "@jest/get-type": 30.1.0 chalk: ^4.1.2 - jest-diff: 30.3.0 - pretty-format: 30.3.0 - checksum: 3bc01ef81d001519fef75a32a0420c207664a829acbdc668bfa3c51e0a3ac2ddbb19c633e1e006ff63840bf231d915dfbe8dccef71e5ee842221ba1ff0da1946 + jest-diff: 30.4.1 + pretty-format: 30.4.1 + checksum: 18bc7a8ee2ce2fec06823e5ca231bd3c68f44f36143f6b738f6e191a60373dd3e5f595780f1b098d6945267b662368d1cd418f899fd6dbf07c3a9f3ba0673566 languageName: node linkType: hard @@ -8289,25 +8347,25 @@ __metadata: linkType: hard "js-yaml@npm:^3.13.1": - version: 3.14.2 - resolution: "js-yaml@npm:3.14.2" + version: 3.15.0 + resolution: "js-yaml@npm:3.15.0" dependencies: argparse: ^1.0.7 esprima: ^4.0.0 bin: js-yaml: bin/js-yaml.js - checksum: 626fc207734a3452d6ba84e1c8c226240e6d431426ed94d0ab043c50926d97c509629c08b1d636f5d27815833b7cfd225865631da9fb33cb957374490bf3e90b + checksum: 0209830c3ce51cec4c2cefee4b2b68c547b56b36920004efbf7c6a91ae7eee784bf64341738ef1acffa975d16e7882e9b69234b9a4411f455b72b33d4d74771e languageName: node linkType: hard "js-yaml@npm:^4.1.0, js-yaml@npm:^4.1.1": - version: 4.1.1 - resolution: "js-yaml@npm:4.1.1" + version: 4.3.0 + resolution: "js-yaml@npm:4.3.0" dependencies: argparse: ^2.0.1 bin: js-yaml: bin/js-yaml.js - checksum: ea2339c6930fe048ec31b007b3c90be2714ab3e7defcc2c27ebf30c74fd940358f29070b4345af0019ef151875bf3bc3f8644bea1bab0372652b5044813ac02d + checksum: 1268fb22ae8504646cedc0823c18816d4af056029578e855b9a4d3e19bcafc3e9ba9e391f70dea4a29b023bb09f23639766071199cb89e853266b90a6ec25f9a languageName: node linkType: hard @@ -8442,11 +8500,11 @@ __metadata: linkType: hard "jsonrepair@npm:^3.12.0": - version: 3.14.0 - resolution: "jsonrepair@npm:3.14.0" + version: 3.14.1 + resolution: "jsonrepair@npm:3.14.1" bin: jsonrepair: bin/cli.js - checksum: 750431a74488052baa281d9104b96f850e978925191d35266b45acd2e2070ec125f21147e375a1c14cc65bc8369f370f933c75f0f4c256a58309021799966ba7 + checksum: a1526116217740a48320a032fe2a565a70fa16e2933ea0f28a72a64834803b477f83033198aa0d641855320e822e1d3bdc6a9ad153f456f59c72c505b9967759 languageName: node linkType: hard @@ -8819,9 +8877,9 @@ __metadata: linkType: hard "lru-cache@npm:^11.0.0": - version: 11.3.6 - resolution: "lru-cache@npm:11.3.6" - checksum: 0bc3ad1cb4c91c8baa532f3bede807a63d36156cc066a6f0e0a44d2dc317f94aa19a92bc11ce0f9933d00ea90995e2e61a0b12a21e1c5f3cc38d4b28956870d4 + version: 11.5.1 + resolution: "lru-cache@npm:11.5.1" + checksum: d92b10275051c5139ee8087bd71b18718f0bd1a964f80b9ddc65dc31eecee0bcedc59b29e7f876fcc3ffce7a9823d7789977ebc4ab247a382a6e55f8895c85a2 languageName: node linkType: hard @@ -9392,7 +9450,7 @@ __metadata: languageName: node linkType: hard -"mime-types@npm:^2.1.12, mime-types@npm:^2.1.27, mime-types@npm:~2.1.34": +"mime-types@npm:^2.1.27, mime-types@npm:^2.1.35, mime-types@npm:~2.1.34": version: 2.1.35 resolution: "mime-types@npm:2.1.35" dependencies: @@ -9538,12 +9596,12 @@ __metadata: languageName: node linkType: hard -"nanoid@npm:^3.3.1, nanoid@npm:^3.3.11, nanoid@npm:^3.3.7, nanoid@npm:^3.3.8": - version: 3.3.12 - resolution: "nanoid@npm:3.3.12" +"nanoid@npm:^3.3.1, nanoid@npm:^3.3.11, nanoid@npm:^3.3.12, nanoid@npm:^3.3.8": + version: 3.3.15 + resolution: "nanoid@npm:3.3.15" bin: nanoid: bin/nanoid.cjs - checksum: 38699257447dc59e21e73e0510d0dfb16b7a610d9ca80633d5c3a68f9b4298c990513d30404ca8f163c2d03225ee01695ff8898bea6179183f38f0477b7635ac + checksum: 0f645685aefba48aae06acfb390be660041d91b4ec63d85544ed01b619c6d661c985170bfbcd29b7265b6c1c3369c631e5dcb2f34c34e2c0023108bc158531d1 languageName: node linkType: hard @@ -9602,14 +9660,14 @@ __metadata: linkType: hard "node-exports-info@npm:^1.6.0": - version: 1.6.0 - resolution: "node-exports-info@npm:1.6.0" + version: 1.6.2 + resolution: "node-exports-info@npm:1.6.2" dependencies: array.prototype.flatmap: ^1.3.3 es-errors: ^1.3.0 object.entries: ^1.1.9 semver: ^6.3.1 - checksum: 6bb93ec7ae95717aa2a2c315a5df1f7efa9f0592ee6fcde83256e112db33b59f0942d4188e154e84ec03f9de2d5ea62aa278e2d57b8624f6434168e8d7701e44 + checksum: d8642f3dc0c03023a0249ab737ab052796b7ae6d51401b49cfcd0c8c41a22dc464e8aeb3207d6d2ea1f3807f37000bc88bcfad111e4ec191d0f9e1eafa768804 languageName: node linkType: hard @@ -9621,22 +9679,22 @@ __metadata: linkType: hard "node-gyp@npm:latest": - version: 12.3.0 - resolution: "node-gyp@npm:12.3.0" + version: 13.0.1 + resolution: "node-gyp@npm:13.0.1" dependencies: env-paths: ^2.2.0 exponential-backoff: ^3.1.1 graceful-fs: ^4.2.6 - nopt: ^9.0.0 - proc-log: ^6.0.0 + nopt: ^10.0.0 + proc-log: ^7.0.0 semver: ^7.3.5 tar: ^7.5.4 tinyglobby: ^0.2.12 - undici: ^6.25.0 - which: ^6.0.0 + undici: ^8.4.1 + which: ^7.0.0 bin: node-gyp: bin/node-gyp.js - checksum: b02e8776908a83f25b8df88f0b79c46b425f9b7f442ebe3c4a50b9820128c1b44df6b386214c73d509964995d820edbda94bb0c811b6b60a686231afb699acf7 + checksum: 9d4e0eeaa415ca33a85278ecf2bdb61a7c32649e9423ec7743912b97e58169606ef44504f7d3a746cd9179f50c8d37d5bb70ae76fbc4803122f7747e819453f6 languageName: node linkType: hard @@ -9647,21 +9705,21 @@ __metadata: languageName: node linkType: hard -"node-releases@npm:^2.0.36": - version: 2.0.38 - resolution: "node-releases@npm:2.0.38" - checksum: fe5af7b5928d06783534b38d0c55e3467b719a8a53acc2fd15f7f2d2ef4cedb38ae411cce59e2c10027827650c81897c41045e742131b9b5e4d118ce1b307025 +"node-releases@npm:^2.0.48": + version: 2.0.50 + resolution: "node-releases@npm:2.0.50" + checksum: fec78f8154182875bb35f3bb93eb178e2f51523b64f674ce5a0b72b73d3c058021c0224e97a707b45c82985f7ba24db68fff6b2bb9b08e7661d3a3e131cf85ea languageName: node linkType: hard -"nopt@npm:^9.0.0": - version: 9.0.0 - resolution: "nopt@npm:9.0.0" +"nopt@npm:^10.0.0": + version: 10.0.1 + resolution: "nopt@npm:10.0.1" dependencies: - abbrev: ^4.0.0 + abbrev: ^5.0.0 bin: nopt: bin/nopt.js - checksum: 7a5d9ab0629eaec1944a95438cc4efa6418ed2834aa8eb21a1bea579a7d8ac3e30120131855376a96ef59ab0e23ad8e0bc94d3349770a95e5cb7119339f7c7fb + checksum: c2903b9171a3293b731189ace4eaeacc2ed8191bb8f8f74ebfb61babc0de2ff158d97a215fd561070ed0dac567f3b8741955f81a534009e57eb37fdc419d5d4e languageName: node linkType: hard @@ -9710,9 +9768,9 @@ __metadata: linkType: hard "nwsapi@npm:^2.2.2": - version: 2.2.23 - resolution: "nwsapi@npm:2.2.23" - checksum: 7af519de08381df9dc0c913d817255cb21e33671641603f6cdabe8cb04b18b32aca1477fdc5dfe08b2039125afa3216d3ef01a3c2603a97d114e842d9414e0c3 + version: 2.2.24 + resolution: "nwsapi@npm:2.2.24" + checksum: c0c4016cc6117748d35a97ca12076b472de0626fa55d1e2d1e54401d24e38f1b9cef743955cbcdb9eb0fcc37ec16924a8a8b66bada52d295dbdf6da3f5108c58 languageName: node linkType: hard @@ -10144,9 +10202,9 @@ __metadata: linkType: hard "picomatch@npm:^4.0.3, picomatch@npm:^4.0.4": - version: 4.0.4 - resolution: "picomatch@npm:4.0.4" - checksum: 76b387b5157951422fa6049a96bdd1695e39dd126cd99df34d343638dc5cdb8bcdc83fff288c23eddcf7c26657c35e3173d4d5f488c4f28b889b314472e0a662 + version: 4.0.5 + resolution: "picomatch@npm:4.0.5" + checksum: 44f305aa44f33c15d304f2e887409961d1c14eebd465b5553d8aefddcfe7b5e40d208148adef8c21a21183dda2fdba1e83c8a56918e3b5e5d3db6bcaba5b7e12 languageName: node linkType: hard @@ -10200,21 +10258,21 @@ __metadata: languageName: node linkType: hard -"possible-typed-array-names@npm:^1.0.0": +"possible-typed-array-names@npm:^1.0.0, possible-typed-array-names@npm:^1.1.0": version: 1.1.0 resolution: "possible-typed-array-names@npm:1.1.0" checksum: cfcd4f05264eee8fd184cd4897a17890561d1d473434b43ab66ad3673d9c9128981ec01e0cb1d65a52cd6b1eebfb2eae1e53e39b2e0eca86afc823ede7a4f41b languageName: node linkType: hard -"postcss@npm:~8.4.32": - version: 8.4.49 - resolution: "postcss@npm:8.4.49" +"postcss@npm:^8.5.14": + version: 8.5.16 + resolution: "postcss@npm:8.5.16" dependencies: - nanoid: ^3.3.7 + nanoid: ^3.3.12 picocolors: ^1.1.1 source-map-js: ^1.2.1 - checksum: eb5d6cbdca24f50399aafa5d2bea489e4caee4c563ea1edd5a2485bc5f84e9ceef3febf170272bc83a99c31d23a316ad179213e853f34c2a7a8ffa534559d63a + checksum: 636fd45f028f487d4873d1d30a541b910e2477d77e5ca368e9f8535fad12b2c20bfc508516fd35258e33214d51dd84b1e089539b9d78737aed528abb1db036a5 languageName: node linkType: hard @@ -10235,22 +10293,23 @@ __metadata: linkType: hard "prettier@npm:^3.8.1": - version: 3.8.3 - resolution: "prettier@npm:3.8.3" + version: 3.9.4 + resolution: "prettier@npm:3.9.4" bin: prettier: bin/prettier.cjs - checksum: f696d5b93b6aa7da53b7e94fd1ac8789915297a67d142159a367901a57a417a25f54500823d26b5580573d388f634d7860bef10922944c47a231194d1613394a + checksum: 96b0125a8af78e1e3c2c3f916a768934a542d5fa64bf1819cbef5c9cb081b793ebe99eb202f98e230741595be9d14d73819380bd91988a7df422d704a97bb38c languageName: node linkType: hard -"pretty-format@npm:30.3.0, pretty-format@npm:^30.0.5": - version: 30.3.0 - resolution: "pretty-format@npm:30.3.0" +"pretty-format@npm:30.4.1, pretty-format@npm:^30.0.5": + version: 30.4.1 + resolution: "pretty-format@npm:30.4.1" dependencies: - "@jest/schemas": 30.0.5 + "@jest/schemas": 30.4.1 ansi-styles: ^5.2.0 - react-is: ^18.3.1 - checksum: 99bb09b51551fb710143a2c0b8270acc2f7723d51cdb5824fe55a0061e04af8bf07c47acd565121cb5b266dfc7a3ecfe77bf85b4f96aacbbeb6f2df0ba246a9c + react-is-18: "npm:react-is@^18.3.1" + react-is-19: "npm:react-is@^19.2.5" + checksum: 9602635027892d7a2f430b0a51972780226d30ce4072643349ad376ccee967539eb8180a656976976c0673d550604847da638b992eefc95c84bab7cebdf9faba languageName: node linkType: hard @@ -10356,10 +10415,10 @@ __metadata: languageName: node linkType: hard -"proc-log@npm:^6.0.0": - version: 6.1.0 - resolution: "proc-log@npm:6.1.0" - checksum: ac450ff8244e95b0c9935b52d629fef92ae69b7e39aea19972a8234259614d644402dd62ce9cb094f4a637d8a4514cba90c1456ad785a40ad5b64d502875a817 +"proc-log@npm:^7.0.0": + version: 7.0.0 + resolution: "proc-log@npm:7.0.0" + checksum: ded2e976dbfa428777496158db93efdd27523ce17cf7eae0e87c6dbba0f30bb46bbe6409ccdcf5ecea7bdea864ed409afc3b5c60b79f008d42dff79fdd481c27 languageName: node linkType: hard @@ -10484,24 +10543,24 @@ __metadata: languageName: node linkType: hard -"react-is@npm:^16.13.1, react-is@npm:^16.7.0": - version: 16.13.1 - resolution: "react-is@npm:16.13.1" - checksum: f7a19ac3496de32ca9ae12aa030f00f14a3d45374f1ceca0af707c831b2a6098ef0d6bdae51bd437b0a306d7f01d4677fcc8de7c0d331eb47ad0f46130e53c5f - languageName: node - linkType: hard - -"react-is@npm:^18.0.0, react-is@npm:^18.3.1": +"react-is-18@npm:react-is@^18.3.1, react-is@npm:^18.0.0": version: 18.3.1 resolution: "react-is@npm:18.3.1" checksum: e20fe84c86ff172fc8d898251b7cc2c43645d108bf96d0b8edf39b98f9a2cae97b40520ee7ed8ee0085ccc94736c4886294456033304151c3f94978cec03df21 languageName: node linkType: hard -"react-is@npm:^19.1.0, react-is@npm:^19.2.0": - version: 19.2.5 - resolution: "react-is@npm:19.2.5" - checksum: 303f022cdec5e3dee3c0ad731ed54b3db41b8aa8ef9503d37904a7acb404dcc656049b19119d37af4d2526a6921c7ad1675b8b3da76402664af56f8e198fd38b +"react-is-19@npm:react-is@^19.2.5, react-is@npm:^19.1.0, react-is@npm:^19.2.0": + version: 19.2.7 + resolution: "react-is@npm:19.2.7" + checksum: 148ee03b481ace8370cb9b96de386598423ed44ad36090c45ce3f905aec496db6f2675da720bbe43f39d66c43c9a16757cb71e9e55217825707dae97f0cf200f + languageName: node + linkType: hard + +"react-is@npm:^16.13.1, react-is@npm:^16.7.0": + version: 16.13.1 + resolution: "react-is@npm:16.13.1" + checksum: f7a19ac3496de32ca9ae12aa030f00f14a3d45374f1ceca0af707c831b2a6098ef0d6bdae51bd437b0a306d7f01d4677fcc8de7c0d331eb47ad0f46130e53c5f languageName: node linkType: hard @@ -10528,9 +10587,9 @@ __metadata: languageName: node linkType: hard -"react-native-drawer-layout@npm:^4.2.2": - version: 4.2.2 - resolution: "react-native-drawer-layout@npm:4.2.2" +"react-native-drawer-layout@npm:^4.2.5": + version: 4.2.5 + resolution: "react-native-drawer-layout@npm:4.2.5" dependencies: color: ^4.2.3 use-latest-callback: ^0.2.4 @@ -10539,7 +10598,7 @@ __metadata: react-native: "*" react-native-gesture-handler: ">= 2.0.0" react-native-reanimated: ">= 2.0.0" - checksum: aa15d18270ecaf88dcffdc6c144bc703b4dff26b79f28fbe3d770480ff237ff89e6d58b15daaa72f2db667d8c27ec5e6cb69228fd4c18156d97b2f2d6d140a89 + checksum: 9b29b0736d62f5aa6f8d367f026e5c4b32365277c15d7d03f0cb75001ea621123753ca40eb38b603c0630b939d355c61b700e83594e720569e1c808dcff76af4 languageName: node linkType: hard @@ -10653,13 +10712,13 @@ __metadata: linkType: hard "react-native-pulsar@npm:^1.4.0": - version: 1.4.0 - resolution: "react-native-pulsar@npm:1.4.0" + version: 1.6.1 + resolution: "react-native-pulsar@npm:1.6.1" peerDependencies: react: "*" react-native: "*" react-native-worklets: "*" - checksum: dc816947c205c301b15a219d0126ae32e7c0a46908839014b6f4ab9b13e49ef6ff4fa578e088652b77ceabea99f4439a741f0b688dcc9e4d4ddde723a5d0a55f + checksum: 020ffd948b3541a500f8ddbbf96fe5002a1b97e14fa9cf6e3637d7f5a2b55424f121c6436122f932cc45a4a2e4b1004485dcfd17ed585cbc47fe29a255a6468c languageName: node linkType: hard @@ -10876,7 +10935,7 @@ __metadata: languageName: node linkType: hard -"react-remove-scroll@npm:^2.6.3": +"react-remove-scroll@npm:^2.7.2": version: 2.7.2 resolution: "react-remove-scroll@npm:2.7.2" dependencies: @@ -10952,7 +11011,7 @@ __metadata: languageName: node linkType: hard -"reflect.getprototypeof@npm:^1.0.6, reflect.getprototypeof@npm:^1.0.9": +"reflect.getprototypeof@npm:^1.0.10, reflect.getprototypeof@npm:^1.0.9": version: 1.0.10 resolution: "reflect.getprototypeof@npm:1.0.10" dependencies: @@ -11027,13 +11086,13 @@ __metadata: linkType: hard "regjsparser@npm:^0.13.0": - version: 0.13.1 - resolution: "regjsparser@npm:0.13.1" + version: 0.13.2 + resolution: "regjsparser@npm:0.13.2" dependencies: jsesc: ~3.1.0 bin: regjsparser: bin/parser - checksum: 7a4e60e1487b6a0702e35540f882c0c6e0151f7f567c6a4c480c5397a3cab05f6d2bf5f64cdbcdf341e41caf232cae801a4db9b531c26eed3ca946b3c50ccb34 + checksum: 20ece45a3e9f63c8a3fa50fbcde7a3cc35d97c244ca9bc1c94d0907d4f6eb3f50bb7326e60f848649024d7cd6ff89c9a75a36d6faded0cf225677a3d13914a31 languageName: node linkType: hard @@ -11117,18 +11176,18 @@ __metadata: linkType: hard "resolve@npm:^2.0.0-next.5": - version: 2.0.0-next.6 - resolution: "resolve@npm:2.0.0-next.6" + version: 2.0.0-next.7 + resolution: "resolve@npm:2.0.0-next.7" dependencies: es-errors: ^1.3.0 - is-core-module: ^2.16.1 + is-core-module: ^2.16.2 node-exports-info: ^1.6.0 object-keys: ^1.1.1 path-parse: ^1.0.7 supports-preserve-symlinks-flag: ^1.0.0 bin: resolve: bin/resolve - checksum: bc5a4f8f4dd7e1a3d2d8cdd2818b7cc3334283d2ef067f462d2ab3a4ab8f969d69438d7553268f59a2b5b4c1b42d18fabb3241a6d0279276ab578ba74455822e + checksum: 62d58c4b2fcd820da8ef4e0f37f14daaac3f1dd1bdc73b4c1b750c4705676a28f30281784d340a60240206f3398a7001d0feca35735bacdc90e01de6ebf606b8 languageName: node linkType: hard @@ -11147,18 +11206,18 @@ __metadata: linkType: hard "resolve@patch:resolve@^2.0.0-next.5#~builtin": - version: 2.0.0-next.6 - resolution: "resolve@patch:resolve@npm%3A2.0.0-next.6#~builtin::version=2.0.0-next.6&hash=c3c19d" + version: 2.0.0-next.7 + resolution: "resolve@patch:resolve@npm%3A2.0.0-next.7#~builtin::version=2.0.0-next.7&hash=c3c19d" dependencies: es-errors: ^1.3.0 - is-core-module: ^2.16.1 + is-core-module: ^2.16.2 node-exports-info: ^1.6.0 object-keys: ^1.1.1 path-parse: ^1.0.7 supports-preserve-symlinks-flag: ^1.0.0 bin: resolve: bin/resolve - checksum: 514c6d4e5e7249f8a93e776724b22c72090ecedb3cb6846ba14c591e918716bb41b2f857e4ce47c8bd88e068aca85f6a8f70f1c5abecc16d345bf00f3a587fb9 + checksum: 6385a1797ae12043ecdbdeed8d39da9099417edf3504495df4f395889cc3548afc1f71b51ae05b3235e82cef3551215b5e5b89cf5c9fb6360cd804df519b76bf languageName: node linkType: hard @@ -11315,11 +11374,11 @@ __metadata: linkType: hard "semver@npm:^7.1.3, semver@npm:^7.3.5, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.3, semver@npm:^7.7.3": - version: 7.7.4 - resolution: "semver@npm:7.7.4" + version: 7.8.5 + resolution: "semver@npm:7.8.5" bin: semver: bin/semver.js - checksum: 9b4a6a58e98b9723fafcafa393c9d4e8edefaa60b8dfbe39e30892a3604cf1f45f52df9cfb1ae1a22b44c8b3d57fec8a9bb7b3e1645431587cb272399ede152e + checksum: 0c580f17e88e2b45806dc5cf3d824f719c946999d3554bf30307c2b68b3300ab3c8bfcd84d8f489b93b4cbb5362f5fc8de4d2858954f18d834253c4450fc9f6b languageName: node linkType: hard @@ -11463,13 +11522,13 @@ __metadata: linkType: hard "shell-quote@npm:^1.6.1": - version: 1.8.3 - resolution: "shell-quote@npm:1.8.3" - checksum: 550dd84e677f8915eb013d43689c80bb114860649ec5298eb978f40b8f3d4bc4ccb072b82c094eb3548dc587144bb3965a8676f0d685c1cf4c40b5dc27166242 + version: 1.9.0 + resolution: "shell-quote@npm:1.9.0" + checksum: 2ec85fb903121c684c3536d051e377e98746cf88525e08c298baaa7b42c168de75c3201cef00db66556e89e340a8cf26bd4c18e7a3fd78799449a21cac43da2e languageName: node linkType: hard -"side-channel-list@npm:^1.0.0": +"side-channel-list@npm:^1.0.1": version: 1.0.1 resolution: "side-channel-list@npm:1.0.1" dependencies: @@ -11505,15 +11564,15 @@ __metadata: linkType: hard "side-channel@npm:^1.1.0": - version: 1.1.0 - resolution: "side-channel@npm:1.1.0" + version: 1.1.1 + resolution: "side-channel@npm:1.1.1" dependencies: es-errors: ^1.3.0 - object-inspect: ^1.13.3 - side-channel-list: ^1.0.0 + object-inspect: ^1.13.4 + side-channel-list: ^1.0.1 side-channel-map: ^1.0.1 side-channel-weakmap: ^1.0.2 - checksum: bf73d6d6682034603eb8e99c63b50155017ed78a522d27c2acec0388a792c3ede3238b878b953a08157093b85d05797217d270b7666ba1f111345fbe933380ff + checksum: e0f217140c463636ee556260bbc8f47ba2931f1248826f58e1502704135814c763b325dd9ab122a04176aa45b4a4f8cc556415bcab7a0179d85250fb7812f063 languageName: node linkType: hard @@ -11733,6 +11792,13 @@ __metadata: languageName: node linkType: hard +"standard-navigation@npm:^0.0.7": + version: 0.0.7 + resolution: "standard-navigation@npm:0.0.7" + checksum: 7c04912a93cd09d3e19c8c47a52f7808b53bc1b6fb897e21261e3401cb4340d505e9e4e387ac50662aa0534cdb2d091c03aeb5efc72fcd678925a3af5bbfce74 + languageName: node + linkType: hard + "statuses@npm:~1.5.0": version: 1.5.0 resolution: "statuses@npm:1.5.0" @@ -11869,29 +11935,30 @@ __metadata: linkType: hard "string.prototype.trim@npm:^1.2.10": - version: 1.2.10 - resolution: "string.prototype.trim@npm:1.2.10" + version: 1.2.11 + resolution: "string.prototype.trim@npm:1.2.11" dependencies: - call-bind: ^1.0.8 - call-bound: ^1.0.2 + call-bind: ^1.0.9 + call-bound: ^1.0.4 define-data-property: ^1.1.4 define-properties: ^1.2.1 - es-abstract: ^1.23.5 - es-object-atoms: ^1.0.0 + es-abstract: ^1.24.2 + es-object-atoms: ^1.1.2 has-property-descriptors: ^1.0.2 - checksum: 87659cd8561237b6c69f5376328fda934693aedde17bb7a2c57008e9d9ff992d0c253a391c7d8d50114e0e49ff7daf86a362f7961cf92f7564cd01342ca2e385 + safe-regex-test: ^1.1.0 + checksum: 1aa0868afe15a54e781cd7fdcc851af4b0d71522108006f136ba35ecfde65b042496ca6926f85192923e6e9287750b772afb13860db15574bf1da454343db0ca languageName: node linkType: hard "string.prototype.trimend@npm:^1.0.9": - version: 1.0.9 - resolution: "string.prototype.trimend@npm:1.0.9" + version: 1.0.10 + resolution: "string.prototype.trimend@npm:1.0.10" dependencies: - call-bind: ^1.0.8 - call-bound: ^1.0.2 + call-bind: ^1.0.9 + call-bound: ^1.0.4 define-properties: ^1.2.1 - es-object-atoms: ^1.0.0 - checksum: cb86f639f41d791a43627784be2175daa9ca3259c7cb83e7a207a729909b74f2ea0ec5d85de5761e6835e5f443e9420c6ff3f63a845378e4a61dd793177bc287 + es-object-atoms: ^1.1.2 + checksum: 17684796ccd12accafaef0f7cafe7a88891e4a57ff8deb5a40665dbe627fb3bed2252f1b9f3b16da2229aa41ba24e082178b44afe91a0302d570149730be1ccb languageName: node linkType: hard @@ -12045,25 +12112,25 @@ __metadata: languageName: node linkType: hard -"synckit@npm:^0.11.12": - version: 0.11.12 - resolution: "synckit@npm:0.11.12" +"synckit@npm:^0.11.13": + version: 0.11.13 + resolution: "synckit@npm:0.11.13" dependencies: - "@pkgr/core": ^0.2.9 - checksum: a53fb563d01ba8912a111b883fc3c701e267896ff8273e7aba9001f5f74711e125888f4039e93060795cd416122cf492ae419eb10a6a3e3b00e830917669d2cf + "@pkgr/core": ^0.3.6 + checksum: ec989ed45f3df2e7eb3141f00060669fbc6cac5649846d0579f336cee4ab047c8bac65baa75b4df991e1c1bd224675b10efbe69af3596ead5180ec81a6d4ec06 languageName: node linkType: hard "tar@npm:^7.5.4": - version: 7.5.13 - resolution: "tar@npm:7.5.13" + version: 7.5.19 + resolution: "tar@npm:7.5.19" dependencies: "@isaacs/fs-minipass": ^4.0.0 chownr: ^3.0.0 minipass: ^7.1.2 minizlib: ^3.1.0 yallist: ^5.0.0 - checksum: adcc2a9179dab1b36ecb26575e698d2df8491a1df2cc83e2a0fdd8eaefd076da60dd2e20383a37760b5790bee34e9291aa2b2a9b3deef37ff03c1046219e5df7 + checksum: 72bedc26089d2b3372f5c833f2e69b1ce9a2e8138a461cbb4b2eab48bef2453b3a12e17d47f6a1883a6929cdc348c622179b016ffed765ce21da51e43f2f4209 languageName: node linkType: hard @@ -12087,8 +12154,8 @@ __metadata: linkType: hard "terser@npm:^5.15.0": - version: 5.46.2 - resolution: "terser@npm:5.46.2" + version: 5.48.0 + resolution: "terser@npm:5.48.0" dependencies: "@jridgewell/source-map": ^0.3.3 acorn: ^8.15.0 @@ -12096,7 +12163,7 @@ __metadata: source-map-support: ~0.5.20 bin: terser: bin/terser - checksum: ce43b30bc91e7b552dd7dde95f0cf7c0dfd3b726c13c97ebb9b23ff524069ad171f9e08b0e0542fb0822c43a018f050b35b5ba445733f8ac7693f0c7f061408b + checksum: ae4f3216af2c2abf145de7fbc212c0559ae61c66abc97cc63faec49e5da82ca79d551eb69c05717dfbe87c5a3c8960f47b52d26a73f077edf9f2e38b03fdeeef languageName: node linkType: hard @@ -12119,19 +12186,19 @@ __metadata: linkType: hard "tinyexec@npm:^1.0.4": - version: 1.1.2 - resolution: "tinyexec@npm:1.1.2" - checksum: be2cb2b60c415bf9ef2006f86b566774445ea59249b62edc293996299d0b235a14b3ec41bc11e942914287306e5d2c390a1f47cbf781cf3e96833312f0dca6bf + version: 1.2.4 + resolution: "tinyexec@npm:1.2.4" + checksum: 3004b0f784c17d35a87251059dd6d81685848472a21a8c258f7b6c0433a25e37552f3955b0844f422895711fae24f44a8b1165597b8b0fe3fe8d0a25264fe9d9 languageName: node linkType: hard "tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.15": - version: 0.2.16 - resolution: "tinyglobby@npm:0.2.16" + version: 0.2.17 + resolution: "tinyglobby@npm:0.2.17" dependencies: fdir: ^6.5.0 picomatch: ^4.0.4 - checksum: db9d22ce1deb1095720a683c492cd5e80da0f71fed21ed697e2752f6f298edd8a1249dab197c86a26f001c180594a81bf532400fe519791ed2a2cb57b03bc337 + checksum: 041e73eae568152c376551b21b8a27909d474166a8f405cdb0345991c50cf6afd0f878d7a387645d9c05d8ea2c9a55cc2fd2cfe6c5d5a5264770972b1adcad86 languageName: node linkType: hard @@ -12272,31 +12339,31 @@ __metadata: linkType: hard "typed-array-length@npm:^1.0.7": - version: 1.0.7 - resolution: "typed-array-length@npm:1.0.7" + version: 1.0.8 + resolution: "typed-array-length@npm:1.0.8" dependencies: - call-bind: ^1.0.7 - for-each: ^0.3.3 - gopd: ^1.0.1 - is-typed-array: ^1.1.13 - possible-typed-array-names: ^1.0.0 - reflect.getprototypeof: ^1.0.6 - checksum: deb1a4ffdb27cd930b02c7030cb3e8e0993084c643208e52696e18ea6dd3953dfc37b939df06ff78170423d353dc8b10d5bae5796f3711c1b3abe52872b3774c + call-bind: ^1.0.9 + for-each: ^0.3.5 + gopd: ^1.2.0 + is-typed-array: ^1.1.15 + possible-typed-array-names: ^1.1.0 + reflect.getprototypeof: ^1.0.10 + checksum: 612a90b6c86fed3aa8de7be20e05fd1fcdf4a5a0f2ce7a04da664b8f5b1ff2fb74a50f91d67dea9dbf2e526fbaac4046093fc0314af4e28533a7d1052d37fbd6 languageName: node linkType: hard "typescript-eslint@npm:^8.58.1": - version: 8.59.2 - resolution: "typescript-eslint@npm:8.59.2" + version: 8.62.1 + resolution: "typescript-eslint@npm:8.62.1" dependencies: - "@typescript-eslint/eslint-plugin": 8.59.2 - "@typescript-eslint/parser": 8.59.2 - "@typescript-eslint/typescript-estree": 8.59.2 - "@typescript-eslint/utils": 8.59.2 + "@typescript-eslint/eslint-plugin": 8.62.1 + "@typescript-eslint/parser": 8.62.1 + "@typescript-eslint/typescript-estree": 8.62.1 + "@typescript-eslint/utils": 8.62.1 peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: ">=4.8.4 <6.1.0" - checksum: 5e51f8d6049dfce4379ad02b19a79b782d947a357c180192b872fd9c8b153dd73161a43e6b74bef4cbad4c142f1630e945585d63689b8d1bc923048689bd078a + checksum: 4cd1349fcf738bfd5012bcfebc4624d736e0e43813a0947263dfc29d42983b97ce19b32f24ba987b8108e67b6bb9e144c9587d50b088d997fa87bf8f66fa7d16 languageName: node linkType: hard @@ -12341,17 +12408,17 @@ __metadata: languageName: node linkType: hard -"undici-types@npm:~7.19.0": - version: 7.19.2 - resolution: "undici-types@npm:7.19.2" - checksum: f721026160e1f068a982401d0272b872819c335a2f64783c235ddd37a65ccd94327ec24489cee4556d57c77c14bd68ced60efa5def11cf11e3991f5ebf5e0e72 +"undici-types@npm:~8.3.0": + version: 8.3.0 + resolution: "undici-types@npm:8.3.0" + checksum: bdc1d54d8b48f3f2a801f7707a2ad53b76711039dd9a276382d4bdbbded79355574309425096127a42313731bde7a76cc299855fc06872797edd0acbfcef5bc0 languageName: node linkType: hard -"undici@npm:^6.25.0": - version: 6.25.0 - resolution: "undici@npm:6.25.0" - checksum: aed372e1b0f16045696c878e46b03e97dfd1c6dd650fb2355d48adeecc730c990ab15ab2de5a5855dbfe04c9af403a3d4f702234d3e25e72c475d1fb3a72fcfe +"undici@npm:^8.4.1": + version: 8.6.0 + resolution: "undici@npm:8.6.0" + checksum: 2f18f803e1c4aff66ba31f8b6deb0917db2a7321aef4e2244a5d6093dfc349c10c300ac57ecb30f6d7e927437dfd8a3ed6d63221ffbb01d1e9bb56b31bf7a75e languageName: node linkType: hard @@ -12615,10 +12682,10 @@ __metadata: languageName: node linkType: hard -"whatwg-url-minimum@npm:^0.1.1": - version: 0.1.1 - resolution: "whatwg-url-minimum@npm:0.1.1" - checksum: 0f6629c5ea0d4518f3f3f9dff4441d59bce5655e30291dcedc68b1ffd2e1c8fe8e21e5a83609d197560e75bdbf626b1b020be24b95874418dd0e7ec98ada9e06 +"whatwg-url-minimum@npm:^0.1.2": + version: 0.1.2 + resolution: "whatwg-url-minimum@npm:0.1.2" + checksum: 31a92ddf3c20b77ca85a253cd54e1bf85e0ddf8f01cccc031c38164415ab383b95000f89d1edc338316dc73cab2a758f1cf39fe41f2b0a0b79d3301badd59e8e languageName: node linkType: hard @@ -12679,17 +12746,17 @@ __metadata: linkType: hard "which-typed-array@npm:^1.1.16, which-typed-array@npm:^1.1.19": - version: 1.1.20 - resolution: "which-typed-array@npm:1.1.20" + version: 1.1.22 + resolution: "which-typed-array@npm:1.1.22" dependencies: available-typed-arrays: ^1.0.7 - call-bind: ^1.0.8 + call-bind: ^1.0.9 call-bound: ^1.0.4 for-each: ^0.3.5 get-proto: ^1.0.1 gopd: ^1.2.0 has-tostringtag: ^1.0.2 - checksum: 82527027127c3a6f7b278b5c0059605b968bec780d1ddd7c0ce3c2172ae4b9d2217486123107e31d229ff57ed8cc2bc76d751f290f392ee6d3aa27b26d2ffc12 + checksum: b81581da1a730f9149948f98034af956c2ee68d757b44c2b026d521922a1d73191bde874db6a6519c98e263e4255e24f2b77dadb6a2aa9c0c03174e6063a9f37 languageName: node linkType: hard @@ -12704,14 +12771,14 @@ __metadata: languageName: node linkType: hard -"which@npm:^6.0.0": - version: 6.0.1 - resolution: "which@npm:6.0.1" +"which@npm:^7.0.0": + version: 7.0.0 + resolution: "which@npm:7.0.0" dependencies: isexe: ^4.0.0 bin: node-which: bin/which.js - checksum: dbea77c7d3058bf6c78bf9659d2dce4d2b57d39a15b826b2af6ac2e5a219b99dc8a831b79fdbc453c0598adb4f3f84cf9c2491fd52beb9f5d2dececcad117f68 + checksum: 913a43ac10df37602ba9795a004dd7ab12ba7dd592aca1f08ec333be1fdd6a49bbf119a88c3f8d0ea70eeb6251726e77069251424d73000299a0a840ed000732 languageName: node linkType: hard @@ -12773,8 +12840,8 @@ __metadata: linkType: hard "ws@npm:^7, ws@npm:^7.5.10": - version: 7.5.10 - resolution: "ws@npm:7.5.10" + version: 7.5.11 + resolution: "ws@npm:7.5.11" peerDependencies: bufferutil: ^4.0.1 utf-8-validate: ^5.0.2 @@ -12783,13 +12850,13 @@ __metadata: optional: true utf-8-validate: optional: true - checksum: f9bb062abf54cc8f02d94ca86dcd349c3945d63851f5d07a3a61c2fcb755b15a88e943a63cf580cbdb5b74436d67ef6b67f745b8f7c0814e411379138e1863cb + checksum: 3f32457a6019e6875cc0c2a037d4b4789da22a55b7be278e78845ec7614d28eaab9b36994cfdf9a18a48fe02f11c496168354b96b6ce202f65fe414cdf96f08c languageName: node linkType: hard "ws@npm:^8.11.0, ws@npm:^8.12.1": - version: 8.20.0 - resolution: "ws@npm:8.20.0" + version: 8.21.0 + resolution: "ws@npm:8.21.0" peerDependencies: bufferutil: ^4.0.1 utf-8-validate: ">=5.0.2" @@ -12798,7 +12865,7 @@ __metadata: optional: true utf-8-validate: optional: true - checksum: 2b31d24a53690770564a033c21ea48390f84d23fbc5abc14b2bbec4e112846f2f3ca66caee769a73fb8bc89ba16b452a6911a553e9742bbc75bccb79e203953e + checksum: 83ff89ae011bc5c3c5605a45a0d50e12589143c7500ca4de83a8d43b3cd26e71f422cb3206fd1a9e6d541d666eeb66255c30d095d62d413b3c7afe5d2c5cb928 languageName: node linkType: hard @@ -12872,11 +12939,11 @@ __metadata: linkType: hard "yaml@npm:^2.6.1, yaml@npm:^2.8.2": - version: 2.8.4 - resolution: "yaml@npm:2.8.4" + version: 2.9.0 + resolution: "yaml@npm:2.9.0" bin: yaml: bin.mjs - checksum: 04658713380cecf0b2c13fce255a5ea39c694e4b38a3e8416834e1dc84b70e4cebe0f3d8cbaa5f14901f581b904739b977b0fb6d04aa2e0e0ac95857e2ac37f0 + checksum: f0f74b349c126bb9acf649be1e7efaec5869b1567bdc047e43b176bcdd7730a9bc7ab8fc9c6b1c358e611fab0fe0a0d8933393f1af383a7dc0b4755a1d5f31f9 languageName: node linkType: hard @@ -12888,8 +12955,8 @@ __metadata: linkType: hard "yargs@npm:^17.3.1, yargs@npm:^17.6.2": - version: 17.7.2 - resolution: "yargs@npm:17.7.2" + version: 17.7.3 + resolution: "yargs@npm:17.7.3" dependencies: cliui: ^8.0.1 escalade: ^3.1.1 @@ -12898,7 +12965,7 @@ __metadata: string-width: ^4.2.3 y18n: ^5.0.5 yargs-parser: ^21.1.1 - checksum: 73b572e863aa4a8cbef323dd911d79d193b772defd5a51aab0aca2d446655216f5002c42c5306033968193bdbf892a7a4c110b0d77954a7fdf563e653967b56a + checksum: 16fb2d4866f04aaf74f206d3df843e7a80b58a26fda47af29be51b27564c19966b1674c3acf679a26fdfa8a7e4b1b442a63cdab5db8cd3c57db7912f4473e343 languageName: node linkType: hard @@ -12942,8 +13009,8 @@ __metadata: linkType: hard "zustand@npm:^5.0.4": - version: 5.0.13 - resolution: "zustand@npm:5.0.13" + version: 5.0.14 + resolution: "zustand@npm:5.0.14" peerDependencies: "@types/react": ">=18.0.0" immer: ">=9.0.6" @@ -12958,6 +13025,6 @@ __metadata: optional: true use-sync-external-store: optional: true - checksum: 37f9c1eb888fc6c570f80a182a54844752ffe05dca75356273038effad642892df3a839f16733636a2dd4a49f68c02be27a37b9ba767af03515ef1f8d97468de + checksum: 9b660a4cd510f1c0512a71cecbe6a03ce0c353da93391c83dbc4d4904d7d1b45ecf44791e235bea514450ee50ff771cabf45a9a49edeec44a5886a5cdf1a7d3d languageName: node linkType: hard