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,80 @@
|
|
|
1
|
+
// Inspired by Chatbot-UI and modified to fit the needs of this project
|
|
2
|
+
// @see https://github.com/mckaywrigley/chatbot-ui/blob/main/components/Chat/ChatMessage.tsx
|
|
3
|
+
|
|
4
|
+
import { Message } from 'ai'
|
|
5
|
+
import remarkGfm from 'remark-gfm'
|
|
6
|
+
import remarkMath from 'remark-math'
|
|
7
|
+
|
|
8
|
+
import { cn } from '@/lib/utils'
|
|
9
|
+
import { CodeBlock } from '@/components/ui/codeblock'
|
|
10
|
+
import { MemoizedReactMarkdown } from '@/components/markdown'
|
|
11
|
+
import { IconOpenAI, IconUser } from '@/components/ui/icons'
|
|
12
|
+
import { ChatMessageActions } from '@/components/chat-message-actions'
|
|
13
|
+
|
|
14
|
+
export interface ChatMessageProps {
|
|
15
|
+
message: Message
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function ChatMessage({ message, ...props }: ChatMessageProps) {
|
|
19
|
+
return (
|
|
20
|
+
<div
|
|
21
|
+
className={cn('group relative mb-4 flex items-start md:-ml-12')}
|
|
22
|
+
{...props}
|
|
23
|
+
>
|
|
24
|
+
<div
|
|
25
|
+
className={cn(
|
|
26
|
+
'flex size-8 shrink-0 select-none items-center justify-center rounded-md border shadow',
|
|
27
|
+
message.role === 'user'
|
|
28
|
+
? 'bg-background'
|
|
29
|
+
: 'bg-primary text-primary-foreground'
|
|
30
|
+
)}
|
|
31
|
+
>
|
|
32
|
+
{message.role === 'user' ? <IconUser /> : <IconOpenAI />}
|
|
33
|
+
</div>
|
|
34
|
+
<div className="flex-1 px-1 ml-4 space-y-2 overflow-hidden">
|
|
35
|
+
<MemoizedReactMarkdown
|
|
36
|
+
className="prose break-words dark:prose-invert prose-p:leading-relaxed prose-pre:p-0"
|
|
37
|
+
remarkPlugins={[remarkGfm, remarkMath]}
|
|
38
|
+
components={{
|
|
39
|
+
p({ children }) {
|
|
40
|
+
return <p className="mb-2 last:mb-0">{children}</p>
|
|
41
|
+
},
|
|
42
|
+
code({ node, inline, className, children, ...props }) {
|
|
43
|
+
if (children.length) {
|
|
44
|
+
if (children[0] == '▍') {
|
|
45
|
+
return (
|
|
46
|
+
<span className="mt-1 cursor-default animate-pulse">▍</span>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
children[0] = (children[0] as string).replace('`▍`', '▍')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const match = /language-(\w+)/.exec(className || '')
|
|
54
|
+
|
|
55
|
+
if (inline) {
|
|
56
|
+
return (
|
|
57
|
+
<code className={className} {...props}>
|
|
58
|
+
{children}
|
|
59
|
+
</code>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<CodeBlock
|
|
65
|
+
key={Math.random()}
|
|
66
|
+
language={(match && match[1]) || ''}
|
|
67
|
+
value={String(children).replace(/\n$/, '')}
|
|
68
|
+
{...props}
|
|
69
|
+
/>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
}}
|
|
73
|
+
>
|
|
74
|
+
{message.content}
|
|
75
|
+
</MemoizedReactMarkdown>
|
|
76
|
+
<ChatMessageActions message={message} />
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
|
|
3
|
+
import { shareChat } from '@/app/actions'
|
|
4
|
+
import { Button } from '@/components/ui/button'
|
|
5
|
+
import { PromptForm } from '@/components/prompt-form'
|
|
6
|
+
import { ButtonScrollToBottom } from '@/components/button-scroll-to-bottom'
|
|
7
|
+
import { IconShare } from '@/components/ui/icons'
|
|
8
|
+
import { FooterText } from '@/components/footer'
|
|
9
|
+
import { ChatShareDialog } from '@/components/chat-share-dialog'
|
|
10
|
+
import { useAIState, useActions, useUIState } from 'ai/rsc'
|
|
11
|
+
import type { AI } from '@/lib/chat/actions'
|
|
12
|
+
import { nanoid } from 'nanoid'
|
|
13
|
+
import { UserMessage } from './stocks/message'
|
|
14
|
+
|
|
15
|
+
export interface ChatPanelProps {
|
|
16
|
+
id?: string
|
|
17
|
+
title?: string
|
|
18
|
+
input: string
|
|
19
|
+
setInput: (value: string) => void
|
|
20
|
+
isAtBottom: boolean
|
|
21
|
+
scrollToBottom: () => void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function ChatPanel({
|
|
25
|
+
id,
|
|
26
|
+
title,
|
|
27
|
+
input,
|
|
28
|
+
setInput,
|
|
29
|
+
isAtBottom,
|
|
30
|
+
scrollToBottom
|
|
31
|
+
}: ChatPanelProps) {
|
|
32
|
+
const [aiState] = useAIState()
|
|
33
|
+
const [messages, setMessages] = useUIState<typeof AI>()
|
|
34
|
+
const { submitUserMessage } = useActions()
|
|
35
|
+
const [shareDialogOpen, setShareDialogOpen] = React.useState(false)
|
|
36
|
+
|
|
37
|
+
const exampleMessages = [
|
|
38
|
+
{
|
|
39
|
+
heading: 'What are the',
|
|
40
|
+
subheading: 'trending memecoins today?',
|
|
41
|
+
message: `What are the trending memecoins today?`
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
heading: 'What is the price of',
|
|
45
|
+
subheading: '$DOGE right now?',
|
|
46
|
+
message: 'What is the price of $DOGE right now?'
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
heading: 'I would like to buy',
|
|
50
|
+
subheading: '42 $DOGE',
|
|
51
|
+
message: `I would like to buy 42 $DOGE`
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
heading: 'What are some',
|
|
55
|
+
subheading: `recent events about $DOGE?`,
|
|
56
|
+
message: `What are some recent events about $DOGE?`
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className="fixed inset-x-0 bottom-0 w-full bg-gradient-to-b from-muted/30 from-0% to-muted/30 to-50% duration-300 ease-in-out animate-in dark:from-background/10 dark:from-10% dark:to-background/80 peer-[[data-state=open]]:group-[]:lg:pl-[250px] peer-[[data-state=open]]:group-[]:xl:pl-[300px]">
|
|
62
|
+
<ButtonScrollToBottom
|
|
63
|
+
isAtBottom={isAtBottom}
|
|
64
|
+
scrollToBottom={scrollToBottom}
|
|
65
|
+
/>
|
|
66
|
+
|
|
67
|
+
<div className="mx-auto sm:max-w-2xl sm:px-4">
|
|
68
|
+
<div className="mb-4 grid grid-cols-2 gap-2 px-4 sm:px-0">
|
|
69
|
+
{messages.length === 0 &&
|
|
70
|
+
exampleMessages.map((example, index) => (
|
|
71
|
+
<div
|
|
72
|
+
key={example.heading}
|
|
73
|
+
className={`cursor-pointer rounded-lg border bg-white p-4 hover:bg-zinc-50 dark:bg-zinc-950 dark:hover:bg-zinc-900 ${
|
|
74
|
+
index > 1 && 'hidden md:block'
|
|
75
|
+
}`}
|
|
76
|
+
onClick={async () => {
|
|
77
|
+
setMessages(currentMessages => [
|
|
78
|
+
...currentMessages,
|
|
79
|
+
{
|
|
80
|
+
id: nanoid(),
|
|
81
|
+
display: <UserMessage>{example.message}</UserMessage>
|
|
82
|
+
}
|
|
83
|
+
])
|
|
84
|
+
|
|
85
|
+
const responseMessage = await submitUserMessage(
|
|
86
|
+
example.message
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
setMessages(currentMessages => [
|
|
90
|
+
...currentMessages,
|
|
91
|
+
responseMessage
|
|
92
|
+
])
|
|
93
|
+
}}
|
|
94
|
+
>
|
|
95
|
+
<div className="text-sm font-semibold">{example.heading}</div>
|
|
96
|
+
<div className="text-sm text-zinc-600">
|
|
97
|
+
{example.subheading}
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
))}
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
{messages?.length >= 2 ? (
|
|
104
|
+
<div className="flex h-12 items-center justify-center">
|
|
105
|
+
<div className="flex space-x-2">
|
|
106
|
+
{id && title ? (
|
|
107
|
+
<>
|
|
108
|
+
<Button
|
|
109
|
+
variant="outline"
|
|
110
|
+
onClick={() => setShareDialogOpen(true)}
|
|
111
|
+
>
|
|
112
|
+
<IconShare className="mr-2" />
|
|
113
|
+
Share
|
|
114
|
+
</Button>
|
|
115
|
+
<ChatShareDialog
|
|
116
|
+
open={shareDialogOpen}
|
|
117
|
+
onOpenChange={setShareDialogOpen}
|
|
118
|
+
onCopy={() => setShareDialogOpen(false)}
|
|
119
|
+
shareChat={shareChat}
|
|
120
|
+
chat={{
|
|
121
|
+
id,
|
|
122
|
+
title,
|
|
123
|
+
messages: aiState.messages
|
|
124
|
+
}}
|
|
125
|
+
/>
|
|
126
|
+
</>
|
|
127
|
+
) : null}
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
) : null}
|
|
131
|
+
|
|
132
|
+
<div className="space-y-4 border-t bg-background px-4 py-2 shadow-lg sm:rounded-t-xl sm:border md:py-4">
|
|
133
|
+
<PromptForm input={input} setInput={setInput} />
|
|
134
|
+
<FooterText className="hidden sm:block" />
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
)
|
|
139
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { type DialogProps } from '@radix-ui/react-dialog'
|
|
5
|
+
import { toast } from 'sonner'
|
|
6
|
+
|
|
7
|
+
import { ServerActionResult, type Chat } from '@/lib/types'
|
|
8
|
+
import { Button } from '@/components/ui/button'
|
|
9
|
+
import {
|
|
10
|
+
Dialog,
|
|
11
|
+
DialogContent,
|
|
12
|
+
DialogDescription,
|
|
13
|
+
DialogFooter,
|
|
14
|
+
DialogHeader,
|
|
15
|
+
DialogTitle
|
|
16
|
+
} from '@/components/ui/dialog'
|
|
17
|
+
import { IconSpinner } from '@/components/ui/icons'
|
|
18
|
+
import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard'
|
|
19
|
+
|
|
20
|
+
interface ChatShareDialogProps extends DialogProps {
|
|
21
|
+
chat: Pick<Chat, 'id' | 'title' | 'messages'>
|
|
22
|
+
shareChat: (id: string) => ServerActionResult<Chat>
|
|
23
|
+
onCopy: () => void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function ChatShareDialog({
|
|
27
|
+
chat,
|
|
28
|
+
shareChat,
|
|
29
|
+
onCopy,
|
|
30
|
+
...props
|
|
31
|
+
}: ChatShareDialogProps) {
|
|
32
|
+
const { copyToClipboard } = useCopyToClipboard({ timeout: 1000 })
|
|
33
|
+
const [isSharePending, startShareTransition] = React.useTransition()
|
|
34
|
+
|
|
35
|
+
const copyShareLink = React.useCallback(
|
|
36
|
+
async (chat: Chat) => {
|
|
37
|
+
if (!chat.sharePath) {
|
|
38
|
+
return toast.error('Could not copy share link to clipboard')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const url = new URL(window.location.href)
|
|
42
|
+
url.pathname = chat.sharePath
|
|
43
|
+
copyToClipboard(url.toString())
|
|
44
|
+
onCopy()
|
|
45
|
+
toast.success('Share link copied to clipboard')
|
|
46
|
+
},
|
|
47
|
+
[copyToClipboard, onCopy]
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<Dialog {...props}>
|
|
52
|
+
<DialogContent>
|
|
53
|
+
<DialogHeader>
|
|
54
|
+
<DialogTitle>Share link to chat</DialogTitle>
|
|
55
|
+
<DialogDescription>
|
|
56
|
+
Anyone with the URL will be able to view the shared chat.
|
|
57
|
+
</DialogDescription>
|
|
58
|
+
</DialogHeader>
|
|
59
|
+
<div className="p-4 space-y-1 text-sm border rounded-md">
|
|
60
|
+
<div className="font-medium">{chat.title}</div>
|
|
61
|
+
<div className="text-muted-foreground">
|
|
62
|
+
{chat.messages.length} messages
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
<DialogFooter className="items-center">
|
|
66
|
+
<Button
|
|
67
|
+
disabled={isSharePending}
|
|
68
|
+
onClick={() => {
|
|
69
|
+
// @ts-ignore
|
|
70
|
+
startShareTransition(async () => {
|
|
71
|
+
const result = await shareChat(chat.id)
|
|
72
|
+
|
|
73
|
+
if (result && 'error' in result) {
|
|
74
|
+
toast.error(result.error)
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
copyShareLink(result)
|
|
79
|
+
})
|
|
80
|
+
}}
|
|
81
|
+
>
|
|
82
|
+
{isSharePending ? (
|
|
83
|
+
<>
|
|
84
|
+
<IconSpinner className="mr-2 animate-spin" />
|
|
85
|
+
Copying...
|
|
86
|
+
</>
|
|
87
|
+
) : (
|
|
88
|
+
<>Copy link</>
|
|
89
|
+
)}
|
|
90
|
+
</Button>
|
|
91
|
+
</DialogFooter>
|
|
92
|
+
</DialogContent>
|
|
93
|
+
</Dialog>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { cn } from '@/lib/utils'
|
|
4
|
+
import { ChatList } from '@/components/chat-list'
|
|
5
|
+
import { ChatPanel } from '@/components/chat-panel'
|
|
6
|
+
import { EmptyScreen } from '@/components/empty-screen'
|
|
7
|
+
import { useLocalStorage } from '@/lib/hooks/use-local-storage'
|
|
8
|
+
import { useEffect, useState } from 'react'
|
|
9
|
+
import { useUIState, useAIState } from 'ai/rsc'
|
|
10
|
+
import { Message, Session } from '@/lib/types'
|
|
11
|
+
import { usePathname, useRouter } from 'next/navigation'
|
|
12
|
+
import { useScrollAnchor } from '@/lib/hooks/use-scroll-anchor'
|
|
13
|
+
import { toast } from 'sonner'
|
|
14
|
+
|
|
15
|
+
export interface ChatProps extends React.ComponentProps<'div'> {
|
|
16
|
+
initialMessages?: Message[]
|
|
17
|
+
id?: string
|
|
18
|
+
session?: Session
|
|
19
|
+
missingKeys: string[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function Chat({ id, className, session, missingKeys }: ChatProps) {
|
|
23
|
+
const router = useRouter()
|
|
24
|
+
const path = usePathname()
|
|
25
|
+
const [input, setInput] = useState('')
|
|
26
|
+
const [messages] = useUIState()
|
|
27
|
+
const [aiState] = useAIState()
|
|
28
|
+
|
|
29
|
+
const [_, setNewChatId] = useLocalStorage('newChatId', id)
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (session?.user) {
|
|
33
|
+
if (!path.includes('chat') && messages.length === 1) {
|
|
34
|
+
window.history.replaceState({}, '', `/chat/${id}`)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}, [id, path, session?.user, messages])
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
const messagesLength = aiState.messages?.length
|
|
41
|
+
if (messagesLength === 2) {
|
|
42
|
+
router.refresh()
|
|
43
|
+
}
|
|
44
|
+
}, [aiState.messages, router])
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
setNewChatId(id)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
missingKeys.map(key => {
|
|
52
|
+
toast.error(`Missing ${key} environment variable!`)
|
|
53
|
+
})
|
|
54
|
+
}, [missingKeys])
|
|
55
|
+
|
|
56
|
+
const { messagesRef, scrollRef, visibilityRef, isAtBottom, scrollToBottom } =
|
|
57
|
+
useScrollAnchor()
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div
|
|
61
|
+
className="group w-full overflow-auto pl-0 peer-[[data-state=open]]:lg:pl-[250px] peer-[[data-state=open]]:xl:pl-[300px]"
|
|
62
|
+
ref={scrollRef}
|
|
63
|
+
>
|
|
64
|
+
<div
|
|
65
|
+
className={cn('pb-[200px] pt-4 md:pt-10', className)}
|
|
66
|
+
ref={messagesRef}
|
|
67
|
+
>
|
|
68
|
+
{messages.length ? (
|
|
69
|
+
<ChatList messages={messages} isShared={false} session={session} />
|
|
70
|
+
) : (
|
|
71
|
+
<EmptyScreen />
|
|
72
|
+
)}
|
|
73
|
+
<div className="w-full h-px" ref={visibilityRef} />
|
|
74
|
+
</div>
|
|
75
|
+
<ChatPanel
|
|
76
|
+
id={id}
|
|
77
|
+
input={input}
|
|
78
|
+
setInput={setInput}
|
|
79
|
+
isAtBottom={isAtBottom}
|
|
80
|
+
scrollToBottom={scrollToBottom}
|
|
81
|
+
/>
|
|
82
|
+
</div>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { useRouter } from 'next/navigation'
|
|
5
|
+
import { toast } from 'sonner'
|
|
6
|
+
|
|
7
|
+
import { ServerActionResult } from '@/lib/types'
|
|
8
|
+
import { Button } from '@/components/ui/button'
|
|
9
|
+
import {
|
|
10
|
+
AlertDialog,
|
|
11
|
+
AlertDialogAction,
|
|
12
|
+
AlertDialogCancel,
|
|
13
|
+
AlertDialogContent,
|
|
14
|
+
AlertDialogDescription,
|
|
15
|
+
AlertDialogFooter,
|
|
16
|
+
AlertDialogHeader,
|
|
17
|
+
AlertDialogTitle,
|
|
18
|
+
AlertDialogTrigger
|
|
19
|
+
} from '@/components/ui/alert-dialog'
|
|
20
|
+
import { IconSpinner } from '@/components/ui/icons'
|
|
21
|
+
|
|
22
|
+
interface ClearHistoryProps {
|
|
23
|
+
isEnabled: boolean
|
|
24
|
+
clearChats: () => ServerActionResult<void>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function ClearHistory({
|
|
28
|
+
isEnabled = false,
|
|
29
|
+
clearChats
|
|
30
|
+
}: ClearHistoryProps) {
|
|
31
|
+
const [open, setOpen] = React.useState(false)
|
|
32
|
+
const [isPending, startTransition] = React.useTransition()
|
|
33
|
+
const router = useRouter()
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<AlertDialog open={open} onOpenChange={setOpen}>
|
|
37
|
+
<AlertDialogTrigger asChild>
|
|
38
|
+
<Button variant="ghost" disabled={!isEnabled || isPending}>
|
|
39
|
+
{isPending && <IconSpinner className="mr-2" />}
|
|
40
|
+
Clear history
|
|
41
|
+
</Button>
|
|
42
|
+
</AlertDialogTrigger>
|
|
43
|
+
<AlertDialogContent>
|
|
44
|
+
<AlertDialogHeader>
|
|
45
|
+
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
|
46
|
+
<AlertDialogDescription>
|
|
47
|
+
This will permanently delete your chat history and remove your data
|
|
48
|
+
from our servers.
|
|
49
|
+
</AlertDialogDescription>
|
|
50
|
+
</AlertDialogHeader>
|
|
51
|
+
<AlertDialogFooter>
|
|
52
|
+
<AlertDialogCancel disabled={isPending}>Cancel</AlertDialogCancel>
|
|
53
|
+
<AlertDialogAction
|
|
54
|
+
disabled={isPending}
|
|
55
|
+
onClick={event => {
|
|
56
|
+
event.preventDefault()
|
|
57
|
+
startTransition(async () => {
|
|
58
|
+
const result = await clearChats()
|
|
59
|
+
if (result && 'error' in result) {
|
|
60
|
+
toast.error(result.error)
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
setOpen(false)
|
|
65
|
+
})
|
|
66
|
+
}}
|
|
67
|
+
>
|
|
68
|
+
{isPending && <IconSpinner className="mr-2 animate-spin" />}
|
|
69
|
+
Delete
|
|
70
|
+
</AlertDialogAction>
|
|
71
|
+
</AlertDialogFooter>
|
|
72
|
+
</AlertDialogContent>
|
|
73
|
+
</AlertDialog>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { UseChatHelpers } from 'ai/react'
|
|
2
|
+
|
|
3
|
+
import { Button } from '@/components/ui/button'
|
|
4
|
+
import { ExternalLink } from '@/components/external-link'
|
|
5
|
+
import { IconArrowRight } from '@/components/ui/icons'
|
|
6
|
+
|
|
7
|
+
export function EmptyScreen() {
|
|
8
|
+
return (
|
|
9
|
+
<div className="mx-auto max-w-2xl px-4">
|
|
10
|
+
<div className="flex flex-col gap-2 rounded-lg border bg-background p-8">
|
|
11
|
+
<h1 className="text-lg font-semibold">
|
|
12
|
+
Welcome to Next.js AI Chatbot!
|
|
13
|
+
</h1>
|
|
14
|
+
<p className="leading-normal text-muted-foreground">
|
|
15
|
+
This is an open source AI chatbot app template built with{' '}
|
|
16
|
+
<ExternalLink href="https://nextjs.org">Next.js</ExternalLink>, the{' '}
|
|
17
|
+
<ExternalLink href="https://sdk.vercel.ai">
|
|
18
|
+
Vercel AI SDK
|
|
19
|
+
</ExternalLink>
|
|
20
|
+
, and{' '}
|
|
21
|
+
<ExternalLink href="https://vercel.com/storage/kv">
|
|
22
|
+
Vercel KV
|
|
23
|
+
</ExternalLink>
|
|
24
|
+
.
|
|
25
|
+
</p>
|
|
26
|
+
<p className="leading-normal text-muted-foreground">
|
|
27
|
+
It uses{' '}
|
|
28
|
+
<ExternalLink href="https://vercel.com/blog/ai-sdk-3-generative-ui">
|
|
29
|
+
React Server Components
|
|
30
|
+
</ExternalLink>{' '}
|
|
31
|
+
to combine text with generative UI as output of the LLM. The UI state
|
|
32
|
+
is synced through the SDK so the model is aware of your interactions
|
|
33
|
+
as they happen.
|
|
34
|
+
</p>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function ExternalLink({
|
|
2
|
+
href,
|
|
3
|
+
children
|
|
4
|
+
}: {
|
|
5
|
+
href: string
|
|
6
|
+
children: React.ReactNode
|
|
7
|
+
}) {
|
|
8
|
+
return (
|
|
9
|
+
<a
|
|
10
|
+
href={href}
|
|
11
|
+
target="_blank"
|
|
12
|
+
className="inline-flex flex-1 justify-center gap-1 leading-4 hover:underline"
|
|
13
|
+
>
|
|
14
|
+
<span>{children}</span>
|
|
15
|
+
<svg
|
|
16
|
+
aria-hidden="true"
|
|
17
|
+
height="7"
|
|
18
|
+
viewBox="0 0 6 6"
|
|
19
|
+
width="7"
|
|
20
|
+
className="opacity-70"
|
|
21
|
+
>
|
|
22
|
+
<path
|
|
23
|
+
d="M1.25215 5.54731L0.622742 4.9179L3.78169 1.75597H1.3834L1.38936 0.890915H5.27615V4.78069H4.40513L4.41109 2.38538L1.25215 5.54731Z"
|
|
24
|
+
fill="currentColor"
|
|
25
|
+
></path>
|
|
26
|
+
</svg>
|
|
27
|
+
</a>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import { cn } from '@/lib/utils'
|
|
4
|
+
import { ExternalLink } from '@/components/external-link'
|
|
5
|
+
|
|
6
|
+
export function FooterText({ className, ...props }: React.ComponentProps<'p'>) {
|
|
7
|
+
return (
|
|
8
|
+
<p
|
|
9
|
+
className={cn(
|
|
10
|
+
'px-2 text-center text-xs leading-normal text-muted-foreground',
|
|
11
|
+
className
|
|
12
|
+
)}
|
|
13
|
+
{...props}
|
|
14
|
+
>
|
|
15
|
+
TypeScript SDK demo chatbot for{' '}
|
|
16
|
+
<ExternalLink href="https://langwatch.ai">LangWatch</ExternalLink>
|
|
17
|
+
</p>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import Link from 'next/link'
|
|
3
|
+
|
|
4
|
+
import { cn } from '@/lib/utils'
|
|
5
|
+
import { auth } from '@/auth'
|
|
6
|
+
import { Button, buttonVariants } from '@/components/ui/button'
|
|
7
|
+
import {
|
|
8
|
+
IconGitHub,
|
|
9
|
+
IconNextChat,
|
|
10
|
+
IconSeparator,
|
|
11
|
+
IconVercel
|
|
12
|
+
} from '@/components/ui/icons'
|
|
13
|
+
import { UserMenu } from '@/components/user-menu'
|
|
14
|
+
import { SidebarMobile } from './sidebar-mobile'
|
|
15
|
+
import { SidebarToggle } from './sidebar-toggle'
|
|
16
|
+
import { ChatHistory } from './chat-history'
|
|
17
|
+
import { Session } from '@/lib/types'
|
|
18
|
+
|
|
19
|
+
async function UserOrLogin() {
|
|
20
|
+
const session = (await auth()) as Session
|
|
21
|
+
return (
|
|
22
|
+
<>
|
|
23
|
+
{session?.user ? (
|
|
24
|
+
<>
|
|
25
|
+
<SidebarMobile>
|
|
26
|
+
<ChatHistory userId={session.user.id} />
|
|
27
|
+
</SidebarMobile>
|
|
28
|
+
<SidebarToggle />
|
|
29
|
+
</>
|
|
30
|
+
) : (
|
|
31
|
+
<Link href="/new" rel="nofollow">
|
|
32
|
+
<IconNextChat className="size-6 mr-2 dark:hidden" inverted />
|
|
33
|
+
<IconNextChat className="hidden size-6 mr-2 dark:block" />
|
|
34
|
+
</Link>
|
|
35
|
+
)}
|
|
36
|
+
<div className="flex items-center">
|
|
37
|
+
<IconSeparator className="size-6 text-muted-foreground/50" />
|
|
38
|
+
{session?.user ? (
|
|
39
|
+
<UserMenu user={session.user} />
|
|
40
|
+
) : (
|
|
41
|
+
<Button variant="link" asChild className="-ml-2">
|
|
42
|
+
<Link href="/login">Login</Link>
|
|
43
|
+
</Button>
|
|
44
|
+
)}
|
|
45
|
+
</div>
|
|
46
|
+
</>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function Header() {
|
|
51
|
+
return (
|
|
52
|
+
<header className="sticky top-0 z-50 flex items-center justify-between w-full h-16 px-4 border-b shrink-0 bg-gradient-to-b from-background/10 via-background/50 to-background/80 backdrop-blur-xl">
|
|
53
|
+
<div className="flex items-center">
|
|
54
|
+
<React.Suspense fallback={<div className="flex-1 overflow-auto" />}>
|
|
55
|
+
<UserOrLogin />
|
|
56
|
+
</React.Suspense>
|
|
57
|
+
</div>
|
|
58
|
+
<div className="flex items-center justify-end space-x-2">
|
|
59
|
+
<a
|
|
60
|
+
target="_blank"
|
|
61
|
+
href="https://github.com/vercel/nextjs-ai-chatbot/"
|
|
62
|
+
rel="noopener noreferrer"
|
|
63
|
+
className={cn(buttonVariants({ variant: 'outline' }))}
|
|
64
|
+
>
|
|
65
|
+
<IconGitHub />
|
|
66
|
+
<span className="hidden ml-2 md:flex">GitHub</span>
|
|
67
|
+
</a>
|
|
68
|
+
<a
|
|
69
|
+
href="https://vercel.com/templates/Next.js/nextjs-ai-chatbot"
|
|
70
|
+
target="_blank"
|
|
71
|
+
className={cn(buttonVariants())}
|
|
72
|
+
>
|
|
73
|
+
<IconVercel className="mr-2" />
|
|
74
|
+
<span className="hidden sm:block">Deploy to Vercel</span>
|
|
75
|
+
<span className="sm:hidden">Deploy</span>
|
|
76
|
+
</a>
|
|
77
|
+
</div>
|
|
78
|
+
</header>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { signIn } from 'next-auth/react'
|
|
5
|
+
|
|
6
|
+
import { cn } from '@/lib/utils'
|
|
7
|
+
import { Button, type ButtonProps } from '@/components/ui/button'
|
|
8
|
+
import { IconGitHub, IconSpinner } from '@/components/ui/icons'
|
|
9
|
+
|
|
10
|
+
interface LoginButtonProps extends ButtonProps {
|
|
11
|
+
showGithubIcon?: boolean
|
|
12
|
+
text?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function LoginButton({
|
|
16
|
+
text = 'Login with GitHub',
|
|
17
|
+
showGithubIcon = true,
|
|
18
|
+
className,
|
|
19
|
+
...props
|
|
20
|
+
}: LoginButtonProps) {
|
|
21
|
+
const [isLoading, setIsLoading] = React.useState(false)
|
|
22
|
+
return (
|
|
23
|
+
<Button
|
|
24
|
+
variant="outline"
|
|
25
|
+
onClick={() => {
|
|
26
|
+
setIsLoading(true)
|
|
27
|
+
// next-auth signIn() function doesn't work yet at Edge Runtime due to usage of BroadcastChannel
|
|
28
|
+
signIn('github', { callbackUrl: `/` })
|
|
29
|
+
}}
|
|
30
|
+
disabled={isLoading}
|
|
31
|
+
className={cn(className)}
|
|
32
|
+
{...props}
|
|
33
|
+
>
|
|
34
|
+
{isLoading ? (
|
|
35
|
+
<IconSpinner className="mr-2 animate-spin" />
|
|
36
|
+
) : showGithubIcon ? (
|
|
37
|
+
<IconGitHub className="mr-2" />
|
|
38
|
+
) : null}
|
|
39
|
+
{text}
|
|
40
|
+
</Button>
|
|
41
|
+
)
|
|
42
|
+
}
|