logged out chat modal, call to action

This commit is contained in:
2026-04-22 21:05:53 +02:00
parent 52e0a65113
commit 64bd5c429e
7 changed files with 75 additions and 45 deletions

0
.codex Normal file
View File

View File

@@ -5,15 +5,9 @@ import ChatInterface from '~/app/chat/_components/ChatInterface'
import { useMessages } from '~/app/_providers/MessagesProvider'; import { useMessages } from '~/app/_providers/MessagesProvider';
import { Spinner } from '~/components/ui/spinner'; import { Spinner } from '~/components/ui/spinner';
type DBMessage = {
id: string
role: 'user' | 'assistant'
content: string
}
export default function ChatModal() { export default function ChatModal() {
const router = useRouter() const router = useRouter()
const {messages,session,clearChat,clearingChat,isLoading,error,refetchMessages} = useMessages() const {messages,session,isLoading,error} = useMessages()
return ( return (
<Dialog modal={true} open onOpenChange={() => router.back()}> <Dialog modal={true} open onOpenChange={() => router.back()}>
<DialogContent className="w-full max-w-full rounded-none sm:max-w-full h-[100svh] lg:max-w-3xl lg:rounded-xl lg:h-[80vh] flex flex-col p-0 gap-0"> <DialogContent className="w-full max-w-full rounded-none sm:max-w-full h-[100svh] lg:max-w-3xl lg:rounded-xl lg:h-[80vh] flex flex-col p-0 gap-0">
@@ -21,8 +15,8 @@ export default function ChatModal() {
<DialogTitle>Talk To My AI-Assistant</DialogTitle> <DialogTitle>Talk To My AI-Assistant</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="flex-1 overflow-hidden min-h-0"> <div className="flex-1 overflow-hidden min-h-0">
{messages && session?.id && {!isLoading &&
<ChatInterface sessionId={session.id} dbMessages={messages}/> <ChatInterface sessionId={session?.id} dbMessages={messages ?? []}/>
} }
{isLoading && {isLoading &&
<><Spinner/> Loading Messages...</> <><Spinner/> Loading Messages...</>

View File

@@ -1,16 +1,8 @@
'use client' 'use client'
import { Skeleton } from '~/components/ui/skeleton';
import ChatModal from './_components/ChatModal' import ChatModal from './_components/ChatModal'
import { trpc } from '~/app/_trpc/Client'
import { useTimeLine } from '~/app/_providers/GsapProvicer';
export default function AssistantModalPage() { export default function AssistantModalPage() {
const { data: session, error, isLoading } = trpc.chat.getSession.useQuery();
return ( return (
<>
<ChatModal/> <ChatModal/>
{error && <div>{error.message}</div>}
{isLoading && <Skeleton />}
</>
) )
} }

View File

@@ -1,7 +1,6 @@
'use client' 'use client'
import Link from 'next/link' import Link from 'next/link'
import { MessageCircle } from 'lucide-react' import { MessageCircle } from 'lucide-react'
import { Show } from '@clerk/nextjs'
import { Button } from '~/components/ui/button' import { Button } from '~/components/ui/button'
import { usePathname } from 'next/navigation' import { usePathname } from 'next/navigation'
export default function ChatFAB() { export default function ChatFAB() {
@@ -10,7 +9,6 @@ export default function ChatFAB() {
return ( return (
<> <>
{!isChat && {!isChat &&
<Show when="signed-in">
<div className="fixed bottom-6 right-6 z-50"> <div className="fixed bottom-6 right-6 z-50">
<Button asChild size="icon" className="h-14 w-14 rounded-full shadow-lg"> <Button asChild size="icon" className="h-14 w-14 rounded-full shadow-lg">
<Link href="/assistant"> <Link href="/assistant">
@@ -18,7 +16,6 @@ export default function ChatFAB() {
</Link> </Link>
</Button> </Button>
</div> </div>
</Show>
} }
</> </>
) )

View File

@@ -1,5 +1,6 @@
'use client' 'use client'
import type { inferRouterOutputs } from '@trpc/server'; import type { inferRouterOutputs } from '@trpc/server';
import { useUser } from '@clerk/nextjs'
import { createContext, useContext, useEffect, useState, type ReactNode } from 'react' import { createContext, useContext, useEffect, useState, type ReactNode } from 'react'
import { trpc } from '~/app/_trpc/Client' import { trpc } from '~/app/_trpc/Client'
import { type ChatRouter } from '~/server/routers/chat' import { type ChatRouter } from '~/server/routers/chat'
@@ -26,15 +27,29 @@ export const useMessages = () => useContext(MessageContext)
export const MessagesProvider = ({children}:{children:ReactNode}) => { export const MessagesProvider = ({children}:{children:ReactNode}) => {
const [error,setError] = useState<string|null>(null) const [error,setError] = useState<string|null>(null)
const [isLoading,setIsLoading] = useState<boolean>(true) const [isLoading,setIsLoading] = useState<boolean>(true)
const { data: session,error:sessionError,isLoading:sessionLoading} = trpc.chat.getSession.useQuery() const { isLoaded, isSignedIn } = useUser()
const { data: messages, refetch, error:messageError, isLoading:messagesLoading } = trpc.chat.getMessages.useQuery(session?.id ? session.id : "") const { data: session,error:sessionError,isLoading:sessionLoading} = trpc.chat.getSession.useQuery(undefined, {
enabled: isSignedIn === true,
})
const { data: messages, refetch, error:messageError, isLoading:messagesLoading } = trpc.chat.getMessages.useQuery(session?.id ? session.id : "", {
enabled: isSignedIn === true && session?.id != undefined,
})
const { mutate ,isPending:clearingChat,isSuccess:clearedChat } = trpc.chat.clearChat.useMutation() const { mutate ,isPending:clearingChat,isSuccess:clearedChat } = trpc.chat.clearChat.useMutation()
const utils = trpc.useUtils() const utils = trpc.useUtils()
const refetchMessages = () => { const refetchMessages = () => {
if (!isSignedIn) {
return;
}
utils.chat.getMessages.invalidate() utils.chat.getMessages.invalidate()
refetch() refetch()
} }
const clearChat = (callback?: () => void) => { const clearChat = (callback?: () => void) => {
if (!isSignedIn) {
if (callback) {
callback()
}
return;
}
mutate(undefined,{onSuccess: () => { mutate(undefined,{onSuccess: () => {
if (callback) { if (callback) {
callback() callback()
@@ -43,18 +58,29 @@ export const MessagesProvider = ({children}:{children:ReactNode}) => {
}}) }})
} }
useEffect(() => { useEffect(() => {
if (isSignedIn !== true) {
setError(null)
return;
}
messageError && setError(messageError.message) messageError && setError(messageError.message)
sessionError && setError(sessionError.message) sessionError && setError(sessionError.message)
},[messageError,sessionError]) },[messageError,sessionError,isSignedIn])
useEffect(() => { useEffect(() => {
!sessionLoading && !messagesLoading && setIsLoading(false) if (!isLoaded) {
sessionLoading || messagesLoading && setIsLoading(true) setIsLoading(true)
},[sessionLoading,messagesLoading]) return;
}
if (isSignedIn !== true) {
setIsLoading(false)
return;
}
setIsLoading(sessionLoading || messagesLoading)
},[isLoaded,isSignedIn,sessionLoading,messagesLoading])
return ( return (
<MessageContext.Provider value={ <MessageContext.Provider value={
{ {
session, session: isSignedIn === true ? session : undefined,
messages, messages: isSignedIn === true ? messages : undefined,
refetchMessages, refetchMessages,
clearChat, clearChat,
error, error,

View File

@@ -4,6 +4,7 @@ import { useChat } from '@ai-sdk/react'
import { DefaultChatTransport, type UIMessage } from 'ai' import { DefaultChatTransport, type UIMessage } from 'ai'
import { Button } from '~/components/ui/button' import { Button } from '~/components/ui/button'
import { Textarea } from '~/components/ui/textarea' import { Textarea } from '~/components/ui/textarea'
import { SignInButton } from '@clerk/nextjs'
import { import {
useGsapContext, useGsapContext,
} from '~/app/_providers/GsapProvicer'; } from '~/app/_providers/GsapProvicer';
@@ -18,10 +19,26 @@ interface DBMessage {
} }
interface ChatInterfaceProps { interface ChatInterfaceProps {
sessionId: string, sessionId?: string,
dbMessages: DBMessage[], dbMessages: DBMessage[],
} }
function SignInChatPrompt() {
return (
<div className="flex h-full w-full flex-col items-center justify-center gap-4 text-center">
<div className="space-y-2">
<h2 className="text-xl font-semibold">Sign in to use the chat</h2>
<p className="text-sm text-muted-foreground">
You need to be signed in before you can talk to Gregor's AI assistant.
</p>
</div>
<SignInButton mode="modal">
<Button type="button">Sign in</Button>
</SignInButton>
</div>
)
}
function toUIMessages(dbMessages: DBMessage[]): UIMessage[] { function toUIMessages(dbMessages: DBMessage[]): UIMessage[] {
return dbMessages.map((m) => ({ return dbMessages.map((m) => ({
id: m.id, id: m.id,
@@ -43,7 +60,7 @@ function addInitMessage(messageArray: UIMessage[]) {
} }
} }
export default function ChatInterface({ dbMessages, sessionId }: ChatInterfaceProps) { function AuthenticatedChatInterface({ dbMessages, sessionId }: ChatInterfaceProps & { sessionId: string }) {
const [input, setInput] = useState('') const [input, setInput] = useState('')
const { clearingChat, clearChat, refetchMessages } = useMessages(); const { clearingChat, clearChat, refetchMessages } = useMessages();
const initialMessages = toUIMessages(dbMessages) const initialMessages = toUIMessages(dbMessages)
@@ -115,7 +132,7 @@ export default function ChatInterface({ dbMessages, sessionId }: ChatInterfacePr
<div className='flex flex-col gap-2'> <div className='flex flex-col gap-2'>
<Button <Button
onClick={handleSend} onClick={handleSend}
disabled={status != "ready" || !input.trim() || sessionId == undefined} disabled={status != "ready" || !input.trim()}
> >
Send Send
</Button> </Button>
@@ -140,3 +157,10 @@ export default function ChatInterface({ dbMessages, sessionId }: ChatInterfacePr
</div> </div>
) )
} }
export default function ChatInterface({ dbMessages, sessionId }: ChatInterfaceProps) {
if (sessionId == undefined) {
return <SignInChatPrompt />
}
return <AuthenticatedChatInterface sessionId={sessionId} dbMessages={dbMessages} />
}

View File

@@ -1,15 +1,12 @@
'use client' 'use client'
import ChatInterface from './_components/ChatInterface' import ChatInterface from './_components/ChatInterface'
import { trpc } from '../_trpc/Client';
import { Skeleton } from '~/components/ui/skeleton';
import AnimatedPageTitle from '../_components/Animated/AnimatedPageTitle'; import AnimatedPageTitle from '../_components/Animated/AnimatedPageTitle';
import { useTimeLine } from '../_providers/GsapProvicer'; import { useTimeLine } from '../_providers/GsapProvicer';
import { useMessages } from '../_providers/MessagesProvider'; import { useMessages } from '../_providers/MessagesProvider';
import { Spinner } from '~/components/ui/spinner'; import { Spinner } from '~/components/ui/spinner';
import { useEffect } from 'react';
export default function ChatPage() { export default function ChatPage() {
const {messages,session,clearChat,clearingChat,isLoading,error,refetchMessages} = useMessages() const {messages,session,isLoading,error} = useMessages()
useTimeLine(messages) useTimeLine(messages)
return ( return (
<div className="flex flex-col px-10 lg:px-0 w-full h-full max-w-4xl mx-auto pt-10"> <div className="flex flex-col px-10 lg:px-0 w-full h-full max-w-4xl mx-auto pt-10">
@@ -17,8 +14,8 @@ export default function ChatPage() {
<span>Talk To My </span> <span> AI-Assistant</span> <span>Talk To My </span> <span> AI-Assistant</span>
</AnimatedPageTitle> </AnimatedPageTitle>
<div className='flex items-center h-[80%] w-full my-auto w-full'> <div className='flex items-center h-[80%] w-full my-auto w-full'>
{messages && session?.id && {!isLoading &&
<ChatInterface sessionId={session.id} dbMessages={messages}/> <ChatInterface sessionId={session?.id} dbMessages={messages ?? []}/>
} }
{isLoading && {isLoading &&
<><Spinner/> Loading Messages...</> <><Spinner/> Loading Messages...</>