chat fab
This commit is contained in:
@@ -18,8 +18,8 @@ export default function ChatModal({ sessionId, initialMessages }: ChatModalProps
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<Dialog open onOpenChange={() => router.back()}>
|
||||
<DialogContent className="max-w-2xl h-[80vh] flex flex-col p-0 gap-0">
|
||||
<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">
|
||||
<DialogHeader className="p-4 border-b shrink-0">
|
||||
<DialogTitle>AI Recruiter</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -1,34 +1,15 @@
|
||||
import { auth } from '@clerk/nextjs/server'
|
||||
import { redirect } from 'next/navigation'
|
||||
import { asc, desc, eq } from 'drizzle-orm'
|
||||
import { db } from '~/server/db'
|
||||
import { chatMessage, chatSession } from '~/server/dbschema/schema'
|
||||
'use client'
|
||||
import { Skeleton } from '~/components/ui/skeleton';
|
||||
import ChatModal from './_components/ChatModal'
|
||||
import { trpc } from '~/app/_trpc/Client'
|
||||
|
||||
export default async function ChatModalPage() {
|
||||
const { userId } = await auth()
|
||||
if (!userId) redirect('/')
|
||||
|
||||
let session = await db
|
||||
.select()
|
||||
.from(chatSession)
|
||||
.where(eq(chatSession.userId, userId))
|
||||
.orderBy(desc(chatSession.createdAt))
|
||||
.limit(1)
|
||||
.then((r) => r[0])
|
||||
|
||||
if (!session) {
|
||||
const [created] = await db.insert(chatSession).values({ userId }).returning()
|
||||
session = created
|
||||
}
|
||||
|
||||
if (!session) redirect('/')
|
||||
|
||||
const messages = await db
|
||||
.select()
|
||||
.from(chatMessage)
|
||||
.where(eq(chatMessage.sessionId, session.id))
|
||||
.orderBy(asc(chatMessage.createdAt))
|
||||
|
||||
return <ChatModal sessionId={session.id} initialMessages={messages} />
|
||||
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 />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
19
src/app/_components/ChatFAB.tsx
Normal file
19
src/app/_components/ChatFAB.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
'use client'
|
||||
import Link from 'next/link'
|
||||
import { MessageCircle } from 'lucide-react'
|
||||
import { Show } from '@clerk/nextjs'
|
||||
import { Button } from '~/components/ui/button'
|
||||
|
||||
export default function ChatFAB() {
|
||||
return (
|
||||
<Show when="signed-in">
|
||||
<div className="fixed bottom-6 right-6 z-50">
|
||||
<Button asChild size="icon" className="h-14 w-14 rounded-full shadow-lg">
|
||||
<Link href="/chat">
|
||||
<MessageCircle className="h-6 w-6" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export default function TopNav() {
|
||||
</Button>
|
||||
<Show when="signed-in">
|
||||
<Button asChild className="flex h-10 lg:h-full w-full lg:w-20" variant="outline">
|
||||
<Link href={"/chat"}> Chat </Link>
|
||||
<a href="/chat"> Chat </a>
|
||||
</Button>
|
||||
</Show>
|
||||
</div>
|
||||
@@ -52,7 +52,13 @@ export default function TopNav() {
|
||||
<Show when="signed-in">
|
||||
<Button asChild className="flex h-10 lg:h-full cursor-pointer lg:w-20 content-center" variant={"outline"}>
|
||||
<div>
|
||||
<UserButton />
|
||||
<UserButton
|
||||
userProfileProps={{
|
||||
additionalOAuthScopes: {
|
||||
google: ['https://www.googleapis.com/auth/calendar'],
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Button>
|
||||
</Show>
|
||||
|
||||
@@ -26,6 +26,9 @@ export default function AdminSideBar() {
|
||||
<SimpleSidebarGroup lable="Blog">
|
||||
<Link href={"/"}> Some Blog Action </Link>
|
||||
</SimpleSidebarGroup>
|
||||
<SimpleSidebarGroup lable="Chat">
|
||||
<Link href={"/admin/chat"}> System Prompt </Link>
|
||||
</SimpleSidebarGroup>
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
</SidebarProvider>
|
||||
|
||||
39
src/app/admin/chat/_components/SystemPromptForm.tsx
Normal file
39
src/app/admin/chat/_components/SystemPromptForm.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
'use client'
|
||||
import { useState } from 'react'
|
||||
import { Textarea } from '~/components/ui/textarea'
|
||||
import { Button } from '~/components/ui/button'
|
||||
import { trpc } from '~/app/_trpc/Client'
|
||||
|
||||
export default function SystemPromptForm({ initialValue }: { initialValue: string }) {
|
||||
const [value, setValue] = useState(initialValue)
|
||||
const [saved, setSaved] = useState(false)
|
||||
|
||||
const mutation = trpc.chat.updateSystemPrompt.useMutation({
|
||||
onSuccess: () => setSaved(true),
|
||||
})
|
||||
|
||||
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault()
|
||||
setSaved(false)
|
||||
mutation.mutate({ prompt: value })
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-4 w-full">
|
||||
<Textarea
|
||||
value={value}
|
||||
onChange={(e) => { setValue(e.target.value); setSaved(false) }}
|
||||
rows={16}
|
||||
className="font-mono text-sm resize-y"
|
||||
placeholder="Enter the system prompt for the AI recruiter..."
|
||||
/>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button type="submit" disabled={mutation.isPending}>
|
||||
{mutation.isPending ? 'Saving…' : 'Save'}
|
||||
</Button>
|
||||
{saved && <span className="text-sm text-muted-foreground">Saved</span>}
|
||||
{mutation.error && <span className="text-sm text-destructive">{mutation.error.message}</span>}
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
22
src/app/admin/chat/page.tsx
Normal file
22
src/app/admin/chat/page.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { isAdmin } from '~/app/actions'
|
||||
import { redirect } from 'next/navigation'
|
||||
import { servTrpc } from '~/app/_trpc/ServerClient'
|
||||
import SystemPromptForm from './_components/SystemPromptForm'
|
||||
|
||||
export default async function SystemPromptPage() {
|
||||
if (!(await isAdmin())) redirect('/admin')
|
||||
|
||||
const prompt = await servTrpc.chat.getSystemPrompt()
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-2xl p-6 flex flex-col gap-4">
|
||||
<div>
|
||||
<h1 className="text-lg font-semibold">AI System Prompt</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
This prompt is sent to the model on every chat request.
|
||||
</p>
|
||||
</div>
|
||||
<SystemPromptForm initialValue={prompt} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -6,13 +6,14 @@ import { eq, and } from 'drizzle-orm'
|
||||
import { env } from '~/env'
|
||||
import { db } from '~/server/db'
|
||||
import { chatSession, chatMessage } from '~/server/dbschema/schema'
|
||||
import { servTrpc } from '~/app/_trpc/ServerClient'
|
||||
import { scheduleMeeting } from '~/app/actions/scheduleMeeting'
|
||||
|
||||
const openai = createOpenAI({ apiKey: env.OPENAI_API_KEY })
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { userId } = await auth()
|
||||
if (!userId) return new Response('Unauthorized', { status: 401 })
|
||||
if (userId == null) return new Response('Unauthorized', { status: 401 })
|
||||
|
||||
const { messages, sessionId } = (await req.json()) as {
|
||||
messages: UIMessage[]
|
||||
@@ -29,6 +30,8 @@ export async function POST(req: Request) {
|
||||
|
||||
if (!session) return new Response('Session not found', { status: 404 })
|
||||
|
||||
const systemPrompt = await servTrpc.chat.getSystemPrompt() || 'You are an AI recruiter assistant.'
|
||||
|
||||
// Save the latest user message
|
||||
const lastMessage = messages[messages.length - 1]
|
||||
if (lastMessage?.role === 'user') {
|
||||
@@ -43,16 +46,7 @@ export async function POST(req: Request) {
|
||||
|
||||
const result = streamText({
|
||||
model: openai('gpt-4o'),
|
||||
system: `You are an AI recruiter assistant on Gregor Lohaus's personal portfolio website.
|
||||
Your role is to help visitors learn about Gregor's background, skills, and experience, and to schedule meetings.
|
||||
|
||||
About Gregor:
|
||||
- Fullstack developer specialising in TypeScript, React, Next.js, and modern web technologies
|
||||
- Experienced with Drizzle ORM, tRPC, PostgreSQL, server components, and the T3 stack
|
||||
- Also experienced with React Native, Expo, Java/Spring, gRPC, AWS, and Linux/Debian server administration
|
||||
- Open to new opportunities and collaboration
|
||||
|
||||
Be professional, friendly, and concise. When a visitor wants to schedule a meeting, collect the necessary details (preferred date/time, duration, and optionally their name/email) and use the scheduleMeeting tool.`,
|
||||
system: systemPrompt,
|
||||
messages: await convertToModelMessages(messages),
|
||||
tools: {
|
||||
scheduleMeeting: tool({
|
||||
|
||||
68
src/app/chat/_components/AssistantMessage.tsx
Normal file
68
src/app/chat/_components/AssistantMessage.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import type { UIMessage } from "ai";
|
||||
import Markdown from "react-markdown";
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
export const AssistantMessage = (props: { message: UIMessage }) => {
|
||||
let message = props.message;
|
||||
return (
|
||||
<div
|
||||
key={message.id}
|
||||
className='flex justify-start'
|
||||
>
|
||||
<div
|
||||
className=
|
||||
'max-w-[80%] px-4 py-2 text-sm space-y-2 bg-muted'
|
||||
>
|
||||
{message.parts.map((part, i) => {
|
||||
if (part.type === 'text') {
|
||||
return (
|
||||
<Markdown>
|
||||
{part.text}
|
||||
</Markdown>
|
||||
)
|
||||
}
|
||||
if (part.type === 'tool-scheduleMeeting') {
|
||||
const toolPart = part as unknown as {
|
||||
type: 'tool-scheduleMeeting'
|
||||
state: string
|
||||
input: unknown
|
||||
output?: { success: boolean; message?: string; htmlLink?: string; error?: string }
|
||||
}
|
||||
if (toolPart.state === 'input-available' || toolPart.state === 'input-streaming') {
|
||||
return (
|
||||
<p key={i} className="text-xs opacity-70 italic">
|
||||
Scheduling meeting…
|
||||
</p>
|
||||
)
|
||||
}
|
||||
if (toolPart.state === 'output-available' && toolPart.output) {
|
||||
const result = toolPart.output
|
||||
return (
|
||||
<div key={i} className="text-xs mt-1 p-2 bg-background/20 rounded">
|
||||
{result.success ? (
|
||||
<span>
|
||||
✓ {result.message}{' '}
|
||||
{result.htmlLink && (
|
||||
<a
|
||||
href={result.htmlLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
View event
|
||||
</a>
|
||||
)}
|
||||
</span>
|
||||
) : (
|
||||
<span>✗ {result.error}</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
return null
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -5,6 +5,9 @@ import { DefaultChatTransport, type UIMessage } from 'ai'
|
||||
import { Button } from '~/components/ui/button'
|
||||
import { Textarea } from '~/components/ui/textarea'
|
||||
import { cn } from '~/lib/utils'
|
||||
import Markdown from 'react-markdown';
|
||||
import { AssistantMessage } from './AssistantMessage';
|
||||
import { UserMessage } from './UserMessage';
|
||||
|
||||
type DBMessage = {
|
||||
id: string
|
||||
@@ -29,7 +32,7 @@ export default function ChatInterface({ sessionId, initialMessages }: ChatInterf
|
||||
const [input, setInput] = useState('')
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const { messages, sendMessage, status } = useChat({
|
||||
const { messages, sendMessage, status, error, clearError } = useChat({
|
||||
transport: new DefaultChatTransport({
|
||||
api: '/api/chat',
|
||||
body: { sessionId },
|
||||
@@ -38,6 +41,7 @@ export default function ChatInterface({ sessionId, initialMessages }: ChatInterf
|
||||
})
|
||||
|
||||
const isLoading = status === 'submitted' || status === 'streaming'
|
||||
const hasError = status === 'error'
|
||||
|
||||
useEffect(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
||||
@@ -61,67 +65,10 @@ export default function ChatInterface({ sessionId, initialMessages }: ChatInterf
|
||||
)}
|
||||
|
||||
{messages.map((message) => (
|
||||
<div
|
||||
key={message.id}
|
||||
className={cn('flex', message.role === 'user' ? 'justify-end' : 'justify-start')}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'max-w-[80%] rounded-lg px-4 py-2 text-sm space-y-2',
|
||||
message.role === 'user' ? 'bg-primary text-primary-foreground' : 'bg-muted',
|
||||
)}
|
||||
>
|
||||
{message.parts.map((part, i) => {
|
||||
if (part.type === 'text') {
|
||||
return (
|
||||
<p key={i} className="whitespace-pre-wrap">
|
||||
{part.text}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
if (part.type === 'tool-scheduleMeeting') {
|
||||
const toolPart = part as unknown as {
|
||||
type: 'tool-scheduleMeeting'
|
||||
state: string
|
||||
input: unknown
|
||||
output?: { success: boolean; message?: string; htmlLink?: string; error?: string }
|
||||
}
|
||||
if (toolPart.state === 'input-available' || toolPart.state === 'input-streaming') {
|
||||
return (
|
||||
<p key={i} className="text-xs opacity-70 italic">
|
||||
Scheduling meeting…
|
||||
</p>
|
||||
)
|
||||
}
|
||||
if (toolPart.state === 'output-available' && toolPart.output) {
|
||||
const result = toolPart.output
|
||||
return (
|
||||
<div key={i} className="text-xs mt-1 p-2 bg-background/20 rounded">
|
||||
{result.success ? (
|
||||
<span>
|
||||
✓ {result.message}{' '}
|
||||
{result.htmlLink && (
|
||||
<a
|
||||
href={result.htmlLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
View event
|
||||
</a>
|
||||
)}
|
||||
</span>
|
||||
) : (
|
||||
<span>✗ {result.error}</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
return null
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<>
|
||||
{message.role == 'assistant' && <AssistantMessage message={message}/>}
|
||||
{message.role == 'user' && <UserMessage message={message}/>}
|
||||
</>
|
||||
))}
|
||||
|
||||
{isLoading && (
|
||||
@@ -135,6 +82,24 @@ export default function ChatInterface({ sessionId, initialMessages }: ChatInterf
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{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">
|
||||
<span className="flex-1">
|
||||
{error.message.includes('quota') || error.message.includes('429')
|
||||
? 'OpenAI quota exceeded. Please try again later.'
|
||||
: `Error: ${error.message}`}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={clearError}
|
||||
className="shrink-0 opacity-60 hover:opacity-100"
|
||||
aria-label="Dismiss"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="p-4 border-t flex gap-2">
|
||||
<Textarea
|
||||
value={input}
|
||||
@@ -151,7 +116,7 @@ export default function ChatInterface({ sessionId, initialMessages }: ChatInterf
|
||||
/>
|
||||
<Button
|
||||
onClick={handleSend}
|
||||
disabled={isLoading || !input.trim()}
|
||||
disabled={isLoading || hasError || !input.trim()}
|
||||
className="self-end"
|
||||
>
|
||||
Send
|
||||
|
||||
23
src/app/chat/_components/UserMessage.tsx
Normal file
23
src/app/chat/_components/UserMessage.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { UIMessage } from "ai"
|
||||
|
||||
export const UserMessage = (props:{message: UIMessage}) => {
|
||||
let message = props.message.parts.reduce((acc, part) => {
|
||||
if (part.type == 'text') {
|
||||
return acc + part.text
|
||||
}
|
||||
return acc
|
||||
},"");
|
||||
return (
|
||||
<div
|
||||
key={props.message.id}
|
||||
className='flex justify-end'
|
||||
>
|
||||
<div
|
||||
className=
|
||||
'max-w-[80%] px-4 py-2 text-sm space-y-2 bg-primary'
|
||||
>
|
||||
{message}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,35 +1,10 @@
|
||||
import { auth } from '@clerk/nextjs/server'
|
||||
import { redirect } from 'next/navigation'
|
||||
import { asc, desc, eq } from 'drizzle-orm'
|
||||
import { db } from '~/server/db'
|
||||
import { chatMessage, chatSession } from '~/server/dbschema/schema'
|
||||
'use client'
|
||||
import ChatInterface from './_components/ChatInterface'
|
||||
import { trpc } from '../_trpc/Client';
|
||||
import { Skeleton } from '~/components/ui/skeleton';
|
||||
|
||||
export default async function ChatPage() {
|
||||
const { userId } = await auth()
|
||||
if (!userId) redirect('/')
|
||||
|
||||
let session = await db
|
||||
.select()
|
||||
.from(chatSession)
|
||||
.where(eq(chatSession.userId, userId))
|
||||
.orderBy(desc(chatSession.createdAt))
|
||||
.limit(1)
|
||||
.then((r) => r[0])
|
||||
|
||||
if (!session) {
|
||||
const [created] = await db.insert(chatSession).values({ userId }).returning()
|
||||
session = created
|
||||
}
|
||||
|
||||
if (!session) redirect('/')
|
||||
|
||||
const messages = await db
|
||||
.select()
|
||||
.from(chatMessage)
|
||||
.where(eq(chatMessage.sessionId, session.id))
|
||||
.orderBy(asc(chatMessage.createdAt))
|
||||
|
||||
export default function ChatPage() {
|
||||
const { data: session, error, isLoading } = trpc.chat.getSession.useQuery();
|
||||
return (
|
||||
<div className="container max-w-2xl mx-auto h-screen pt-10 pb-4 flex flex-col">
|
||||
<div className="flex flex-col flex-1 bg-background border rounded-lg overflow-hidden">
|
||||
@@ -42,7 +17,9 @@ export default async function ChatPage() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<ChatInterface sessionId={session.id} initialMessages={messages} />
|
||||
{session && <ChatInterface sessionId={session?.id} initialMessages={session?.messages} /> }
|
||||
{error && <div>{error.message}</div>}
|
||||
{isLoading && <Skeleton/>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ClerkProvider } from "@clerk/nextjs";
|
||||
import { config } from "@fortawesome/fontawesome-svg-core"
|
||||
import "@fortawesome/fontawesome-svg-core/styles.css"
|
||||
import TopNav from "./_components/TopNav";
|
||||
import ChatFAB from "./_components/ChatFAB";
|
||||
import TrpcProvider from "./_trpc/TrpcProvider";
|
||||
// import dynamic from "next/dynamic";
|
||||
// const ThemeProvider = dynamic(() => import("./_providers/ThemeProvider"),{ssr:true})
|
||||
@@ -51,6 +52,7 @@ export default async function RootLayout({
|
||||
</main>
|
||||
{modal}
|
||||
</AnimatedBackGroundContainer>
|
||||
<ChatFAB />
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -137,3 +137,10 @@ export const chatMessageRelations = relations(chatMessage, ({ one }) => ({
|
||||
references: [chatSession.id],
|
||||
}),
|
||||
}))
|
||||
|
||||
export const systemSettings = createTable(
|
||||
"systemSetting",
|
||||
(d) => ({
|
||||
systemPropmt: d.text()
|
||||
})
|
||||
)
|
||||
|
||||
@@ -8,6 +8,7 @@ import { cvEntryRouter } from "./cvEntry";
|
||||
import { musicRouter } from "./music";
|
||||
import { trpcCrudRouterFromDrizzleEntity } from "../lib";
|
||||
import { cvCategory } from "../dbschema/schema";
|
||||
import { chatRouter } from "./chat";
|
||||
|
||||
export const trpcRouter = router({
|
||||
project: trpcCrudRouterFromDrizzleEntity('project').router,
|
||||
@@ -19,6 +20,7 @@ export const trpcRouter = router({
|
||||
entry: trpcCrudRouterFromDrizzleEntity('cvEntry').router,
|
||||
entryv2: cvEntryRouter,
|
||||
music: musicRouter,
|
||||
chat: chatRouter
|
||||
});
|
||||
|
||||
export type TrpcRouter = typeof trpcRouter;
|
||||
|
||||
53
src/server/routers/chat.ts
Normal file
53
src/server/routers/chat.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { auth } from '@clerk/nextjs/server'
|
||||
import { publicProcedure, router } from "../trpc";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { db } from '~/server/db'
|
||||
import { chatSession, systemSettings } from "../dbschema/schema";
|
||||
import { isAdmin } from '~/app/actions';
|
||||
import { z } from 'zod';
|
||||
export const chatRouter = router({
|
||||
getSession: publicProcedure.query(async () => {
|
||||
const { userId } = await auth();
|
||||
if (userId == null) {
|
||||
throw new TRPCError({message: "chat is only available to signed in users",code: 'UNAUTHORIZED'});
|
||||
}
|
||||
let session = await db.query.chatSession.findFirst({
|
||||
with: {
|
||||
messages: true
|
||||
},
|
||||
where(fields, operators) {
|
||||
return operators.eq(fields.userId,userId)
|
||||
},
|
||||
})
|
||||
if (session !== undefined) {
|
||||
return session;
|
||||
}
|
||||
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 () => {
|
||||
const row = await db.select().from(systemSettings).limit(1).then((r) => r[0])
|
||||
return row?.systemPropmt ?? ''
|
||||
}),
|
||||
updateSystemPrompt: publicProcedure.input(z.object({ prompt: z.string() })).mutation(async ({ input }) => {
|
||||
if (!(await isAdmin())) throw new TRPCError({ code: 'FORBIDDEN' })
|
||||
await db.delete(systemSettings)
|
||||
await db.insert(systemSettings).values({ systemPropmt: input.prompt })
|
||||
}),
|
||||
})
|
||||
Reference in New Issue
Block a user