feat: add tabby playground for q&a use case (#493)
* init commit * support chat * add theme toggle * limit message to 2 lines * update * update formatting * update * update * update * fix formatting * updaterelease-0.2
parent
0d6840e372
commit
eb15933255
|
|
@ -2620,8 +2620,19 @@ version = "6.6.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b68543d5527e158213414a92832d2aab11a84d2571a5eb021ebe22c43aab066"
|
||||
dependencies = [
|
||||
"rust-embed-impl",
|
||||
"rust-embed-utils",
|
||||
"rust-embed-impl 6.5.0",
|
||||
"rust-embed-utils 7.5.0",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed"
|
||||
version = "8.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1e7d90385b59f0a6bf3d3b757f3ca4ece2048265d70db20a2016043d4509a40"
|
||||
dependencies = [
|
||||
"rust-embed-impl 8.0.0",
|
||||
"rust-embed-utils 8.0.0",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
|
|
@ -2633,12 +2644,25 @@ checksum = "4d4e0f0ced47ded9a68374ac145edd65a6c1fa13a96447b873660b2a568a0fd7"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rust-embed-utils",
|
||||
"rust-embed-utils 7.5.0",
|
||||
"shellexpand",
|
||||
"syn 1.0.109",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-impl"
|
||||
version = "8.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c3d8c6fd84090ae348e63a84336b112b5c3918b3bf0493a581f7bd8ee623c29"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rust-embed-utils 8.0.0",
|
||||
"syn 2.0.28",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-utils"
|
||||
version = "7.5.0"
|
||||
|
|
@ -2649,6 +2673,16 @@ dependencies = [
|
|||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-utils"
|
||||
version = "8.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "873feff8cb7bf86fdf0a71bb21c95159f4e4a37dd7a4bd1855a940909b583ada"
|
||||
dependencies = [
|
||||
"sha2",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-stemmers"
|
||||
version = "1.2.0"
|
||||
|
|
@ -3047,7 +3081,7 @@ dependencies = [
|
|||
"nvml-wrapper",
|
||||
"opentelemetry",
|
||||
"opentelemetry-otlp",
|
||||
"rust-embed",
|
||||
"rust-embed 8.0.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serdeconv",
|
||||
|
|
@ -4015,7 +4049,7 @@ dependencies = [
|
|||
"axum",
|
||||
"mime_guess",
|
||||
"regex",
|
||||
"rust-embed",
|
||||
"rust-embed 6.6.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"utoipa",
|
||||
|
|
|
|||
5
Makefile
5
Makefile
|
|
@ -6,3 +6,8 @@ loadtest:
|
|||
|
||||
fix:
|
||||
cargo clippy --fix --allow-dirty --allow-staged && cargo +nightly fmt
|
||||
|
||||
update-playground:
|
||||
cd clients/tabby-playground && yarn build
|
||||
rm -rf crates/tabby/playground && cp -R clients/tabby-playground/out crates/tabby/playground
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
NEXT_PUBLIC_TABBY_SERVER_URL=http://127.0.0.1:8080
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"$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"
|
||||
},
|
||||
"settings": {
|
||||
"tailwindcss": {
|
||||
"callees": ["cn", "cva"],
|
||||
"config": "tailwind.config.js"
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts", "*.tsx"],
|
||||
"parser": "@typescript-eslint/parser"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
# 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
|
||||
|
||||
.contentlayer
|
||||
.env
|
||||
.vercel
|
||||
.vscode
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
Copyright 2023 Vercel, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
<a href="https://chat.vercel.ai/">
|
||||
<img alt="Next.js 13 and app template Router-ready AI chatbot." src="https://chat.vercel.ai/opengraph-image.png">
|
||||
<h1 align="center">Next.js AI Chatbot</h1>
|
||||
</a>
|
||||
|
||||
<p align="center">
|
||||
An open-source AI chatbot app template built with Next.js, the Vercel AI SDK, OpenAI, and Vercel KV.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="#features"><strong>Features</strong></a> ·
|
||||
<a href="#model-providers"><strong>Model Providers</strong></a> ·
|
||||
<a href="#deploy-your-own"><strong>Deploy Your Own</strong></a> ·
|
||||
<a href="#running-locally"><strong>Running locally</strong></a> ·
|
||||
<a href="#authors"><strong>Authors</strong></a>
|
||||
</p>
|
||||
<br/>
|
||||
|
||||
## Features
|
||||
|
||||
- [Next.js](https://nextjs.org) App Router
|
||||
- React Server Components (RSCs), Suspense, and Server Actions
|
||||
- [Vercel AI SDK](https://sdk.vercel.ai/docs) for streaming chat UI
|
||||
- Support for OpenAI (default), Anthropic, Hugging Face, or custom AI chat models and/or LangChain
|
||||
- Edge runtime-ready
|
||||
- [shadcn/ui](https://ui.shadcn.com)
|
||||
- Styling with [Tailwind CSS](https://tailwindcss.com)
|
||||
- [Radix UI](https://radix-ui.com) for headless component primitives
|
||||
- Icons from [Phosphor Icons](https://phosphoricons.com)
|
||||
- Chat History, rate limiting, and session storage with [Vercel KV](https://vercel.com/storage/kv)
|
||||
- [NextAuth.js](https://github.com/nextauthjs/next-auth) for authentication
|
||||
|
||||
## Model Providers
|
||||
|
||||
This template ships with OpenAI `gpt-3.5-turbo` as the default. However, thanks to the [Vercel AI SDK](https://sdk.vercel.ai/docs), you can switch LLM providers to [Anthropic](https://anthropic.com), [Hugging Face](https://huggingface.co), or using [LangChain](https://js.langchain.com) with just a few lines of code.
|
||||
|
||||
## Deploy Your Own
|
||||
|
||||
You can deploy your own version of the Next.js AI Chatbot to Vercel with one click:
|
||||
|
||||
[](https://vercel.com/new/clone?demo-title=Next.js+Chat&demo-description=A+full-featured%2C+hackable+Next.js+AI+chatbot+built+by+Vercel+Labs&demo-url=https%3A%2F%2Fchat.vercel.ai%2F&demo-image=%2F%2Fimages.ctfassets.net%2Fe5382hct74si%2F4aVPvWuTmBvzM5cEdRdqeW%2F4234f9baf160f68ffb385a43c3527645%2FCleanShot_2023-06-16_at_17.09.21.png&project-name=Next.js+Chat&repository-name=nextjs-chat&repository-url=https%3A%2F%2Fgithub.com%2Fvercel-labs%2Fai-chatbot&from=templates&skippable-integrations=1&env=OPENAI_API_KEY%2CAUTH_GITHUB_ID%2CAUTH_GITHUB_SECRET%2CAUTH_SECRET&envDescription=How+to+get+these+env+vars&envLink=https%3A%2F%2Fgithub.com%2Fvercel-labs%2Fai-chatbot%2Fblob%2Fmain%2F.env.example&teamCreateStatus=hidden&stores=[{"type":"kv"}])
|
||||
|
||||
## Creating a KV Database Instance
|
||||
|
||||
Follow the steps outlined in the [quick start guide](https://vercel.com/docs/storage/vercel-kv/quickstart#create-a-kv-database) provided by Vercel. This guide will assist you in creating and configuring your KV database instance on Vercel, enabling your application to interact with it.
|
||||
|
||||
Remember to update your environment variables (`KV_URL`, `KV_REST_API_URL`, `KV_REST_API_TOKEN`, `KV_REST_API_READ_ONLY_TOKEN`) in the `.env` file with the appropriate credentials provided during the KV database setup.
|
||||
|
||||
|
||||
## Running locally
|
||||
|
||||
You will need to use the environment variables [defined in `.env.example`](.env.example) to run Next.js AI Chatbot. It's recommended you use [Vercel Environment Variables](https://vercel.com/docs/concepts/projects/environment-variables) for this, but a `.env` file is all that is necessary.
|
||||
|
||||
> Note: You should not commit your `.env` file or it will expose secrets that will allow others to control access to your various OpenAI and authentication provider accounts.
|
||||
|
||||
1. Install Vercel CLI: `npm i -g vercel`
|
||||
2. Link local instance with Vercel and GitHub accounts (creates `.vercel` directory): `vercel link`
|
||||
3. Download your environment variables: `vercel env pull`
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
Your app template should now be running on [localhost:3000](http://localhost:3000/).
|
||||
|
||||
## Authors
|
||||
|
||||
This library is created by [Vercel](https://vercel.com) and [Next.js](https://nextjs.org) team members, with contributions from:
|
||||
|
||||
- Jared Palmer ([@jaredpalmer](https://twitter.com/jaredpalmer)) - [Vercel](https://vercel.com)
|
||||
- Shu Ding ([@shuding\_](https://twitter.com/shuding_)) - [Vercel](https://vercel.com)
|
||||
- shadcn ([@shadcn](https://twitter.com/shadcn)) - [Contractor](https://shadcn.com)
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
@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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import { Metadata } from 'next'
|
||||
|
||||
import { Toaster } from 'react-hot-toast'
|
||||
|
||||
import '@/app/globals.css'
|
||||
import { fontMono, fontSans } from '@/lib/fonts'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { TailwindIndicator } from '@/components/tailwind-indicator'
|
||||
import { Providers } from '@/components/providers'
|
||||
import { Header } from '@/components/header'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
default: 'Tabby Playground',
|
||||
template: `%s - Tabby Playground`
|
||||
},
|
||||
description: 'Tabby, an opensource, self-hosted AI coding assistant.',
|
||||
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 (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<head />
|
||||
<body
|
||||
className={cn(
|
||||
'font-sans antialiased',
|
||||
fontSans.variable,
|
||||
fontMono.variable
|
||||
)}
|
||||
>
|
||||
<Toaster />
|
||||
<Providers attribute="class" defaultTheme="system" enableSystem>
|
||||
<div className="flex flex-col min-h-screen">
|
||||
{/* @ts-ignore */}
|
||||
<Header />
|
||||
<main className="flex flex-col flex-1 bg-muted/50">{children}</main>
|
||||
</div>
|
||||
<TailwindIndicator />
|
||||
</Providers>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import { nanoid } from '@/lib/utils'
|
||||
import { Chat } from '@/components/chat'
|
||||
|
||||
export default function IndexPage() {
|
||||
const id = nanoid()
|
||||
|
||||
return <Chat id={id} />
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -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 (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className={cn(
|
||||
'absolute right-4 top-1 z-10 bg-background transition-opacity duration-300 sm:right-8 md:top-2',
|
||||
isAtBottom ? 'opacity-0' : 'opacity-100',
|
||||
className
|
||||
)}
|
||||
onClick={() =>
|
||||
window.scrollTo({
|
||||
top: document.body.offsetHeight,
|
||||
behavior: 'smooth'
|
||||
})
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
<IconArrowDown />
|
||||
<span className="sr-only">Scroll to bottom</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
|
@ -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">
|
||||
{messages.map((message, index) => (
|
||||
<div key={index}>
|
||||
<ChatMessage message={message} />
|
||||
{index < messages.length - 1 && (
|
||||
<Separator className="my-4 md:my-8" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
// Inspired by Chatbot-UI and modified to fit the needs of this project
|
||||
// @see https://github.com/mckaywrigley/chatbot-ui/blob/main/components/Chat/ChatMessage.tsx
|
||||
|
||||
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 { IconUser } from '@/components/ui/icons'
|
||||
import Image from 'next/image'
|
||||
import { ChatMessageActions } from '@/components/chat-message-actions'
|
||||
|
||||
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-md border shadow',
|
||||
message.role === 'user'
|
||||
? 'bg-background'
|
||||
: 'bg-primary text-primary-foreground'
|
||||
)}
|
||||
>
|
||||
{message.role === 'user' ? <IconUser /> : <IconTabby />}
|
||||
</div>
|
||||
<div className="flex-1 px-1 ml-4 space-y-2 overflow-hidden">
|
||||
<MemoizedReactMarkdown
|
||||
className="prose break-words dark:prose-invert prose-p:leading-relaxed prose-pre:p-0"
|
||||
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 cursor-default animate-pulse">▍</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>
|
||||
)
|
||||
}
|
||||
|
||||
function IconTabby() {
|
||||
return (
|
||||
<Image
|
||||
style={{ borderRadius: 4 }}
|
||||
src="https://avatars.githubusercontent.com/u/125617854?s=128&v=4"
|
||||
alt="tabby"
|
||||
width="128"
|
||||
height="128"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
import { type UseChatHelpers } from 'ai/react'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { PromptForm } from '@/components/prompt-form'
|
||||
import { ButtonScrollToBottom } from '@/components/button-scroll-to-bottom'
|
||||
import { IconRefresh, IconStop } from '@/components/ui/icons'
|
||||
import { FooterText } from '@/components/footer'
|
||||
|
||||
export interface ChatPanelProps
|
||||
extends Pick<
|
||||
UseChatHelpers,
|
||||
| 'append'
|
||||
| 'isLoading'
|
||||
| 'reload'
|
||||
| 'messages'
|
||||
| 'stop'
|
||||
| 'input'
|
||||
| 'setInput'
|
||||
> {
|
||||
id?: string
|
||||
}
|
||||
|
||||
export function ChatPanel({
|
||||
id,
|
||||
isLoading,
|
||||
stop,
|
||||
append,
|
||||
reload,
|
||||
input,
|
||||
setInput,
|
||||
messages
|
||||
}: ChatPanelProps) {
|
||||
return (
|
||||
<div className="fixed inset-x-0 bottom-0 bg-gradient-to-b from-muted/10 from-10% to-muted/30 to-50%">
|
||||
<ButtonScrollToBottom />
|
||||
<div className="mx-auto sm:max-w-2xl sm:px-4">
|
||||
<div className="flex h-10 items-center justify-center">
|
||||
{isLoading ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => stop()}
|
||||
className="bg-background"
|
||||
>
|
||||
<IconStop className="mr-2" />
|
||||
Stop generating
|
||||
</Button>
|
||||
) : (
|
||||
messages?.length > 0 && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => reload()}
|
||||
className="bg-background"
|
||||
>
|
||||
<IconRefresh className="mr-2" />
|
||||
Regenerate response
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-4 border-t bg-background px-4 py-2 shadow-lg sm:rounded-t-xl sm:border md:py-4">
|
||||
<PromptForm
|
||||
onSubmit={async value => {
|
||||
await append({
|
||||
id,
|
||||
content: value,
|
||||
role: 'user'
|
||||
})
|
||||
}}
|
||||
input={input}
|
||||
setInput={setInput}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<FooterText className="hidden sm:block" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -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 <div ref={ref} className="h-px w-full" />
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
'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 { toast } from 'react-hot-toast'
|
||||
import { usePatchFetch } from '@/lib/hooks/use-patch-fetch'
|
||||
|
||||
export interface ChatProps extends React.ComponentProps<'div'> {
|
||||
initialMessages?: Message[]
|
||||
id?: string
|
||||
}
|
||||
|
||||
export function Chat({ id, initialMessages, className }: ChatProps) {
|
||||
usePatchFetch()
|
||||
|
||||
const {
|
||||
messages,
|
||||
append,
|
||||
reload,
|
||||
stop,
|
||||
isLoading,
|
||||
input,
|
||||
setInput,
|
||||
setMessages
|
||||
} = useChat({
|
||||
initialMessages,
|
||||
id,
|
||||
body: {
|
||||
id
|
||||
},
|
||||
onResponse(response) {
|
||||
if (response.status === 401) {
|
||||
toast.error(response.statusText)
|
||||
}
|
||||
}
|
||||
})
|
||||
if (messages.length > 2) {
|
||||
setMessages(messages.slice(messages.length - 2, messages.length))
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className={cn('pb-[200px] pt-4 md:pt-10', className)}>
|
||||
{messages.length ? (
|
||||
<>
|
||||
<ChatList messages={messages} />
|
||||
<ChatScrollAnchor trackVisibility={isLoading} />
|
||||
</>
|
||||
) : (
|
||||
<EmptyScreen setInput={setInput} />
|
||||
)}
|
||||
</div>
|
||||
<ChatPanel
|
||||
id={id}
|
||||
isLoading={isLoading}
|
||||
stop={stop}
|
||||
append={append}
|
||||
reload={reload}
|
||||
messages={messages}
|
||||
input={input}
|
||||
setInput={setInput}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
'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 {
|
||||
clearChats: () => ServerActionResult<void>
|
||||
}
|
||||
|
||||
export function ClearHistory({ clearChats }: ClearHistoryProps) {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [isPending, startTransition] = React.useTransition()
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={setOpen}>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="ghost" disabled={isPending}>
|
||||
{isPending && <IconSpinner className="mr-2" />}
|
||||
Clear history
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will permanently delete your chat history and remove your data
|
||||
from our servers.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isPending}>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
disabled={isPending}
|
||||
onClick={event => {
|
||||
event.preventDefault()
|
||||
startTransition(async () => {
|
||||
const result = await clearChats()
|
||||
|
||||
if (result && 'error' in result) {
|
||||
toast.error(result.error)
|
||||
return
|
||||
}
|
||||
|
||||
setOpen(false)
|
||||
router.push('/')
|
||||
})
|
||||
}}
|
||||
>
|
||||
{isPending && <IconSpinner className="mr-2 animate-spin" />}
|
||||
Delete
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
import { UseChatHelpers } from 'ai/react'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { IconArrowRight } from '@/components/ui/icons'
|
||||
|
||||
const exampleMessages = [
|
||||
{
|
||||
heading: 'Explain technical concepts',
|
||||
message: `What is a "serverless function"?`
|
||||
},
|
||||
{
|
||||
heading: 'Explain how to parse email address',
|
||||
message: 'How to parse email address with regex'
|
||||
}
|
||||
]
|
||||
|
||||
export function EmptyScreen({ setInput }: Pick<UseChatHelpers, 'setInput'>) {
|
||||
return (
|
||||
<div className="mx-auto max-w-2xl px-4">
|
||||
<div className="rounded-lg border bg-background p-8">
|
||||
<h1 className="mb-2 text-lg font-semibold">
|
||||
Welcome to Tabby Playground!
|
||||
</h1>
|
||||
<p className="leading-normal text-muted-foreground">
|
||||
You can start a conversation here or try the following examples:
|
||||
</p>
|
||||
<div className="mt-4 flex flex-col items-start space-y-2">
|
||||
{exampleMessages.map((message, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
variant="link"
|
||||
className="h-auto p-0 text-base"
|
||||
onClick={() => setInput(message.message)}
|
||||
>
|
||||
<IconArrowRight className="mr-2 text-muted-foreground" />
|
||||
{message.heading}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
export function ExternalLink({
|
||||
href,
|
||||
children
|
||||
}: {
|
||||
href: string
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
className="inline-flex flex-1 justify-center gap-1 leading-4 hover:underline"
|
||||
>
|
||||
<span>{children}</span>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
height="7"
|
||||
viewBox="0 0 6 6"
|
||||
width="7"
|
||||
className="opacity-70"
|
||||
>
|
||||
<path
|
||||
d="M1.25215 5.54731L0.622742 4.9179L3.78169 1.75597H1.3834L1.38936 0.890915H5.27615V4.78069H4.40513L4.41109 2.38538L1.25215 5.54731Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import React from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { ExternalLink } from '@/components/external-link'
|
||||
|
||||
export function FooterText({ className, ...props }: React.ComponentProps<'p'>) {
|
||||
return (
|
||||
<p
|
||||
className={cn(
|
||||
'px-2 text-center text-xs leading-normal text-muted-foreground',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ExternalLink href="https://tabby.tabbyml.com">Tabby</ExternalLink>, an
|
||||
opensource, self-hosted AI coding assistant .
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { buttonVariants } from '@/components/ui/button'
|
||||
import { IconGitHub, IconVercel } from '@/components/ui/icons'
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
const ThemeToggle = dynamic(
|
||||
() => import('@/components/theme-toggle').then(x => x.ThemeToggle),
|
||||
{ ssr: false }
|
||||
)
|
||||
|
||||
export function Header() {
|
||||
return (
|
||||
<header className="sticky top-0 z-50 flex items-center justify-between w-full h-16 px-4 border-b shrink-0 bg-gradient-to-b from-background/10 via-background/50 to-background/80 backdrop-blur-xl">
|
||||
<ThemeToggle />
|
||||
<div className="flex items-center"></div>
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://github.com/TabbyML/tabby"
|
||||
rel="noopener noreferrer"
|
||||
className={cn(buttonVariants({ variant: 'outline' }))}
|
||||
>
|
||||
<IconGitHub />
|
||||
<span className="hidden ml-2 md:flex">GitHub</span>
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setIsLoading(true)
|
||||
// next-auth signIn() function doesn't work yet at Edge Runtime due to usage of BroadcastChannel
|
||||
signIn('github', { callbackUrl: `/` })
|
||||
}}
|
||||
disabled={isLoading}
|
||||
className={cn(className)}
|
||||
{...props}
|
||||
>
|
||||
{isLoading ? (
|
||||
<IconSpinner className="mr-2 animate-spin" />
|
||||
) : showGithubIcon ? (
|
||||
<IconGitHub className="mr-2" />
|
||||
) : null}
|
||||
{text}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
import { UseChatHelpers } from 'ai/react'
|
||||
import * as React from 'react'
|
||||
import Textarea from 'react-textarea-autosize'
|
||||
|
||||
import { Button, buttonVariants } from '@/components/ui/button'
|
||||
import { IconArrowElbow, IconEdit, IconPlus } from '@/components/ui/icons'
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger
|
||||
} from '@/components/ui/tooltip'
|
||||
import { useEnterSubmit } from '@/lib/hooks/use-enter-submit'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
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)
|
||||
const router = useRouter()
|
||||
|
||||
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="relative flex max-h-60 w-full grow flex-col overflow-hidden bg-background px-8 sm:rounded-md sm:border sm:px-12">
|
||||
<span
|
||||
className={cn(
|
||||
buttonVariants({ size: 'sm', variant: 'ghost' }),
|
||||
'absolute left-0 top-4 h-8 w-8 rounded-full bg-background p-0 sm:left-4 hover:bg-background'
|
||||
)}
|
||||
>
|
||||
<IconEdit />
|
||||
</span>
|
||||
<Textarea
|
||||
ref={inputRef}
|
||||
tabIndex={0}
|
||||
onKeyDown={onKeyDown}
|
||||
rows={1}
|
||||
value={input}
|
||||
onChange={e => setInput(e.target.value)}
|
||||
placeholder="Ask a question."
|
||||
spellCheck={false}
|
||||
className="min-h-[60px] w-full resize-none bg-transparent px-4 py-[1.3rem] focus-within:outline-none sm:text-sm"
|
||||
/>
|
||||
<div className="absolute right-0 top-4 sm:right-4">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="submit"
|
||||
size="icon"
|
||||
disabled={isLoading || input === ''}
|
||||
>
|
||||
<IconArrowElbow />
|
||||
<span className="sr-only">Send message</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Send message</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import { ThemeProvider as NextThemesProvider } from 'next-themes'
|
||||
import { ThemeProviderProps } from 'next-themes/dist/types'
|
||||
|
||||
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||
|
||||
export function Providers({ children, ...props }: ThemeProviderProps) {
|
||||
return (
|
||||
<NextThemesProvider {...props}>
|
||||
<TooltipProvider>{children}</TooltipProvider>
|
||||
</NextThemesProvider>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
export function TailwindIndicator() {
|
||||
if (process.env.NODE_ENV === 'production') return null
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-1 left-1 z-50 flex h-6 w-6 items-center justify-center rounded-full bg-gray-800 p-3 font-mono text-xs text-white">
|
||||
<div className="block sm:hidden">xs</div>
|
||||
<div className="hidden sm:block md:hidden">sm</div>
|
||||
<div className="hidden md:block lg:hidden">md</div>
|
||||
<div className="hidden lg:block xl:hidden">lg</div>
|
||||
<div className="hidden xl:block 2xl:hidden">xl</div>
|
||||
<div className="hidden 2xl:block">2xl</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import { useTheme } from 'next-themes'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { IconMoon, IconSun } from '@/components/ui/icons'
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { setTheme, theme } = useTheme()
|
||||
const [_, startTransition] = React.useTransition()
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
startTransition(() => {
|
||||
setTheme(theme === 'light' ? 'dark' : 'light')
|
||||
})
|
||||
}}
|
||||
>
|
||||
{!theme ? null : theme === 'dark' ? (
|
||||
<IconMoon className="transition-all" />
|
||||
) : (
|
||||
<IconSun className="transition-all" />
|
||||
)}
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
'use client'
|
||||
|
||||
export { Toaster } from 'react-hot-toast'
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { buttonVariants } from '@/components/ui/button'
|
||||
|
||||
const AlertDialog = AlertDialogPrimitive.Root
|
||||
|
||||
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
|
||||
|
||||
const AlertDialogPortal = ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: AlertDialogPrimitive.AlertDialogPortalProps) => (
|
||||
<AlertDialogPrimitive.Portal className={cn(className)} {...props}>
|
||||
<div className="fixed inset-0 z-50 flex items-end justify-center sm:items-center">
|
||||
{children}
|
||||
</div>
|
||||
</AlertDialogPrimitive.Portal>
|
||||
)
|
||||
AlertDialogPortal.displayName = AlertDialogPrimitive.Portal.displayName
|
||||
|
||||
const AlertDialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
className={cn(
|
||||
'fixed inset-0 z-50 bg-background/80 backdrop-blur-sm transition-opacity animate-in fade-in',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
))
|
||||
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
||||
|
||||
const AlertDialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'fixed z-50 grid w-full max-w-lg scale-100 gap-4 border bg-background p-6 opacity-100 shadow-lg animate-in fade-in-90 slide-in-from-bottom-10 sm:rounded-lg sm:zoom-in-90 sm:slide-in-from-bottom-0 md:w-full',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
))
|
||||
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
||||
|
||||
const AlertDialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col space-y-2 text-center sm:text-left',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
AlertDialogHeader.displayName = 'AlertDialogHeader'
|
||||
|
||||
const AlertDialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
AlertDialogFooter.displayName = 'AlertDialogFooter'
|
||||
|
||||
const AlertDialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn('text-lg font-semibold', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
||||
|
||||
const AlertDialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn('text-sm text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogDescription.displayName =
|
||||
AlertDialogPrimitive.Description.displayName
|
||||
|
||||
const AlertDialogAction = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Action
|
||||
ref={ref}
|
||||
className={cn(buttonVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
||||
|
||||
const AlertDialogCancel = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Cancel
|
||||
ref={ref}
|
||||
className={cn(
|
||||
buttonVariants({ variant: 'outline' }),
|
||||
'mt-2 sm:mt-0',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
||||
|
||||
export {
|
||||
AlertDialog,
|
||||
AlertDialogTrigger,
|
||||
AlertDialogContent,
|
||||
AlertDialogHeader,
|
||||
AlertDialogFooter,
|
||||
AlertDialogTitle,
|
||||
AlertDialogDescription,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import * as React from 'react'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const badgeVariants = cva(
|
||||
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
|
||||
secondary:
|
||||
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
destructive:
|
||||
'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
|
||||
outline: 'text-foreground'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import * as React from 'react'
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const buttonVariants = cva(
|
||||
'inline-flex items-center justify-center rounded-md text-sm font-medium shadow ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
'bg-primary text-primary-foreground shadow-md hover:bg-primary/90',
|
||||
destructive:
|
||||
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
||||
outline:
|
||||
'border border-input hover:bg-accent hover:text-accent-foreground',
|
||||
secondary:
|
||||
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
ghost: 'shadow-none hover:bg-accent hover:text-accent-foreground',
|
||||
link: 'text-primary underline-offset-4 shadow-none hover:underline'
|
||||
},
|
||||
size: {
|
||||
default: 'h-8 px-4 py-2',
|
||||
sm: 'h-8 rounded-md px-3',
|
||||
lg: 'h-11 rounded-md px-8',
|
||||
icon: 'h-8 w-8 p-0'
|
||||
}
|
||||
},
|
||||
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 }
|
||||
|
|
@ -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 { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard'
|
||||
import { IconCheck, IconCopy, IconDownload } from '@/components/ui/icons'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
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="relative w-full font-sans codeblock bg-zinc-950">
|
||||
<div className="flex items-center justify-between w-full px-6 py-2 pr-4 bg-zinc-800 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 }
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { IconClose } from '@/components/ui/icons'
|
||||
|
||||
const Dialog = DialogPrimitive.Root
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger
|
||||
|
||||
const DialogPortal = ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: DialogPrimitive.DialogPortalProps) => (
|
||||
<DialogPrimitive.Portal className={cn(className)} {...props}>
|
||||
<div className="fixed inset-0 z-50 flex items-start justify-center sm:items-center">
|
||||
{children}
|
||||
</div>
|
||||
</DialogPrimitive.Portal>
|
||||
)
|
||||
DialogPortal.displayName = DialogPrimitive.Portal.displayName
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'fixed inset-0 z-50 bg-background/80 backdrop-blur-sm transition-all duration-100 data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:fade-in',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'fixed z-50 grid w-full gap-4 rounded-b-lg border bg-background p-6 shadow-sm animate-in data-[state=open]:fade-in-90 data-[state=open]:slide-in-from-bottom-10 sm:max-w-lg sm:rounded-lg sm:zoom-in-90 data-[state=open]:sm:slide-in-from-bottom-0',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<IconClose />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
))
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col space-y-1.5 text-center sm:text-left',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogHeader.displayName = 'DialogHeader'
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogFooter.displayName = 'DialogFooter'
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'text-lg font-semibold leading-none tracking-tight',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn('text-sm text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription
|
||||
}
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow animate-in data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
))
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
inset && 'pl-8',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'px-2 py-1.5 text-sm font-semibold',
|
||||
inset && 'pl-8',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn('-mx-1 my-1 h-px bg-muted', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
||||
|
||||
const DropdownMenuShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn('ml-auto text-xs tracking-widest opacity-60', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuRadioGroup
|
||||
}
|
||||
|
|
@ -0,0 +1,507 @@
|
|||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
function IconNextChat({
|
||||
className,
|
||||
inverted,
|
||||
...props
|
||||
}: React.ComponentProps<'svg'> & { inverted?: boolean }) {
|
||||
const id = React.useId()
|
||||
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 17 17"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={cn('h-4 w-4', className)}
|
||||
{...props}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id={`gradient-${id}-1`}
|
||||
x1="10.6889"
|
||||
y1="10.3556"
|
||||
x2="13.8445"
|
||||
y2="14.2667"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor={inverted ? 'white' : 'black'} />
|
||||
<stop
|
||||
offset={1}
|
||||
stopColor={inverted ? 'white' : 'black'}
|
||||
stopOpacity={0}
|
||||
/>
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id={`gradient-${id}-2`}
|
||||
x1="11.7555"
|
||||
y1="4.8"
|
||||
x2="11.7376"
|
||||
y2="9.50002"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor={inverted ? 'white' : 'black'} />
|
||||
<stop
|
||||
offset={1}
|
||||
stopColor={inverted ? 'white' : 'black'}
|
||||
stopOpacity={0}
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path
|
||||
d="M1 16L2.58314 11.2506C1.83084 9.74642 1.63835 8.02363 2.04013 6.39052C2.4419 4.75741 3.41171 3.32057 4.776 2.33712C6.1403 1.35367 7.81003 0.887808 9.4864 1.02289C11.1628 1.15798 12.7364 1.8852 13.9256 3.07442C15.1148 4.26363 15.842 5.83723 15.9771 7.5136C16.1122 9.18997 15.6463 10.8597 14.6629 12.224C13.6794 13.5883 12.2426 14.5581 10.6095 14.9599C8.97637 15.3616 7.25358 15.1692 5.74942 14.4169L1 16Z"
|
||||
fill={inverted ? 'black' : 'white'}
|
||||
stroke={inverted ? 'black' : 'white'}
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<mask
|
||||
id="mask0_91_2047"
|
||||
style={{ maskType: 'alpha' }}
|
||||
maskUnits="userSpaceOnUse"
|
||||
x={1}
|
||||
y={0}
|
||||
width={16}
|
||||
height={16}
|
||||
>
|
||||
<circle cx={9} cy={8} r={8} fill={inverted ? 'black' : 'white'} />
|
||||
</mask>
|
||||
<g mask="url(#mask0_91_2047)">
|
||||
<circle cx={9} cy={8} r={8} fill={inverted ? 'black' : 'white'} />
|
||||
<path
|
||||
d="M14.2896 14.0018L7.146 4.8H5.80005V11.1973H6.87681V6.16743L13.4444 14.6529C13.7407 14.4545 14.0231 14.2369 14.2896 14.0018Z"
|
||||
fill={`url(#gradient-${id}-1)`}
|
||||
/>
|
||||
<rect
|
||||
x="11.2222"
|
||||
y="4.8"
|
||||
width="1.06667"
|
||||
height="6.4"
|
||||
fill={`url(#gradient-${id}-2)`}
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function IconOpenAI({ className, ...props }: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
role="img"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={cn('h-4 w-4', className)}
|
||||
{...props}
|
||||
>
|
||||
<title>OpenAI icon</title>
|
||||
<path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function IconVercel({ className, ...props }: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
aria-label="Vercel logomark"
|
||||
role="img"
|
||||
viewBox="0 0 74 64"
|
||||
className={cn('h-4 w-4', className)}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M37.5896 0.25L74.5396 64.25H0.639648L37.5896 0.25Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function IconGitHub({ className, ...props }: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
className={cn('h-4 w-4', className)}
|
||||
{...props}
|
||||
>
|
||||
<title>GitHub</title>
|
||||
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function IconSeparator({ className, ...props }: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
fill="none"
|
||||
shapeRendering="geometricPrecision"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
className={cn('h-4 w-4', className)}
|
||||
{...props}
|
||||
>
|
||||
<path d="M16.88 3.549L7.12 20.451"></path>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function IconArrowDown({ className, ...props }: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 256 256"
|
||||
fill="currentColor"
|
||||
className={cn('h-4 w-4', className)}
|
||||
{...props}
|
||||
>
|
||||
<path d="m205.66 149.66-72 72a8 8 0 0 1-11.32 0l-72-72a8 8 0 0 1 11.32-11.32L120 196.69V40a8 8 0 0 1 16 0v156.69l58.34-58.35a8 8 0 0 1 11.32 11.32Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function IconArrowRight({ className, ...props }: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 256 256"
|
||||
fill="currentColor"
|
||||
className={cn('h-4 w-4', className)}
|
||||
{...props}
|
||||
>
|
||||
<path d="m221.66 133.66-72 72a8 8 0 0 1-11.32-11.32L196.69 136H40a8 8 0 0 1 0-16h156.69l-58.35-58.34a8 8 0 0 1 11.32-11.32l72 72a8 8 0 0 1 0 11.32Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function IconUser({ className, ...props }: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 256 256"
|
||||
fill="currentColor"
|
||||
className={cn('h-4 w-4', className)}
|
||||
{...props}
|
||||
>
|
||||
<path d="M230.92 212c-15.23-26.33-38.7-45.21-66.09-54.16a72 72 0 1 0-73.66 0c-27.39 8.94-50.86 27.82-66.09 54.16a8 8 0 1 0 13.85 8c18.84-32.56 52.14-52 89.07-52s70.23 19.44 89.07 52a8 8 0 1 0 13.85-8ZM72 96a56 56 0 1 1 56 56 56.06 56.06 0 0 1-56-56Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function IconPlus({ className, ...props }: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 256 256"
|
||||
fill="currentColor"
|
||||
className={cn('h-4 w-4', className)}
|
||||
{...props}
|
||||
>
|
||||
<path d="M224 128a8 8 0 0 1-8 8h-80v80a8 8 0 0 1-16 0v-80H40a8 8 0 0 1 0-16h80V40a8 8 0 0 1 16 0v80h80a8 8 0 0 1 8 8Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function IconArrowElbow({ className, ...props }: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 256 256"
|
||||
fill="currentColor"
|
||||
className={cn('h-4 w-4', className)}
|
||||
{...props}
|
||||
>
|
||||
<path d="M200 32v144a8 8 0 0 1-8 8H67.31l34.35 34.34a8 8 0 0 1-11.32 11.32l-48-48a8 8 0 0 1 0-11.32l48-48a8 8 0 0 1 11.32 11.32L67.31 168H184V32a8 8 0 0 1 16 0Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function IconSpinner({ className, ...props }: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 256 256"
|
||||
fill="currentColor"
|
||||
className={cn('h-4 w-4 animate-spin', className)}
|
||||
{...props}
|
||||
>
|
||||
<path d="M232 128a104 104 0 0 1-208 0c0-41 23.81-78.36 60.66-95.27a8 8 0 0 1 6.68 14.54C60.15 61.59 40 93.27 40 128a88 88 0 0 0 176 0c0-34.73-20.15-66.41-51.34-80.73a8 8 0 0 1 6.68-14.54C208.19 49.64 232 87 232 128Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function IconMessage({ className, ...props }: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 256 256"
|
||||
fill="currentColor"
|
||||
className={cn('h-4 w-4', className)}
|
||||
{...props}
|
||||
>
|
||||
<path d="M216 48H40a16 16 0 0 0-16 16v160a15.84 15.84 0 0 0 9.25 14.5A16.05 16.05 0 0 0 40 240a15.89 15.89 0 0 0 10.25-3.78.69.69 0 0 0 .13-.11L82.5 208H216a16 16 0 0 0 16-16V64a16 16 0 0 0-16-16ZM40 224Zm176-32H82.5a16 16 0 0 0-10.3 3.75l-.12.11L40 224V64h176Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function IconTrash({ className, ...props }: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 256 256"
|
||||
fill="currentColor"
|
||||
className={cn('h-4 w-4', className)}
|
||||
{...props}
|
||||
>
|
||||
<path d="M216 48h-40v-8a24 24 0 0 0-24-24h-48a24 24 0 0 0-24 24v8H40a8 8 0 0 0 0 16h8v144a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16V64h8a8 8 0 0 0 0-16ZM96 40a8 8 0 0 1 8-8h48a8 8 0 0 1 8 8v8H96Zm96 168H64V64h128Zm-80-104v64a8 8 0 0 1-16 0v-64a8 8 0 0 1 16 0Zm48 0v64a8 8 0 0 1-16 0v-64a8 8 0 0 1 16 0Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function IconRefresh({ className, ...props }: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 256 256"
|
||||
fill="currentColor"
|
||||
className={cn('h-4 w-4', className)}
|
||||
{...props}
|
||||
>
|
||||
<path d="M197.67 186.37a8 8 0 0 1 0 11.29C196.58 198.73 170.82 224 128 224c-37.39 0-64.53-22.4-80-39.85V208a8 8 0 0 1-16 0v-48a8 8 0 0 1 8-8h48a8 8 0 0 1 0 16H55.44C67.76 183.35 93 208 128 208c36 0 58.14-21.46 58.36-21.68a8 8 0 0 1 11.31.05ZM216 40a8 8 0 0 0-8 8v23.85C192.53 54.4 165.39 32 128 32c-42.82 0-68.58 25.27-69.66 26.34a8 8 0 0 0 11.3 11.34C69.86 69.46 92 48 128 48c35 0 60.24 24.65 72.56 40H168a8 8 0 0 0 0 16h48a8 8 0 0 0 8-8V48a8 8 0 0 0-8-8Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function IconStop({ className, ...props }: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 256 256"
|
||||
fill="currentColor"
|
||||
className={cn('h-4 w-4', className)}
|
||||
{...props}
|
||||
>
|
||||
<path d="M128 24a104 104 0 1 0 104 104A104.11 104.11 0 0 0 128 24Zm0 192a88 88 0 1 1 88-88 88.1 88.1 0 0 1-88 88Zm24-120h-48a8 8 0 0 0-8 8v48a8 8 0 0 0 8 8h48a8 8 0 0 0 8-8v-48a8 8 0 0 0-8-8Zm-8 48h-32v-32h32Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function IconSidebar({ className, ...props }: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 256 256"
|
||||
fill="currentColor"
|
||||
className={cn('h-4 w-4', className)}
|
||||
{...props}
|
||||
>
|
||||
<path d="M216 40H40a16 16 0 0 0-16 16v144a16 16 0 0 0 16 16h176a16 16 0 0 0 16-16V56a16 16 0 0 0-16-16ZM40 56h40v144H40Zm176 144H96V56h120v144Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function IconMoon({ className, ...props }: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 256 256"
|
||||
fill="currentColor"
|
||||
className={cn('h-4 w-4', className)}
|
||||
{...props}
|
||||
>
|
||||
<path d="M233.54 142.23a8 8 0 0 0-8-2 88.08 88.08 0 0 1-109.8-109.8 8 8 0 0 0-10-10 104.84 104.84 0 0 0-52.91 37A104 104 0 0 0 136 224a103.09 103.09 0 0 0 62.52-20.88 104.84 104.84 0 0 0 37-52.91 8 8 0 0 0-1.98-7.98Zm-44.64 48.11A88 88 0 0 1 65.66 67.11a89 89 0 0 1 31.4-26A106 106 0 0 0 96 56a104.11 104.11 0 0 0 104 104 106 106 0 0 0 14.92-1.06 89 89 0 0 1-26.02 31.4Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function IconSun({ className, ...props }: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 256 256"
|
||||
fill="currentColor"
|
||||
className={cn('h-4 w-4', className)}
|
||||
{...props}
|
||||
>
|
||||
<path d="M120 40V16a8 8 0 0 1 16 0v24a8 8 0 0 1-16 0Zm72 88a64 64 0 1 1-64-64 64.07 64.07 0 0 1 64 64Zm-16 0a48 48 0 1 0-48 48 48.05 48.05 0 0 0 48-48ZM58.34 69.66a8 8 0 0 0 11.32-11.32l-16-16a8 8 0 0 0-11.32 11.32Zm0 116.68-16 16a8 8 0 0 0 11.32 11.32l16-16a8 8 0 0 0-11.32-11.32ZM192 72a8 8 0 0 0 5.66-2.34l16-16a8 8 0 0 0-11.32-11.32l-16 16A8 8 0 0 0 192 72Zm5.66 114.34a8 8 0 0 0-11.32 11.32l16 16a8 8 0 0 0 11.32-11.32ZM48 128a8 8 0 0 0-8-8H16a8 8 0 0 0 0 16h24a8 8 0 0 0 8-8Zm80 80a8 8 0 0 0-8 8v24a8 8 0 0 0 16 0v-24a8 8 0 0 0-8-8Zm112-88h-24a8 8 0 0 0 0 16h24a8 8 0 0 0 0-16Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function IconCopy({ className, ...props }: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 256 256"
|
||||
fill="currentColor"
|
||||
className={cn('h-4 w-4', className)}
|
||||
{...props}
|
||||
>
|
||||
<path d="M216 32H88a8 8 0 0 0-8 8v40H40a8 8 0 0 0-8 8v128a8 8 0 0 0 8 8h128a8 8 0 0 0 8-8v-40h40a8 8 0 0 0 8-8V40a8 8 0 0 0-8-8Zm-56 176H48V96h112Zm48-48h-32V88a8 8 0 0 0-8-8H96V48h112Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function IconCheck({ className, ...props }: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 256 256"
|
||||
fill="currentColor"
|
||||
className={cn('h-4 w-4', className)}
|
||||
{...props}
|
||||
>
|
||||
<path d="m229.66 77.66-128 128a8 8 0 0 1-11.32 0l-56-56a8 8 0 0 1 11.32-11.32L96 188.69 218.34 66.34a8 8 0 0 1 11.32 11.32Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function IconDownload({ className, ...props }: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 256 256"
|
||||
fill="currentColor"
|
||||
className={cn('h-4 w-4', className)}
|
||||
{...props}
|
||||
>
|
||||
<path d="M224 152v56a16 16 0 0 1-16 16H48a16 16 0 0 1-16-16v-56a8 8 0 0 1 16 0v56h160v-56a8 8 0 0 1 16 0Zm-101.66 5.66a8 8 0 0 0 11.32 0l40-40a8 8 0 0 0-11.32-11.32L136 132.69V40a8 8 0 0 0-16 0v92.69l-26.34-26.35a8 8 0 0 0-11.32 11.32Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function IconClose({ className, ...props }: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 256 256"
|
||||
fill="currentColor"
|
||||
className={cn('h-4 w-4', className)}
|
||||
{...props}
|
||||
>
|
||||
<path d="M205.66 194.34a8 8 0 0 1-11.32 11.32L128 139.31l-66.34 66.35a8 8 0 0 1-11.32-11.32L116.69 128 50.34 61.66a8 8 0 0 1 11.32-11.32L128 116.69l66.34-66.35a8 8 0 0 1 11.32 11.32L139.31 128Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function IconEdit({ className, ...props }: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className={cn('h-4 w-4', className)}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function IconShare({ className, ...props }: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
className={cn('h-4 w-4', className)}
|
||||
viewBox="0 0 256 256"
|
||||
{...props}
|
||||
>
|
||||
<path d="m237.66 106.35-80-80A8 8 0 0 0 144 32v40.35c-25.94 2.22-54.59 14.92-78.16 34.91-28.38 24.08-46.05 55.11-49.76 87.37a12 12 0 0 0 20.68 9.58c11-11.71 50.14-48.74 107.24-52V192a8 8 0 0 0 13.66 5.65l80-80a8 8 0 0 0 0-11.3ZM160 172.69V144a8 8 0 0 0-8-8c-28.08 0-55.43 7.33-81.29 21.8a196.17 196.17 0 0 0-36.57 26.52c5.8-23.84 20.42-46.51 42.05-64.86C99.41 99.77 127.75 88 152 88a8 8 0 0 0 8-8V51.32L220.69 112Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function IconUsers({ className, ...props }: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
className={cn('h-4 w-4', className)}
|
||||
viewBox="0 0 256 256"
|
||||
{...props}
|
||||
>
|
||||
<path d="M117.25 157.92a60 60 0 1 0-66.5 0 95.83 95.83 0 0 0-47.22 37.71 8 8 0 1 0 13.4 8.74 80 80 0 0 1 134.14 0 8 8 0 0 0 13.4-8.74 95.83 95.83 0 0 0-47.22-37.71ZM40 108a44 44 0 1 1 44 44 44.05 44.05 0 0 1-44-44Zm210.14 98.7a8 8 0 0 1-11.07-2.33A79.83 79.83 0 0 0 172 168a8 8 0 0 1 0-16 44 44 0 1 0-16.34-84.87 8 8 0 1 1-5.94-14.85 60 60 0 0 1 55.53 105.64 95.83 95.83 0 0 1 47.22 37.71 8 8 0 0 1-2.33 11.07Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function IconExternalLink({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
className={cn('h-4 w-4', className)}
|
||||
viewBox="0 0 256 256"
|
||||
{...props}
|
||||
>
|
||||
<path d="M224 104a8 8 0 0 1-16 0V59.32l-66.33 66.34a8 8 0 0 1-11.32-11.32L196.68 48H152a8 8 0 0 1 0-16h64a8 8 0 0 1 8 8Zm-40 24a8 8 0 0 0-8 8v72H48V80h72a8 8 0 0 0 0-16H48a16 16 0 0 0-16 16v128a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16v-72a8 8 0 0 0-8-8Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function IconChevronUpDown({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
className={cn('h-4 w-4', className)}
|
||||
viewBox="0 0 256 256"
|
||||
{...props}
|
||||
>
|
||||
<path d="M181.66 170.34a8 8 0 0 1 0 11.32l-48 48a8 8 0 0 1-11.32 0l-48-48a8 8 0 0 1 11.32-11.32L128 212.69l42.34-42.35a8 8 0 0 1 11.32 0Zm-96-84.68L128 43.31l42.34 42.35a8 8 0 0 0 11.32-11.32l-48-48a8 8 0 0 0-11.32 0l-48 48a8 8 0 0 0 11.32 11.32Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
IconEdit,
|
||||
IconNextChat,
|
||||
IconOpenAI,
|
||||
IconVercel,
|
||||
IconGitHub,
|
||||
IconSeparator,
|
||||
IconArrowDown,
|
||||
IconArrowRight,
|
||||
IconUser,
|
||||
IconPlus,
|
||||
IconArrowElbow,
|
||||
IconSpinner,
|
||||
IconMessage,
|
||||
IconTrash,
|
||||
IconRefresh,
|
||||
IconStop,
|
||||
IconSidebar,
|
||||
IconMoon,
|
||||
IconSun,
|
||||
IconCopy,
|
||||
IconCheck,
|
||||
IconDownload,
|
||||
IconClose,
|
||||
IconShare,
|
||||
IconUsers,
|
||||
IconExternalLink,
|
||||
IconChevronUpDown
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import * as React from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Input.displayName = 'Input'
|
||||
|
||||
export { Input }
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import * as LabelPrimitive from '@radix-ui/react-label'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const labelVariants = cva(
|
||||
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
|
||||
)
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
|
||||
export { Label }
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import * as SelectPrimitive from '@radix-ui/react-select'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
IconArrowDown,
|
||||
IconCheck,
|
||||
IconChevronUpDown
|
||||
} from '@/components/ui/icons'
|
||||
|
||||
const Select = SelectPrimitive.Root
|
||||
|
||||
const SelectGroup = SelectPrimitive.Group
|
||||
|
||||
const SelectValue = SelectPrimitive.Value
|
||||
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex h-9 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<IconChevronUpDown className="opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
))
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
||||
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(({ className, children, position = 'popper', ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md animate-in fade-in-80',
|
||||
position === 'popper' && 'translate-y-1',
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
'p-1',
|
||||
position === 'popper' &&
|
||||
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]'
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
))
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName
|
||||
|
||||
const SelectLabel = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn('py-1.5 pl-8 pr-2 text-sm font-semibold', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
||||
|
||||
const SelectItem = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<IconCheck className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
))
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName
|
||||
|
||||
const SelectSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn('-mx-1 my-1 h-px bg-muted', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import * as SeparatorPrimitive from '@radix-ui/react-separator'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
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-[1px] w-full' : 'h-full w-[1px]',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
||||
|
||||
export { Separator }
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import * as SheetPrimitive from '@radix-ui/react-dialog'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { IconClose } from '@/components/ui/icons'
|
||||
|
||||
const Sheet = SheetPrimitive.Root
|
||||
|
||||
const SheetTrigger = SheetPrimitive.Trigger
|
||||
|
||||
const SheetClose = SheetPrimitive.Close
|
||||
|
||||
const SheetPortal = ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: SheetPrimitive.DialogPortalProps) => (
|
||||
<SheetPrimitive.Portal
|
||||
className={cn('fixed inset-0 z-50 flex', className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</SheetPrimitive.Portal>
|
||||
)
|
||||
SheetPortal.displayName = SheetPrimitive.Portal.displayName
|
||||
|
||||
const SheetOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SheetPrimitive.Overlay
|
||||
className={cn(
|
||||
'fixed inset-0 z-50 transition-all duration-100 data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:fade-in',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
))
|
||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
|
||||
|
||||
const SheetContent = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SheetPortal>
|
||||
<SheetPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'fixed inset-y-0 left-0 z-50 h-full border-r bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left data-[state=closed]:duration-300 data-[state=open]:duration-500 sm:max-w-sm',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
|
||||
<IconClose />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
))
|
||||
SheetContent.displayName = SheetPrimitive.Content.displayName
|
||||
|
||||
const SheetHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn('flex flex-col space-y-2', className)} {...props} />
|
||||
)
|
||||
SheetHeader.displayName = 'SheetHeader'
|
||||
|
||||
const SheetFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
SheetFooter.displayName = 'SheetFooter'
|
||||
|
||||
const SheetTitle = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn('text-lg font-semibold text-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SheetTitle.displayName = SheetPrimitive.Title.displayName
|
||||
|
||||
const SheetDescription = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn('text-sm text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SheetDescription.displayName = SheetPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import * as SwitchPrimitives from '@radix-ui/react-switch'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const Switch = React.forwardRef<
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
'peer inline-flex h-[24px] w-[44px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
'pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0'
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
))
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName
|
||||
|
||||
export { Switch }
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import * as React from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export interface TextareaProps
|
||||
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
||||
|
||||
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
'flex min-h-[80px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Textarea.displayName = 'Textarea'
|
||||
|
||||
export { Textarea }
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import * as TooltipPrimitive from '@radix-ui/react-tooltip'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-xs font-medium text-popover-foreground shadow-md animate-in fade-in-50 data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
'use client'
|
||||
|
||||
import Image from 'next/image'
|
||||
import { type Session } from 'next-auth'
|
||||
import { signOut } from 'next-auth/react'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { IconExternalLink } from '@/components/ui/icons'
|
||||
|
||||
export interface UserMenuProps {
|
||||
user: Session['user']
|
||||
}
|
||||
|
||||
function getUserInitials(name: string) {
|
||||
const [firstName, lastName] = name.split(' ')
|
||||
return lastName ? `${firstName[0]}${lastName[0]}` : firstName.slice(0, 2)
|
||||
}
|
||||
|
||||
export function UserMenu({ user }: UserMenuProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="pl-0">
|
||||
{user?.image ? (
|
||||
<Image
|
||||
className="w-6 h-6 transition-opacity duration-300 rounded-full select-none ring-1 ring-zinc-100/10 hover:opacity-80"
|
||||
src={user?.image ? `${user.image}&s=60` : ''}
|
||||
alt={user.name ?? 'Avatar'}
|
||||
height={48}
|
||||
width={48}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center justify-center text-xs font-medium uppercase rounded-full select-none h-7 w-7 shrink-0 bg-muted/50 text-muted-foreground">
|
||||
{user?.name ? getUserInitials(user?.name) : null}
|
||||
</div>
|
||||
)}
|
||||
<span className="ml-2">{user?.name}</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent sideOffset={8} align="start" className="w-[180px]">
|
||||
<DropdownMenuItem className="flex-col items-start">
|
||||
<div className="text-xs font-medium">{user?.name}</div>
|
||||
<div className="text-xs text-zinc-500">{user?.email}</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild>
|
||||
<a
|
||||
href="https://vercel.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center justify-between w-full text-xs"
|
||||
>
|
||||
Vercel Homepage
|
||||
<IconExternalLink className="w-3 h-3 ml-auto" />
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
signOut({
|
||||
callbackUrl: '/'
|
||||
})
|
||||
}
|
||||
className="text-xs"
|
||||
>
|
||||
Log Out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
import type { NextApiRequest } from 'next'
|
||||
import type { NextFetchEvent, NextRequest } from 'next/server'
|
||||
|
||||
export const initAnalytics = ({
|
||||
request,
|
||||
event
|
||||
}: {
|
||||
request: NextRequest | NextApiRequest | Request
|
||||
event?: NextFetchEvent
|
||||
}) => {
|
||||
const endpoint = process.env.VERCEL_URL
|
||||
|
||||
return {
|
||||
track: async (eventName: string, data?: any) => {
|
||||
try {
|
||||
if (!endpoint && process.env.NODE_ENV === 'development') {
|
||||
console.log(
|
||||
`[Vercel Web Analytics] Track "${eventName}"` +
|
||||
(data ? ` with data ${JSON.stringify(data || {})}` : '')
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const headers: { [key: string]: string } = {}
|
||||
Object.entries(request.headers).map(([key, value]) => {
|
||||
headers[key] = value
|
||||
})
|
||||
|
||||
const body = {
|
||||
o: headers.referer,
|
||||
ts: new Date().getTime(),
|
||||
r: '',
|
||||
en: eventName,
|
||||
ed: data
|
||||
}
|
||||
|
||||
const promise = fetch(
|
||||
`https://${process.env.VERCEL_URL}/_vercel/insights/event`,
|
||||
{
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'user-agent': headers['user-agent'] as string,
|
||||
'x-forwarded-for': headers['x-forwarded-for'] as string,
|
||||
'x-va-server': '1'
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
method: 'POST'
|
||||
}
|
||||
)
|
||||
|
||||
if (event) {
|
||||
event.waitUntil(promise)
|
||||
}
|
||||
{
|
||||
await promise
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { JetBrains_Mono as FontMono, Inter as FontSans } from 'next/font/google'
|
||||
|
||||
export const fontSans = FontSans({
|
||||
subsets: ['latin'],
|
||||
variable: '--font-sans'
|
||||
})
|
||||
|
||||
export const fontMono = FontMono({
|
||||
subsets: ['latin'],
|
||||
variable: '--font-mono'
|
||||
})
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import * as React from 'react'
|
||||
|
||||
export function useAtBottom(offset = 0) {
|
||||
const [isAtBottom, setIsAtBottom] = React.useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
setIsAtBottom(
|
||||
window.innerHeight + window.scrollY >=
|
||||
document.body.offsetHeight - offset
|
||||
)
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', handleScroll, { passive: true })
|
||||
handleScroll()
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('scroll', handleScroll)
|
||||
}
|
||||
}, [offset])
|
||||
|
||||
return isAtBottom
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
|
||||
export const useLocalStorage = <T>(
|
||||
key: string,
|
||||
initialValue: T
|
||||
): [T, (value: T) => void] => {
|
||||
const [storedValue, setStoredValue] = useState(initialValue)
|
||||
|
||||
useEffect(() => {
|
||||
// Retrieve from localStorage
|
||||
const item = window.localStorage.getItem(key)
|
||||
if (item) {
|
||||
setStoredValue(JSON.parse(item))
|
||||
}
|
||||
}, [key])
|
||||
|
||||
const setValue = (value: T) => {
|
||||
// Save state
|
||||
setStoredValue(value)
|
||||
// Save to localStorage
|
||||
window.localStorage.setItem(key, JSON.stringify(value))
|
||||
}
|
||||
return [storedValue, setValue]
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import { type Message } from 'ai/react'
|
||||
import { CohereStream, StreamingTextResponse } from 'ai'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
const serverUrl =
|
||||
process.env.NEXT_PUBLIC_TABBY_SERVER_URL || 'http://localhost:8080'
|
||||
|
||||
export function usePatchFetch() {
|
||||
useEffect(() => {
|
||||
const fetch = window.fetch
|
||||
|
||||
window.fetch = async function (url, options) {
|
||||
if (url !== '/api/chat') {
|
||||
return fetch(url, options)
|
||||
}
|
||||
|
||||
const { messages } = JSON.parse(options!.body as string)
|
||||
const res = await fetch(`${serverUrl}/v1beta/generate_stream`, {
|
||||
...options,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
prompt: messagesToPrompt(messages)
|
||||
})
|
||||
})
|
||||
|
||||
const stream = CohereStream(res, undefined)
|
||||
return new StreamingTextResponse(stream)
|
||||
}
|
||||
}, [])
|
||||
}
|
||||
|
||||
function messagesToPrompt(messages: Message[]) {
|
||||
const instruction = messages[messages.length - 1].content
|
||||
const prompt = `Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\n${instruction}\n\n### Response:`
|
||||
return prompt
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
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
|
||||
}
|
||||
|
||||
export type ServerActionResult<Result> = Promise<
|
||||
| Result
|
||||
| {
|
||||
error: string
|
||||
}
|
||||
>
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
import { clsx, type ClassValue } from 'clsx'
|
||||
import { customAlphabet } from 'nanoid'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
export const nanoid = customAlphabet(
|
||||
'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
|
||||
7
|
||||
) // 7-character random string
|
||||
|
||||
export async function fetcher<JSON = any>(
|
||||
input: RequestInfo,
|
||||
init?: RequestInit
|
||||
): Promise<JSON> {
|
||||
const res = await fetch(input, init)
|
||||
|
||||
if (!res.ok) {
|
||||
const json = await res.json()
|
||||
if (json.error) {
|
||||
const error = new Error(json.error) as Error & {
|
||||
status: number
|
||||
}
|
||||
error.status = res.status
|
||||
throw error
|
||||
} else {
|
||||
throw new Error('An unexpected error occurred')
|
||||
}
|
||||
}
|
||||
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export function formatDate(input: string | number | Date): string {
|
||||
const date = new Date(input)
|
||||
return date.toLocaleDateString('en-US', {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
module.exports = {
|
||||
reactStrictMode: true,
|
||||
basePath: '/playground',
|
||||
output: "export",
|
||||
images: {
|
||||
unoptimized: true
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
{
|
||||
"name": "tabby-playground",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"lint:fix": "next lint --fix",
|
||||
"preview": "next build && next start",
|
||||
"type-check": "tsc --noEmit",
|
||||
"format:write": "prettier --write \"{app,lib,components}/**/*.{ts,tsx,mdx}\" --cache",
|
||||
"format:check": "prettier --check \"{app,lib,components}**/*.{ts,tsx,mdx}\" --cache"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-alert-dialog": "1.0.4",
|
||||
"@radix-ui/react-dialog": "1.0.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.5",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-select": "^1.2.2",
|
||||
"@radix-ui/react-separator": "^1.0.3",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@radix-ui/react-tooltip": "^1.0.6",
|
||||
"@vercel/analytics": "^1.0.0",
|
||||
"@vercel/kv": "^0.2.1",
|
||||
"@vercel/og": "^0.5.7",
|
||||
"ai": "^2.1.6",
|
||||
"class-variance-authority": "^0.4.0",
|
||||
"clsx": "^1.2.1",
|
||||
"focus-trap-react": "^10.1.1",
|
||||
"nanoid": "^4.0.2",
|
||||
"next": "^13.4.7",
|
||||
"next-auth": "0.0.0-manual.83c4ebd1",
|
||||
"next-themes": "^0.2.1",
|
||||
"openai-edge": "^0.5.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-intersection-observer": "^9.4.4",
|
||||
"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"
|
||||
},
|
||||
"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.7-canary.1",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-tailwindcss": "^3.12.0",
|
||||
"postcss": "^8.4.21",
|
||||
"prettier": "^2.7.1",
|
||||
"tailwind-merge": "^1.12.0",
|
||||
"tailwindcss": "^3.3.1",
|
||||
"tailwindcss-animate": "^1.0.5",
|
||||
"typescript": "^5.1.3"
|
||||
},
|
||||
"packageManager": "pnpm@8.6.3"
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
const { fontFamily } = require('tailwindcss/defaultTheme')
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
darkMode: ['class'],
|
||||
content: ['app/**/*.{ts,tsx}', 'components/**/*.{ts,tsx}'],
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: '2rem',
|
||||
screens: {
|
||||
'2xl': '1400px'
|
||||
}
|
||||
},
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['var(--font-sans)', ...fontFamily.sans]
|
||||
},
|
||||
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))'
|
||||
},
|
||||
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))'
|
||||
}
|
||||
},
|
||||
borderRadius: {
|
||||
lg: `var(--radius)`,
|
||||
md: `calc(var(--radius) - 2px)`,
|
||||
sm: 'calc(var(--radius) - 4px)'
|
||||
},
|
||||
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-from-left': {
|
||||
'0%': {
|
||||
transform: 'translateX(-100%)'
|
||||
},
|
||||
'100%': {
|
||||
transform: 'translateX(0)'
|
||||
}
|
||||
},
|
||||
'slide-to-left': {
|
||||
'0%': {
|
||||
transform: 'translateX(0)'
|
||||
},
|
||||
'100%': {
|
||||
transform: 'translateX(-100%)'
|
||||
}
|
||||
}
|
||||
},
|
||||
animation: {
|
||||
'slide-from-left':
|
||||
'slide-from-left 0.3s cubic-bezier(0.82, 0.085, 0.395, 0.895)',
|
||||
'slide-to-left':
|
||||
'slide-to-left 0.25s cubic-bezier(0.82, 0.085, 0.395, 0.895)',
|
||||
'accordion-down': 'accordion-down 0.2s ease-out',
|
||||
'accordion-up': 'accordion-up 0.2s ease-out'
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [require('tailwindcss-animate'), require('@tailwindcss/typography')]
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"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",
|
||||
"next-auth.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts"
|
||||
, "lib/patch-fetch.js" ],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -21,7 +21,7 @@ serde_json = { workspace = true }
|
|||
tower-http = { version = "0.4.0", features = ["cors"] }
|
||||
clap = { version = "4.3.0", features = ["derive"] }
|
||||
lazy_static = { workspace = true }
|
||||
rust-embed = "6.6.1"
|
||||
rust-embed = "8.0.0"
|
||||
mime_guess = "2.0.4"
|
||||
strum = { version = "0.24", features = ["derive"] }
|
||||
strfmt = "0.2.4"
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
self.__BUILD_MANIFEST={__rewrites:{afterFiles:[],beforeFiles:[],fallback:[]},"/_error":["static/chunks/pages/_error-9de0d1f4f4d1fcb4.js"],sortedPages:["/_app","/_error"]},self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB();
|
||||
|
|
@ -0,0 +1 @@
|
|||
self.__SSG_MANIFEST=new Set([]);self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB()
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[376],{15376:function(n,s,e){e.r(s),e.d(s,{ThemeToggle:function(){return c}});var l=e(57437),a=e(2265),i=e(6435),t=e(93023),r=e(84168);function c(){let{setTheme:n,theme:s}=(0,i.F)(),[e,c]=a.useTransition();return(0,l.jsxs)(t.z,{variant:"ghost",size:"icon",onClick:()=>{c(()=>{n("light"===s?"dark":"light")})},children:[s?"dark"===s?(0,l.jsx)(r.C9,{className:"transition-all"}):(0,l.jsx)(r.O3,{className:"transition-all"}):null,(0,l.jsx)("span",{className:"sr-only",children:"Toggle theme"})]})}}}]);
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[165],{83155:function(e,t,n){(window.__NEXT_P=window.__NEXT_P||[]).push(["/_not-found",function(){return n(15991)}])},15991:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"default",{enumerable:!0,get:function(){return i}});let l=n(21024),r=l._(n(2265)),o={error:{fontFamily:'system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"',height:"100vh",textAlign:"center",display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center"},desc:{display:"inline-block"},h1:{display:"inline-block",margin:"0 20px 0 0",padding:"0 23px 0 0",fontSize:24,fontWeight:500,verticalAlign:"top",lineHeight:"49px"},h2:{fontSize:14,fontWeight:400,lineHeight:"49px",margin:0}};function i(){return r.default.createElement(r.default.Fragment,null,r.default.createElement("title",null,"404: This page could not be found."),r.default.createElement("div",{style:o.error},r.default.createElement("div",null,r.default.createElement("style",{dangerouslySetInnerHTML:{__html:"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}),r.default.createElement("h1",{className:"next-error-h1",style:o.h1},"404"),r.default.createElement("div",{style:o.desc},r.default.createElement("h2",{style:o.h2},"This page could not be found.")))))}("function"==typeof t.default||"object"==typeof t.default&&null!==t.default)&&void 0===t.default.__esModule&&(Object.defineProperty(t.default,"__esModule",{value:!0}),Object.assign(t.default,t),e.exports=t.default)}},function(e){e.O(0,[971,864,744],function(){return e(e.s=83155)}),_N_E=e.O()}]);
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[744],{47469:function(e,n,t){Promise.resolve().then(t.t.bind(t,76054,23)),Promise.resolve().then(t.t.bind(t,41729,23)),Promise.resolve().then(t.t.bind(t,81443,23)),Promise.resolve().then(t.t.bind(t,36384,23)),Promise.resolve().then(t.t.bind(t,18639,23)),Promise.resolve().then(t.t.bind(t,65146,23))}},function(e){var n=function(n){return e(e.s=n)};e.O(0,[971,864],function(){return n(23123),n(47469)}),_N_E=e.O()}]);
|
||||
|
|
@ -0,0 +1 @@
|
|||
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[888],{41597:function(n,_,u){(window.__NEXT_P=window.__NEXT_P||[]).push(["/_app",function(){return u(2840)}])}},function(n){var _=function(_){return n(n.s=_)};n.O(0,[774,179],function(){return _(41597),_(42338)}),_N_E=n.O()}]);
|
||||
|
|
@ -0,0 +1 @@
|
|||
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[820],{81981:function(n,_,u){(window.__NEXT_P=window.__NEXT_P||[]).push(["/_error",function(){return u(92534)}])}},function(n){n.O(0,[888,774,179],function(){return n(n.s=81981)}),_N_E=n.O()}]);
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
!function(){"use strict";var e,t,n,r,o,u,i,c,f,a={},l={};function s(e){var t=l[e];if(void 0!==t)return t.exports;var n=l[e]={exports:{}},r=!0;try{a[e](n,n.exports,s),r=!1}finally{r&&delete l[e]}return n.exports}s.m=a,e=[],s.O=function(t,n,r,o){if(n){o=o||0;for(var u=e.length;u>0&&e[u-1][2]>o;u--)e[u]=e[u-1];e[u]=[n,r,o];return}for(var i=1/0,u=0;u<e.length;u++){for(var n=e[u][0],r=e[u][1],o=e[u][2],c=!0,f=0;f<n.length;f++)i>=o&&Object.keys(s.O).every(function(e){return s.O[e](n[f])})?n.splice(f--,1):(c=!1,o<i&&(i=o));if(c){e.splice(u--,1);var a=r()}}return a},s.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return s.d(t,{a:t}),t},n=Object.getPrototypeOf?function(e){return Object.getPrototypeOf(e)}:function(e){return e.__proto__},s.t=function(e,r){if(1&r&&(e=this(e)),8&r||"object"==typeof e&&e&&(4&r&&e.__esModule||16&r&&"function"==typeof e.then))return e;var o=Object.create(null);s.r(o);var u={};t=t||[null,n({}),n([]),n(n)];for(var i=2&r&&e;"object"==typeof i&&!~t.indexOf(i);i=n(i))Object.getOwnPropertyNames(i).forEach(function(t){u[t]=function(){return e[t]}});return u.default=function(){return e},s.d(o,u),o},s.d=function(e,t){for(var n in t)s.o(t,n)&&!s.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},s.f={},s.e=function(e){return Promise.all(Object.keys(s.f).reduce(function(t,n){return s.f[n](e,t),t},[]))},s.u=function(e){return"static/chunks/"+e+".2b6536d53b303d15.js"},s.miniCssF=function(e){return"static/css/d091dc2da2a795e4.css"},s.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||Function("return this")()}catch(e){if("object"==typeof window)return window}}(),s.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r={},o="_N_E:",s.l=function(e,t,n,u){if(r[e]){r[e].push(t);return}if(void 0!==n)for(var i,c,f=document.getElementsByTagName("script"),a=0;a<f.length;a++){var l=f[a];if(l.getAttribute("src")==e||l.getAttribute("data-webpack")==o+n){i=l;break}}i||(c=!0,(i=document.createElement("script")).charset="utf-8",i.timeout=120,s.nc&&i.setAttribute("nonce",s.nc),i.setAttribute("data-webpack",o+n),i.src=s.tu(e)),r[e]=[t];var d=function(t,n){i.onerror=i.onload=null,clearTimeout(p);var o=r[e];if(delete r[e],i.parentNode&&i.parentNode.removeChild(i),o&&o.forEach(function(e){return e(n)}),t)return t(n)},p=setTimeout(d.bind(null,void 0,{type:"timeout",target:i}),12e4);i.onerror=d.bind(null,i.onerror),i.onload=d.bind(null,i.onload),c&&document.head.appendChild(i)},s.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},s.tt=function(){return void 0===u&&(u={createScriptURL:function(e){return e}},"undefined"!=typeof trustedTypes&&trustedTypes.createPolicy&&(u=trustedTypes.createPolicy("nextjs#bundler",u))),u},s.tu=function(e){return s.tt().createScriptURL(e)},s.p="/playground/_next/",i={272:0},s.f.j=function(e,t){var n=s.o(i,e)?i[e]:void 0;if(0!==n){if(n)t.push(n[2]);else if(272!=e){var r=new Promise(function(t,r){n=i[e]=[t,r]});t.push(n[2]=r);var o=s.p+s.u(e),u=Error();s.l(o,function(t){if(s.o(i,e)&&(0!==(n=i[e])&&(i[e]=void 0),n)){var r=t&&("load"===t.type?"missing":t.type),o=t&&t.target&&t.target.src;u.message="Loading chunk "+e+" failed.\n("+r+": "+o+")",u.name="ChunkLoadError",u.type=r,u.request=o,n[1](u)}},"chunk-"+e,e)}else i[e]=0}},s.O.j=function(e){return 0===i[e]},c=function(e,t){var n,r,o=t[0],u=t[1],c=t[2],f=0;if(o.some(function(e){return 0!==i[e]})){for(n in u)s.o(u,n)&&(s.m[n]=u[n]);if(c)var a=c(s)}for(e&&e(t);f<o.length;f++)r=o[f],s.o(i,r)&&i[r]&&i[r][0](),i[r]=0;return s.O(a)},(f=self.webpackChunk_N_E=self.webpackChunk_N_E||[]).forEach(c.bind(null,0)),f.push=c.bind(null,f.push.bind(f))}();
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,13 @@
|
|||
1:HL["/playground/_next/static/media/86fdec36ddd9097e-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}]
|
||||
2:HL["/playground/_next/static/media/c9a5bc6a7c948fb0-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}]
|
||||
3:HL["/playground/_next/static/css/d091dc2da2a795e4.css","style"]
|
||||
0:["IDbc2tNjcWJDV9VC-wJcu",[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],"$L4",[[["$","link","0",{"rel":"stylesheet","href":"/playground/_next/static/css/d091dc2da2a795e4.css","precedence":"next"}]],"$L5"]]]]
|
||||
6:I{"id":5925,"chunks":["346:static/chunks/346-c4227fa5fd95e485.js","524:static/chunks/524-e377ca48d97ab2b7.js","185:static/chunks/app/layout-21eaa53709d9db66.js"],"name":"Toaster","async":false}
|
||||
7:I{"id":78495,"chunks":["346:static/chunks/346-c4227fa5fd95e485.js","524:static/chunks/524-e377ca48d97ab2b7.js","185:static/chunks/app/layout-21eaa53709d9db66.js"],"name":"Providers","async":false}
|
||||
8:I{"id":78963,"chunks":["346:static/chunks/346-c4227fa5fd95e485.js","524:static/chunks/524-e377ca48d97ab2b7.js","185:static/chunks/app/layout-21eaa53709d9db66.js"],"name":"Header","async":false}
|
||||
9:I{"id":81443,"chunks":["272:static/chunks/webpack-52ce74dd37dd8861.js","971:static/chunks/fd9d1056-5dfc77aa37d8c76f.js","864:static/chunks/864-1669531662d5540a.js"],"name":"","async":false}
|
||||
a:I{"id":18639,"chunks":["272:static/chunks/webpack-52ce74dd37dd8861.js","971:static/chunks/fd9d1056-5dfc77aa37d8c76f.js","864:static/chunks/864-1669531662d5540a.js"],"name":"","async":false}
|
||||
c:I{"id":64074,"chunks":["346:static/chunks/346-c4227fa5fd95e485.js","978:static/chunks/978-ab68c4a2390585a1.js","524:static/chunks/524-e377ca48d97ab2b7.js","931:static/chunks/app/page-f0348ea0b604a423.js"],"name":"Chat","async":false}
|
||||
5:[["$","meta","0",{"charSet":"utf-8"}],["$","title","1",{"children":"Tabby Playground"}],["$","meta","2",{"name":"description","content":"Tabby, an opensource, self-hosted AI coding assistant."}],["$","meta","3",{"name":"theme-color","media":"(prefers-color-scheme: light)","content":"white"}],["$","meta","4",{"name":"theme-color","media":"(prefers-color-scheme: dark)","content":"black"}],["$","meta","5",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","6",{"name":"next-size-adjust"}]]
|
||||
4:[null,["$","html",null,{"lang":"en","suppressHydrationWarning":true,"children":[["$","head",null,{}],["$","body",null,{"className":"font-sans antialiased __variable_4e6684 __variable_3d950d","children":[["$","$L6",null,{}],["$","$L7",null,{"attribute":"class","defaultTheme":"system","enableSystem":true,"children":[["$","div",null,{"className":"flex flex-col min-h-screen","children":[["$","$L8",null,{}],["$","main",null,{"className":"flex flex-col flex-1 bg-muted/50","children":["$","$L9",null,{"parallelRouterKey":"children","segmentPath":["children"],"loading":"$undefined","loadingStyles":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","template":["$","$La",null,{}],"templateStyles":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[],"childProp":{"current":["$Lb",["$","$Lc",null,{"id":"WhCLNGT"}],null],"segment":"__PAGE__"},"styles":[]}]}]]}],null]}]]}]]}],null]
|
||||
b:null
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue