🌐 feat: Add support to SubDirectory hosting (#9155)

* feat: Add support to SubDirectory hosting

* fix: address linting and failing test

* fix: browser context validation

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
José Pedro Silva 2025-08-27 07:00:18 +01:00 committed by GitHub
parent a820863e8b
commit 18d5a75cdc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 252 additions and 199 deletions

View file

@ -62,7 +62,7 @@ export default () => (
<ScreenshotProvider>
<App />
<iframe
src="/assets/silence.mp3"
src="assets/silence.mp3"
allow="autoplay"
id="audio"
title="audio-silence"

View file

@ -62,7 +62,7 @@ function AuthLayout({
<BlinkAnimation active={isFetching}>
<div className="mt-6 h-10 w-full bg-cover">
<img
src="/assets/logo.svg"
src="assets/logo.svg"
className="h-full w-full object-contain"
alt={localize('com_ui_logo', { 0: startupConfig?.appTitle ?? 'LibreChat' })}
/>

View file

@ -1,7 +1,7 @@
import React, { memo, useMemo, useRef, useEffect } from 'react';
import { useRecoilValue } from 'recoil';
import { useToastContext } from '@librechat/client';
import { PermissionTypes, Permissions } from 'librechat-data-provider';
import { PermissionTypes, Permissions, dataService } from 'librechat-data-provider';
import CodeBlock from '~/components/Messages/Content/CodeBlock';
import useHasAccess from '~/hooks/Roles/useHasAccess';
import { useFileDownload } from '~/data-provider';
@ -135,9 +135,15 @@ export const a: React.ElementType = memo(({ href, children }: TAnchorProps) => {
props.onClick = handleDownload;
props.target = '_blank';
const domainServerBaseUrl = dataService.getDomainServerBaseUrl();
return (
<a
href={filepath?.startsWith('files/') ? `/api/${filepath}` : `/api/files/${filepath}`}
href={
filepath?.startsWith('files/')
? `${domainServerBaseUrl}/${filepath}`
: `${domainServerBaseUrl}/files/${filepath}`
}
{...props}
>
{children}

View file

@ -65,7 +65,7 @@ export default function ApiKeyDialog({
{languageIcons.map((icon) => (
<div key={icon} className="h-6 w-6">
<img
src={`/assets/${icon}`}
src={`assets/${icon}`}
alt=""
className="h-full w-full object-contain opacity-[0.85] dark:invert"
/>

View file

@ -5,24 +5,24 @@ import { IconContext } from '~/common';
import { cn } from '~/utils';
const knownEndpointAssets = {
[KnownEndpoints.anyscale]: '/assets/anyscale.png',
[KnownEndpoints.apipie]: '/assets/apipie.png',
[KnownEndpoints.cohere]: '/assets/cohere.png',
[KnownEndpoints.deepseek]: '/assets/deepseek.svg',
[KnownEndpoints.fireworks]: '/assets/fireworks.png',
[KnownEndpoints.google]: '/assets/google.svg',
[KnownEndpoints.groq]: '/assets/groq.png',
[KnownEndpoints.huggingface]: '/assets/huggingface.svg',
[KnownEndpoints.mistral]: '/assets/mistral.png',
[KnownEndpoints.mlx]: '/assets/mlx.png',
[KnownEndpoints.ollama]: '/assets/ollama.png',
[KnownEndpoints.openai]: '/assets/openai.svg',
[KnownEndpoints.openrouter]: '/assets/openrouter.png',
[KnownEndpoints.perplexity]: '/assets/perplexity.png',
[KnownEndpoints.qwen]: '/assets/qwen.svg',
[KnownEndpoints.shuttleai]: '/assets/shuttleai.png',
[KnownEndpoints['together.ai']]: '/assets/together.png',
[KnownEndpoints.unify]: '/assets/unify.webp',
[KnownEndpoints.anyscale]: 'assets/anyscale.png',
[KnownEndpoints.apipie]: 'assets/apipie.png',
[KnownEndpoints.cohere]: 'assets/cohere.png',
[KnownEndpoints.deepseek]: 'assets/deepseek.svg',
[KnownEndpoints.fireworks]: 'assets/fireworks.png',
[KnownEndpoints.google]: 'assets/google.svg',
[KnownEndpoints.groq]: 'assets/groq.png',
[KnownEndpoints.huggingface]: 'assets/huggingface.svg',
[KnownEndpoints.mistral]: 'assets/mistral.png',
[KnownEndpoints.mlx]: 'assets/mlx.png',
[KnownEndpoints.ollama]: 'assets/ollama.png',
[KnownEndpoints.openai]: 'assets/openai.svg',
[KnownEndpoints.openrouter]: 'assets/openrouter.png',
[KnownEndpoints.perplexity]: 'assets/perplexity.png',
[KnownEndpoints.qwen]: 'assets/qwen.svg',
[KnownEndpoints.shuttleai]: 'assets/shuttleai.png',
[KnownEndpoints['together.ai']]: 'assets/together.png',
[KnownEndpoints.unify]: 'assets/unify.webp',
};
const knownEndpointClasses = {

View file

@ -11,7 +11,7 @@ export default function useAttachmentHandler(queryClient?: QueryClient) {
return ({ data }: { data: TAttachment; submission: EventSubmission }) => {
const { messageId } = data;
if (queryClient && data?.filepath && !data.filepath.startsWith('/api/files')) {
if (queryClient && data?.filepath && !data.filepath.includes('/api/files')) {
queryClient.setQueryData([QueryKeys.files], (oldData: TAttachment[] | undefined) => {
return [data, ...(oldData || [])];
});

View file

@ -27,95 +27,101 @@ const AuthLayout = () => (
</AuthContextProvider>
);
export const router = createBrowserRouter([
{
path: 'share/:shareId',
element: <ShareRoute />,
errorElement: <RouteErrorBoundary />,
},
{
path: 'oauth',
errorElement: <RouteErrorBoundary />,
children: [
{
path: 'success',
element: <OAuthSuccess />,
},
{
path: 'error',
element: <OAuthError />,
},
],
},
{
path: '/',
element: <StartupLayout />,
errorElement: <RouteErrorBoundary />,
children: [
{
path: 'register',
element: <Registration />,
},
{
path: 'forgot-password',
element: <RequestPasswordReset />,
},
{
path: 'reset-password',
element: <ResetPassword />,
},
],
},
{
path: 'verify',
element: <VerifyEmail />,
errorElement: <RouteErrorBoundary />,
},
{
element: <AuthLayout />,
errorElement: <RouteErrorBoundary />,
children: [
{
path: '/',
element: <LoginLayout />,
children: [
{
path: 'login',
element: <Login />,
},
{
path: 'login/2fa',
element: <TwoFactorScreen />,
},
],
},
dashboardRoutes,
{
path: '/',
element: <Root />,
children: [
{
index: true,
element: <Navigate to="/c/new" replace={true} />,
},
{
path: 'c/:conversationId?',
element: <ChatRoute />,
},
{
path: 'search',
element: <Search />,
},
{
path: 'agents',
element: <AgentMarketplace />,
},
{
path: 'agents/:category',
element: <AgentMarketplace />,
},
],
},
],
},
]);
const baseEl = document.querySelector('base');
const baseHref = baseEl?.getAttribute('href') || '/';
export const router = createBrowserRouter(
[
{
path: 'share/:shareId',
element: <ShareRoute />,
errorElement: <RouteErrorBoundary />,
},
{
path: 'oauth',
errorElement: <RouteErrorBoundary />,
children: [
{
path: 'success',
element: <OAuthSuccess />,
},
{
path: 'error',
element: <OAuthError />,
},
],
},
{
path: '/',
element: <StartupLayout />,
errorElement: <RouteErrorBoundary />,
children: [
{
path: 'register',
element: <Registration />,
},
{
path: 'forgot-password',
element: <RequestPasswordReset />,
},
{
path: 'reset-password',
element: <ResetPassword />,
},
],
},
{
path: 'verify',
element: <VerifyEmail />,
errorElement: <RouteErrorBoundary />,
},
{
element: <AuthLayout />,
errorElement: <RouteErrorBoundary />,
children: [
{
path: '/',
element: <LoginLayout />,
children: [
{
path: 'login',
element: <Login />,
},
{
path: 'login/2fa',
element: <TwoFactorScreen />,
},
],
},
dashboardRoutes,
{
path: '/',
element: <Root />,
children: [
{
index: true,
element: <Navigate to="/c/new" replace={true} />,
},
{
path: 'c/:conversationId?',
element: <ChatRoute />,
},
{
path: 'search',
element: <Search />,
},
{
path: 'agents',
element: <AgentMarketplace />,
},
{
path: 'agents/:category',
element: <AgentMarketplace />,
},
],
},
],
},
],
{ basename: baseHref },
);