Compare commits
15 Commits
18abc4b3f7
...
aiassistan
| Author | SHA1 | Date | |
|---|---|---|---|
| 64bd5c429e | |||
| 52e0a65113 | |||
| 30e3dbb42b | |||
| caa9604704 | |||
| c62ee37538 | |||
| ead9548744 | |||
| c5b3ee3875 | |||
| 2b5c105abb | |||
| e25fc39bac | |||
| e481fa66cd | |||
| 009d2b8d60 | |||
| d567fa3e02 | |||
| 399d78e508 | |||
| bfc2bb1501 | |||
| d7a9e53d9a |
@@ -2,29 +2,28 @@
|
|||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '~/components/ui/dialog'
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '~/components/ui/dialog'
|
||||||
import ChatInterface from '~/app/chat/_components/ChatInterface'
|
import ChatInterface from '~/app/chat/_components/ChatInterface'
|
||||||
|
import { useMessages } from '~/app/_providers/MessagesProvider';
|
||||||
|
import { Spinner } from '~/components/ui/spinner';
|
||||||
|
|
||||||
type DBMessage = {
|
export default function ChatModal() {
|
||||||
id: string
|
|
||||||
role: 'user' | 'assistant'
|
|
||||||
content: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ChatModalProps {
|
|
||||||
sessionId: string
|
|
||||||
initialMessages: DBMessage[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ChatModal({ sessionId, initialMessages }: ChatModalProps) {
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
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">
|
||||||
<DialogHeader className="p-4 border-b shrink-0">
|
<DialogHeader className="p-4 border-b shrink-0">
|
||||||
<DialogTitle>AI Recruiter</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">
|
||||||
<ChatInterface sessionId={sessionId} initialMessages={initialMessages} />
|
{!isLoading &&
|
||||||
|
<ChatInterface sessionId={session?.id} dbMessages={messages ?? []}/>
|
||||||
|
}
|
||||||
|
{isLoading &&
|
||||||
|
<><Spinner/> Loading Messages...</>
|
||||||
|
}
|
||||||
|
{error &&
|
||||||
|
<div> {error} </div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
8
src/app/@modal/(.)assistant/page.tsx
Normal file
8
src/app/@modal/(.)assistant/page.tsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
'use client'
|
||||||
|
import ChatModal from './_components/ChatModal'
|
||||||
|
|
||||||
|
export default function AssistantModalPage() {
|
||||||
|
return (
|
||||||
|
<ChatModal/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
'use client'
|
|
||||||
import { Skeleton } from '~/components/ui/skeleton';
|
|
||||||
import ChatModal from './_components/ChatModal'
|
|
||||||
import { trpc } from '~/app/_trpc/Client'
|
|
||||||
|
|
||||||
export default function ChatModalPage() {
|
|
||||||
const { data: session, error, isLoading } = trpc.chat.getSession.useQuery();
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{session && <ChatModal sessionId={session.id} initialMessages={session.messages} />}
|
|
||||||
{error && <div>{error.message}</div>}
|
|
||||||
{isLoading && <Skeleton />}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -7,28 +7,54 @@ import { cn } from "~/lib/utils";
|
|||||||
const AnimateTextIn = ({
|
const AnimateTextIn = ({
|
||||||
children,
|
children,
|
||||||
animation = "type",
|
animation = "type",
|
||||||
position,
|
position = 0,
|
||||||
|
tlId = undefined,
|
||||||
|
speed = 1,
|
||||||
|
scrollOnly = false,
|
||||||
className
|
className
|
||||||
}: {
|
}: {
|
||||||
children: ReactNode,
|
children: ReactNode,
|
||||||
animation?: "type" | "slide",
|
animation?: "type" | "slide",
|
||||||
position: gsap.Position,
|
position?: gsap.Position,
|
||||||
|
tlId?: string,
|
||||||
|
scrollOnly?: boolean,
|
||||||
|
speed?: number,
|
||||||
className?: HTMLAttributes<HTMLDivElement>['className']
|
className?: HTMLAttributes<HTMLDivElement>['className']
|
||||||
}) => {
|
}) => {
|
||||||
const el = useRef<HTMLDivElement>(null)
|
const el = useRef<HTMLDivElement>(null)
|
||||||
const gsapContext = useGsapContext();
|
const gsapContext = useGsapContext();
|
||||||
useGSAP(() => {
|
useGSAP(() => {
|
||||||
const rect = el.current?.getBoundingClientRect()
|
const rect = el.current?.getBoundingClientRect()
|
||||||
const isInView = rect && rect.top < window.innerHeight
|
const scroller = gsapContext?.getScroller()
|
||||||
|
console.log(scroller)
|
||||||
|
let viewportTop = 0
|
||||||
|
let viewportBottom = window.innerHeight
|
||||||
|
if (scroller && scroller instanceof Element) {
|
||||||
|
const scrollerRect = scroller.getBoundingClientRect()
|
||||||
|
viewportTop = scrollerRect.top
|
||||||
|
viewportBottom = scrollerRect.top + scrollerRect.height
|
||||||
|
}
|
||||||
|
const isInView = rect && rect.bottom > viewportTop && rect.top < viewportBottom
|
||||||
|
console.log(isInView)
|
||||||
const chars = new SplitText(el.current, { type: 'chars' })
|
const chars = new SplitText(el.current, { type: 'chars' })
|
||||||
gsapContext?.addAnimation(gsap.to(el.current, { opacity: 100, duration: 0 }), 0)
|
gsapContext?.addAnimation(gsap.to(el.current, { opacity: 100, duration: 0 }), 0, tlId)
|
||||||
const fromVars = animation === "slide"
|
const fromVars = animation === "slide"
|
||||||
? { opacity: 0, x: -10, duration: 0.2, stagger: { each: 0.08 }, ease: 'bounce.inOut', onComplete: () => chars.revert() }
|
? { opacity: 0, x: -10, duration: 0.2 * speed, stagger: { each: 0.08 * speed }, ease: 'bounce.inOut', onComplete: () => chars.revert() }
|
||||||
: { opacity: 0, duration: 0.01, stagger: { each: 0.04 }, ease: 'bounce.inOut', onComplete: () => chars.revert() }
|
: { opacity: 0, duration: 0.01 * speed, stagger: { each: 0.04 * speed }, ease: 'bounce.inOut', onComplete: () => chars.revert() }
|
||||||
if (isInView) {
|
if (isInView && !scrollOnly) {
|
||||||
gsapContext?.addAnimation(gsap.from(chars.chars, fromVars), position)
|
gsapContext?.addAnimation(gsap.from(chars.chars, fromVars), position, tlId)
|
||||||
} else {
|
} else {
|
||||||
gsap.from(chars.chars, { ...fromVars, scrollTrigger: { trigger: el.current, start: 'top 85%', scroller: gsapContext?.getScroller() } })
|
gsap.from(chars.chars,
|
||||||
|
{
|
||||||
|
...fromVars,
|
||||||
|
scrollTrigger: {
|
||||||
|
trigger: el.current,
|
||||||
|
start: 'top bottom',
|
||||||
|
end: 'bottom top',
|
||||||
|
toggleActions: "play reverse play reverse",
|
||||||
|
scroller
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}, { dependencies: [] })
|
}, { dependencies: [] })
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,19 +1,22 @@
|
|||||||
'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'
|
||||||
export default function ChatFAB() {
|
export default function ChatFAB() {
|
||||||
|
const pathName = usePathname()
|
||||||
|
const isChat = pathName.indexOf('\/chat') > -1
|
||||||
return (
|
return (
|
||||||
<Show when="signed-in">
|
<>
|
||||||
|
{!isChat &&
|
||||||
<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="/chat">
|
<Link href="/assistant">
|
||||||
<MessageCircle className="h-6 w-6" />
|
<MessageCircle className="h-6 w-6" />
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export default function TopNav() {
|
|||||||
</Button>
|
</Button>
|
||||||
<Show when="signed-in">
|
<Show when="signed-in">
|
||||||
<Button asChild className="flex h-10 lg:h-full w-full lg:w-20" variant="outline">
|
<Button asChild className="flex h-10 lg:h-full w-full lg:w-20" variant="outline">
|
||||||
<a href="/chat"> Chat </a>
|
<Link href="/chat"> Chat </Link>
|
||||||
</Button>
|
</Button>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useGSAP } from '@gsap/react'
|
|||||||
import gsap from 'gsap'
|
import gsap from 'gsap'
|
||||||
import { SplitText } from 'gsap/SplitText'
|
import { SplitText } from 'gsap/SplitText'
|
||||||
import { ScrollTrigger, GSDevTools } from 'gsap/all'
|
import { ScrollTrigger, GSDevTools } from 'gsap/all'
|
||||||
import { createContext, useCallback, useContext, useEffect, useRef, type ReactNode } from 'react'
|
import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useRef, type ReactNode } from 'react'
|
||||||
|
|
||||||
gsap.registerPlugin(useGSAP)
|
gsap.registerPlugin(useGSAP)
|
||||||
gsap.registerPlugin(ScrollTrigger)
|
gsap.registerPlugin(ScrollTrigger)
|
||||||
@@ -38,7 +38,7 @@ export const useTimeLine = (dep:any,all?:boolean) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},[dep])
|
},[dep])
|
||||||
useEffect(() => {
|
useLayoutEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
gsapContext?.resetTimeline()
|
gsapContext?.resetTimeline()
|
||||||
}
|
}
|
||||||
@@ -49,18 +49,30 @@ export default function GsapProvider({ children }: { children: ReactNode }) {
|
|||||||
const tl = useRef<gsap.core.Timeline | null>(null)
|
const tl = useRef<gsap.core.Timeline | null>(null)
|
||||||
const scrollerRef = useRef<Element | Window | null>(null)
|
const scrollerRef = useRef<Element | Window | null>(null)
|
||||||
const getScroller = useCallback(() => {
|
const getScroller = useCallback(() => {
|
||||||
const cached = scrollerRef.current
|
// const cached = scrollerRef.current
|
||||||
if (!cached || (cached instanceof Element && !document.contains(cached))) {
|
// if (!cached || (cached instanceof Element && !document.contains(cached))) {
|
||||||
scrollerRef.current = document.querySelector('[data-slot="scroll-area-viewport"]') ?? window
|
let scrollers = document.querySelectorAll('[data-slot="scroll-area-viewport"]')
|
||||||
|
if (scrollers.length < 1) {
|
||||||
|
scrollerRef.current = window
|
||||||
|
} else {
|
||||||
|
let scrollerArray = Array.from(scrollers.values()).sort((a,b) => {
|
||||||
|
const s1 = a as HTMLDivElement;
|
||||||
|
const s2 = b as HTMLDivElement;
|
||||||
|
// using bitwise not (~~) to coerce NaN values to 0
|
||||||
|
const aPriority = ~~Number(s1.dataset?.scrollerPriority)
|
||||||
|
const bPriority = ~~Number(s2.dataset?.scrollerPriority)
|
||||||
|
return aPriority - bPriority;
|
||||||
|
})
|
||||||
|
let prioScroller = scrollerArray.pop();
|
||||||
|
scrollerRef.current = prioScroller || window;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// }
|
||||||
return scrollerRef.current
|
return scrollerRef.current
|
||||||
}, [])
|
}, [])
|
||||||
useGSAP(() => {
|
useGSAP(() => {
|
||||||
if (!tl.current) {
|
if (!tl.current) {
|
||||||
tl.current = gsap.timeline({ paused: true, id:'mainTimeline', onComplete: ()=>{
|
tl.current = gsap.timeline({ paused: true })
|
||||||
console.log('timeline revert')
|
|
||||||
gsap.getById('mainTimeline')?.revert()
|
|
||||||
} })
|
|
||||||
}
|
}
|
||||||
return () => { console.log("gsap cleanup") }
|
return () => { console.log("gsap cleanup") }
|
||||||
})
|
})
|
||||||
|
|||||||
95
src/app/_providers/MessagesProvider.tsx
Normal file
95
src/app/_providers/MessagesProvider.tsx
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
'use client'
|
||||||
|
import type { inferRouterOutputs } from '@trpc/server';
|
||||||
|
import { useUser } from '@clerk/nextjs'
|
||||||
|
import { createContext, useContext, useEffect, useState, type ReactNode } from 'react'
|
||||||
|
import { trpc } from '~/app/_trpc/Client'
|
||||||
|
import { type ChatRouter } from '~/server/routers/chat'
|
||||||
|
const MessageContext = createContext<{
|
||||||
|
session?: inferRouterOutputs<ChatRouter>['getSession']
|
||||||
|
messages?: inferRouterOutputs<ChatRouter>['getMessages']
|
||||||
|
refetchMessages: () => void
|
||||||
|
clearChat: (callback?: () => void) => void
|
||||||
|
error: string|null
|
||||||
|
isLoading: boolean
|
||||||
|
clearingChat: boolean
|
||||||
|
clearedChat: boolean
|
||||||
|
}>({
|
||||||
|
session: undefined,
|
||||||
|
messages: undefined,
|
||||||
|
refetchMessages: () => undefined,
|
||||||
|
clearChat: () => undefined,
|
||||||
|
error: null,
|
||||||
|
isLoading: true,
|
||||||
|
clearingChat: false,
|
||||||
|
clearedChat: false
|
||||||
|
})
|
||||||
|
export const useMessages = () => useContext(MessageContext)
|
||||||
|
export const MessagesProvider = ({children}:{children:ReactNode}) => {
|
||||||
|
const [error,setError] = useState<string|null>(null)
|
||||||
|
const [isLoading,setIsLoading] = useState<boolean>(true)
|
||||||
|
const { isLoaded, isSignedIn } = useUser()
|
||||||
|
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 utils = trpc.useUtils()
|
||||||
|
const refetchMessages = () => {
|
||||||
|
if (!isSignedIn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
utils.chat.getMessages.invalidate()
|
||||||
|
refetch()
|
||||||
|
}
|
||||||
|
const clearChat = (callback?: () => void) => {
|
||||||
|
if (!isSignedIn) {
|
||||||
|
if (callback) {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mutate(undefined,{onSuccess: () => {
|
||||||
|
if (callback) {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
utils.chat.getMessages.invalidate()
|
||||||
|
}})
|
||||||
|
}
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSignedIn !== true) {
|
||||||
|
setError(null)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
messageError && setError(messageError.message)
|
||||||
|
sessionError && setError(sessionError.message)
|
||||||
|
},[messageError,sessionError,isSignedIn])
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isLoaded) {
|
||||||
|
setIsLoading(true)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isSignedIn !== true) {
|
||||||
|
setIsLoading(false)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsLoading(sessionLoading || messagesLoading)
|
||||||
|
},[isLoaded,isSignedIn,sessionLoading,messagesLoading])
|
||||||
|
return (
|
||||||
|
<MessageContext.Provider value={
|
||||||
|
{
|
||||||
|
session: isSignedIn === true ? session : undefined,
|
||||||
|
messages: isSignedIn === true ? messages : undefined,
|
||||||
|
refetchMessages,
|
||||||
|
clearChat,
|
||||||
|
error,
|
||||||
|
isLoading,
|
||||||
|
clearingChat,
|
||||||
|
clearedChat
|
||||||
|
}
|
||||||
|
}>
|
||||||
|
{children}
|
||||||
|
</MessageContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
8
src/app/actions/currentTime.ts
Normal file
8
src/app/actions/currentTime.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export default function currentTime() {
|
||||||
|
let now = Date.now();
|
||||||
|
console.log(now);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
time: now
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
'use server'
|
'use server'
|
||||||
import { clerkClient } from '@clerk/nextjs/server'
|
import { clerkClient, auth } from '@clerk/nextjs/server'
|
||||||
import { google } from 'googleapis'
|
import { google } from 'googleapis'
|
||||||
import { env } from '~/env'
|
import { env } from '~/env'
|
||||||
|
|
||||||
@@ -10,7 +10,6 @@ export async function scheduleMeeting({
|
|||||||
durationMinutes,
|
durationMinutes,
|
||||||
attendeeEmail,
|
attendeeEmail,
|
||||||
attendeeName,
|
attendeeName,
|
||||||
userId,
|
|
||||||
}: {
|
}: {
|
||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
@@ -18,11 +17,11 @@ export async function scheduleMeeting({
|
|||||||
durationMinutes: number
|
durationMinutes: number
|
||||||
attendeeEmail?: string
|
attendeeEmail?: string
|
||||||
attendeeName?: string
|
attendeeName?: string
|
||||||
userId: string
|
|
||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
const clerk = await clerkClient()
|
const clerk = await clerkClient()
|
||||||
|
const userAuth = await auth()
|
||||||
|
const user = await clerk.users.getUser(userAuth.userId?userAuth.userId:"")
|
||||||
// Get admin's Google OAuth token to create the event on Gregor's calendar
|
// Get admin's Google OAuth token to create the event on Gregor's calendar
|
||||||
const adminTokenResponse = await clerk.users.getUserOauthAccessToken(
|
const adminTokenResponse = await clerk.users.getUserOauthAccessToken(
|
||||||
env.ADMIN_USER_CLERK_ID,
|
env.ADMIN_USER_CLERK_ID,
|
||||||
@@ -37,16 +36,7 @@ export async function scheduleMeeting({
|
|||||||
// Try to resolve visitor's Google email for the invite
|
// Try to resolve visitor's Google email for the invite
|
||||||
let visitorEmail: string | undefined = attendeeEmail
|
let visitorEmail: string | undefined = attendeeEmail
|
||||||
if (!visitorEmail) {
|
if (!visitorEmail) {
|
||||||
try {
|
visitorEmail = user?.emailAddresses.at(0)?.emailAddress ?? undefined
|
||||||
const visitorTokenResponse = await clerk.users.getUserOauthAccessToken(userId, 'oauth_google')
|
|
||||||
if (visitorTokenResponse.data[0]) {
|
|
||||||
const user = await clerk.users.getUser(userId)
|
|
||||||
const googleAccount = user.externalAccounts.find((a) => a.provider === 'google')
|
|
||||||
visitorEmail = googleAccount?.emailAddress ?? undefined
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Visitor not signed in with Google — no invite
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const oAuth2Client = new google.auth.OAuth2()
|
const oAuth2Client = new google.auth.OAuth2()
|
||||||
@@ -71,6 +61,7 @@ export async function scheduleMeeting({
|
|||||||
end: { dateTime: endTime.toISOString(), timeZone: 'UTC' },
|
end: { dateTime: endTime.toISOString(), timeZone: 'UTC' },
|
||||||
attendees,
|
attendees,
|
||||||
},
|
},
|
||||||
|
sendNotifications: true
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { auth } from '@clerk/nextjs/server'
|
import { auth } from '@clerk/nextjs/server'
|
||||||
import { createOpenAI } from '@ai-sdk/openai'
|
import { createOpenAI } from '@ai-sdk/openai'
|
||||||
import { streamText, tool, convertToModelMessages, stepCountIs, type UIMessage } from 'ai'
|
import { streamText, tool, convertToModelMessages, stepCountIs, type UIMessage } from 'ai'
|
||||||
import { z } from 'zod'
|
import { success, z } from 'zod'
|
||||||
import { eq, and } from 'drizzle-orm'
|
import { eq, and } from 'drizzle-orm'
|
||||||
import { env } from '~/env'
|
import { env } from '~/env'
|
||||||
import { db } from '~/server/db'
|
import { db } from '~/server/db'
|
||||||
import { chatSession, chatMessage } from '~/server/dbschema/schema'
|
import { chatSession, chatMessage } from '~/server/dbschema/schema'
|
||||||
import { servTrpc } from '~/app/_trpc/ServerClient'
|
import { servTrpc } from '~/app/_trpc/ServerClient'
|
||||||
import { scheduleMeeting } from '~/app/actions/scheduleMeeting'
|
import { scheduleMeeting } from '~/app/actions/scheduleMeeting'
|
||||||
|
import currentTime from '~/app/actions/currentTime';
|
||||||
|
|
||||||
const openai = createOpenAI({ apiKey: env.OPENAI_API_KEY })
|
const openai = createOpenAI({ apiKey: env.OPENAI_API_KEY })
|
||||||
|
|
||||||
@@ -45,15 +46,15 @@ export async function POST(req: Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = streamText({
|
const result = streamText({
|
||||||
model: openai('gpt-4o'),
|
model: openai('gpt-5-mini'),
|
||||||
system: systemPrompt,
|
system: systemPrompt,
|
||||||
messages: await convertToModelMessages(messages),
|
messages: await convertToModelMessages(messages),
|
||||||
tools: {
|
tools: {
|
||||||
scheduleMeeting: tool({
|
scheduleMeeting: tool({
|
||||||
description: 'Schedule a meeting with Gregor Lohaus and add it to his Google Calendar',
|
description: 'Schedule a meeting with Gregor Lohaus and add it to his Google Calendar',
|
||||||
inputSchema: z.object({
|
inputSchema: z.object({
|
||||||
title: z.string().describe('Meeting title'),
|
title: z.string().describe('Meeting title, make something up if not provided'),
|
||||||
description: z.string().describe('Meeting description / agenda'),
|
description: z.string().describe('Meeting description / agenda, make something up if not provided'),
|
||||||
dateTime: z
|
dateTime: z
|
||||||
.string()
|
.string()
|
||||||
.describe(
|
.describe(
|
||||||
@@ -64,16 +65,23 @@ export async function POST(req: Request) {
|
|||||||
.int()
|
.int()
|
||||||
.min(15)
|
.min(15)
|
||||||
.max(120)
|
.max(120)
|
||||||
.describe('Duration of the meeting in minutes'),
|
.describe('Duration of the meeting in minutes, if none provided ask if 20 minutes is ok'),
|
||||||
attendeeEmail: z
|
attendeeEmail: z
|
||||||
.string()
|
.string()
|
||||||
.email()
|
.email()
|
||||||
.optional()
|
.optional()
|
||||||
.describe('Email of the visitor to invite (if provided)'),
|
.describe('Optional Email of the visitor to invite (if provided)'),
|
||||||
attendeeName: z.string().optional().describe('Name of the visitor'),
|
attendeeName: z.string().optional().describe('Name of the visitor'),
|
||||||
}),
|
}),
|
||||||
execute: async (input) => scheduleMeeting({ ...input, userId }),
|
execute: async (input) => scheduleMeeting({ ...input }),
|
||||||
}),
|
}),
|
||||||
|
getCurrentUnixTime: tool({
|
||||||
|
description: 'Get the current unix time to reference for meeting dates',
|
||||||
|
inputSchema: z.object({
|
||||||
|
none: z.string().optional().describe("no inputs are needed")
|
||||||
|
}),
|
||||||
|
execute: async () => currentTime()
|
||||||
|
})
|
||||||
},
|
},
|
||||||
stopWhen: stepCountIs(5),
|
stopWhen: stepCountIs(5),
|
||||||
onFinish: async ({ text, finishReason }) => {
|
onFinish: async ({ text, finishReason }) => {
|
||||||
|
|||||||
5
src/app/assistant/page.tsx
Normal file
5
src/app/assistant/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { redirect } from 'next/navigation'
|
||||||
|
|
||||||
|
export default function AssistantPage() {
|
||||||
|
redirect('/chat')
|
||||||
|
}
|
||||||
@@ -1,23 +1,42 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { useRef, useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useChat } from '@ai-sdk/react'
|
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 { cn } from '~/lib/utils'
|
import { SignInButton } from '@clerk/nextjs'
|
||||||
import Markdown from 'react-markdown';
|
import {
|
||||||
import { AssistantMessage } from './AssistantMessage';
|
useGsapContext,
|
||||||
import { UserMessage } from './UserMessage';
|
} from '~/app/_providers/GsapProvicer';
|
||||||
|
import Messages from './Messages'
|
||||||
type DBMessage = {
|
import { DeleteIcon } from 'lucide-react';
|
||||||
|
import { Spinner } from '~/components/ui/spinner';
|
||||||
|
import { useMessages } from '~/app/_providers/MessagesProvider';
|
||||||
|
interface DBMessage {
|
||||||
id: string
|
id: string
|
||||||
role: 'user' | 'assistant'
|
role: 'user' | 'assistant'
|
||||||
content: string
|
content: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ChatInterfaceProps {
|
interface ChatInterfaceProps {
|
||||||
sessionId: string
|
sessionId?: string,
|
||||||
initialMessages: 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[] {
|
||||||
@@ -28,60 +47,55 @@ function toUIMessages(dbMessages: DBMessage[]): UIMessage[] {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ChatInterface({ sessionId, initialMessages }: ChatInterfaceProps) {
|
function addInitMessage(messageArray: UIMessage[]) {
|
||||||
const [input, setInput] = useState('')
|
if (messageArray.at(0)?.id != 'init') {
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null)
|
messageArray.unshift({
|
||||||
|
id: "init",
|
||||||
const { messages, sendMessage, status, error, clearError } = useChat({
|
role: 'assistant',
|
||||||
transport: new DefaultChatTransport({
|
parts: [{
|
||||||
api: '/api/chat',
|
type: 'text',
|
||||||
body: { sessionId },
|
text: "Hi im gregors ai assistant,you can ask me to provide general information or to schedule a meeting."
|
||||||
}),
|
}],
|
||||||
messages: toUIMessages(initialMessages),
|
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const isLoading = status === 'submitted' || status === 'streaming'
|
function AuthenticatedChatInterface({ dbMessages, sessionId }: ChatInterfaceProps & { sessionId: string }) {
|
||||||
const hasError = status === 'error'
|
const [input, setInput] = useState('')
|
||||||
|
const { clearingChat, clearChat, refetchMessages } = useMessages();
|
||||||
|
const initialMessages = toUIMessages(dbMessages)
|
||||||
|
addInitMessage(initialMessages)
|
||||||
|
const { messages, sendMessage, status, error, clearError, setMessages } = useChat({
|
||||||
|
transport: new DefaultChatTransport({
|
||||||
|
api: '/api/chat', body: { sessionId },
|
||||||
|
}),
|
||||||
|
messages: initialMessages,
|
||||||
|
})
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
return () => {
|
||||||
}, [messages])
|
refetchMessages()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
const handleSend = () => {
|
const handleSend = () => {
|
||||||
const text = input.trim()
|
const text = input.trim()
|
||||||
if (!text || isLoading) return
|
if (!text || status != 'ready' || clearingChat) return
|
||||||
setInput('')
|
setInput('')
|
||||||
sendMessage({ text })
|
sendMessage({ text })
|
||||||
}
|
}
|
||||||
|
const gsapContext = useGsapContext()
|
||||||
|
useEffect(() => {
|
||||||
|
let scroller = gsapContext?.getScroller()
|
||||||
|
if (scroller instanceof Window) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(scroller?.scrollHeight)
|
||||||
|
scroller?.scrollTo({ behavior: 'smooth', top: scroller.scrollHeight })
|
||||||
|
}, [messages])
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
{messages &&
|
||||||
{messages.length === 0 && (
|
<Messages status={status} messages={messages} />
|
||||||
<div className="text-center text-muted-foreground py-12">
|
}
|
||||||
<p className="text-base font-medium mb-1">Hi! I'm Gregor's AI recruiter assistant.</p>
|
|
||||||
<p className="text-sm">Ask me about his skills and experience, or schedule a meeting!</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{messages.map((message) => (
|
|
||||||
<>
|
|
||||||
{message.role == 'assistant' && <AssistantMessage message={message}/>}
|
|
||||||
{message.role == 'user' && <UserMessage message={message}/>}
|
|
||||||
</>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{isLoading && (
|
|
||||||
<div className="flex justify-start">
|
|
||||||
<div className="bg-muted rounded-lg px-4 py-2 text-sm">
|
|
||||||
<span className="animate-pulse">Thinking…</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div ref={messagesEndRef} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="mx-4 mb-2 flex items-start gap-2 rounded-lg border border-destructive/50 bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
<div className="mx-4 mb-2 flex items-start gap-2 rounded-lg border border-destructive/50 bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
||||||
<span className="flex-1">
|
<span className="flex-1">
|
||||||
@@ -89,19 +103,20 @@ export default function ChatInterface({ sessionId, initialMessages }: ChatInterf
|
|||||||
? 'OpenAI quota exceeded. Please try again later.'
|
? 'OpenAI quota exceeded. Please try again later.'
|
||||||
: `Error: ${error.message}`}
|
: `Error: ${error.message}`}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={clearError}
|
onClick={clearError}
|
||||||
className="shrink-0 opacity-60 hover:opacity-100"
|
className="shrink-0 opacity-60 hover:opacity-100"
|
||||||
aria-label="Dismiss"
|
variant='destructive'
|
||||||
>
|
>
|
||||||
✕
|
<DeleteIcon />
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="p-4 border-t flex gap-2">
|
<div className="p-4 border-t flex flex-row gap-2">
|
||||||
<Textarea
|
<Textarea
|
||||||
|
name='message'
|
||||||
value={input}
|
value={input}
|
||||||
onChange={(e) => setInput(e.target.value)}
|
onChange={(e) => setInput(e.target.value)}
|
||||||
placeholder="Ask about Gregor's experience or schedule a meeting…"
|
placeholder="Ask about Gregor's experience or schedule a meeting…"
|
||||||
@@ -114,14 +129,38 @@ export default function ChatInterface({ sessionId, initialMessages }: ChatInterf
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<div className='flex flex-col gap-2'>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSend}
|
onClick={handleSend}
|
||||||
disabled={isLoading || hasError || !input.trim()}
|
disabled={status != "ready" || !input.trim()}
|
||||||
className="self-end"
|
|
||||||
>
|
>
|
||||||
Send
|
Send
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant='destructive'
|
||||||
|
onClick={() => {
|
||||||
|
clearChat(() => {
|
||||||
|
let messages: UIMessage[] = [];
|
||||||
|
addInitMessage(messages);
|
||||||
|
setMessages(messages)
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
disabled={status != "ready" || clearingChat}
|
||||||
|
>
|
||||||
|
{clearingChat ?
|
||||||
|
<Spinner /> :
|
||||||
|
"Clear Chat"
|
||||||
|
}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default function ChatInterface({ dbMessages, sessionId }: ChatInterfaceProps) {
|
||||||
|
if (sessionId == undefined) {
|
||||||
|
return <SignInChatPrompt />
|
||||||
|
}
|
||||||
|
return <AuthenticatedChatInterface sessionId={sessionId} dbMessages={dbMessages} />
|
||||||
|
}
|
||||||
|
|||||||
36
src/app/chat/_components/Messages.tsx
Normal file
36
src/app/chat/_components/Messages.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { type ChatStatus, type UIMessage } from 'ai'
|
||||||
|
import * as Card from "~/components/ui/card"
|
||||||
|
import { UserMessage } from './UserMessage';
|
||||||
|
import { AssistantMessage } from './AssistantMessage';
|
||||||
|
import { ScrollArea } from '~/components/ui/scroll-area';
|
||||||
|
import { memo } from 'react';
|
||||||
|
const Messages = memo(({messages,status}: { messages: UIMessage[],status:ChatStatus}) => {
|
||||||
|
return (
|
||||||
|
<ScrollArea data-scroller-priority='1' className="w-full h-[90%] max-w-4xl mx-auto">
|
||||||
|
{messages.map((message, i) => (
|
||||||
|
<Card.AnimatedCard scrollOnly={true} key={i}>
|
||||||
|
<Card.CardContent>
|
||||||
|
{message.role == 'assistant' && <AssistantMessage message={message} />}
|
||||||
|
{message.role == 'user' && <UserMessage message={message} />}
|
||||||
|
</Card.CardContent>
|
||||||
|
</Card.AnimatedCard>
|
||||||
|
))}
|
||||||
|
{status == 'submitted' &&
|
||||||
|
<Card.AnimatedCard scrollOnly={true}>
|
||||||
|
<Card.CardContent>
|
||||||
|
<AssistantMessage message={{
|
||||||
|
id:"",
|
||||||
|
role:"assistant",
|
||||||
|
parts:[{
|
||||||
|
type:'text',
|
||||||
|
text:'Thinking ...'
|
||||||
|
}]
|
||||||
|
}}/>
|
||||||
|
</Card.CardContent>
|
||||||
|
</Card.AnimatedCard>
|
||||||
|
|
||||||
|
}
|
||||||
|
</ScrollArea>)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Messages;
|
||||||
@@ -1,26 +1,28 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import ChatInterface from './_components/ChatInterface'
|
import ChatInterface from './_components/ChatInterface'
|
||||||
import { trpc } from '../_trpc/Client';
|
import AnimatedPageTitle from '../_components/Animated/AnimatedPageTitle';
|
||||||
import { Skeleton } from '~/components/ui/skeleton';
|
import { useTimeLine } from '../_providers/GsapProvicer';
|
||||||
|
import { useMessages } from '../_providers/MessagesProvider';
|
||||||
|
import { Spinner } from '~/components/ui/spinner';
|
||||||
|
|
||||||
export default function ChatPage() {
|
export default function ChatPage() {
|
||||||
const { data: session, error, isLoading } = trpc.chat.getSession.useQuery();
|
const {messages,session,isLoading,error} = useMessages()
|
||||||
|
useTimeLine(messages)
|
||||||
return (
|
return (
|
||||||
<div className="container max-w-2xl mx-auto h-screen pt-10 pb-4 flex flex-col">
|
<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 flex-1 bg-background border rounded-lg overflow-hidden">
|
<AnimatedPageTitle position={0}>
|
||||||
<div className="p-4 border-b flex items-center justify-between">
|
<span>Talk To My </span> <span> AI-Assistant</span>
|
||||||
<div>
|
</AnimatedPageTitle>
|
||||||
<h1 className="text-lg font-semibold">AI Recruiter</h1>
|
<div className='flex items-center h-[80%] w-full my-auto w-full'>
|
||||||
<p className="text-xs text-muted-foreground">
|
{!isLoading &&
|
||||||
Chat with Gregor's AI assistant
|
<ChatInterface sessionId={session?.id} dbMessages={messages ?? []}/>
|
||||||
</p>
|
}
|
||||||
</div>
|
{isLoading &&
|
||||||
</div>
|
<><Spinner/> Loading Messages...</>
|
||||||
<div className="flex-1 overflow-hidden">
|
}
|
||||||
{session && <ChatInterface sessionId={session?.id} initialMessages={session?.messages} /> }
|
{error &&
|
||||||
{error && <div>{error.message}</div>}
|
<div> {error} </div>
|
||||||
{isLoading && <Skeleton/>}
|
}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import TrpcProvider from "./_trpc/TrpcProvider";
|
|||||||
// const ThemeProvider = dynamic(() => import("./_providers/ThemeProvider"),{ssr:true})
|
// const ThemeProvider = dynamic(() => import("./_providers/ThemeProvider"),{ssr:true})
|
||||||
import ThemeProvider from './_providers/ThemeProvider'
|
import ThemeProvider from './_providers/ThemeProvider'
|
||||||
import GsapProvider from "./_providers/GsapProvicer";
|
import GsapProvider from "./_providers/GsapProvicer";
|
||||||
|
import {MessagesProvider} from "./_providers/MessagesProvider";
|
||||||
import { CodeHighlightStyle } from "./_components/CodeHighlightSyle";
|
import { CodeHighlightStyle } from "./_components/CodeHighlightSyle";
|
||||||
import { cn } from "~/lib/utils";
|
import { cn } from "~/lib/utils";
|
||||||
import AnimatedBackGroundContainer from "./_components/Animated/AnimatedBackGroundContainer";
|
import AnimatedBackGroundContainer from "./_components/Animated/AnimatedBackGroundContainer";
|
||||||
@@ -45,14 +46,16 @@ export default async function RootLayout({
|
|||||||
</head>
|
</head>
|
||||||
<body className="flex flex-col bg-background text-foreground">
|
<body className="flex flex-col bg-background text-foreground">
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
|
<MessagesProvider>
|
||||||
<AnimatedBackGroundContainer followSpeed={0.003} particleCount={100} orbitRadius={2000}>
|
<AnimatedBackGroundContainer followSpeed={0.003} particleCount={100} orbitRadius={2000}>
|
||||||
<TopNav />
|
<TopNav />
|
||||||
<main className="absolute lg:top-10 h-screen w-screen">
|
<main className="absolute lg:top-10 h-screen lg:h-[calc(100vh-var(--spacing)*10)] w-screen">
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
{modal}
|
{modal}
|
||||||
</AnimatedBackGroundContainer>
|
</AnimatedBackGroundContainer>
|
||||||
<ChatFAB />
|
<ChatFAB />
|
||||||
|
</MessagesProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -26,20 +26,39 @@ function AnimatedCard({
|
|||||||
className,
|
className,
|
||||||
position = 0,
|
position = 0,
|
||||||
size = "default",
|
size = "default",
|
||||||
|
scrollOnly = false,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"div"> & { size?: "default" | "sm", position: gsap.Position }) {
|
}: React.ComponentProps<"div"> & { size?: "default" | "sm", position?: gsap.Position, scrollOnly?: boolean }) {
|
||||||
const gsapContext = useGsapContext()
|
const gsapContext = useGsapContext()
|
||||||
const ref = useRef<HTMLDivElement | null>(null)
|
const ref = useRef<HTMLDivElement | null>(null)
|
||||||
useGSAP(() => {
|
useGSAP(() => {
|
||||||
const rect = ref.current?.getBoundingClientRect()
|
const rect = ref.current?.getBoundingClientRect()
|
||||||
const isInView = rect && rect.top < window.innerHeight
|
const scroller = gsapContext?.getScroller()
|
||||||
|
console.log(scroller)
|
||||||
|
let viewportTop = 0
|
||||||
|
let viewportBottom = window.innerHeight
|
||||||
|
if (scroller && scroller instanceof Element) {
|
||||||
|
const scrollerRect = scroller.getBoundingClientRect()
|
||||||
|
viewportTop = scrollerRect.top
|
||||||
|
viewportBottom = scrollerRect.top + scrollerRect.height
|
||||||
|
}
|
||||||
|
const isInView = rect && rect.bottom > viewportTop && rect.top < viewportBottom
|
||||||
|
console.log(isInView)
|
||||||
const fromVars = { x: -100, opacity: 0, duration: 0.5 }
|
const fromVars = { x: -100, opacity: 0, duration: 0.5 }
|
||||||
if (isInView) {
|
if (isInView && !scrollOnly) {
|
||||||
gsapContext?.addAnimation(gsap.from(ref.current, fromVars), position)
|
gsapContext?.addAnimation(gsap.from(ref.current, fromVars), position)
|
||||||
} else {
|
} else {
|
||||||
const scroller = gsapContext?.getScroller()
|
gsap.from(ref.current,
|
||||||
console.log('scroller:', scroller)
|
{
|
||||||
gsap.from(ref.current, { ...fromVars, scrollTrigger: { trigger: ref.current, start: 'top 85%', scroller } })
|
...fromVars,
|
||||||
|
scrollTrigger: {
|
||||||
|
trigger: ref.current,
|
||||||
|
start: 'top bottom',
|
||||||
|
end: 'bottom top',
|
||||||
|
toggleActions: "play reverse play reverse",
|
||||||
|
scroller
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}, { dependencies: [] })
|
}, { dependencies: [] })
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import type { UseTRPCQueryResult } from "node_modules/@trpc/react-query/dist/getQueryKey.d-CruH3ncI.mjs"
|
import type { UseTRPCQueryResult } from "node_modules/@trpc/react-query/dist/getQueryKey.d-CruH3ncI.mjs"
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useRef, useState } from "react"
|
||||||
|
|
||||||
function useRelationShipSuccess<T extends Record<string,any> & {id:string},K extends keyof T>(
|
function useRelationShipSuccess<T extends Record<string,any> & {id:string},K extends keyof T>(
|
||||||
relationShipData: T[] | undefined,
|
relationShipData: T[] | undefined,
|
||||||
@@ -56,3 +56,10 @@ export function makeUseRelationShipWithNameIndex<K extends string>(key:K) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function usePrevious<T>(value:T,initialValue:T) {
|
||||||
|
const ref = useRef(initialValue)
|
||||||
|
useEffect(() => {
|
||||||
|
ref.current = value
|
||||||
|
})
|
||||||
|
return ref.current;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,53 @@
|
|||||||
import { auth } from '@clerk/nextjs/server'
|
|
||||||
import { publicProcedure, router } from "../trpc";
|
import { publicProcedure, router } from "../trpc";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { db } from '~/server/db'
|
import { db } from '~/server/db'
|
||||||
import { chatSession, systemSettings } from "../dbschema/schema";
|
import { chatMessage,
|
||||||
|
chatSession, systemSettings } from "../dbschema/schema";
|
||||||
import { isAdmin } from '~/app/actions';
|
import { isAdmin } from '~/app/actions';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { clerkClient, auth } from '@clerk/nextjs/server'
|
||||||
export const chatRouter = router({
|
export const chatRouter = router({
|
||||||
getSession: publicProcedure.query(async () => {
|
getSession: publicProcedure.query(async () => {
|
||||||
|
const clerk = await clerkClient()
|
||||||
|
const { userId } = await auth();
|
||||||
|
const user = await clerk.users.getUser(userId?userId:"")
|
||||||
|
if (user == undefined) {
|
||||||
|
throw new TRPCError({ message: "chat is only available to signed in users", code: 'UNAUTHORIZED' });
|
||||||
|
}
|
||||||
|
let session = await db.query.chatSession.findFirst({
|
||||||
|
where(fields, operators) {
|
||||||
|
return operators.eq(fields.userId, user.id)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (session !== undefined) {
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
let newSession = await db.insert(chatSession).values({ userId: user.id}).returning().execute().then((r) => r.at(0)); if (newSession == undefined) {
|
||||||
|
throw new TRPCError({ message: "failed to create session", code: "INTERNAL_SERVER_ERROR" });
|
||||||
|
}
|
||||||
|
session = await db.query.chatSession.findFirst({
|
||||||
|
where(fields, operators) {
|
||||||
|
return operators.eq(fields.userId, user.id)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (session == undefined) {
|
||||||
|
throw new TRPCError({ message: "session not found", code: "NOT_FOUND" });
|
||||||
|
}
|
||||||
|
if (session !== undefined) {
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
getMessages: publicProcedure.input(z.string()).query(async ({input}) => {
|
||||||
|
let res = await db.query.chatMessage.findMany({
|
||||||
|
where(fields,operators) {
|
||||||
|
return operators.eq(fields.sessionId,input)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return res;
|
||||||
|
}),
|
||||||
|
clearChat: publicProcedure.mutation(async () => {
|
||||||
|
console.log("deleting session")
|
||||||
const { userId } = await auth();
|
const { userId } = await auth();
|
||||||
if (userId == null) {
|
if (userId == null) {
|
||||||
throw new TRPCError({ message: "chat is only available to signed in users", code: 'UNAUTHORIZED' });
|
throw new TRPCError({ message: "chat is only available to signed in users", code: 'UNAUTHORIZED' });
|
||||||
@@ -19,27 +60,10 @@ export const chatRouter = router({
|
|||||||
return operators.eq(fields.userId, userId)
|
return operators.eq(fields.userId, userId)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if (session !== undefined) {
|
if (session != undefined) {
|
||||||
return session;
|
db.delete(chatMessage).where(eq(chatMessage.sessionId,session.id)).execute()
|
||||||
}
|
|
||||||
let newSession = await db.insert(chatSession).values({userId: userId}).returning().execute().then((r) => r.at(0));
|
|
||||||
if (newSession == undefined) {
|
|
||||||
throw new TRPCError({message: "failed to create session", code:"INTERNAL_SERVER_ERROR"});
|
|
||||||
}
|
|
||||||
session = await db.query.chatSession.findFirst({
|
|
||||||
with: {
|
|
||||||
messages: true
|
|
||||||
},
|
|
||||||
where(fields, operators) {
|
|
||||||
return operators.eq(fields.id,newSession.id)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if (session == undefined) {
|
|
||||||
throw new TRPCError({message: "session not found", code:"NOT_FOUND"});
|
|
||||||
}
|
|
||||||
if (session !== undefined) {
|
|
||||||
return session;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}),
|
}),
|
||||||
getSystemPrompt: publicProcedure.query(async () => {
|
getSystemPrompt: publicProcedure.query(async () => {
|
||||||
const row = await db.select().from(systemSettings).limit(1).then((r) => r[0])
|
const row = await db.select().from(systemSettings).limit(1).then((r) => r[0])
|
||||||
@@ -51,3 +75,5 @@ export const chatRouter = router({
|
|||||||
await db.insert(systemSettings).values({ systemPropmt: input.prompt })
|
await db.insert(systemSettings).values({ systemPropmt: input.prompt })
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export type ChatRouter = typeof chatRouter;
|
||||||
|
|||||||
@@ -140,3 +140,7 @@
|
|||||||
-ms-overflow-style: none; /* IE and Edge */
|
-ms-overflow-style: none; /* IE and Edge */
|
||||||
scrollbar-width: none; /* Firefox */
|
scrollbar-width: none; /* Firefox */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cl-button__google {
|
||||||
|
display: none
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user