From 5623baf14edd0ff8070601cf77cc427ea0bad4e5 Mon Sep 17 00:00:00 2001 From: liuziting <57311725+liu-ziting@users.noreply.github.com> Date: Thu, 28 Dec 2023 13:56:47 +0800 Subject: [PATCH] init --- .eslintrc.json | 26 + .gitignore | 39 + LICENSE | 21 + README.md | 2 +- app/(chat)/chat/[id]/page.tsx | 47 + app/(chat)/layout.tsx | 17 + app/(chat)/page.tsx | 8 + app/actions.ts | 120 + app/api/auth/[...nextauth]/route.ts | 2 + app/api/chat/route.ts | 81 + app/globals.css | 95 + app/layout.tsx | 63 + app/opengraph-image.jpg | Bin 0 -> 22593 bytes app/share/[id]/page.tsx | 50 + app/sign-in/page.tsx | 16 + app/twitter-image.jpg | Bin 0 -> 22593 bytes auth.ts | 39 + components/button-scroll-to-bottom.tsx | 34 + components/chat-history.tsx | 46 + components/chat-list.tsx | 27 + components/chat-message-actions.tsx | 40 + components/chat-message.tsx | 77 + components/chat-panel.tsx | 105 + components/chat-scroll-anchor.tsx | 29 + components/chat-share-dialog.tsx | 106 + components/chat.tsx | 72 + components/clear-history.tsx | 77 + components/empty-screen.tsx | 36 + components/external-link.tsx | 29 + components/footer.tsx | 25 + components/header.tsx | 79 + components/login-button.tsx | 42 + components/markdown.tsx | 9 + components/prompt-form.tsx | 97 + components/providers.tsx | 17 + components/sidebar-actions.tsx | 125 + components/sidebar-desktop.tsx | 19 + components/sidebar-footer.tsx | 16 + components/sidebar-item.tsx | 124 + components/sidebar-items.tsx | 42 + components/sidebar-list.tsx | 38 + components/sidebar-mobile.tsx | 28 + components/sidebar-toggle.tsx | 24 + components/sidebar.tsx | 21 + components/tailwind-indicator.tsx | 14 + components/theme-toggle.tsx | 31 + components/ui/alert-dialog.tsx | 141 + components/ui/badge.tsx | 36 + components/ui/button.tsx | 57 + components/ui/codeblock.tsx | 148 + components/ui/dialog.tsx | 122 + components/ui/dropdown-menu.tsx | 128 + components/ui/icons.tsx | 432 ++ components/ui/input.tsx | 25 + components/ui/label.tsx | 26 + components/ui/select.tsx | 123 + components/ui/separator.tsx | 31 + components/ui/sheet.tsx | 140 + components/ui/switch.tsx | 29 + components/ui/textarea.tsx | 24 + components/ui/tooltip.tsx | 30 + components/user-menu.tsx | 67 + lib/config.ts | 29 + lib/hooks/use-at-bottom.tsx | 23 + lib/hooks/use-copy-to-clipboard.tsx | 33 + lib/hooks/use-enter-submit.tsx | 23 + lib/hooks/use-local-storage.ts | 24 + lib/hooks/use-sidebar.tsx | 60 + lib/types.ts | 18 + lib/utils.ts | 43 + middleware.ts | 5 + next-env.d.ts | 5 + next.config.js | 13 + package-lock.json | 8252 ++++++++++++++++++++++++ package.json | 69 + postcss.config.js | 6 + prettier.config.cjs | 34 + public/apple-touch-icon.png | Bin 0 -> 6828 bytes public/favicon-16x16.png | Bin 0 -> 437 bytes public/favicon.ico | Bin 0 -> 15406 bytes tailwind.config.js | 95 + tsconfig.json | 35 + 82 files changed, 12380 insertions(+), 1 deletion(-) create mode 100644 .eslintrc.json create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 app/(chat)/chat/[id]/page.tsx create mode 100644 app/(chat)/layout.tsx create mode 100644 app/(chat)/page.tsx create mode 100644 app/actions.ts create mode 100644 app/api/auth/[...nextauth]/route.ts create mode 100644 app/api/chat/route.ts create mode 100644 app/globals.css create mode 100644 app/layout.tsx create mode 100644 app/opengraph-image.jpg create mode 100644 app/share/[id]/page.tsx create mode 100644 app/sign-in/page.tsx create mode 100644 app/twitter-image.jpg create mode 100644 auth.ts create mode 100644 components/button-scroll-to-bottom.tsx create mode 100644 components/chat-history.tsx create mode 100644 components/chat-list.tsx create mode 100644 components/chat-message-actions.tsx create mode 100644 components/chat-message.tsx create mode 100644 components/chat-panel.tsx create mode 100644 components/chat-scroll-anchor.tsx create mode 100644 components/chat-share-dialog.tsx create mode 100644 components/chat.tsx create mode 100644 components/clear-history.tsx create mode 100644 components/empty-screen.tsx create mode 100644 components/external-link.tsx create mode 100644 components/footer.tsx create mode 100644 components/header.tsx create mode 100644 components/login-button.tsx create mode 100644 components/markdown.tsx create mode 100644 components/prompt-form.tsx create mode 100644 components/providers.tsx create mode 100644 components/sidebar-actions.tsx create mode 100644 components/sidebar-desktop.tsx create mode 100644 components/sidebar-footer.tsx create mode 100644 components/sidebar-item.tsx create mode 100644 components/sidebar-items.tsx create mode 100644 components/sidebar-list.tsx create mode 100644 components/sidebar-mobile.tsx create mode 100644 components/sidebar-toggle.tsx create mode 100644 components/sidebar.tsx create mode 100644 components/tailwind-indicator.tsx create mode 100644 components/theme-toggle.tsx create mode 100644 components/ui/alert-dialog.tsx create mode 100644 components/ui/badge.tsx create mode 100644 components/ui/button.tsx create mode 100644 components/ui/codeblock.tsx create mode 100644 components/ui/dialog.tsx create mode 100644 components/ui/dropdown-menu.tsx create mode 100644 components/ui/icons.tsx create mode 100644 components/ui/input.tsx create mode 100644 components/ui/label.tsx create mode 100644 components/ui/select.tsx create mode 100644 components/ui/separator.tsx create mode 100644 components/ui/sheet.tsx create mode 100644 components/ui/switch.tsx create mode 100644 components/ui/textarea.tsx create mode 100644 components/ui/tooltip.tsx create mode 100644 components/user-menu.tsx create mode 100644 lib/config.ts create mode 100644 lib/hooks/use-at-bottom.tsx create mode 100644 lib/hooks/use-copy-to-clipboard.tsx create mode 100644 lib/hooks/use-enter-submit.tsx create mode 100644 lib/hooks/use-local-storage.ts create mode 100644 lib/hooks/use-sidebar.tsx create mode 100644 lib/types.ts create mode 100644 lib/utils.ts create mode 100644 middleware.ts create mode 100644 next-env.d.ts create mode 100644 next.config.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 postcss.config.js create mode 100644 prettier.config.cjs create mode 100644 public/apple-touch-icon.png create mode 100644 public/favicon-16x16.png create mode 100644 public/favicon.ico create mode 100644 tailwind.config.js create mode 100644 tsconfig.json diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..c17b532 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://json.schemastore.org/eslintrc", + "root": true, + "extends": [ + "next/core-web-vitals", + "prettier", + "plugin:tailwindcss/recommended" + ], + "plugins": ["tailwindcss"], + "rules": { + "tailwindcss/no-custom-classname": "off", + "tailwindcss/classnames-order": "off" + }, + "settings": { + "tailwindcss": { + "callees": ["cn", "cva"], + "config": "tailwind.config.js" + } + }, + "overrides": [ + { + "files": ["*.ts", "*.tsx"], + "parser": "@typescript-eslint/parser" + } + ] +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2930359 --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +node_modules +.pnp +.pnp.js + +# testing +coverage + +# next.js +.next/ +out/ +build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env.local +.env.development.local +.env.test.local +.env.production.local + +# turbo +.turbo + +.env +.vercel +.vscode +.env*.local +node_modules diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d787207 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 sinnedpenguin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 7133dae..152981c 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# gemini-chat \ No newline at end of file +## GeminiChat diff --git a/app/(chat)/chat/[id]/page.tsx b/app/(chat)/chat/[id]/page.tsx new file mode 100644 index 0000000..0ca46b1 --- /dev/null +++ b/app/(chat)/chat/[id]/page.tsx @@ -0,0 +1,47 @@ +import { type Metadata } from 'next' +import { notFound, redirect } from 'next/navigation' + +import { auth } from '@/auth' +import { getChat } from '@/app/actions' +import { Chat } from '@/components/chat' + +export interface ChatPageProps { + params: { + id: string + } +} + +export async function generateMetadata({ + params +}: ChatPageProps): Promise { + const session = await auth() + + if (!session?.user) { + return {} + } + + const chat = await getChat(params.id, session.user.id) + return { + title: chat?.title.toString().slice(0, 50) ?? 'Chat' + } +} + +export default async function ChatPage({ params }: ChatPageProps) { + const session = await auth() + + if (!session?.user) { + redirect(`/sign-in?next=/chat/${params.id}`) + } + + const chat = await getChat(params.id, session.user.id) + + if (!chat) { + notFound() + } + + if (chat?.userId !== session?.user?.id) { + notFound() + } + + return +} diff --git a/app/(chat)/layout.tsx b/app/(chat)/layout.tsx new file mode 100644 index 0000000..a19df3f --- /dev/null +++ b/app/(chat)/layout.tsx @@ -0,0 +1,17 @@ +import { SidebarDesktop } from '@/components/sidebar-desktop' + +interface ChatLayoutProps { + children: React.ReactNode +} + +export default async function ChatLayout({ children }: ChatLayoutProps) { + return ( +
+ {/* @ts-ignore */} + +
+ {children} +
+
+ ) +} diff --git a/app/(chat)/page.tsx b/app/(chat)/page.tsx new file mode 100644 index 0000000..c464137 --- /dev/null +++ b/app/(chat)/page.tsx @@ -0,0 +1,8 @@ +import { nanoid } from '@/lib/utils' +import { Chat } from '@/components/chat' + +export default function IndexPage() { + const id = nanoid() + + return +} diff --git a/app/actions.ts b/app/actions.ts new file mode 100644 index 0000000..7db3951 --- /dev/null +++ b/app/actions.ts @@ -0,0 +1,120 @@ +'use server' + +import { revalidatePath } from 'next/cache' +import { redirect } from 'next/navigation' +import { kv } from '@vercel/kv' + +import { auth } from '@/auth' +import { type Chat } from '@/lib/types' + +export async function getChats(userId?: string | null) { + if (!userId) { + return [] + } + + try { + const pipeline = kv.pipeline() + const chats: string[] = await kv.zrange(`user:chat:${userId}`, 0, -1, { + rev: true + }) + + for (const chat of chats) { + pipeline.hgetall(chat) + } + + const results = await pipeline.exec() + + return results as Chat[] + } catch (error) { + return [] + } +} + +export async function getChat(id: string, userId: string) { + const chat = await kv.hgetall(`chat:${id}`) + + if (!chat || (userId && chat.userId !== userId)) { + return null + } + + return chat +} + +export async function removeChat({ id, path }: { id: string; path: string }) { + const session = await auth() + + if (!session) { + return { + error: 'Unauthorized' + } + } + + await kv.del(`chat:${id}`) + await kv.zrem(`user:chat:${session.user.id}`, `chat:${id}`) + + revalidatePath('/') + return revalidatePath(path) +} + +export async function clearChats() { + const session = await auth() + + if (!session?.user?.id) { + return { + error: 'Unauthorized' + } + } + + const chats: string[] = await kv.zrange(`user:chat:${session.user.id}`, 0, -1) + if (!chats.length) { + return redirect('/') + } + const pipeline = kv.pipeline() + + for (const chat of chats) { + pipeline.del(chat) + pipeline.zrem(`user:chat:${session.user.id}`, chat) + } + + await pipeline.exec() + + revalidatePath('/') + return redirect('/') +} + +export async function getSharedChat(id: string) { + const chat = await kv.hgetall(`chat:${id}`) + + if (!chat || !chat.sharePath) { + return null + } + + return chat +} + +export async function shareChat(id: string) { + const session = await auth() + + if (!session?.user?.id) { + return { + error: 'Unauthorized' + } + } + + const chat = await kv.hgetall(`chat:${id}`) + + if (!chat || chat.userId !== session.user.id) { + return { + error: 'Something went wrong' + } + } + + const payload = { + ...chat, + sharePath: `/share/${chat.id}` + } + + await kv.hmset(`chat:${chat.id}`, payload) + + return payload +} diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..883210b --- /dev/null +++ b/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,2 @@ +export { GET, POST } from '@/auth' +export const runtime = 'edge' diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts new file mode 100644 index 0000000..bb27f2e --- /dev/null +++ b/app/api/chat/route.ts @@ -0,0 +1,81 @@ +import { auth } from '@/auth'; +import { kv } from '@vercel/kv'; +import { nanoid } from '@/lib/utils'; +import { genAI, generationConfig, safetySettings } from '@/lib/config'; + +export async function POST(req: Request) { + const json = await req.json(); + const { messages } = json; + const userId = (await auth())?.user.id; + + if (!userId) { + return new Response('Unauthorized', { + status: 401, + }); + } + + const model = genAI.getGenerativeModel({ model: process.env.MODEL }); + + if (messages.length === 0 || messages[messages.length - 1].role !== 'user') { + messages.push({ + role: 'user', + content: '', + }); + } + + const chatHistory = messages.map((message: { role: string; content: string; }) => ({ + role: message.role, + parts: [message.content], + })); + + const chat = model.startChat({ + history: chatHistory.map((entry: { parts: string; responseMessage: string; }) => [ + { role: 'user', parts: entry.parts }, + { role: 'model', parts: entry.responseMessage || '' }, + ]).flat(), + generationConfig, + safetySettings, + }); + + const { readable, writable } = new TransformStream(); + const writer = writable.getWriter(); + + (async () => { + let text = ''; + const result = await chat.sendMessageStream(messages[messages.length - 1].content); + + for await (const chunk of result.stream) { + const chunkText = chunk.text(); + text += chunkText; + await writer.write(new TextEncoder().encode(chunkText)); + } + + const id = json.id ?? nanoid(); + const createdAt = new Date().toISOString(); + const path = `/chat/${id}`; + + const assistantReply = { + content: text, + role: 'model', + }; + + const payload = { + id, + title: messages[0].content.substring(0, 100), + userId, + createdAt, + path, + messages: [...messages, assistantReply], + }; + + await kv.hmset(`chat:${id}`, payload); + await kv.zadd(`user:chat:${userId}`, { + score: new Date(createdAt).getTime(), + member: `chat:${id}`, + }); + + await writer.close(); + })(); + + return new Response(readable); +} \ No newline at end of file diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..786c197 --- /dev/null +++ b/app/globals.css @@ -0,0 +1,95 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 240 10% 3.9%; + + --muted: 240 4.8% 95.9%; + --muted-foreground: 240 3.8% 46.1%; + + --popover: 0 0% 100%; + --popover-foreground: 240 10% 3.9%; + + --card: 0 0% 100%; + --card-foreground: 240 10% 3.9%; + + --border: 240 5.9% 90%; + --input: 240 5.9% 90%; + + --primary: 240 5.9% 10%; + --primary-foreground: 0 0% 98%; + + --secondary: 240 4.8% 95.9%; + --secondary-foreground: 240 5.9% 10%; + + --accent: 240 4.8% 95.9%; + --accent-foreground: ; + + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + + --ring: 240 5% 64.9%; + + --radius: 0.5rem; + } + + .dark { + --background: 240 10% 3.9%; + --foreground: 0 0% 98%; + + --muted: 240 3.7% 15.9%; + --muted-foreground: 240 5% 64.9%; + + --popover: 240 10% 3.9%; + --popover-foreground: 0 0% 98%; + + --card: 240 10% 3.9%; + --card-foreground: 0 0% 98%; + + --border: 240 3.7% 15.9%; + --input: 240 3.7% 15.9%; + + --primary: 0 0% 98%; + --primary-foreground: 240 5.9% 10%; + + --secondary: 240 3.7% 15.9%; + --secondary-foreground: 0 0% 98%; + + --accent: 240 3.7% 15.9%; + --accent-foreground: ; + + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 85.7% 97.3%; + + --ring: 240 3.7% 15.9%; + } +} + +@layer base { + * { + @apply border-border; + } + + body { + @apply bg-background text-foreground; + } +} + + +/* 滚动条的样式 */ +::-webkit-scrollbar { + width: 0px; +} + +/* 滚动条轨道的样式 */ +::-webkit-scrollbar-track { + background-color: #f5f5f5; +} + +/* 滚动条滑块的样式 */ +::-webkit-scrollbar-thumb { + background-color: #c1c1c1; +} \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..029ddd1 --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,63 @@ +import { Toaster } from 'react-hot-toast' +import { GeistSans } from 'geist/font/sans' +import { GeistMono } from 'geist/font/mono' + +import '@/app/globals.css' +import { cn } from '@/lib/utils' +import { TailwindIndicator } from '@/components/tailwind-indicator' +import { Providers } from '@/components/providers' +import { Header } from '@/components/header' + +export const metadata = { + metadataBase: new URL(`https://${process.env.VERCEL_URL}`), + title: { + default: 'GeminiChat', + template: `%s - AI-powered chatbot` + }, + description: 'An AI-powered chatbot.', + icons: { + icon: '/favicon.ico', + shortcut: '/favicon-16x16.png', + apple: '/apple-touch-icon.png' + } +} + +export const viewport = { + themeColor: [ + { media: '(prefers-color-scheme: light)', color: 'white' }, + { media: '(prefers-color-scheme: dark)', color: 'black' } + ] +} + +interface RootLayoutProps { + children: React.ReactNode +} + +export default function RootLayout({ children }: RootLayoutProps) { + return ( + + + + +
+ {/* @ts-ignore */} +
+
{children}
+
+ +
+ + + ) +} diff --git a/app/opengraph-image.jpg b/app/opengraph-image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..473761288ecf984580a17cf4564c5f3d56c42390 GIT binary patch literal 22593 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 literal 0 HcmV?d00001 diff --git a/app/share/[id]/page.tsx b/app/share/[id]/page.tsx new file mode 100644 index 0000000..638fed5 --- /dev/null +++ b/app/share/[id]/page.tsx @@ -0,0 +1,50 @@ +import { type Metadata } from 'next' +import { notFound } from 'next/navigation' + +import { formatDate } from '@/lib/utils' +import { getSharedChat } from '@/app/actions' +import { ChatList } from '@/components/chat-list' +import { FooterText } from '@/components/footer' + +interface SharePageProps { + params: { + id: string + } +} + +export async function generateMetadata({ + params +}: SharePageProps): Promise { + const chat = await getSharedChat(params.id) + + return { + title: chat?.title.slice(0, 50) ?? 'Chat' + } +} + +export default async function SharePage({ params }: SharePageProps) { + const chat = await getSharedChat(params.id) + + if (!chat || !chat?.sharePath) { + notFound() + } + + return ( + <> +
+
+
+
+

{chat.title}

+
+ {formatDate(chat.createdAt)} · {chat.messages.length} messages +
+
+
+
+ +
+ + + ) +} diff --git a/app/sign-in/page.tsx b/app/sign-in/page.tsx new file mode 100644 index 0000000..280333e --- /dev/null +++ b/app/sign-in/page.tsx @@ -0,0 +1,16 @@ +import { auth } from '@/auth' +import { LoginButton } from '@/components/login-button' +import { redirect } from 'next/navigation' + +export default async function SignInPage() { + const session = await auth() + // redirect to home if user is already logged in + if (session?.user) { + redirect('/') + } + return ( +
+ +
+ ) +} diff --git a/app/twitter-image.jpg b/app/twitter-image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..473761288ecf984580a17cf4564c5f3d56c42390 GIT binary patch literal 22593 zcmeFZ2{hDk_%A$mLSxTTh6rUVSrVC4wj{JEVv?Q4B-^NA?E5YhAt9-djD75bvSceV zVm4VqW)gm7X1u@t_nv#tdC$G~-1j}_zW064d4J}6=IhLF{JzigeZJ50c{Y1r_s9^D zi{@755DpFw$Q|$pvPXe9nxTC>ArKoI$T0{6!Vlr$;(+jgGY)WE z|NF&W6v7R@fz$oH_G15a{_j2rNBnaLq%LD`7IGfK$-(jO^`9>;9IDlD}B-~8Vn`SVs>0t5!Gln3O*KW6;ri-VJkn}-)%(mp}(h6WLE`CMF_;9_~W zL2^05!OtPwqC8@XCro+8?Opi}2T15dWtH+PovZ7XyfRK#K6&>+w7@>81JW|GM^seR zjvmw1(>FL}c>4SWGjj_|tBY5!UBBUQ^OmEVyN9Qjw~udNP;f{nI_zOgY+QUoVp4K; z&g0y?{3lNfUX+zrysUgx_4<81uA#B1xuvzIm(bTg@Zsa&#N_8MQ`28(h!iSq;rrs! z^2!g!ug%|E+dIIYzyIjsfN=dMvi=KY|0lXcLAp4>J>cg1M;8ZYC^)!8xp@>%@QRt* z^SK6yAJ&QDmpGSITGuV0bn*&a^6rE2eNxK0lp~CPr2PkF|8s;z|KFnQzYz9c=^{de zK*8h`N#Bw79JKD8H-|F0b1=+i0i`knJ( zi?^Mk9`kg#n+#0WN+y^=B<%mcF@jBW0XZ5fo-HzlF{Ntr0hOj>nCnoYuCulMgjYlT z7g`r`d*VZs%59xzrApinCzuz312j~OeycJ2I6X8Oe-xTn-7#BX^>*dt*P2Q`SbOAP z@{x>1#8Vv;*HOLQ@ZDQ%f{P%l9!}0A)t%`~jWA{!0x!-a*@jw9zNT8BW=eO zF;m`j!a?<=oMZ?)WVQ1%)&rL82v5VnDQ%tc9qiVHQ$kDbvNOTDHYp&lOquMXrQ5@B@Kdb^VK97hsE_jY6V zn;f3=mJN-?rF_)g{(?GEAR>Q@cKB>ujjL(hW9?5)ArP1$w)@&1#ML&s0KR5G$8MJa zT&}ffi*C4humDbd53I+XF#9S4C; zvwC_kL8^ec^Ot81_we9k?D2u1QE3t5Xv;y3LTkR;UP@;ynBryZD^R*Bdk~he2l@RS zg{Z*NZ@_0y3q#ra#CI>Thg0xtwk*Cal*MkW`sKo78Lt0SYNVpMOh&{Xq5rccLH1M{SL(bWe3Ny zTLK1Qp7xw3U9&K`x}xgz_)?&K_Y8V>8}oU64IfU%N~7g}F|50JJIijby#G{Ib^WQt zJDGzSj;y15keEkT8ilb3i@#7fN=NO}g&$_F-RziU@-^_SlGd`rA)_KPb z^Zh*vsE)4N(lP*b1EyHXHjZw)&he%xU9&?52rxFIbc+4h9@m@73Q}wdmcM^p!@l~< z`{A2!(<>S?)wmeG$?V7up*=`<8=OBv0;31mg1kF`fiiabZ33x0u?`XHjJ|}FNpl?C z>gl!G9G@j~e@>|diNH7wE9E0$s_|HIDyhD5EsUPBT{GQra@j-`Nb;g9^@aGnxc$}l z*UHYrnxZq+c=~K-9#!GfWa2`Or}@DeQvN=2gvPbJ#`iAY282$0)SWzTNi); zlyiuk0XR6I1YH|q@Bz+$0{h#(CY-VPDGAm`bnIk31oN%e?y`fn5O0pYMDCCnr4= z&R|LoZOhQ*8yJ&a1(nIcB{K zQ&+4L-rEPK2RJ0>(Doo}n+&cVEOd=|CX{Z_vq3SMRd4qubyMFMN=ZC16uE%h;g6Eu z)Nh>3gS?5jKYkX5VSnkQUmKm-pu)Pb2fa!A_8{(9K0v!4Azqoi>BN91>WNa-s}JA0 z?7<${d@)jY_tSav@i=9Xr(7rt29zzh2k`@+n*^`VAgBO}6-1^rz_smnORtn#7~ja? zi4FO6Zu`aLM?qA3$*`Dz?&DW5oow~Z^kfL@%mkK9#&Y^b7y*4{K+JYPV>_VAAl}@9 zivw`BxcbwY$r~&Br93Nf`(!x#cFQmDyPkWCUwR4L+Jo@;02aHh5uZu*r0#Y%%7Lg3 zdG;U)>aS(z?V`%yFZgV^U9UY`^KaY6Q{~NjG$-iL3~ypde*U`SXJAd+G_z$7T;NwA1`g}nuLa(_h;UrsV*H;{Z%&Af~425l0# z$^-^3G7Noy29Lws0;Nc5I^IOjM~W!_#&r)OaG?O2kCbk`B|e-k;BIpFTh?RmnUDoB z+d~8E;BNA?jt6Uh9d!i6>0D&(c&yhb7tm5iwbE>uX;`?y^H5A+f9-z z$-P8A4H7uny9c2=*T!L`*)mK6kWFc_aGu{5ueVP0^UZp;v*x?D+-Y7o%JV- zuRwDJ{l$(V35VTd7CPy`hm5tP-E+*NO@IX@oRdUrZ0D^P1VX;+R-fz{?|JvasrPQ) z_uI%=kJzL<&$|Mua50eDmoPp!nTiFjk?2Ev5Qf(t1P7-S;?#@8+2)<Qu2lszK@wwZV@v9H*3HOts}(2Cq-Z!L5BVY3ZMcQUV2kEoOch>lTQ!rDtxjf=d7c7??+m#%d2CP z&ejX2&8LmKA6I=W`e@j%YjOBV{(p@^|#sIFn3uR1mnZ1x$+6W^+wK1pDCC)Q{8}| z8EAvN+}o=^W)oCXDr?H!locc7z$4lj5{QeGj`k}?%|!QFuu6hp@~g#$<3YB7RoaBl z!%KM%SDl^Dv=8K{4SRoxyg&2cvvB92grK0F$?tCRK+Q+hU$Gi zf2o9-Ii(LrlAg?~e@IS|dj~NqS}iuaixFo%#TYPDdu5Rui6&}*SC7HGLOT*}IVp@I zrhR_TE7(eL9i4I07?dpk9)DA7Jo`c0SLmS^tr?O`*fbu-h~DgK(to3x_;%}j*^|S_ zAsbiiOgu2Sq2^ZK>F`>lJ9h^9;@Qj6UeT)ear{B^Up;=L6i^^OvQSlaK)l zqTlfMx^!xwx7|~()N^&K)s`OtrSRbz-b50SXaYl}sJ{f&Pg6`>Q9s$Z1Q%Yi{s>cx z`-3pZpEUO0xXy;Ldro7MgKW7>>KN*#^q+N%W78w_pN0A*f%m7y!$$ejeMFFP)~_R% z3^mYytSSe){>&$Dp~NFTuocH}Fuu9j5meP}biGq6eWo$?(BFtM?p4vI%-=_v)T<6% z+jZ~=+!a`f*q`Il!N@19^YzNhO>E6z4BV<}3Ij33_5}qQX0w}Ip4vy^#EE`$h+$c7 z)MmTnZz04xy~U_Ez+bo&da}3Uz@!Px=5A9#PIo|<@Pb3~2uedx?7)4p3h&tcKc|R3 zE4zX#c`m|Zi(lAkfP*PRJ4H99X8EBrtC}Fy)L3#NwSDtdyv!l{MDyj4Xj9z>?@S)O zXzwL=uBAL@UKtOP-hw8(NRDCbsVfDf3u6Rv#D|T}A=6+Dh3cA}F1tTQmUkgF(a;&C zZldiPmI{?X$MlPMcj%AdZMBm0-ptHh%CeErDS20vcFv*lw^j_ zF4r7TLbZs&k%WQX^V7yBX{*+=)p7*+3sYltxHO`7Wl}%je&vb6f@>6aDj9qd%k1G& zKWrxd65XQ5T3Zfq7?4G!8=L$s9oH_8v2Cu)?%y2TJh%3QlkuMOe5#jp*Xs@uR&H7^&!&xEU|+T949b(K*Kpq)r-l0V8&O~A;! zMj!X_oEKKV+!+qPWHrIHxW;Lo-!q`9qTr1Qj?S>O>;k-n+W*B4t%7xXf8ShPb?a_VHJ+%eDWMj zOi)h8fr5m2(QZK7SzCwp2#fc2D2yB1xY0nTy-tNa@};`kqrpu>NXpwI-n7-=N^ffB zc~0SnB<9Hjy(CHYFo`cX9MMmN_Kr9a0(_5KX%NFl7Kpf^hgS@82BnjgZy(LdkXgQO zX?PQ+KJL=ZNZ8h97<6~;YhZovD`L%+Bze-SSw+u1{hS)ivc;sM+Evqs;S29WFIV74_3R>m%|zekNsO!G1#?9RG;fO7193?(Gzau z)HK;~1dSq?9HUn+gvd@sBE2SmW;-BDmS5iI`3mj+%E2WZz2DD?P(ru;GOoEbCpJxm zCTwXZ6(5)&Ieft!6)x)XzMFnRxo#lHVKC2oEP6b{9P&&S^A`GSxwhO`X%E89#xWG= z#ZLo3rTG_|YW%3QyBhuILb%D|`H=U7Pjy45(=xN|bFDs6p%6DiEEMbJd;%{?2@f2Q zHyOtq2%xFwXP8d>eCw}O%yP>g^m++hU+(MrZUUkU%Se z0w`;GO;V*cVIAJ9@1P=6@M~!OxV|Vqvc!wq|Jb#xms>1TdR$seAmzNe+&r&{2~DeaNT(=6}(^&*WgD=XSUiZ5myaoGoiGnf6G&-ckD z#5zzD;`6C0ah0}JzJK|?YBkQhwUa4UZkhc)bS$kj_dc%^Ph$a{6oU{}0BUK8xVBMI zZx_kBo9-=bpSOIN&6+RLLiNAEbl=OT$Z`%_a<%ZObLGBhze*cjoAm+Fgb9p+if7*P zqM`VJICL2{bwx= zkuZgP{o}82bw$^;_d=a`>pZ)3igMcUo%?b!9}brsV2XcvQkei7n1^Lhajmx7J!qq~ ze4gP5TOv2_s=Qsx3CujAR<3Ovv==?ef4R=F5`zAiz|g+C2;eCNyAR0dZ*^LgmCCJh zvOQEi(yVb{q@yeP05UD^?%6-43h!Pqd3MEp{*rp(LaW89Kn|sodmdQ+ma(6RAdVhs zCuJ!nrS5nezX{elE}+@XO-;YS7d7rOL@(S%&`Bx3(Last8MZyeqOG6xR3~t=YP6gi z%`LgoN3%^)SwcLCf=c3e_gBYsf^;*!M$>wyT~0Iuanup*u7}X*trEbZMvu3l(B5zM zm*CqQbb`U@KW<^3M9%k&?6V+FkTeUIe2wik-oSMx<~Q5#OnU3?wSL%rM2%UdSS1;Ji{P?Y41iu@B+_Ah4ZRVKAMH)^jxuC z4V|>{=5PE9@mI4f)X#>6Oa4R%??K$!wVi-q%G{%Vgs849$48ovdq!_w=#2vg*@#%I z+R(`h@Lcf&8=4S7?$&WYf zpXNp{h(U;<2i+tdyfZ$WyaW9zPPv@K$fvSf%2q$VWaY3$Z5$VmZt;3F)jiexm`)j7 zh?4p9o?~TfYB6D#>nqc|q_@dL0LUr@3QecC%*SL|xY&N!xEXJfcU5t-)_>;Kg4vpJx${VgojR7h zwANjT9;IOB4H)@pI1&V~Z-A|(s6YMvXG<{c+_FOGO-iiS-J?9uGC33=Hd;6MGRF}Y zU^7FPfdIM#jd1!|0W`4zC2)uXSM6q7GMX^d(qdGsBHa%RMoYT~DTq2o7_!m}37w)_ zM%7MN=oz3pB7pYlJ#GHx%|C0b<8)T*x$P0LgZS)k7YPfT5}5<(9H8=gs#aUsA;%u< zfQ<#$9z;&*!V(KKr+aO#669f>^$zKlOKG$VI!=ws@$Sf8KdxsB;a8V; zQ!?U%+cJcI$ZIn6K3{PG{OqEATqoReSXW%0%v|2NB27qmkm|3bAPxebFOt9c65wt~ z*mZhCLK6Nu18Pb)$)^hY@0!;o?CLKvMhIfBGeu3-;Bc*xUuk98=yd2;7h67OppiyZ zO~qw!d7Fq&jV8$1?JECG@imw=9?3i}^6lzSZ$7eFh*#hPs8qU^Yb{xoSTCzi&k||x zzqbn(pHkI=N2Ex%8`|GF+*FQ#uzLc}yZm$k+CC<`T?KmjqWCY|#V~1A56bg?F z_u6J@S$PVO4tN1Ve%39e)fMPbQelM0riF{skj zG;;V|(>Lgdsh-7eD^kDaaP}HkKIq$iue*k9?p}6E?F|O4!;U`5hrZ0%gCr~fRbJ&q zcTs%R75!VO=a-)FOc|FwIBbNk`#m=g9`;YV_GXqm682~Rdjn>HR;PlYw!&qmC{_3z z9ELf=D2Z98G_|7yZaIELRX?#Ex?eMSfqwfG+(R`HLofz%X zP9W`tckBLq;k1qD-)ByLn!ViIA}YPumi^t!qD9CmXDIKuHBE~L^PMq3(~qH*MDIbM z7}Xa8Zx7I&``>#sw>PDW21w443ph)aF~?0eW4hcTZw9(tHQR6VhGUh4tL=vhF#YJ2 zQ5(2D2w?e^X-{Es4)qe_iu%goGwX-mpH6yF;&Iv9JG1sCLpB}NWy5z5v)ou6Vtm1o%Q+&xkOtdkV0XeGE z9z+&syEuUls6w~(^I8{bp1fjNy=H6}rd#Yj&58EX4s1vX^B#XqW|F5zb?t}wrq*}w zFg0z{su@)p81D$BhA1#r#N{*#%AQs^GGY`AdxUj(85Y}}<9IB2BZcUVkao7F zrOYYf!nRVzU){Fg$uUIXhDX~1c|Tke;=Q;85&wpPvKk@|0bCSP!f)0^8da#krSN5; zjTZ>YocvDnwwb`h&5nHTd)r&j8=P*!|0kSn`Xb$v?`sJs)}O@9)S#mxX$>8WotQlc zMKWh(DhMWg%W(jj{8Id>)U8_PLZOfID@N<7V{PUfD$Q6}#5f5Wp@8P2V&fN0ba0If z)l_#zUR+2)FfLoSR^m0X#tjU$mX_~9ZpnSPf3m3ovt)K~82Y-QcJ^c!R#cB9>D{`O zkX+4_qiG9z7wCo;XZsNtG{xH1Je@{ec6`p`)QDc4?Xhxowa7SO*?>y zsU0wYW#FQb;$ju8I&(q5o1UxL7Q!0duTML)*%w+;r@fx`f<59Q!SIN0ulV8kHeXH; zAS zd!m+Tzs1vnP~AyTz0)m>^QS2X-fXSyUsu`P9>XSnM7v@|i|P-z)0}J=l`(cheyP zc>hs_K$i+K6i`b~pjfjw`}G+$NnjM9r?rkAQdzYkd{DNyBowK1u}QzPy?3BF^7@RR zy20fH+F!LR-cTf~7VbR+Po0vNoxp8KFfACdrw=}8pRc424Yz;$b$n6IRXK(8Sma53 zz1K6hA{O+YmhF^hN&{Y$J%}8(r)x6UHeoezI|-K}=kz4lvZ5_)Sa9)jnhc~yxx3qo z=Q0P}5M)z(*KY5&FYDF4h@+TWzz{>QU)Hn4k1V6yz>v_NmVGR!SyNV-i#)BI_SLxd zuwq*B(T}H2jbNODX;sD+3=>-9@N9Ai~(B&^3(Y$`jiZmO(hYT}xCmoYD{QiW^g^?q}y}pmH8<}2> zUu-Y|lT+q6Y~pEDl3p8DhKWqL#*lh4#X#HWDWYEZ8H5KE2xtb zlX=WL0**NfRFk#2dTGndfXY?a{_n;7;^w%fVv^H)zr1G4qVlK0_|j)p_YWeMxXI8$ zwTfYoBm}DyFZUW_KHY#0>J&dxQ^hc{W|XT<(ZiBFka9}>q>h1SR&r;LSpMk2kDg^! z{w@-{3{1T{|Jp2!A#T^$K3?(5?Wp(N&o)zDf7pn}QL)g?fXMLn=bS%M(gxFrmI`n^V_#a-LJAZXYaZ+5Z)hpc~pXb$ZA>;A;uo)l*Sm* zEvSUQ14$bGTAGU!Kkuz=Am2XkXdc%p=NfEJmidrwks1?82&bBUko+EGx4<`9(h$0i zqLieO>R%>Nq?u+6xjq-=F};>x?e!9o*`6%YfQG5ebt&rkdsR4|w9uH3N38n7N|;(B z>TDSUYdUOZ3%Z{2&o38H`8B!L&)Z;mE`F|U_**&6{{*QaEc~D1e>%MfxknQGQ6kSa zK)*B+4hsV9ii>3H*zXTSe*Cg(o|>qm>mtQYy6}kgLDBAY5>UwAXI~EVjN?rMXxeEH z{p(ax8?({Ymp+@-B%<;}MT)d;pma_Na17rSn@y7Gu1#7lD~j4$>VnT+1%Q7z18gFw&WmOJjE{rAlso-KB+t?zn(n4u{e=V$bW zl<0D(sjjua#;^BA$kgATF;s8)D-7h}j z4U?*jre32I=ZCvj+Z*W>X;nD{{r97bPQ-Rbo?$ifF>8A8P{=QdoG8+YydJ%BeXa&` zdD3-q>x^4(!A4>=)83;(tKQ=UQ2c3a_K62tiu}4)_j(tjK^ zY~&l4KLpIsYpGs68wo@d!Ig$Ix&ipVcQj)qc%g!Mo7^X#PO0b&$wb=?58?TE!uru7 zm;l!Mdl9EF`aptL@JpAQt1dc>qNc>^iQ{R0etyEZOpQysF)|SMl@iS<3%F=ytMcpnMq0{4xs=}j{xgwm82Pu2y)T2| zXE}{=00lz;h)`EQb#DS8vbwz;YVUlm;0pu#pxE6kE7kS-gYu|s^(erLf$cKUU=<#|`O!)fT77k9L_@z{`sQ zOO-z`#)MO~g@DlhyjEXtYdKBkN4?bev(x6c;z@?-H~Vi|JUjM|vy~*mo&jxI(IaXG z6o?+}yAdJ6Kqcs&Ic+^;jh5eoT<)B_d;Qi8mB-4N^(Y?Dpm$3fI}8TdgR18d#bF&m zr)}S1%x#y_S1lOIffnSpIjKWl_VI>V!DnvxD4~CPH2M^%xn|tqu&ckrd0w{GZTBwP z)(Kdmw^8HUD_7spP*7(A#`#DQPLsDelWZ0$?|yYV@2r&Bo!10jAw#~!0#Fe>G9Zi+ zRm~VB-5b0h6qvr2zS-NM@+G)1KkpR5R`~Nfyl<@Gk*?IA+zr!FUkNGL%FCeoIrzB>eo$%=_S0+GC&@S(q;OEgkNiXJ3fD3UIBJme zThotb5RV&$vf_=f`_LAb$CjGvJDh-fm5G*)9QR8!s-=T(1e4$S7aeUiSXU)KWlGXr zY|P;jvp@>Pq*Nwb(C?JW`_D)$j%Ttt_oao^xjYUb4=b)ky9Z#HIUVWG*V4Uvc{$`4 z=239Nml%wF;m8Cu7xveHc`5GeCL5c-ApOUH`7LO%Js*@c`W2*Vf8+H0VU!H!-sD!w zx&oM1>N(>2Dg`MfxS?#ZbYyU3Q4o{HcF1Y2QwmY1J_H#C);9f*y`kIa{OwSBA}f!oU?s&Xz*>wPBC(+SWN$hXZNKljWTY!us=G5w$xLS;35O$1kktPa)?!uxF+oK z3BSC&?d`w9{|Ezd_@$#Yk^axG>ZUb~P{Cl9#uLEACO!cOz zw}&|ziSv5ZIZvlc@QxO6y3%d!IP4wE;1sOz^505=Ez}z!it%!jFM9#pr4D{)E`N6( zQ=Y!~@|1F}>9JV(H`#*wmi4f+a^OKt>`(;?Z#KvD>rc7p1L3eZJ)ZO)qE*l;E`DBt z!FB(IcShE;y0rL>)_}|7dl27}xYHZyctM6bAumE-{xsdZ$P1~G=7sbpy3_@FMrpp> zJ%3R-Za+as`|(mn{^!mh3rPy%t17`ogF)&ImpXLSHZDTZ$@#K>h560jp|~)ypN4x7 zV&cHZgNJ%50qm~qs;p}2#!tN@oO8zk##};7UoP{?nU1=^Va7-=OUwZFVV{4FOR?7My;a#w@`94_F;^w9nmHIaxW4O*s#WP4j%Ok;0@_E$6ojp>fd?Lt#$Vcj`T^8!BfChjre0Q^JRC z>aPPP`%i1fG1bYBQv95X!jN7aPAmRyo5<{W{{9_pnf-w{g-^DUC5+MS{ILiXrs6aP z?&|aAeAsJ@*=LNi_%}0ILSw$@`h~kSwgpf-4*Qw3N2C}NRWx>+#84%KbFVbXCd4;1 zjyDJ*)%^4e;7L4Rs$a|+PetF~uGf-`DYJYlnj87)tj3cD6L(fcr)M6km@m#>k)e`L zEz<6nb^Y>tcUHPQNuEpTaQ1u1AVKS5=Duvx1qVMe?5hch&Hx$-mvjYA#9&3BKm6!UNU{&AQrX^E`bt%z=iRurDiDKc|m&5%1&N@ zZ-B3j>(OF|Ulq*>I}a{P#OHVo%6vHJfFz>;wU89mNQ}6A9SxQ`FN3H1XUS&;m!j{k zD^3P~kClu!HF%bGZviPLDz~6LG$Q5j*()D_vIYK7Z4-%?bE z{KzxarePg3(+g@9S@{XOczJa`O6b};c@HA(W8hesXrhjO)YIzcYxB}4dHW;+@k8i1 zW~m`Tn_F3Q^W*0QNPtTky>ok(j_QT;HYGUq7Bse1V$QX+lu&KH0OlPXR@R9x>T`xw zcZ`N#SR9+M`mCl~!rAFt!pT$vY*Z71<^rkE=vriOTgu0+G%l-p_5?#S1}QQ-WLV^m z3kItr-uQ^1+~QA_&2c@M1hYzDD*iT*z?X`5eF0e80$z`Nd|uBN`fS|rXdx|pl5kFNi2*oa1!vS2+}RW>Xs8i&D{Id4ZdYW*ZJ|0<#eiXx z{VW_)ld?g?f_W~_N#faWhC=0Jr)kKa07PAdYvtcOriucLF@DxhKb(nlY1y z5{MWMedsZjB#1dz54^;vd895{@qKNMv+7ct$m}9}DNKh^ogb0cD|<2`Pv@uUL8ZVF z8Z`FY=0fAxoyIon0LU37L1FZK-p^zldLlo8< z7a-?sLw$3~T7c5APp-|uqTj+U^`f1dW%>h>Z4*iFz9jU*_q#ax!o`g>k+cy5il;wm(1YxqcCn9PUdbU7h&- zW<$ZeNs1V|d4^!GB>dx5R22D+8(*tziTt=Tyw8<9Uh9bM*^jwQM0|DKb=Y2xyd7Q( z>zG+!cH9VPg6p52K5;8m?L{)={~!Mi9#oHrQ}UCHtt+b+3n{EXJ6vXFz0*x=jeDlu zy~nb;scrBiQhi=2r6fVWxL%)Wr84Pe3&9<uA(IP?YA&P)9<6@G5RzS9BycKwd?MDqon@;k>Rd37uogt9hdYnh@K;YstS z0lx~S05D6&rrrPpgKZg5e41O}^B?l^ElufGp4q>8XhDqGw*i)z2}1>lfX4^E9-PA* zNxGAq{MV(3UWQ;K^uW{S#etDZfRCWxaggy=O7_TnVF2Y%Wzn5mSsou+n#4}|;*P!Fdi8p}{e9`v5=d{=V5 z{eh63Twtq1`uE0jIi)>M$LUu!AaLX5+F5KomTsjzGu=}{w*wKK8>Kj-2JJ;C@Je9v zRed|}TC+0U3Acc{=&86Ah1 zj{pZo&h55|wb}+}&R?Uper?__b!O*gjFOVmQcZ;_A3WfbfiQ<(bedO#LXpxT!ts`K}(1R1OE)`-`>nNM}JRyYC4#|k1O*& z2VGBci;pRRWedE6lS}@>dsvR6RIM1WqzI@h8?9OXo0sFEiBtKad_65O^XJf;H+Leh zeZOou@+iNz6HtW5Z6Oj$c!F(Z*%R{ubl#TTv)Jb#S_ax8+u)0vD{rOd%;s_`9q2C*o;#w^iTPRMnW ziSO5(8gHB(#qr#6LY-Q~gDDE`Gvfoj+OUsgEK$4*D`s*8qelOWF6=87AJ;8x(OgIB z3n)vq=Iie~I9$>k^}v+7>Nzy>Rc)JQCpoU;*q2UZCl5w-5+6`TLWlOV1?5c|7%#qB zHj1pcoVWa?>+$}g&TA35x5cvo=Oo{Pn;C2I?1oP2=E408{nG(>6(IW_kzv@cw6*O1*0--odQRj11B(+QDGgD_~HCFbnB zwiWU`ROE>xTzwrzn#oE1JpzfiV2|*O_>7=iv(Aya91s$CzAB%;jTyDfTVEpNu|0-f zp5xY(p$2@e=HZZU*9H!A6VFP2E32ygm-LE`80e&TV;fZ3=aW-IF~0?#rZ8qdrUV$$X&kMjGEpR(sC*zjz3bh*MEX#(I_2#f4PiKwDgD_Pbex zwS=$L$DTJF9~aw3lSw#sDHR(xWx_L#YS^ML{jAwa+EVW?*hI8*8GZlrL|OF2VKs?N zlU=F6++Pom&P6(`IP{TbGqz8#-t9p=HzHl+fI7bQGZpB3GAZgUQ&k;pZ8J8K_AP5b zI?v3nFng^c=lc;m+Uupt#cGL27AFf#(U4zu^1{h^jU6ToL=Q?3J=<3zVQl(#pmI61 zSi7!1dD5Cb_$k{)5HDWmHxz4Ab3-91w;S1MFb;cH+mD*595_(Yl#rxrggAsoQJ`sf zK4dVqbS_2R*|*=SM#Uh&hqf+eq2GU(!KYp6diIe!R9#K=K=bLla+QUu}aCqp!2od{UpU-;^8 zOCxg`lyyX|t{q=Hwp$OZb%b(93?drxl4#nAr)|^pZ1>M}8*8e@Zhib6?q!oZDLUsg zB=j!d-B^0w%tGkA`+cm1F>(*WC%`-kc+n8Cr$412X&dP#T3}sVph+DKCFSktnef*7 z#qHUT+y(b+eFIOb1!D#!Z&9xnnk8MWR0!qM;sW? z487dJIJA_aiPUr((;a&BvPJ*ChE7wFzRLw6f)>mu{oBFOWH{YnG`>ThRo8hBns}oX zmhLtVnwx|`v~};hAunI8w?9d?jg8Qd7v$Gw9a4sA<;ZeO%LmxAAUnPvqidit;9=9@ zYi)GWw(B?&f0FiSB|>t!U^KzleY~Q*&DQUg&hh-qk1yXGk~2>=?o0Ft$SUfU+wK9^ z+y_7%0S^jX7_c1x3!_8(orYjCE9_&RgJFrX9^D_G}!?=1ukVauGbi z9W9B~{;7eJ zHzz)~51t$p?(rAX@IC(3tskrB{TzyidyfLg4n&%0fjv!7yx_}a_VE}8dP)BoFRJ9X zY5m3_q)JIxs0PDKPGhZ~tP!b`%g=x6fnjYcSjI8q!Ua-3hF;o!Lcb)dIaNWQ9vCX~ zyWw)6gmA+}j=|`xk5;qkFK*(sIUU3+`R)DfQoHB5cgPMDO$m;_&Y+|m0P4FQ>O_3P z3L48!g@hsm@RC}&ua`4dPx$Eo;pMrq>V7HBffD(0;z3dSIs8qol@RU817Nyj%RGvjd23F<-MlH&T2{ogSN)Wc+o zsgDF`mh_Ym-;hHINI9xQ6w{SeSIxYcK{ZLMTp4$~?c*4S_Y?A~B;nt`>6cX-Y%@Ff zbS{(c;$OKwEd4%qZRZ&J$J80W?MOV`mVgqvjea=LEhR5M{(D`O^80~P;%LsnQ2wSX z2M!vQWLe*Nwhm7KpSxC*zl-?B9sz@r{>6m>)bHbKYz;r687b0OAW*8o$D}Nn^5)Nr z<~)Sf9PuW%Wty~D?49cmc*(ZCFp*CGT7ICO&HtUOJ@bQfnIVgw>>-KxV{U~$I)YcS z&YrS<^dwiz^yuTERu5(AXfo^x9Xbe};NyHBnpp{$Eo zlAxiXab)7HyVQudbNR2^N?>U56(TdzS~ODvAgzG_o(Bt@p|5r!>K03D(V^6c+Uf*}M$4nKXei0gssM;FvHu9PtGv z;$PN|nyF|=#Du@RjRud(rMfQ~cnjavu!9J<^w2D#m9kpA=8qd{-FMc^A`CGPfR@*o zaA1U5d(bc2xS%N|*ssWOxjuhZc>)(_YP{||Yw;Z7p#J|*1L}>%B>E7c_{ikcrVX9l z^2*WD@>ccRBYFA?WJ<0jnc-YQ*q9lzqmT9$BBWPZQHhFUO%^^?W>rL82U(lh{i2_~ z2+L*m8$viD6(R?_jE)=kB&%Cr|D8ES%lY>kokVbE6m8K1VU6b|L zs30R7ETMHY>7JH9Uwn<1kbDnOba(dc=|g4rPo3gsmR6$?UOttjf%VHou)ndj4lDzTfremje4Nm(6Z7Zogb$Zy;onnAAv^V3AO zk5PCPsAAOjV1=3L^s9srqDs}<+ulu~AF(_mxKODb{c-aP*(Z};nC^=Xm6A)JKMOZp zuI*@m0tyCc4RB&xcLYDZGlL1E+9u4Op}*}goaj)zmpyI6Cd7CVwLGHg4;y=04&>+L z9K-Z366b8${7%7)i@jYBm%0$C5&MC0a;+@78Y3B4tx_2JqA{vUE*w%2;m50v;nm;1#d)MMR2dqONJj?>iyeZTo!QJ ziJ(boHEc`x6ns_Vj>>+rCEp<^s9+R`fKpGVm~M$6CWwy6h&aKN8;@_J*>bG>HCxkk z^?xnvihM{LYDIU;m|a&oWn=}3jKJ8t%+LB~1U!;uJq>PAbdR3E{mm zYjQ6#;_Nu$MZ`agB?ObJjShlIVEpL^diNmdumifG4As~UxsmeuOI}UEK8op&`i4Vl zC^wHC5B!xWC=w7yKgs5gLe((&=sRg0$H6bwcEbQ0yiGU-wtvRgQFFe$Nj9#0!fEKx z9Kp=B9&uXL?q{E+%&0E}P)XjMrf4%oIp=NTid7iXeWU5ce1H?VUA$kD{7Cce=;aoM zUQJa5Kc~%hvnA3=A*uY%>7N45M%eD-dKnxM;0ooy{D2=lh02$ti%lZ(#o{u=eeUfv z?bu?F_$arK_x#y1#-AN5u5{I>G72C}+KbNf9jYL=xTbp_VhK=T^m*Dq6egU}L@`MX zCP|?=hc4oqvZv-Gbcg&N(gs_zj}JY*enk)xlFhMz>$T}LqMSeqd1K|O3CP1H`LRX0Nz~16*DyKhxlpn^P(MLPpSyMrUO;Ss(-7- zV#)LCZq$Tb${CXUxca6*%mRU4?-!K1hP9# z;T43r`l8b+2t4Ja-Xe$Y4n77B-Y^j54)`(nZkQYx-=a`7Ek^Ovm_3N{ts|w1Onf}t zJLCd{EA6dtSbIKGblMuc)q=X6!gxa)h-8beVEMN`VBlv)zt-rrTiT_y2 zN~K?LQ#oFq^=V3hi+W7`_dpknelhLpHW*;uy@d87gkS~PN=|?Qg?rv`e9QT&ZA+d} z%2CMzxi@O&E+b_{sWDvWcviTD=?+;MnF_?t_!39FrtD*DA77UO@CC9%&E7M@}__~(qo&n__T zcZ=23>45-4oIR*`orp;*u!XKuyhCc)4)CSjq$4YG=bBP*%lq$${UPXHjYMQ}aw#2r z7TNhs_X^kV-Ls(db+bp=It;IVQhY&WPns7#zA&ZHuX&Ct zazkeEaf{F8#XCx7yq6oM{-bJjo+-e}F*(c>r6VH2rg{T=K%I`Tm@W%9|2s9d+PYm; zRUTC-61F07|BaEn`&eF9ozbHY^F`!u5;828f;35yvCuVf6z{3D~w2>y@zwO@OTc7L6^X;6o{G zXj)_Z09(vRM&W%^n*WW@pBO_TxyJ{UKlR@|-nH-lDCRn&npoE^7Nir3^b!$75CJJx z0)Z=4g&Z3oQP6`(i4YZoA|Vlw-Ywt-Aru8XNRvQlh8l_}5K)kJNKpd_G699bOmgmc ze}DJSw{H10Yvsqxnm6-4@3Z%Q_J;IeG<$9@n$_AaDN-cf1jE_pQlLv;9&K@zg-?Lz zd}Nmk`BlYy)k?Yf^fJ0@cjQj{5xp~kza6DV046C7@Jc-ty+yx7cVJ{7P4IXz^xe<1K_bWDU*rnV&wtCjiD5#+KtbTGI*$O$@Bg-3)H1cVagJnB2_4w#ky!SH9dOAhZ@%EKIyO3PKyJ?7}J)ncom z^K4&=(Uv6ysK0yZ12Dd(3&!v_f9)I794dGY);nkhCZd8%gU0UApRp=y-?;Z2Sa;ON zZ{6(def4Cll>P{sRqF?wsYxKy$`=QKzD~N%d-}fHnGCq55wPvl+o`>tYaytkxFZ4I z)Vpn{ry~D|(UYpv=SJB3K+EuQUxb`EL@uHG;H7?7czw>S6!KF(0Wt#xF9{^m|p2Bz^+9}%2{DDaGp(Sc{WpC_r z!~0))<|egs1!HsL&AgU^zRUSqjgIMm0y|g^{B>TQfLRuFrmnKEka(K^u8*rq;ajcY zMysp^Sd8q{#PmGfJmcNjocC?_`!i({(q({c>UXiUE&N=Oj3QjJCneU7316VBI0(eo zF}p*;qBeZl?fhc`?cWB=f*Z%`r)UpQReJ`%NES&;J9g$%9_pge6-9`NVJUKFH(8#D z2Lv4?J$`vQIv67bR(9HHr8<5fB3P}UDbbeG`nc4h`=%N3bIwQYge)=1Jn;nGkUkE%`IoqOEN5> zy3n4hP%*sPKIlN0?VxlZ#6d2{thlJA*KdN3Q2>Yh6wV!tWzg$N7xFHh^{btkSF;qR zXkfQ)Wc9cjAof*s2NJ#DzX*T~>i{0Z5cC4Z{Q%N44mw$+!~YU@W$%lnn;WCb=<>m!)CNR7;LNulo{jc6|2qo~m3;9;3IOmKBD>F`L~{$~rideKL;w6M2E zcdeFR5A$(-dO7Gx#q9l~mk)9;z?{X5zysb?1d;?4$ai66Dh3}x_Nu^eiU>m@a&9Ho z+oRUc#O>30H_I9P?T^R4xJfVftEa8tK4U;*+R`XodDtKtIN&bf!SBuHE+>+ds6Zht zv;sQ`8Z4&6ZoZT^`DuKXX$;=fyQ{m&?sIoSg1Il7dP0viu(kg%?A zrvkFep)-JCb`%$(R2CP-^B&%NE&1vFpU4^e#p-uqS+SD4?UE2(lP2G%)*ivz(3H3@ zb?p)OgAH(yR>! zrhq#rDRfm3&8e=t#FyzN88w1rwuw1%ZrDG-vX8U0nbm}A(#ZVwr=FHtQiA^HjoOY8 ziH`SP3kXPgRV1Ym&Kg#M4vwQv?(WW14QW@g)A&laY6NnVsZsflt@E{RrZKMT8zd3e z}?i|8GegjASND90F{pV;2?uDYSQLC_x% zboqf0DI-_&xJgZxA<4UYh87Qn2=AfJ32a3(rhpp5OVyLUGZRiQLQb*dOrKWX^V`} zy83d^y2PTXQ{6yS+FQx$HJB)pgXna=X+e7|X&)@o{5B8um?zYm&To7H^Mqw*4}qIa@0!o5)jda$7CT+A5FY0{0ECMq<2W@Llk1DK$VB3$GGBNYh-&zV4BOy?SE4Ch zRlhg4B=C6Mmb#n7Z$!yzixJUI(E~WRg*w8&l0#9P>LFRY`z{V3duFWIgj$@$-x~(M zP9^!DE7cmm8Z65n>iD!|Ab&@IU;L%s#slq(Lb!g~jFJ=v_{!{qy?9OUb0IumV5o*& z@aW-l8SONG{3{q_RhP7%X#4fZaZo$}zKnn-{5W?o{TUYI_UY=k zBwn68P-&W&6sAg))6<(uB^!K8FT0`SpnFuOWBjDxJh@W?h2|`${UxRY;W?Q}elny@ zL^NnqJ#UBn@W2W9@PiSXUvOZIy{oa&P1iFb>fTZHfxYKmiZR4Url6UbOTI~& zEcFEni}^?4?13)W<8C&~(OK?^T;ySmzPqg_JrkJS+E`@vqpE~zHZSe%0tI+-r`dP> z1a1ujV0tOrApc)NLd|BP{rGMswXzEB+&mf}d~;%Gg>iURE79eshK9NXYDA*{;r}S} zEi8zlOmt&c*I9u5~0vnCcmrZ@mrU zug-8HvIM?hP-y;UBhp^5k~Kq#4@0TcIB=rf2^a<3nwKJtvd_&>!SU_BhKHNyLW-%;lO&Arc+fxeaI2Q^9I+V)vRgBhmnQ2 zIyd#Z+)WucnlKh7@2E()DGa1c@H+08HCGUCyjvog!}$X#BAvaebR&S4aoOnfo1}%) zt={^|%_a97;|^_V-o0W5v${8hdF`w=w0xlO$#=9ESlWZsri)L3GCg-Po|V>gYnXm& z9J9uy<`&xSA!Kt`o+hCm-Z$E@CvcB<3P@gy5z@Z5XP|*ogXFX&9H#+zo0&{qa{hDb z?E5LtG4b@LbqY1sHLI^rSzg12l>84UvKp%%&Bd4rWk+GH=bwQLf2!U?aqFgXkGHfb~(skc#*p-rCqH~gH$qG7+JgaXW6tAGQ7kO@$R z{g#n7D@T%MC z<~m>9tP*i|f2HIOoUM}WvF#bWp-4j|lWj^z7SlVYtuk<{W$>Ch3Y*ESR+5CUqD)xu zmso9^IcG@~Ee^Jy;LrBNS*!LFs!n|{)w@EKv5Bnw>|i)*oO-`a-j!>o2!A|Sx)vxS zl8NFDPkOF8@u`L4uxXAmarIJtL#Pj$Iw^UYc!lM-uY0%B6ZyZRym*A|o7VAn(X1k} zIAItx^y)_5vpX~@GCp^k%UVvsquh&Uf;@B7G(9U;rz1Pfe1hM3cgS{2rn!mn?n@NG z540z3~Pka-2IRPp4r{F&`aeLQzvo_ZE{WYZCf8n z>KuDCXl#IZt7)`leajj=D-in_M2e)v`L9RWq}=i;7uKu-qzs?WoC0M=OOLPi`yYb* z3!ljT)5e7}<=dP;=9+HxjXXj&w<-pW#*hWB@hYtP$ zru1DHH#H4Ne}8^<@OPg{<6YSU-dvl%Wmq-TZ5;kD$p!zT8u|ap<2=kPiSSepIHW%^ zimTu^rb|z_R2T&mviq~Ptkmi77ox_^|D_}NUpa;UYrnhUal59L($9UAWyya~|1^HR dXufVvGvF12&bFOY`Zk@lY0kEFMB=ZBe*ks#eC+@L literal 0 HcmV?d00001 diff --git a/auth.ts b/auth.ts new file mode 100644 index 0000000..7c0c6a0 --- /dev/null +++ b/auth.ts @@ -0,0 +1,39 @@ +import NextAuth, { type DefaultSession } from 'next-auth' +import GitHub from 'next-auth/providers/github' + +declare module 'next-auth' { + interface Session { + user: { + /** The user's id. */ + id: string + } & DefaultSession['user'] + } +} + +export const { + handlers: { GET, POST }, + auth +} = NextAuth({ + providers: [GitHub], + callbacks: { + jwt({ token, profile }) { + if (profile) { + token.id = profile.id + token.image = profile.avatar_url || profile.picture + } + return token + }, + session: ({ session, token }) => { + if (session?.user && token?.id) { + session.user.id = String(token.id) + } + return session + }, + authorized({ auth }) { + return !!auth?.user // this ensures there is a logged in user for -every- request + } + }, + pages: { + signIn: '/sign-in' // overrides the next-auth default signin page https://authjs.dev/guides/basics/pages + } +}) diff --git a/components/button-scroll-to-bottom.tsx b/components/button-scroll-to-bottom.tsx new file mode 100644 index 0000000..436a6c0 --- /dev/null +++ b/components/button-scroll-to-bottom.tsx @@ -0,0 +1,34 @@ +'use client' + +import * as React from 'react' + +import { cn } from '@/lib/utils' +import { useAtBottom } from '@/lib/hooks/use-at-bottom' +import { Button, type ButtonProps } from '@/components/ui/button' +import { IconArrowDown } from '@/components/ui/icons' + +export function ButtonScrollToBottom({ className, ...props }: ButtonProps) { + const isAtBottom = useAtBottom() + + return ( + + ) +} diff --git a/components/chat-history.tsx b/components/chat-history.tsx new file mode 100644 index 0000000..7450fe8 --- /dev/null +++ b/components/chat-history.tsx @@ -0,0 +1,46 @@ +import * as React from 'react' + +import Link from 'next/link' + +import { cn } from '@/lib/utils' +import { SidebarList } from '@/components/sidebar-list' +import { buttonVariants } from '@/components/ui/button' +import { IconPlus } from '@/components/ui/icons' + +interface ChatHistoryProps { + userId?: string +} + +export async function ChatHistory({ userId }: ChatHistoryProps) { + return ( +
+
+ + + New Chat + +
+ + {Array.from({ length: 10 }).map((_, i) => ( +
+ ))} +
+ } + > + {/* @ts-ignore */} + +
+
+ ) +} diff --git a/components/chat-list.tsx b/components/chat-list.tsx new file mode 100644 index 0000000..0aa677e --- /dev/null +++ b/components/chat-list.tsx @@ -0,0 +1,27 @@ +import { type Message } from 'ai' + +import { Separator } from '@/components/ui/separator' +import { ChatMessage } from '@/components/chat-message' + +export interface ChatList { + messages: Message[] +} + +export function ChatList({ messages }: ChatList) { + if (!messages.length) { + return null + } + + return ( +
+ {messages.map((message, index) => ( +
+ + {index < messages.length - 1 && ( + + )} +
+ ))} +
+ ) +} diff --git a/components/chat-message-actions.tsx b/components/chat-message-actions.tsx new file mode 100644 index 0000000..d4e4b40 --- /dev/null +++ b/components/chat-message-actions.tsx @@ -0,0 +1,40 @@ +'use client' + +import { type Message } from 'ai' + +import { Button } from '@/components/ui/button' +import { IconCheck, IconCopy } from '@/components/ui/icons' +import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard' +import { cn } from '@/lib/utils' + +interface ChatMessageActionsProps extends React.ComponentProps<'div'> { + message: Message +} + +export function ChatMessageActions({ + message, + className, + ...props +}: ChatMessageActionsProps) { + const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 }) + + const onCopy = () => { + if (isCopied) return + copyToClipboard(message.content) + } + + return ( +
+ +
+ ) +} diff --git a/components/chat-message.tsx b/components/chat-message.tsx new file mode 100644 index 0000000..76c9370 --- /dev/null +++ b/components/chat-message.tsx @@ -0,0 +1,77 @@ +import { Message } from 'ai' +import remarkGfm from 'remark-gfm' +import remarkMath from 'remark-math' + +import { cn } from '@/lib/utils' +import { CodeBlock } from '@/components/ui/codeblock' +import { MemoizedReactMarkdown } from '@/components/markdown' +import { IconAI, IconUser } from '@/components/ui/icons' +import { ChatMessageActions } from '@/components/chat-message-actions' + +export interface ChatMessageProps { + message: Message +} + +export function ChatMessage({ message, ...props }: ChatMessageProps) { + return ( +
+
+ {message.role === 'user' ? : } +
+
+ {children}

+ }, + code({ node, inline, className, children, ...props }) { + if (children.length) { + if (children[0] == '▍') { + return ( + + ) + } + + children[0] = (children[0] as string).replace('`▍`', '▍') + } + + const match = /language-(\w+)/.exec(className || '') + + if (inline) { + return ( + + {children} + + ) + } + + return ( + + ) + } + }} + > + {message.content} +
+ +
+
+ ) +} diff --git a/components/chat-panel.tsx b/components/chat-panel.tsx new file mode 100644 index 0000000..c92844a --- /dev/null +++ b/components/chat-panel.tsx @@ -0,0 +1,105 @@ +import * as React from 'react' +import { type UseChatHelpers } from 'ai/react' + +import { shareChat } from '@/app/actions' +import { Button } from '@/components/ui/button' +import { PromptForm } from '@/components/prompt-form' +import { ButtonScrollToBottom } from '@/components/button-scroll-to-bottom' +import { IconRefresh, IconShare, IconStop } from '@/components/ui/icons' +import { FooterText } from '@/components/footer' +import { ChatShareDialog } from '@/components/chat-share-dialog' + +export interface ChatPanelProps + extends Pick< + UseChatHelpers, + | 'append' + | 'isLoading' + | 'reload' + | 'messages' + | 'stop' + | 'input' + | 'setInput' + > { + id?: string + title?: string +} + +export function ChatPanel({ + id, + title, + isLoading, + stop, + append, + reload, + input, + setInput, + messages +}: ChatPanelProps) { + const [shareDialogOpen, setShareDialogOpen] = React.useState(false) + + return ( +
+ +
+
+ {isLoading ? ( + + ) : ( + messages?.length >= 2 && ( +
+ + {id && title ? ( + <> + + setShareDialogOpen(false)} + shareChat={shareChat} + chat={{ + id, + title, + messages + }} + /> + + ) : null} +
+ ) + )} +
+
+ { + await append({ + id, + content: value, + role: 'user' + }) + }} + input={input} + setInput={setInput} + isLoading={isLoading} + /> + +
+
+
+ ) +} diff --git a/components/chat-scroll-anchor.tsx b/components/chat-scroll-anchor.tsx new file mode 100644 index 0000000..ac809f4 --- /dev/null +++ b/components/chat-scroll-anchor.tsx @@ -0,0 +1,29 @@ +'use client' + +import * as React from 'react' +import { useInView } from 'react-intersection-observer' + +import { useAtBottom } from '@/lib/hooks/use-at-bottom' + +interface ChatScrollAnchorProps { + trackVisibility?: boolean +} + +export function ChatScrollAnchor({ trackVisibility }: ChatScrollAnchorProps) { + const isAtBottom = useAtBottom() + const { ref, entry, inView } = useInView({ + trackVisibility, + delay: 100, + rootMargin: '0px 0px -150px 0px' + }) + + React.useEffect(() => { + if (isAtBottom && trackVisibility && !inView) { + entry?.target.scrollIntoView({ + block: 'start' + }) + } + }, [inView, entry, isAtBottom, trackVisibility]) + + return
+} diff --git a/components/chat-share-dialog.tsx b/components/chat-share-dialog.tsx new file mode 100644 index 0000000..83bac71 --- /dev/null +++ b/components/chat-share-dialog.tsx @@ -0,0 +1,106 @@ +'use client' + +import * as React from 'react' +import { type DialogProps } from '@radix-ui/react-dialog' +import { toast } from 'react-hot-toast' + +import { ServerActionResult, type Chat } from '@/lib/types' +import { Button } from '@/components/ui/button' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle +} from '@/components/ui/dialog' +import { IconSpinner } from '@/components/ui/icons' +import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard' + +interface ChatShareDialogProps extends DialogProps { + chat: Pick + shareChat: (id: string) => ServerActionResult + onCopy: () => void +} + +export function ChatShareDialog({ + chat, + shareChat, + onCopy, + ...props +}: ChatShareDialogProps) { + const { copyToClipboard } = useCopyToClipboard({ timeout: 1000 }) + const [isSharePending, startShareTransition] = React.useTransition() + + const copyShareLink = React.useCallback( + async (chat: Chat) => { + if (!chat.sharePath) { + return toast.error('Could not copy share link to clipboard') + } + + const url = new URL(window.location.href) + url.pathname = chat.sharePath + copyToClipboard(url.toString()) + onCopy() + toast.success('Share link copied to clipboard', { + style: { + borderRadius: '10px', + background: '#333', + color: '#fff', + fontSize: '14px' + }, + iconTheme: { + primary: 'white', + secondary: 'black' + } + }) + }, + [copyToClipboard, onCopy] + ) + + return ( + + + + Share link to chat + + Anyone with the URL will be able to view the shared chat. + + +
+
{chat.title}
+
+ {chat.messages.length} messages +
+
+ + + +
+
+ ) +} diff --git a/components/chat.tsx b/components/chat.tsx new file mode 100644 index 0000000..c7a40e5 --- /dev/null +++ b/components/chat.tsx @@ -0,0 +1,72 @@ +'use client' + +import { useChat, type Message } from 'ai/react' + +import { cn } from '@/lib/utils' +import { ChatList } from '@/components/chat-list' +import { ChatPanel } from '@/components/chat-panel' +import { EmptyScreen } from '@/components/empty-screen' +import { ChatScrollAnchor } from '@/components/chat-scroll-anchor' +import { useLocalStorage } from '@/lib/hooks/use-local-storage' +import { useState } from 'react' +import { toast } from 'react-hot-toast' +import { usePathname, useRouter } from 'next/navigation' + +const IS_PREVIEW = process.env.VERCEL_ENV === 'preview' +export interface ChatProps extends React.ComponentProps<'div'> { + initialMessages?: Message[] + id?: string +} + +export function Chat({ id, initialMessages, className }: ChatProps) { + const router = useRouter() + const path = usePathname() + const [previewToken, setPreviewToken] = useLocalStorage( + 'ai-token', + null + ) + const { messages, append, reload, stop, isLoading, input, setInput } = + useChat({ + initialMessages, + id, + body: { + id, + previewToken + }, + onResponse(response) { + if (response.status === 401) { + toast.error(response.statusText) + } + }, + onFinish() { + if (!path.includes('chat')) { + router.push(`/chat/${id}`, { scroll: false }) + router.refresh() + } + } + }) + return ( + <> +
+ {messages.length ? ( + <> + + + + ) : ( + + )} +
+ + + ) +} diff --git a/components/clear-history.tsx b/components/clear-history.tsx new file mode 100644 index 0000000..553d2db --- /dev/null +++ b/components/clear-history.tsx @@ -0,0 +1,77 @@ +'use client' + +import * as React from 'react' +import { useRouter } from 'next/navigation' +import { toast } from 'react-hot-toast' + +import { ServerActionResult } from '@/lib/types' +import { Button } from '@/components/ui/button' +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger +} from '@/components/ui/alert-dialog' +import { IconSpinner } from '@/components/ui/icons' + +interface ClearHistoryProps { + isEnabled: boolean + clearChats: () => ServerActionResult +} + +export function ClearHistory({ + isEnabled = false, + clearChats +}: ClearHistoryProps) { + const [open, setOpen] = React.useState(false) + const [isPending, startTransition] = React.useTransition() + const router = useRouter() + + return ( + + + + + + + Are you absolutely sure? + + This will permanently delete your chat history and remove your data + from our servers. + + + + Cancel + { + event.preventDefault() + startTransition(() => { + clearChats().then(result => { + if (result && 'error' in result) { + toast.error(result.error) + return + } + + setOpen(false) + router.push('/') + }) + }) + }} + > + {isPending && } + Delete + + + + + ) +} diff --git a/components/empty-screen.tsx b/components/empty-screen.tsx new file mode 100644 index 0000000..2ee64c5 --- /dev/null +++ b/components/empty-screen.tsx @@ -0,0 +1,36 @@ +import { UseChatHelpers } from 'ai/react' + +import { Button } from '@/components/ui/button' +import { IconArrowRight } from '@/components/ui/icons' + +const exampleMessages = [ + +]; + +export function EmptyScreen({ setInput }: Pick) { + return ( +
+
+

+ Hello, I'm GeminiChat! +

+

+ Welcome to GeminiChat, an AI chatbot powered by GeminiProApi. This project utilizes the GeminiAPI interface and implements GitHub authentication for login. It is deployed on Vercel. We assure you that we do not collect any user information and warmly invite you to enjoy our service free of charge. +

+
+ {exampleMessages.map((message, index) => ( + + ))} +
+
+
+ ) +} diff --git a/components/external-link.tsx b/components/external-link.tsx new file mode 100644 index 0000000..ba6cc01 --- /dev/null +++ b/components/external-link.tsx @@ -0,0 +1,29 @@ +export function ExternalLink({ + href, + children +}: { + href: string + children: React.ReactNode +}) { + return ( + + {children} + + + ) +} diff --git a/components/footer.tsx b/components/footer.tsx new file mode 100644 index 0000000..b37137d --- /dev/null +++ b/components/footer.tsx @@ -0,0 +1,25 @@ +import React from 'react' + +import { cn } from '@/lib/utils' +import { ExternalLink } from '@/components/external-link' + +export function FooterText({ className, ...props }: React.ComponentProps<'p'>) { + return ( +

+ AI chatbot powered by Google's Gemini Pro -{' '} + + liuziting + -{' '} + + sinnedpenguin + + . +

+ ) +} diff --git a/components/header.tsx b/components/header.tsx new file mode 100644 index 0000000..cfa9300 --- /dev/null +++ b/components/header.tsx @@ -0,0 +1,79 @@ +import * as React from 'react' +import Link from 'next/link' + +import { cn } from '@/lib/utils' +import { auth } from '@/auth' +import { Button, buttonVariants } from '@/components/ui/button' +import { + IconAI, + IconDiscord, + IconGitHub, + IconSeparator, +} from '@/components/ui/icons' +import { UserMenu } from '@/components/user-menu' +import { SidebarMobile } from './sidebar-mobile' +import { SidebarToggle } from './sidebar-toggle' +import { ChatHistory } from './chat-history' + +async function UserOrLogin() { + const session = await auth() + return ( + <> + {session?.user ? ( + <> + + + + + + ) : ( + + + + + )} +
+ + {session?.user ? ( + + ) : ( + + )} +
+ + ) +} + +export function Header() { + return ( +
+
+ }> + + +
+ +
+ ) +} diff --git a/components/login-button.tsx b/components/login-button.tsx new file mode 100644 index 0000000..ae8f842 --- /dev/null +++ b/components/login-button.tsx @@ -0,0 +1,42 @@ +'use client' + +import * as React from 'react' +import { signIn } from 'next-auth/react' + +import { cn } from '@/lib/utils' +import { Button, type ButtonProps } from '@/components/ui/button' +import { IconGitHub, IconSpinner } from '@/components/ui/icons' + +interface LoginButtonProps extends ButtonProps { + showGithubIcon?: boolean + text?: string +} + +export function LoginButton({ + text = 'Login with GitHub', + showGithubIcon = true, + className, + ...props +}: LoginButtonProps) { + const [isLoading, setIsLoading] = React.useState(false) + return ( + + ) +} diff --git a/components/markdown.tsx b/components/markdown.tsx new file mode 100644 index 0000000..d449146 --- /dev/null +++ b/components/markdown.tsx @@ -0,0 +1,9 @@ +import { FC, memo } from 'react' +import ReactMarkdown, { Options } from 'react-markdown' + +export const MemoizedReactMarkdown: FC = memo( + ReactMarkdown, + (prevProps, nextProps) => + prevProps.children === nextProps.children && + prevProps.className === nextProps.className +) diff --git a/components/prompt-form.tsx b/components/prompt-form.tsx new file mode 100644 index 0000000..fdb7c7b --- /dev/null +++ b/components/prompt-form.tsx @@ -0,0 +1,97 @@ +import * as React from 'react' +import Textarea from 'react-textarea-autosize' +import { UseChatHelpers } from 'ai/react' +import { useEnterSubmit } from '@/lib/hooks/use-enter-submit' +import { cn } from '@/lib/utils' +import { Button, buttonVariants } from '@/components/ui/button' +import { + Tooltip, + TooltipContent, + TooltipTrigger +} from '@/components/ui/tooltip' +import { IconArrowElbow, IconPlus } from '@/components/ui/icons' +import { useRouter } from 'next/navigation' + +export interface PromptProps + extends Pick { + onSubmit: (value: string) => void + isLoading: boolean +} + +export function PromptForm({ + onSubmit, + input, + setInput, + isLoading +}: PromptProps) { + const { formRef, onKeyDown } = useEnterSubmit() + const inputRef = React.useRef(null) + const router = useRouter() + React.useEffect(() => { + if (inputRef.current) { + inputRef.current.focus() + } + }, []) + + return ( +
{ + e.preventDefault() + if (!input?.trim()) { + return + } + setInput('') + await onSubmit(input) + }} + ref={formRef} + > +
+ + + + + New Chat + +