langwatch 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.cjs +37 -0
- package/README.md +3 -0
- package/dist/chunk-GOA2HL4A.mjs +269 -0
- package/dist/chunk-GOA2HL4A.mjs.map +1 -0
- package/dist/index.d.mts +82 -0
- package/dist/index.d.ts +82 -0
- package/dist/index.js +940 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +666 -0
- package/dist/index.mjs.map +1 -0
- package/dist/utils-s3gGR6vj.d.mts +209 -0
- package/dist/utils-s3gGR6vj.d.ts +209 -0
- package/dist/utils.d.mts +3 -0
- package/dist/utils.d.ts +3 -0
- package/dist/utils.js +263 -0
- package/dist/utils.js.map +1 -0
- package/dist/utils.mjs +7 -0
- package/dist/utils.mjs.map +1 -0
- package/example/.env.example +12 -0
- package/example/.eslintrc.json +26 -0
- package/example/LICENSE +13 -0
- package/example/README.md +10 -0
- package/example/app/(chat)/chat/[id]/page.tsx +60 -0
- package/example/app/(chat)/layout.tsx +14 -0
- package/example/app/(chat)/page.tsx +22 -0
- package/example/app/actions.ts +156 -0
- package/example/app/globals.css +76 -0
- package/example/app/layout.tsx +64 -0
- package/example/app/login/actions.ts +71 -0
- package/example/app/login/page.tsx +18 -0
- package/example/app/new/page.tsx +5 -0
- package/example/app/opengraph-image.png +0 -0
- package/example/app/share/[id]/page.tsx +58 -0
- package/example/app/signup/actions.ts +111 -0
- package/example/app/signup/page.tsx +18 -0
- package/example/app/twitter-image.png +0 -0
- package/example/auth.config.ts +42 -0
- package/example/auth.ts +45 -0
- package/example/components/button-scroll-to-bottom.tsx +36 -0
- package/example/components/chat-history.tsx +49 -0
- package/example/components/chat-list.tsx +52 -0
- package/example/components/chat-message-actions.tsx +40 -0
- package/example/components/chat-message.tsx +80 -0
- package/example/components/chat-panel.tsx +139 -0
- package/example/components/chat-share-dialog.tsx +95 -0
- package/example/components/chat.tsx +84 -0
- package/example/components/clear-history.tsx +75 -0
- package/example/components/empty-screen.tsx +38 -0
- package/example/components/external-link.tsx +29 -0
- package/example/components/footer.tsx +19 -0
- package/example/components/header.tsx +80 -0
- package/example/components/login-button.tsx +42 -0
- package/example/components/login-form.tsx +97 -0
- package/example/components/markdown.tsx +9 -0
- package/example/components/prompt-form.tsx +115 -0
- package/example/components/providers.tsx +17 -0
- package/example/components/sidebar-actions.tsx +125 -0
- package/example/components/sidebar-desktop.tsx +19 -0
- package/example/components/sidebar-footer.tsx +16 -0
- package/example/components/sidebar-item.tsx +124 -0
- package/example/components/sidebar-items.tsx +42 -0
- package/example/components/sidebar-list.tsx +38 -0
- package/example/components/sidebar-mobile.tsx +31 -0
- package/example/components/sidebar-toggle.tsx +24 -0
- package/example/components/sidebar.tsx +21 -0
- package/example/components/signup-form.tsx +95 -0
- package/example/components/stocks/events-skeleton.tsx +31 -0
- package/example/components/stocks/events.tsx +30 -0
- package/example/components/stocks/index.tsx +36 -0
- package/example/components/stocks/message.tsx +134 -0
- package/example/components/stocks/spinner.tsx +16 -0
- package/example/components/stocks/stock-purchase.tsx +146 -0
- package/example/components/stocks/stock-skeleton.tsx +22 -0
- package/example/components/stocks/stock.tsx +210 -0
- package/example/components/stocks/stocks-skeleton.tsx +9 -0
- package/example/components/stocks/stocks.tsx +67 -0
- package/example/components/tailwind-indicator.tsx +14 -0
- package/example/components/theme-toggle.tsx +31 -0
- package/example/components/ui/alert-dialog.tsx +141 -0
- package/example/components/ui/badge.tsx +36 -0
- package/example/components/ui/button.tsx +57 -0
- package/example/components/ui/codeblock.tsx +148 -0
- package/example/components/ui/dialog.tsx +122 -0
- package/example/components/ui/dropdown-menu.tsx +205 -0
- package/example/components/ui/icons.tsx +507 -0
- package/example/components/ui/input.tsx +25 -0
- package/example/components/ui/label.tsx +26 -0
- package/example/components/ui/select.tsx +164 -0
- package/example/components/ui/separator.tsx +31 -0
- package/example/components/ui/sheet.tsx +140 -0
- package/example/components/ui/sonner.tsx +31 -0
- package/example/components/ui/switch.tsx +29 -0
- package/example/components/ui/textarea.tsx +24 -0
- package/example/components/ui/tooltip.tsx +30 -0
- package/example/components/user-menu.tsx +53 -0
- package/example/components.json +17 -0
- package/example/lib/chat/actions.tsx +606 -0
- package/example/lib/hooks/use-copy-to-clipboard.tsx +33 -0
- package/example/lib/hooks/use-enter-submit.tsx +23 -0
- package/example/lib/hooks/use-local-storage.ts +24 -0
- package/example/lib/hooks/use-scroll-anchor.tsx +86 -0
- package/example/lib/hooks/use-sidebar.tsx +60 -0
- package/example/lib/hooks/use-streamable-text.ts +25 -0
- package/example/lib/types.ts +41 -0
- package/example/lib/utils.ts +89 -0
- package/example/middleware.ts +8 -0
- package/example/next-env.d.ts +5 -0
- package/example/next.config.js +13 -0
- package/example/package-lock.json +9249 -0
- package/example/package.json +77 -0
- package/example/pnpm-lock.yaml +5712 -0
- package/example/postcss.config.js +6 -0
- package/example/prettier.config.cjs +34 -0
- package/example/public/apple-touch-icon.png +0 -0
- package/example/public/favicon-16x16.png +0 -0
- package/example/public/favicon.ico +0 -0
- package/example/public/next.svg +1 -0
- package/example/public/thirteen.svg +1 -0
- package/example/public/vercel.svg +1 -0
- package/example/tailwind.config.ts +81 -0
- package/example/tsconfig.json +35 -0
- package/package.json +45 -0
- package/src/helpers.ts +64 -0
- package/src/index.test.ts +255 -0
- package/src/index.ts +397 -0
- package/src/server/types/.gitkeep +0 -0
- package/src/types.ts +69 -0
- package/src/utils.ts +134 -0
- package/ts-to-zod.config.js +18 -0
- package/tsconfig.json +32 -0
- package/tsup.config.ts +10 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
@layer base {
|
|
6
|
+
:root {
|
|
7
|
+
--background: 0 0% 100%;
|
|
8
|
+
--foreground: 240 10% 3.9%;
|
|
9
|
+
|
|
10
|
+
--card: 0 0% 100%;
|
|
11
|
+
--card-foreground: 240 10% 3.9%;
|
|
12
|
+
|
|
13
|
+
--popover: 0 0% 100%;
|
|
14
|
+
--popover-foreground: 240 10% 3.9%;
|
|
15
|
+
|
|
16
|
+
--primary: 240 5.9% 10%;
|
|
17
|
+
--primary-foreground: 0 0% 98%;
|
|
18
|
+
|
|
19
|
+
--secondary: 240 4.8% 95.9%;
|
|
20
|
+
--secondary-foreground: 240 5.9% 10%;
|
|
21
|
+
|
|
22
|
+
--muted: 240 4.8% 95.9%;
|
|
23
|
+
--muted-foreground: 240 3.8% 46.1%;
|
|
24
|
+
|
|
25
|
+
--accent: 240 4.8% 95.9%;
|
|
26
|
+
--accent-foreground: 240 5.9% 10%;
|
|
27
|
+
|
|
28
|
+
--destructive: 0 84.2% 60.2%;
|
|
29
|
+
--destructive-foreground: 0 0% 98%;
|
|
30
|
+
|
|
31
|
+
--border: 240 5.9% 90%;
|
|
32
|
+
--input: 240 5.9% 90%;
|
|
33
|
+
--ring: 240 10% 3.9%;
|
|
34
|
+
|
|
35
|
+
--radius: 0.5rem;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.dark {
|
|
39
|
+
--background: 240 10% 3.9%;
|
|
40
|
+
--foreground: 0 0% 98%;
|
|
41
|
+
|
|
42
|
+
--card: 240 10% 3.9%;
|
|
43
|
+
--card-foreground: 0 0% 98%;
|
|
44
|
+
|
|
45
|
+
--popover: 240 10% 3.9%;
|
|
46
|
+
--popover-foreground: 0 0% 98%;
|
|
47
|
+
|
|
48
|
+
--primary: 0 0% 98%;
|
|
49
|
+
--primary-foreground: 240 5.9% 10%;
|
|
50
|
+
|
|
51
|
+
--secondary: 240 3.7% 15.9%;
|
|
52
|
+
--secondary-foreground: 0 0% 98%;
|
|
53
|
+
|
|
54
|
+
--muted: 240 3.7% 15.9%;
|
|
55
|
+
--muted-foreground: 240 5% 64.9%;
|
|
56
|
+
|
|
57
|
+
--accent: 240 3.7% 15.9%;
|
|
58
|
+
--accent-foreground: 0 0% 98%;
|
|
59
|
+
|
|
60
|
+
--destructive: 0 62.8% 30.6%;
|
|
61
|
+
--destructive-foreground: 0 0% 98%;
|
|
62
|
+
|
|
63
|
+
--border: 240 3.7% 15.9%;
|
|
64
|
+
--input: 240 3.7% 15.9%;
|
|
65
|
+
--ring: 240 4.9% 83.9%;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@layer base {
|
|
70
|
+
* {
|
|
71
|
+
@apply border-border;
|
|
72
|
+
}
|
|
73
|
+
body {
|
|
74
|
+
@apply bg-background text-foreground;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { GeistSans } from 'geist/font/sans'
|
|
2
|
+
import { GeistMono } from 'geist/font/mono'
|
|
3
|
+
|
|
4
|
+
import '@/app/globals.css'
|
|
5
|
+
import { cn } from '@/lib/utils'
|
|
6
|
+
import { TailwindIndicator } from '@/components/tailwind-indicator'
|
|
7
|
+
import { Providers } from '@/components/providers'
|
|
8
|
+
import { Header } from '@/components/header'
|
|
9
|
+
import { Toaster } from '@/components/ui/sonner'
|
|
10
|
+
|
|
11
|
+
export const metadata = {
|
|
12
|
+
metadataBase: process.env.VERCEL_URL
|
|
13
|
+
? new URL(`https://${process.env.VERCEL_URL}`)
|
|
14
|
+
: undefined,
|
|
15
|
+
title: {
|
|
16
|
+
default: 'Next.js AI Chatbot',
|
|
17
|
+
template: `%s - Next.js AI Chatbot`
|
|
18
|
+
},
|
|
19
|
+
description: 'An AI-powered chatbot template built with Next.js and Vercel.',
|
|
20
|
+
icons: {
|
|
21
|
+
icon: '/favicon.ico',
|
|
22
|
+
shortcut: '/favicon-16x16.png',
|
|
23
|
+
apple: '/apple-touch-icon.png'
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const viewport = {
|
|
28
|
+
themeColor: [
|
|
29
|
+
{ media: '(prefers-color-scheme: light)', color: 'white' },
|
|
30
|
+
{ media: '(prefers-color-scheme: dark)', color: 'black' }
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface RootLayoutProps {
|
|
35
|
+
children: React.ReactNode
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default function RootLayout({ children }: RootLayoutProps) {
|
|
39
|
+
return (
|
|
40
|
+
<html lang="en" suppressHydrationWarning>
|
|
41
|
+
<body
|
|
42
|
+
className={cn(
|
|
43
|
+
'font-sans antialiased',
|
|
44
|
+
GeistSans.variable,
|
|
45
|
+
GeistMono.variable
|
|
46
|
+
)}
|
|
47
|
+
>
|
|
48
|
+
<Toaster position="top-center" />
|
|
49
|
+
<Providers
|
|
50
|
+
attribute="class"
|
|
51
|
+
defaultTheme="system"
|
|
52
|
+
enableSystem
|
|
53
|
+
disableTransitionOnChange
|
|
54
|
+
>
|
|
55
|
+
<div className="flex flex-col min-h-screen">
|
|
56
|
+
<Header />
|
|
57
|
+
<main className="flex flex-col flex-1 bg-muted/50">{children}</main>
|
|
58
|
+
</div>
|
|
59
|
+
<TailwindIndicator />
|
|
60
|
+
</Providers>
|
|
61
|
+
</body>
|
|
62
|
+
</html>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use server'
|
|
2
|
+
|
|
3
|
+
import { signIn } from '@/auth'
|
|
4
|
+
import { User } from '@/lib/types'
|
|
5
|
+
import { AuthError } from 'next-auth'
|
|
6
|
+
import { z } from 'zod'
|
|
7
|
+
import { kv } from '@vercel/kv'
|
|
8
|
+
import { ResultCode } from '@/lib/utils'
|
|
9
|
+
|
|
10
|
+
export async function getUser(email: string) {
|
|
11
|
+
const user = await kv.hgetall<User>(`user:${email}`)
|
|
12
|
+
return user
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface Result {
|
|
16
|
+
type: string
|
|
17
|
+
resultCode: ResultCode
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function authenticate(
|
|
21
|
+
_prevState: Result | undefined,
|
|
22
|
+
formData: FormData
|
|
23
|
+
): Promise<Result | undefined> {
|
|
24
|
+
try {
|
|
25
|
+
const email = formData.get('email')
|
|
26
|
+
const password = formData.get('password')
|
|
27
|
+
|
|
28
|
+
const parsedCredentials = z
|
|
29
|
+
.object({
|
|
30
|
+
email: z.string().email(),
|
|
31
|
+
password: z.string().min(6)
|
|
32
|
+
})
|
|
33
|
+
.safeParse({
|
|
34
|
+
email,
|
|
35
|
+
password
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
if (parsedCredentials.success) {
|
|
39
|
+
await signIn('credentials', {
|
|
40
|
+
email,
|
|
41
|
+
password,
|
|
42
|
+
redirect: false
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
type: 'success',
|
|
47
|
+
resultCode: ResultCode.UserLoggedIn
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
return {
|
|
51
|
+
type: 'error',
|
|
52
|
+
resultCode: ResultCode.InvalidCredentials
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (error instanceof AuthError) {
|
|
57
|
+
switch (error.type) {
|
|
58
|
+
case 'CredentialsSignin':
|
|
59
|
+
return {
|
|
60
|
+
type: 'error',
|
|
61
|
+
resultCode: ResultCode.InvalidCredentials
|
|
62
|
+
}
|
|
63
|
+
default:
|
|
64
|
+
return {
|
|
65
|
+
type: 'error',
|
|
66
|
+
resultCode: ResultCode.UnknownError
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { auth } from '@/auth'
|
|
2
|
+
import LoginForm from '@/components/login-form'
|
|
3
|
+
import { Session } from '@/lib/types'
|
|
4
|
+
import { redirect } from 'next/navigation'
|
|
5
|
+
|
|
6
|
+
export default async function LoginPage() {
|
|
7
|
+
const session = (await auth()) as Session
|
|
8
|
+
|
|
9
|
+
if (session) {
|
|
10
|
+
redirect('/')
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<main className="flex flex-col p-4">
|
|
15
|
+
<LoginForm />
|
|
16
|
+
</main>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { type Metadata } from 'next'
|
|
2
|
+
import { notFound, redirect } from 'next/navigation'
|
|
3
|
+
|
|
4
|
+
import { formatDate } from '@/lib/utils'
|
|
5
|
+
import { getSharedChat } from '@/app/actions'
|
|
6
|
+
import { ChatList } from '@/components/chat-list'
|
|
7
|
+
import { FooterText } from '@/components/footer'
|
|
8
|
+
import { AI, UIState, getUIStateFromAIState } from '@/lib/chat/actions'
|
|
9
|
+
|
|
10
|
+
export const runtime = 'edge'
|
|
11
|
+
export const preferredRegion = 'home'
|
|
12
|
+
|
|
13
|
+
interface SharePageProps {
|
|
14
|
+
params: {
|
|
15
|
+
id: string
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function generateMetadata({
|
|
20
|
+
params
|
|
21
|
+
}: SharePageProps): Promise<Metadata> {
|
|
22
|
+
const chat = await getSharedChat(params.id)
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
title: chat?.title.slice(0, 50) ?? 'Chat'
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default async function SharePage({ params }: SharePageProps) {
|
|
30
|
+
const chat = await getSharedChat(params.id)
|
|
31
|
+
|
|
32
|
+
if (!chat || !chat?.sharePath) {
|
|
33
|
+
notFound()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const uiState: UIState = getUIStateFromAIState(chat)
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<>
|
|
40
|
+
<div className="flex-1 space-y-6">
|
|
41
|
+
<div className="border-b bg-background px-4 py-6 md:px-6 md:py-8">
|
|
42
|
+
<div className="mx-auto max-w-2xl">
|
|
43
|
+
<div className="space-y-1 md:-mx-8">
|
|
44
|
+
<h1 className="text-2xl font-bold">{chat.title}</h1>
|
|
45
|
+
<div className="text-sm text-muted-foreground">
|
|
46
|
+
{formatDate(chat.createdAt)} · {chat.messages.length} messages
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
<AI>
|
|
52
|
+
<ChatList messages={uiState} isShared={true} />
|
|
53
|
+
</AI>
|
|
54
|
+
</div>
|
|
55
|
+
<FooterText className="py-8" />
|
|
56
|
+
</>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
'use server'
|
|
2
|
+
|
|
3
|
+
import { signIn } from '@/auth'
|
|
4
|
+
import { ResultCode, getStringFromBuffer } from '@/lib/utils'
|
|
5
|
+
import { z } from 'zod'
|
|
6
|
+
import { kv } from '@vercel/kv'
|
|
7
|
+
import { getUser } from '../login/actions'
|
|
8
|
+
import { AuthError } from 'next-auth'
|
|
9
|
+
|
|
10
|
+
export async function createUser(
|
|
11
|
+
email: string,
|
|
12
|
+
hashedPassword: string,
|
|
13
|
+
salt: string
|
|
14
|
+
) {
|
|
15
|
+
const existingUser = await getUser(email)
|
|
16
|
+
|
|
17
|
+
if (existingUser) {
|
|
18
|
+
return {
|
|
19
|
+
type: 'error',
|
|
20
|
+
resultCode: ResultCode.UserAlreadyExists
|
|
21
|
+
}
|
|
22
|
+
} else {
|
|
23
|
+
const user = {
|
|
24
|
+
id: crypto.randomUUID(),
|
|
25
|
+
email,
|
|
26
|
+
password: hashedPassword,
|
|
27
|
+
salt
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
await kv.hmset(`user:${email}`, user)
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
type: 'success',
|
|
34
|
+
resultCode: ResultCode.UserCreated
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface Result {
|
|
40
|
+
type: string
|
|
41
|
+
resultCode: ResultCode
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function signup(
|
|
45
|
+
_prevState: Result | undefined,
|
|
46
|
+
formData: FormData
|
|
47
|
+
): Promise<Result | undefined> {
|
|
48
|
+
const email = formData.get('email') as string
|
|
49
|
+
const password = formData.get('password') as string
|
|
50
|
+
|
|
51
|
+
const parsedCredentials = z
|
|
52
|
+
.object({
|
|
53
|
+
email: z.string().email(),
|
|
54
|
+
password: z.string().min(6)
|
|
55
|
+
})
|
|
56
|
+
.safeParse({
|
|
57
|
+
email,
|
|
58
|
+
password
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
if (parsedCredentials.success) {
|
|
62
|
+
const salt = crypto.randomUUID()
|
|
63
|
+
|
|
64
|
+
const encoder = new TextEncoder()
|
|
65
|
+
const saltedPassword = encoder.encode(password + salt)
|
|
66
|
+
const hashedPasswordBuffer = await crypto.subtle.digest(
|
|
67
|
+
'SHA-256',
|
|
68
|
+
saltedPassword
|
|
69
|
+
)
|
|
70
|
+
const hashedPassword = getStringFromBuffer(hashedPasswordBuffer)
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const result = await createUser(email, hashedPassword, salt)
|
|
74
|
+
|
|
75
|
+
if (result.resultCode === ResultCode.UserCreated) {
|
|
76
|
+
await signIn('credentials', {
|
|
77
|
+
email,
|
|
78
|
+
password,
|
|
79
|
+
redirect: false
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return result
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (error instanceof AuthError) {
|
|
86
|
+
switch (error.type) {
|
|
87
|
+
case 'CredentialsSignin':
|
|
88
|
+
return {
|
|
89
|
+
type: 'error',
|
|
90
|
+
resultCode: ResultCode.InvalidCredentials
|
|
91
|
+
}
|
|
92
|
+
default:
|
|
93
|
+
return {
|
|
94
|
+
type: 'error',
|
|
95
|
+
resultCode: ResultCode.UnknownError
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
return {
|
|
100
|
+
type: 'error',
|
|
101
|
+
resultCode: ResultCode.UnknownError
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
return {
|
|
107
|
+
type: 'error',
|
|
108
|
+
resultCode: ResultCode.InvalidCredentials
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { auth } from '@/auth'
|
|
2
|
+
import SignupForm from '@/components/signup-form'
|
|
3
|
+
import { Session } from '@/lib/types'
|
|
4
|
+
import { redirect } from 'next/navigation'
|
|
5
|
+
|
|
6
|
+
export default async function SignupPage() {
|
|
7
|
+
const session = (await auth()) as Session
|
|
8
|
+
|
|
9
|
+
if (session) {
|
|
10
|
+
redirect('/')
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<main className="flex flex-col p-4">
|
|
15
|
+
<SignupForm />
|
|
16
|
+
</main>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { NextAuthConfig } from 'next-auth'
|
|
2
|
+
|
|
3
|
+
export const authConfig = {
|
|
4
|
+
secret: process.env.AUTH_SECRET,
|
|
5
|
+
pages: {
|
|
6
|
+
signIn: '/login',
|
|
7
|
+
newUser: '/signup'
|
|
8
|
+
},
|
|
9
|
+
callbacks: {
|
|
10
|
+
async authorized({ auth, request: { nextUrl } }) {
|
|
11
|
+
const isLoggedIn = !!auth?.user
|
|
12
|
+
const isOnLoginPage = nextUrl.pathname.startsWith('/login')
|
|
13
|
+
const isOnSignupPage = nextUrl.pathname.startsWith('/signup')
|
|
14
|
+
|
|
15
|
+
if (isLoggedIn) {
|
|
16
|
+
if (isOnLoginPage || isOnSignupPage) {
|
|
17
|
+
return Response.redirect(new URL('/', nextUrl))
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return true
|
|
22
|
+
},
|
|
23
|
+
async jwt({ token, user }) {
|
|
24
|
+
if (user) {
|
|
25
|
+
token = { ...token, id: user.id }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return token
|
|
29
|
+
},
|
|
30
|
+
async session({ session, token }) {
|
|
31
|
+
if (token) {
|
|
32
|
+
const { id } = token as { id: string }
|
|
33
|
+
const { user } = session
|
|
34
|
+
|
|
35
|
+
session = { ...session, user: { ...user, id } }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return session
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
providers: []
|
|
42
|
+
} satisfies NextAuthConfig
|
package/example/auth.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import NextAuth from 'next-auth'
|
|
2
|
+
import Credentials from 'next-auth/providers/credentials'
|
|
3
|
+
import { authConfig } from './auth.config'
|
|
4
|
+
import { z } from 'zod'
|
|
5
|
+
import { getStringFromBuffer } from './lib/utils'
|
|
6
|
+
import { getUser } from './app/login/actions'
|
|
7
|
+
|
|
8
|
+
export const { auth, signIn, signOut } = NextAuth({
|
|
9
|
+
...authConfig,
|
|
10
|
+
providers: [
|
|
11
|
+
Credentials({
|
|
12
|
+
async authorize(credentials) {
|
|
13
|
+
const parsedCredentials = z
|
|
14
|
+
.object({
|
|
15
|
+
email: z.string().email(),
|
|
16
|
+
password: z.string().min(6)
|
|
17
|
+
})
|
|
18
|
+
.safeParse(credentials)
|
|
19
|
+
|
|
20
|
+
if (parsedCredentials.success) {
|
|
21
|
+
const { email, password } = parsedCredentials.data
|
|
22
|
+
const user = await getUser(email)
|
|
23
|
+
|
|
24
|
+
if (!user) return null
|
|
25
|
+
|
|
26
|
+
const encoder = new TextEncoder()
|
|
27
|
+
const saltedPassword = encoder.encode(password + user.salt)
|
|
28
|
+
const hashedPasswordBuffer = await crypto.subtle.digest(
|
|
29
|
+
'SHA-256',
|
|
30
|
+
saltedPassword
|
|
31
|
+
)
|
|
32
|
+
const hashedPassword = getStringFromBuffer(hashedPasswordBuffer)
|
|
33
|
+
|
|
34
|
+
if (hashedPassword === user.password) {
|
|
35
|
+
return user
|
|
36
|
+
} else {
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
]
|
|
45
|
+
})
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
|
|
5
|
+
import { cn } from '@/lib/utils'
|
|
6
|
+
import { Button, type ButtonProps } from '@/components/ui/button'
|
|
7
|
+
import { IconArrowDown } from '@/components/ui/icons'
|
|
8
|
+
|
|
9
|
+
interface ButtonScrollToBottomProps extends ButtonProps {
|
|
10
|
+
isAtBottom: boolean
|
|
11
|
+
scrollToBottom: () => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function ButtonScrollToBottom({
|
|
15
|
+
className,
|
|
16
|
+
isAtBottom,
|
|
17
|
+
scrollToBottom,
|
|
18
|
+
...props
|
|
19
|
+
}: ButtonScrollToBottomProps) {
|
|
20
|
+
return (
|
|
21
|
+
<Button
|
|
22
|
+
variant="outline"
|
|
23
|
+
size="icon"
|
|
24
|
+
className={cn(
|
|
25
|
+
'absolute right-4 top-1 z-10 bg-background transition-opacity duration-300 sm:right-8 md:top-2',
|
|
26
|
+
isAtBottom ? 'opacity-0' : 'opacity-100',
|
|
27
|
+
className
|
|
28
|
+
)}
|
|
29
|
+
onClick={() => scrollToBottom()}
|
|
30
|
+
{...props}
|
|
31
|
+
>
|
|
32
|
+
<IconArrowDown />
|
|
33
|
+
<span className="sr-only">Scroll to bottom</span>
|
|
34
|
+
</Button>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
|
|
3
|
+
import Link from 'next/link'
|
|
4
|
+
|
|
5
|
+
import { cn } from '@/lib/utils'
|
|
6
|
+
import { SidebarList } from '@/components/sidebar-list'
|
|
7
|
+
import { buttonVariants } from '@/components/ui/button'
|
|
8
|
+
import { IconPlus } from '@/components/ui/icons'
|
|
9
|
+
|
|
10
|
+
interface ChatHistoryProps {
|
|
11
|
+
userId?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function ChatHistory({ userId }: ChatHistoryProps) {
|
|
15
|
+
return (
|
|
16
|
+
<div className="flex flex-col h-full">
|
|
17
|
+
<div className="flex items-center justify-between p-4">
|
|
18
|
+
<h4 className="text-sm font-medium">Chat History</h4>
|
|
19
|
+
</div>
|
|
20
|
+
<div className="mb-2 px-2">
|
|
21
|
+
<Link
|
|
22
|
+
href="/"
|
|
23
|
+
className={cn(
|
|
24
|
+
buttonVariants({ variant: 'outline' }),
|
|
25
|
+
'h-10 w-full justify-start bg-zinc-50 px-4 shadow-none transition-colors hover:bg-zinc-200/40 dark:bg-zinc-900 dark:hover:bg-zinc-300/10'
|
|
26
|
+
)}
|
|
27
|
+
>
|
|
28
|
+
<IconPlus className="-translate-x-2 stroke-2" />
|
|
29
|
+
New Chat
|
|
30
|
+
</Link>
|
|
31
|
+
</div>
|
|
32
|
+
<React.Suspense
|
|
33
|
+
fallback={
|
|
34
|
+
<div className="flex flex-col flex-1 px-4 space-y-4 overflow-auto">
|
|
35
|
+
{Array.from({ length: 10 }).map((_, i) => (
|
|
36
|
+
<div
|
|
37
|
+
key={i}
|
|
38
|
+
className="w-full h-6 rounded-md shrink-0 animate-pulse bg-zinc-200 dark:bg-zinc-800"
|
|
39
|
+
/>
|
|
40
|
+
))}
|
|
41
|
+
</div>
|
|
42
|
+
}
|
|
43
|
+
>
|
|
44
|
+
{/* @ts-ignore */}
|
|
45
|
+
<SidebarList userId={userId} />
|
|
46
|
+
</React.Suspense>
|
|
47
|
+
</div>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Separator } from '@/components/ui/separator'
|
|
2
|
+
import { UIState } from '@/lib/chat/actions'
|
|
3
|
+
import { Session } from '@/lib/types'
|
|
4
|
+
import Link from 'next/link'
|
|
5
|
+
import { ExclamationTriangleIcon } from '@radix-ui/react-icons'
|
|
6
|
+
|
|
7
|
+
export interface ChatList {
|
|
8
|
+
messages: UIState
|
|
9
|
+
session?: Session
|
|
10
|
+
isShared: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function ChatList({ messages, session, isShared }: ChatList) {
|
|
14
|
+
if (!messages.length) {
|
|
15
|
+
return null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className="relative mx-auto max-w-2xl px-4">
|
|
20
|
+
{!isShared && !session ? (
|
|
21
|
+
<>
|
|
22
|
+
<div className="group relative mb-4 flex items-start md:-ml-12">
|
|
23
|
+
<div className="bg-background flex size-[25px] shrink-0 select-none items-center justify-center rounded-md border shadow-sm">
|
|
24
|
+
<ExclamationTriangleIcon />
|
|
25
|
+
</div>
|
|
26
|
+
<div className="ml-4 flex-1 space-y-2 overflow-hidden px-1">
|
|
27
|
+
<p className="text-muted-foreground leading-normal">
|
|
28
|
+
Please{' '}
|
|
29
|
+
<Link href="/login" className="underline">
|
|
30
|
+
log in
|
|
31
|
+
</Link>{' '}
|
|
32
|
+
or{' '}
|
|
33
|
+
<Link href="/signup" className="underline">
|
|
34
|
+
sign up
|
|
35
|
+
</Link>{' '}
|
|
36
|
+
to save and revisit your chat history!
|
|
37
|
+
</p>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
<Separator className="my-4" />
|
|
41
|
+
</>
|
|
42
|
+
) : null}
|
|
43
|
+
|
|
44
|
+
{messages.map((message, index) => (
|
|
45
|
+
<div key={message.id}>
|
|
46
|
+
{message.display}
|
|
47
|
+
{index < messages.length - 1 && <Separator className="my-4" />}
|
|
48
|
+
</div>
|
|
49
|
+
))}
|
|
50
|
+
</div>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { type Message } from 'ai'
|
|
4
|
+
|
|
5
|
+
import { Button } from '@/components/ui/button'
|
|
6
|
+
import { IconCheck, IconCopy } from '@/components/ui/icons'
|
|
7
|
+
import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard'
|
|
8
|
+
import { cn } from '@/lib/utils'
|
|
9
|
+
|
|
10
|
+
interface ChatMessageActionsProps extends React.ComponentProps<'div'> {
|
|
11
|
+
message: Message
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function ChatMessageActions({
|
|
15
|
+
message,
|
|
16
|
+
className,
|
|
17
|
+
...props
|
|
18
|
+
}: ChatMessageActionsProps) {
|
|
19
|
+
const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 })
|
|
20
|
+
|
|
21
|
+
const onCopy = () => {
|
|
22
|
+
if (isCopied) return
|
|
23
|
+
copyToClipboard(message.content)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div
|
|
28
|
+
className={cn(
|
|
29
|
+
'flex items-center justify-end transition-opacity group-hover:opacity-100 md:absolute md:-right-10 md:-top-2 md:opacity-0',
|
|
30
|
+
className
|
|
31
|
+
)}
|
|
32
|
+
{...props}
|
|
33
|
+
>
|
|
34
|
+
<Button variant="ghost" size="icon" onClick={onCopy}>
|
|
35
|
+
{isCopied ? <IconCheck /> : <IconCopy />}
|
|
36
|
+
<span className="sr-only">Copy message</span>
|
|
37
|
+
</Button>
|
|
38
|
+
</div>
|
|
39
|
+
)
|
|
40
|
+
}
|