Skip to content

📦 NEW: StyleScribe AI Assistant #18

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/style-scribe-bot/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
NEXT_LB_PIPE_API_KEY=""
54 changes: 54 additions & 0 deletions examples/style-scribe-bot/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# 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/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env
.env*.local
.copy.local.env
.copy.remote.env

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts

# Supabase
seed.sql
xseed.sql
xxseed.sql
-seed.sql
/supabase/seed.sql
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/

# No lock files.
package-lock.json
yarn.lock
dist
79 changes: 79 additions & 0 deletions examples/style-scribe-bot/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
![StyleScribe AI Assistant by ⌘ Langbase][cover]

![License: MIT][mit] [![Fork to ⌘ Langbase][fork]][pipe]

## Build a StyleScribe AI Assistant with Pipes — ⌘ Langbase

This chatbot is built by using an AI Pipe on Langbase, it works with 30+ LLMs (OpenAI, Gemini, Mistral, Llama, Gemma, etc), any Data (10M+ context with Memory sets), and any Framework (standard web API you can use with any software).

Check out the live demo [here][demo].

## Features

- 💬 [StyleScribe AI Assistant][demo] — Built with an [AI Pipe on ⌘ Langbase][pipe]
- ⚡️ Streaming — Real-time chat experience with streamed responses
- 🗣️ Q/A — Ask questions and get pre-defined answers with your preferred AI model and tone
- 🔋 Responsive and open source — Works on all devices and platforms

## Learn more

1. Check the [StyleScribe AI Assistant Pipe on ⌘ Langbase][pipe]
2. Read the [source code on GitHub][gh] for this example
3. Go through Documentaion: [Pipe Quick Start][qs]
4. Learn more about [Pipes & Memory features on ⌘ Langbase][docs]

## Get started

Let's get started with the project:

To get started with Langbase, you'll need to [create a free personal account on Langbase.com][signup] and verify your email address. _Done? Cool, cool!_

1. Fork the [StyleScribe AI Assistant][pipe] Pipe on ⌘ Langbase.
2. Go to the API tab to copy the Pipe's API key (to be used on server-side only).
3. Download the example project folder from [here][download] or clone the reppository.
4. `cd` into the project directory and open it in your code editor.
5. Duplicate the `.env.example` file in this project and rename it to `.env.local`.
6. Add the following environment variables (.env.local):
```
# Replace `PIPE_API_KEY` with the copied API key.
NEXT_LB_PIPE_API_KEY="PIPE_API_KEY"
```

7. Issue the following in your CLI:
```sh
# Install the dependencies using the following command:
npm install

# Run the project using the following command:
npm run dev
```

8. Your app template should now be running on [localhost:3000][local].

> NOTE:
> This is a Next.js project, so you can build and deploy it to any platform of your choice, like Vercel, Netlify, Cloudflare, etc.
---

## Authors

This project is created by [Langbase][lb] team members, with contributions from:

- Muhammad-Ali Danish - Software Engineer, [Langbase][lb] <br>
**_Built by ⌘ [Langbase.com][lb] — Ship hyper-personalized AI assistants with memory!_**


[demo]: https://style-scribe-bot.langbase.dev
[lb]: https://langbase.com
[pipe]: https://beta.langbase.com/examples/style-scribe-bot
[gh]: https://github.com/LangbaseInc/langbase-examples/tree/main/examples/style-scribe-bot
[cover]:https://raw.githubusercontent.com/LangbaseInc/docs-images/main/examples/style-scribe-bot/style-scribe-bot.png
[download]:https://download-directory.github.io/?url=https://github.com/LangbaseInc/langbase-examples/tree/main/examples/style-scribe-bot
[signup]: https://langbase.fyi/io
[qs]:https://langbase.com/docs/pipe/quickstart
[docs]:https://langbase.com/docs
[xaa]:https://x.com/MrAhmadAwais
[xab]:https://x.com/AhmadBilalDev
[local]:http://localhost:3000
[mit]: https://img.shields.io/badge/license-MIT-blue.svg?style=for-the-badge&color=%23000000
[fork]: https://img.shields.io/badge/FORK%20ON-%E2%8C%98%20Langbase-000000.svg?style=for-the-badge&logo=%E2%8C%98%20Langbase&logoColor=000000
57 changes: 57 additions & 0 deletions examples/style-scribe-bot/app/api/chat/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { OpenAIStream, StreamingTextResponse } from 'ai'

export const runtime = 'edge'

/**
* Stream AI Chat Messages from Langbase
*
* @param req
* @returns
*/
export async function POST(req: Request) {
try {
if (!process.env.NEXT_LB_PIPE_API_KEY) {
throw new Error(
'Please set NEXT_LB_PIPE_API_KEY in your environment variables.'
)
}

const endpointUrl = 'https://api.langbase.com/beta/chat'

const headers = {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.NEXT_LB_PIPE_API_KEY}`
}

// Get chat prompt messages and threadId from the client.
const body = await req.json()
const { messages, threadId } = body

const requestBody = {
messages,
...(threadId && { threadId }) // Only include threadId if it exists
}

// Send the request to Langbase API.
const response = await fetch(endpointUrl, {
method: 'POST',
headers,
body: JSON.stringify(requestBody)
})

if (!response.ok) {
const res = await response.json()
throw new Error(`Error ${res.error.status}: ${res.error.message}`)
}

// Handle Langbase response, which is a stream in OpenAI format.
const stream = OpenAIStream(response)
// Respond with a text stream.
return new StreamingTextResponse(stream, {
headers: response.headers
})
} catch (error: any) {
console.error('Uncaught API Error:', error)
return new Response(JSON.stringify(error), { status: 500 })
}
}
Binary file added examples/style-scribe-bot/app/favicon.ico
Binary file not shown.
99 changes: 99 additions & 0 deletions examples/style-scribe-bot/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

/* Zinc */
/* --background: 240 10% 3.9%; */
/* --muted: 240 3.7% 15.9%; */
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
/* --destructive: 0 84.2% 60.2%; */
--destructive: 2.74 92.59% 62.94%;
--destructive-foreground: 0 0% 98%;
--warning: 46.38 70.61% 48.04%;
--warning-foreground: 120 12.5% 3.14%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 5.9% 10%;
--radius: 6px;
--danger: 2.74 92.59% 62.94%;
}

.dark {
/* --background: 120 12.5% 3.14%; */
--background: 240, 3%, 9%;
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
/* --muted: 165 10% 7.84%; */
--muted: 240 3.45% 11.37%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
/* --destructive: 0 62.8% 30.6%; */
--destructive: 356.18 70.61% 48.04%;
--destructive-foreground: 0 0% 98%;
--warning: 46.38 70.61% 48.04%;
--warning-foreground: 120 12.5% 3.14%;
/* --border: 240 3.7% 15.9%; */
--border: 240 2% 14%;
--border-muted: 240 2% 14%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
--danger: 356.18 70.61% 48.04%;
}
}

@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

::selection {
color: hsl(var(--background));
background: hsl(var(--foreground));
}

.google {
display: inline-block;
width: 20px;
height: 20px;
position: relative;
background-size: contain;
background-repeat: no-repeat;
background-position: 50%;
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' viewBox='0 0 48 48'%3E%3Cdefs%3E%3Cpath id='a' d='M44.5 20H24v8.5h11.8C34.7 33.9 30.1 37 24 37c-7.2 0-13-5.8-13-13s5.8-13 13-13c3.1 0 5.9 1.1 8.1 2.9l6.4-6.4C34.6 4.1 29.6 2 24 2 11.8 2 2 11.8 2 24s9.8 22 22 22c11 0 21-8 21-22 0-1.3-.2-2.7-.5-4z'/%3E%3C/defs%3E%3CclipPath id='b'%3E%3Cuse xlink:href='%23a' overflow='visible'/%3E%3C/clipPath%3E%3Cpath clip-path='url(%23b)' fill='%23FBBC05' d='M0 37V11l17 13z'/%3E%3Cpath clip-path='url(%23b)' fill='%23EA4335' d='M0 11l17 13 7-6.1L48 14V0H0z'/%3E%3Cpath clip-path='url(%23b)' fill='%2334A853' d='M0 37l30-23 7.9 1L48 0v48H0z'/%3E%3Cpath clip-path='url(%23b)' fill='%234285F4' d='M48 48L17 24l-4-3 35-10z'/%3E%3C/svg%3E");
}

@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
36 changes: 36 additions & 0 deletions examples/style-scribe-bot/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Header } from '@/components/header'
import cn from 'mxcn'
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import { Toaster } from 'sonner'
import './globals.css'

const inter = Inter({ subsets: ['latin'] })

export const metadata: Metadata = {
title: 'StyleScribe AI Assistant - Langbase',
description: 'Build a StyleScribe AI Assistant with ⌘ Langbase using any LLM model.',
keywords: ['StyleScribe', 'AI Assistant', 'Langbase']
}

export default function RootLayout({
children
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html lang="en">
<body className={cn(inter.className, 'dark bg-background')}>
<div className="flex min-h-screen flex-col px-3 pr-0 pt-6">
<div className="rounded-l-[calc(var(--radius)+2px)] border border-r-0 pb-1 pl-1">
<Toaster />
<Header />
<main className="rounded-l-[calc(var(--radius)+2px)] bg-muted">
{children}
</main>
</div>
</div>
</body>
</html>
)
}
7 changes: 7 additions & 0 deletions examples/style-scribe-bot/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Chatbot } from '@/components/chatbot-page'

export const runtime = 'edge'

export default function ChatPage() {
return <Chatbot />
}
75 changes: 75 additions & 0 deletions examples/style-scribe-bot/components/chat-input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { type UseChatHelpers } from 'ai/react'

import { PromptForm } from '@/components/prompt-form'
import { Button } from '@/components/ui/button'
import { IconRegenerate, IconStop } from '@/components/ui/icons'

export interface ChatInputProps
extends Pick<
UseChatHelpers,
| 'append'
| 'isLoading'
| 'reload'
| 'messages'
| 'stop'
| 'input'
| 'setInput'
> {
id?: string
}

export function ChatInput({
id,
isLoading,
stop,
append,
reload,
input,
setInput,
messages
}: ChatInputProps) {
return (
<div className="fixed inset-x-0 bottom-0">
<div className="xbg-muted mx-auto max-w-3xl sm:max-w-4xl">
<div className="flex h-10 items-center justify-center">
{isLoading ? (
<Button
variant="outline"
onClick={() => stop()}
className="bg-background"
size={'sm'}
>
<IconStop className="text-muted-foreground/50 group-hover:text-background" />
Stop generating
</Button>
) : (
messages?.length > 0 && (
<Button
variant="outline"
onClick={() => reload()}
className="bg-background"
size={'sm'}
>
<IconRegenerate className="size-4 text-muted-foreground/50 group-hover:text-background" />
Regenerate response
</Button>
)
)}
</div>
<div className="space-y-4 py-2 md:pb-4 md:pt-2">
<PromptForm
onSubmit={async value => {
await append({
content: value,
role: 'user'
})
}}
input={input}
setInput={setInput}
isLoading={isLoading}
/>
</div>
</div>
</div>
)
}
27 changes: 27 additions & 0 deletions examples/style-scribe-bot/components/chat-list.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="relative mx-auto max-w-2xl px-4 pb-[100px]">
{messages.map((message, index) => (
<div key={index}>
<ChatMessage message={message} />
{index < messages.length - 1 && (
<Separator className="my-4 md:my-8" />
)}
</div>
))}
</div>
)
}
40 changes: 40 additions & 0 deletions examples/style-scribe-bot/components/chat-message-actions.tsx
Original file line number Diff line number Diff line change
@@ -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 'mxcn'

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 (
<div
className={cn(
'flex items-center justify-end transition-opacity group-hover:opacity-100 md:absolute md:-right-10 md:-top-2 md:opacity-0',
className
)}
{...props}
>
<Button variant="ghost" size="icon" onClick={onCopy}>
{isCopied ? <IconCheck /> : <IconCopy />}
<span className="sr-only">Copy message</span>
</Button>
</div>
)
}
77 changes: 77 additions & 0 deletions examples/style-scribe-bot/components/chat-message.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { Message } from 'ai'
import remarkGfm from 'remark-gfm'
import remarkMath from 'remark-math'

import { ChatMessageActions } from '@/components/chat-message-actions'
import { MemoizedReactMarkdown } from '@/components/markdown'
import { CodeBlock } from '@/components/ui/codeblock'
import { IconSparkles, IconUser } from '@/components/ui/icons'
import cn from 'mxcn'

export interface ChatMessageProps {
message: Message
}

export function ChatMessage({ message, ...props }: ChatMessageProps) {
return (
<div
className={cn('group relative mb-4 flex items-start md:-ml-12')}
{...props}
>
<div
className={cn(
'flex h-8 w-8 shrink-0 select-none items-center justify-center rounded-xl border shadow',
message.role === 'user'
? 'bg-background'
: 'bg-primary text-primary-foreground'
)}
>
{message.role === 'user' ? <IconUser /> : <IconSparkles />}
</div>
<div className="ml-4 flex-1 space-y-2 overflow-hidden px-1">
<MemoizedReactMarkdown
className="prose rounded-xl dark:prose-invert prose-p:leading-relaxed prose-pre:p-0 break-words prose-pre:rounded-xl"
remarkPlugins={[remarkGfm, remarkMath]}
components={{
p({ children }) {
return <p className="mb-2 last:mb-0">{children}</p>
},
code({ node, inline, className, children, ...props }) {
if (children.length) {
if (children[0] == '▍') {
return (
<span className="mt-1 animate-pulse cursor-default"></span>
)
}

children[0] = (children[0] as string).replace('`▍`', '▍')
}

const match = /language-(\w+)/.exec(className || '')

if (inline) {
return (
<code className={className} {...props}>
{children}
</code>
)
}

return (
<CodeBlock
key={Math.random()}
language={(match && match[1]) || ''}
value={String(children).replace(/\n$/, '')}
{...props}
/>
)
}
}}
>
{message.content}
</MemoizedReactMarkdown>
<ChatMessageActions message={message} />
</div>
</div>
)
}
57 changes: 57 additions & 0 deletions examples/style-scribe-bot/components/chatbot-page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
'use client'

import { ChatList } from '@/components/chat-list'
import { useChat, type Message } from 'ai/react'
import cn from 'mxcn'
import { useState } from 'react'
import { toast } from 'sonner'
import { ChatInput } from './chat-input'
import { Opening } from './opening'

export interface ChatProps extends React.ComponentProps<'div'> {
id?: string // Optional: Thread ID if you want to persist the chat in a DB
initialMessages?: Message[] // Optional: Messages to pre-populate the chat from DB
}

export function Chatbot({ id, initialMessages, className }: ChatProps) {
const [threadId, setThreadId] = useState<null | string>(null)
const { messages, append, reload, stop, isLoading, input, setInput } =
useChat({
api: '/api/chat',
initialMessages,
body: { threadId },
onResponse(response) {
if (response.status !== 200) {
console.log('✨ ~ response:', response)
toast.error(response.statusText)
}

// Get Thread ID from response header
const lbThreadId = response.headers.get('lb-thread-id')
setThreadId(lbThreadId)
}
})
return (
<div className="min-h-screen">
<div className={cn('pb-36 pt-4 md:pt-10', className)}>
{messages.length ? (
<>
<ChatList messages={messages} />
</>
) : (
<Opening />
)}
</div>
<ChatInput
id={id}
isLoading={isLoading}
stop={stop}
append={append}
reload={reload}
messages={messages}
input={input}
setInput={setInput}
/>
</div>
)
}
47 changes: 47 additions & 0 deletions examples/style-scribe-bot/components/header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { buttonVariants } from '@/components/ui/button'
import cn from 'mxcn'
import Link from 'next/link'
import { IconFork, IconGitHub } from './ui/icons'

export async function Header() {
return (
<header className="bg-background sticky top-0 z-50 flex h-16 w-full shrink-0 items-center justify-between px-4">
<div className="flex h-16 shrink-0 items-center">
<h1>
<Link href="/" className="font-bold">
<span
aria-hidden="true"
className="border-muted-foreground/10 bg-muted mr-1 select-none rounded-lg border px-[0.2rem] py-[0.1rem] text-sm font-bold shadow-2xl"
>
</span>
Langbase
</Link>
</h1>
</div>

<div className="flex items-center justify-end space-x-2">
<a
target="_blank"
href="https://github.com/LangbaseInc/langbase-examples/tree/main/examples/style-scribe-bot"
rel="noopener noreferrer"
className={cn(buttonVariants({ variant: 'outline' }))}
>
<IconGitHub />
<span className="hidden md:flex">GitHub</span>
</a>
<a
target="_blank"
href="https://beta.langbase.com/examples/style-scribe-bot"
rel="noopener noreferrer"
className={cn(buttonVariants({ variant: 'default' }))}
>
<IconFork />
<span className="hidden md:flex gap-1">
Fork on <span className="font-bold">Langbase</span>
</span>
</a>
</div>
</header>
)
}
9 changes: 9 additions & 0 deletions examples/style-scribe-bot/components/markdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { FC, memo } from 'react'
import ReactMarkdown, { Options } from 'react-markdown'

export const MemoizedReactMarkdown: FC<Options> = memo(
ReactMarkdown,
(prevProps, nextProps) =>
prevProps.children === nextProps.children &&
prevProps.className === nextProps.className
)
81 changes: 81 additions & 0 deletions examples/style-scribe-bot/components/opening.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import Link from 'next/link'

export function Opening() {
return (
<div className="mx-auto max-w-3xl px-2 sm:max-w-4xl sm:px-0">
<div className="light:ring-ring:ring-border ring-ring/10 relative my-7 rounded-lg py-3.5 pl-[1.625rem] pr-4 ring-1 ring-inset [--callout-border:theme(colors.indigo.400)] [--callout-icon:theme(colors.indigo.400)] [--callout-title:theme(colors.indigo.400)] dark:[--callout-border:theme(colors.indigo.400)] dark:[--callout-icon:theme(colors.indigo.400)] dark:[--callout-title:theme(colors.indigo.400)] [&>*]:my-0 [&>*]:py-0">
<div className="absolute inset-y-2 left-2 w-0.5 rounded-full bg-[--callout-border]"></div>
<div className="mb-2 mt-0 flex items-center justify-start gap-1">
<span className="text-xs font-medium text-[--callout-title]">
Chatbot Example
</span>
</div>

<div className="mt-2">
<header className="mb-8">
<h4 className="text-foreground text-sm sm:text-base mt-4 flex gap-1 tracking-wide">
<span>StyleScribe AI Assistant by a</span>
<Link
target="_blank"
className="underline hover:text-indigo-400 mb-2"
href="https://beta.langbase.com/examples/style-scribe-bot"
>
<span className="font-bold">pipe on ⌘ Langbase</span>
</Link>
</h4>
<h5 className="text-sm text-muted-foreground">
Ship hyper-personalized AI assistants with memory.
</h5>
</header>

<div className="mt-4 flex flex-col gap-4 text-sm [&>p]:my-0 [&>p]:py-0">
<p>Learn more by checking out:</p>
<div className="flex flex-col gap-4 mt-2 text-sm">
<Dlink href="https://beta.langbase.com/examples/style-scribe-bot">
<span>1.</span>
<span>Fork this StyleScribe AI Assistant Pipe on ⌘ Langbase</span>
</Dlink>
<Dlink href="https://github.com/LangbaseInc/langbase-examples/tree/main/examples/style-scribe-bot">
<span>2.</span>
<span>Use LangUI.dev's open source code components</span>
</Dlink>

<Dlink href="https://langbase.com/docs/pipe/quickstart">
<span>3.</span>
<span>Go through Documentaion: Pipe Quickstart </span>
</Dlink>
<Dlink href="https://langbase.com/docs">
<span>4.</span>
<span>
Learn more about Pipes & Memory features on Langbase
</span>
</Dlink>
</div>
</div>
</div>
</div>
</div>
)
}

// Description Link
function Dlink({
href,
children,
...props
}: {
href: string
children: React.ReactNode
[key: string]: any
}) {
return (
<Link
href={href}
target="_blank"
className="flex hover:text-indigo-400 flex items-center gap-2 [&>span:first-child]:text-indigo-400"
{...props}
>
{children}
</Link>
)
}
95 changes: 95 additions & 0 deletions examples/style-scribe-bot/components/prompt-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { Button } from '@/components/ui/button'
import { IconChat, IconCommand, IconSpinner } from '@/components/ui/icons'
import { useEnterSubmit } from '@/lib/hooks/use-enter-submit'
import { UseChatHelpers } from 'ai/react'
import * as React from 'react'
import Textarea from 'react-textarea-autosize'

export interface PromptProps
extends Pick<UseChatHelpers, 'input' | 'setInput'> {
onSubmit: (value: string) => Promise<void>
isLoading: boolean
}

export function PromptForm({
onSubmit,
input,
setInput,
isLoading
}: PromptProps) {
const { formRef, onKeyDown } = useEnterSubmit()
const inputRef = React.useRef<HTMLTextAreaElement>(null)

React.useEffect(() => {
if (inputRef.current) {
inputRef.current.focus()
}
}, [])

return (
<form
onSubmit={async e => {
e.preventDefault()
if (!input?.trim()) {
return
}
setInput('')
await onSubmit(input)
}}
ref={formRef}
>
<div className="bg-background relative flex max-h-60 w-full grow flex-col overflow-hidden px-2 pb-2 sm:rounded-2xl sm:border">
<div className="flex w-full flex-col">
<label
htmlFor="playground"
className="text-config text-foreground flex justify-between gap-y-4 rounded-xl px-3 py-4 font-medium leading-6 md:flex-row md:items-center md:gap-y-0"
>
<div className="flex items-center gap-x-2">
<IconChat
className="text-muted-foreground/50 h-5 w-5"
aria-hidden="true"
/>
<h3>Chat</h3>
</div>

<div className="flex items-center justify-center gap-2 md:justify-start">
{/* Reset chat */}
<Button
variant="ghost"
className="max-w-xs"
onClick={e => {
e.preventDefault()
location.reload()
}}
>
Reset
</Button>
{/* Send button */}
<Button type="submit" disabled={isLoading || input === ''}>
{isLoading ? (
<IconSpinner />
) : (
<IconCommand className="size-4" />
)}
Send
<span className="sr-only">Send message</span>
</Button>
</div>
</label>
</div>
<Textarea
ref={inputRef}
tabIndex={0}
onKeyDown={onKeyDown}
rows={1}
maxRows={10}
value={input}
onChange={e => setInput(e.target.value)}
placeholder="Enter your prompt message..."
spellCheck={false}
className="bg-muted min-h-[60px] w-full resize-none rounded-lg px-4 py-[1.3rem] focus-within:outline-none sm:text-sm"
/>
</div>
</form>
)
}
70 changes: 70 additions & 0 deletions examples/style-scribe-bot/components/ui/button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'
import * as React from 'react'

import cn from 'mxcn'

const buttonVariants = cva(
'focus-visible:ring-ring-muted-foreground/25 inline-flex cursor-pointer select-none items-center justify-center rounded-lg text-sm font-medium transition-colors focus:ring-1 focus:ring-muted-foreground/25 focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50 gap-2 group',
{
variants: {
variant: {
default:
'border border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/90',
warn: 'bg-warning text-warning-foreground hover:bg-warning/90 shadow-sm',
destructive:
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
'destructive-hover':
'border border-input bg-muted font-bold text-destructive shadow-sm hover:bg-destructive hover:text-destructive-foreground',
'outline-background':
'border border-input bg-background text-foreground shadow-sm transition-colors hover:bg-foreground hover:text-background',
'outline-inverse':
'border border-input bg-muted-foreground text-muted shadow-sm hover:bg-foreground hover:text-background',
outline:
'border border-input bg-transparent shadow-sm hover:bg-foreground hover:text-background',
'outline-muted':
'border border-input bg-muted text-foreground shadow-sm hover:bg-foreground hover:text-background',
secondary:
'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
green:
'rounded-lg bg-green-500 text-primary shadow-sm hover:bg-green-400 dark:bg-green-700 dark:hover:bg-green-800'
},
size: {
default: 'h-9 px-4 py-2',
xs: 'h-6 rounded-lg px-2 text-xs',
sm: 'h-8 rounded-lg px-3 text-xs',
lg: 'h-10 rounded-lg px-8',
xl: 'h-14 rounded-lg px-10',
icon: 'h-9 w-9'
}
},
defaultVariants: {
variant: 'default',
size: 'default'
}
}
)

export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button'
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = 'Button'

export { Button, buttonVariants }
145 changes: 145 additions & 0 deletions examples/style-scribe-bot/components/ui/codeblock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// Inspired by Chatbot-UI and modified to fit the needs of this project
// @see https://github.com/mckaywrigley/chatbot-ui/blob/main/components/Markdown/CodeBlock.tsx

'use client'

import { FC, memo } from 'react'
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
import { coldarkDark } from 'react-syntax-highlighter/dist/cjs/styles/prism'

import { Button } from '@/components/ui/button'
import { IconCheck, IconCopy, IconDownload } from '@/components/ui/icons'
import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard'

interface Props {
language: string
value: string
}

interface languageMap {
[key: string]: string | undefined
}

export const programmingLanguages: languageMap = {
javascript: '.js',
python: '.py',
java: '.java',
c: '.c',
cpp: '.cpp',
'c++': '.cpp',
'c#': '.cs',
ruby: '.rb',
php: '.php',
swift: '.swift',
'objective-c': '.m',
kotlin: '.kt',
typescript: '.ts',
go: '.go',
perl: '.pl',
rust: '.rs',
scala: '.scala',
haskell: '.hs',
lua: '.lua',
shell: '.sh',
sql: '.sql',
html: '.html',
css: '.css'
// add more file extensions here, make sure the key is same as language prop in CodeBlock.tsx component
}

export const generateRandomString = (length: number, lowercase = false) => {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXY3456789' // excluding similar looking characters like Z, 2, I, 1, O, 0
let result = ''
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length))
}
return lowercase ? result.toLowerCase() : result
}

const CodeBlock: FC<Props> = memo(({ language, value }) => {
const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 })

const downloadAsFile = () => {
if (typeof window === 'undefined') {
return
}
const fileExtension = programmingLanguages[language] || '.file'
const suggestedFileName = `file-${generateRandomString(
3,
true
)}${fileExtension}`
const fileName = window.prompt('Enter file name' || '', suggestedFileName)

if (!fileName) {
// User pressed cancel on prompt.
return
}

const blob = new Blob([value], { type: 'text/plain' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.download = fileName
link.href = url
link.style.display = 'none'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
}

const onCopy = () => {
if (isCopied) return
copyToClipboard(value)
}

return (
<div className="codeblock relative rounded-xl w-full bg-zinc-950 font-sans">
<div className="flex w-full rounded-xl items-center justify-between bg-zinc-800 px-6 py-2 pr-4 text-zinc-100">
<span className="text-xs lowercase">{language}</span>
<div className="flex items-center space-x-1">
<Button
variant="ghost"
className="hover:bg-zinc-800 focus-visible:ring-1 focus-visible:ring-slate-700 focus-visible:ring-offset-0"
onClick={downloadAsFile}
size="icon"
>
<IconDownload />
<span className="sr-only">Download</span>
</Button>
<Button
variant="ghost"
size="icon"
className="text-xs hover:bg-zinc-800 focus-visible:ring-1 focus-visible:ring-slate-700 focus-visible:ring-offset-0"
onClick={onCopy}
>
{isCopied ? <IconCheck /> : <IconCopy />}
<span className="sr-only">Copy code</span>
</Button>
</div>
</div>
<SyntaxHighlighter
language={language}
style={coldarkDark}
PreTag="div"
showLineNumbers
customStyle={{
margin: 0,
width: '100%',
background: 'transparent',
padding: '1.5rem 1rem'
}}
codeTagProps={{
style: {
fontSize: '0.9rem',
fontFamily: 'var(--font-mono)'
}
}}
>
{value}
</SyntaxHighlighter>
</div>
)
})
CodeBlock.displayName = 'CodeBlock'

export { CodeBlock }
339 changes: 339 additions & 0 deletions examples/style-scribe-bot/components/ui/icons.tsx

Large diffs are not rendered by default.

32 changes: 32 additions & 0 deletions examples/style-scribe-bot/components/ui/separator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
'use client'

import * as React from 'react'
import * as SeparatorPrimitive from '@radix-ui/react-separator'
import cn from 'mxcn'

const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = 'horizontal', decorative = true, ...props },
ref
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
'shrink-0 bg-border',
orientation === 'horizontal'
? 'h-[2px] w-full border-0 border-b-[1px] border-border bg-muted'
: 'h-full w-[1px]',
className
)}
{...props}
/>
)
)
Separator.displayName = SeparatorPrimitive.Root.displayName

export { Separator }
23 changes: 23 additions & 0 deletions examples/style-scribe-bot/components/ui/textarea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import cn from 'mxcn'
import * as React from 'react'

export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}

const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
'border-input ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex min-h-[80px] w-full rounded-md border bg-transparent px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className
)}
ref={ref}
{...props}
/>
)
}
)
Textarea.displayName = 'Textarea'

export { Textarea }
33 changes: 33 additions & 0 deletions examples/style-scribe-bot/lib/hooks/use-copy-to-clipboard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
'use client'

import * as React from 'react'

export interface useCopyToClipboardProps {
timeout?: number
}

export function useCopyToClipboard({
timeout = 2000
}: useCopyToClipboardProps) {
const [isCopied, setIsCopied] = React.useState<Boolean>(false)

const copyToClipboard = (value: string) => {
if (typeof window === 'undefined' || !navigator.clipboard?.writeText) {
return
}

if (!value) {
return
}

navigator.clipboard.writeText(value).then(() => {
setIsCopied(true)

setTimeout(() => {
setIsCopied(false)
}, timeout)
})
}

return { isCopied, copyToClipboard }
}
23 changes: 23 additions & 0 deletions examples/style-scribe-bot/lib/hooks/use-enter-submit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useRef, type RefObject } from 'react'

export function useEnterSubmit(): {
formRef: RefObject<HTMLFormElement>
onKeyDown: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void
} {
const formRef = useRef<HTMLFormElement>(null)

const handleKeyDown = (
event: React.KeyboardEvent<HTMLTextAreaElement>
): void => {
if (
event.key === 'Enter' &&
!event.shiftKey &&
!event.nativeEvent.isComposing
) {
formRef.current?.requestSubmit()
event.preventDefault()
}
}

return { formRef, onKeyDown: handleKeyDown }
}
11 changes: 11 additions & 0 deletions examples/style-scribe-bot/lib/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { type Message } from 'ai'

export interface Chat extends Record<string, any> {
id: string
title: string
createdAt: Date
userId: string
path: string
messages: Message[]
sharePath?: string
}
8 changes: 8 additions & 0 deletions examples/style-scribe-bot/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export function formatDate(input: string | number | Date): string {
const date = new Date(input)
return date.toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric'
})
}
13 changes: 13 additions & 0 deletions examples/style-scribe-bot/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/** @type {import('next').NextConfig} */
module.exports = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'avatars.githubusercontent.com',
port: '',
pathname: '**'
}
]
}
}
49 changes: 49 additions & 0 deletions examples/style-scribe-bot/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"name": "style-scribe-bot-example",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3",
"ai": "3.0.16",
"class-variance-authority": "^0.7.0",
"mxcn": "^2.0.0",
"next": "14.0.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^8.0.7",
"react-syntax-highlighter": "^15.5.0",
"react-textarea-autosize": "^8.4.1",
"remark-gfm": "^3.0.1",
"remark-math": "^5.1.1",
"sonner": "^1.5.0"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.9",
"@types/node": "^17.0.12",
"@types/react": "^18.0.22",
"@types/react-dom": "^18.0.7",
"@types/react-syntax-highlighter": "^15.5.6",
"@typescript-eslint/parser": "^5.59.7",
"autoprefixer": "^10.4.13",
"eslint": "^8.31.0",
"eslint-config-next": "13.4.19",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-tailwindcss": "^3.12.0",
"postcss": "^8.4.21",
"prettier": "^3.0.3",
"prettier-plugin-tailwindcss": "^0.5.4",
"tailwind-merge": "^1.12.0",
"tailwindcss": "^3.4.1",
"tailwindcss-animate": "^1.0.5",
"typescript": "^5.2.2"
}
}
6 changes: 6 additions & 0 deletions examples/style-scribe-bot/postcss.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
34 changes: 34 additions & 0 deletions examples/style-scribe-bot/prettier.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/** @type {import('prettier').Config} */
module.exports = {
endOfLine: "lf",
semi: false,
useTabs: false,
singleQuote: true,
arrowParens: "avoid",
tabWidth: 2,
trailingComma: "none",
importOrder: [
"^(react/(.*)$)|^(react$)",
"^(next/(.*)$)|^(next$)",
"<THIRD_PARTY_MODULES>",
"",
"^types$",
"^@/types/(.*)$",
"^@/config/(.*)$",
"^@/lib/(.*)$",
"^@/hooks/(.*)$",
"^@/components/ui/(.*)$",
"^@/components/(.*)$",
"^@/registry/(.*)$",
"^@/styles/(.*)$",
"^@/app/(.*)$",
"",
"^[./]"
],
importOrderSeparation: false,
importOrderSortSpecifiers: true,
importOrderBuiltinModulesToTop: true,
importOrderParserPlugins: ["typescript", "jsx", "decorators-legacy"],
importOrderMergeDuplicateImports: true,
importOrderCombineTypeAndValueImports: true
}
Binary file added examples/style-scribe-bot/public/chatbot.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
177 changes: 177 additions & 0 deletions examples/style-scribe-bot/tailwind.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
const { fontFamily } = require('tailwindcss/defaultTheme')
import type { Config } from 'tailwindcss'

const config: Config = {
darkMode: 'selector',
content: [
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
'./app/**/*.{js,ts,jsx,tsx,mdx}'
],
theme: {
transparent: 'transparent',
current: 'currentColor',
extend: {
backgroundImage: {
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
'gradient-conic':
'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))'
},
colors: {
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))'
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))'
},
warning: {
DEFAULT: 'hsl(var(--warning))',
foreground: 'hsl(var(--warning-foreground))'
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))'
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))'
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))'
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
},
// light mode
tremor: {
brand: {
faint: '#eff6ff', // blue-50
muted: '#bfdbfe', // blue-200
subtle: '#60a5fa', // blue-400
DEFAULT: '#3b82f6', // blue-500
emphasis: '#1d4ed8', // blue-700
inverted: '#ffffff' // white
},
background: {
muted: '#f9fafb', // gray-50
subtle: '#f3f4f6', // gray-100
DEFAULT: '#ffffff', // white
emphasis: '#374151' // gray-700
},
border: {
DEFAULT: '#e5e7eb' // gray-200
},
ring: {
DEFAULT: '#e5e7eb' // gray-200
},
content: {
subtle: '#9ca3af', // gray-400
DEFAULT: '#6b7280', // gray-500
emphasis: '#374151', // gray-700
strong: '#111827', // gray-900
inverted: '#ffffff' // white
}
},
// dark mode
'dark-tremor': {
brand: {
faint: 'hsl(var(--background))', // custom
muted: 'hsl(var(--muted))', // blue-950
subtle: '#1e40af', // blue-800
DEFAULT: '#3b82f6', // blue-500
emphasis: '#60a5fa', // blue-400
inverted: 'hsl(var(--muted))' // gray-950
},
background: {
muted: 'hsl(var(--muted))', // custom
subtle: 'hsl(var(--muted))', // gray-800
DEFAULT: 'hsl(var(--background))', // gray-900
emphasis: '#d1d5db' // gray-300
},
border: {
DEFAULT: 'hsl(var(--border))' // gray-800
},
ring: {
DEFAULT: 'hsl(var(--muted))' // gray-800
},
content: {
subtle: '#4b5563', // gray-600
DEFAULT: '#6b7280', // gray-600
emphasis: '#e5e7eb', // gray-200
strong: '#f9fafb', // gray-50
inverted: '#000000' // black
}
}
},
boxShadow: {
// light
'tremor-input': '0 1px 2px 0 rgb(0 0 0 / 0.05)',
'tremor-card':
'0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)',
'tremor-dropdown':
'0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)',
// dark
'dark-tremor-input': '0 1px 2px 0 rgb(0 0 0 / 0.05)',
'dark-tremor-card':
'0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)',
'dark-tremor-dropdown':
'0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)'
},
borderRadius: {
lg: `var(--radius)`,
md: `calc(var(--radius) - 2px)`,
sm: 'calc(var(--radius) - 4px)',
'tremor-small': '0.375rem',
'tremor-default': '0.5rem',
'tremor-full': '9999px'
},
fontSize: {
// 'tremor-label': ['0.75rem'],
'tremor-label': '0.75rem',
'tremor-default': ['0.875rem', { lineHeight: '1.25rem' }],
'tremor-title': ['1.125rem', { lineHeight: '1.75rem' }],
'tremor-metric': ['1.875rem', { lineHeight: '2.25rem' }]
},
fontFamily: {
sans: ['var(--font-sans)', ...fontFamily.sans]
},
keyframes: {
'accordion-down': {
from: { height: '0' },
to: { height: 'var(--radix-accordion-content-height)' }
},
'accordion-up': {
from: { height: 'var(--radix-accordion-content-height)' },
to: { height: '0' }
},

slide: {
to: {
transform: 'translate(calc(100cqw - 100%), 0)'
}
}
},
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out',
// spin: 'spin calc(var(--speed) * 2) infinite linear',
slide: 'slide var(--speed) ease-in-out infinite alternate'
}
}
},
plugins: [require('@tailwindcss/typography')]
}
export default config
29 changes: 29 additions & 0 deletions examples/style-scribe-bot/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"incremental": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
},
"plugins": [
{
"name": "next"
}
],
"strictNullChecks": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}