Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,26 @@ import { SearchModeSelector } from "@/app/[domain]/components/searchModeSelector
import { Separator } from "@/components/ui/separator";
import { ChatBox } from "@/features/chat/components/chatBox";
import { ChatBoxToolbar } from "@/features/chat/components/chatBox/chatBoxToolbar";
import { LoginModal } from "./loginModal";
import { NotConfiguredErrorBanner } from "@/features/chat/components/notConfiguredErrorBanner";
import { LanguageModelInfo, RepoSearchScope } from "@/features/chat/types";
import { useCreateNewChatThread } from "@/features/chat/useCreateNewChatThread";
import { getRepoImageSrc } from '@/lib/utils';
import { useMemo, useState } from "react";
import type { IdentityProviderMetadata } from "@/lib/identityProviders";
import { Descendant, Transforms } from "slate";
import { useSlate } from "slate-react";
import { useCallback, useEffect, useMemo, useState, useRef } from "react";

const PENDING_MESSAGE_KEY = "askgh_pending_message";

interface LandingPageProps {
languageModels: LanguageModelInfo[];
repoName: string;
repoDisplayName?: string;
imageUrl?: string | null;
repoId: number;
providers: IdentityProviderMetadata[];
isAuthenticated: boolean;
}

export const LandingPage = ({
Expand All @@ -25,9 +33,14 @@ export const LandingPage = ({
repoDisplayName,
imageUrl,
repoId,
providers,
isAuthenticated,
}: LandingPageProps) => {
const editor = useSlate();
const { createNewChatThread, isLoading } = useCreateNewChatThread();
const [isContextSelectorOpen, setIsContextSelectorOpen] = useState(false);
const [isLoginModalOpen, setIsLoginModalOpen] = useState(false);
const hasRestoredPendingMessage = useRef(false);
const isChatBoxDisabled = languageModels.length === 0;

const selectedSearchScopes = useMemo(() => [
Expand All @@ -39,6 +52,44 @@ export const LandingPage = ({
} satisfies RepoSearchScope,
], [repoDisplayName, repoName]);

// Intercept submit to check auth status
const handleSubmit = useCallback((children: Descendant[]) => {
if (!isAuthenticated) {
// Store message in sessionStorage to survive OAuth redirect
sessionStorage.setItem(PENDING_MESSAGE_KEY, JSON.stringify(children));
setIsLoginModalOpen(true);
return;
}
createNewChatThread(children, selectedSearchScopes);
}, [isAuthenticated, createNewChatThread, selectedSearchScopes]);

// Restore pending message to editor and auto-submit after login
useEffect(() => {
if (isAuthenticated && !hasRestoredPendingMessage.current) {
const stored = sessionStorage.getItem(PENDING_MESSAGE_KEY);
if (stored) {
hasRestoredPendingMessage.current = true;
sessionStorage.removeItem(PENDING_MESSAGE_KEY);
try {
const message = JSON.parse(stored) as Descendant[];

// Restore the message content to the editor by replacing all nodes
// Remove all existing nodes
while (editor.children.length > 0) {
Transforms.removeNodes(editor, { at: [0] });
}
// Insert the restored content at the beginning
Transforms.insertNodes(editor, message, { at: [0] });

// Allow the UI to render the restored text before auto-submitting
createNewChatThread(message, selectedSearchScopes);
} catch (error) {
console.error('Failed to restore pending message:', error);
}
}
}
}, [isAuthenticated, editor, createNewChatThread, selectedSearchScopes]);

const imageSrc = imageUrl ? getRepoImageSrc(imageUrl, repoId) : undefined;
const displayName = repoDisplayName ?? repoName;

Expand Down Expand Up @@ -66,9 +117,7 @@ export const LandingPage = ({
<div className="w-full max-w-[800px]">
<div className="border rounded-md w-full shadow-sm">
<ChatBox
onSubmit={(children) => {
createNewChatThread(children, selectedSearchScopes);
}}
onSubmit={handleSubmit}
className="min-h-[50px]"
isRedirecting={isLoading}
languageModels={languageModels}
Expand All @@ -85,7 +134,7 @@ export const LandingPage = ({
repos={[]}
searchContexts={[]}
selectedSearchScopes={selectedSearchScopes}
onSelectedSearchScopesChange={() => {}}
onSelectedSearchScopesChange={() => { }}
isContextSelectorOpen={isContextSelectorOpen}
onContextSelectorOpenChanged={setIsContextSelectorOpen}
/>
Expand All @@ -102,6 +151,13 @@ export const LandingPage = ({
)}
</div>
</div>

<LoginModal
isOpen={isLoginModalOpen}
onOpenChange={setIsLoginModalOpen}
providers={providers}
callbackUrl={typeof window !== 'undefined' ? window.location.href : ''}
/>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
'use client';

import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { AuthMethodSelector } from "@/app/components/authMethodSelector";
import type { IdentityProviderMetadata } from "@/lib/identityProviders";

interface LoginModalProps {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
providers: IdentityProviderMetadata[];
callbackUrl: string;
}

export const LoginModal = ({
isOpen,
onOpenChange,
providers,
callbackUrl,
}: LoginModalProps) => {
return (
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="mb-3">Sign up to continue</DialogTitle>
<DialogDescription>
Sign into your account to continue.
</DialogDescription>
</DialogHeader>
<div className="mt-4">
<AuthMethodSelector
providers={providers}
callbackUrl={callbackUrl}
context="login"
securityNoticeClosable={true}
/>
</div>
</DialogContent>
</Dialog>
);
};
8 changes: 7 additions & 1 deletion packages/web/src/app/[domain]/askgh/[owner]/[repo]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { CustomSlateEditor } from "@/features/chat/customSlateEditor";
import { RepoIndexedGuard } from "./components/repoIndexedGuard";
import { LandingPage } from "./components/landingPage";
import { getConfiguredLanguageModelsInfo } from "@/features/chat/actions";
import { getIdentityProviderMetadata } from "@/lib/identityProviders";
import { auth } from "@/auth";

interface PageProps {
params: Promise<{ owner: string; repo: string }>;
Expand All @@ -16,7 +18,8 @@ interface PageProps {
export default async function GitHubRepoPage(props: PageProps) {
const params = await props.params;
const { owner, repo } = params;

const session = await auth();

const repoId = await (async () => {
// 1. Look up repo by owner/repo
const displayName = `${owner}/${repo}`;
Expand Down Expand Up @@ -45,6 +48,7 @@ export default async function GitHubRepoPage(props: PageProps) {

const repoInfo = await unwrapServiceError(getRepoInfo(repoId));
const languageModels = await unwrapServiceError(getConfiguredLanguageModelsInfo());
const providers = getIdentityProviderMetadata();

return (
<RepoIndexedGuard initialRepoInfo={repoInfo}>
Expand All @@ -55,6 +59,8 @@ export default async function GitHubRepoPage(props: PageProps) {
repoDisplayName={repoInfo.displayName ?? undefined}
imageUrl={repoInfo.imageUrl ?? undefined}
repoId={repoInfo.id}
providers={providers}
isAuthenticated={!!session?.user}
/>
</CustomSlateEditor>
</RepoIndexedGuard>
Expand Down