diff --git a/app/chat-list.tsx b/app/chat-list.tsx index 7675690..491fa78 100644 --- a/app/chat-list.tsx +++ b/app/chat-list.tsx @@ -3,9 +3,7 @@ import { type Message } from 'ai-connector' import { ChatMessage } from './chat-message' -import { NextChatLogo } from '@/components/ui/nextchat-logo' -import { Plus } from 'lucide-react' -import { EmptyScreen } from './empty' +import { EmptyScreen } from './empty-screen' export interface ChatList { messages: Message[] @@ -13,8 +11,8 @@ export interface ChatList { export function ChatList({ messages }: ChatList) { return ( -
-
+
+ {/*
@@ -26,21 +24,21 @@ export function ChatList({ messages }: ChatList) {
-
-
- {messages.length > 0 ? ( -
- {messages.map(message => ( - - ))} -
- ) : ( +
*/} + {messages.length > 0 ? ( +
+ {messages.map(message => ( + + ))} +
+ ) : ( +
- )} -
+
+ )}
) } diff --git a/app/chat-message.tsx b/app/chat-message.tsx index dab5d07..96aae87 100644 --- a/app/chat-message.tsx +++ b/app/chat-message.tsx @@ -5,115 +5,109 @@ import remarkMath from 'remark-math' import { cn } from '@/lib/utils' import { fontMessage } from '@/lib/fonts' import { Message } from 'ai-connector' +import { User } from 'lucide-react' +import { OpenAI } from '@/components/icons' + export interface ChatMessageProps { message: Message } -export function ChatMessage(props: ChatMessageProps) { +export function ChatMessage({ message, ...props }: ChatMessageProps) { return (
-
-
- {props.message.role === 'user' ? ( -
-
- User -
-
- ) : ( -
- -
- )} - - ▍ - - ) - } - - children[0] = (children[0] as string).replace('`▍`', '▍') +
+ {message.role === 'user' ? ( + + ) : ( + + )} +
+
+ + {children} +

+ ) + }, + code({ node, inline, className, children, ...props }) { + if (children.length) { + if (children[0] == '▍') { + return ( + + ) } - const match = /language-(\w+)/.exec(className || '') - - return !inline ? ( - - ) : ( - - {children} - - ) - }, - table({ children }) { - return ( - - {children} -
- ) - }, - th({ children }) { - return ( - - {children} - - ) - }, - td({ children }) { - return ( - - {children} - - ) + children[0] = (children[0] as string).replace('`▍`', '▍') } - }} - > - {props.message.content} -
-
+ + const match = /language-(\w+)/.exec(className || '') + + return !inline ? ( + + ) : ( + + {children} + + ) + }, + table({ children }) { + return ( + + {children} +
+ ) + }, + th({ children }) { + return ( + + {children} + + ) + }, + td({ children }) { + return ( + + {children} + + ) + } + }} + > + {message.content} +
) } - -ChatMessage.displayName = 'ChatMessage' - -export function IconOpenAI(props: JSX.IntrinsicElements['svg']) { - return ( - - OpenAI icon - - - ) -} diff --git a/app/chat.tsx b/app/chat.tsx index d328db2..71e4e94 100644 --- a/app/chat.tsx +++ b/app/chat.tsx @@ -4,6 +4,7 @@ import { useRouter } from 'next/navigation' import { Prompt } from './prompt' import { useChat, type Message } from 'ai-connector' import { ChatList } from './chat-list' +import { ExternalLink } from '@/app/external-link' export interface ChatProps { // create?: (input: string) => Chat | undefined; @@ -27,24 +28,30 @@ export function Chat({ }) return ( -
-
- +
+ +
+
+ { + append({ + content: value, + role: 'user' + }) + }} + onRefresh={messages.length ? reload : undefined} + isLoading={isLoading} + /> +

+ This is an open source AI chatbot app built with{' '} + Next.js and{' '} + + Vercel KV + + . +

+
-
- { - append({ - content: value, - role: 'user' - }) - }} - onRefresh={messages.length ? reload : undefined} - isLoading={isLoading} - /> -
-
+
) } - -Chat.displayName = 'Chat' diff --git a/app/chat/[id]/page.tsx b/app/chat/[id]/page.tsx index 61094d6..3f1e08e 100644 --- a/app/chat/[id]/page.tsx +++ b/app/chat/[id]/page.tsx @@ -32,6 +32,7 @@ export default async function ChatPage({ params }: ChatPageProps) { return (
+ {/* @ts-ignore */}
diff --git a/app/empty-screen.tsx b/app/empty-screen.tsx new file mode 100644 index 0000000..9b998f5 --- /dev/null +++ b/app/empty-screen.tsx @@ -0,0 +1,69 @@ +import { cn } from '@/lib/utils' +import { ExternalLink } from './external-link' +import { ArrowRight } from 'lucide-react' +import { useChatStore } from '@/hooks/use-chat-store' +import { Button } from '@/components/ui/button' +import { OpenAI } from '@/components/icons' + +const exampleMessages = [ + { + heading: 'Explain technical concepts', + message: `What is a "serverless function"?` + }, + { + heading: 'Summarize article', + message: 'Summarize the following article for a 2nd grader: \n' + }, + { + heading: 'Draft an email', + message: `Draft an email to my boss about the following: \n` + } +] + +export function EmptyScreen({ className }: React.ComponentProps<'div'>) { + const { setDefaultMessage } = useChatStore() + + return ( +
+
+ +
+
+

Welcome to Next.js Chatbot!

+

+ This is an open source AI chatbot app built with{' '} + Next.js and{' '} + + Vercel KV + + . +

+

+ You can start a conversation here or try the following examples: +

+
+ {exampleMessages.map((message, index) => ( + + ))} +
+
+
+ ) +} diff --git a/app/empty.tsx b/app/empty.tsx deleted file mode 100644 index 0deaf75..0000000 --- a/app/empty.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { cn } from '@/lib/utils' -import { ExternalLink } from './external-link' -import { fontMessage } from '@/lib/fonts' - -function ExampleBubble({ children }: { children?: React.ReactNode }) { - return ( -
-
-

{children}

-
- ) -} - -export function EmptyScreen() { - return ( -
-
-
-
-

Welcome to Next.js Chatbot!

-

- This is an open source AI chatbot app built with{' '} - Next.js and{' '} - - Vercel KV - - . You can start a conversation here or try the following examples: -

-
-
-
- Explain technical concepts - Summarize article - Get assistance - Draft an email -
-
-
- ) -} diff --git a/app/globals.css b/app/globals.css index 69f3827..b2bc4f4 100644 --- a/app/globals.css +++ b/app/globals.css @@ -3,14 +3,80 @@ @tailwind utilities; @layer base { - #__next, - #root { - height: 100%; + :root { + --background: 0 0% 100%; + --foreground: 222.2 47.4% 11.2%; + + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + + --popover: 0 0% 100%; + --popover-foreground: 222.2 47.4% 11.2%; + + --card: 0 0% 100%; + --card-foreground: 222.2 47.4% 11.2%; + + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; + + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + + --destructive: 0 100% 50%; + --destructive-foreground: 210 40% 98%; + + --ring: 215 20.2% 65.1%; + + --radius: 0.5rem; } - body, - html { - height: 100%; + .dark { + --background: 224 71% 4%; + --foreground: 213 31% 91%; + + --muted: 223 47% 11%; + --muted-foreground: 215.4 16.3% 56.9%; + + --popover: 224 71% 4%; + --popover-foreground: 215 20.2% 65.1%; + + --card: 224 71% 4%; + --card-foreground: 213 31% 91%; + + --border: 216 34% 17%; + --input: 216 34% 17%; + + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 1.2%; + + --secondary: 222.2 47.4% 11.2%; + --secondary-foreground: 210 40% 98%; + + --accent: 216 34% 17%; + --accent-foreground: 210 40% 98%; + + --destructive: 0 63% 31%; + --destructive-foreground: 210 40% 98%; + + --ring: 216 34% 17%; + + --radius: 0.5rem; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + font-feature-settings: 'rlig' 1, 'calt' 1; } } diff --git a/app/header.tsx b/app/header.tsx new file mode 100644 index 0000000..7cce3f7 --- /dev/null +++ b/app/header.tsx @@ -0,0 +1,52 @@ +import { GitHub, Vercel } from '@/components/icons' +import './globals.css' + +import { Sidebar } from '@/app/sidebar' +import { buttonVariants } from '@/components/ui/button' +import { cn } from '@/lib/utils' +import { Plus } from 'lucide-react' +import Link from 'next/link' +import { UserMenu } from '@/components/user-menu' +import { auth } from '@/auth' + +export async function Header() { + const session = await auth() + + return ( +
+
+ {/* @ts-ignore */} + + +
+ +
+ ) +} diff --git a/app/layout.tsx b/app/layout.tsx index b9e8d26..496838e 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,9 +1,11 @@ -import './globals.css' import { Metadata } from 'next' +import './globals.css' +import { TailwindIndicator } from '@/components/tailwind-indicator' import { ThemeProvider } from '@/components/theme-provider' import { fontMono, fontSans } from '@/lib/fonts' import { cn } from '@/lib/utils' +import { Header } from '@/app/header' export const metadata: Metadata = { title: { @@ -28,22 +30,24 @@ interface RootLayoutProps { export default function RootLayout({ children }: RootLayoutProps) { return ( - <> - - - - - {children} - {/* */} - - - - + + + + +
+ {/* @ts-ignore */} +
+
{children}
+
+ +
+ + ) } diff --git a/app/page.tsx b/app/page.tsx index 0804ebc..f934585 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,18 +1,13 @@ +import { type Message } from 'ai-connector' import { Chat } from './chat' -import { Sidebar } from './sidebar' -import { auth } from '@/auth' export const runtime = 'edge' export const preferredRegion = 'home' export default async function IndexPage() { - const session = await auth() return ( -
- -
- -
+
+
) } diff --git a/app/prompt.tsx b/app/prompt.tsx index 0c03997..ebfab29 100644 --- a/app/prompt.tsx +++ b/app/prompt.tsx @@ -1,5 +1,6 @@ 'use client' +import * as React from 'react' import { CornerDownLeft, RefreshCcw, StopCircle } from 'lucide-react' import { useState } from 'react' import Textarea from 'react-textarea-autosize' @@ -8,6 +9,13 @@ import { Button } from '@/components/ui/button' import { fontMessage } from '@/lib/fonts' import { useCmdEnterSubmit } from '@/lib/hooks/use-command-enter-submit' import { cn } from '@/lib/utils' +import { useChatStore } from '@/hooks/use-chat-store' +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger +} from '@/components/ui/tooltip' export interface PromptProps { onSubmit: (value: string) => void @@ -22,8 +30,21 @@ export function Prompt({ onAbort, isLoading }: PromptProps) { - const [input, setInput] = useState('') + const { defaultMessage } = useChatStore() + const [input, setInput] = useState(defaultMessage) const { formRef, onKeyDown } = useCmdEnterSubmit() + const inputRef = React.useRef(null) + + React.useEffect(() => { + if (inputRef.current) { + inputRef.current.focus() + } + }, []) + + React.useEffect(() => { + setInput(defaultMessage) + }, [defaultMessage]) + return (
{ @@ -32,7 +53,6 @@ export function Prompt({ await onSubmit(input) }} ref={formRef} - className="stretch flex w-full flex-row gap-3 md:max-w-2xl lg:max-w-xl xl:max-w-3xl mx-auto px-4" >
@@ -71,8 +91,9 @@ export function Prompt({ */}
-
+