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.
Files changed (132) hide show
  1. package/.eslintrc.cjs +37 -0
  2. package/README.md +3 -0
  3. package/dist/chunk-GOA2HL4A.mjs +269 -0
  4. package/dist/chunk-GOA2HL4A.mjs.map +1 -0
  5. package/dist/index.d.mts +82 -0
  6. package/dist/index.d.ts +82 -0
  7. package/dist/index.js +940 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/index.mjs +666 -0
  10. package/dist/index.mjs.map +1 -0
  11. package/dist/utils-s3gGR6vj.d.mts +209 -0
  12. package/dist/utils-s3gGR6vj.d.ts +209 -0
  13. package/dist/utils.d.mts +3 -0
  14. package/dist/utils.d.ts +3 -0
  15. package/dist/utils.js +263 -0
  16. package/dist/utils.js.map +1 -0
  17. package/dist/utils.mjs +7 -0
  18. package/dist/utils.mjs.map +1 -0
  19. package/example/.env.example +12 -0
  20. package/example/.eslintrc.json +26 -0
  21. package/example/LICENSE +13 -0
  22. package/example/README.md +10 -0
  23. package/example/app/(chat)/chat/[id]/page.tsx +60 -0
  24. package/example/app/(chat)/layout.tsx +14 -0
  25. package/example/app/(chat)/page.tsx +22 -0
  26. package/example/app/actions.ts +156 -0
  27. package/example/app/globals.css +76 -0
  28. package/example/app/layout.tsx +64 -0
  29. package/example/app/login/actions.ts +71 -0
  30. package/example/app/login/page.tsx +18 -0
  31. package/example/app/new/page.tsx +5 -0
  32. package/example/app/opengraph-image.png +0 -0
  33. package/example/app/share/[id]/page.tsx +58 -0
  34. package/example/app/signup/actions.ts +111 -0
  35. package/example/app/signup/page.tsx +18 -0
  36. package/example/app/twitter-image.png +0 -0
  37. package/example/auth.config.ts +42 -0
  38. package/example/auth.ts +45 -0
  39. package/example/components/button-scroll-to-bottom.tsx +36 -0
  40. package/example/components/chat-history.tsx +49 -0
  41. package/example/components/chat-list.tsx +52 -0
  42. package/example/components/chat-message-actions.tsx +40 -0
  43. package/example/components/chat-message.tsx +80 -0
  44. package/example/components/chat-panel.tsx +139 -0
  45. package/example/components/chat-share-dialog.tsx +95 -0
  46. package/example/components/chat.tsx +84 -0
  47. package/example/components/clear-history.tsx +75 -0
  48. package/example/components/empty-screen.tsx +38 -0
  49. package/example/components/external-link.tsx +29 -0
  50. package/example/components/footer.tsx +19 -0
  51. package/example/components/header.tsx +80 -0
  52. package/example/components/login-button.tsx +42 -0
  53. package/example/components/login-form.tsx +97 -0
  54. package/example/components/markdown.tsx +9 -0
  55. package/example/components/prompt-form.tsx +115 -0
  56. package/example/components/providers.tsx +17 -0
  57. package/example/components/sidebar-actions.tsx +125 -0
  58. package/example/components/sidebar-desktop.tsx +19 -0
  59. package/example/components/sidebar-footer.tsx +16 -0
  60. package/example/components/sidebar-item.tsx +124 -0
  61. package/example/components/sidebar-items.tsx +42 -0
  62. package/example/components/sidebar-list.tsx +38 -0
  63. package/example/components/sidebar-mobile.tsx +31 -0
  64. package/example/components/sidebar-toggle.tsx +24 -0
  65. package/example/components/sidebar.tsx +21 -0
  66. package/example/components/signup-form.tsx +95 -0
  67. package/example/components/stocks/events-skeleton.tsx +31 -0
  68. package/example/components/stocks/events.tsx +30 -0
  69. package/example/components/stocks/index.tsx +36 -0
  70. package/example/components/stocks/message.tsx +134 -0
  71. package/example/components/stocks/spinner.tsx +16 -0
  72. package/example/components/stocks/stock-purchase.tsx +146 -0
  73. package/example/components/stocks/stock-skeleton.tsx +22 -0
  74. package/example/components/stocks/stock.tsx +210 -0
  75. package/example/components/stocks/stocks-skeleton.tsx +9 -0
  76. package/example/components/stocks/stocks.tsx +67 -0
  77. package/example/components/tailwind-indicator.tsx +14 -0
  78. package/example/components/theme-toggle.tsx +31 -0
  79. package/example/components/ui/alert-dialog.tsx +141 -0
  80. package/example/components/ui/badge.tsx +36 -0
  81. package/example/components/ui/button.tsx +57 -0
  82. package/example/components/ui/codeblock.tsx +148 -0
  83. package/example/components/ui/dialog.tsx +122 -0
  84. package/example/components/ui/dropdown-menu.tsx +205 -0
  85. package/example/components/ui/icons.tsx +507 -0
  86. package/example/components/ui/input.tsx +25 -0
  87. package/example/components/ui/label.tsx +26 -0
  88. package/example/components/ui/select.tsx +164 -0
  89. package/example/components/ui/separator.tsx +31 -0
  90. package/example/components/ui/sheet.tsx +140 -0
  91. package/example/components/ui/sonner.tsx +31 -0
  92. package/example/components/ui/switch.tsx +29 -0
  93. package/example/components/ui/textarea.tsx +24 -0
  94. package/example/components/ui/tooltip.tsx +30 -0
  95. package/example/components/user-menu.tsx +53 -0
  96. package/example/components.json +17 -0
  97. package/example/lib/chat/actions.tsx +606 -0
  98. package/example/lib/hooks/use-copy-to-clipboard.tsx +33 -0
  99. package/example/lib/hooks/use-enter-submit.tsx +23 -0
  100. package/example/lib/hooks/use-local-storage.ts +24 -0
  101. package/example/lib/hooks/use-scroll-anchor.tsx +86 -0
  102. package/example/lib/hooks/use-sidebar.tsx +60 -0
  103. package/example/lib/hooks/use-streamable-text.ts +25 -0
  104. package/example/lib/types.ts +41 -0
  105. package/example/lib/utils.ts +89 -0
  106. package/example/middleware.ts +8 -0
  107. package/example/next-env.d.ts +5 -0
  108. package/example/next.config.js +13 -0
  109. package/example/package-lock.json +9249 -0
  110. package/example/package.json +77 -0
  111. package/example/pnpm-lock.yaml +5712 -0
  112. package/example/postcss.config.js +6 -0
  113. package/example/prettier.config.cjs +34 -0
  114. package/example/public/apple-touch-icon.png +0 -0
  115. package/example/public/favicon-16x16.png +0 -0
  116. package/example/public/favicon.ico +0 -0
  117. package/example/public/next.svg +1 -0
  118. package/example/public/thirteen.svg +1 -0
  119. package/example/public/vercel.svg +1 -0
  120. package/example/tailwind.config.ts +81 -0
  121. package/example/tsconfig.json +35 -0
  122. package/package.json +45 -0
  123. package/src/helpers.ts +64 -0
  124. package/src/index.test.ts +255 -0
  125. package/src/index.ts +397 -0
  126. package/src/server/types/.gitkeep +0 -0
  127. package/src/types.ts +69 -0
  128. package/src/utils.ts +134 -0
  129. package/ts-to-zod.config.js +18 -0
  130. package/tsconfig.json +32 -0
  131. package/tsup.config.ts +10 -0
  132. 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
+ }
@@ -0,0 +1,5 @@
1
+ import { redirect } from 'next/navigation'
2
+
3
+ export default async function NewPage() {
4
+ redirect('/')
5
+ }
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
@@ -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
+ }