mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-10-23 18:43:21 +00:00
👷 ci: Type-check the Client Workspace (#13560)
The `client/` workspace was never type-checked: the existing typecheck
job only covered `packages/` and `api/`, and Vite/esbuild transpiles
without type-checking, so type errors shipped through every CI gate.
- Add a `typecheck` job to frontend-review.yml running `tsc --noEmit`
over `client/` (zero tolerance), reusing the data-provider +
client-package build artifacts. Triggers on `client/**`,
`packages/client/**`, `packages/data-provider/**`.
- Fix all 168 pre-existing client type errors this surfaced (source +
tests), including genuine latent bugs:
- `getFileConfig()` was typed as merged `FileConfig`, but the server
returns the raw config that `mergeFileConfig()` consumes (`TFileConfig`).
- SidePanel/Agents `Retrieval`/`ImageVision` were bound to `AgentForm`
but use the assistants `Capabilities` enum → `AssistantForm`.
- `useSearchResultsByTurn` read a `sources` field its type lacked.
- Removed orphaned dead code: `Artifacts/Mermaid.tsx` (imported a
never-installed dep) and dead barrel re-exports (`./Plugins`, `./MCPAuth`).
- Narrow `client/tsconfig.json` to the client app (drop `../e2e` and
`../config/translations`, which reference backend/tooling modules) so
the gate's scope matches its trigger.
No `any`/`@ts-ignore`/`as unknown as`. Localized newly-surfaced strings.
This commit is contained in:
parent
727dd3bea4
commit
265d660076
70 changed files with 407 additions and 399 deletions
45
.github/workflows/frontend-review.yml
vendored
45
.github/workflows/frontend-review.yml
vendored
|
|
@ -4,6 +4,7 @@ on:
|
|||
pull_request:
|
||||
paths:
|
||||
- 'client/**'
|
||||
- 'packages/client/**'
|
||||
- 'packages/data-provider/**'
|
||||
|
||||
permissions:
|
||||
|
|
@ -76,6 +77,50 @@ jobs:
|
|||
path: packages/client/dist
|
||||
retention-days: 2
|
||||
|
||||
typecheck:
|
||||
name: TypeScript type checks (client)
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js 24.16.0
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24.16.0'
|
||||
|
||||
- name: Restore node_modules cache
|
||||
id: cache-node-modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
node_modules
|
||||
client/node_modules
|
||||
packages/client/node_modules
|
||||
packages/data-provider/node_modules
|
||||
key: node-modules-frontend-${{ runner.os }}-24.16.0-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.cache-node-modules.outputs.cache-hit != 'true'
|
||||
run: npm ci
|
||||
|
||||
- name: Download data-provider build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build-data-provider
|
||||
path: packages/data-provider/dist
|
||||
|
||||
- name: Download client-package build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build-client-package
|
||||
path: packages/client/dist
|
||||
|
||||
- name: Type check client
|
||||
run: npm run typecheck
|
||||
working-directory: client
|
||||
|
||||
test-ubuntu:
|
||||
name: 'Tests: Ubuntu (shard ${{ matrix.shard }}/4)'
|
||||
needs: build
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { createContext, useContext, useEffect, useMemo, useRef } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { Tools, Constants, LocalStorageKeys, AgentCapabilities } from 'librechat-data-provider';
|
||||
import type { TAgentsEndpoint } from 'librechat-data-provider';
|
||||
import type { TAgentsEndpoint, TEphemeralAgent } from 'librechat-data-provider';
|
||||
import {
|
||||
useMCPServerManager,
|
||||
useSearchApiKeyForm,
|
||||
|
|
@ -170,7 +170,7 @@ export default function BadgeRowProvider({
|
|||
if (prev == null) {
|
||||
/** ephemeralAgent is null — use localStorage defaults */
|
||||
if (hasOverrides || mcpOverrides) {
|
||||
const result = { ...initialValues };
|
||||
const result: TEphemeralAgent = { ...initialValues };
|
||||
if (mcpOverrides) {
|
||||
result.mcp = mcpOverrides;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,17 +16,17 @@ const LiveMessage: React.FC<LiveMessageProps> = ({
|
|||
|
||||
useEffect(() => {
|
||||
if (ariaLive === 'assertive') {
|
||||
announceAssertive(message);
|
||||
announceAssertive({ message });
|
||||
} else if (ariaLive === 'polite') {
|
||||
announcePolite(message);
|
||||
announcePolite({ message });
|
||||
}
|
||||
}, [message, ariaLive, announceAssertive, announcePolite]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (clearOnUnmount === true || clearOnUnmount === 'true') {
|
||||
announceAssertive('');
|
||||
announcePolite('');
|
||||
announceAssertive({ message: '' });
|
||||
announcePolite({ message: '' });
|
||||
}
|
||||
};
|
||||
}, [clearOnUnmount, announceAssertive, announcePolite]);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import React, { useMemo, useEffect, useCallback, useRef } from 'react';
|
||||
import { AutoSizer, List as VirtualList, WindowScroller } from 'react-virtualized';
|
||||
import { throttle } from 'lodash';
|
||||
import { Spinner } from '@librechat/client';
|
||||
import { PermissionBits } from 'librechat-data-provider';
|
||||
import { AutoSizer, List as VirtualList, WindowScroller } from 'react-virtualized';
|
||||
import type t from 'librechat-data-provider';
|
||||
import { useMarketplaceAgentsInfiniteQuery } from '~/data-provider/Agents';
|
||||
import { useAgentCategories, useLocalize } from '~/hooks';
|
||||
|
|
@ -175,7 +175,7 @@ const VirtualizedAgentGrid: React.FC<VirtualizedAgentGridProps> = ({
|
|||
const globalIndex = index * cardsPerRow + cardIndex;
|
||||
return (
|
||||
<div key={`${agent.id}-${globalIndex}`} role="gridcell">
|
||||
<AgentCard agent={agent} onClick={() => onSelectAgent(agent)} />
|
||||
<AgentCard agent={agent} onSelect={onSelectAgent} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
|
@ -282,7 +282,7 @@ const VirtualizedAgentGrid: React.FC<VirtualizedAgentGridProps> = ({
|
|||
const rowCount = getRowCount(currentAgents.length, cardsPerRow);
|
||||
|
||||
return (
|
||||
<div ref={registerChild}>
|
||||
<div ref={registerChild as React.LegacyRef<HTMLDivElement>}>
|
||||
<VirtualList
|
||||
ref={listRef}
|
||||
autoHeight
|
||||
|
|
|
|||
|
|
@ -272,7 +272,7 @@ describe('ErrorDisplay', () => {
|
|||
});
|
||||
|
||||
it('handles null/undefined errors', () => {
|
||||
render(<ErrorDisplay error={null} />);
|
||||
render(<ErrorDisplay error={{}} />);
|
||||
|
||||
expect(screen.getByText('Something went wrong')).toBeInTheDocument();
|
||||
expect(
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { jest } from '@jest/globals';
|
||||
import type * as t from 'librechat-data-provider';
|
||||
import VirtualizedAgentGrid from '../VirtualizedAgentGrid';
|
||||
|
||||
|
|
@ -48,7 +47,9 @@ const mockRowRenderer = jest.fn();
|
|||
|
||||
jest.mock('react-virtualized', () => {
|
||||
const ReactActual = jest.requireActual<typeof import('react')>('react');
|
||||
const mockRowRendererRef = { current: jest.fn() };
|
||||
const mockRowRendererRef: { current: VirtualListMockProps['rowRenderer'] | null } = {
|
||||
current: null,
|
||||
};
|
||||
|
||||
return {
|
||||
AutoSizer: ({
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { jest } from '@jest/globals';
|
||||
import type t from 'librechat-data-provider';
|
||||
import VirtualizedAgentGrid from '../VirtualizedAgentGrid';
|
||||
|
||||
|
|
@ -166,9 +165,15 @@ jest.mock('../SmartLoader', () => ({
|
|||
}));
|
||||
|
||||
jest.mock('../AgentCard', () => {
|
||||
return function MockAgentCard({ agent, onClick }: { agent: t.Agent; onClick: () => void }) {
|
||||
return function MockAgentCard({
|
||||
agent,
|
||||
onSelect,
|
||||
}: {
|
||||
agent: t.Agent;
|
||||
onSelect: (agent: t.Agent) => void;
|
||||
}) {
|
||||
return (
|
||||
<div data-testid={`agent-card-${agent.id}`} onClick={onClick}>
|
||||
<div data-testid={`agent-card-${agent.id}`} onClick={() => onSelect(agent)}>
|
||||
<h3>{agent.name}</h3>
|
||||
<p>{agent.description}</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,176 +0,0 @@
|
|||
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import { Button } from '@librechat/client';
|
||||
import { ZoomIn, ZoomOut, RotateCcw } from 'lucide-react';
|
||||
import { TransformWrapper, TransformComponent } from 'react-zoom-pan-pinch';
|
||||
import type { ReactZoomPanPinchRef } from 'react-zoom-pan-pinch';
|
||||
import { artifactFlowchartConfig } from '~/utils/mermaid';
|
||||
|
||||
interface MermaidDiagramProps {
|
||||
content: string;
|
||||
isDarkMode?: boolean;
|
||||
}
|
||||
|
||||
let mermaidPromise: Promise<typeof import('mermaid').default> | null = null;
|
||||
|
||||
const loadMermaid = () => {
|
||||
if (!mermaidPromise) {
|
||||
mermaidPromise = import('mermaid').then((mod) => mod.default);
|
||||
}
|
||||
|
||||
return mermaidPromise;
|
||||
};
|
||||
|
||||
const MermaidDiagram: React.FC<MermaidDiagramProps> = ({ content, isDarkMode = true }) => {
|
||||
const mermaidRef = useRef<HTMLDivElement>(null);
|
||||
const transformRef = useRef<ReactZoomPanPinchRef>(null);
|
||||
const [isRendered, setIsRendered] = useState(false);
|
||||
const theme = isDarkMode ? 'dark' : 'neutral';
|
||||
const bgColor = isDarkMode ? '#212121' : '#FFFFFF';
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
const renderDiagram = async () => {
|
||||
try {
|
||||
const mermaid = await loadMermaid();
|
||||
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
theme,
|
||||
securityLevel: 'sandbox',
|
||||
flowchart: artifactFlowchartConfig,
|
||||
});
|
||||
|
||||
if (!mermaidRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { svg } = await mermaid.render('mermaid-diagram', content);
|
||||
mermaidRef.current.innerHTML = svg;
|
||||
|
||||
const svgElement = mermaidRef.current.querySelector('svg');
|
||||
if (svgElement) {
|
||||
svgElement.style.width = '100%';
|
||||
svgElement.style.height = '100%';
|
||||
}
|
||||
if (isMounted) {
|
||||
setIsRendered(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Mermaid rendering error:', error);
|
||||
if (mermaidRef.current) {
|
||||
mermaidRef.current.innerHTML = 'Error rendering diagram';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
renderDiagram();
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, [content, theme]);
|
||||
|
||||
const centerAndFitDiagram = useCallback(() => {
|
||||
if (transformRef.current && mermaidRef.current) {
|
||||
const { centerView, zoomToElement } = transformRef.current;
|
||||
zoomToElement(mermaidRef.current as HTMLElement);
|
||||
centerView(1, 0);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isRendered) {
|
||||
centerAndFitDiagram();
|
||||
}
|
||||
}, [isRendered, centerAndFitDiagram]);
|
||||
|
||||
const handlePanning = useCallback(() => {
|
||||
if (!transformRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { state, instance } = transformRef.current;
|
||||
if (!state || !instance) {
|
||||
return;
|
||||
}
|
||||
const { scale, positionX, positionY } = state;
|
||||
const { wrapperComponent, contentComponent } = instance;
|
||||
|
||||
if (!wrapperComponent || !contentComponent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const wrapperRect = wrapperComponent.getBoundingClientRect();
|
||||
const contentRect = contentComponent.getBoundingClientRect();
|
||||
const maxX = wrapperRect.width - contentRect.width * scale;
|
||||
const maxY = wrapperRect.height - contentRect.height * scale;
|
||||
|
||||
let newX = positionX;
|
||||
let newY = positionY;
|
||||
|
||||
if (newX > 0) {
|
||||
newX = 0;
|
||||
}
|
||||
if (newY > 0) {
|
||||
newY = 0;
|
||||
}
|
||||
if (newX < maxX) {
|
||||
newX = maxX;
|
||||
}
|
||||
if (newY < maxY) {
|
||||
newY = maxY;
|
||||
}
|
||||
|
||||
if (newX !== positionX || newY !== positionY) {
|
||||
instance.setTransformState(scale, newX, newY);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative h-screen w-screen cursor-move p-5"
|
||||
style={{ backgroundColor: bgColor }}
|
||||
>
|
||||
<TransformWrapper
|
||||
ref={transformRef}
|
||||
initialScale={1}
|
||||
minScale={0.1}
|
||||
maxScale={10}
|
||||
limitToBounds={false}
|
||||
centerOnInit={true}
|
||||
initialPositionY={0}
|
||||
wheel={{ step: 0.1 }}
|
||||
panning={{ velocityDisabled: true }}
|
||||
alignmentAnimation={{ disabled: true }}
|
||||
onPanning={handlePanning}
|
||||
>
|
||||
{({ zoomIn, zoomOut }) => (
|
||||
<>
|
||||
<TransformComponent
|
||||
wrapperStyle={{ width: '100%', height: '100%', overflow: 'hidden' }}
|
||||
>
|
||||
<div
|
||||
ref={mermaidRef}
|
||||
style={{ width: 'auto', height: 'auto', minWidth: '100%', minHeight: '100%' }}
|
||||
/>
|
||||
</TransformComponent>
|
||||
<div className="absolute bottom-2 right-2 flex space-x-2">
|
||||
<Button onClick={() => zoomIn(0.1)} variant="outline" size="icon">
|
||||
<ZoomIn className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button onClick={() => zoomOut(0.1)} variant="outline" size="icon">
|
||||
<ZoomOut className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button onClick={centerAndFitDiagram} variant="outline" size="icon">
|
||||
<RotateCcw className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</TransformWrapper>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MermaidDiagram;
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import reactRouter from 'react-router-dom';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { getByTestId, render, waitFor } from 'test/layout-test-utils';
|
||||
import type { TStartupConfig } from 'librechat-data-provider';
|
||||
import { getByTestId, render, waitFor } from 'test/layout-test-utils';
|
||||
import * as endpointQueries from '~/data-provider/Endpoints/queries';
|
||||
import * as miscDataProvider from '~/data-provider/Misc/queries';
|
||||
import * as authMutations from '~/data-provider/Auth/mutations';
|
||||
|
|
@ -176,7 +176,7 @@ test('calls loginUser.mutate on login', async () => {
|
|||
});
|
||||
|
||||
test('Navigates to / on successful login', async () => {
|
||||
const { getByLabelText, history } = setup({
|
||||
const { getByLabelText } = setup({
|
||||
// @ts-ignore - we don't need all parameters of the QueryObserverResult
|
||||
useLoginUserReturnValue: {
|
||||
isLoading: false,
|
||||
|
|
@ -202,5 +202,5 @@ test('Navigates to / on successful login', async () => {
|
|||
await userEvent.type(passwordInput, 'password');
|
||||
await userEvent.click(submitButton);
|
||||
|
||||
waitFor(() => expect(history.location.pathname).toBe('/'));
|
||||
waitFor(() => expect(window.location.pathname).toBe('/'));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { render, getByTestId } from 'test/layout-test-utils';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import type { TStartupConfig } from 'librechat-data-provider';
|
||||
import * as endpointQueries from '~/data-provider/Endpoints/queries';
|
||||
import * as miscDataProvider from '~/data-provider/Misc/queries';
|
||||
import * as authMutations from '~/data-provider/Auth/mutations';
|
||||
import { render, getByTestId } from 'test/layout-test-utils';
|
||||
import * as authQueries from '~/data-provider/Auth/queries';
|
||||
import Login from '../LoginForm';
|
||||
|
||||
|
|
@ -18,8 +18,10 @@ const mockStartupConfig: TStartupConfig = {
|
|||
githubLoginEnabled: true,
|
||||
googleLoginEnabled: true,
|
||||
openidLoginEnabled: true,
|
||||
appleLoginEnabled: false,
|
||||
openidLabel: 'Test OpenID',
|
||||
openidImageUrl: 'http://test-server.com',
|
||||
openidAutoRedirect: false,
|
||||
samlLoginEnabled: true,
|
||||
samlLabel: 'Test SAML',
|
||||
samlImageUrl: 'http://test-server.com',
|
||||
|
|
@ -33,9 +35,11 @@ const mockStartupConfig: TStartupConfig = {
|
|||
enabled: false,
|
||||
},
|
||||
emailEnabled: false,
|
||||
checkBalance: false,
|
||||
showBirthdayIcon: false,
|
||||
helpAndFaqURL: '',
|
||||
sharedLinksEnabled: true,
|
||||
publicSharedLinksEnabled: true,
|
||||
allowAccountDeletion: true,
|
||||
};
|
||||
|
||||
const setup = ({
|
||||
|
|
@ -106,15 +110,25 @@ beforeEach(() => {
|
|||
|
||||
test('renders login form', () => {
|
||||
const { getByLabelText } = render(
|
||||
<Login onSubmit={mockLogin} startupConfig={mockStartupConfig} />,
|
||||
<Login
|
||||
onSubmit={mockLogin}
|
||||
startupConfig={mockStartupConfig}
|
||||
error={undefined}
|
||||
setError={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(getByLabelText(/email/i)).toBeInTheDocument();
|
||||
expect(getByLabelText(/password/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('submits login form', async () => {
|
||||
const { getByLabelText, getByRole } = render(
|
||||
<Login onSubmit={mockLogin} startupConfig={mockStartupConfig} />,
|
||||
const { getByLabelText } = render(
|
||||
<Login
|
||||
onSubmit={mockLogin}
|
||||
startupConfig={mockStartupConfig}
|
||||
error={undefined}
|
||||
setError={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
const emailInput = getByLabelText(/email/i);
|
||||
const passwordInput = getByLabelText(/password/i);
|
||||
|
|
@ -128,8 +142,13 @@ test('submits login form', async () => {
|
|||
});
|
||||
|
||||
test('displays validation error messages', async () => {
|
||||
const { getByLabelText, getByRole, getByText } = render(
|
||||
<Login onSubmit={mockLogin} startupConfig={mockStartupConfig} />,
|
||||
const { getByLabelText, getByText } = render(
|
||||
<Login
|
||||
onSubmit={mockLogin}
|
||||
startupConfig={mockStartupConfig}
|
||||
error={undefined}
|
||||
setError={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
const emailInput = getByLabelText(/email/i);
|
||||
const passwordInput = getByLabelText(/password/i);
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export const data: TModelSpec[] = [
|
|||
// iconURL: 'https://i.ytimg.com/vi/SaneSRqePVY/maxresdefault.jpg',
|
||||
iconURL: EModelEndpoint.openAI, // Allow using project-included icons
|
||||
preset: {
|
||||
chatGptLabel: 'Vision Helper',
|
||||
modelLabel: 'Vision Helper',
|
||||
greeting: "What's up!!",
|
||||
endpoint: EModelEndpoint.openAI,
|
||||
model: 'gpt-4-turbo',
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import React from 'react';
|
||||
import { render, screen, fireEvent, act } from '@testing-library/react';
|
||||
import { RecoilRoot, useRecoilValue } from 'recoil';
|
||||
import type { MutableSnapshot } from 'recoil';
|
||||
import { render, screen, fireEvent, act } from '@testing-library/react';
|
||||
import type { TAttachment } from 'librechat-data-provider';
|
||||
import type { MutableSnapshot } from 'recoil';
|
||||
import Attachment, { AttachmentGroup } from '../Attachment';
|
||||
import store from '~/store';
|
||||
|
||||
|
|
@ -198,9 +198,8 @@ describe('Attachment routing for tool artifacts', () => {
|
|||
* yet); use it as the canonical "unrouted text" example. */
|
||||
const json = baseAttachment({
|
||||
filename: 'data.json',
|
||||
type: 'application/json',
|
||||
text: '{"a":1,"b":2}',
|
||||
} as Partial<TAttachment>);
|
||||
});
|
||||
const { container } = renderWith(<Attachment attachment={json} />);
|
||||
expect(container.querySelector('pre')).not.toBeNull();
|
||||
expect(screen.queryByTestId('mermaid-render')).not.toBeInTheDocument();
|
||||
|
|
@ -544,10 +543,9 @@ describe('ToolArtifactCard click behaviour', () => {
|
|||
const xlsx = baseAttachment({
|
||||
file_id: 'just-resolved-xlsx',
|
||||
filename: 'data.xlsx',
|
||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
text: '<table>resolved</table>',
|
||||
textFormat: 'html',
|
||||
} as Partial<TAttachment>);
|
||||
});
|
||||
const initializeState = (snap: MutableSnapshot) => {
|
||||
snap.set(store.isSubmittingFamily(0), false);
|
||||
snap.set(store.artifactsVisibility, false);
|
||||
|
|
@ -580,10 +578,9 @@ describe('ToolArtifactCard click behaviour', () => {
|
|||
const xlsx = baseAttachment({
|
||||
file_id: 'one-shot-xlsx',
|
||||
filename: 'data.xlsx',
|
||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
text: '<table>resolved</table>',
|
||||
textFormat: 'html',
|
||||
} as Partial<TAttachment>);
|
||||
});
|
||||
const initializeState = (snap: MutableSnapshot) => {
|
||||
snap.set(store.isSubmittingFamily(0), false);
|
||||
snap.set(store.artifactsVisibility, false);
|
||||
|
|
@ -710,15 +707,13 @@ describe('AttachmentGroup routing', () => {
|
|||
const empty = baseAttachment({
|
||||
file_id: 'empty-zip',
|
||||
filename: 'placeholder.zip',
|
||||
type: 'application/zip',
|
||||
bytes: 0,
|
||||
} as Partial<TAttachment>);
|
||||
});
|
||||
const real = baseAttachment({
|
||||
file_id: 'real-zip',
|
||||
filename: 'archive.zip',
|
||||
type: 'application/zip',
|
||||
bytes: 1024,
|
||||
} as Partial<TAttachment>);
|
||||
});
|
||||
const { container } = renderWith(<AttachmentGroup attachments={[empty, real]} />);
|
||||
fireEvent.click(screen.getByRole('button', { name: 'com_ui_show_n_files' }));
|
||||
const chips = Array.from(container.querySelectorAll('[data-testid="file-container"]'));
|
||||
|
|
@ -733,26 +728,22 @@ describe('AttachmentGroup routing', () => {
|
|||
const first = baseAttachment({
|
||||
file_id: 'file-a',
|
||||
filename: 'a.zip',
|
||||
type: 'application/zip',
|
||||
} as Partial<TAttachment>);
|
||||
});
|
||||
const second = baseAttachment({
|
||||
file_id: 'file-b',
|
||||
filename: 'b.zip',
|
||||
type: 'application/zip',
|
||||
} as Partial<TAttachment>);
|
||||
});
|
||||
const json = baseAttachment({
|
||||
file_id: 'file-c',
|
||||
filename: 'c.json',
|
||||
type: 'application/json',
|
||||
text: '{"c":true}',
|
||||
} as Partial<TAttachment>);
|
||||
});
|
||||
const image = baseAttachment({
|
||||
file_id: 'image-a',
|
||||
filename: 'preview.png',
|
||||
type: 'image/png',
|
||||
width: 16,
|
||||
height: 16,
|
||||
} as Partial<TAttachment>);
|
||||
});
|
||||
|
||||
const { container } = renderWith(
|
||||
<AttachmentGroup attachments={[first, second, json, image]} />,
|
||||
|
|
@ -784,9 +775,8 @@ describe('AttachmentGroup routing', () => {
|
|||
const sandboxFile = baseAttachment({
|
||||
file_id: 'sandbox-zip',
|
||||
filename: 'archive-deadbe.zip',
|
||||
type: 'application/zip',
|
||||
bytes: 1024,
|
||||
} as Partial<TAttachment>);
|
||||
});
|
||||
const { container } = renderWith(<AttachmentGroup attachments={[sandboxFile]} />);
|
||||
const chip = container.querySelector('[data-testid="file-container"]');
|
||||
expect(chip?.textContent).toBe('archive-deadbe.zip');
|
||||
|
|
@ -802,9 +792,8 @@ describe('AttachmentGroup routing', () => {
|
|||
const sandboxDotfile = baseAttachment({
|
||||
file_id: 'sandbox-config',
|
||||
filename: '_.config-abcdef.zip',
|
||||
type: 'application/zip',
|
||||
bytes: 12,
|
||||
} as Partial<TAttachment>);
|
||||
});
|
||||
const { container } = renderWith(<AttachmentGroup attachments={[sandboxDotfile]} />);
|
||||
const chip = container.querySelector('[data-testid="file-container"]');
|
||||
expect(chip?.textContent).toBe('.config.zip');
|
||||
|
|
@ -823,7 +812,6 @@ describe('AttachmentGroup routing', () => {
|
|||
baseAttachment({
|
||||
file_id: 'pending-1',
|
||||
filename: 'data.xlsx',
|
||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
status: 'pending',
|
||||
} as Partial<TAttachment>),
|
||||
baseAttachment({
|
||||
|
|
@ -859,7 +847,6 @@ describe('AttachmentGroup routing', () => {
|
|||
baseAttachment({
|
||||
file_id: 'c',
|
||||
filename: 'data.json',
|
||||
type: 'application/json',
|
||||
text: '{"a":1}',
|
||||
} as Partial<TAttachment>),
|
||||
baseAttachment({
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
import type { MutableSnapshot } from 'recoil';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import type { TAttachment } from 'librechat-data-provider';
|
||||
import type { MutableSnapshot } from 'recoil';
|
||||
import LogContent from '../LogContent';
|
||||
import store from '~/store';
|
||||
|
||||
|
|
@ -110,9 +110,8 @@ describe('LogContent attachment routing', () => {
|
|||
const json = baseAttachment({
|
||||
file_id: 'c',
|
||||
filename: 'data.json',
|
||||
type: 'application/json',
|
||||
text: '{"a":1,"b":2}',
|
||||
} as Partial<TAttachment>);
|
||||
});
|
||||
const { container } = renderWith(<LogContent output="" attachments={[json]} />);
|
||||
expect(container.querySelector('pre')).not.toBeNull();
|
||||
expect(screen.queryByRole('button', { pressed: true })).not.toBeInTheDocument();
|
||||
|
|
@ -123,8 +122,7 @@ describe('LogContent attachment routing', () => {
|
|||
const zip = baseAttachment({
|
||||
file_id: 'd',
|
||||
filename: 'archive.zip',
|
||||
type: 'application/zip',
|
||||
} as Partial<TAttachment>);
|
||||
});
|
||||
renderWith(<LogContent output="" attachments={[zip]} />);
|
||||
expect(screen.getByTestId('log-link')).toHaveAttribute('data-filename', 'archive.zip');
|
||||
});
|
||||
|
|
@ -137,9 +135,8 @@ describe('LogContent attachment routing', () => {
|
|||
const pptx = baseAttachment({
|
||||
file_id: 'e',
|
||||
filename: 'slides.pptx',
|
||||
type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
text: '<!DOCTYPE html><body><ol><li>Slide 1</li></ol></body>',
|
||||
} as Partial<TAttachment>);
|
||||
});
|
||||
renderWith(<LogContent output="" attachments={[pptx]} />);
|
||||
expect(screen.getByText('slides.pptx')).toBeInTheDocument();
|
||||
});
|
||||
|
|
@ -151,9 +148,8 @@ describe('LogContent attachment routing', () => {
|
|||
const pptx = baseAttachment({
|
||||
file_id: 'e2',
|
||||
filename: 'slides.pptx',
|
||||
type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
text: undefined as unknown as string,
|
||||
} as Partial<TAttachment>);
|
||||
text: undefined,
|
||||
});
|
||||
renderWith(<LogContent output="" attachments={[pptx]} />);
|
||||
expect(screen.queryByRole('button', { pressed: true })).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('log-link')).toHaveAttribute('data-filename', 'slides.pptx');
|
||||
|
|
@ -168,10 +164,9 @@ describe('LogContent attachment routing', () => {
|
|||
const expired = baseAttachment({
|
||||
file_id: 'x-expired',
|
||||
filename: 'slides.pptx',
|
||||
type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
text: '<!DOCTYPE html><body><ol><li>Slide 1</li></ol></body>',
|
||||
expiresAt: Date.now() - 60_000,
|
||||
} as Partial<TAttachment>);
|
||||
});
|
||||
renderWith(<LogContent output="" attachments={[expired]} />);
|
||||
// No panel card and no log-link (the expired branch returns plain text).
|
||||
expect(screen.queryByRole('button', { pressed: true })).not.toBeInTheDocument();
|
||||
|
|
@ -211,7 +206,6 @@ describe('LogContent attachment routing', () => {
|
|||
* bucket, so it would no longer satisfy the "inline pre" check
|
||||
* below. */
|
||||
filename: 'notes.json',
|
||||
type: 'application/json',
|
||||
text: '{"a":1,"b":2}',
|
||||
} as Partial<TAttachment>),
|
||||
] as TAttachment[];
|
||||
|
|
|
|||
|
|
@ -8,15 +8,14 @@ import type {
|
|||
SubagentAggregatorState,
|
||||
} from '~/utils/subagentContent';
|
||||
import type { SubagentProgress } from '~/store/subagents';
|
||||
|
||||
import {
|
||||
foldSubagentEvent,
|
||||
foldSubagentEventIntoTicker,
|
||||
initSubagentAggregatorState,
|
||||
initSubagentTickerState,
|
||||
} from '~/utils/subagentContent';
|
||||
import { subagentProgressByToolCallId } from '~/store/subagents';
|
||||
import SubagentCall, { SUBAGENT_TICKER_THROTTLE_MS } from '../SubagentCall';
|
||||
import { subagentProgressByToolCallId } from '~/store/subagents';
|
||||
|
||||
jest.mock('~/hooks', () => ({
|
||||
useLocalize:
|
||||
|
|
@ -622,7 +621,7 @@ describe('SubagentCall — dialog content', () => {
|
|||
);
|
||||
openSubagentDialog();
|
||||
expect(screen.getByText('raw final text')).toBeInTheDocument();
|
||||
rerender(<RecoilRoot />);
|
||||
rerender(<RecoilRoot>{null}</RecoilRoot>);
|
||||
});
|
||||
|
||||
it('renders persistedContent parts when no live events are available (page-refresh flow)', () => {
|
||||
|
|
|
|||
|
|
@ -71,7 +71,6 @@ const textAttachment = (overrides: Partial<TAttachment> = {}): TAttachment =>
|
|||
* bearing, downloadable, expandable) without the panel coupling. */
|
||||
filename: 'output.json',
|
||||
filepath: '/files/output.json',
|
||||
type: 'application/json',
|
||||
text: '{"a":1,"b":2,"c":3}',
|
||||
...overrides,
|
||||
}) as TAttachment;
|
||||
|
|
@ -190,8 +189,7 @@ describe('AttachmentGroup', () => {
|
|||
textAttachment({
|
||||
file_id: 'b',
|
||||
filename: 'archive.zip',
|
||||
type: 'application/zip',
|
||||
text: undefined as unknown as string,
|
||||
text: undefined,
|
||||
}),
|
||||
] as TAttachment[];
|
||||
const { container } = render(<AttachmentGroup attachments={attachments} />);
|
||||
|
|
@ -205,8 +203,7 @@ describe('AttachmentGroup', () => {
|
|||
file_id: 'placeholder',
|
||||
filename: 'placeholder.zip',
|
||||
filepath: '',
|
||||
type: 'application/zip',
|
||||
text: undefined as unknown as string,
|
||||
text: undefined,
|
||||
}),
|
||||
textAttachment({
|
||||
file_id: 'json',
|
||||
|
|
@ -230,8 +227,7 @@ describe('AttachmentGroup', () => {
|
|||
textAttachment({
|
||||
file_id: 'archive',
|
||||
filename: 'archive.zip',
|
||||
type: 'application/zip',
|
||||
text: undefined as unknown as string,
|
||||
text: undefined,
|
||||
}),
|
||||
textAttachment({
|
||||
file_id: 'json',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import type { TAttachment } from 'librechat-data-provider';
|
||||
import { TOOL_ARTIFACT_TYPES } from '~/utils/artifacts';
|
||||
import {
|
||||
artifactTypeForAttachment,
|
||||
attachmentSalience,
|
||||
|
|
@ -8,6 +7,7 @@ import {
|
|||
isInternalSandboxArtifact,
|
||||
isTextAttachment,
|
||||
} from '../attachmentTypes';
|
||||
import { TOOL_ARTIFACT_TYPES } from '~/utils/artifacts';
|
||||
|
||||
const baseAttachment = (overrides: Partial<TAttachment> = {}): TAttachment =>
|
||||
({
|
||||
|
|
@ -138,9 +138,8 @@ describe('artifactTypeForAttachment', () => {
|
|||
* pipeline instead. */
|
||||
const attachment = baseAttachment({
|
||||
filename: 'photo.jpg',
|
||||
type: 'image/jpeg',
|
||||
text: undefined,
|
||||
} as Partial<TAttachment>);
|
||||
});
|
||||
expect(artifactTypeForAttachment(attachment)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -35,11 +35,11 @@ Object.defineProperty(HTMLElement.prototype, 'scrollTo', {
|
|||
|
||||
describe('UIResourceCarousel', () => {
|
||||
const mockUIResources: UIResource[] = [
|
||||
{ uri: 'resource1', mimeType: 'text/html', text: 'Resource 1' },
|
||||
{ uri: 'resource2', mimeType: 'text/html', text: 'Resource 2' },
|
||||
{ uri: 'resource3', mimeType: 'text/html', text: 'Resource 3' },
|
||||
{ uri: 'resource4', mimeType: 'text/html', text: 'Resource 4' },
|
||||
{ uri: 'resource5', mimeType: 'text/html', text: 'Resource 5' },
|
||||
{ resourceId: 'resource1', uri: 'resource1', mimeType: 'text/html', text: 'Resource 1' },
|
||||
{ resourceId: 'resource2', uri: 'resource2', mimeType: 'text/html', text: 'Resource 2' },
|
||||
{ resourceId: 'resource3', uri: 'resource3', mimeType: 'text/html', text: 'Resource 3' },
|
||||
{ resourceId: 'resource4', uri: 'resource4', mimeType: 'text/html', text: 'Resource 4' },
|
||||
{ resourceId: 'resource5', uri: 'resource5', mimeType: 'text/html', text: 'Resource 5' },
|
||||
];
|
||||
|
||||
const mockHandleUIAction = handleUIAction as jest.MockedFunction<typeof handleUIAction>;
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ describe('MessageIcon render cycles', () => {
|
|||
const { rerender } = render(<MessageIcon iconData={baseIconData} agent={agent1} />);
|
||||
iconRenderCount.current = 0;
|
||||
|
||||
const agent2 = makeAgent({ avatar: { filepath: '/images/new-avatar.png' } });
|
||||
const agent2 = makeAgent({ avatar: { filepath: '/images/new-avatar.png', source: 'local' } });
|
||||
rerender(<MessageIcon iconData={baseIconData} agent={agent2} />);
|
||||
|
||||
expect(iconRenderCount.current).toBe(1);
|
||||
|
|
|
|||
|
|
@ -62,8 +62,14 @@ describe('MessageIcon arePropsEqual', () => {
|
|||
it('returns false when agent avatar filepath changes', () => {
|
||||
expect(
|
||||
arePropsEqual(
|
||||
{ iconData: baseIconData, agent: makeAgent({ avatar: { filepath: '/a.png' } }) },
|
||||
{ iconData: baseIconData, agent: makeAgent({ avatar: { filepath: '/b.png' } }) },
|
||||
{
|
||||
iconData: baseIconData,
|
||||
agent: makeAgent({ avatar: { filepath: '/a.png', source: 'local' } }),
|
||||
},
|
||||
{
|
||||
iconData: baseIconData,
|
||||
agent: makeAgent({ avatar: { filepath: '/b.png', source: 'local' } }),
|
||||
},
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
import { useState, useRef } from 'react';
|
||||
import { Trans } from 'react-i18next';
|
||||
import { QrCode, RotateCw, Trash2 } from 'lucide-react';
|
||||
import {
|
||||
PermissionTypes,
|
||||
Permissions,
|
||||
PermissionBits,
|
||||
ResourceType,
|
||||
} from 'librechat-data-provider';
|
||||
import {
|
||||
Label,
|
||||
Button,
|
||||
|
|
@ -13,19 +19,13 @@ import {
|
|||
useToastContext,
|
||||
OGDialogContent,
|
||||
} from '@librechat/client';
|
||||
import {
|
||||
PermissionTypes,
|
||||
Permissions,
|
||||
PermissionBits,
|
||||
ResourceType,
|
||||
} from 'librechat-data-provider';
|
||||
import type { TSharedLinkGetResponse } from 'librechat-data-provider';
|
||||
import GenericGrantAccessDialog from '~/components/Sharing/GenericGrantAccessDialog';
|
||||
import {
|
||||
useCreateSharedLinkMutation,
|
||||
useUpdateSharedLinkMutation,
|
||||
useDeleteSharedLinkMutation,
|
||||
} from '~/data-provider';
|
||||
import GenericGrantAccessDialog from '~/components/Sharing/GenericGrantAccessDialog';
|
||||
import { useHasAccess, useResourcePermissions, useLocalize } from '~/hooks';
|
||||
import { NotificationSeverity } from '~/common';
|
||||
import { buildShareLinkUrl } from '~/utils';
|
||||
|
|
@ -220,7 +220,7 @@ export default function SharedLinkButton({
|
|||
<GenericGrantAccessDialog
|
||||
resourceType={ResourceType.SHARED_LINK}
|
||||
resourceDbId={share?._id}
|
||||
resourceName={share?.shareId}
|
||||
resourceName={share?.shareId ?? undefined}
|
||||
>
|
||||
<TooltipAnchor
|
||||
description={localize('com_ui_shared_link_manage_access')}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,13 @@ import { PlusIcon } from 'lucide-react';
|
|||
import { Button, Checkbox, DotsIcon, FileIcon } from '@librechat/client';
|
||||
import type { ColumnDef } from '@tanstack/react-table';
|
||||
import type { TFile } from 'librechat-data-provider';
|
||||
import type { TVectorStore } from '~/common';
|
||||
import { formatDate, getFileType } from '~/utils';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
export const fileTableColumns: ColumnDef<TFile>[] = [
|
||||
type TFileWithVectors = TFile & { vectorsAttached?: TVectorStore[] };
|
||||
|
||||
export const fileTableColumns: ColumnDef<TFileWithVectors>[] = [
|
||||
{
|
||||
id: 'select',
|
||||
header: ({ table }) => {
|
||||
|
|
@ -53,7 +56,7 @@ export const fileTableColumns: ColumnDef<TFile>[] = [
|
|||
size: '150px',
|
||||
},
|
||||
accessorKey: 'filename',
|
||||
header: ({ column }) => {
|
||||
header: () => {
|
||||
const localize = useLocalize();
|
||||
return <>{localize('com_ui_name')}</>;
|
||||
},
|
||||
|
|
@ -68,7 +71,8 @@ export const fileTableColumns: ColumnDef<TFile>[] = [
|
|||
return 'Vector Stores';
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const { vectorsAttached: attachedVectorStores } = row.original;
|
||||
const localize = useLocalize();
|
||||
const { vectorsAttached: attachedVectorStores = [] } = row.original;
|
||||
return (
|
||||
<>
|
||||
{attachedVectorStores.map((vectorStore, index) => {
|
||||
|
|
@ -80,7 +84,7 @@ export const fileTableColumns: ColumnDef<TFile>[] = [
|
|||
>
|
||||
<PlusIcon className="h-3 w-3" />
|
||||
|
||||
{attachedVectorStores.length - index} more
|
||||
{localize('com_ui_more_count', { 0: attachedVectorStores.length - index })}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
|
@ -100,17 +104,19 @@ export const fileTableColumns: ColumnDef<TFile>[] = [
|
|||
{
|
||||
accessorKey: 'updatedAt',
|
||||
header: () => {
|
||||
const localize = useLocalize();
|
||||
return 'Modified';
|
||||
},
|
||||
cell: ({ row }) => formatDate(row.original.updatedAt),
|
||||
cell: ({ row }) => {
|
||||
const { updatedAt } = row.original;
|
||||
return formatDate(updatedAt instanceof Date ? updatedAt.toISOString() : (updatedAt ?? ''));
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'actions',
|
||||
header: () => {
|
||||
return 'Actions';
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
cell: () => {
|
||||
return (
|
||||
<>
|
||||
<Button className="w-min content-center bg-transparent text-gray-500 hover:bg-slate-200">
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
import { useNavigate } from 'react-router-dom';
|
||||
import { Button, DotsIcon, TrashIcon } from '@librechat/client';
|
||||
import { TVectorStore } from '~/common';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
type VectorStoreListItemProps = {
|
||||
vectorStore: TVectorStore;
|
||||
|
|
@ -13,6 +14,7 @@ export default function VectorStoreListItem({
|
|||
deleteVectorStore,
|
||||
}: VectorStoreListItemProps) {
|
||||
const navigate = useNavigate();
|
||||
const localize = useLocalize();
|
||||
return (
|
||||
<div
|
||||
onClick={() => {
|
||||
|
|
@ -26,7 +28,10 @@ export default function VectorStoreListItem({
|
|||
</div>
|
||||
<div className="w-2/6 text-gray-500">
|
||||
<p>
|
||||
{vectorStore.file_counts.total} Files ({vectorStore.bytes / 1000}KB)
|
||||
{localize('com_ui_files_count_size', {
|
||||
0: vectorStore.file_counts?.total ?? 0,
|
||||
1: (vectorStore.bytes ?? 0) / 1000,
|
||||
})}
|
||||
</p>
|
||||
<p className="text-sm">{vectorStore.created_at.toString()}</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ function AccountSettings({ collapsed = false }: { collapsed?: boolean }) {
|
|||
const accountSettingsButtonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
return (
|
||||
<Menu.MenuProvider>
|
||||
<Menu.MenuProvider placement={collapsed ? 'right-end' : undefined}>
|
||||
<Menu.MenuButton
|
||||
ref={accountSettingsButtonRef}
|
||||
aria-label={localize('com_nav_account_settings')}
|
||||
|
|
@ -50,7 +50,6 @@ function AccountSettings({ collapsed = false }: { collapsed?: boolean }) {
|
|||
<Menu.Menu
|
||||
portal
|
||||
className="account-settings-popover popover-ui z-[125] w-[305px] rounded-lg md:w-[244px]"
|
||||
placement={collapsed ? 'right-end' : undefined}
|
||||
style={{
|
||||
transformOrigin: collapsed ? 'left bottom' : 'bottom',
|
||||
translate: collapsed ? '4px 0' : '0 -4px',
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import React from 'react';
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { dataService } from 'librechat-data-provider';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import type { Agent } from 'librechat-data-provider';
|
||||
|
||||
// Mock store before importing FavoritesList
|
||||
|
|
@ -71,7 +71,7 @@ jest.mock('~/hooks/Input/useSelectMention', () => () => ({
|
|||
onSelectSpec: mockOnSelectSpec,
|
||||
}));
|
||||
|
||||
const mockUseGetStartupConfig = jest.fn(() => ({
|
||||
const mockUseGetStartupConfig = jest.fn((): { data?: { modelSpecs: { list: unknown[] } } } => ({
|
||||
data: { modelSpecs: { list: [] as unknown[] } },
|
||||
}));
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
Progress,
|
||||
} from '@librechat/client';
|
||||
import type { TUser, TVerify2FARequest } from 'librechat-data-provider';
|
||||
import type { Variants } from 'framer-motion';
|
||||
import {
|
||||
useConfirmTwoFactorMutation,
|
||||
useDisableTwoFactorMutation,
|
||||
|
|
@ -24,7 +25,7 @@ import store from '~/store';
|
|||
|
||||
export type Phase = 'setup' | 'qr' | 'verify' | 'backup' | 'disable';
|
||||
|
||||
const phaseVariants = {
|
||||
const phaseVariants: Variants = {
|
||||
initial: { opacity: 0, scale: 0.95 },
|
||||
animate: { opacity: 1, scale: 1, transition: { duration: 0.3, ease: 'easeOut' } },
|
||||
exit: { opacity: 0, scale: 0.95, transition: { duration: 0.3, ease: 'easeIn' } },
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export default function ArchivedChats() {
|
|||
title={localize('com_nav_archived_chats')}
|
||||
className="max-w-[1000px]"
|
||||
showCancelButton={false}
|
||||
main={<ArchivedChatsTable isOpen={isOpen} onOpenChange={setIsOpen} />}
|
||||
main={<ArchivedChatsTable onOpenChange={setIsOpen} />}
|
||||
/>
|
||||
</OGDialog>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -40,8 +40,9 @@ export default function AutoSendTextSelector() {
|
|||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (value: number[] | null) => {
|
||||
const newValue = value ? value[0] : 3;
|
||||
const handleInputChange = (value: string | number | null) => {
|
||||
const parsed = typeof value === 'number' ? value : parseInt(String(value ?? ''), 10);
|
||||
const newValue = Number.isNaN(parsed) ? 3 : parsed;
|
||||
setDelayValue(newValue);
|
||||
if (isEnabled) {
|
||||
setAutoSendText(newValue);
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ export default function AutoSendPrompt({
|
|||
checked={autoSendPrompts}
|
||||
tabIndex={-1}
|
||||
aria-hidden="true"
|
||||
aria-label={localize('com_nav_auto_send_prompts')}
|
||||
className="pointer-events-none"
|
||||
/>
|
||||
{localize('com_nav_auto_send_prompts')}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
*/
|
||||
import * as React from 'react';
|
||||
import { render, waitFor, fireEvent } from '@testing-library/react';
|
||||
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import type { Agent } from 'librechat-data-provider';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { Checkbox } from '@librechat/client';
|
||||
import { Capabilities } from 'librechat-data-provider';
|
||||
import { useFormContext, Controller } from 'react-hook-form';
|
||||
import type { AgentForm } from '~/common';
|
||||
import type { AssistantForm } from '~/common';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
export default function ImageVision() {
|
||||
const localize = useLocalize();
|
||||
const methods = useFormContext<AgentForm>();
|
||||
const methods = useFormContext<AssistantForm>();
|
||||
const { control, setValue, getValues } = methods;
|
||||
|
||||
return (
|
||||
|
|
@ -21,10 +21,12 @@ export default function ImageVision() {
|
|||
onCheckedChange={field.onChange}
|
||||
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
|
||||
value={field.value?.toString()}
|
||||
aria-labelledby="image-vision-label"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<label
|
||||
id="image-vision-label"
|
||||
className="form-check-label text-token-text-primary w-full cursor-pointer"
|
||||
htmlFor={Capabilities.image_vision}
|
||||
onClick={() =>
|
||||
|
|
|
|||
|
|
@ -112,7 +112,6 @@ export function AvatarMenu({
|
|||
isOpen={isOpen}
|
||||
setIsOpen={setIsOpen}
|
||||
menuId="agent-avatar-menu"
|
||||
placement="bottom"
|
||||
gutter={8}
|
||||
portal
|
||||
mountByState
|
||||
|
|
|
|||
|
|
@ -9,15 +9,15 @@ import {
|
|||
HoverCardPortal,
|
||||
HoverCardTrigger,
|
||||
} from '@librechat/client';
|
||||
import type { AssistantForm } from '~/common';
|
||||
import OptionHover from '~/components/SidePanel/Parameters/OptionHover';
|
||||
import type { AgentForm } from '~/common';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { ESide } from '~/common';
|
||||
import { cn } from '~/utils/';
|
||||
|
||||
export default function Retrieval({ retrievalModels }: { retrievalModels: Set<string> }) {
|
||||
const localize = useLocalize();
|
||||
const methods = useFormContext<AgentForm>();
|
||||
const methods = useFormContext<AssistantForm>();
|
||||
const { control, setValue, getValues } = methods;
|
||||
const model = useWatch({ control, name: 'model' });
|
||||
|
||||
|
|
@ -44,11 +44,13 @@ export default function Retrieval({ retrievalModels }: { retrievalModels: Set<st
|
|||
onCheckedChange={field.onChange}
|
||||
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
|
||||
value={field.value?.toString()}
|
||||
aria-labelledby="retrieval-label"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center space-x-2">
|
||||
<label
|
||||
id="retrieval-label"
|
||||
className={cn(
|
||||
'form-check-label text-token-text-primary w-full select-none',
|
||||
isDisabled ? 'cursor-no-drop opacity-50' : 'cursor-pointer',
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import '@testing-library/jest-dom/extend-expect';
|
||||
import { render, fireEvent } from '@testing-library/react';
|
||||
import type { VersionContext } from '../types';
|
||||
import VersionContent from '../VersionContent';
|
||||
import { VersionContext } from '../VersionPanel';
|
||||
|
||||
const mockRestore = 'Restore';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import '@testing-library/jest-dom/extend-expect';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import type { VersionRecord } from '../types';
|
||||
import VersionItem from '../VersionItem';
|
||||
import { VersionRecord } from '../VersionPanel';
|
||||
|
||||
jest.mock('~/hooks', () => ({
|
||||
useLocalize: jest.fn().mockImplementation(() => (key, params) => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { AgentState, VersionRecord } from '../types';
|
||||
import { isActiveVersion } from '../isActiveVersion';
|
||||
import type { AgentState, VersionRecord } from '../VersionPanel';
|
||||
|
||||
describe('isActiveVersion', () => {
|
||||
const createVersion = (overrides = {}): VersionRecord => ({
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
* @jest-environment jsdom
|
||||
*/
|
||||
/* eslint-disable i18next/no-literal-string */
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
import { render, fireEvent } from '@testing-library/react';
|
||||
import { FormProvider, useForm, type UseFormReturn } from 'react-hook-form';
|
||||
import type { AgentForm } from '~/common';
|
||||
|
|
@ -41,7 +40,15 @@ const defaultFormValues: AgentForm = {
|
|||
description: null,
|
||||
instructions: null,
|
||||
model: 'gpt-4',
|
||||
model_parameters: {},
|
||||
model_parameters: {
|
||||
temperature: 1,
|
||||
maxContextTokens: null,
|
||||
max_context_tokens: null,
|
||||
max_output_tokens: null,
|
||||
top_p: 1,
|
||||
frequency_penalty: 0,
|
||||
presence_penalty: 0,
|
||||
},
|
||||
tools: [],
|
||||
provider: 'openai',
|
||||
agent_ids: [],
|
||||
|
|
@ -62,7 +69,7 @@ const defaultFormValues: AgentForm = {
|
|||
|
||||
describe('AgentAvatar reset menu', () => {
|
||||
it('clears preview and file state when reset is triggered', () => {
|
||||
let methodsRef: UseFormReturn<AgentForm>;
|
||||
let methodsRef: UseFormReturn<AgentForm> | undefined;
|
||||
const Wrapper = () => {
|
||||
methodsRef = useForm<AgentForm>({
|
||||
defaultValues: {
|
||||
|
|
@ -88,6 +95,10 @@ describe('AgentAvatar reset menu', () => {
|
|||
const { getByTestId } = render(<Wrapper />);
|
||||
fireEvent.click(getByTestId('reset-avatar'));
|
||||
|
||||
if (!methodsRef) {
|
||||
throw new Error('Form methods were not initialized');
|
||||
}
|
||||
|
||||
expect(methodsRef.getValues('avatar_preview')).toBe('');
|
||||
expect(methodsRef.getValues('avatar_file')).toBeNull();
|
||||
expect(methodsRef.getValues('avatar_action')).toBe('reset');
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
import { describe, it, expect, jest } from '@jest/globals';
|
||||
import { Constants, type Agent } from 'librechat-data-provider';
|
||||
import type { FieldNamesMarkedBoolean } from 'react-hook-form';
|
||||
import type { AgentForm } from '~/common';
|
||||
|
|
@ -18,7 +17,15 @@ const createForm = (): AgentForm => ({
|
|||
description: null,
|
||||
instructions: null,
|
||||
model: 'gpt-4',
|
||||
model_parameters: {},
|
||||
model_parameters: {
|
||||
temperature: 1,
|
||||
maxContextTokens: null,
|
||||
max_context_tokens: null,
|
||||
max_output_tokens: null,
|
||||
top_p: 1,
|
||||
frequency_penalty: 0,
|
||||
presence_penalty: 0,
|
||||
},
|
||||
tools: [],
|
||||
provider: 'openai',
|
||||
agent_ids: [],
|
||||
|
|
@ -70,7 +77,7 @@ describe('persistAvatarChanges', () => {
|
|||
it('returns false for ephemeral agents', async () => {
|
||||
const uploadAvatar = jest.fn();
|
||||
const result = await persistAvatarChanges({
|
||||
agentId: Constants.EPHEMERAL_AGENT_ID,
|
||||
agentId: String(Constants.EPHEMERAL_AGENT_ID),
|
||||
avatarActionState: 'upload',
|
||||
avatarFile: new File(['avatar'], 'avatar.png', { type: 'image/png' }),
|
||||
uploadAvatar,
|
||||
|
|
|
|||
|
|
@ -5,4 +5,3 @@ export { default as MCPServerDialog } from './MCPServerDialog';
|
|||
export { default as MCPServerCard } from './MCPServerCard';
|
||||
export { default as MCPStatusBadge, getStatusDotColor } from './MCPStatusBadge';
|
||||
export { default as MCPCardActions } from './MCPCardActions';
|
||||
export { default as MCPAuth } from './MCPAuth';
|
||||
|
|
|
|||
|
|
@ -182,6 +182,7 @@ export default function MemoryPanel() {
|
|||
checked={referenceSavedMemories}
|
||||
tabIndex={-1}
|
||||
aria-hidden="true"
|
||||
aria-label={localize('com_ui_use_memory')}
|
||||
className="pointer-events-none mr-2"
|
||||
/>
|
||||
{localize('com_ui_use_memory')}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,9 @@ export default function UploadSkillDialog({ isOpen, setIsOpen }: UploadSkillDial
|
|||
const { showToast } = useToastContext();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const { data: skillFileConfig = { fileConfig: defaultFileConfig } } = useGetFileConfig({
|
||||
const {
|
||||
data: skillFileConfig = { configuredSizeLimitMb: undefined, fileConfig: defaultFileConfig },
|
||||
} = useGetFileConfig({
|
||||
select: (data) => ({
|
||||
configuredSizeLimitMb: data?.skills?.fileSizeLimit,
|
||||
fileConfig: mergeFileConfig(data),
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { render, screen } from '@testing-library/react';
|
|||
import { createMemoryRouter, RouterProvider } from 'react-router-dom';
|
||||
import SkillsView from '../SkillsView';
|
||||
|
||||
const mockUseHasAccess = jest.fn(() => true);
|
||||
const mockUseHasAccess = jest.fn((..._args: unknown[]) => true);
|
||||
|
||||
jest.mock(
|
||||
'librechat-data-provider',
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { memo, useState, useContext, useCallback } from 'react';
|
||||
import { Button } from '@librechat/client';
|
||||
import { ChevronLeft, ChevronRight, FileText } from 'lucide-react';
|
||||
import type { SourceData } from '~/components/Web/SourceHovercard';
|
||||
import type { CitationProps } from './types';
|
||||
import { SourceHovercard, FaviconImage, getCleanDomain } from '~/components/Web/SourceHovercard';
|
||||
import FilePreviewDialog from '~/components/Chat/Messages/Content/FilePreviewDialog';
|
||||
|
|
@ -40,6 +41,15 @@ function getFileCitationData(source?: FileCitationSource) {
|
|||
};
|
||||
}
|
||||
|
||||
function toSourceData(source: FileCitationSource): SourceData {
|
||||
return {
|
||||
link: source.link ?? '',
|
||||
title: source.title,
|
||||
attribution: source.attribution,
|
||||
snippet: source.snippet,
|
||||
};
|
||||
}
|
||||
|
||||
interface CompositeCitationProps {
|
||||
citationId?: string;
|
||||
node?: {
|
||||
|
|
@ -107,7 +117,7 @@ export function CompositeCitation(props: CompositeCitationProps) {
|
|||
return (
|
||||
<>
|
||||
<SourceHovercard
|
||||
source={currentSource}
|
||||
source={toSourceData(currentSource)}
|
||||
label={getCitationLabel()}
|
||||
onMouseEnter={() => setHoveredCitationId(citationId || null)}
|
||||
onMouseLeave={() => setHoveredCitationId(null)}
|
||||
|
|
@ -329,7 +339,7 @@ export function Citation(props: CitationComponentProps) {
|
|||
return (
|
||||
<>
|
||||
<SourceHovercard
|
||||
source={refData}
|
||||
source={toSourceData(refData)}
|
||||
label={getCitationLabel()}
|
||||
onMouseEnter={() => setHoveredCitationId(citationId || null)}
|
||||
onMouseLeave={() => setHoveredCitationId(null)}
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ import { FaviconImage, getCleanDomain } from '~/components/Web/SourceHovercard';
|
|||
import SourcesErrorBoundary from './SourcesErrorBoundary';
|
||||
import { useFileDownload } from '~/data-provider';
|
||||
import { useSearchContext } from '~/Providers';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { cn, triggerDownload } from '~/utils';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import store from '~/store';
|
||||
|
||||
interface SourceItemProps {
|
||||
|
|
@ -89,7 +89,6 @@ function SourceItem({ source, expanded = false }: SourceItemProps) {
|
|||
</Ariakit.HovercardDisclosure>
|
||||
|
||||
<Ariakit.Hovercard
|
||||
animated
|
||||
gutter={16}
|
||||
className={cn(
|
||||
'z-[999] w-[320px] max-w-[calc(100vw-2rem)] rounded-xl border border-border-medium bg-surface-secondary p-3 text-text-primary shadow-lg',
|
||||
|
|
@ -172,6 +171,7 @@ type AgentFileSource = {
|
|||
filename: string;
|
||||
bytes?: number;
|
||||
type?: string;
|
||||
source?: string;
|
||||
pages?: number[];
|
||||
relevance?: number;
|
||||
pageRelevance?: Record<number, number>;
|
||||
|
|
|
|||
|
|
@ -1,2 +1 @@
|
|||
export * from './ui';
|
||||
export * from './Plugins';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
export { Button } from '@librechat/client';
|
||||
export { default as TermsAndConditionsModal } from './TermsAndConditionsModal';
|
||||
export { default as AdminSettingsDialog } from './AdminSettingsDialog';
|
||||
export type { PermissionConfig, AdminSettingsDialogProps } from './AdminSettingsDialog';
|
||||
|
|
|
|||
|
|
@ -34,9 +34,9 @@ async function fetchAllAgentPages(params: t.AgentListParams): Promise<t.AgentLis
|
|||
object: 'list',
|
||||
data: pages.flatMap((p) => p.data),
|
||||
has_more: false,
|
||||
after: null,
|
||||
first_id: pages[0]?.first_id ?? null,
|
||||
last_id: lastPage?.last_id ?? null,
|
||||
after: undefined,
|
||||
first_id: pages[0]?.first_id ?? '',
|
||||
last_id: lastPage?.last_id ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useRecoilValue } from 'recoil';
|
||||
import { QueryKeys, dataService } from 'librechat-data-provider';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { QueryKeys, dataService } from 'librechat-data-provider';
|
||||
import type { QueryObserverResult, UseQueryOptions } from '@tanstack/react-query';
|
||||
import type t from 'librechat-data-provider';
|
||||
import store from '~/store';
|
||||
|
|
@ -32,7 +32,7 @@ export const useGraphTokenQuery = (
|
|||
|
||||
return useQuery({
|
||||
queryKey: [QueryKeys.graphToken, scopes],
|
||||
queryFn: () => dataService.getGraphApiToken({ scopes }),
|
||||
queryFn: () => dataService.getGraphApiToken({ scopes: scopes ?? '' }),
|
||||
enabled,
|
||||
staleTime: 50 * 60 * 1000, // 50 minutes (tokens expire in 60 minutes)
|
||||
retry: 1,
|
||||
|
|
|
|||
|
|
@ -38,10 +38,10 @@ export const useGetAgentFiles = <TData = t.TFile[]>(
|
|||
);
|
||||
};
|
||||
|
||||
export const useGetFileConfig = <TData = t.FileConfig>(
|
||||
config?: UseQueryOptions<t.FileConfig, unknown, TData>,
|
||||
export const useGetFileConfig = <TData = t.TFileConfig>(
|
||||
config?: UseQueryOptions<t.TFileConfig, unknown, TData>,
|
||||
): QueryObserverResult<TData, unknown> => {
|
||||
return useQuery<t.FileConfig, unknown, TData>(
|
||||
return useQuery<t.TFileConfig, unknown, TData>(
|
||||
[QueryKeys.fileConfig],
|
||||
() => dataService.getFileConfig(),
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useMutation, useQueryClient, UseMutationResult } from '@tanstack/react-query';
|
||||
import { dataService, QueryKeys, ResourceType } from 'librechat-data-provider';
|
||||
import { useMutation, useQueryClient, UseMutationResult } from '@tanstack/react-query';
|
||||
import type * as t from 'librechat-data-provider';
|
||||
|
||||
/**
|
||||
|
|
@ -84,9 +84,12 @@ export const useUpdateMCPServerMutation = (options?: {
|
|||
// Update list cache
|
||||
const listRes = queryClient.getQueryData<t.MCPServersListResponse>([QueryKeys.mcpServers]);
|
||||
if (listRes) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { type, ...oldServer } = listRes[variables.serverName!];
|
||||
listRes[variables.serverName!] = { ...oldServer, ...updatedServer };
|
||||
const oldServer = listRes[variables.serverName];
|
||||
listRes[variables.serverName] = {
|
||||
...updatedServer,
|
||||
dbId: updatedServer.dbId ?? oldServer?.dbId,
|
||||
consumeOnly: updatedServer.consumeOnly ?? oldServer?.consumeOnly,
|
||||
};
|
||||
|
||||
queryClient.setQueryData<t.MCPServersListResponse>([QueryKeys.mcpServers], {
|
||||
...listRes,
|
||||
|
|
|
|||
|
|
@ -21,7 +21,9 @@ export class MediaSourceAppender {
|
|||
}
|
||||
|
||||
public addBase64Data(base64Data: string) {
|
||||
this.addData(Uint8Array.from(atob(base64Data), (char) => char.charCodeAt(0)).buffer);
|
||||
this.addData(
|
||||
Uint8Array.from(atob(base64Data), (char) => char.charCodeAt(0)).buffer as ArrayBuffer,
|
||||
);
|
||||
}
|
||||
|
||||
public addData(data: ArrayBuffer) {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@ import { Constants } from 'librechat-data-provider';
|
|||
import type { TMessage } from 'librechat-data-provider';
|
||||
import { getMessageCacheIds, getMessagesConversationId } from '../cache';
|
||||
|
||||
/** `Constants` is a heterogeneous enum (`string | number`); annotate as
|
||||
* `string` so the member is usable where a `string` field is expected. */
|
||||
const NEW_CONVO: string = Constants.NEW_CONVO;
|
||||
|
||||
const message = (conversationId?: string | null): TMessage =>
|
||||
({
|
||||
messageId: 'message-id',
|
||||
|
|
@ -22,8 +26,8 @@ describe('chat message cache helpers', () => {
|
|||
it('mirrors new-chat messages into the generated conversation cache', () => {
|
||||
expect(
|
||||
getMessageCacheIds({
|
||||
queryParam: Constants.NEW_CONVO,
|
||||
conversationId: Constants.NEW_CONVO,
|
||||
queryParam: NEW_CONVO,
|
||||
conversationId: NEW_CONVO,
|
||||
messages: [message('generated-convo-id')],
|
||||
}),
|
||||
).toEqual([Constants.NEW_CONVO, 'generated-convo-id']);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ type MessageCacheIdsParams = {
|
|||
messages: TMessage[];
|
||||
};
|
||||
|
||||
function isConcreteConversationId(conversationId?: string | null) {
|
||||
function isConcreteConversationId(conversationId?: string | null): conversationId is string {
|
||||
return (
|
||||
!!conversationId &&
|
||||
conversationId !== Constants.NEW_CONVO &&
|
||||
|
|
|
|||
|
|
@ -4,14 +4,20 @@ import { renderHook } from '@testing-library/react';
|
|||
import { PermissionTypes, Permissions } from 'librechat-data-provider';
|
||||
import type { TUser } from 'librechat-data-provider';
|
||||
|
||||
type CloudFrontRetryOptions = { getAuthorizationHeader: () => string | undefined };
|
||||
|
||||
const mockUseHasAccess = jest.fn();
|
||||
const mockUseMCPServersQuery = jest.fn();
|
||||
const mockUseMCPToolsQuery = jest.fn();
|
||||
const mockInstallCloudFrontImageRetry = jest.fn(() => jest.fn());
|
||||
const mockInstallCloudFrontImageRetry = jest.fn(
|
||||
(_startupConfig: unknown, _options: CloudFrontRetryOptions): (() => void) =>
|
||||
() =>
|
||||
undefined,
|
||||
);
|
||||
const mockGetTokenHeader = jest.fn();
|
||||
|
||||
jest.mock('@librechat/client', () => ({
|
||||
installCloudFrontImageRetry: (startupConfig: unknown, options: unknown) =>
|
||||
installCloudFrontImageRetry: (startupConfig: unknown, options: CloudFrontRetryOptions) =>
|
||||
mockInstallCloudFrontImageRetry(startupConfig, options),
|
||||
}));
|
||||
|
||||
|
|
|
|||
|
|
@ -16,12 +16,20 @@
|
|||
*/
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { Tools } from 'librechat-data-provider';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { RecoilRoot, useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import type {
|
||||
TAttachment,
|
||||
TFile,
|
||||
TAttachmentMetadata,
|
||||
TFilePreview,
|
||||
} from 'librechat-data-provider';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { TAttachment, TFilePreview } from 'librechat-data-provider';
|
||||
import store from '~/store';
|
||||
|
||||
type AttachmentFixture = TFile & TAttachmentMetadata;
|
||||
|
||||
const mockUseFilePreview = jest.fn();
|
||||
jest.mock('~/data-provider', () => ({
|
||||
useFilePreview: (...args: unknown[]) => mockUseFilePreview(...args),
|
||||
|
|
@ -34,19 +42,24 @@ const wrapper = ({ children }: { children: ReactNode }) => <RecoilRoot>{children
|
|||
const messageId = 'msg-1';
|
||||
const fileId = 'fid-1';
|
||||
|
||||
function makeAttachment(overrides: Partial<TAttachment> = {}): TAttachment {
|
||||
function makeAttachment(overrides: Partial<AttachmentFixture> = {}): AttachmentFixture {
|
||||
return {
|
||||
user: 'user-1',
|
||||
object: 'file',
|
||||
bytes: 1024,
|
||||
embedded: false,
|
||||
usage: 0,
|
||||
file_id: fileId,
|
||||
filename: 'data.xlsx',
|
||||
filepath: '/uploads/data.xlsx',
|
||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
type: Tools.execute_code,
|
||||
messageId,
|
||||
toolCallId: 'tc-1',
|
||||
text: null,
|
||||
textFormat: null,
|
||||
text: undefined,
|
||||
textFormat: undefined,
|
||||
status: 'pending',
|
||||
...overrides,
|
||||
} as unknown as TAttachment;
|
||||
};
|
||||
}
|
||||
|
||||
/** Read messageAttachmentsMap and bridge it out via a mutable ref. */
|
||||
|
|
@ -261,7 +274,7 @@ describe('useAttachmentPreviewSync', () => {
|
|||
textFormat: 'html',
|
||||
},
|
||||
});
|
||||
const updated = ctx.map[messageId]?.[0] as TAttachment & { text?: string };
|
||||
const updated = ctx.map[messageId]?.[0] as AttachmentFixture;
|
||||
expect(updated.status).toBe('ready');
|
||||
expect(updated.text).toBe('<table>final</table>');
|
||||
expect(ctx.result.current.status).toBe('ready');
|
||||
|
|
@ -277,7 +290,7 @@ describe('useAttachmentPreviewSync', () => {
|
|||
previewError: 'parser-error',
|
||||
},
|
||||
});
|
||||
const updated = ctx.map[messageId]?.[0] as TAttachment & { previewError?: string };
|
||||
const updated = ctx.map[messageId]?.[0] as AttachmentFixture;
|
||||
expect(updated.status).toBe('failed');
|
||||
expect(updated.previewError).toBe('parser-error');
|
||||
expect(ctx.result.current.status).toBe('failed');
|
||||
|
|
@ -293,7 +306,7 @@ describe('useAttachmentPreviewSync', () => {
|
|||
/* Map should be unchanged from the initial seed — no patch. */
|
||||
const list = ctx.map[messageId] ?? [];
|
||||
expect(list).toHaveLength(1);
|
||||
expect((list[0] as TAttachment & { status?: string }).status).toBe('pending');
|
||||
expect((list[0] as AttachmentFixture).status).toBe('pending');
|
||||
});
|
||||
|
||||
it('reports isPolling true when the query is fetching and the gate is open', () => {
|
||||
|
|
@ -336,7 +349,7 @@ describe('useAttachmentPreviewSync', () => {
|
|||
});
|
||||
const list = ctx.map[messageId] ?? [];
|
||||
expect(list).toHaveLength(1);
|
||||
const inserted = list[0] as TAttachment & { text?: string; textFormat?: string };
|
||||
const inserted = list[0] as AttachmentFixture;
|
||||
expect(inserted.file_id).toBe(fileId);
|
||||
expect(inserted.status).toBe('ready');
|
||||
expect(inserted.text).toBe('<table>resolved-on-reload</table>');
|
||||
|
|
@ -361,7 +374,7 @@ describe('useAttachmentPreviewSync', () => {
|
|||
});
|
||||
const list = ctx.map[messageId] ?? [];
|
||||
expect(list).toHaveLength(1);
|
||||
const inserted = list[0] as TAttachment & { previewError?: string };
|
||||
const inserted = list[0] as AttachmentFixture;
|
||||
expect(inserted.status).toBe('failed');
|
||||
expect(inserted.previewError).toBe('render-timeout');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ export default function useDragHelpers() {
|
|||
currentEndpoint,
|
||||
agent?.provider,
|
||||
);
|
||||
const cfg = queryClient.getQueryData<t.FileConfig>([QueryKeys.fileConfig]);
|
||||
const cfg = queryClient.getQueryData<t.TFileConfig>([QueryKeys.fileConfig]);
|
||||
if (cfg) {
|
||||
const mergedCfg = mergeFileConfig(cfg);
|
||||
const endpointCfg = getEndpointFileConfig({
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import React from 'react';
|
||||
import { Provider, createStore } from 'jotai';
|
||||
import { renderHook, act, waitFor } from '@testing-library/react';
|
||||
import { RecoilRoot, useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { Constants, LocalStorageKeys } from 'librechat-data-provider';
|
||||
import { RecoilRoot, useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { MCPServerDefinition } from '../useMCPServerManager';
|
||||
import { ephemeralAgentByConvoId } from '~/store';
|
||||
import { setTimestamp } from '~/utils/timestamps';
|
||||
import { useMCPSelect } from '../useMCPSelect';
|
||||
import { MCPServerDefinition } from '../useMCPServerManager';
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('~/utils/timestamps', () => ({
|
||||
|
|
@ -20,6 +20,7 @@ const createMCPServers = (serverNames: string[]): MCPServerDefinition[] => {
|
|||
return serverNames.map((serverName) => ({
|
||||
serverName,
|
||||
config: {
|
||||
type: 'sse',
|
||||
url: 'http://mcp',
|
||||
},
|
||||
effectivePermissions: 15, // All permissions (VIEW=1, EDIT=2, DELETE=4, SHARE=8)
|
||||
|
|
|
|||
|
|
@ -11,31 +11,39 @@
|
|||
*/
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { Tools } from 'librechat-data-provider';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { RecoilRoot, useSetRecoilState } from 'recoil';
|
||||
import type { TAttachment, TFile, TAttachmentMetadata } from 'librechat-data-provider';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { TAttachment } from 'librechat-data-provider';
|
||||
import store from '~/store';
|
||||
|
||||
type AttachmentFixture = TFile & TAttachmentMetadata;
|
||||
|
||||
jest.mock('~/hooks/useLocalize', () => () => (key: string) => key);
|
||||
|
||||
import useAttachments from '../useAttachments';
|
||||
|
||||
const messageId = 'msg-1';
|
||||
|
||||
function makeAttachment(overrides: Partial<TAttachment> = {}): TAttachment {
|
||||
function makeAttachment(overrides: Partial<AttachmentFixture> = {}): AttachmentFixture {
|
||||
return {
|
||||
user: 'user-1',
|
||||
object: 'file',
|
||||
bytes: 1024,
|
||||
embedded: false,
|
||||
usage: 0,
|
||||
file_id: 'fid-1',
|
||||
filename: 'data.xlsx',
|
||||
filepath: '/uploads/data.xlsx',
|
||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
type: Tools.execute_code,
|
||||
messageId,
|
||||
toolCallId: 'tc-1',
|
||||
text: null,
|
||||
textFormat: null,
|
||||
text: undefined,
|
||||
textFormat: undefined,
|
||||
status: 'pending',
|
||||
...overrides,
|
||||
} as unknown as TAttachment;
|
||||
};
|
||||
}
|
||||
|
||||
function setup({
|
||||
|
|
@ -91,7 +99,7 @@ describe('useAttachments', () => {
|
|||
* immediate-persist snapshot; the polling layer fetched the
|
||||
* resolved record and inserted it into messageAttachmentsMap; the
|
||||
* merge here must surface the resolved fields to the renderer. */
|
||||
const db = makeAttachment({ status: 'pending', text: null, textFormat: null });
|
||||
const db = makeAttachment({ status: 'pending', text: undefined, textFormat: undefined });
|
||||
const live = makeAttachment({
|
||||
status: 'ready',
|
||||
text: '<table>resolved</table>',
|
||||
|
|
@ -102,10 +110,7 @@ describe('useAttachments', () => {
|
|||
liveMap: { [messageId]: [live] },
|
||||
});
|
||||
expect(result.current.attachments).toHaveLength(1);
|
||||
const merged = result.current.attachments[0] as TAttachment & {
|
||||
text?: string;
|
||||
textFormat?: string;
|
||||
};
|
||||
const merged = result.current.attachments[0] as AttachmentFixture;
|
||||
expect(merged.status).toBe('ready');
|
||||
expect(merged.text).toBe('<table>resolved</table>');
|
||||
expect(merged.textFormat).toBe('html');
|
||||
|
|
@ -123,7 +128,7 @@ describe('useAttachments', () => {
|
|||
/* DB attachments are the authoritative list for THIS message — a
|
||||
* live entry without a matching file_id must NOT bleed in. */
|
||||
expect(result.current.attachments).toHaveLength(1);
|
||||
expect((result.current.attachments[0] as TAttachment).file_id).toBe('fid-A');
|
||||
expect((result.current.attachments[0] as TAttachment).status).toBe('pending');
|
||||
expect((result.current.attachments[0] as AttachmentFixture).file_id).toBe('fid-A');
|
||||
expect((result.current.attachments[0] as AttachmentFixture).status).toBe('pending');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ export default function useCopyToClipboard({
|
|||
if (content) {
|
||||
messageText = content.reduce((acc, curr, i) => {
|
||||
if (curr.type === ContentTypes.TEXT) {
|
||||
const text = typeof curr.text === 'string' ? curr.text : curr.text.value;
|
||||
const text = typeof curr.text === 'string' ? curr.text : (curr.text?.value ?? '');
|
||||
return acc + text + (i === content.length - 1 ? '' : '\n');
|
||||
}
|
||||
return acc;
|
||||
|
|
|
|||
|
|
@ -20,6 +20,17 @@ interface DeduplicatedSource {
|
|||
metadata?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* The `file_search` attachment is typed as {@link SearchResultData} in the
|
||||
* shared schema, but at runtime the agent file-search tool emits a
|
||||
* `{ sources: FileSource[] }` payload. Read the sources defensively.
|
||||
*/
|
||||
type FileSearchAttachmentData = SearchResultData & { sources?: FileSource[] };
|
||||
|
||||
function getFileSearchSources(data: FileSearchAttachmentData): FileSource[] {
|
||||
return Array.isArray(data.sources) ? data.sources : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook that creates a map of turn numbers to SearchResultData from web search and agent file search attachments
|
||||
* @param attachments Array of attachment metadata
|
||||
|
|
@ -42,7 +53,7 @@ export function useSearchResultsByTurn(attachments?: TAttachment[]) {
|
|||
|
||||
// Handle agent file search attachments (following web search pattern)
|
||||
if (attachment.type === Tools.file_search && attachment[Tools.file_search]) {
|
||||
const sources = attachment[Tools.file_search].sources;
|
||||
const sources = getFileSearchSources(attachment[Tools.file_search]);
|
||||
|
||||
// Deduplicate sources by fileId and merge pages
|
||||
const deduplicatedSources = new Map<string, DeduplicatedSource>();
|
||||
|
|
|
|||
|
|
@ -14,33 +14,45 @@
|
|||
* unrelated citations both show up.
|
||||
*/
|
||||
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { Tools } from 'librechat-data-provider';
|
||||
import { RecoilRoot, useRecoilValue } from 'recoil';
|
||||
import { QueryClient } from '@tanstack/react-query';
|
||||
import { Tools } from 'librechat-data-provider';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import type {
|
||||
TFile,
|
||||
TAttachment,
|
||||
EventSubmission,
|
||||
TAttachmentMetadata,
|
||||
} from 'librechat-data-provider';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { TAttachment, EventSubmission } from 'librechat-data-provider';
|
||||
import useAttachmentHandler from '../useAttachmentHandler';
|
||||
import store from '~/store';
|
||||
|
||||
type AttachmentFixture = TFile & TAttachmentMetadata;
|
||||
|
||||
const wrapper = ({ children }: { children: ReactNode }) => <RecoilRoot>{children}</RecoilRoot>;
|
||||
|
||||
const submission = {} as EventSubmission;
|
||||
const messageId = 'msg-1';
|
||||
|
||||
function makeAttachment(overrides: Partial<TAttachment>): TAttachment {
|
||||
function makeAttachment(overrides: Partial<AttachmentFixture>): AttachmentFixture {
|
||||
return {
|
||||
user: 'user-1',
|
||||
object: 'file',
|
||||
bytes: 1024,
|
||||
embedded: false,
|
||||
usage: 0,
|
||||
file_id: 'fid-1',
|
||||
filename: 'data.xlsx',
|
||||
filepath: '/uploads/data.xlsx',
|
||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
type: Tools.execute_code,
|
||||
messageId,
|
||||
toolCallId: 'tc-1',
|
||||
text: null,
|
||||
textFormat: null,
|
||||
text: undefined,
|
||||
textFormat: undefined,
|
||||
status: 'pending',
|
||||
...overrides,
|
||||
} as unknown as TAttachment;
|
||||
};
|
||||
}
|
||||
|
||||
/* Co-mount the handler and a reader of the messageAttachmentsMap atom in
|
||||
|
|
@ -162,7 +174,7 @@ describe('useAttachmentHandler upsert-by-file_id', () => {
|
|||
* downgrade it. Otherwise the chip flickers back to "pending" and
|
||||
* polling restarts until the lazy sweep catches up. (Codex P1.) */
|
||||
const ctx = setup();
|
||||
ctx.handle(makeAttachment({ status: 'pending', text: null }));
|
||||
ctx.handle(makeAttachment({ status: 'pending', text: undefined }));
|
||||
ctx.handle({
|
||||
file_id: 'fid-1',
|
||||
messageId,
|
||||
|
|
@ -171,7 +183,7 @@ describe('useAttachmentHandler upsert-by-file_id', () => {
|
|||
textFormat: 'html',
|
||||
} as unknown as TAttachment);
|
||||
/* Phase-1 replay arrives last (finalHandler at stream end). */
|
||||
ctx.handle(makeAttachment({ status: 'pending', text: null }));
|
||||
ctx.handle(makeAttachment({ status: 'pending', text: undefined }));
|
||||
expect(ctx.list).toHaveLength(1);
|
||||
expect(ctx.list[0]).toMatchObject({
|
||||
status: 'ready',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { renderHook, act } from '@testing-library/react';
|
||||
import { RecoilRoot, useRecoilCallback } from 'recoil';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import {
|
||||
Constants,
|
||||
StepTypes,
|
||||
|
|
@ -17,8 +17,20 @@ import type {
|
|||
SubagentUpdateEvent,
|
||||
Agents,
|
||||
} from 'librechat-data-provider';
|
||||
import useStepHandler from '~/hooks/SSE/useStepHandler';
|
||||
import { subagentProgressByToolCallId } from '~/store/subagents';
|
||||
import useStepHandler from '~/hooks/SSE/useStepHandler';
|
||||
|
||||
/** `Constants` is a heterogeneous enum (`string | number`); annotate as
|
||||
* `string` so the member is usable where a `string` field is expected. */
|
||||
const USE_PRELIM_RESPONSE_MESSAGE_ID: string = Constants.USE_PRELIM_RESPONSE_MESSAGE_ID;
|
||||
|
||||
const getToolCallName = (part?: TMessageContentParts): string | undefined => {
|
||||
if (part?.type !== ContentTypes.TOOL_CALL) {
|
||||
return undefined;
|
||||
}
|
||||
const { tool_call: toolCall } = part;
|
||||
return 'name' in toolCall ? toolCall.name : undefined;
|
||||
};
|
||||
|
||||
type TSubmissionForTest = {
|
||||
userMessage: TMessage;
|
||||
|
|
@ -247,7 +259,7 @@ describe('useStepHandler', () => {
|
|||
|
||||
const firstRunStep = createToolCallRunStep({
|
||||
id: 'step-oauth-eli',
|
||||
runId: Constants.USE_PRELIM_RESPONSE_MESSAGE_ID,
|
||||
runId: USE_PRELIM_RESPONSE_MESSAGE_ID,
|
||||
index: 0,
|
||||
stepDetails: {
|
||||
type: StepTypes.TOOL_CALLS,
|
||||
|
|
@ -263,7 +275,7 @@ describe('useStepHandler', () => {
|
|||
});
|
||||
const secondRunStep = createToolCallRunStep({
|
||||
id: 'step-oauth-vespa',
|
||||
runId: Constants.USE_PRELIM_RESPONSE_MESSAGE_ID,
|
||||
runId: USE_PRELIM_RESPONSE_MESSAGE_ID,
|
||||
index: 1,
|
||||
stepDetails: {
|
||||
type: StepTypes.TOOL_CALLS,
|
||||
|
|
@ -291,8 +303,8 @@ describe('useStepHandler', () => {
|
|||
|
||||
const responseMsg = currentMessages.find((m) => !m.isCreatedByUser);
|
||||
expect(responseMsg?.content).toHaveLength(2);
|
||||
expect(responseMsg?.content?.[0]?.tool_call?.name).toBe(`oauth${Constants.mcp_delimiter}ELI`);
|
||||
expect(responseMsg?.content?.[1]?.tool_call?.name).toBe(
|
||||
expect(getToolCallName(responseMsg?.content?.[0])).toBe(`oauth${Constants.mcp_delimiter}ELI`);
|
||||
expect(getToolCallName(responseMsg?.content?.[1])).toBe(
|
||||
`oauth${Constants.mcp_delimiter}Vespa`,
|
||||
);
|
||||
});
|
||||
|
|
@ -314,7 +326,7 @@ describe('useStepHandler', () => {
|
|||
event: StepEvents.ON_RUN_STEP,
|
||||
data: createToolCallRunStep({
|
||||
id: 'step-oauth-eli',
|
||||
runId: Constants.USE_PRELIM_RESPONSE_MESSAGE_ID,
|
||||
runId: USE_PRELIM_RESPONSE_MESSAGE_ID,
|
||||
index: 0,
|
||||
stepDetails: {
|
||||
type: StepTypes.TOOL_CALLS,
|
||||
|
|
@ -336,7 +348,7 @@ describe('useStepHandler', () => {
|
|||
event: StepEvents.ON_RUN_STEP,
|
||||
data: createToolCallRunStep({
|
||||
id: 'step-oauth-vespa',
|
||||
runId: Constants.USE_PRELIM_RESPONSE_MESSAGE_ID,
|
||||
runId: USE_PRELIM_RESPONSE_MESSAGE_ID,
|
||||
index: 1,
|
||||
stepDetails: {
|
||||
type: StepTypes.TOOL_CALLS,
|
||||
|
|
@ -358,7 +370,7 @@ describe('useStepHandler', () => {
|
|||
event: StepEvents.ON_RUN_STEP,
|
||||
data: createRunStep({
|
||||
id: 'step-message',
|
||||
runId: Constants.USE_PRELIM_RESPONSE_MESSAGE_ID,
|
||||
runId: USE_PRELIM_RESPONSE_MESSAGE_ID,
|
||||
index: 0,
|
||||
}),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -20,14 +20,14 @@ import type {
|
|||
} from 'librechat-data-provider';
|
||||
import type { SetterOrUpdater } from 'recoil';
|
||||
import type { AnnounceOptions } from '~/common';
|
||||
import { MESSAGE_UPDATE_INTERVAL } from '~/common';
|
||||
import { subagentProgressByToolCallId } from '~/store';
|
||||
import {
|
||||
foldSubagentEvent,
|
||||
foldSubagentEventIntoTicker,
|
||||
initSubagentAggregatorState,
|
||||
initSubagentTickerState,
|
||||
} from '~/utils/subagentContent';
|
||||
import { subagentProgressByToolCallId } from '~/store';
|
||||
import { MESSAGE_UPDATE_INTERVAL } from '~/common';
|
||||
|
||||
type TUseStepHandler = {
|
||||
announcePolite: (options: AnnounceOptions) => void;
|
||||
|
|
@ -66,10 +66,14 @@ type AllContentTypes =
|
|||
const isOAuthToolCallName = (name?: string) =>
|
||||
typeof name === 'string' && name.startsWith(`oauth${Constants.mcp_delimiter}`);
|
||||
|
||||
const isOAuthToolCallContent = (part?: Partial<TMessageContentParts>) =>
|
||||
part?.type === ContentTypes.TOOL_CALL &&
|
||||
'tool_call' in part &&
|
||||
isOAuthToolCallName(part.tool_call?.name);
|
||||
const isOAuthToolCallContent = (part?: Partial<TMessageContentParts>) => {
|
||||
if (part?.type !== ContentTypes.TOOL_CALL || !('tool_call' in part)) {
|
||||
return false;
|
||||
}
|
||||
const { tool_call: toolCall } = part;
|
||||
const name = toolCall != null && 'name' in toolCall ? toolCall.name : undefined;
|
||||
return isOAuthToolCallName(name);
|
||||
};
|
||||
|
||||
export default function useStepHandler({
|
||||
setMessages,
|
||||
|
|
|
|||
|
|
@ -105,8 +105,11 @@ describe('useRum', () => {
|
|||
});
|
||||
|
||||
it('initializes proxy RUM with the LibreChat bearer token for same-origin ingest', async () => {
|
||||
const fetchMock = jest.fn(() => Promise.resolve({}));
|
||||
window.fetch = fetchMock;
|
||||
const fetchMock = jest.fn(
|
||||
(_input: RequestInfo | URL, _init?: RequestInit): Promise<Response> =>
|
||||
Promise.resolve({ ok: true, status: 200 } as Response),
|
||||
);
|
||||
window.fetch = Object.assign(fetchMock, { preconnect: () => undefined });
|
||||
mockUseGetStartupConfig.mockReturnValue({
|
||||
data: {
|
||||
rum: {
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ function ensureRumProxyAuth(proxyUrl: string): void {
|
|||
}
|
||||
|
||||
const originalFetch = window.fetch.bind(window);
|
||||
window.fetch = (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const patchedFetch = (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
||||
if (rumProxyToken && isRumProxyRequest(input, proxyUrl)) {
|
||||
const [authorizedInput, authorizedInit] = withAuthorization(input, init, rumProxyToken);
|
||||
return originalFetch(authorizedInput, authorizedInit);
|
||||
|
|
@ -84,6 +84,7 @@ function ensureRumProxyAuth(proxyUrl: string): void {
|
|||
|
||||
return originalFetch(input, init);
|
||||
};
|
||||
window.fetch = Object.assign(patchedFetch, { preconnect: window.fetch.preconnect });
|
||||
rumProxyFetchPatched = true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1063,6 +1063,7 @@
|
|||
"com_ui_file_token_limit": "File Token Limit",
|
||||
"com_ui_file_token_limit_desc": "Set maximum token limit for file processing to control costs and resource usage",
|
||||
"com_ui_files": "Files",
|
||||
"com_ui_files_count_size": "{{0}} Files ({{1}}KB)",
|
||||
"com_ui_filter_mcp_servers": "Filter MCP servers by name",
|
||||
"com_ui_filter_prompts": "Filter Prompts",
|
||||
"com_ui_filter_prompts_name": "Filter prompts by name",
|
||||
|
|
@ -1261,6 +1262,7 @@
|
|||
"com_ui_model_parameters_reset": "Model Parameters have been reset.",
|
||||
"com_ui_model_selected": "{{0}} selected",
|
||||
"com_ui_model_spec": "Model Spec",
|
||||
"com_ui_more_count": "{{0}} more",
|
||||
"com_ui_more_info": "More info",
|
||||
"com_ui_more_options": "More options",
|
||||
"com_ui_my_prompts": "My Prompts",
|
||||
|
|
|
|||
|
|
@ -323,7 +323,7 @@ describe('deleteData', () => {
|
|||
it('removes the matching item from its page', () => {
|
||||
const data = makeInfiniteData([{ items: [makeItem('a', 'Alpha'), makeItem('b', 'Beta')] }]);
|
||||
|
||||
const result = deleteData<Page, Item>(data, 'items', (page) =>
|
||||
const result = deleteData<Page, InfiniteData<Page>>(data, 'items', (page) =>
|
||||
page.items.findIndex((i) => i.id === 'a'),
|
||||
);
|
||||
|
||||
|
|
@ -334,7 +334,7 @@ describe('deleteData', () => {
|
|||
it('leaves data unchanged when item is not found', () => {
|
||||
const data = makeInfiniteData([{ items: [makeItem('a', 'Alpha')] }]);
|
||||
|
||||
const result = deleteData<Page, Item>(data, 'items', (page) =>
|
||||
const result = deleteData<Page, InfiniteData<Page>>(data, 'items', (page) =>
|
||||
page.items.findIndex((i) => i.id === 'z'),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ describe('ConversationTag Utilities', () => {
|
|||
beforeEach(() => {
|
||||
conversations = [
|
||||
{
|
||||
_id: 'tag-id-saved',
|
||||
tag: 'saved',
|
||||
count: 1,
|
||||
position: 0,
|
||||
|
|
@ -16,6 +17,7 @@ describe('ConversationTag Utilities', () => {
|
|||
user: 'user1',
|
||||
},
|
||||
{
|
||||
_id: 'tag-id-1',
|
||||
tag: 'tag1',
|
||||
count: 1,
|
||||
position: 1,
|
||||
|
|
@ -25,6 +27,7 @@ describe('ConversationTag Utilities', () => {
|
|||
user: 'user1',
|
||||
},
|
||||
{
|
||||
_id: 'tag-id-2',
|
||||
tag: 'tag2',
|
||||
count: 20,
|
||||
position: 2,
|
||||
|
|
@ -34,6 +37,7 @@ describe('ConversationTag Utilities', () => {
|
|||
user: 'user1',
|
||||
},
|
||||
{
|
||||
_id: 'tag-id-3',
|
||||
tag: 'tag3',
|
||||
count: 30,
|
||||
position: 3,
|
||||
|
|
@ -43,6 +47,7 @@ describe('ConversationTag Utilities', () => {
|
|||
user: 'user1',
|
||||
},
|
||||
{
|
||||
_id: 'tag-id-4',
|
||||
tag: 'tag4',
|
||||
count: 40,
|
||||
position: 4,
|
||||
|
|
@ -52,6 +57,7 @@ describe('ConversationTag Utilities', () => {
|
|||
user: 'user1',
|
||||
},
|
||||
{
|
||||
_id: 'tag-id-5',
|
||||
tag: 'tag5',
|
||||
count: 50,
|
||||
position: 5,
|
||||
|
|
@ -175,6 +181,7 @@ describe('ConversationTag Utilities', () => {
|
|||
conversations,
|
||||
{ tag: 'newtag', description: 'newDescription' },
|
||||
{
|
||||
_id: 'tag-id-newtag',
|
||||
tag: 'newtag',
|
||||
description: 'newDescription',
|
||||
position: 1,
|
||||
|
|
@ -208,6 +215,7 @@ describe('ConversationTag Utilities', () => {
|
|||
[],
|
||||
{ tag: 'newtag', description: 'newDescription' },
|
||||
{
|
||||
_id: 'tag-id-saved',
|
||||
tag: 'saved',
|
||||
description: 'newDescription',
|
||||
position: 0,
|
||||
|
|
|
|||
|
|
@ -45,7 +45,9 @@ export function supportsClientResize(): boolean {
|
|||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
return !!(ctx && ctx.drawImage && canvas.toBlob);
|
||||
return (
|
||||
ctx != null && typeof ctx.drawImage === 'function' && typeof canvas.toBlob === 'function'
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,10 +28,8 @@
|
|||
"include": [
|
||||
"src/**/*",
|
||||
"test/**/*",
|
||||
"../e2e/**/*",
|
||||
"test/setupTests.js",
|
||||
"env.d.ts",
|
||||
"../config/translations/**/*.ts",
|
||||
"../packages/client/src/hooks/useDelayedRender.tsx",
|
||||
"../packages/client/src/components/InfoHoverCard.tsx"
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,18 +1,19 @@
|
|||
import type { AxiosResponse } from 'axios';
|
||||
import type { TFileConfig } from './file-config';
|
||||
import type * as t from './types';
|
||||
import * as permissions from './accessPermissions';
|
||||
import * as endpoints from './api-endpoints';
|
||||
import * as a from './types/assistants';
|
||||
import * as ag from './types/agents';
|
||||
import * as m from './types/mutations';
|
||||
import * as q from './types/queries';
|
||||
import * as f from './types/files';
|
||||
import * as sk from './types/skills';
|
||||
import * as mcp from './types/mcpServers';
|
||||
import * as a from './types/assistants';
|
||||
import * as m from './types/mutations';
|
||||
import * as ag from './types/agents';
|
||||
import * as q from './types/queries';
|
||||
import * as sk from './types/skills';
|
||||
import * as f from './types/files';
|
||||
import * as config from './config';
|
||||
import request from './request';
|
||||
import * as s from './schemas';
|
||||
import * as r from './roles';
|
||||
import * as permissions from './accessPermissions';
|
||||
|
||||
export function revokeUserKey(name: string): Promise<unknown> {
|
||||
return request.delete(endpoints.revokeUserKey(name));
|
||||
|
|
@ -429,7 +430,7 @@ export const getAgentFiles = (agentId: string): Promise<f.TFile[]> => {
|
|||
return request.get(endpoints.agentFiles(agentId));
|
||||
};
|
||||
|
||||
export const getFileConfig = (): Promise<f.FileConfig> => {
|
||||
export const getFileConfig = (): Promise<TFileConfig> => {
|
||||
return request.get(`${endpoints.files()}/config`);
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue