Merge pull request #1186 from buster-so/big-nate-bus-1959-public-share-url-for-a-chat-doesnt-include-chat-id

Big nate bus 1959 public share url for a chat doesnt include chat
This commit is contained in:
Nate Kelley 2025-09-26 13:39:37 -06:00 committed by GitHub
commit 20a602b006
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 1127 additions and 102 deletions

View File

@ -12,6 +12,7 @@ import type { BusterChatMessage } from '@/api/asset_interfaces/chat';
import type { IBusterChat } from '@/api/asset_interfaces/chat/iChatInterfaces';
import { chatQueryKeys } from '@/api/query_keys/chat';
import { collectionQueryKeys } from '@/api/query_keys/collection';
import { silenceAssetErrors } from '@/api/response-helpers/silenece-asset-errors';
import { useBusterNotifications } from '@/context/BusterNotifications';
import { useMemoizedFn } from '@/hooks/useMemoizedFn';
import { updateChatToIChat } from '@/lib/chat';
@ -146,6 +147,7 @@ export const prefetchGetChat = async (
await queryClient.prefetchQuery({
...query,
queryFn: () => getChatQueryFn(params, queryClient),
retry: silenceAssetErrors,
});
}
return existingData || queryClient.getQueryData(query.queryKey);

View File

@ -30,7 +30,6 @@ export const getListLogs = async (params?: GetLogsListRequest): Promise<GetLogsL
.then((res) => res.data);
};
// Client-side fetch version
export const getChat = async ({ id }: GetChatRequest): Promise<GetChatResponse> => {
return mainApi.get<GetChatResponse>(`${CHATS_BASE}/${id}`).then((res) => res.data);
};

View File

@ -14,6 +14,10 @@ export const silenceAssetErrors = (_count: number, error: RustApiError): boolean
return false;
}
if (error.status === 412) {
return false;
}
openErrorNotification(error);
return false;

View File

@ -44,8 +44,12 @@ export const ShareMenuContent: React.FC<{
},
});
} else if (assetType === 'collection') {
console.warn('collection is actually not supported for embeds...', assetId);
url = buildLocation({
to: '/auth/login',
to: '/app/chats/$chatId',
params: {
chatId: assetId,
},
});
} else if (assetType === 'report_file') {
url = buildLocation({
@ -56,7 +60,10 @@ export const ShareMenuContent: React.FC<{
});
} else if (assetType === 'chat') {
url = buildLocation({
to: '/auth/login',
to: '/embed/chat/$chatId',
params: {
chatId: assetId,
},
});
} else {
const _exhaustiveCheck: never = assetType;

View File

@ -201,7 +201,7 @@ export const ShareMenuContentPublish: React.FC<ShareMenuContentBodyProps> = Reac
);
ShareMenuContentPublish.displayName = 'ShareMenuContentPublish';
const IsPublishedInfo: React.FC<{ isPublished: boolean }> = React.memo(({ isPublished }) => {
const IsPublishedInfo: React.FC<{ isPublished: boolean }> = ({ isPublished }) => {
if (!isPublished) return null;
return (
@ -210,8 +210,7 @@ const IsPublishedInfo: React.FC<{ isPublished: boolean }> = React.memo(({ isPubl
<Text variant="link">Live on the web</Text>
</div>
);
});
IsPublishedInfo.displayName = 'IsPublishedInfo';
};
const LinkExpiration: React.FC<{
linkExpiry: Date | null;

View File

@ -0,0 +1,101 @@
import type { AssetType } from '@buster/server-shared/assets';
import type { QueryClient } from '@tanstack/react-query';
import { Outlet, useRouteContext } from '@tanstack/react-router';
import omit from 'lodash/omit';
import { prefetchGetChat } from '@/api/buster_rest/chats';
import { getAppLayout } from '@/api/server-functions/getAppLayout';
import { ErrorCard } from '@/components/features/global/GlobalErrorCard';
import {
chooseInitialLayout,
getDefaultLayout,
getDefaultLayoutMode,
} from '@/context/Chats/selected-mode-helpers';
import { ChatLayout } from '@/layouts/ChatLayout/ChatLayout';
export const beforeLoad = async ({
params,
context,
}: {
params: { chatId: string };
context: { assetType: AssetType };
}) => {
const chatId = params.chatId;
const assetParams = omit(params, 'chatId');
const assetType = context.assetType;
const autoSaveId = `chat-${chatId ? 'C' : 'NXC'}-${assetType ? 'Y' : 'NXA'}`;
const selectedLayout = getDefaultLayoutMode({
chatId,
assetParams,
});
const defaultLayout = getDefaultLayout({
layout: selectedLayout,
});
const chatLayout = await getAppLayout({ id: autoSaveId });
const initialLayout = chooseInitialLayout({
layout: selectedLayout,
initialLayout: chatLayout,
defaultLayout,
});
return {
autoSaveId,
initialLayout,
defaultLayout,
selectedLayout,
};
};
export const loader = async ({
params,
context,
}: {
params: { chatId: string };
context: { queryClient: QueryClient };
}) => {
const chatId = params.chatId;
const [chat] = await Promise.all([prefetchGetChat({ id: chatId }, context.queryClient)]);
const title = chat?.title;
return {
title,
};
};
export const head = ({ loaderData }: { loaderData?: { title: string | undefined } } = {}) => ({
meta: [
{ title: loaderData?.title || 'Chat' },
{ name: 'description', content: 'View and interact with your chat conversation' },
{ name: 'og:title', content: 'Chat' },
{ name: 'og:description', content: 'View and interact with your chat conversation' },
],
});
export const staticData = {
assetType: 'chat' as Extract<AssetType, 'chat'>,
};
export const component = () => {
const { initialLayout, selectedLayout, autoSaveId, defaultLayout } = useRouteContext({
strict: false,
});
if (!initialLayout || !selectedLayout || !autoSaveId || !defaultLayout) {
return (
<ErrorCard
header="Hmmm... Something went wrong."
message="An error occurred while loading the chat."
/>
);
}
return (
<ChatLayout
initialLayout={initialLayout}
autoSaveId={autoSaveId}
defaultLayout={defaultLayout}
selectedLayout={selectedLayout}
>
<Outlet />
</ChatLayout>
);
};

View File

@ -16,7 +16,10 @@ export const getAssetIdAndVersionNumber = (
dashboard_version_number?: number;
report_version_number?: number;
}
) => {
): {
assetId: string;
versionNumber: number | undefined;
} => {
if (assetType === 'chat') {
return { assetId: params.chatId ?? '', versionNumber: undefined };
}

View File

@ -20,7 +20,8 @@ interface AssetAccess {
const getAssetAccess = (
error: RustApiError | null,
isFetched: boolean,
selectedQuery: QueryKey
selectedQuery: QueryKey,
hasData: boolean
): AssetAccess => {
if (error) {
console.error('Error in getAssetAccess', error, isFetched, selectedQuery);
@ -59,7 +60,7 @@ const getAssetAccess = (
};
}
if (typeof error?.status === 'number') {
if (typeof error?.status === 'number' || !hasData) {
return {
hasAccess: false,
passwordRequired: false,
@ -85,34 +86,45 @@ export const useGetAssetPasswordConfig = (
) => {
const chosenVersionNumber = versionNumber || 'LATEST';
const selectedQuery = useMemo(() => {
if (type === 'metric_file') {
return metricsQueryKeys.metricsGetMetric(assetId, chosenVersionNumber);
}
if (type === 'dashboard_file') {
return dashboardQueryKeys.dashboardGetDashboard(assetId, chosenVersionNumber);
}
if (type === 'report_file') {
return reportsQueryKeys.reportsGetReport(assetId, chosenVersionNumber);
}
if (type === 'collection') {
return collectionQueryKeys.collectionsGetCollection(assetId);
}
if (type === 'reasoning') {
return chatQueryKeys.chatsGetChat(assetId);
}
const selectedQuery = useMemo(
() => getSelectedQuery(type, assetId, chosenVersionNumber),
[type, assetId, chosenVersionNumber]
);
const _exhaustiveCheck: 'chat' = type;
return chatQueryKeys.chatsGetChat(assetId);
}, [type, assetId, chosenVersionNumber]);
const { error, isFetched, data } = useQuery({
const { error, isFetched, data, ...rest } = useQuery({
queryKey: selectedQuery.queryKey,
enabled: true,
select: useCallback((v: unknown) => !!v, []),
notifyOnChangeProps: ['error', 'isFetched', 'data'],
retry: false,
initialData: false,
});
return getAssetAccess(error, isFetched, selectedQuery.queryKey);
return getAssetAccess(error, isFetched, selectedQuery.queryKey, !!data);
};
const getSelectedQuery = (
type: AssetType | ResponseMessageFileType,
assetId: string,
chosenVersionNumber: number | 'LATEST'
) => {
if (type === 'metric_file') {
return metricsQueryKeys.metricsGetMetric(assetId, chosenVersionNumber);
}
if (type === 'dashboard_file') {
return dashboardQueryKeys.dashboardGetDashboard(assetId, chosenVersionNumber);
}
if (type === 'report_file') {
return reportsQueryKeys.reportsGetReport(assetId, chosenVersionNumber);
}
if (type === 'collection') {
return collectionQueryKeys.collectionsGetCollection(assetId);
}
if (type === 'reasoning') {
return chatQueryKeys.chatsGetChat(assetId);
}
const _exhaustiveCheck: 'chat' = type;
return chatQueryKeys.chatsGetChat(assetId);
};

File diff suppressed because it is too large Load Diff

View File

@ -1,73 +1,6 @@
import type { AssetType } from '@buster/server-shared/assets';
import { createFileRoute, Outlet } from '@tanstack/react-router';
import omit from 'lodash/omit';
import { prefetchGetChat } from '@/api/buster_rest/chats';
import {
chooseInitialLayout,
getDefaultLayout,
getDefaultLayoutMode,
} from '@/context/Chats/selected-mode-helpers';
import { ChatLayout } from '@/layouts/ChatLayout';
import { createFileRoute } from '@tanstack/react-router';
import * as chatLayoutServerContext from '@/context/BusterAssets/chat-server/chatLayoutServer';
export const Route = createFileRoute('/app/_app/_asset/chats/$chatId')({
beforeLoad: async ({ params, context }) => {
const chatId = params.chatId;
const assetParams = omit(params, 'chatId');
const assetType = context.assetType;
const autoSaveId = `chat-${chatId ? 'C' : 'NXC'}-${assetType ? 'Y' : 'NXA'}`;
const selectedLayout = getDefaultLayoutMode({
chatId,
assetParams,
});
const defaultLayout = getDefaultLayout({
layout: selectedLayout,
});
const chatLayout = await context.getAppLayout({ id: autoSaveId });
const initialLayout = chooseInitialLayout({
layout: selectedLayout,
initialLayout: chatLayout,
defaultLayout,
});
return {
autoSaveId,
initialLayout,
defaultLayout,
selectedLayout,
};
},
loader: async ({ params, context }) => {
const chatId = params.chatId;
const [chat] = await Promise.all([prefetchGetChat({ id: chatId }, context.queryClient)]);
const title = chat?.title;
return {
title,
};
},
head: ({ loaderData }) => ({
meta: [
{ title: loaderData?.title || 'Chat' },
{ name: 'description', content: 'View and interact with your chat conversation' },
{ name: 'og:title', content: 'Chat' },
{ name: 'og:description', content: 'View and interact with your chat conversation' },
],
}),
component: () => {
const { initialLayout, selectedLayout, autoSaveId, defaultLayout } = Route.useRouteContext();
return (
<ChatLayout
initialLayout={initialLayout}
autoSaveId={autoSaveId}
defaultLayout={defaultLayout}
selectedLayout={selectedLayout}
>
<Outlet />
</ChatLayout>
);
},
staticData: {
assetType: 'chat' as Extract<AssetType, 'chat'>,
},
...chatLayoutServerContext,
});

View File

@ -1,6 +1,7 @@
import type { AssetType } from '@buster/server-shared/assets';
import { createFileRoute, Outlet, type RouteContext } from '@tanstack/react-router';
import { prefetchGetMyUserInfo } from '@/api/buster_rest/users';
import { Text } from '@/components/ui/typography';
import { getSupabaseSession } from '@/integrations/supabase/getSupabaseUserClient';
import { signInWithAnonymousUser } from '@/integrations/supabase/signIn';
import { AppAssetCheckLayout } from '@/layouts/AppAssetCheckLayout';
@ -35,6 +36,16 @@ function RouteComponent() {
);
}
if (assetType === 'chat') {
return (
<div className="flex h-full w-full items-center justify-center">
<Text className="text-lg">
Sharing a chat is not supported yet... But it is on our roadmap!
</Text>
</div>
);
}
return (
<main className="h-full w-full bg-page-background overflow-y-auto">
<AppAssetCheckLayout assetType={assetType}>

View File

@ -0,0 +1,10 @@
import { createFileRoute } from '@tanstack/react-router';
import * as chatLayoutServerContext from '@/context/BusterAssets/chat-server/chatLayoutServer';
export const Route = createFileRoute('/embed/chat/$chatId')({
...chatLayoutServerContext,
ssr: false,
component: () => {
return <div>Hello "/embed/chat/$chatId"!</div>;
},
});

View File

@ -0,0 +1,6 @@
import { createFileRoute } from '@tanstack/react-router';
import * as dashboardLayoutServerAssetContext from '@/context/BusterAssets/dashboard-server/dashboardLayoutServerAssetContext';
export const Route = createFileRoute('/embed/chats/$chatId/dashboards/$dashboardId/_layout')({
...dashboardLayoutServerAssetContext,
});

View File

@ -0,0 +1,6 @@
import { createFileRoute } from '@tanstack/react-router';
import * as dashboardContentServerContext from '@/context/BusterAssets/dashboard-server/dashboardContentContext';
export const Route = createFileRoute('/embed/chats/$chatId/dashboards/$dashboardId/_layout/')({
...dashboardContentServerContext,
});

View File

@ -0,0 +1,9 @@
import { createFileRoute } from '@tanstack/react-router';
import * as metricLayoutServerContext from '@/context/BusterAssets/metric-server/metricLayoutServerAssetContext';
export const Route = createFileRoute(
'/embed/chats/$chatId/dashboards/$dashboardId/metrics/$metricId/_content'
)({
...metricLayoutServerContext,
loader: metricLayoutServerContext.loader<{ metricId: string; dashboardId: string }>,
});

View File

@ -0,0 +1,8 @@
import { createFileRoute } from '@tanstack/react-router';
import * as metricChartServerAssetContext from '@/context/BusterAssets/metric-server/metricChartServerAssetContext';
export const Route = createFileRoute(
'/embed/chats/$chatId/dashboards/$dashboardId/metrics/$metricId/_content/chart'
)({
...metricChartServerAssetContext,
});

View File

@ -0,0 +1,8 @@
import { createFileRoute } from '@tanstack/react-router';
import * as metricIndexServerContext from '@/context/BusterAssets/metric-server/metricIndexServerAssetContext';
export const Route = createFileRoute(
'/embed/chats/$chatId/dashboards/$dashboardId/metrics/$metricId/_content/'
)({
...metricIndexServerContext,
});

View File

@ -0,0 +1,8 @@
import { createFileRoute } from '@tanstack/react-router';
import * as metricResultsServerAssetContext from '@/context/BusterAssets/metric-server/metricResultsServerAssetContext';
export const Route = createFileRoute(
'/embed/chats/$chatId/dashboards/$dashboardId/metrics/$metricId/_content/results'
)({
...metricResultsServerAssetContext,
});

View File

@ -0,0 +1,8 @@
import { createFileRoute } from '@tanstack/react-router';
import * as metricSQLServerAsssetContext from '@/context/BusterAssets/metric-server/metricSQLServerAsssetContext';
export const Route = createFileRoute(
'/embed/chats/$chatId/dashboards/$dashboardId/metrics/$metricId/_content/sql'
)({
...metricSQLServerAsssetContext,
});

View File

@ -0,0 +1,9 @@
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/embed/chats/$chatId/')({
component: RouteComponent,
});
function RouteComponent() {
return null;
}

View File

@ -0,0 +1,7 @@
import { createFileRoute } from '@tanstack/react-router';
import * as metricLayoutServerContext from '@/context/BusterAssets/metric-server/metricLayoutServerAssetContext';
export const Route = createFileRoute('/embed/chats/$chatId/metrics/$metricId/_layout')({
...metricLayoutServerContext,
loader: metricLayoutServerContext.loader<{ metricId: string }>,
});

View File

@ -0,0 +1,6 @@
import { createFileRoute } from '@tanstack/react-router';
import * as metricChartServerAssetContext from '@/context/BusterAssets/metric-server/metricChartServerAssetContext';
export const Route = createFileRoute('/embed/chats/$chatId/metrics/$metricId/_layout/chart')({
...metricChartServerAssetContext,
});

View File

@ -0,0 +1,6 @@
import { createFileRoute } from '@tanstack/react-router';
import * as metricIndexServerContext from '@/context/BusterAssets/metric-server/metricIndexServerAssetContext';
export const Route = createFileRoute('/embed/chats/$chatId/metrics/$metricId/_layout/')({
...metricIndexServerContext,
});

View File

@ -0,0 +1,6 @@
import { createFileRoute } from '@tanstack/react-router';
import * as metricResultsServerAssetContext from '@/context/BusterAssets/metric-server/metricResultsServerAssetContext';
export const Route = createFileRoute('/embed/chats/$chatId/metrics/$metricId/_layout/results')({
...metricResultsServerAssetContext,
});

View File

@ -0,0 +1,6 @@
import { createFileRoute } from '@tanstack/react-router';
import * as metricSQLServerAsssetContext from '@/context/BusterAssets/metric-server/metricSQLServerAsssetContext';
export const Route = createFileRoute('/embed/chats/$chatId/metrics/$metricId/_layout/sql')({
...metricSQLServerAsssetContext,
});

View File

@ -0,0 +1,35 @@
import { createFileRoute } from '@tanstack/react-router';
import { ClosePageButton } from '@/components/features/chat/ClosePageButton';
import { AppSegmented } from '@/components/ui/segmented';
import { ReasoningController } from '@/controllers/ReasoningController/ReasoningController';
import { AssetContainer } from '@/layouts/AssetContainer/AssetContainer';
export const Route = createFileRoute('/embed/chats/$chatId/reasoning/$messageId/')({
component: RouteComponent,
});
function RouteComponent() {
const { chatId, messageId } = Route.useParams();
return (
<AssetContainer header={<ReasoningControllerHeader />} headerBorderVariant="ghost" scrollable>
<ReasoningController chatId={chatId} messageId={messageId} />
</AssetContainer>
);
}
const ReasoningControllerHeader: React.FC = () => {
return (
<div className="w-full flex items-center justify-between">
<AppSegmented
type="button"
options={[
{
value: 'reasoning',
label: 'Reasoning',
},
]}
/>
<ClosePageButton />
</div>
);
};

View File

@ -0,0 +1,6 @@
import { createFileRoute } from '@tanstack/react-router';
import * as reportLayoutServerAssetContext from '@/context/BusterAssets/report-server/reportLayoutServerAssetContext';
export const Route = createFileRoute('/embed/chats/$chatId/reports/$reportId/_layout')({
...reportLayoutServerAssetContext,
});

View File

@ -0,0 +1,7 @@
import { createFileRoute } from '@tanstack/react-router';
import * as reportContentServerContext from '@/context/BusterAssets/report-server/reportContentServerAssetContext';
export const Route = createFileRoute('/embed/chats/$chatId/reports/$reportId/_layout/content')({
...reportContentServerContext,
});

View File

@ -0,0 +1,6 @@
import { createFileRoute } from '@tanstack/react-router';
import * as reportIndexServerAssetContext from '@/context/BusterAssets/report-server/reportContentServerAssetContext';
export const Route = createFileRoute('/embed/chats/$chatId/reports/$reportId/_layout/')({
...reportIndexServerAssetContext,
});

View File

@ -0,0 +1,9 @@
import { createFileRoute } from '@tanstack/react-router';
import * as metricLayoutServerContext from '@/context/BusterAssets/metric-server/metricLayoutServerAssetContext';
export const Route = createFileRoute(
'/embed/chats/$chatId/reports/$reportId/metrics/$metricId/_content'
)({
...metricLayoutServerContext,
loader: metricLayoutServerContext.loader<{ metricId: string }>,
});

View File

@ -0,0 +1,8 @@
import { createFileRoute } from '@tanstack/react-router';
import * as metricChartServerAssetContext from '@/context/BusterAssets/metric-server/metricChartServerAssetContext';
export const Route = createFileRoute(
'/embed/chats/$chatId/reports/$reportId/metrics/$metricId/_content/chart'
)({
...metricChartServerAssetContext,
});

View File

@ -0,0 +1,8 @@
import { createFileRoute } from '@tanstack/react-router';
import * as metricIndexServerContext from '@/context/BusterAssets/metric-server/metricIndexServerAssetContext';
export const Route = createFileRoute(
'/embed/chats/$chatId/reports/$reportId/metrics/$metricId/_content/'
)({
...metricIndexServerContext,
});

View File

@ -0,0 +1,8 @@
import { createFileRoute } from '@tanstack/react-router';
import * as metricResultsServerAssetContext from '@/context/BusterAssets/metric-server/metricResultsServerAssetContext';
export const Route = createFileRoute(
'/embed/chats/$chatId/reports/$reportId/metrics/$metricId/_content/results'
)({
...metricResultsServerAssetContext,
});

View File

@ -0,0 +1,8 @@
import { createFileRoute } from '@tanstack/react-router';
import * as metricSQLServerAsssetContext from '@/context/BusterAssets/metric-server/metricSQLServerAsssetContext';
export const Route = createFileRoute(
'/embed/chats/$chatId/reports/$reportId/metrics/$metricId/_content/sql'
)({
...metricSQLServerAsssetContext,
});