Skip to content

Commit 93e7865

Browse files
Merge branch 'main' into brendan/permission-sync-banner-improvements
2 parents b96e4bd + 808dc94 commit 93e7865

File tree

12 files changed

+169
-76
lines changed

12 files changed

+169
-76
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
- Added `wa_user_created` PostHog event fired on successful user sign-up. [#933](https://github.com/sourcebot-dev/sourcebot/pull/933)
1515
- Added `wa_askgh_login_wall_prompted` PostHog event fired when an unauthenticated user attempts to ask a question on Ask GitHub. [#933](https://github.com/sourcebot-dev/sourcebot/pull/933)
1616
- Added Bitbucket Server (Data Center) OAuth 2.0 SSO identity provider support (`provider: "bitbucket-server"`). [#934](https://github.com/sourcebot-dev/sourcebot/pull/934)
17+
- Added login wall when anonymous users try to send messages on duplicated chats (askgh experiment). [#939](https://github.com/sourcebot-dev/sourcebot/pull/939)
1718
- Added `GET /api/ee/user` endpoint that returns the authenticated owner's user info (name, email, createdAt, updatedAt). [#940](https://github.com/sourcebot-dev/sourcebot/pull/940)
1819
- Added `selectedReposCount` to the `wa_chat_message_sent` PostHog event to track the number of selected repositories when users ask questions. [#941](https://github.com/sourcebot-dev/sourcebot/pull/941)
1920

packages/web/src/app/[domain]/askgh/[owner]/[repo]/components/landingPage.tsx

Lines changed: 10 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,19 @@ import { SearchModeSelector } from "@/app/[domain]/components/searchModeSelector
55
import { Separator } from "@/components/ui/separator";
66
import { ChatBox } from "@/features/chat/components/chatBox";
77
import { ChatBoxToolbar } from "@/features/chat/components/chatBox/chatBoxToolbar";
8-
import { LoginModal } from "./loginModal";
8+
import { LoginModal } from "@/app/components/loginModal";
99
import { NotConfiguredErrorBanner } from "@/features/chat/components/notConfiguredErrorBanner";
1010
import { LanguageModelInfo, RepoSearchScope } from "@/features/chat/types";
1111
import { useCreateNewChatThread } from "@/features/chat/useCreateNewChatThread";
1212
import { getRepoImageSrc } from '@/lib/utils';
13-
import type { IdentityProviderMetadata } from "@/lib/identityProviders";
14-
import { Descendant, Transforms } from "slate";
15-
import { useSlate } from "slate-react";
16-
import { useCallback, useEffect, useMemo, useState, useRef } from "react";
17-
import { captureEvent } from "@/hooks/useCaptureEvent";
18-
19-
const PENDING_MESSAGE_KEY = "askgh_pending_message";
13+
import { useMemo, useState } from "react";
2014

2115
interface LandingPageProps {
2216
languageModels: LanguageModelInfo[];
2317
repoName: string;
2418
repoDisplayName?: string;
2519
imageUrl?: string | null;
2620
repoId: number;
27-
providers: IdentityProviderMetadata[];
2821
isAuthenticated: boolean;
2922
}
3023

@@ -34,14 +27,10 @@ export const LandingPage = ({
3427
repoDisplayName,
3528
imageUrl,
3629
repoId,
37-
providers,
3830
isAuthenticated,
3931
}: LandingPageProps) => {
40-
const editor = useSlate();
41-
const { createNewChatThread, isLoading } = useCreateNewChatThread();
32+
const { createNewChatThread, isLoading, loginWall } = useCreateNewChatThread({ isAuthenticated });
4233
const [isContextSelectorOpen, setIsContextSelectorOpen] = useState(false);
43-
const [isLoginModalOpen, setIsLoginModalOpen] = useState(false);
44-
const hasRestoredPendingMessage = useRef(false);
4534
const isChatBoxDisabled = languageModels.length === 0;
4635

4736
const selectedSearchScopes = useMemo(() => [
@@ -53,45 +42,6 @@ export const LandingPage = ({
5342
} satisfies RepoSearchScope,
5443
], [repoDisplayName, repoName]);
5544

56-
// Intercept submit to check auth status
57-
const handleSubmit = useCallback((children: Descendant[]) => {
58-
if (!isAuthenticated) {
59-
captureEvent('wa_askgh_login_wall_prompted', {});
60-
// Store message in sessionStorage to survive OAuth redirect
61-
sessionStorage.setItem(PENDING_MESSAGE_KEY, JSON.stringify(children));
62-
setIsLoginModalOpen(true);
63-
return;
64-
}
65-
createNewChatThread(children, selectedSearchScopes);
66-
}, [isAuthenticated, createNewChatThread, selectedSearchScopes]);
67-
68-
// Restore pending message to editor and auto-submit after login
69-
useEffect(() => {
70-
if (isAuthenticated && !hasRestoredPendingMessage.current) {
71-
const stored = sessionStorage.getItem(PENDING_MESSAGE_KEY);
72-
if (stored) {
73-
hasRestoredPendingMessage.current = true;
74-
sessionStorage.removeItem(PENDING_MESSAGE_KEY);
75-
try {
76-
const message = JSON.parse(stored) as Descendant[];
77-
78-
// Restore the message content to the editor by replacing all nodes
79-
// Remove all existing nodes
80-
while (editor.children.length > 0) {
81-
Transforms.removeNodes(editor, { at: [0] });
82-
}
83-
// Insert the restored content at the beginning
84-
Transforms.insertNodes(editor, message, { at: [0] });
85-
86-
// Allow the UI to render the restored text before auto-submitting
87-
createNewChatThread(message, selectedSearchScopes);
88-
} catch (error) {
89-
console.error('Failed to restore pending message:', error);
90-
}
91-
}
92-
}
93-
}, [isAuthenticated, editor, createNewChatThread, selectedSearchScopes]);
94-
9545
const imageSrc = imageUrl ? getRepoImageSrc(imageUrl, repoId) : undefined;
9646
const displayName = repoDisplayName ?? repoName;
9747

@@ -119,7 +69,9 @@ export const LandingPage = ({
11969
<div className="w-full max-w-[800px]">
12070
<div className="border rounded-md w-full shadow-sm">
12171
<ChatBox
122-
onSubmit={handleSubmit}
72+
onSubmit={(children) => {
73+
createNewChatThread(children, selectedSearchScopes);
74+
}}
12375
className="min-h-[50px]"
12476
isRedirecting={isLoading}
12577
languageModels={languageModels}
@@ -155,11 +107,11 @@ export const LandingPage = ({
155107
</div>
156108

157109
<LoginModal
158-
isOpen={isLoginModalOpen}
159-
onOpenChange={setIsLoginModalOpen}
160-
providers={providers}
110+
isOpen={loginWall.isOpen}
111+
onOpenChange={loginWall.onOpenChange}
112+
providers={loginWall.providers}
161113
callbackUrl={typeof window !== 'undefined' ? window.location.href : ''}
162114
/>
163115
</div>
164116
)
165-
}
117+
}

packages/web/src/app/[domain]/askgh/[owner]/[repo]/page.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { CustomSlateEditor } from "@/features/chat/customSlateEditor";
88
import { RepoIndexedGuard } from "./components/repoIndexedGuard";
99
import { LandingPage } from "./components/landingPage";
1010
import { getConfiguredLanguageModelsInfo } from "@/features/chat/actions";
11-
import { getIdentityProviderMetadata } from "@/lib/identityProviders";
1211
import { auth } from "@/auth";
1312

1413
interface PageProps {
@@ -48,7 +47,6 @@ export default async function GitHubRepoPage(props: PageProps) {
4847

4948
const repoInfo = await unwrapServiceError(getRepoInfo(repoId));
5049
const languageModels = await unwrapServiceError(getConfiguredLanguageModelsInfo());
51-
const providers = getIdentityProviderMetadata();
5250

5351
return (
5452
<RepoIndexedGuard initialRepoInfo={repoInfo}>
@@ -59,7 +57,6 @@ export default async function GitHubRepoPage(props: PageProps) {
5957
repoDisplayName={repoInfo.displayName ?? undefined}
6058
imageUrl={repoInfo.imageUrl ?? undefined}
6159
repoId={repoInfo.id}
62-
providers={providers}
6360
isAuthenticated={!!session?.user}
6461
/>
6562
</CustomSlateEditor>

packages/web/src/app/[domain]/chat/[id]/page.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import { ChatVisibility } from '@sourcebot/db';
1818
import { Metadata } from 'next';
1919
import { SBChatMessage } from '@/features/chat/types';
2020
import { env, hasEntitlement } from '@sourcebot/shared';
21-
2221
import { captureEvent } from '@/lib/posthog';
2322

2423
interface PageProps {

packages/web/src/app/[domain]/chat/components/landingPageChatBox.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,22 @@ import { useState } from "react";
1010
import { useLocalStorage } from "usehooks-ts";
1111
import { SearchModeSelector } from "../../components/searchModeSelector";
1212
import { NotConfiguredErrorBanner } from "@/features/chat/components/notConfiguredErrorBanner";
13+
import { LoginModal } from "@/app/components/loginModal";
1314

1415
interface LandingPageChatBox {
1516
languageModels: LanguageModelInfo[];
1617
repos: RepositoryQuery[];
1718
searchContexts: SearchContextQuery[];
19+
isAuthenticated: boolean;
1820
}
1921

2022
export const LandingPageChatBox = ({
2123
languageModels,
2224
repos,
2325
searchContexts,
26+
isAuthenticated,
2427
}: LandingPageChatBox) => {
25-
const { createNewChatThread, isLoading } = useCreateNewChatThread();
28+
const { createNewChatThread, isLoading, loginWall } = useCreateNewChatThread({ isAuthenticated });
2629
const [selectedSearchScopes, setSelectedSearchScopes] = useLocalStorage<SearchScope[]>("selectedSearchScopes", [], { initializeWithValue: false });
2730
const [isContextSelectorOpen, setIsContextSelectorOpen] = useState(false);
2831
const isChatBoxDisabled = languageModels.length === 0;
@@ -65,6 +68,13 @@ export const LandingPageChatBox = ({
6568
{isChatBoxDisabled && (
6669
<NotConfiguredErrorBanner className="mt-4" />
6770
)}
71+
72+
<LoginModal
73+
isOpen={loginWall.isOpen}
74+
onOpenChange={loginWall.onOpenChange}
75+
providers={loginWall.providers}
76+
callbackUrl={typeof window !== 'undefined' ? window.location.href : ''}
77+
/>
6878
</div >
6979
)
70-
}
80+
}

packages/web/src/app/[domain]/chat/page.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ export default async function Page(props: PageProps) {
103103
languageModels={languageModels}
104104
repos={allRepos}
105105
searchContexts={searchContexts}
106+
isAuthenticated={!!session}
106107
/>
107108
</CustomSlateEditor>
108109

packages/web/src/app/[domain]/askgh/[owner]/[repo]/components/loginModal.tsx renamed to packages/web/src/app/components/loginModal.tsx

File renamed without changes.

packages/web/src/features/chat/actions.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -981,6 +981,16 @@ export const _getAISDKLanguageModelAndOptions = async (config: LanguageModel): P
981981

982982
}
983983

984+
export const getAskGhLoginWallData = async () => sew(async () => {
985+
const isEnabled = env.EXPERIMENT_ASK_GH_ENABLED === 'true';
986+
if (!isEnabled) {
987+
return { isEnabled: false as const, providers: [] };
988+
}
989+
990+
const { getIdentityProviderMetadata } = await import('@/lib/identityProviders');
991+
return { isEnabled: true as const, providers: getIdentityProviderMetadata() };
992+
});
993+
984994
const extractLanguageModelKeyValuePairs = async (
985995
pairs: {
986996
[k: string]: string | Token;

packages/web/src/features/chat/components/chatThread/chatThread.tsx

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,17 @@ import { NotConfiguredErrorBanner } from '../notConfiguredErrorBanner';
2828
import useCaptureEvent from '@/hooks/useCaptureEvent';
2929
import { SignInPromptBanner } from './signInPromptBanner';
3030
import { DuplicateChatDialog } from '@/app/[domain]/chat/components/duplicateChatDialog';
31+
import { LoginModal } from '@/app/components/loginModal';
32+
import type { IdentityProviderMetadata } from '@/lib/identityProviders';
33+
import { getAskGhLoginWallData } from '../../actions';
3134
import { useParams } from 'next/navigation';
3235

3336
type ChatHistoryState = {
3437
scrollOffset?: number;
3538
}
3639

40+
const PENDING_MESSAGE_STORAGE_KEY = "askgh_chat_pending_message";
41+
3742
interface ChatThreadProps {
3843
id?: string | undefined;
3944
initialMessages?: SBChatMessage[];
@@ -71,6 +76,9 @@ export const ChatThread = ({
7176
const params = useParams<{ domain: string }>();
7277
const [isContextSelectorOpen, setIsContextSelectorOpen] = useState(false);
7378
const [isDuplicateDialogOpen, setIsDuplicateDialogOpen] = useState(false);
79+
const [isLoginModalOpen, setIsLoginModalOpen] = useState(false);
80+
const [loginWallProviders, setLoginWallProviders] = useState<IdentityProviderMetadata[]>([]);
81+
const hasRestoredPendingMessage = useRef(false);
7482
const captureEvent = useCaptureEvent();
7583

7684
// Initial state is from attachments that exist in in the chat history.
@@ -200,6 +208,38 @@ export const ChatThread = ({
200208
hasSubmittedInputMessage.current = true;
201209
}, [inputMessage, sendMessage]);
202210

211+
// Restore pending message after OAuth redirect (askgh login wall)
212+
useEffect(() => {
213+
if (!isAuthenticated || !isOwner || hasRestoredPendingMessage.current) {
214+
return;
215+
}
216+
217+
const stored = sessionStorage.getItem(PENDING_MESSAGE_STORAGE_KEY);
218+
if (!stored) {
219+
return;
220+
}
221+
222+
hasRestoredPendingMessage.current = true;
223+
sessionStorage.removeItem(PENDING_MESSAGE_STORAGE_KEY);
224+
225+
try {
226+
const { chatId: storedChatId, children } = JSON.parse(stored) as { chatId: string; children: Descendant[] };
227+
228+
// Only restore if we're on the same chat that stored the pending message
229+
if (storedChatId !== chatId) {
230+
return;
231+
}
232+
233+
const text = slateContentToString(children);
234+
const mentions = getAllMentionElements(children);
235+
const message = createUIMessage(text, mentions.map(({ data }) => data), selectedSearchScopes);
236+
sendMessage(message);
237+
setIsAutoScrollEnabled(true);
238+
} catch (error) {
239+
console.error('Failed to restore pending message:', error);
240+
}
241+
}, [isAuthenticated, isOwner, chatId, sendMessage, selectedSearchScopes]);
242+
203243
// Track scroll position changes.
204244
useEffect(() => {
205245
const scrollElement = scrollAreaRef.current?.querySelector('[data-radix-scroll-area-viewport]') as HTMLElement;
@@ -287,7 +327,18 @@ export const ChatThread = ({
287327
}
288328
}, [error]);
289329

290-
const onSubmit = useCallback((children: Descendant[], editor: CustomEditor) => {
330+
const onSubmit = useCallback(async (children: Descendant[], editor: CustomEditor) => {
331+
if (!isAuthenticated) {
332+
const result = await getAskGhLoginWallData();
333+
if (!isServiceError(result) && result.isEnabled) {
334+
captureEvent('wa_askgh_login_wall_prompted', {});
335+
sessionStorage.setItem(PENDING_MESSAGE_STORAGE_KEY, JSON.stringify({ chatId, children }));
336+
setLoginWallProviders(result.providers);
337+
setIsLoginModalOpen(true);
338+
return;
339+
}
340+
}
341+
291342
const text = slateContentToString(children);
292343
const mentions = getAllMentionElements(children);
293344

@@ -297,7 +348,7 @@ export const ChatThread = ({
297348
setIsAutoScrollEnabled(true);
298349

299350
resetEditor(editor);
300-
}, [sendMessage, selectedSearchScopes]);
351+
}, [sendMessage, selectedSearchScopes, isAuthenticated, captureEvent, chatId]);
301352

302353
const onDuplicate = useCallback(async (newName: string): Promise<string | null> => {
303354
if (!defaultChatId) {
@@ -449,6 +500,13 @@ export const ChatThread = ({
449500
</div>
450501
)}
451502
</div>
503+
504+
<LoginModal
505+
isOpen={isLoginModalOpen}
506+
onOpenChange={setIsLoginModalOpen}
507+
providers={loginWallProviders}
508+
callbackUrl={typeof window !== 'undefined' ? window.location.href : ''}
509+
/>
452510
</>
453511
);
454512
}

0 commit comments

Comments
 (0)