From eb15933255e7cca5a9e3d82f79f3d094b117b68f Mon Sep 17 00:00:00 2001 From: Meng Zhang Date: Fri, 29 Sep 2023 15:51:54 -0700 Subject: [PATCH] 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 * update --- Cargo.lock | 44 +- Makefile | 5 + clients/tabby-playground/.env.example | 1 + clients/tabby-playground/.eslintrc.json | 25 + clients/tabby-playground/.gitignore | 38 + clients/tabby-playground/LICENSE | 13 + clients/tabby-playground/README.md | 73 + clients/tabby-playground/app/globals.css | 78 + clients/tabby-playground/app/layout.tsx | 51 + clients/tabby-playground/app/page.tsx | 8 + .../assets/fonts/Inter-Bold.woff | Bin 0 -> 25760 bytes .../assets/fonts/Inter-Regular.woff | Bin 0 -> 24576 bytes .../components/button-scroll-to-bottom.tsx | 34 + .../tabby-playground/components/chat-list.tsx | 27 + .../components/chat-message-actions.tsx | 40 + .../components/chat-message.tsx | 93 + .../components/chat-panel.tsx | 78 + .../components/chat-scroll-anchor.tsx | 29 + clients/tabby-playground/components/chat.tsx | 69 + .../components/clear-history.tsx | 73 + .../components/empty-screen.tsx | 43 + .../components/external-link.tsx | 29 + .../tabby-playground/components/footer.tsx | 19 + .../tabby-playground/components/header.tsx | 33 + .../components/login-button.tsx | 42 + .../tabby-playground/components/markdown.tsx | 9 + .../components/prompt-form.tsx | 88 + .../tabby-playground/components/providers.tsx | 15 + .../components/tailwind-indicator.tsx | 14 + .../components/theme-toggle.tsx | 31 + .../tabby-playground/components/toaster.tsx | 3 + .../components/ui/alert-dialog.tsx | 150 + .../tabby-playground/components/ui/badge.tsx | 36 + .../tabby-playground/components/ui/button.tsx | 57 + .../components/ui/codeblock.tsx | 145 + .../tabby-playground/components/ui/dialog.tsx | 128 + .../components/ui/dropdown-menu.tsx | 128 + .../tabby-playground/components/ui/icons.tsx | 507 ++ .../tabby-playground/components/ui/input.tsx | 25 + .../tabby-playground/components/ui/label.tsx | 26 + .../tabby-playground/components/ui/select.tsx | 123 + .../components/ui/separator.tsx | 31 + .../tabby-playground/components/ui/sheet.tsx | 122 + .../tabby-playground/components/ui/switch.tsx | 29 + .../components/ui/textarea.tsx | 24 + .../components/ui/tooltip.tsx | 30 + .../tabby-playground/components/user-menu.tsx | 79 + clients/tabby-playground/lib/analytics.ts | 62 + clients/tabby-playground/lib/fonts.ts | 11 + .../lib/hooks/use-at-bottom.tsx | 23 + .../lib/hooks/use-copy-to-clipboard.tsx | 33 + .../lib/hooks/use-enter-submit.tsx | 23 + .../lib/hooks/use-local-storage.ts | 24 + .../lib/hooks/use-patch-fetch.ts | 39 + clients/tabby-playground/lib/types.ts | 18 + clients/tabby-playground/lib/utils.ts | 43 + clients/tabby-playground/next-env.d.ts | 5 + clients/tabby-playground/next.config.js | 9 + clients/tabby-playground/package.json | 68 + clients/tabby-playground/postcss.config.js | 6 + clients/tabby-playground/prettier.config.cjs | 34 + clients/tabby-playground/tailwind.config.js | 96 + clients/tabby-playground/tsconfig.json | 35 + clients/tabby-playground/yarn.lock | 4522 +++++++++++++++++ crates/tabby/Cargo.toml | 2 +- crates/tabby/playground/404.html | 1 + .../IDbc2tNjcWJDV9VC-wJcu/_buildManifest.js | 1 + .../IDbc2tNjcWJDV9VC-wJcu/_ssgManifest.js | 1 + .../static/chunks/346-c4227fa5fd95e485.js | 185 + .../static/chunks/376.2b6536d53b303d15.js | 1 + .../static/chunks/524-e377ca48d97ab2b7.js | 1 + .../static/chunks/864-1669531662d5540a.js | 25 + .../static/chunks/978-ab68c4a2390585a1.js | 34 + .../chunks/app/_not-found-58bcddf7b3e44a54.js | 1 + .../chunks/app/layout-21eaa53709d9db66.js | 1 + .../chunks/app/page-f0348ea0b604a423.js | 1 + .../chunks/fd9d1056-5dfc77aa37d8c76f.js | 9 + .../chunks/framework-43665103d101a22d.js | 25 + .../static/chunks/main-02b01a461f5aae93.js | 1 + .../chunks/main-app-63509e933f53c55d.js | 1 + .../chunks/pages/_app-6ca4a4ec31e39f3d.js | 1 + .../chunks/pages/_error-9de0d1f4f4d1fcb4.js | 1 + .../chunks/polyfills-c67a75d1b6f99dc8.js | 1 + .../static/chunks/webpack-52ce74dd37dd8861.js | 1 + .../_next/static/css/d091dc2da2a795e4.css | 3 + .../static/media/05a31a2ca4975f99-s.woff2 | Bin 0 -> 10496 bytes .../static/media/34dd45dcdd6d47ee-s.woff2 | Bin 0 -> 7240 bytes .../static/media/513657b02c5c193f-s.woff2 | Bin 0 -> 17612 bytes .../static/media/51ed15f9841b9f9d-s.woff2 | Bin 0 -> 22524 bytes .../static/media/86fdec36ddd9097e-s.p.woff2 | Bin 0 -> 39888 bytes .../static/media/9e58c89b9633dcad-s.woff2 | Bin 0 -> 11824 bytes .../static/media/a1ab2e69d2f53384-s.woff2 | Bin 0 -> 14428 bytes .../static/media/c4a41ea065a0023c-s.woff2 | Bin 0 -> 8848 bytes .../static/media/c9a5bc6a7c948fb0-s.p.woff2 | Bin 0 -> 46552 bytes .../static/media/d6b16ce4a6175f26-s.woff2 | Bin 0 -> 80044 bytes .../static/media/de2ba2ebf355004e-s.woff2 | Bin 0 -> 1944 bytes .../static/media/ec159349637c90ad-s.woff2 | Bin 0 -> 27316 bytes .../static/media/fd4db3eb5472fc27-s.woff2 | Bin 0 -> 12768 bytes crates/tabby/playground/index.html | 1 + crates/tabby/playground/index.txt | 13 + crates/tabby/src/serve/mod.rs | 3 + .../src/serve/{admin.rs => playground.rs} | 20 +- 102 files changed, 8116 insertions(+), 14 deletions(-) create mode 100644 clients/tabby-playground/.env.example create mode 100644 clients/tabby-playground/.eslintrc.json create mode 100644 clients/tabby-playground/.gitignore create mode 100644 clients/tabby-playground/LICENSE create mode 100644 clients/tabby-playground/README.md create mode 100644 clients/tabby-playground/app/globals.css create mode 100644 clients/tabby-playground/app/layout.tsx create mode 100644 clients/tabby-playground/app/page.tsx create mode 100644 clients/tabby-playground/assets/fonts/Inter-Bold.woff create mode 100644 clients/tabby-playground/assets/fonts/Inter-Regular.woff create mode 100644 clients/tabby-playground/components/button-scroll-to-bottom.tsx create mode 100644 clients/tabby-playground/components/chat-list.tsx create mode 100644 clients/tabby-playground/components/chat-message-actions.tsx create mode 100644 clients/tabby-playground/components/chat-message.tsx create mode 100644 clients/tabby-playground/components/chat-panel.tsx create mode 100644 clients/tabby-playground/components/chat-scroll-anchor.tsx create mode 100644 clients/tabby-playground/components/chat.tsx create mode 100644 clients/tabby-playground/components/clear-history.tsx create mode 100644 clients/tabby-playground/components/empty-screen.tsx create mode 100644 clients/tabby-playground/components/external-link.tsx create mode 100644 clients/tabby-playground/components/footer.tsx create mode 100644 clients/tabby-playground/components/header.tsx create mode 100644 clients/tabby-playground/components/login-button.tsx create mode 100644 clients/tabby-playground/components/markdown.tsx create mode 100644 clients/tabby-playground/components/prompt-form.tsx create mode 100644 clients/tabby-playground/components/providers.tsx create mode 100644 clients/tabby-playground/components/tailwind-indicator.tsx create mode 100644 clients/tabby-playground/components/theme-toggle.tsx create mode 100644 clients/tabby-playground/components/toaster.tsx create mode 100644 clients/tabby-playground/components/ui/alert-dialog.tsx create mode 100644 clients/tabby-playground/components/ui/badge.tsx create mode 100644 clients/tabby-playground/components/ui/button.tsx create mode 100644 clients/tabby-playground/components/ui/codeblock.tsx create mode 100644 clients/tabby-playground/components/ui/dialog.tsx create mode 100644 clients/tabby-playground/components/ui/dropdown-menu.tsx create mode 100644 clients/tabby-playground/components/ui/icons.tsx create mode 100644 clients/tabby-playground/components/ui/input.tsx create mode 100644 clients/tabby-playground/components/ui/label.tsx create mode 100644 clients/tabby-playground/components/ui/select.tsx create mode 100644 clients/tabby-playground/components/ui/separator.tsx create mode 100644 clients/tabby-playground/components/ui/sheet.tsx create mode 100644 clients/tabby-playground/components/ui/switch.tsx create mode 100644 clients/tabby-playground/components/ui/textarea.tsx create mode 100644 clients/tabby-playground/components/ui/tooltip.tsx create mode 100644 clients/tabby-playground/components/user-menu.tsx create mode 100644 clients/tabby-playground/lib/analytics.ts create mode 100644 clients/tabby-playground/lib/fonts.ts create mode 100644 clients/tabby-playground/lib/hooks/use-at-bottom.tsx create mode 100644 clients/tabby-playground/lib/hooks/use-copy-to-clipboard.tsx create mode 100644 clients/tabby-playground/lib/hooks/use-enter-submit.tsx create mode 100644 clients/tabby-playground/lib/hooks/use-local-storage.ts create mode 100644 clients/tabby-playground/lib/hooks/use-patch-fetch.ts create mode 100644 clients/tabby-playground/lib/types.ts create mode 100644 clients/tabby-playground/lib/utils.ts create mode 100644 clients/tabby-playground/next-env.d.ts create mode 100644 clients/tabby-playground/next.config.js create mode 100644 clients/tabby-playground/package.json create mode 100644 clients/tabby-playground/postcss.config.js create mode 100644 clients/tabby-playground/prettier.config.cjs create mode 100644 clients/tabby-playground/tailwind.config.js create mode 100644 clients/tabby-playground/tsconfig.json create mode 100644 clients/tabby-playground/yarn.lock create mode 100644 crates/tabby/playground/404.html create mode 100644 crates/tabby/playground/_next/static/IDbc2tNjcWJDV9VC-wJcu/_buildManifest.js create mode 100644 crates/tabby/playground/_next/static/IDbc2tNjcWJDV9VC-wJcu/_ssgManifest.js create mode 100644 crates/tabby/playground/_next/static/chunks/346-c4227fa5fd95e485.js create mode 100644 crates/tabby/playground/_next/static/chunks/376.2b6536d53b303d15.js create mode 100644 crates/tabby/playground/_next/static/chunks/524-e377ca48d97ab2b7.js create mode 100644 crates/tabby/playground/_next/static/chunks/864-1669531662d5540a.js create mode 100644 crates/tabby/playground/_next/static/chunks/978-ab68c4a2390585a1.js create mode 100644 crates/tabby/playground/_next/static/chunks/app/_not-found-58bcddf7b3e44a54.js create mode 100644 crates/tabby/playground/_next/static/chunks/app/layout-21eaa53709d9db66.js create mode 100644 crates/tabby/playground/_next/static/chunks/app/page-f0348ea0b604a423.js create mode 100644 crates/tabby/playground/_next/static/chunks/fd9d1056-5dfc77aa37d8c76f.js create mode 100644 crates/tabby/playground/_next/static/chunks/framework-43665103d101a22d.js create mode 100644 crates/tabby/playground/_next/static/chunks/main-02b01a461f5aae93.js create mode 100644 crates/tabby/playground/_next/static/chunks/main-app-63509e933f53c55d.js create mode 100644 crates/tabby/playground/_next/static/chunks/pages/_app-6ca4a4ec31e39f3d.js create mode 100644 crates/tabby/playground/_next/static/chunks/pages/_error-9de0d1f4f4d1fcb4.js create mode 100644 crates/tabby/playground/_next/static/chunks/polyfills-c67a75d1b6f99dc8.js create mode 100644 crates/tabby/playground/_next/static/chunks/webpack-52ce74dd37dd8861.js create mode 100644 crates/tabby/playground/_next/static/css/d091dc2da2a795e4.css create mode 100644 crates/tabby/playground/_next/static/media/05a31a2ca4975f99-s.woff2 create mode 100644 crates/tabby/playground/_next/static/media/34dd45dcdd6d47ee-s.woff2 create mode 100644 crates/tabby/playground/_next/static/media/513657b02c5c193f-s.woff2 create mode 100644 crates/tabby/playground/_next/static/media/51ed15f9841b9f9d-s.woff2 create mode 100644 crates/tabby/playground/_next/static/media/86fdec36ddd9097e-s.p.woff2 create mode 100644 crates/tabby/playground/_next/static/media/9e58c89b9633dcad-s.woff2 create mode 100644 crates/tabby/playground/_next/static/media/a1ab2e69d2f53384-s.woff2 create mode 100644 crates/tabby/playground/_next/static/media/c4a41ea065a0023c-s.woff2 create mode 100644 crates/tabby/playground/_next/static/media/c9a5bc6a7c948fb0-s.p.woff2 create mode 100644 crates/tabby/playground/_next/static/media/d6b16ce4a6175f26-s.woff2 create mode 100644 crates/tabby/playground/_next/static/media/de2ba2ebf355004e-s.woff2 create mode 100644 crates/tabby/playground/_next/static/media/ec159349637c90ad-s.woff2 create mode 100644 crates/tabby/playground/_next/static/media/fd4db3eb5472fc27-s.woff2 create mode 100644 crates/tabby/playground/index.html create mode 100644 crates/tabby/playground/index.txt rename crates/tabby/src/serve/{admin.rs => playground.rs} (72%) diff --git a/Cargo.lock b/Cargo.lock index 20cc422..1acdf4e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Makefile b/Makefile index 30e9e41..87b4bce 100644 --- a/Makefile +++ b/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 + diff --git a/clients/tabby-playground/.env.example b/clients/tabby-playground/.env.example new file mode 100644 index 0000000..e0d09d0 --- /dev/null +++ b/clients/tabby-playground/.env.example @@ -0,0 +1 @@ +NEXT_PUBLIC_TABBY_SERVER_URL=http://127.0.0.1:8080 \ No newline at end of file diff --git a/clients/tabby-playground/.eslintrc.json b/clients/tabby-playground/.eslintrc.json new file mode 100644 index 0000000..6ec5479 --- /dev/null +++ b/clients/tabby-playground/.eslintrc.json @@ -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" + } + ] +} diff --git a/clients/tabby-playground/.gitignore b/clients/tabby-playground/.gitignore new file mode 100644 index 0000000..83d560e --- /dev/null +++ b/clients/tabby-playground/.gitignore @@ -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 \ No newline at end of file diff --git a/clients/tabby-playground/LICENSE b/clients/tabby-playground/LICENSE new file mode 100644 index 0000000..6c16c29 --- /dev/null +++ b/clients/tabby-playground/LICENSE @@ -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. \ No newline at end of file diff --git a/clients/tabby-playground/README.md b/clients/tabby-playground/README.md new file mode 100644 index 0000000..14fa983 --- /dev/null +++ b/clients/tabby-playground/README.md @@ -0,0 +1,73 @@ + + Next.js 13 and app template Router-ready AI chatbot. +

Next.js AI Chatbot

+
+ +

+ An open-source AI chatbot app template built with Next.js, the Vercel AI SDK, OpenAI, and Vercel KV. +

+ +

+ Features · + Model Providers · + Deploy Your Own · + Running locally · + Authors +

+
+ +## 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: + +[![Deploy with Vercel](https://vercel.com/button)](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) diff --git a/clients/tabby-playground/app/globals.css b/clients/tabby-playground/app/globals.css new file mode 100644 index 0000000..9beeb2c --- /dev/null +++ b/clients/tabby-playground/app/globals.css @@ -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; + } +} diff --git a/clients/tabby-playground/app/layout.tsx b/clients/tabby-playground/app/layout.tsx new file mode 100644 index 0000000..3d0d91e --- /dev/null +++ b/clients/tabby-playground/app/layout.tsx @@ -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 ( + + + + + +
+ {/* @ts-ignore */} +
+
{children}
+
+ +
+ + + ) +} diff --git a/clients/tabby-playground/app/page.tsx b/clients/tabby-playground/app/page.tsx new file mode 100644 index 0000000..c464137 --- /dev/null +++ b/clients/tabby-playground/app/page.tsx @@ -0,0 +1,8 @@ +import { nanoid } from '@/lib/utils' +import { Chat } from '@/components/chat' + +export default function IndexPage() { + const id = nanoid() + + return +} diff --git a/clients/tabby-playground/assets/fonts/Inter-Bold.woff b/clients/tabby-playground/assets/fonts/Inter-Bold.woff new file mode 100644 index 0000000000000000000000000000000000000000..1e80f6235899fc654faf3af6894340926a306d62 GIT binary patch literal 25760 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 L=W6`_^DzDk5^lI- literal 0 HcmV?d00001 diff --git a/clients/tabby-playground/assets/fonts/Inter-Regular.woff b/clients/tabby-playground/assets/fonts/Inter-Regular.woff new file mode 100644 index 0000000000000000000000000000000000000000..4c6b7118eec99ba089704281a3ba39e067977537 GIT binary patch literal 24576 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 ZPaXO@;{RvL|96|$fFA!R_@93B{{_4rJwE^d literal 0 HcmV?d00001 diff --git a/clients/tabby-playground/components/button-scroll-to-bottom.tsx b/clients/tabby-playground/components/button-scroll-to-bottom.tsx new file mode 100644 index 0000000..436a6c0 --- /dev/null +++ b/clients/tabby-playground/components/button-scroll-to-bottom.tsx @@ -0,0 +1,34 @@ +'use client' + +import * as React from 'react' + +import { cn } from '@/lib/utils' +import { useAtBottom } from '@/lib/hooks/use-at-bottom' +import { Button, type ButtonProps } from '@/components/ui/button' +import { IconArrowDown } from '@/components/ui/icons' + +export function ButtonScrollToBottom({ className, ...props }: ButtonProps) { + const isAtBottom = useAtBottom() + + return ( + + ) +} diff --git a/clients/tabby-playground/components/chat-list.tsx b/clients/tabby-playground/components/chat-list.tsx new file mode 100644 index 0000000..0aa677e --- /dev/null +++ b/clients/tabby-playground/components/chat-list.tsx @@ -0,0 +1,27 @@ +import { type Message } from 'ai' + +import { Separator } from '@/components/ui/separator' +import { ChatMessage } from '@/components/chat-message' + +export interface ChatList { + messages: Message[] +} + +export function ChatList({ messages }: ChatList) { + if (!messages.length) { + return null + } + + return ( +
+ {messages.map((message, index) => ( +
+ + {index < messages.length - 1 && ( + + )} +
+ ))} +
+ ) +} diff --git a/clients/tabby-playground/components/chat-message-actions.tsx b/clients/tabby-playground/components/chat-message-actions.tsx new file mode 100644 index 0000000..d4e4b40 --- /dev/null +++ b/clients/tabby-playground/components/chat-message-actions.tsx @@ -0,0 +1,40 @@ +'use client' + +import { type Message } from 'ai' + +import { Button } from '@/components/ui/button' +import { IconCheck, IconCopy } from '@/components/ui/icons' +import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard' +import { cn } from '@/lib/utils' + +interface ChatMessageActionsProps extends React.ComponentProps<'div'> { + message: Message +} + +export function ChatMessageActions({ + message, + className, + ...props +}: ChatMessageActionsProps) { + const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 }) + + const onCopy = () => { + if (isCopied) return + copyToClipboard(message.content) + } + + return ( +
+ +
+ ) +} diff --git a/clients/tabby-playground/components/chat-message.tsx b/clients/tabby-playground/components/chat-message.tsx new file mode 100644 index 0000000..88f6316 --- /dev/null +++ b/clients/tabby-playground/components/chat-message.tsx @@ -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 ( +
+
+ {message.role === 'user' ? : } +
+
+ {children}

+ }, + code({ node, inline, className, children, ...props }) { + if (children.length) { + if (children[0] == '▍') { + return ( + + ) + } + + children[0] = (children[0] as string).replace('`▍`', '▍') + } + + const match = /language-(\w+)/.exec(className || '') + + if (inline) { + return ( + + {children} + + ) + } + + return ( + + ) + } + }} + > + {message.content} +
+ +
+
+ ) +} + +function IconTabby() { + return ( + tabby + ) +} diff --git a/clients/tabby-playground/components/chat-panel.tsx b/clients/tabby-playground/components/chat-panel.tsx new file mode 100644 index 0000000..13e3c2f --- /dev/null +++ b/clients/tabby-playground/components/chat-panel.tsx @@ -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 ( +
+ +
+
+ {isLoading ? ( + + ) : ( + messages?.length > 0 && ( + + ) + )} +
+
+ { + await append({ + id, + content: value, + role: 'user' + }) + }} + input={input} + setInput={setInput} + isLoading={isLoading} + /> + +
+
+
+ ) +} diff --git a/clients/tabby-playground/components/chat-scroll-anchor.tsx b/clients/tabby-playground/components/chat-scroll-anchor.tsx new file mode 100644 index 0000000..ac809f4 --- /dev/null +++ b/clients/tabby-playground/components/chat-scroll-anchor.tsx @@ -0,0 +1,29 @@ +'use client' + +import * as React from 'react' +import { useInView } from 'react-intersection-observer' + +import { useAtBottom } from '@/lib/hooks/use-at-bottom' + +interface ChatScrollAnchorProps { + trackVisibility?: boolean +} + +export function ChatScrollAnchor({ trackVisibility }: ChatScrollAnchorProps) { + const isAtBottom = useAtBottom() + const { ref, entry, inView } = useInView({ + trackVisibility, + delay: 100, + rootMargin: '0px 0px -150px 0px' + }) + + React.useEffect(() => { + if (isAtBottom && trackVisibility && !inView) { + entry?.target.scrollIntoView({ + block: 'start' + }) + } + }, [inView, entry, isAtBottom, trackVisibility]) + + return
\ No newline at end of file diff --git a/crates/tabby/playground/index.txt b/crates/tabby/playground/index.txt new file mode 100644 index 0000000..417bfbd --- /dev/null +++ b/crates/tabby/playground/index.txt @@ -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 diff --git a/crates/tabby/src/serve/mod.rs b/crates/tabby/src/serve/mod.rs index 50052c4..9d7cace 100644 --- a/crates/tabby/src/serve/mod.rs +++ b/crates/tabby/src/serve/mod.rs @@ -3,6 +3,7 @@ mod engine; mod events; mod generate; mod health; +mod playground; use std::{ net::{Ipv4Addr, SocketAddr}, @@ -161,6 +162,8 @@ pub async fn main(config: &Config, args: &ServeArgs) { let doc = add_proxy_server(doc, config.swagger.server_url.clone()); let app = api_router(args, config) .merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", doc)) + .route("/playground", routing::get(playground::handler)) + .route("/playground/*path", routing::get(playground::handler)) .fallback(fallback()); let address = SocketAddr::from((Ipv4Addr::UNSPECIFIED, args.port)); diff --git a/crates/tabby/src/serve/admin.rs b/crates/tabby/src/serve/playground.rs similarity index 72% rename from crates/tabby/src/serve/admin.rs rename to crates/tabby/src/serve/playground.rs index b6c2c40..db8e824 100644 --- a/crates/tabby/src/serve/admin.rs +++ b/crates/tabby/src/serve/playground.rs @@ -7,21 +7,21 @@ use axum::{ use crate::fatal; #[derive(rust_embed::RustEmbed)] -#[folder = "../tabby-admin/dist/"] -struct AdminAssets; +#[folder = "./playground"] +struct WebAssets; -struct AdminStaticFile(pub T); +struct WebStaticFile(pub T); -impl IntoResponse for AdminStaticFile +impl IntoResponse for WebStaticFile where T: Into, { fn into_response(self) -> Response { let mut path = self.0.into(); - if AdminAssets::get(path.as_str()).is_none() { + if WebAssets::get(path.as_str()).is_none() { path = "index.html".to_owned(); } - match AdminAssets::get(path.as_str()) { + match WebAssets::get(path.as_str()) { Some(content) => { let body = boxed(Full::from(content.data)); let mime = mime_guess::from_path(path).first_or_octet_stream(); @@ -39,6 +39,10 @@ where } pub async fn handler(uri: Uri) -> impl IntoResponse { - let path = uri.path().trim_start_matches('/').to_string(); - AdminStaticFile(path) + let path = uri + .path() + .trim_start_matches("/playground") + .trim_start_matches('/') + .to_string(); + WebStaticFile(path) }
+} diff --git a/clients/tabby-playground/components/chat.tsx b/clients/tabby-playground/components/chat.tsx new file mode 100644 index 0000000..21113d4 --- /dev/null +++ b/clients/tabby-playground/components/chat.tsx @@ -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 ( + <> +
+ {messages.length ? ( + <> + + + + ) : ( + + )} +
+ + + ) +} diff --git a/clients/tabby-playground/components/clear-history.tsx b/clients/tabby-playground/components/clear-history.tsx new file mode 100644 index 0000000..16e3643 --- /dev/null +++ b/clients/tabby-playground/components/clear-history.tsx @@ -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 +} + +export function ClearHistory({ clearChats }: ClearHistoryProps) { + const [open, setOpen] = React.useState(false) + const [isPending, startTransition] = React.useTransition() + const router = useRouter() + + return ( + + + + + + + Are you absolutely sure? + + This will permanently delete your chat history and remove your data + from our servers. + + + + Cancel + { + event.preventDefault() + startTransition(async () => { + const result = await clearChats() + + if (result && 'error' in result) { + toast.error(result.error) + return + } + + setOpen(false) + router.push('/') + }) + }} + > + {isPending && } + Delete + + + + + ) +} diff --git a/clients/tabby-playground/components/empty-screen.tsx b/clients/tabby-playground/components/empty-screen.tsx new file mode 100644 index 0000000..44d1f85 --- /dev/null +++ b/clients/tabby-playground/components/empty-screen.tsx @@ -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) { + return ( +
+
+

+ Welcome to Tabby Playground! +

+

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

+
+ {exampleMessages.map((message, index) => ( + + ))} +
+
+
+ ) +} diff --git a/clients/tabby-playground/components/external-link.tsx b/clients/tabby-playground/components/external-link.tsx new file mode 100644 index 0000000..ba6cc01 --- /dev/null +++ b/clients/tabby-playground/components/external-link.tsx @@ -0,0 +1,29 @@ +export function ExternalLink({ + href, + children +}: { + href: string + children: React.ReactNode +}) { + return ( +
+ {children} + + + ) +} diff --git a/clients/tabby-playground/components/footer.tsx b/clients/tabby-playground/components/footer.tsx new file mode 100644 index 0000000..f59a863 --- /dev/null +++ b/clients/tabby-playground/components/footer.tsx @@ -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 ( +

+ Tabby, an + opensource, self-hosted AI coding assistant . +

+ ) +} diff --git a/clients/tabby-playground/components/header.tsx b/clients/tabby-playground/components/header.tsx new file mode 100644 index 0000000..31d754e --- /dev/null +++ b/clients/tabby-playground/components/header.tsx @@ -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 ( +
+ +
+ +
+ ) +} diff --git a/clients/tabby-playground/components/login-button.tsx b/clients/tabby-playground/components/login-button.tsx new file mode 100644 index 0000000..ae8f842 --- /dev/null +++ b/clients/tabby-playground/components/login-button.tsx @@ -0,0 +1,42 @@ +'use client' + +import * as React from 'react' +import { signIn } from 'next-auth/react' + +import { cn } from '@/lib/utils' +import { Button, type ButtonProps } from '@/components/ui/button' +import { IconGitHub, IconSpinner } from '@/components/ui/icons' + +interface LoginButtonProps extends ButtonProps { + showGithubIcon?: boolean + text?: string +} + +export function LoginButton({ + text = 'Login with GitHub', + showGithubIcon = true, + className, + ...props +}: LoginButtonProps) { + const [isLoading, setIsLoading] = React.useState(false) + return ( + + ) +} diff --git a/clients/tabby-playground/components/markdown.tsx b/clients/tabby-playground/components/markdown.tsx new file mode 100644 index 0000000..d449146 --- /dev/null +++ b/clients/tabby-playground/components/markdown.tsx @@ -0,0 +1,9 @@ +import { FC, memo } from 'react' +import ReactMarkdown, { Options } from 'react-markdown' + +export const MemoizedReactMarkdown: FC = memo( + ReactMarkdown, + (prevProps, nextProps) => + prevProps.children === nextProps.children && + prevProps.className === nextProps.className +) diff --git a/clients/tabby-playground/components/prompt-form.tsx b/clients/tabby-playground/components/prompt-form.tsx new file mode 100644 index 0000000..cdba06c --- /dev/null +++ b/clients/tabby-playground/components/prompt-form.tsx @@ -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 { + onSubmit: (value: string) => Promise + isLoading: boolean +} + +export function PromptForm({ + onSubmit, + input, + setInput, + isLoading +}: PromptProps) { + const { formRef, onKeyDown } = useEnterSubmit() + const inputRef = React.useRef(null) + const router = useRouter() + + React.useEffect(() => { + if (inputRef.current) { + inputRef.current.focus() + } + }, []) + + return ( +
{ + e.preventDefault() + if (!input?.trim()) { + return + } + setInput('') + await onSubmit(input) + }} + ref={formRef} + > +
+ + + +