├── .env.example ├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── banner.png ├── components.json ├── eslint.config.js ├── next.config.js ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── prettier.config.js ├── prisma ├── migrations │ ├── 20250608171639_users │ │ └── migration.sql │ ├── 20250608175206_chats │ │ └── migration.sql │ ├── 20250608210029_streams │ │ └── migration.sql │ ├── 20250612033842_files │ │ └── migration.sql │ ├── 20250613013025_images │ │ └── migration.sql │ ├── 20250615144652_chat_branching │ │ └── migration.sql │ ├── 20250615224432_features │ │ └── migration.sql │ ├── 20250616064236_workbenches │ │ └── migration.sql │ ├── 20250714211437_add_starring │ │ └── migration.sql │ └── migration_lock.toml ├── schema.prisma └── seed.ts ├── public ├── icons │ ├── anthropic.png │ ├── deepseek.png │ ├── discord.png │ ├── e2b.png │ ├── google.png │ ├── lucide.png │ ├── mem0.png │ ├── meta-llama.png │ ├── motion.png │ ├── next.png │ ├── nextauth.png │ ├── openai.png │ ├── perplexity.png │ ├── postgres.png │ ├── prisma.png │ ├── qwen.png │ ├── radix.png │ ├── react.png │ ├── recharts.png │ ├── redis.png │ ├── shadcn.png │ ├── t3.png │ ├── tRPC.png │ ├── tailwind.png │ ├── tanstack.png │ ├── vercel.png │ ├── x-ai.png │ ├── xai.png │ └── zod.png └── logo │ ├── dark.svg │ └── light.svg ├── src ├── ai │ ├── generate.ts │ ├── models │ │ ├── all.ts │ │ ├── anthropic.ts │ │ ├── deepseek.ts │ │ ├── google.ts │ │ ├── index.ts │ │ ├── llama.ts │ │ ├── openai.ts │ │ ├── perplexity.ts │ │ ├── qwen.ts │ │ └── xai.ts │ ├── registry.ts │ └── types.ts ├── app │ ├── [id] │ │ └── page.tsx │ ├── _components │ │ ├── auth │ │ │ └── auth-buttons.tsx │ │ ├── chat │ │ │ ├── chat.tsx │ │ │ ├── index.tsx │ │ │ ├── input │ │ │ │ ├── index.tsx │ │ │ │ ├── model-select │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── native-search-toggle.tsx │ │ │ │ │ ├── use-model-select.ts │ │ │ │ │ └── utils.tsx │ │ │ │ └── tools.tsx │ │ │ ├── layout.tsx │ │ │ ├── messages │ │ │ │ ├── greeting.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── message-actions.tsx │ │ │ │ ├── message-editor.tsx │ │ │ │ ├── message-reasoning.tsx │ │ │ │ ├── message-tool.tsx │ │ │ │ └── message.tsx │ │ │ ├── preview-attachment.tsx │ │ │ └── starter-prompts.tsx │ │ ├── landing-page │ │ │ ├── auth-modal.tsx │ │ │ ├── dependencies │ │ │ │ ├── data.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── types.ts │ │ │ ├── hero │ │ │ │ ├── data.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── message.tsx │ │ │ │ ├── message │ │ │ │ │ ├── code-execution.tsx │ │ │ │ │ ├── memory.tsx │ │ │ │ │ ├── recent-ai-news.tsx │ │ │ │ │ ├── repository.tsx │ │ │ │ │ └── top-repositories.tsx │ │ │ │ ├── motion-container.tsx │ │ │ │ ├── toolkit-demo-list.tsx │ │ │ │ └── types.ts │ │ │ ├── index.tsx │ │ │ ├── navbar │ │ │ │ └── index.tsx │ │ │ ├── toolkit-creation │ │ │ │ ├── data.ts │ │ │ │ └── index.tsx │ │ │ └── workbenches │ │ │ │ ├── card.tsx │ │ │ │ ├── data.tsx │ │ │ │ ├── graphic.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── types.ts │ │ ├── navbar │ │ │ ├── account-button │ │ │ │ ├── authenticated.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── provider-icon.tsx │ │ │ │ └── unauthenticated.tsx │ │ │ ├── color-mode-toggle.tsx │ │ │ └── index.tsx │ │ ├── sidebar │ │ │ ├── chats │ │ │ │ ├── index.tsx │ │ │ │ └── item.tsx │ │ │ ├── index.tsx │ │ │ ├── main.tsx │ │ │ ├── user.tsx │ │ │ └── workbench-select.tsx │ │ └── welcome-dialog.tsx │ ├── _contexts │ │ ├── chat-context.tsx │ │ └── theme.tsx │ ├── _hooks │ │ ├── use-auto-resume.tsx │ │ ├── use-chat-visibility.ts │ │ ├── use-delete-chat.ts │ │ ├── use-delete-messages.tsx │ │ ├── use-messages.tsx │ │ ├── use-scroll-to-bottom.tsx │ │ └── use-star-chat.ts │ ├── account │ │ ├── components │ │ │ ├── header.tsx │ │ │ └── tabs │ │ │ │ ├── attachments │ │ │ │ ├── attachment.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── table.tsx │ │ │ │ ├── connected-accounts │ │ │ │ ├── connect-disconnect.tsx │ │ │ │ └── index.tsx │ │ │ │ ├── images │ │ │ │ ├── index.tsx │ │ │ │ └── table.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── memories │ │ │ │ ├── index.tsx │ │ │ │ └── table.tsx │ │ └── page.tsx │ ├── admin │ │ ├── _components │ │ │ └── admin-panel.tsx │ │ └── page.tsx │ ├── api │ │ ├── auth │ │ │ └── [...nextauth] │ │ │ │ └── route.ts │ │ ├── chat │ │ │ ├── route.ts │ │ │ └── schema.ts │ │ ├── files │ │ │ └── upload │ │ │ │ └── route.ts │ │ ├── mcp │ │ │ └── [server] │ │ │ │ └── [transport] │ │ │ │ └── route.ts │ │ └── trpc │ │ │ └── [trpc] │ │ │ └── route.ts │ ├── favicon.ico │ ├── icon.png │ ├── layout.tsx │ ├── login │ │ ├── login-form.tsx │ │ └── page.tsx │ ├── opengraph-image.png │ ├── page.tsx │ ├── twitter-image.png │ └── workbench │ │ ├── [id] │ │ ├── [chatId] │ │ │ └── page.tsx │ │ ├── _components │ │ │ └── header.tsx │ │ ├── edit │ │ │ ├── _components │ │ │ │ └── edit-workbench-form.tsx │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ └── page.tsx │ │ └── new │ │ ├── _components │ │ ├── index.ts │ │ └── new-workbench-form.tsx │ │ └── page.tsx ├── components │ ├── magicui │ │ ├── animated-beam.tsx │ │ ├── animated-list.tsx │ │ ├── animated-shiny-text.tsx │ │ └── marquee.tsx │ ├── toolkit │ │ ├── toolkit-configure.tsx │ │ ├── toolkit-icons.tsx │ │ ├── toolkit-list.tsx │ │ └── types.ts │ └── ui │ │ ├── accordion.tsx │ │ ├── alert-dialog.tsx │ │ ├── avatar.tsx │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── checkbox.tsx │ │ ├── code-block.tsx │ │ ├── dialog.tsx │ │ ├── dropdown-menu.tsx │ │ ├── info-tooltip.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── logo.tsx │ │ ├── markdown.tsx │ │ ├── model-icon.tsx │ │ ├── popover.tsx │ │ ├── search-type-icon.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── sidebar.tsx │ │ ├── skeleton.tsx │ │ ├── slider.tsx │ │ ├── sonner.tsx │ │ ├── stack.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ └── tooltip.tsx ├── env.js ├── hooks │ └── use-mobile.ts ├── lib │ ├── constants.ts │ ├── cookies │ │ ├── client.ts │ │ ├── keys.ts │ │ ├── server.ts │ │ └── types.ts │ ├── errors.ts │ ├── fetch.ts │ └── utils.ts ├── server │ ├── api │ │ ├── root.ts │ │ ├── routers │ │ │ ├── accounts.ts │ │ │ ├── chats.ts │ │ │ ├── features.ts │ │ │ ├── files.ts │ │ │ ├── images.ts │ │ │ ├── index.ts │ │ │ ├── memories.ts │ │ │ ├── messages.ts │ │ │ ├── streams.ts │ │ │ ├── users.ts │ │ │ └── workbenches.ts │ │ └── trpc.ts │ ├── auth │ │ ├── config.ts │ │ ├── index.ts │ │ └── providers.ts │ └── db.ts ├── styles │ └── globals.css ├── toolkits │ ├── README.md │ ├── create-tool.ts │ ├── create-toolkit.ts │ ├── toolkit-groups.ts │ ├── toolkits │ │ ├── client.ts │ │ ├── e2b │ │ │ ├── base.ts │ │ │ ├── client.tsx │ │ │ ├── server.ts │ │ │ └── tools │ │ │ │ ├── run_code │ │ │ │ ├── base.ts │ │ │ │ ├── client.tsx │ │ │ │ └── server.ts │ │ │ │ └── tools.ts │ │ ├── exa │ │ │ ├── base.ts │ │ │ ├── client.tsx │ │ │ ├── components │ │ │ │ ├── index.ts │ │ │ │ ├── result-item.tsx │ │ │ │ ├── results-list.tsx │ │ │ │ └── tool-call-display.tsx │ │ │ ├── server.ts │ │ │ └── tools │ │ │ │ ├── company_research │ │ │ │ ├── base.ts │ │ │ │ ├── client.tsx │ │ │ │ └── server.ts │ │ │ │ ├── competitor_finder │ │ │ │ ├── base.ts │ │ │ │ ├── client.tsx │ │ │ │ └── server.ts │ │ │ │ ├── crawling │ │ │ │ ├── base.ts │ │ │ │ ├── client.tsx │ │ │ │ └── server.ts │ │ │ │ ├── github_search │ │ │ │ ├── base.ts │ │ │ │ ├── client.tsx │ │ │ │ └── server.ts │ │ │ │ ├── linkedin_search │ │ │ │ ├── base.ts │ │ │ │ ├── client.tsx │ │ │ │ └── server.ts │ │ │ │ ├── research_paper_search │ │ │ │ ├── base.ts │ │ │ │ ├── client.tsx │ │ │ │ └── server.ts │ │ │ │ ├── search │ │ │ │ ├── base.ts │ │ │ │ ├── client.tsx │ │ │ │ └── server.ts │ │ │ │ ├── tools.ts │ │ │ │ └── wikipedia_search │ │ │ │ ├── base.ts │ │ │ │ ├── client.tsx │ │ │ │ └── server.ts │ │ ├── github │ │ │ ├── base.ts │ │ │ ├── client.tsx │ │ │ ├── components │ │ │ │ ├── activity-chart.tsx │ │ │ │ └── user-avatar.tsx │ │ │ ├── lib │ │ │ │ ├── commits.ts │ │ │ │ └── prs.ts │ │ │ ├── server.ts │ │ │ └── tools │ │ │ │ ├── client.ts │ │ │ │ ├── index.ts │ │ │ │ ├── repo │ │ │ │ ├── base.ts │ │ │ │ ├── client.tsx │ │ │ │ └── server.ts │ │ │ │ ├── search │ │ │ │ ├── base.ts │ │ │ │ ├── client.tsx │ │ │ │ └── server.ts │ │ │ │ └── server.ts │ │ ├── google-calendar │ │ │ ├── base.ts │ │ │ ├── client.tsx │ │ │ ├── components │ │ │ │ ├── calendar-card.tsx │ │ │ │ ├── event-card.tsx │ │ │ │ └── tool-call.tsx │ │ │ ├── server.ts │ │ │ └── tools │ │ │ │ ├── client.ts │ │ │ │ ├── get-calendar │ │ │ │ ├── base.ts │ │ │ │ ├── client.tsx │ │ │ │ └── server.ts │ │ │ │ ├── get-event │ │ │ │ ├── base.ts │ │ │ │ ├── client.tsx │ │ │ │ └── server.ts │ │ │ │ ├── index.ts │ │ │ │ ├── list-calendars │ │ │ │ ├── base.ts │ │ │ │ ├── client.tsx │ │ │ │ └── server.ts │ │ │ │ ├── list-events │ │ │ │ ├── base.ts │ │ │ │ ├── client.tsx │ │ │ │ └── server.ts │ │ │ │ ├── search-events │ │ │ │ ├── base.ts │ │ │ │ ├── client.tsx │ │ │ │ └── server.ts │ │ │ │ └── server.ts │ │ ├── google-drive │ │ │ ├── base.ts │ │ │ ├── client.tsx │ │ │ ├── components │ │ │ │ ├── file-card.tsx │ │ │ │ └── tool-call.tsx │ │ │ ├── server.ts │ │ │ └── tools │ │ │ │ ├── client.ts │ │ │ │ ├── index.ts │ │ │ │ ├── read-file │ │ │ │ ├── base.ts │ │ │ │ ├── client.tsx │ │ │ │ └── server.ts │ │ │ │ ├── search-files │ │ │ │ ├── base.ts │ │ │ │ ├── client.tsx │ │ │ │ └── server.ts │ │ │ │ └── server.ts │ │ ├── image │ │ │ ├── base.ts │ │ │ ├── client.tsx │ │ │ ├── server.ts │ │ │ └── tools │ │ │ │ ├── generate │ │ │ │ ├── base.ts │ │ │ │ ├── client.tsx │ │ │ │ └── server.ts │ │ │ │ └── tools.ts │ │ ├── mem0 │ │ │ ├── base.ts │ │ │ ├── client.tsx │ │ │ ├── components │ │ │ │ └── tool-call-display.tsx │ │ │ ├── server.ts │ │ │ └── tools │ │ │ │ ├── add_memory │ │ │ │ ├── base.ts │ │ │ │ ├── client.tsx │ │ │ │ └── server.ts │ │ │ │ ├── search_memories │ │ │ │ ├── base.ts │ │ │ │ ├── client.tsx │ │ │ │ └── server.ts │ │ │ │ └── tools.ts │ │ ├── notion │ │ │ ├── base.ts │ │ │ ├── client.tsx │ │ │ ├── components │ │ │ │ ├── block.tsx │ │ │ │ ├── database.tsx │ │ │ │ ├── icon.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── page.tsx │ │ │ │ └── tool-call-display.tsx │ │ │ ├── lib │ │ │ │ └── title.ts │ │ │ ├── server.ts │ │ │ └── tools │ │ │ │ ├── blocks │ │ │ │ ├── base.ts │ │ │ │ ├── client.tsx │ │ │ │ └── server.ts │ │ │ │ ├── client.ts │ │ │ │ ├── databases │ │ │ │ ├── base.ts │ │ │ │ ├── client.tsx │ │ │ │ └── server.ts │ │ │ │ ├── index.ts │ │ │ │ ├── pages │ │ │ │ ├── base.ts │ │ │ │ ├── client.tsx │ │ │ │ └── server.ts │ │ │ │ ├── server.ts │ │ │ │ └── users │ │ │ │ ├── base.ts │ │ │ │ ├── client.tsx │ │ │ │ └── server.ts │ │ ├── server.ts │ │ ├── shared.ts │ │ └── toolkit-types.ts │ └── types.ts └── trpc │ ├── query-client.ts │ ├── react.tsx │ └── server.ts ├── start-database.sh └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | # This file will be committed to version control, so make sure not to have any 2 | # secrets in it. If you are cloning this repo, create a copy of this file named 3 | # ".env" and populate it with your secrets. 4 | 5 | # When adding additional environment variables, the schema in "/src/env.js" 6 | # should be updated accordingly. 7 | 8 | APP_URL="http://localhost:3000" 9 | 10 | # Next Auth 11 | # You can generate a new secret on the command line with: 12 | # npx auth secret 13 | # https://next-auth.js.org/configuration/options#secret 14 | AUTH_SECRET="" 15 | 16 | # Authentication Providers 17 | # Adding credentials for a given auth provider will automatically include it in the sign-in options 18 | # Dynamically sets the provider options for next-auth 19 | 20 | # You must include at least one of these 21 | 22 | # Next Auth Discord Provider 23 | AUTH_DISCORD_ID="" 24 | AUTH_DISCORD_SECRET="" 25 | 26 | # Google auth credentials -- https://developers.google.com/identity/protocols/oauth2 27 | AUTH_GOOGLE_ID="" 28 | AUTH_GOOGLE_SECRET="" 29 | 30 | # Github auth credentials 31 | AUTH_GITHUB_ID="" 32 | AUTH_GITHUB_SECRET="" 33 | 34 | # Twitter auth credentials 35 | AUTH_TWITTER_ID="" 36 | AUTH_TWITTER_SECRET="" 37 | 38 | # Notion auth credentials 39 | AUTH_NOTION_ID="" 40 | AUTH_NOTION_SECRET="" 41 | 42 | # Prisma 43 | # https://www.prisma.io/docs/reference/database-reference/connection-urls#env 44 | DATABASE_URL="postgresql://postgres:password@localhost:5432/open-chat" 45 | 46 | 47 | # You must provide this key to run the app 48 | 49 | OPENROUTER_API_KEY="" 50 | 51 | # Tool Providers (optional) 52 | 53 | EXA_API_KEY="" # set this to access the Web Search Toolkit 54 | E2B_API_KEY="" # set this to access the Code Interpreter Toolkit 55 | MEM0_API_KEY="" # set this to access the Memory Toolkit 56 | 57 | # Optional data storage 58 | 59 | REDIS_URL="" # You must set this to have resumable streams 60 | BLOB_READ_WRITE_TOKEN="" # You must set this to allow attachment uploads -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # database 12 | /prisma/db.sqlite 13 | /prisma/db.sqlite-journal 14 | db.sqlite 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | next-env.d.ts 20 | 21 | # production 22 | /build 23 | 24 | # misc 25 | .DS_Store 26 | *.pem 27 | 28 | # debug 29 | npm-debug.log* 30 | yarn-debug.log* 31 | yarn-error.log* 32 | .pnpm-debug.log* 33 | 34 | # local env files 35 | # do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables 36 | .env 37 | .env*.local 38 | 39 | # vercel 40 | .vercel 41 | 42 | # typescript 43 | *.tsbuildinfo 44 | 45 | # idea files 46 | .idea -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | public-hoist-pattern[]=*eslint* 2 | public-hoist-pattern[]=*prettier* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Jason Hedman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonhedman/toolkit.dev/6bb4636d55ffcdf152fa21e668c65ca4a5266a73/banner.png -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "src/styles/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import { FlatCompat } from "@eslint/eslintrc"; 2 | import tseslint from "typescript-eslint"; 3 | 4 | const compat = new FlatCompat({ 5 | baseDirectory: import.meta.dirname, 6 | }); 7 | 8 | export default tseslint.config( 9 | { 10 | ignores: [".next"], 11 | }, 12 | ...compat.extends("next/core-web-vitals"), 13 | { 14 | files: ["**/*.ts", "**/*.tsx"], 15 | extends: [ 16 | ...tseslint.configs.recommended, 17 | ...tseslint.configs.recommendedTypeChecked, 18 | ...tseslint.configs.stylisticTypeChecked, 19 | ], 20 | rules: { 21 | "@typescript-eslint/array-type": "off", 22 | "@typescript-eslint/consistent-type-definitions": "off", 23 | "@typescript-eslint/consistent-type-imports": [ 24 | "warn", 25 | { prefer: "type-imports", fixStyle: "inline-type-imports" }, 26 | ], 27 | "@typescript-eslint/no-unused-vars": [ 28 | "warn", 29 | { argsIgnorePattern: "^_" }, 30 | ], 31 | "@typescript-eslint/require-await": "off", 32 | "@typescript-eslint/no-misused-promises": [ 33 | "error", 34 | { checksVoidReturn: { attributes: false } }, 35 | ], 36 | }, 37 | }, 38 | { 39 | linterOptions: { 40 | reportUnusedDisableDirectives: true, 41 | }, 42 | languageOptions: { 43 | parserOptions: { 44 | projectService: true, 45 | }, 46 | }, 47 | }, 48 | ); 49 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful 3 | * for Docker builds. 4 | */ 5 | import "./src/env.js"; 6 | 7 | /** @type {import("next").NextConfig} */ 8 | const config = { 9 | images: { 10 | remotePatterns: [ 11 | { 12 | protocol: "https", 13 | hostname: "3kjwme0xuhfnyrkn.public.blob.vercel-storage.com", 14 | }, 15 | ], 16 | }, 17 | }; 18 | 19 | export default config; 20 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | "@tailwindcss/postcss": {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('prettier').Config & import('prettier-plugin-tailwindcss').PluginOptions} */ 2 | export default { 3 | plugins: ["prettier-plugin-tailwindcss"], 4 | }; 5 | -------------------------------------------------------------------------------- /prisma/migrations/20250608175206_chats/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "Visibility" AS ENUM ('public', 'private'); 3 | 4 | -- CreateTable 5 | CREATE TABLE "Chat" ( 6 | "id" TEXT NOT NULL, 7 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 8 | "title" TEXT NOT NULL, 9 | "userId" TEXT NOT NULL, 10 | "visibility" "Visibility" NOT NULL DEFAULT 'private', 11 | 12 | CONSTRAINT "Chat_pkey" PRIMARY KEY ("id") 13 | ); 14 | 15 | -- CreateTable 16 | CREATE TABLE "Message" ( 17 | "id" TEXT NOT NULL, 18 | "chatId" TEXT NOT NULL, 19 | "role" TEXT NOT NULL, 20 | "parts" JSONB NOT NULL, 21 | "attachments" JSONB NOT NULL, 22 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 23 | 24 | CONSTRAINT "Message_pkey" PRIMARY KEY ("id") 25 | ); 26 | 27 | -- AddForeignKey 28 | ALTER TABLE "Chat" ADD CONSTRAINT "Chat_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 29 | 30 | -- AddForeignKey 31 | ALTER TABLE "Message" ADD CONSTRAINT "Message_chatId_fkey" FOREIGN KEY ("chatId") REFERENCES "Chat"("id") ON DELETE CASCADE ON UPDATE CASCADE; 32 | -------------------------------------------------------------------------------- /prisma/migrations/20250608210029_streams/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Stream" ( 3 | "id" TEXT NOT NULL, 4 | "chatId" TEXT NOT NULL, 5 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 6 | 7 | CONSTRAINT "Stream_pkey" PRIMARY KEY ("id") 8 | ); 9 | 10 | -- CreateIndex 11 | CREATE INDEX "Stream_chatId_idx" ON "Stream"("chatId"); 12 | 13 | -- AddForeignKey 14 | ALTER TABLE "Stream" ADD CONSTRAINT "Stream_chatId_fkey" FOREIGN KEY ("chatId") REFERENCES "Chat"("id") ON DELETE CASCADE ON UPDATE CASCADE; 15 | -------------------------------------------------------------------------------- /prisma/migrations/20250612033842_files/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - The `attachments` column on the `Message` table would be dropped and recreated. This will lead to data loss if there is data in the column. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "Message" DROP COLUMN "attachments", 9 | ADD COLUMN "attachments" JSONB[]; 10 | 11 | -- CreateTable 12 | CREATE TABLE "File" ( 13 | "id" TEXT NOT NULL, 14 | "userId" TEXT NOT NULL, 15 | "name" TEXT NOT NULL, 16 | "contentType" TEXT NOT NULL, 17 | "url" TEXT NOT NULL, 18 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 19 | 20 | CONSTRAINT "File_pkey" PRIMARY KEY ("id") 21 | ); 22 | 23 | -- AddForeignKey 24 | ALTER TABLE "File" ADD CONSTRAINT "File_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 25 | -------------------------------------------------------------------------------- /prisma/migrations/20250613013025_images/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Image" ( 3 | "id" TEXT NOT NULL, 4 | "userId" TEXT NOT NULL, 5 | "contentType" TEXT NOT NULL, 6 | "url" TEXT NOT NULL, 7 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 8 | 9 | CONSTRAINT "Image_pkey" PRIMARY KEY ("id") 10 | ); 11 | 12 | -- AddForeignKey 13 | ALTER TABLE "Image" ADD CONSTRAINT "Image_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 14 | -------------------------------------------------------------------------------- /prisma/migrations/20250615144652_chat_branching/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Chat" ADD COLUMN "parentChatId" TEXT; 3 | 4 | -- AlterTable 5 | ALTER TABLE "Message" ADD COLUMN "modelId" TEXT NOT NULL DEFAULT 'openai:gpt-4o'; 6 | 7 | -- AddForeignKey 8 | ALTER TABLE "Chat" ADD CONSTRAINT "Chat_parentChatId_fkey" FOREIGN KEY ("parentChatId") REFERENCES "Chat"("id") ON DELETE CASCADE ON UPDATE CASCADE; 9 | -------------------------------------------------------------------------------- /prisma/migrations/20250615224432_features/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Feature" ( 3 | "id" TEXT NOT NULL, 4 | "name" TEXT NOT NULL, 5 | "description" TEXT, 6 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 7 | "updatedAt" TIMESTAMP(3) NOT NULL, 8 | 9 | CONSTRAINT "Feature_pkey" PRIMARY KEY ("id") 10 | ); 11 | 12 | -- CreateTable 13 | CREATE TABLE "UserFeature" ( 14 | "id" TEXT NOT NULL, 15 | "userId" TEXT NOT NULL, 16 | "featureId" TEXT NOT NULL, 17 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 18 | 19 | CONSTRAINT "UserFeature_pkey" PRIMARY KEY ("id") 20 | ); 21 | 22 | -- CreateIndex 23 | CREATE UNIQUE INDEX "Feature_name_key" ON "Feature"("name"); 24 | 25 | -- CreateIndex 26 | CREATE UNIQUE INDEX "UserFeature_userId_featureId_key" ON "UserFeature"("userId", "featureId"); 27 | 28 | -- AddForeignKey 29 | ALTER TABLE "UserFeature" ADD CONSTRAINT "UserFeature_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 30 | 31 | -- AddForeignKey 32 | ALTER TABLE "UserFeature" ADD CONSTRAINT "UserFeature_featureId_fkey" FOREIGN KEY ("featureId") REFERENCES "Feature"("id") ON DELETE CASCADE ON UPDATE CASCADE; 33 | -------------------------------------------------------------------------------- /prisma/migrations/20250616064236_workbenches/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Chat" ADD COLUMN "workbenchId" TEXT; 3 | 4 | -- CreateTable 5 | CREATE TABLE "Workbench" ( 6 | "id" TEXT NOT NULL, 7 | "name" TEXT NOT NULL, 8 | "systemPrompt" TEXT NOT NULL, 9 | "toolkitIds" TEXT[], 10 | "userId" TEXT NOT NULL, 11 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 12 | "updatedAt" TIMESTAMP(3) NOT NULL, 13 | 14 | CONSTRAINT "Workbench_pkey" PRIMARY KEY ("id") 15 | ); 16 | 17 | -- AddForeignKey 18 | ALTER TABLE "Chat" ADD CONSTRAINT "Chat_workbenchId_fkey" FOREIGN KEY ("workbenchId") REFERENCES "Workbench"("id") ON DELETE SET NULL ON UPDATE CASCADE; 19 | 20 | -- AddForeignKey 21 | ALTER TABLE "Workbench" ADD CONSTRAINT "Workbench_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 22 | -------------------------------------------------------------------------------- /prisma/migrations/20250714211437_add_starring/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Chat" ADD COLUMN "starred" BOOLEAN NOT NULL DEFAULT false; 3 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (e.g., Git) 3 | provider = "postgresql" 4 | -------------------------------------------------------------------------------- /public/icons/anthropic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonhedman/toolkit.dev/6bb4636d55ffcdf152fa21e668c65ca4a5266a73/public/icons/anthropic.png -------------------------------------------------------------------------------- /public/icons/deepseek.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonhedman/toolkit.dev/6bb4636d55ffcdf152fa21e668c65ca4a5266a73/public/icons/deepseek.png -------------------------------------------------------------------------------- /public/icons/discord.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonhedman/toolkit.dev/6bb4636d55ffcdf152fa21e668c65ca4a5266a73/public/icons/discord.png -------------------------------------------------------------------------------- /public/icons/e2b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonhedman/toolkit.dev/6bb4636d55ffcdf152fa21e668c65ca4a5266a73/public/icons/e2b.png -------------------------------------------------------------------------------- /public/icons/google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonhedman/toolkit.dev/6bb4636d55ffcdf152fa21e668c65ca4a5266a73/public/icons/google.png -------------------------------------------------------------------------------- /public/icons/lucide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonhedman/toolkit.dev/6bb4636d55ffcdf152fa21e668c65ca4a5266a73/public/icons/lucide.png -------------------------------------------------------------------------------- /public/icons/mem0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonhedman/toolkit.dev/6bb4636d55ffcdf152fa21e668c65ca4a5266a73/public/icons/mem0.png -------------------------------------------------------------------------------- /public/icons/meta-llama.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonhedman/toolkit.dev/6bb4636d55ffcdf152fa21e668c65ca4a5266a73/public/icons/meta-llama.png -------------------------------------------------------------------------------- /public/icons/motion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonhedman/toolkit.dev/6bb4636d55ffcdf152fa21e668c65ca4a5266a73/public/icons/motion.png -------------------------------------------------------------------------------- /public/icons/next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonhedman/toolkit.dev/6bb4636d55ffcdf152fa21e668c65ca4a5266a73/public/icons/next.png -------------------------------------------------------------------------------- /public/icons/nextauth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonhedman/toolkit.dev/6bb4636d55ffcdf152fa21e668c65ca4a5266a73/public/icons/nextauth.png -------------------------------------------------------------------------------- /public/icons/openai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonhedman/toolkit.dev/6bb4636d55ffcdf152fa21e668c65ca4a5266a73/public/icons/openai.png -------------------------------------------------------------------------------- /public/icons/perplexity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonhedman/toolkit.dev/6bb4636d55ffcdf152fa21e668c65ca4a5266a73/public/icons/perplexity.png -------------------------------------------------------------------------------- /public/icons/postgres.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonhedman/toolkit.dev/6bb4636d55ffcdf152fa21e668c65ca4a5266a73/public/icons/postgres.png -------------------------------------------------------------------------------- /public/icons/prisma.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonhedman/toolkit.dev/6bb4636d55ffcdf152fa21e668c65ca4a5266a73/public/icons/prisma.png -------------------------------------------------------------------------------- /public/icons/qwen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonhedman/toolkit.dev/6bb4636d55ffcdf152fa21e668c65ca4a5266a73/public/icons/qwen.png -------------------------------------------------------------------------------- /public/icons/radix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonhedman/toolkit.dev/6bb4636d55ffcdf152fa21e668c65ca4a5266a73/public/icons/radix.png -------------------------------------------------------------------------------- /public/icons/react.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonhedman/toolkit.dev/6bb4636d55ffcdf152fa21e668c65ca4a5266a73/public/icons/react.png -------------------------------------------------------------------------------- /public/icons/recharts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonhedman/toolkit.dev/6bb4636d55ffcdf152fa21e668c65ca4a5266a73/public/icons/recharts.png -------------------------------------------------------------------------------- /public/icons/redis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonhedman/toolkit.dev/6bb4636d55ffcdf152fa21e668c65ca4a5266a73/public/icons/redis.png -------------------------------------------------------------------------------- /public/icons/shadcn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonhedman/toolkit.dev/6bb4636d55ffcdf152fa21e668c65ca4a5266a73/public/icons/shadcn.png -------------------------------------------------------------------------------- /public/icons/t3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonhedman/toolkit.dev/6bb4636d55ffcdf152fa21e668c65ca4a5266a73/public/icons/t3.png -------------------------------------------------------------------------------- /public/icons/tRPC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonhedman/toolkit.dev/6bb4636d55ffcdf152fa21e668c65ca4a5266a73/public/icons/tRPC.png -------------------------------------------------------------------------------- /public/icons/tailwind.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonhedman/toolkit.dev/6bb4636d55ffcdf152fa21e668c65ca4a5266a73/public/icons/tailwind.png -------------------------------------------------------------------------------- /public/icons/tanstack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonhedman/toolkit.dev/6bb4636d55ffcdf152fa21e668c65ca4a5266a73/public/icons/tanstack.png -------------------------------------------------------------------------------- /public/icons/vercel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonhedman/toolkit.dev/6bb4636d55ffcdf152fa21e668c65ca4a5266a73/public/icons/vercel.png -------------------------------------------------------------------------------- /public/icons/x-ai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonhedman/toolkit.dev/6bb4636d55ffcdf152fa21e668c65ca4a5266a73/public/icons/x-ai.png -------------------------------------------------------------------------------- /public/icons/xai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonhedman/toolkit.dev/6bb4636d55ffcdf152fa21e668c65ca4a5266a73/public/icons/xai.png -------------------------------------------------------------------------------- /public/icons/zod.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonhedman/toolkit.dev/6bb4636d55ffcdf152fa21e668c65ca4a5266a73/public/icons/zod.png -------------------------------------------------------------------------------- /src/ai/generate.ts: -------------------------------------------------------------------------------- 1 | import { generateText as generateTextAi, streamText as streamTextAi } from "ai"; 2 | 3 | import { openrouter } from "@openrouter/ai-sdk-provider"; 4 | 5 | export const generateText = ( 6 | model: `${string}/${string}`, 7 | params: Omit<Parameters<typeof generateTextAi>[0], "model">, 8 | ) => { 9 | return generateTextAi({ 10 | model: openrouter(model), 11 | ...params, 12 | }); 13 | }; 14 | 15 | export const streamText = ( 16 | model: `${string}/${string}`, 17 | params: Omit<Parameters<typeof streamTextAi>[0], "model">, 18 | ) => { 19 | return streamTextAi({ 20 | model: openrouter(model), 21 | ...params, 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /src/ai/models/all.ts: -------------------------------------------------------------------------------- 1 | // for type inference 2 | 3 | import { openAiImageModels, openAiLanguageModels } from "./openai"; 4 | import { xaiImageModels, xaiLanguageModels } from "./xai"; 5 | import { perplexityModels } from "./perplexity"; 6 | import { googleModels } from "./google"; 7 | import { anthropicModels } from "./anthropic"; 8 | import { llamaModels } from "./llama"; 9 | import { qwenModels } from "./qwen"; 10 | import { deepseekModels } from "./deepseek"; 11 | 12 | export const allLanguageModels = [ 13 | ...anthropicModels, 14 | ...googleModels, 15 | ...openAiLanguageModels, 16 | ...xaiLanguageModels, 17 | ...perplexityModels, 18 | ...llamaModels, 19 | ...qwenModels, 20 | ...deepseekModels, 21 | ]; 22 | 23 | export const allImageModels = [...openAiImageModels, ...xaiImageModels]; 24 | -------------------------------------------------------------------------------- /src/ai/models/deepseek.ts: -------------------------------------------------------------------------------- 1 | import { type LanguageModel, LanguageModelCapability } from "@/ai/types"; 2 | 3 | const deepseekModelData: Omit<LanguageModel, "provider">[] = [ 4 | { 5 | name: "DeepSeek R1", 6 | modelId: "deepseek-r1-0528", 7 | description: 8 | "DeepSeek R1 is a large language model optimized for various natural language processing tasks", 9 | capabilities: [], 10 | bestFor: ["General purpose", "Text generation", "Conversation"], 11 | contextLength: 131072, 12 | }, 13 | { 14 | name: "DeepSeek 3", 15 | modelId: "deepseek-chat-v3-0324", 16 | description: 17 | "DeepSeek 3 is the latest generation of DeepSeek models with enhanced capabilities", 18 | capabilities: [LanguageModelCapability.ToolCalling], 19 | bestFor: ["Advanced reasoning", "Code generation", "Creative writing"], 20 | contextLength: 131072, 21 | }, 22 | ]; 23 | 24 | export const deepseekModels: LanguageModel[] = deepseekModelData.map( 25 | (model) => ({ 26 | ...model, 27 | provider: "deepseek", 28 | }), 29 | ); 30 | -------------------------------------------------------------------------------- /src/ai/models/index.ts: -------------------------------------------------------------------------------- 1 | import { env } from "@/env"; 2 | 3 | import { type LanguageModel } from "@/ai/types"; 4 | 5 | import { anthropicModels } from "./anthropic"; 6 | import { googleModels } from "./google"; 7 | import { openAiLanguageModels, openAiImageModels } from "./openai"; 8 | import { xaiImageModels, xaiLanguageModels } from "./xai"; 9 | import { perplexityModels } from "./perplexity"; 10 | import { llamaModels } from "./llama"; 11 | import { qwenModels } from "./qwen"; 12 | import { deepseekModels } from "./deepseek"; 13 | 14 | export const languageModels: LanguageModel[] = [ 15 | ...anthropicModels, 16 | ...googleModels, 17 | ...openAiLanguageModels, 18 | ...xaiLanguageModels, 19 | ...perplexityModels, 20 | ...llamaModels, 21 | ...qwenModels, 22 | ...deepseekModels, 23 | ]; 24 | 25 | export const imageModels = [ 26 | ...("OPENAI_API_KEY" in env ? openAiImageModels : []), 27 | ...("XAI_API_KEY" in env ? xaiImageModels : []), 28 | ]; 29 | -------------------------------------------------------------------------------- /src/ai/models/perplexity.ts: -------------------------------------------------------------------------------- 1 | import { LanguageModelCapability, type LanguageModel } from "@/ai/types"; 2 | 3 | const perplexityModelData: Omit<LanguageModel, "provider">[] = [ 4 | { 5 | name: "Sonar Pro", 6 | modelId: "sonar-pro", 7 | description: "Next-generation Sonar with enhanced reasoning", 8 | capabilities: [LanguageModelCapability.WebSearch], 9 | bestFor: ["Complex reasoning", "Advanced analysis", "Research"], 10 | contextLength: 2000000, 11 | }, 12 | { 13 | name: "Sonar", 14 | modelId: "sonar", 15 | description: "Fast version of Sonar for quick responses", 16 | capabilities: [LanguageModelCapability.WebSearch], 17 | bestFor: ["Quick tasks", "Real-time responses", "Efficient processing"], 18 | contextLength: 1000000, 19 | }, 20 | ]; 21 | 22 | export const perplexityModels: LanguageModel[] = perplexityModelData.map( 23 | (model) => ({ 24 | ...model, 25 | provider: "perplexity", 26 | }), 27 | ); 28 | -------------------------------------------------------------------------------- /src/ai/models/qwen.ts: -------------------------------------------------------------------------------- 1 | import { type LanguageModel, LanguageModelCapability } from "@/ai/types"; 2 | 3 | const qwenModelData: Omit<LanguageModel, "provider">[] = [ 4 | { 5 | name: "Qwen QwQ 32B", 6 | modelId: "qwq-32b", 7 | description: 8 | "Qwen QWQ 32B is a large language model optimized for various natural language processing tasks", 9 | capabilities: [LanguageModelCapability.ToolCalling], 10 | bestFor: ["General purpose", "Text generation", "Conversation"], 11 | contextLength: 131072, 12 | }, 13 | { 14 | name: "Qwen 3 32B", 15 | modelId: "qwen3-32b", 16 | description: 17 | "Qwen 3 32B is the latest generation of Qwen models with enhanced capabilities", 18 | capabilities: [LanguageModelCapability.ToolCalling], 19 | bestFor: ["Advanced reasoning", "Code generation", "Creative writing"], 20 | contextLength: 131072, 21 | }, 22 | ]; 23 | 24 | export const qwenModels: LanguageModel[] = qwenModelData.map((model) => ({ 25 | ...model, 26 | provider: "qwen", 27 | })); 28 | -------------------------------------------------------------------------------- /src/ai/registry.ts: -------------------------------------------------------------------------------- 1 | import { createProviderRegistry } from "ai"; 2 | 3 | import { openai } from "@ai-sdk/openai"; 4 | import { xai } from "@ai-sdk/xai"; 5 | 6 | export const registry = createProviderRegistry({ 7 | openai, 8 | xai, 9 | }); 10 | -------------------------------------------------------------------------------- /src/ai/types.ts: -------------------------------------------------------------------------------- 1 | import type { ProviderMetadata } from "ai"; 2 | 3 | export enum LanguageModelCapability { 4 | Vision = "vision", 5 | WebSearch = "web-search", 6 | Reasoning = "reasoning", 7 | Pdf = "pdf", 8 | ToolCalling = "tool-calling", 9 | } 10 | 11 | export type LanguageModel = { 12 | name: string; 13 | provider: string; 14 | modelId: string; 15 | description?: string; 16 | capabilities?: LanguageModelCapability[]; 17 | bestFor?: string[]; 18 | contextLength?: number; 19 | isNew?: boolean; 20 | providerOptions?: ProviderMetadata; 21 | }; 22 | 23 | export enum SearchOptions { 24 | Native = "Native", 25 | OpenAiResponses = "OpenAI Responses", 26 | Exa = "Exa Search", 27 | } 28 | 29 | export type ImageModelProvider = "openai" | "xai"; 30 | 31 | export type ImageModel = { 32 | name: string; 33 | provider: ImageModelProvider; 34 | modelId: string; 35 | description?: string; 36 | }; 37 | -------------------------------------------------------------------------------- /src/app/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { notFound, redirect } from "next/navigation"; 2 | 3 | import { Chat } from "@/app/_components/chat"; 4 | import { api } from "@/trpc/server"; 5 | 6 | import { auth } from "@/server/auth"; 7 | 8 | export default async function Page(props: { params: Promise<{ id: string }> }) { 9 | const params = await props.params; 10 | const { id } = params; 11 | 12 | const session = await auth(); 13 | 14 | if (!session) { 15 | redirect(`/login?redirect=/${id}`); 16 | } 17 | 18 | const chat = await api.chats.getChat(id); 19 | 20 | if (!chat) { 21 | notFound(); 22 | } 23 | 24 | return ( 25 | <> 26 | <Chat 27 | id={chat.id} 28 | initialVisibilityType={chat.visibility} 29 | isReadonly={session?.user?.id !== chat.userId} 30 | isNew={false} 31 | /> 32 | </> 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/app/_components/auth/auth-buttons.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | import { AuthProviderIcon } from "../navbar/account-button/provider-icon"; 5 | import { signIn } from "next-auth/react"; 6 | 7 | interface AuthButtonsProps { 8 | providers: { 9 | name: string; 10 | id: string; 11 | }[]; 12 | redirect?: string; 13 | } 14 | 15 | export const AuthButtons = ({ providers, redirect }: AuthButtonsProps) => { 16 | return ( 17 | <div className="flex flex-col gap-2"> 18 | {providers.map((provider) => ( 19 | <Button 20 | key={provider.id} 21 | variant="outline" 22 | className="w-full" 23 | onClick={() => signIn(provider.id, { redirectTo: redirect })} 24 | > 25 | <AuthProviderIcon provider={provider.name} /> 26 | Sign in with {provider.name} 27 | </Button> 28 | ))} 29 | </div> 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /src/app/_components/chat/input/model-select/native-search-toggle.tsx: -------------------------------------------------------------------------------- 1 | import { LanguageModelCapability } from "@/ai/types"; 2 | import { useChatContext } from "@/app/_contexts/chat-context"; 3 | import { 4 | Tooltip, 5 | TooltipContent, 6 | TooltipTrigger, 7 | } from "@/components/ui/tooltip"; 8 | import { cn } from "@/lib/utils"; 9 | import { Globe } from "lucide-react"; 10 | 11 | export const NativeSearchToggle = () => { 12 | const { useNativeSearch, setUseNativeSearch, selectedChatModel } = 13 | useChatContext(); 14 | 15 | if ( 16 | !selectedChatModel?.capabilities?.includes( 17 | LanguageModelCapability.WebSearch, 18 | ) 19 | ) { 20 | return null; 21 | } 22 | 23 | return ( 24 | <Tooltip> 25 | <TooltipTrigger asChild> 26 | <div 27 | onClick={(event) => { 28 | event.preventDefault(); 29 | event.stopPropagation(); 30 | setUseNativeSearch(!useNativeSearch); 31 | }} 32 | onMouseDown={(event) => { 33 | event.preventDefault(); 34 | event.stopPropagation(); 35 | }} 36 | className={cn( 37 | "size-fit cursor-pointer rounded-full bg-transparent p-1", 38 | useNativeSearch && "bg-primary/10 text-primary", 39 | )} 40 | data-native-search-toggle="true" 41 | > 42 | <Globe className="size-4" /> 43 | </div> 44 | </TooltipTrigger> 45 | <TooltipContent> 46 | {useNativeSearch ? ( 47 | <p>Native search enabled</p> 48 | ) : ( 49 | <p>Native search disabled</p> 50 | )} 51 | </TooltipContent> 52 | </Tooltip> 53 | ); 54 | }; 55 | -------------------------------------------------------------------------------- /src/app/_components/chat/input/model-select/utils.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Eye, Search, Sparkles, File, Wrench } from "lucide-react"; 4 | 5 | import { LanguageModelCapability } from "@/ai/types"; 6 | 7 | export const capabilityIcons: Record< 8 | LanguageModelCapability, 9 | React.ComponentType<{ className?: string }> 10 | > = { 11 | [LanguageModelCapability.Vision]: Eye, 12 | [LanguageModelCapability.WebSearch]: Search, 13 | [LanguageModelCapability.Reasoning]: Sparkles, 14 | [LanguageModelCapability.Pdf]: File, 15 | [LanguageModelCapability.ToolCalling]: Wrench, 16 | }; 17 | 18 | export const capabilityLabels: Record<LanguageModelCapability, string> = { 19 | [LanguageModelCapability.Vision]: "Vision", 20 | [LanguageModelCapability.WebSearch]: "Web Search", 21 | [LanguageModelCapability.Reasoning]: "Reasoning", 22 | [LanguageModelCapability.Pdf]: "PDF", 23 | [LanguageModelCapability.ToolCalling]: "Tool Calling", 24 | }; 25 | 26 | export const capabilityColors: Record<LanguageModelCapability, string> = { 27 | [LanguageModelCapability.Vision]: "bg-green-100 text-green-800", 28 | [LanguageModelCapability.WebSearch]: "bg-yellow-100 text-yellow-800", 29 | [LanguageModelCapability.Reasoning]: "bg-orange-100 text-orange-800", 30 | [LanguageModelCapability.Pdf]: "bg-gray-200 text-gray-800", 31 | [LanguageModelCapability.ToolCalling]: "bg-blue-100 text-blue-800", 32 | }; 33 | 34 | export const formatContextLength = (length?: number) => { 35 | if (!length) return null; 36 | if (length >= 1000000) return `${(length / 1000000).toFixed(1)}M tokens`; 37 | if (length >= 1000) return `${(length / 1000).toFixed(0)}K tokens`; 38 | return `${length} tokens`; 39 | }; 40 | 41 | export const modelProviderNames: Record<string, string> = { 42 | openai: "OpenAI", 43 | google: "Google", 44 | anthropic: "Anthropic", 45 | perplexity: "Perplexity", 46 | "x-ai": "xAI", 47 | "meta-llama": "Llama", 48 | qwen: "Qwen", 49 | deepseek: "DeepSeek", 50 | }; 51 | -------------------------------------------------------------------------------- /src/app/_components/chat/layout.tsx: -------------------------------------------------------------------------------- 1 | interface Props { 2 | children: React.ReactNode; 3 | } 4 | 5 | export const ChatLayout: React.FC<Props> = ({ children }) => { 6 | return ( 7 | <div className="flex h-0 flex-1 flex-col overflow-hidden">{children}</div> 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/app/_components/chat/messages/greeting.tsx: -------------------------------------------------------------------------------- 1 | import { motion } from "motion/react"; 2 | 3 | export const Greeting = () => { 4 | return ( 5 | <div 6 | key="overview" 7 | className="mx-auto flex size-full max-w-3xl flex-col justify-center px-8 md:mt-20" 8 | > 9 | <motion.div 10 | initial={{ opacity: 0, y: 10 }} 11 | animate={{ opacity: 1, y: 0 }} 12 | exit={{ opacity: 0, y: 10 }} 13 | transition={{ delay: 0.5 }} 14 | className="text-2xl font-semibold" 15 | > 16 | Hello there! 17 | </motion.div> 18 | <motion.div 19 | initial={{ opacity: 0, y: 10 }} 20 | animate={{ opacity: 1, y: 0 }} 21 | exit={{ opacity: 0, y: 10 }} 22 | transition={{ delay: 0.6 }} 23 | className="text-2xl text-zinc-500" 24 | > 25 | How can I help you today? 26 | </motion.div> 27 | </div> 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /src/app/_components/landing-page/auth-modal.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Dialog, 3 | DialogContent, 4 | DialogHeader, 5 | DialogTitle, 6 | DialogDescription, 7 | DialogTrigger, 8 | } from "@/components/ui/dialog"; 9 | import { AuthButtons } from "../auth/auth-buttons"; 10 | import { providers } from "@/server/auth/providers"; 11 | import { Logo } from "@/components/ui/logo"; 12 | import { VStack } from "@/components/ui/stack"; 13 | 14 | interface AuthModalProps { 15 | children: React.ReactNode; 16 | } 17 | 18 | export const AuthModal = ({ children }: AuthModalProps) => { 19 | return ( 20 | <Dialog> 21 | <DialogTrigger asChild>{children}</DialogTrigger> 22 | <DialogContent showCloseButton={false} className="gap-6"> 23 | <DialogHeader className="items-center gap-2"> 24 | <Logo className="size-16" /> 25 | <VStack> 26 | <DialogTitle className="text-primary text-xl"> 27 | Sign in to Toolkit 28 | </DialogTitle> 29 | <DialogDescription className="hidden"> 30 | Sign in to your account to get started with Toolkit. 31 | </DialogDescription> 32 | </VStack> 33 | </DialogHeader> 34 | <AuthButtons 35 | providers={providers.map((provider) => ({ 36 | name: provider.name, 37 | id: provider.id, 38 | }))} 39 | /> 40 | </DialogContent> 41 | </Dialog> 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /src/app/_components/landing-page/dependencies/types.ts: -------------------------------------------------------------------------------- 1 | export interface Dependency { 2 | name: string; 3 | icon: React.ReactNode | null; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/_components/landing-page/hero/message/memory.tsx: -------------------------------------------------------------------------------- 1 | import { mem0AddMemoryToolConfigClient } from "@/toolkits/toolkits/mem0/tools/add_memory/client"; 2 | 3 | export const MemoryCalling: React.FC = () => { 4 | return ( 5 | <mem0AddMemoryToolConfigClient.CallComponent 6 | args={{ 7 | content: 8 | "User is building an AI chatbot and wants to learn how to use the vercel/ai toolkit.", 9 | }} 10 | isPartial={false} 11 | /> 12 | ); 13 | }; 14 | 15 | export const MemoryResult: React.FC = () => { 16 | return ( 17 | <mem0AddMemoryToolConfigClient.ResultComponent 18 | result={{ 19 | content: 20 | "User is building an AI chatbot and wants to learn how to use the vercel/ai toolkit.", 21 | success: true, 22 | }} 23 | args={{ 24 | content: 25 | "User is building an AI chatbot and wants to learn how to use the vercel/ai toolkit.", 26 | }} 27 | append={() => void 0} 28 | /> 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /src/app/_components/landing-page/hero/message/top-repositories.tsx: -------------------------------------------------------------------------------- 1 | import { githubSearchReposToolConfigClient } from "@/toolkits/toolkits/github/tools/client"; 2 | 3 | export const TopRepositoriesCalling: React.FC = () => { 4 | return ( 5 | <githubSearchReposToolConfigClient.CallComponent 6 | args={{ 7 | query: "org:vercel ai in:name,description,topics", 8 | per_page: 5, 9 | page: 1, 10 | }} 11 | isPartial={false} 12 | /> 13 | ); 14 | }; 15 | 16 | export const TopRepositoriesResult: React.FC = () => { 17 | return ( 18 | <githubSearchReposToolConfigClient.ResultComponent 19 | result={{ 20 | repositories: [ 21 | { 22 | stars: 14997, 23 | language: "TypeScript", 24 | full_name: "vercel/ai", 25 | description: 26 | "The AI Toolkit for TypeScript. From the creators of Next.js, the AI SDK is a free open-source library for building AI-powered applications and agents ", 27 | }, 28 | { 29 | stars: 16639, 30 | language: "TypeScript", 31 | full_name: "vercel/ai-chatbot", 32 | description: 33 | "A full-featured, hackable Next.js AI chatbot built by Vercel", 34 | }, 35 | { 36 | stars: 1280, 37 | language: "TypeScript", 38 | full_name: "vercel/modelfusion", 39 | description: "The TypeScript library for building AI applications.", 40 | }, 41 | ], 42 | }} 43 | args={{ query: "org:vercel", per_page: 5, page: 1 }} 44 | append={() => void 0} 45 | /> 46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /src/app/_components/landing-page/hero/motion-container.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { motion } from "motion/react"; 4 | 5 | export const MotionContainer = ({ 6 | children, 7 | initial = { opacity: 0, y: 30 }, 8 | animate = { opacity: 1, y: 0 }, 9 | transition = { duration: 0.6 }, 10 | className, 11 | }: { 12 | children: React.ReactNode; 13 | initial?: { 14 | opacity: number; 15 | y: number; 16 | }; 17 | animate?: { 18 | opacity: number; 19 | y: number; 20 | }; 21 | transition?: { 22 | duration: number; 23 | }; 24 | className?: string; 25 | }) => { 26 | return ( 27 | <motion.div 28 | initial={initial} 29 | animate={animate} 30 | transition={transition} 31 | className={className} 32 | > 33 | {children} 34 | </motion.div> 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /src/app/_components/landing-page/hero/types.ts: -------------------------------------------------------------------------------- 1 | import type { Toolkits } from "@/toolkits/toolkits/shared"; 2 | 3 | export type Message = { 4 | id: string; 5 | } & ( 6 | | { 7 | type: "user"; 8 | content: string; 9 | } 10 | | { 11 | type: "assistant"; 12 | content: string; 13 | } 14 | | { 15 | type: "tool"; 16 | callComponent: React.ReactNode; 17 | resultComponent: React.ReactNode; 18 | toolkit: Toolkits; 19 | } 20 | ); 21 | -------------------------------------------------------------------------------- /src/app/_components/landing-page/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { HeroSection } from "./hero"; 3 | import { ToolkitCreationSection } from "./toolkit-creation"; 4 | import { WorkbenchSection } from "./workbenches"; 5 | import { DependenciesSection } from "./dependencies"; 6 | import { Navbar } from "./navbar"; 7 | 8 | export const LandingPage: React.FC = () => { 9 | return ( 10 | <div className="h-fit min-h-screen"> 11 | <Navbar /> 12 | <HeroSection /> 13 | <DependenciesSection /> 14 | <ToolkitCreationSection /> 15 | <WorkbenchSection /> 16 | </div> 17 | ); 18 | }; 19 | 20 | export default LandingPage; 21 | -------------------------------------------------------------------------------- /src/app/_components/landing-page/navbar/index.tsx: -------------------------------------------------------------------------------- 1 | import { Logo } from "@/components/ui/logo"; 2 | import { HStack } from "@/components/ui/stack"; 3 | import { ColorModeToggle } from "../../navbar/color-mode-toggle"; 4 | import { Button } from "@/components/ui/button"; 5 | import { AuthModal } from "../auth-modal"; 6 | 7 | export const Navbar = () => { 8 | return ( 9 | <HStack className="bg-background fixed top-0 z-50 w-full border-b py-2"> 10 | <HStack className="container mx-auto justify-between px-2"> 11 | <HStack> 12 | <Logo className="size-6" /> 13 | <h1 className="shimmer-text overflow-hidden text-lg font-bold whitespace-nowrap"> 14 | Toolkit.dev 15 | </h1> 16 | </HStack> 17 | <HStack> 18 | <AuthModal> 19 | <Button className="user-message">Try it Out</Button> 20 | </AuthModal> 21 | <ColorModeToggle /> 22 | </HStack> 23 | </HStack> 24 | </HStack> 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /src/app/_components/landing-page/workbenches/data.tsx: -------------------------------------------------------------------------------- 1 | import { Toolkits } from "@/toolkits/toolkits/shared"; 2 | import type { WorkbenchExample } from "./types"; 3 | import { BarChart3, Code, Briefcase } from "lucide-react"; 4 | 5 | export const workbenchExamples: WorkbenchExample[] = [ 6 | { 7 | title: "Research Assistant", 8 | description: "Comprehensive research and documentation workbench", 9 | systemPrompt: 10 | "You are a research assistant that helps users gather, analyze, and document information from multiple sources.", 11 | toolkits: [ 12 | Toolkits.Exa, 13 | Toolkits.Github, 14 | Toolkits.Image, 15 | Toolkits.Notion, 16 | Toolkits.Memory, 17 | ], 18 | icon: <BarChart3 className="h-5 w-5" />, 19 | color: "blue", 20 | }, 21 | { 22 | title: "Data Analyst", 23 | description: "Data processing and visualization workbench", 24 | systemPrompt: 25 | "You are a data analyst specializing in extracting insights from various data sources and creating compelling visualizations.", 26 | toolkits: [ 27 | Toolkits.Notion, 28 | Toolkits.GoogleDrive, 29 | Toolkits.E2B, 30 | Toolkits.Image, 31 | ], 32 | icon: <Code className="h-5 w-5" />, 33 | color: "green", 34 | }, 35 | { 36 | title: "Project Manager", 37 | description: "Team coordination and project management workbench", 38 | systemPrompt: 39 | "You are a project manager focused on coordinating teams, scheduling meetings, and tracking project progress.", 40 | toolkits: [ 41 | Toolkits.GoogleCalendar, 42 | Toolkits.Notion, 43 | Toolkits.GoogleDrive, 44 | Toolkits.Memory, 45 | ], 46 | icon: <Briefcase className="h-5 w-5" />, 47 | color: "purple", 48 | }, 49 | ]; 50 | -------------------------------------------------------------------------------- /src/app/_components/landing-page/workbenches/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { motion } from "motion/react"; 5 | import { WorkbenchCard } from "./card"; 6 | import { workbenchExamples } from "./data"; 7 | 8 | export const WorkbenchSection: React.FC = () => { 9 | return ( 10 | <section className="from-muted/20 to-background bg-gradient-to-b py-24"> 11 | <div className="container mx-auto px-2 md:px-4"> 12 | <motion.div 13 | initial={{ opacity: 0, y: 20 }} 14 | whileInView={{ opacity: 1, y: 0 }} 15 | transition={{ duration: 0.6 }} 16 | viewport={{ once: true }} 17 | className="mb-16 text-center" 18 | > 19 | <h2 className="mb-4 text-3xl font-bold md:text-4xl"> 20 | Configure Custom 21 | <span className="text-primary block">Workbenches</span> 22 | </h2> 23 | <p className="text-muted-foreground mx-auto mb-8 max-w-2xl text-lg"> 24 | Workbenches combine multiple toolkits with specialized system 25 | prompts to create focused AI assistants for specific use cases. Each 26 | workbench orchestrates toolkit interactions seamlessly. 27 | </p> 28 | </motion.div> 29 | 30 | <div className="grid grid-cols-1 gap-4 lg:grid-cols-3"> 31 | {workbenchExamples.map((workbench, index) => ( 32 | <WorkbenchCard 33 | key={workbench.title} 34 | workbench={workbench} 35 | delay={index * 0.1} 36 | /> 37 | ))} 38 | </div> 39 | </div> 40 | </section> 41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /src/app/_components/landing-page/workbenches/types.ts: -------------------------------------------------------------------------------- 1 | import type { Toolkits } from "@/toolkits/toolkits/shared"; 2 | 3 | export interface WorkbenchExample { 4 | title: string; 5 | description: string; 6 | systemPrompt: string; 7 | toolkits: Toolkits[]; 8 | icon: React.ReactNode; 9 | color: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/_components/navbar/account-button/authenticated.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { LogOut, User } from "lucide-react"; 4 | import Link from "next/link"; 5 | 6 | import { signOut } from "next-auth/react"; 7 | 8 | import { Button } from "@/components/ui/button"; 9 | import { 10 | DropdownMenu, 11 | DropdownMenuContent, 12 | DropdownMenuItem, 13 | DropdownMenuSeparator, 14 | DropdownMenuTrigger, 15 | } from "@/components/ui/dropdown-menu"; 16 | 17 | import type { Session } from "next-auth"; 18 | 19 | interface Props { 20 | session: Session; 21 | } 22 | 23 | export const Authenticated: React.FC<Props> = ({ session }) => { 24 | return ( 25 | <DropdownMenu> 26 | <DropdownMenuTrigger asChild> 27 | <Button variant="outline"> 28 | {session.user.image ? ( 29 | // eslint-disable-next-line @next/next/no-img-element 30 | <img 31 | src={session.user.image} 32 | alt={session.user.name ?? ""} 33 | className="size-4 rounded-full" 34 | /> 35 | ) : ( 36 | <User className="size-4" /> 37 | )} 38 | {session.user.name ?? "Signed In"} 39 | </Button> 40 | </DropdownMenuTrigger> 41 | <DropdownMenuContent align="end"> 42 | <DropdownMenuItem asChild> 43 | <Link href="/account"> 44 | <User /> 45 | Account 46 | </Link> 47 | </DropdownMenuItem> 48 | <DropdownMenuSeparator /> 49 | <DropdownMenuItem onClick={() => signOut()}> 50 | <LogOut /> 51 | Sign Out 52 | </DropdownMenuItem> 53 | </DropdownMenuContent> 54 | </DropdownMenu> 55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /src/app/_components/navbar/account-button/index.tsx: -------------------------------------------------------------------------------- 1 | import { Unauthenticated } from "./unauthenticated"; 2 | import { Authenticated } from "./authenticated"; 3 | 4 | import { auth } from "@/server/auth"; 5 | import { providers } from "@/server/auth/providers"; 6 | 7 | export const AccountButton = async () => { 8 | const session = await auth(); 9 | 10 | if (!session) { 11 | return ( 12 | <Unauthenticated 13 | providers={providers.map((provider) => ({ 14 | name: provider.name, 15 | id: provider.id, 16 | }))} 17 | /> 18 | ); 19 | } 20 | 21 | return <Authenticated session={session} />; 22 | }; 23 | -------------------------------------------------------------------------------- /src/app/_components/navbar/account-button/provider-icon.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | import { SiGithub, SiNotion, SiX } from "@icons-pack/react-simple-icons"; 5 | 6 | interface Props { 7 | provider: string; 8 | className?: string; 9 | } 10 | 11 | export const AuthProviderIcon: React.FC<Props> = ({ provider, className }) => { 12 | const Icon = 13 | { 14 | Discord: ({ className }: { className?: string }) => ( 15 | <Image 16 | src="/icons/discord.png" 17 | alt="Discord" 18 | className={className} 19 | width={16} 20 | height={16} 21 | /> 22 | ), 23 | Google: ({ className }: { className?: string }) => ( 24 | <Image 25 | src="/icons/google.png" 26 | alt="Google" 27 | className={className} 28 | width={16} 29 | height={16} 30 | /> 31 | ), 32 | GitHub: SiGithub, 33 | Twitter: SiX, 34 | Notion: SiNotion, 35 | }[provider] ?? null; 36 | 37 | return Icon ? <Icon className={cn("size-4", className)} /> : null; 38 | }; 39 | -------------------------------------------------------------------------------- /src/app/_components/navbar/account-button/unauthenticated.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { User } from "lucide-react"; 4 | 5 | import { signIn } from "next-auth/react"; 6 | 7 | import { Button } from "@/components/ui/button"; 8 | import { 9 | DropdownMenu, 10 | DropdownMenuContent, 11 | DropdownMenuItem, 12 | DropdownMenuTrigger, 13 | } from "@/components/ui/dropdown-menu"; 14 | 15 | import { AuthProviderIcon } from "./provider-icon"; 16 | 17 | interface Props { 18 | providers: { 19 | name: string; 20 | id: string; 21 | }[]; 22 | } 23 | 24 | export const Unauthenticated: React.FC<Props> = ({ providers }) => { 25 | return ( 26 | <DropdownMenu> 27 | <DropdownMenuTrigger asChild> 28 | <Button variant="outline"> 29 | <User /> 30 | Sign In 31 | </Button> 32 | </DropdownMenuTrigger> 33 | <DropdownMenuContent> 34 | {providers.map((provider) => ( 35 | <DropdownMenuItem 36 | key={provider.id} 37 | onClick={() => signIn(provider.id, { callbackUrl: "/" })} 38 | > 39 | <AuthProviderIcon provider={provider.name} /> 40 | Sign In with {provider.name} 41 | </DropdownMenuItem> 42 | ))} 43 | </DropdownMenuContent> 44 | </DropdownMenu> 45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /src/app/_components/navbar/color-mode-toggle.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useTheme } from "@/app/_contexts/theme"; 4 | import { Button } from "@/components/ui/button"; 5 | import { Moon, Sun } from "lucide-react"; 6 | 7 | export const ColorModeToggle = () => { 8 | const { theme, toggleTheme } = useTheme(); 9 | 10 | return ( 11 | <Button 12 | onClick={toggleTheme} 13 | variant="outline" 14 | aria-label={`Switch to ${theme === "light" ? "dark" : "light"} mode`} 15 | size="icon" 16 | > 17 | {theme === "light" ? ( 18 | <Moon className="h-5 w-5" /> 19 | ) : ( 20 | <Sun className="h-5 w-5" /> 21 | )} 22 | </Button> 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/app/_components/navbar/index.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | import { AccountButton } from "./account-button"; 4 | import { ColorModeToggle } from "./color-mode-toggle"; 5 | import { HStack } from "@/components/ui/stack"; 6 | import { auth } from "@/server/auth"; 7 | import { SidebarTrigger } from "@/components/ui/sidebar"; 8 | import { Menu } from "lucide-react"; 9 | 10 | export const Navbar = async () => { 11 | const session = await auth(); 12 | 13 | if (!session) return null; 14 | 15 | return ( 16 | <HStack className="bg-background sticky top-0 z-10 justify-between p-2 md:hidden"> 17 | <HStack> 18 | <SidebarTrigger className="hover:bg-accent/50 rounded-lg p-2"> 19 | <Menu className="size-4" /> 20 | </SidebarTrigger> 21 | <Link href="/"> 22 | <h1 className="overflow-hidden text-lg font-bold whitespace-nowrap"> 23 | Toolkit.dev 24 | </h1> 25 | </Link> 26 | </HStack> 27 | <HStack> 28 | <AccountButton /> 29 | <ColorModeToggle /> 30 | </HStack> 31 | </HStack> 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /src/app/_components/sidebar/main.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Link from "next/link"; 4 | 5 | import { Edit } from "lucide-react"; 6 | 7 | import { 8 | SidebarGroup, 9 | SidebarMenu, 10 | SidebarMenuButton, 11 | SidebarMenuItem, 12 | } from "@/components/ui/sidebar"; 13 | import { usePathname } from "next/navigation"; 14 | 15 | export const NavMain = () => { 16 | const pathname = usePathname(); 17 | 18 | const workbenchId = 19 | pathname.split("/")[2] === "new" ? undefined : pathname.split("/")[2]; 20 | 21 | const items = [ 22 | { 23 | title: "New Chat", 24 | url: workbenchId ? `/workbench/${workbenchId}` : "/", 25 | icon: Edit, 26 | }, 27 | ]; 28 | 29 | return ( 30 | <SidebarGroup> 31 | <SidebarMenu> 32 | {items.map((item) => ( 33 | <SidebarMenuItem key={item.title}> 34 | <SidebarMenuButton key={item.title} tooltip={item.title} asChild> 35 | <Link href={item.url}> 36 | {item.icon && <item.icon />} 37 | <span>{item.title}</span> 38 | </Link> 39 | </SidebarMenuButton> 40 | </SidebarMenuItem> 41 | ))} 42 | </SidebarMenu> 43 | </SidebarGroup> 44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /src/app/_hooks/use-auto-resume.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect } from "react"; 4 | 5 | import type { UIMessage } from "ai"; 6 | import type { UseChatHelpers } from "@ai-sdk/react"; 7 | 8 | type DataPart = { type: "append-message"; message: string }; 9 | 10 | export interface UseAutoResumeParams { 11 | autoResume: boolean; 12 | initialMessages: UIMessage[]; 13 | experimental_resume: UseChatHelpers["experimental_resume"]; 14 | data: UseChatHelpers["data"]; 15 | setMessages: UseChatHelpers["setMessages"]; 16 | } 17 | 18 | export function useAutoResume({ 19 | autoResume, 20 | initialMessages, 21 | experimental_resume, 22 | data, 23 | setMessages, 24 | }: UseAutoResumeParams) { 25 | useEffect(() => { 26 | if (!autoResume) return; 27 | 28 | const mostRecentMessage = initialMessages.at(-1); 29 | 30 | if (mostRecentMessage?.role === "user") { 31 | experimental_resume(); 32 | } 33 | 34 | // we intentionally run this once 35 | // eslint-disable-next-line react-hooks/exhaustive-deps 36 | }, []); 37 | 38 | useEffect(() => { 39 | if (!data) return; 40 | if (data.length === 0) return; 41 | 42 | const dataPart = data[0] as DataPart; 43 | 44 | if (dataPart.type === "append-message") { 45 | const message = JSON.parse(dataPart.message) as UIMessage; 46 | setMessages([...initialMessages, message]); 47 | } 48 | }, [data, initialMessages, setMessages]); 49 | } 50 | -------------------------------------------------------------------------------- /src/app/_hooks/use-chat-visibility.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { api } from "@/trpc/react"; 4 | 5 | export const useUpdateChatVisibility = () => { 6 | const utils = api.useUtils(); 7 | 8 | return api.chats.updateChatVisibility.useMutation({ 9 | onSuccess: async () => { 10 | await utils.chats.getChats.invalidate(); 11 | }, 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /src/app/_hooks/use-delete-chat.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { api } from "@/trpc/react"; 4 | 5 | export const useDeleteChat = () => { 6 | const utils = api.useUtils(); 7 | 8 | return api.chats.deleteChat.useMutation({ 9 | onSuccess: async () => { 10 | await utils.chats.getChats.invalidate(); 11 | }, 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /src/app/_hooks/use-delete-messages.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { api } from "@/trpc/react"; 4 | 5 | export const useDeleteMessagesAfterTimestamp = () => { 6 | const utils = api.useUtils(); 7 | 8 | return api.messages.deleteMessagesAfterTimestamp.useMutation({ 9 | onSuccess: async (_, { chatId }) => { 10 | await utils.messages.getMessagesForChat.invalidate({ chatId }); 11 | }, 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /src/app/_hooks/use-messages.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | import type { UseChatHelpers } from "@ai-sdk/react"; 4 | 5 | export function useMessages({ 6 | chatId, 7 | status, 8 | scrollToBottom, 9 | }: { 10 | chatId: string; 11 | status: UseChatHelpers["status"]; 12 | scrollToBottom: (behavior: ScrollBehavior) => void; 13 | }) { 14 | const [hasSentMessage, setHasSentMessage] = useState(false); 15 | 16 | useEffect(() => { 17 | if (chatId) { 18 | scrollToBottom("instant"); 19 | setHasSentMessage(false); 20 | } 21 | }, [chatId, scrollToBottom]); 22 | 23 | useEffect(() => { 24 | if (status === "submitted") { 25 | setHasSentMessage(true); 26 | } 27 | }, [status]); 28 | 29 | return { 30 | hasSentMessage, 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /src/app/_hooks/use-scroll-to-bottom.tsx: -------------------------------------------------------------------------------- 1 | import { useRef, useEffect, useCallback } from "react"; 2 | import { useQuery, useQueryClient } from "@tanstack/react-query"; 3 | 4 | type ScrollFlag = ScrollBehavior | false; 5 | 6 | export function useScrollToBottom() { 7 | const containerRef = useRef<HTMLDivElement>(null); 8 | const endRef = useRef<HTMLDivElement>(null); 9 | const queryClient = useQueryClient(); 10 | 11 | const { data: isAtBottom = false } = useQuery({ 12 | queryKey: ["messages", "is-at-bottom"], 13 | queryFn: () => false, 14 | initialData: false, 15 | }); 16 | 17 | const { data: scrollBehavior = false } = useQuery({ 18 | queryKey: ["messages", "should-scroll"], 19 | queryFn: () => false as ScrollFlag, 20 | initialData: false, 21 | }); 22 | 23 | useEffect(() => { 24 | if (scrollBehavior) { 25 | endRef.current?.scrollIntoView({ behavior: scrollBehavior }); 26 | queryClient.setQueryData(["messages", "should-scroll"], false); 27 | } 28 | }, [queryClient, scrollBehavior]); 29 | 30 | const scrollToBottom = useCallback( 31 | (behavior: ScrollBehavior = "smooth") => { 32 | queryClient.setQueryData(["messages", "should-scroll"], behavior); 33 | }, 34 | [queryClient], 35 | ); 36 | 37 | function onViewportEnter() { 38 | queryClient.setQueryData(["messages", "is-at-bottom"], true); 39 | } 40 | 41 | function onViewportLeave() { 42 | queryClient.setQueryData(["messages", "is-at-bottom"], false); 43 | } 44 | 45 | return { 46 | containerRef, 47 | endRef, 48 | isAtBottom, 49 | scrollToBottom, 50 | onViewportEnter, 51 | onViewportLeave, 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /src/app/_hooks/use-star-chat.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { api } from "@/trpc/react"; 4 | 5 | export const useStarChat = () => { 6 | const utils = api.useUtils(); 7 | 8 | return api.chats.starChat.useMutation({ 9 | onSuccess: async () => { 10 | await utils.chats.getChats.invalidate(); 11 | }, 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /src/app/account/components/header.tsx: -------------------------------------------------------------------------------- 1 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; 2 | import type { User } from "@prisma/client"; 3 | 4 | interface Props { 5 | user: User; 6 | } 7 | 8 | export const AccountHeader: React.FC<Props> = ({ user }) => { 9 | return ( 10 | <div> 11 | <div className="flex items-center gap-4"> 12 | <Avatar className="size-16 md:size-20"> 13 | <AvatarImage src={user.image ?? undefined} /> 14 | <AvatarFallback> 15 | {(user.name ?? user.email)?.charAt(0).toUpperCase() ?? "?"} 16 | </AvatarFallback> 17 | </Avatar> 18 | <div className="flex flex-col gap-1"> 19 | {user.name && <h1 className="text-4xl font-bold">{user.name}</h1>} 20 | {user.email && ( 21 | <p className="text-muted-foreground text-lg">{user.email}</p> 22 | )} 23 | {!user.name && !user.email && ( 24 | <p className="text-muted-foreground text-lg">No name or email</p> 25 | )} 26 | </div> 27 | </div> 28 | </div> 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /src/app/account/components/tabs/attachments/attachment.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonhedman/toolkit.dev/6bb4636d55ffcdf152fa21e668c65ca4a5266a73/src/app/account/components/tabs/attachments/attachment.tsx -------------------------------------------------------------------------------- /src/app/account/components/tabs/attachments/index.tsx: -------------------------------------------------------------------------------- 1 | import { DataTableDemo } from "./table"; 2 | 3 | export const Attachments = async () => { 4 | return <DataTableDemo />; 5 | }; 6 | -------------------------------------------------------------------------------- /src/app/account/components/tabs/connected-accounts/connect-disconnect.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | 5 | import { Check, Loader2 } from "lucide-react"; 6 | 7 | import { useRouter } from "next/navigation"; 8 | import { toast } from "sonner"; 9 | 10 | import { Button } from "@/components/ui/button"; 11 | 12 | import { api } from "@/trpc/react"; 13 | import { signIn } from "next-auth/react"; 14 | 15 | interface ConnectProps { 16 | provider: string; 17 | } 18 | 19 | export const ConnectButton: React.FC<ConnectProps> = ({ provider }) => { 20 | return ( 21 | <Button 22 | onClick={() => { 23 | void signIn(provider, { 24 | callbackUrl: "/account?tab=connected-accounts", 25 | }); 26 | }} 27 | > 28 | Connect 29 | </Button> 30 | ); 31 | }; 32 | 33 | interface DisconnectProps { 34 | accountId: string; 35 | } 36 | 37 | export const DisconnectButton: React.FC<DisconnectProps> = ({ accountId }) => { 38 | const router = useRouter(); 39 | const utils = api.useUtils(); 40 | const { 41 | mutate: deleteAccount, 42 | isPending, 43 | isSuccess, 44 | } = api.accounts.deleteAccount.useMutation({ 45 | onSuccess: async () => { 46 | await utils.accounts.getAccounts.invalidate(); 47 | router.refresh(); 48 | toast.success("Account disconnected"); 49 | }, 50 | }); 51 | 52 | return ( 53 | <Button 54 | variant="outline" 55 | onClick={() => { 56 | void deleteAccount(accountId); 57 | }} 58 | disabled={isPending} 59 | > 60 | Disconnect 61 | {isPending ? ( 62 | <Loader2 className="animate-spin" /> 63 | ) : isSuccess ? ( 64 | <Check /> 65 | ) : null} 66 | </Button> 67 | ); 68 | }; 69 | -------------------------------------------------------------------------------- /src/app/account/components/tabs/connected-accounts/index.tsx: -------------------------------------------------------------------------------- 1 | import { AuthProviderIcon } from "@/app/_components/navbar/account-button/provider-icon"; 2 | import { Badge } from "@/components/ui/badge"; 3 | import { HStack } from "@/components/ui/stack"; 4 | import { providers } from "@/server/auth/providers"; 5 | import { api } from "@/trpc/server"; 6 | import { ConnectButton, DisconnectButton } from "./connect-disconnect"; 7 | 8 | export const ConnectedAccounts = async () => { 9 | const accounts = await api.accounts.getAccounts({ 10 | limit: 100, 11 | }); 12 | 13 | return ( 14 | <div className="flex flex-col gap-2"> 15 | {providers.map((provider) => { 16 | const account = accounts?.items.find( 17 | (account) => account.provider === provider.id, 18 | ); 19 | 20 | return ( 21 | <HStack 22 | key={provider.id} 23 | className="w-full justify-between rounded-md border px-4 py-2" 24 | > 25 | <HStack className="gap-4"> 26 | <AuthProviderIcon provider={provider.name} /> 27 | <HStack className="gap-2"> 28 | <h2 className="font-medium">{provider.name}</h2> 29 | {account && <Badge variant="success">Connected</Badge>} 30 | </HStack> 31 | </HStack> 32 | {account ? ( 33 | <DisconnectButton accountId={account.id} /> 34 | ) : ( 35 | <ConnectButton provider={provider.id} /> 36 | )} 37 | </HStack> 38 | ); 39 | })} 40 | </div> 41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /src/app/account/components/tabs/images/index.tsx: -------------------------------------------------------------------------------- 1 | import { ImagesTable } from "./table"; 2 | 3 | export const Images = () => { 4 | return <ImagesTable />; 5 | }; 6 | -------------------------------------------------------------------------------- /src/app/account/components/tabs/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { File, User, Brain, Image as ImageIcon } from "lucide-react"; 4 | 5 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; 6 | 7 | import { ConnectedAccounts } from "./connected-accounts"; 8 | import { Attachments } from "./attachments"; 9 | import { Memories } from "./memories"; 10 | import { Images } from "./images"; 11 | import { HStack } from "@/components/ui/stack"; 12 | 13 | const tabs = [ 14 | { 15 | label: "Connected Accounts", 16 | value: "connected-accounts", 17 | component: <ConnectedAccounts />, 18 | icon: <User />, 19 | }, 20 | { 21 | label: "Attachments", 22 | value: "attachments", 23 | component: <Attachments />, 24 | icon: <File />, 25 | }, 26 | { 27 | label: "Memories", 28 | value: "memories", 29 | component: <Memories />, 30 | icon: <Brain />, 31 | }, 32 | { 33 | label: "Images Generated", 34 | value: "images", 35 | component: <Images />, 36 | icon: <ImageIcon />, 37 | }, 38 | ]; 39 | 40 | interface Props { 41 | defaultTab?: string; 42 | } 43 | 44 | export const AccountTabs: React.FC<Props> = ({ defaultTab }) => { 45 | return ( 46 | <Tabs defaultValue={defaultTab ?? "connected-accounts"}> 47 | <TabsList> 48 | {tabs.map((tab) => ( 49 | <TabsTrigger key={tab.value} value={tab.value}> 50 | <HStack className="gap-2"> 51 | {tab.icon} 52 | {tab.label} 53 | </HStack> 54 | </TabsTrigger> 55 | ))} 56 | </TabsList> 57 | {tabs.map((tab) => ( 58 | <TabsContent key={tab.value} value={tab.value} className="mt-2"> 59 | {tab.component} 60 | </TabsContent> 61 | ))} 62 | </Tabs> 63 | ); 64 | }; 65 | -------------------------------------------------------------------------------- /src/app/account/components/tabs/memories/index.tsx: -------------------------------------------------------------------------------- 1 | import { MemoriesTable } from "./table"; 2 | 3 | export const Memories = () => { 4 | return <MemoriesTable />; 5 | }; 6 | -------------------------------------------------------------------------------- /src/app/account/page.tsx: -------------------------------------------------------------------------------- 1 | import { auth } from "@/server/auth"; 2 | 3 | import { redirect } from "next/navigation"; 4 | 5 | import { api } from "@/trpc/server"; 6 | 7 | import { AccountHeader } from "./components/header"; 8 | import { AccountTabs } from "./components/tabs"; 9 | 10 | const AccountPage = async ({ 11 | searchParams, 12 | }: { 13 | searchParams: Promise<{ tab?: string }>; 14 | }) => { 15 | const { tab } = await searchParams; 16 | 17 | const session = await auth(); 18 | 19 | if (!session) { 20 | redirect("/login?redirect=/account"); 21 | } 22 | 23 | const user = await api.users.getCurrentUser(); 24 | 25 | if (!user) { 26 | redirect("/login?redirect=/account"); 27 | } 28 | 29 | return ( 30 | <div className="mx-auto max-w-4xl space-y-4 px-2 py-4 md:space-y-8"> 31 | <AccountHeader user={user} /> 32 | <AccountTabs defaultTab={tab} /> 33 | </div> 34 | ); 35 | }; 36 | 37 | export default AccountPage; 38 | -------------------------------------------------------------------------------- /src/app/admin/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect, notFound } from "next/navigation"; 2 | import { auth } from "@/server/auth"; 3 | import { createCaller } from "@/server/api/root"; 4 | import { createTRPCContext } from "@/server/api/trpc"; 5 | import { headers } from "next/headers"; 6 | import { AdminPanel } from "./_components/admin-panel"; 7 | 8 | export default async function AdminPage() { 9 | // Check authentication 10 | const session = await auth(); 11 | 12 | if (!session) { 13 | redirect("/login?redirect=/admin"); 14 | } 15 | 16 | // Create TRPC context and caller for server-side calls 17 | const ctx = await createTRPCContext({ headers: await headers() }); 18 | const caller = createCaller(ctx); 19 | 20 | // Check if user has admin access 21 | try { 22 | const isAdmin = await caller.features.isAdmin(); 23 | 24 | if (!isAdmin) { 25 | notFound(); 26 | } 27 | } catch (error) { 28 | console.error(error); 29 | // If there's an error checking admin status, deny access 30 | notFound(); 31 | } 32 | 33 | // If user is admin, render the admin panel 34 | return <AdminPanel />; 35 | } 36 | -------------------------------------------------------------------------------- /src/app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import { handlers } from "@/server/auth"; 2 | 3 | export const { GET, POST } = handlers; 4 | -------------------------------------------------------------------------------- /src/app/api/mcp/[server]/[transport]/route.ts: -------------------------------------------------------------------------------- 1 | import { serverToolkits } from "@/toolkits/toolkits/server"; 2 | import type { Toolkits } from "@/toolkits/toolkits/shared"; 3 | import { createMcpHandler } from "@vercel/mcp-adapter"; 4 | 5 | // Create a wrapper function that can access Next.js route parameters 6 | async function createHandlerWithParams( 7 | request: Request, 8 | { params }: { params: Promise<{ server: Toolkits }> }, 9 | ) { 10 | const { server } = await params; 11 | 12 | const serverToolkit = serverToolkits[server]; 13 | 14 | const handler = createMcpHandler( 15 | async (mcpServer) => { 16 | const tools = await serverToolkit.tools({ 17 | model: "openai:gpt-image-1", 18 | }); 19 | 20 | Object.entries(tools).forEach(([toolName, tool]) => { 21 | const { description, inputSchema, callback, message } = tool; 22 | mcpServer.tool( 23 | toolName, 24 | description, 25 | inputSchema.shape, 26 | async (args) => { 27 | const result = await callback(args); 28 | return { 29 | content: [ 30 | { 31 | type: "text", 32 | text: message 33 | ? typeof message === "function" 34 | ? message(result) 35 | : message 36 | : JSON.stringify(result, null, 2), 37 | }, 38 | ], 39 | structuredContent: result, 40 | }; 41 | }, 42 | ); 43 | }); 44 | }, 45 | { 46 | // Optional server options 47 | }, 48 | { 49 | redisUrl: process.env.REDIS_URL, 50 | basePath: `/mcp/${server}`, 51 | sseEndpoint: "/sse", 52 | maxDuration: 120, 53 | verboseLogs: true, 54 | }, 55 | ); 56 | 57 | return await handler(request); 58 | } 59 | 60 | export { createHandlerWithParams as GET, createHandlerWithParams as POST }; 61 | -------------------------------------------------------------------------------- /src/app/api/trpc/[trpc]/route.ts: -------------------------------------------------------------------------------- 1 | import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; 2 | import { type NextRequest } from "next/server"; 3 | 4 | import { env } from "@/env"; 5 | import { appRouter } from "@/server/api/root"; 6 | import { createTRPCContext } from "@/server/api/trpc"; 7 | 8 | /** 9 | * This wraps the `createTRPCContext` helper and provides the required context for the tRPC API when 10 | * handling a HTTP request (e.g. when you make requests from Client Components). 11 | */ 12 | const createContext = async (req: NextRequest) => { 13 | return createTRPCContext({ 14 | headers: req.headers, 15 | }); 16 | }; 17 | 18 | const handler = (req: NextRequest) => 19 | fetchRequestHandler({ 20 | endpoint: "/api/trpc", 21 | req, 22 | router: appRouter, 23 | createContext: () => createContext(req), 24 | onError: 25 | env.NODE_ENV === "development" 26 | ? ({ path, error }) => { 27 | console.error( 28 | `❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`, 29 | ); 30 | } 31 | : undefined, 32 | }); 33 | 34 | export { handler as GET, handler as POST }; 35 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonhedman/toolkit.dev/6bb4636d55ffcdf152fa21e668c65ca4a5266a73/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonhedman/toolkit.dev/6bb4636d55ffcdf152fa21e668c65ca4a5266a73/src/app/icon.png -------------------------------------------------------------------------------- /src/app/login/login-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | import { Card } from "@/components/ui/card"; 5 | 6 | import { VStack } from "@/components/ui/stack"; 7 | import { Logo } from "@/components/ui/logo"; 8 | import { AuthButtons } from "../_components/auth/auth-buttons"; 9 | import { useSearchParams } from "next/navigation"; 10 | 11 | interface LoginFormProps { 12 | providers: { 13 | name: string; 14 | id: string; 15 | }[]; 16 | } 17 | 18 | export function LoginForm({ 19 | providers, 20 | className, 21 | ...props 22 | }: LoginFormProps & React.ComponentProps<"div">) { 23 | const searchParams = useSearchParams(); 24 | const redirect = searchParams.get("redirect"); 25 | 26 | return ( 27 | <div className={cn("flex flex-col gap-6", className)} {...props}> 28 | <VStack className="w-full max-w-md gap-4"> 29 | <VStack className="gap-4"> 30 | <Logo className="size-16" /> 31 | <VStack className="gap-1"> 32 | <h1 className="text-primary text-2xl font-bold"> 33 | Welcome to Toolkit 34 | </h1> 35 | </VStack> 36 | </VStack> 37 | <Card className="w-full gap-4 p-4"> 38 | <p className="text-muted-foreground text-center text-sm"> 39 | Sign in with your preferred account to continue 40 | </p> 41 | <AuthButtons providers={providers} redirect={redirect ?? undefined} /> 42 | </Card> 43 | </VStack> 44 | </div> 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/app/login/page.tsx: -------------------------------------------------------------------------------- 1 | import { providers } from "@/server/auth/providers"; 2 | 3 | import { LoginForm } from "./login-form"; 4 | import { auth } from "@/server/auth"; 5 | import { redirect } from "next/navigation"; 6 | 7 | export default async function LoginPage() { 8 | const session = await auth(); 9 | 10 | if (session) { 11 | redirect("/"); 12 | } 13 | 14 | const mappedProviders = providers.map((provider) => ({ 15 | name: provider.name, 16 | id: provider.id, 17 | })); 18 | 19 | return ( 20 | <div className="flex flex-1 flex-col items-center justify-center p-4"> 21 | <div className="w-full max-w-md"> 22 | <LoginForm providers={mappedProviders} /> 23 | </div> 24 | </div> 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/app/opengraph-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonhedman/toolkit.dev/6bb4636d55ffcdf152fa21e668c65ca4a5266a73/src/app/opengraph-image.png -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { Chat } from "@/app/_components/chat"; 2 | import { auth } from "@/server/auth"; 3 | import { generateUUID } from "@/lib/utils"; 4 | import LandingPage from "./_components/landing-page"; 5 | 6 | export default async function Page() { 7 | const session = await auth(); 8 | 9 | if (!session) { 10 | return <LandingPage />; 11 | } 12 | 13 | const id = generateUUID(); 14 | 15 | return ( 16 | <Chat 17 | key={id} 18 | id={id} 19 | initialVisibilityType="private" 20 | isReadonly={false} 21 | isNew={true} 22 | /> 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/app/twitter-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonhedman/toolkit.dev/6bb4636d55ffcdf152fa21e668c65ca4a5266a73/src/app/twitter-image.png -------------------------------------------------------------------------------- /src/app/workbench/[id]/[chatId]/page.tsx: -------------------------------------------------------------------------------- 1 | import { notFound } from "next/navigation"; 2 | 3 | import { auth } from "@/server/auth"; 4 | import { Chat } from "@/app/_components/chat"; 5 | import { api } from "@/trpc/server"; 6 | 7 | export default async function Page(props: { 8 | params: Promise<{ id: string; chatId: string }>; 9 | }) { 10 | const params = await props.params; 11 | const { id, chatId } = params; 12 | 13 | const session = await auth(); 14 | 15 | const [chat, workbench] = await Promise.all([ 16 | api.chats.getChat(chatId), 17 | api.workbenches.getWorkbench(id), 18 | ]); 19 | 20 | if (!chat || !workbench) { 21 | notFound(); 22 | } 23 | 24 | return ( 25 | <Chat 26 | id={chat.id} 27 | initialVisibilityType={chat.visibility} 28 | isReadonly={session?.user?.id !== chat.userId} 29 | isNew={false} 30 | workbench={workbench} 31 | /> 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/app/workbench/[id]/_components/header.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ToolkitIcons } from "@/components/toolkit/toolkit-icons"; 4 | import { Button } from "@/components/ui/button"; 5 | import { HStack } from "@/components/ui/stack"; 6 | import type { Toolkits } from "@/toolkits/toolkits/shared"; 7 | import type { Workbench } from "@prisma/client"; 8 | import { Settings, Anvil } from "lucide-react"; 9 | import Link from "next/link"; 10 | 11 | interface WorkbenchHeaderProps { 12 | workbench: Workbench; 13 | } 14 | 15 | export function WorkbenchHeader({ workbench }: WorkbenchHeaderProps) { 16 | return ( 17 | <HStack className="justify-between border-b p-4"> 18 | <HStack> 19 | <Anvil className="size-5" /> 20 | <h1 className="text-lg font-semibold">{workbench.name}</h1> 21 | <ToolkitIcons toolkits={workbench.toolkitIds as Toolkits[]} /> 22 | </HStack> 23 | <Link href={`/workbench/${workbench.id}/edit`}> 24 | <Button variant="ghost" size="icon" className="size-fit p-1"> 25 | <Settings className="size-4" /> 26 | </Button> 27 | </Link> 28 | </HStack> 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/app/workbench/[id]/edit/page.tsx: -------------------------------------------------------------------------------- 1 | import { notFound } from "next/navigation"; 2 | import { api } from "@/trpc/server"; 3 | import { EditWorkbenchForm } from "./_components/edit-workbench-form"; 4 | 5 | interface EditWorkbenchPageProps { 6 | params: Promise<{ id: string }>; 7 | } 8 | 9 | export default async function EditWorkbenchPage({ 10 | params, 11 | }: EditWorkbenchPageProps) { 12 | const { id } = await params; 13 | 14 | try { 15 | const workbench = await api.workbenches.getWorkbench(id); 16 | 17 | if (!workbench) { 18 | notFound(); 19 | } 20 | 21 | return <EditWorkbenchForm workbench={workbench} />; 22 | } catch (error) { 23 | console.error(error); 24 | notFound(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/workbench/[id]/layout.tsx: -------------------------------------------------------------------------------- 1 | import { api } from "@/trpc/server"; 2 | import { WorkbenchHeader } from "./_components/header"; 3 | import { notFound, redirect } from "next/navigation"; 4 | import { auth } from "@/server/auth"; 5 | 6 | export default async function WorkbenchLayout({ 7 | children, 8 | params, 9 | }: { 10 | children: React.ReactNode; 11 | params: Promise<{ id: string }>; 12 | }) { 13 | const { id } = await params; 14 | 15 | const session = await auth(); 16 | 17 | if (!session) { 18 | redirect(`/login?redirect=/workbench/${id}`); 19 | } 20 | 21 | const workbench = await api.workbenches.getWorkbench(id); 22 | 23 | if (!workbench) { 24 | notFound(); 25 | } 26 | 27 | return ( 28 | <div className="flex h-0 flex-1 flex-col overflow-hidden"> 29 | <WorkbenchHeader workbench={workbench} /> 30 | <div className="flex h-0 flex-1 flex-col overflow-y-auto">{children}</div> 31 | </div> 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/app/workbench/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { notFound } from "next/navigation"; 2 | 3 | import { Chat } from "@/app/_components/chat"; 4 | import { api } from "@/trpc/server"; 5 | import { generateUUID } from "@/lib/utils"; 6 | 7 | export default async function WorkbenchPage(props: { 8 | params: Promise<{ id: string }>; 9 | }) { 10 | const params = await props.params; 11 | const { id } = params; 12 | 13 | try { 14 | const workbench = await api.workbenches.getWorkbench(id); 15 | 16 | if (!workbench) { 17 | notFound(); 18 | } 19 | 20 | const chatId = generateUUID(); 21 | 22 | return ( 23 | <Chat 24 | id={chatId} 25 | isReadonly={false} 26 | isNew={true} 27 | initialVisibilityType="private" 28 | workbench={workbench} 29 | /> 30 | ); 31 | } catch (error) { 32 | console.error(error); 33 | notFound(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/app/workbench/new/_components/index.ts: -------------------------------------------------------------------------------- 1 | export { NewWorkbenchForm } from "./new-workbench-form"; 2 | -------------------------------------------------------------------------------- /src/app/workbench/new/page.tsx: -------------------------------------------------------------------------------- 1 | import { auth } from "@/server/auth"; 2 | import { redirect } from "next/navigation"; 3 | import { NewWorkbenchForm } from "./_components"; 4 | 5 | export default async function NewWorkbenchPage() { 6 | const session = await auth(); 7 | 8 | if (!session) { 9 | redirect("/"); 10 | } 11 | 12 | return <NewWorkbenchForm />; 13 | } 14 | -------------------------------------------------------------------------------- /src/components/magicui/animated-shiny-text.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | type ComponentPropsWithoutRef, 3 | type CSSProperties, 4 | type FC, 5 | } from "react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | export interface AnimatedShinyTextProps 10 | extends ComponentPropsWithoutRef<"span"> { 11 | shimmerWidth?: number; 12 | } 13 | 14 | export const AnimatedShinyText: FC<AnimatedShinyTextProps> = ({ 15 | children, 16 | className, 17 | shimmerWidth = 100, 18 | ...props 19 | }) => { 20 | return ( 21 | <span 22 | style={ 23 | { 24 | "--shiny-width": `${shimmerWidth}px`, 25 | } as CSSProperties 26 | } 27 | className={cn( 28 | "text-neutral-600/70 dark:text-neutral-400/70", 29 | 30 | // Shine effect 31 | "animate-shiny-text [background-size:var(--shiny-width)_100%] bg-clip-text [background-position:0_0] bg-no-repeat [transition:background-position_1s_cubic-bezier(.6,.6,0,1)_infinite]", 32 | 33 | // Shine gradient 34 | "bg-gradient-to-r from-transparent via-black/80 via-50% to-transparent dark:via-white/80", 35 | 36 | className, 37 | )} 38 | {...props} 39 | > 40 | {children} 41 | </span> 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /src/components/toolkit/toolkit-configure.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { Plus } from "lucide-react"; 3 | import { useState } from "react"; 4 | import type z from "zod"; 5 | import type { 6 | Toolkits, 7 | ServerToolkitParameters, 8 | } from "@/toolkits/toolkits/shared"; 9 | import type { ClientToolkit } from "@/toolkits/types"; 10 | import type { SelectedToolkit } from "./types"; 11 | 12 | interface ClientToolkitConfigureProps { 13 | toolkit: ClientToolkit; 14 | id: Toolkits; 15 | schema: z.ZodObject<z.ZodRawShape>; 16 | onAdd: (toolkit: SelectedToolkit) => void; 17 | } 18 | 19 | export const ClientToolkitConfigure: React.FC<ClientToolkitConfigureProps> = ({ 20 | toolkit, 21 | id, 22 | schema, 23 | onAdd, 24 | }) => { 25 | const [parameters, setParameters] = useState< 26 | ServerToolkitParameters[typeof id] 27 | >({} as ServerToolkitParameters[typeof id]); 28 | 29 | const handleSubmit = () => { 30 | onAdd({ id, toolkit, parameters }); 31 | }; 32 | 33 | return ( 34 | <div className="space-y-4"> 35 | <div> 36 | <h4 className="font-medium">{toolkit.name}</h4> 37 | </div> 38 | 39 | <div className="space-y-4"> 40 | {toolkit.form && ( 41 | <toolkit.form parameters={parameters} setParameters={setParameters} /> 42 | )} 43 | </div> 44 | 45 | <Button 46 | onClick={handleSubmit} 47 | disabled={!schema.safeParse(parameters).success} 48 | className="w-full" 49 | > 50 | <Plus className="mr-2 size-4" /> 51 | Add {toolkit.name} 52 | </Button> 53 | </div> 54 | ); 55 | }; 56 | -------------------------------------------------------------------------------- /src/components/toolkit/toolkit-icons.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | import { getClientToolkit } from "@/toolkits/toolkits/client"; 3 | import type { Toolkits } from "@/toolkits/toolkits/shared"; 4 | 5 | interface Props { 6 | toolkits: Toolkits[]; 7 | orientation?: "horizontal" | "vertical"; 8 | containerClassName?: string; 9 | iconContainerClassName?: string; 10 | iconClassName?: string; 11 | } 12 | 13 | export const ToolkitIcons = ({ 14 | toolkits, 15 | orientation = "horizontal", 16 | containerClassName, 17 | iconContainerClassName, 18 | iconClassName, 19 | }: Props) => { 20 | if (toolkits.length === 0) return null; 21 | 22 | return ( 23 | <div 24 | className={cn( 25 | "flex items-center", 26 | { 27 | "flex-col pt-2": orientation === "vertical", 28 | "flex-row pl-2": orientation === "horizontal", 29 | }, 30 | containerClassName, 31 | )} 32 | > 33 | {toolkits.map((toolkit) => { 34 | const Icon = getClientToolkit(toolkit).icon; 35 | return ( 36 | <div 37 | className={cn( 38 | "border-primary bg-muted rounded-full border p-1", 39 | iconContainerClassName, 40 | { 41 | "-mt-2": orientation === "vertical", 42 | "-ml-2": orientation === "horizontal", 43 | }, 44 | )} 45 | key={toolkit} 46 | > 47 | <Icon 48 | className={cn("text-primary size-3 md:size-4", iconClassName)} 49 | /> 50 | </div> 51 | ); 52 | })} 53 | </div> 54 | ); 55 | }; 56 | -------------------------------------------------------------------------------- /src/components/toolkit/types.ts: -------------------------------------------------------------------------------- 1 | import type { Toolkits } from "@/toolkits/toolkits/shared"; 2 | import type { ClientToolkit } from "@/toolkits/types"; 3 | 4 | export type SelectedToolkit = { 5 | id: Toolkits; 6 | toolkit: ClientToolkit; 7 | parameters: Record<string, unknown>; 8 | }; 9 | -------------------------------------------------------------------------------- /src/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | function Avatar({ 9 | className, 10 | ...props 11 | }: React.ComponentProps<typeof AvatarPrimitive.Root>) { 12 | return ( 13 | <AvatarPrimitive.Root 14 | data-slot="avatar" 15 | className={cn( 16 | "relative flex size-8 shrink-0 overflow-hidden rounded-full", 17 | className, 18 | )} 19 | {...props} 20 | /> 21 | ); 22 | } 23 | 24 | function AvatarImage({ 25 | className, 26 | ...props 27 | }: React.ComponentProps<typeof AvatarPrimitive.Image>) { 28 | return ( 29 | <AvatarPrimitive.Image 30 | data-slot="avatar-image" 31 | className={cn("aspect-square size-full", className)} 32 | {...props} 33 | /> 34 | ); 35 | } 36 | 37 | function AvatarFallback({ 38 | className, 39 | ...props 40 | }: React.ComponentProps<typeof AvatarPrimitive.Fallback>) { 41 | return ( 42 | <AvatarPrimitive.Fallback 43 | data-slot="avatar-fallback" 44 | className={cn( 45 | "bg-muted flex size-full items-center justify-center rounded-full", 46 | className, 47 | )} 48 | {...props} 49 | /> 50 | ); 51 | } 52 | 53 | export { Avatar, AvatarImage, AvatarFallback }; 54 | -------------------------------------------------------------------------------- /src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { cva, type VariantProps } from "class-variance-authority"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | const badgeVariants = cva( 7 | "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", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | capability: 19 | "border-transparent bg-muted text-muted-foreground hover:bg-muted/80", 20 | success: "border-transparent bg-green-500/20 text-green-500", 21 | primary: "border-transparent bg-primary/30 text-primary", 22 | }, 23 | }, 24 | defaultVariants: { 25 | variant: "default", 26 | }, 27 | }, 28 | ); 29 | 30 | export interface BadgeProps 31 | extends React.HTMLAttributes<HTMLDivElement>, 32 | VariantProps<typeof badgeVariants> {} 33 | 34 | function Badge({ className, variant, ...props }: BadgeProps) { 35 | return ( 36 | <div className={cn(badgeVariants({ variant }), className)} {...props} /> 37 | ); 38 | } 39 | 40 | export { Badge, badgeVariants }; 41 | -------------------------------------------------------------------------------- /src/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; 5 | import { CheckIcon } from "lucide-react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | function Checkbox({ 10 | className, 11 | ...props 12 | }: React.ComponentProps<typeof CheckboxPrimitive.Root>) { 13 | return ( 14 | <CheckboxPrimitive.Root 15 | data-slot="checkbox" 16 | className={cn( 17 | "peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50", 18 | className, 19 | )} 20 | {...props} 21 | > 22 | <CheckboxPrimitive.Indicator 23 | data-slot="checkbox-indicator" 24 | className="flex items-center justify-center text-current transition-none" 25 | > 26 | <CheckIcon className="size-3.5" /> 27 | </CheckboxPrimitive.Indicator> 28 | </CheckboxPrimitive.Root> 29 | ); 30 | } 31 | 32 | export { Checkbox }; 33 | -------------------------------------------------------------------------------- /src/components/ui/info-tooltip.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { 4 | Tooltip, 5 | TooltipContent, 6 | TooltipProvider, 7 | TooltipTrigger, 8 | } from "@/components/ui/tooltip"; 9 | import { cn } from "@/lib/utils"; 10 | import { Info } from "lucide-react"; 11 | 12 | interface Props { 13 | content: string; 14 | className?: string; 15 | contentClassName?: string; 16 | } 17 | 18 | export const InfoTooltip: React.FC<Props> = ({ 19 | content, 20 | className, 21 | contentClassName, 22 | }) => ( 23 | <TooltipProvider delayDuration={100}> 24 | <Tooltip> 25 | <TooltipTrigger> 26 | <div 27 | className={cn( 28 | "text-muted-foreground hover:text-foreground size-5 transition-colors", 29 | className, 30 | )} 31 | > 32 | <Info className="size-full" /> 33 | </div> 34 | </TooltipTrigger> 35 | <TooltipContent 36 | side="bottom" 37 | className={cn("max-w-[200px] text-center", contentClassName)} 38 | > 39 | {content} 40 | </TooltipContent> 41 | </Tooltip> 42 | </TooltipProvider> 43 | ); 44 | -------------------------------------------------------------------------------- /src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | function Input({ className, type, ...props }: React.ComponentProps<"input">) { 6 | return ( 7 | <input 8 | type={type} 9 | data-slot="input" 10 | className={cn( 11 | "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", 12 | "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", 13 | "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", 14 | className, 15 | )} 16 | {...props} 17 | /> 18 | ); 19 | } 20 | 21 | export { Input }; 22 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as LabelPrimitive from "@radix-ui/react-label"; 5 | import { cva, type VariantProps } from "class-variance-authority"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", 11 | ); 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef<typeof LabelPrimitive.Root>, 15 | React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & 16 | VariantProps<typeof labelVariants> 17 | >(({ className, ...props }, ref) => ( 18 | <LabelPrimitive.Root 19 | ref={ref} 20 | className={cn(labelVariants(), className)} 21 | {...props} 22 | /> 23 | )); 24 | Label.displayName = LabelPrimitive.Root.displayName; 25 | 26 | export { Label }; 27 | -------------------------------------------------------------------------------- /src/components/ui/logo.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import Image from "next/image"; 4 | import { cn } from "@/lib/utils"; 5 | 6 | interface Props { 7 | className?: string; 8 | } 9 | 10 | export const Logo: React.FC<Props> = ({ className }) => { 11 | return ( 12 | <> 13 | <Image 14 | src="/logo/light.svg" 15 | alt="Toolkit.dev" 16 | width={100} 17 | height={100} 18 | className={cn(className, "dark:hidden")} 19 | /> 20 | <Image 21 | src="/logo/dark.svg" 22 | alt="Toolkit.dev" 23 | width={100} 24 | height={100} 25 | className={cn(className, "hidden dark:block")} 26 | /> 27 | </> 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /src/components/ui/model-icon.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | interface Props { 6 | provider: string; 7 | className?: string; 8 | } 9 | 10 | export const ModelProviderIcon: React.FC<Props> = ({ provider, className }) => { 11 | return ( 12 | <Image 13 | src={`/icons/${provider}.png`} 14 | alt={provider} 15 | width={16} 16 | height={16} 17 | className={cn("rounded-full", className ?? "")} 18 | /> 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as PopoverPrimitive from "@radix-ui/react-popover"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | function Popover({ 9 | ...props 10 | }: React.ComponentProps<typeof PopoverPrimitive.Root>) { 11 | return <PopoverPrimitive.Root data-slot="popover" {...props} />; 12 | } 13 | 14 | function PopoverTrigger({ 15 | ...props 16 | }: React.ComponentProps<typeof PopoverPrimitive.Trigger>) { 17 | return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />; 18 | } 19 | 20 | function PopoverContent({ 21 | className, 22 | align = "center", 23 | sideOffset = 4, 24 | ...props 25 | }: React.ComponentProps<typeof PopoverPrimitive.Content>) { 26 | return ( 27 | <PopoverPrimitive.Portal> 28 | <PopoverPrimitive.Content 29 | data-slot="popover-content" 30 | align={align} 31 | sideOffset={sideOffset} 32 | className={cn( 33 | "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden", 34 | className, 35 | )} 36 | {...props} 37 | /> 38 | </PopoverPrimitive.Portal> 39 | ); 40 | } 41 | 42 | function PopoverAnchor({ 43 | ...props 44 | }: React.ComponentProps<typeof PopoverPrimitive.Anchor>) { 45 | return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />; 46 | } 47 | 48 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }; 49 | -------------------------------------------------------------------------------- /src/components/ui/search-type-icon.tsx: -------------------------------------------------------------------------------- 1 | import { Brain, Sparkles } from "lucide-react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | import { SearchOptions } from "@/ai/types"; 6 | import { SiOpenai } from "@icons-pack/react-simple-icons"; 7 | 8 | interface Props { 9 | searchOption: SearchOptions; 10 | className?: string; 11 | } 12 | 13 | export const SearchTypeIcon: React.FC<Props> = ({ 14 | searchOption, 15 | className, 16 | }) => { 17 | const Icon = { 18 | [SearchOptions.Native]: Brain, 19 | [SearchOptions.OpenAiResponses]: SiOpenai, 20 | [SearchOptions.Exa]: Sparkles, 21 | }[searchOption]; 22 | 23 | return Icon ? <Icon className={cn("size-4", className)} /> : null; 24 | }; 25 | -------------------------------------------------------------------------------- /src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as SeparatorPrimitive from "@radix-ui/react-separator"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | function Separator({ 9 | className, 10 | orientation = "horizontal", 11 | decorative = true, 12 | ...props 13 | }: React.ComponentProps<typeof SeparatorPrimitive.Root>) { 14 | return ( 15 | <SeparatorPrimitive.Root 16 | data-slot="separator" 17 | decorative={decorative} 18 | orientation={orientation} 19 | className={cn( 20 | "bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px", 21 | className, 22 | )} 23 | {...props} 24 | /> 25 | ); 26 | } 27 | 28 | export { Separator }; 29 | -------------------------------------------------------------------------------- /src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | 3 | function Skeleton({ className, ...props }: React.ComponentProps<"div">) { 4 | return ( 5 | <div 6 | data-slot="skeleton" 7 | className={cn("bg-accent animate-pulse rounded-md", className)} 8 | {...props} 9 | /> 10 | ); 11 | } 12 | 13 | export { Skeleton }; 14 | -------------------------------------------------------------------------------- /src/components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useTheme } from "next-themes"; 4 | import { Toaster as Sonner, type ToasterProps } from "sonner"; 5 | 6 | const Toaster = ({ ...props }: ToasterProps) => { 7 | const { theme = "system" } = useTheme(); 8 | 9 | return ( 10 | <Sonner 11 | theme={theme as ToasterProps["theme"]} 12 | className="toaster group" 13 | style={ 14 | { 15 | "--normal-bg": "var(--popover)", 16 | "--normal-text": "var(--popover-foreground)", 17 | "--normal-border": "var(--border)", 18 | } as React.CSSProperties 19 | } 20 | {...props} 21 | /> 22 | ); 23 | }; 24 | 25 | export { Toaster }; 26 | -------------------------------------------------------------------------------- /src/components/ui/stack.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | import React from "react"; 3 | 4 | export const HStack: React.FC<React.ComponentProps<"div">> = ({ 5 | children, 6 | className, 7 | ...props 8 | }) => { 9 | return ( 10 | <div 11 | className={cn("flex flex-row items-center gap-2", className)} 12 | {...props} 13 | > 14 | {children} 15 | </div> 16 | ); 17 | }; 18 | 19 | export const VStack: React.FC<React.ComponentProps<"div">> = ({ 20 | children, 21 | className, 22 | ...props 23 | }) => { 24 | return ( 25 | <div 26 | className={cn("flex flex-col items-center gap-2", className)} 27 | {...props} 28 | > 29 | {children} 30 | </div> 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /src/components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as SwitchPrimitive from "@radix-ui/react-switch"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | function Switch({ 9 | className, 10 | ...props 11 | }: React.ComponentProps<typeof SwitchPrimitive.Root>) { 12 | return ( 13 | <SwitchPrimitive.Root 14 | data-slot="switch" 15 | className={cn( 16 | "peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50", 17 | className, 18 | )} 19 | {...props} 20 | > 21 | <SwitchPrimitive.Thumb 22 | data-slot="switch-thumb" 23 | className={cn( 24 | "bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0", 25 | )} 26 | /> 27 | </SwitchPrimitive.Root> 28 | ); 29 | } 30 | 31 | export { Switch }; 32 | -------------------------------------------------------------------------------- /src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { 6 | return ( 7 | <textarea 8 | data-slot="textarea" 9 | className={cn( 10 | "border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", 11 | className, 12 | )} 13 | {...props} 14 | /> 15 | ); 16 | } 17 | 18 | export { Textarea }; 19 | -------------------------------------------------------------------------------- /src/hooks/use-mobile.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const MOBILE_BREAKPOINT = 768; 4 | 5 | export function useIsMobile() { 6 | const [isMobile, setIsMobile] = React.useState<boolean>(false); 7 | 8 | React.useEffect(() => { 9 | // Set initial value 10 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); 11 | 12 | // Create media query 13 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); 14 | 15 | // Define change handler 16 | const onChange = (e: MediaQueryListEvent) => { 17 | setIsMobile(e.matches); 18 | }; 19 | 20 | // Add event listener 21 | mql.addEventListener("change", onChange); 22 | 23 | // Cleanup 24 | return () => mql.removeEventListener("change", onChange); 25 | }, []); 26 | 27 | return isMobile; 28 | } 29 | -------------------------------------------------------------------------------- /src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Global validation constants for the application 3 | */ 4 | 5 | export const VALIDATION_LIMITS = { 6 | /** Maximum length for message content and text parts */ 7 | MESSAGE_MAX_LENGTH: 16348, 8 | 9 | /** Maximum length for file and attachment names */ 10 | FILE_NAME_MAX_LENGTH: 2000, 11 | 12 | /** Maximum file size for uploads (in bytes) */ 13 | FILE_MAX_SIZE: 5 * 1024 * 1024, // 5MB 14 | } as const; 15 | 16 | /** 17 | * Convenience exports for commonly used limits 18 | */ 19 | export const MESSAGE_MAX_LENGTH = VALIDATION_LIMITS.MESSAGE_MAX_LENGTH; 20 | export const FILE_NAME_MAX_LENGTH = VALIDATION_LIMITS.FILE_NAME_MAX_LENGTH; 21 | export const FILE_MAX_SIZE = VALIDATION_LIMITS.FILE_MAX_SIZE; 22 | -------------------------------------------------------------------------------- /src/lib/cookies/client.ts: -------------------------------------------------------------------------------- 1 | import { setCookie } from "cookies-next/client"; 2 | import type { ImageModel, LanguageModel } from "@/ai/types"; 3 | import { COOKIE_KEYS } from "./keys"; 4 | import type { ClientToolkit } from "@/toolkits/types"; 5 | import type { z } from "zod"; 6 | import type { PersistedToolkit } from "./types"; 7 | 8 | // Client-side cookie utilities 9 | export const clientCookieUtils = { 10 | // Save model selection 11 | setSelectedChatModel(model: LanguageModel | undefined): void { 12 | setCookie(COOKIE_KEYS.SELECTED_CHAT_MODEL, model); 13 | }, 14 | 15 | // Save image generation model 16 | setImageGenerationModel(model: ImageModel | undefined): void { 17 | setCookie(COOKIE_KEYS.IMAGE_GENERATION_MODEL, model); 18 | }, 19 | 20 | // Save native search preference 21 | setUseNativeSearch(enabled: boolean): void { 22 | setCookie(COOKIE_KEYS.USE_NATIVE_SEARCH, enabled); 23 | }, 24 | 25 | // Save toolkits 26 | setToolkits( 27 | toolkits: Array<{ 28 | id: string; 29 | toolkit: ClientToolkit; 30 | parameters: z.infer<ClientToolkit["parameters"]>; 31 | }>, 32 | ): void { 33 | const persistedToolkits: PersistedToolkit[] = toolkits.map((t) => ({ 34 | id: t.id, 35 | parameters: t.parameters, 36 | })); 37 | setCookie(COOKIE_KEYS.TOOLKITS, persistedToolkits); 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /src/lib/cookies/keys.ts: -------------------------------------------------------------------------------- 1 | export const COOKIE_KEYS = { 2 | SELECTED_CHAT_MODEL: "open-chat-selected-model", 3 | IMAGE_GENERATION_MODEL: "open-chat-image-model", 4 | USE_NATIVE_SEARCH: "open-chat-native-search", 5 | TOOLKITS: "open-chat-toolkits", 6 | } as const; 7 | -------------------------------------------------------------------------------- /src/lib/cookies/server.ts: -------------------------------------------------------------------------------- 1 | import { cookies } from "next/headers"; 2 | import type { ChatPreferences } from "./types"; 3 | import { COOKIE_KEYS } from "./keys"; 4 | 5 | // Helper to safely parse JSON from cookie value 6 | const safeParseJson = <T>(value: string | null | undefined, fallback: T): T => { 7 | if (!value) return fallback; 8 | try { 9 | return JSON.parse(decodeURIComponent(value)) as T; 10 | } catch { 11 | return fallback; 12 | } 13 | }; 14 | 15 | // Server-side cookie utilities 16 | export const serverCookieUtils = { 17 | // Get all preferences from server-side cookies 18 | async getPreferences(): Promise<ChatPreferences> { 19 | try { 20 | const cookieStore = await cookies(); 21 | 22 | return { 23 | selectedChatModel: safeParseJson( 24 | cookieStore.get(COOKIE_KEYS.SELECTED_CHAT_MODEL)?.value, 25 | undefined, 26 | ), 27 | imageGenerationModel: safeParseJson( 28 | cookieStore.get(COOKIE_KEYS.IMAGE_GENERATION_MODEL)?.value, 29 | undefined, 30 | ), 31 | useNativeSearch: safeParseJson( 32 | cookieStore.get(COOKIE_KEYS.USE_NATIVE_SEARCH)?.value, 33 | false, 34 | ), 35 | toolkits: safeParseJson( 36 | cookieStore.get(COOKIE_KEYS.TOOLKITS)?.value, 37 | [], 38 | ), 39 | }; 40 | } catch (error) { 41 | console.warn("Failed to read cookies:", error); 42 | return {}; 43 | } 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /src/lib/cookies/types.ts: -------------------------------------------------------------------------------- 1 | import type { ImageModel, LanguageModel } from "@/ai/types"; 2 | 3 | export interface PersistedToolkit { 4 | id: string; 5 | parameters: Record<string, unknown>; 6 | } 7 | 8 | export interface ChatPreferences { 9 | selectedChatModel?: LanguageModel; 10 | imageGenerationModel?: ImageModel; 11 | useNativeSearch?: boolean; 12 | toolkits?: PersistedToolkit[]; 13 | } 14 | -------------------------------------------------------------------------------- /src/lib/fetch.ts: -------------------------------------------------------------------------------- 1 | import { ChatSDKError, type ErrorCode } from "@/lib/errors"; 2 | 3 | export async function fetchWithErrorHandlers( 4 | input: RequestInfo | URL, 5 | init?: RequestInit, 6 | ) { 7 | try { 8 | const response = await fetch(input, init); 9 | 10 | if (!response.ok) { 11 | const { code, cause } = (await response.json()) as { 12 | code: ErrorCode; 13 | cause: string; 14 | }; 15 | throw new ChatSDKError(code, cause); 16 | } 17 | 18 | return response; 19 | } catch (error: unknown) { 20 | if (typeof navigator !== "undefined" && !navigator.onLine) { 21 | throw new ChatSDKError("offline:chat"); 22 | } 23 | 24 | throw error; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | 8 | export function generateUUID(): string { 9 | return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { 10 | const r = (Math.random() * 16) | 0; 11 | const v = c === "x" ? r : (r & 0x3) | 0x8; 12 | return v.toString(16); 13 | }); 14 | } 15 | 16 | export function sanitizeText(text: string) { 17 | return text.replace("<has_function_call>", ""); 18 | } 19 | -------------------------------------------------------------------------------- /src/server/api/root.ts: -------------------------------------------------------------------------------- 1 | import { createCallerFactory, createTRPCRouter } from "@/server/api/trpc"; 2 | 3 | import { 4 | chatsRouter, 5 | filesRouter, 6 | messagesRouter, 7 | streamsRouter, 8 | usersRouter, 9 | accountsRouter, 10 | imagesRouter, 11 | memoriesRouter, 12 | featuresRouter, 13 | workbenchesRouter, 14 | } from "./routers"; 15 | 16 | /** 17 | * This is the primary router for your server. 18 | * 19 | * All routers added in /api/routers should be manually added here. 20 | */ 21 | export const appRouter = createTRPCRouter({ 22 | chats: chatsRouter, 23 | messages: messagesRouter, 24 | streams: streamsRouter, 25 | files: filesRouter, 26 | users: usersRouter, 27 | accounts: accountsRouter, 28 | images: imagesRouter, 29 | memories: memoriesRouter, 30 | features: featuresRouter, 31 | workbenches: workbenchesRouter, 32 | }); 33 | 34 | // export type definition of API 35 | export type AppRouter = typeof appRouter; 36 | 37 | /** 38 | * Create a server-side caller for the tRPC API. 39 | * @example 40 | * const trpc = createCaller(createContext); 41 | * const res = await trpc.post.all(); 42 | * ^? Post[] 43 | */ 44 | export const createCaller = createCallerFactory(appRouter); 45 | -------------------------------------------------------------------------------- /src/server/api/routers/index.ts: -------------------------------------------------------------------------------- 1 | export { chatsRouter } from "./chats"; 2 | export { messagesRouter } from "./messages"; 3 | export { streamsRouter } from "./streams"; 4 | export { filesRouter } from "./files"; 5 | export { usersRouter } from "./users"; 6 | export { accountsRouter } from "./accounts"; 7 | export { imagesRouter } from "./images"; 8 | export { memoriesRouter } from "./memories"; 9 | export { featuresRouter } from "./features"; 10 | export { workbenchesRouter } from "./workbenches"; 11 | -------------------------------------------------------------------------------- /src/server/api/routers/streams.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; 4 | 5 | export const streamsRouter = createTRPCRouter({ 6 | createStreamId: protectedProcedure 7 | .input( 8 | z.object({ 9 | streamId: z.string(), 10 | chatId: z.string(), 11 | }), 12 | ) 13 | .mutation(async ({ ctx, input }) => { 14 | return await ctx.db.stream.create({ 15 | data: { 16 | id: input.streamId, 17 | chatId: input.chatId, 18 | createdAt: new Date(), 19 | }, 20 | }); 21 | }), 22 | 23 | getStreamIdsByChatId: protectedProcedure 24 | .input( 25 | z.object({ 26 | chatId: z.string(), 27 | }), 28 | ) 29 | .query(async ({ ctx, input }) => { 30 | const streams = await ctx.db.stream.findMany({ 31 | where: { 32 | chatId: input.chatId, 33 | }, 34 | orderBy: { 35 | createdAt: "asc", 36 | }, 37 | select: { 38 | id: true, 39 | }, 40 | }); 41 | 42 | return streams.map((stream) => stream.id); 43 | }), 44 | }); 45 | -------------------------------------------------------------------------------- /src/server/api/routers/users.ts: -------------------------------------------------------------------------------- 1 | import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; 2 | import { z } from "zod"; 3 | 4 | export const usersRouter = createTRPCRouter({ 5 | getCurrentUser: protectedProcedure.query(async ({ ctx }) => { 6 | return ctx.db.user.findUnique({ 7 | where: { 8 | id: ctx.session.user.id, 9 | }, 10 | select: { 11 | id: true, 12 | name: true, 13 | email: true, 14 | image: true, 15 | emailVerified: true, 16 | }, 17 | }); 18 | }), 19 | 20 | updateUser: protectedProcedure 21 | .input( 22 | z.object({ 23 | name: z.string().optional(), 24 | email: z.string().email().optional(), 25 | image: z.string().optional(), 26 | }), 27 | ) 28 | .mutation(async ({ ctx, input }) => { 29 | return ctx.db.user.update({ 30 | where: { 31 | id: ctx.session.user.id, 32 | }, 33 | data: input, 34 | }); 35 | }), 36 | 37 | deleteUser: protectedProcedure.mutation(async ({ ctx }) => { 38 | return ctx.db.user.delete({ 39 | where: { 40 | id: ctx.session.user.id, 41 | }, 42 | }); 43 | }), 44 | }); 45 | -------------------------------------------------------------------------------- /src/server/auth/index.ts: -------------------------------------------------------------------------------- 1 | import { cache } from "react"; 2 | 3 | import NextAuth from "next-auth"; 4 | 5 | import { authConfig } from "./config"; 6 | 7 | const { auth: uncachedAuth, handlers, signIn, signOut } = NextAuth(authConfig); 8 | 9 | const auth = cache(uncachedAuth); 10 | 11 | export { auth, handlers, signIn, signOut }; 12 | -------------------------------------------------------------------------------- /src/server/db.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | import { env } from "@/env"; 4 | 5 | const createPrismaClient = () => 6 | new PrismaClient({ 7 | log: 8 | env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"], 9 | }); 10 | 11 | const globalForPrisma = globalThis as unknown as { 12 | prisma: ReturnType<typeof createPrismaClient> | undefined; 13 | }; 14 | 15 | export const db = globalForPrisma.prisma ?? createPrismaClient(); 16 | 17 | if (env.NODE_ENV !== "production") globalForPrisma.prisma = db; 18 | -------------------------------------------------------------------------------- /src/toolkits/create-tool.ts: -------------------------------------------------------------------------------- 1 | import type { ZodObject, ZodRawShape } from "zod"; 2 | import type { 3 | BaseTool, 4 | ClientTool, 5 | ClientToolConfig, 6 | ServerTool, 7 | ServerToolConfig, 8 | } from "./types"; 9 | 10 | export const createBaseTool = < 11 | Args extends ZodRawShape, 12 | Output extends ZodRawShape, 13 | >({ 14 | description, 15 | inputSchema, 16 | outputSchema, 17 | }: { 18 | description: string; 19 | inputSchema: ZodObject<Args>; 20 | outputSchema: ZodObject<Output>; 21 | }): BaseTool<Args, Output> => { 22 | return { description, inputSchema, outputSchema }; 23 | }; 24 | 25 | export const createServerTool = < 26 | Args extends ZodRawShape, 27 | Result extends ZodRawShape, 28 | >( 29 | tool: BaseTool<Args, Result>, 30 | config: ServerToolConfig<Args, Result>, 31 | ): ServerTool<Args, Result> => { 32 | return { 33 | ...tool, 34 | ...config, 35 | }; 36 | }; 37 | 38 | export const createClientTool = < 39 | Args extends ZodRawShape, 40 | Result extends ZodRawShape, 41 | >( 42 | tool: BaseTool<Args, Result>, 43 | config: ClientToolConfig<Args, Result>, 44 | ): ClientTool<Args, Result> => { 45 | return { 46 | ...tool, 47 | ...config, 48 | }; 49 | }; 50 | -------------------------------------------------------------------------------- /src/toolkits/toolkit-groups.ts: -------------------------------------------------------------------------------- 1 | import { Logo } from "@/components/ui/logo"; 2 | import { ToolkitGroups, type ToolkitGroup } from "./types"; 3 | import { BookCopy, Database } from "lucide-react"; 4 | 5 | export const toolkitGroups: ToolkitGroup[] = [ 6 | { 7 | id: ToolkitGroups.Native, 8 | name: "Native Tools", 9 | icon: Logo, 10 | }, 11 | { 12 | id: ToolkitGroups.DataSource, 13 | name: "Data Sources", 14 | icon: Database, 15 | }, 16 | { 17 | id: ToolkitGroups.KnowledgeBase, 18 | name: "Knowledge Base", 19 | icon: BookCopy, 20 | }, 21 | ]; 22 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/client.ts: -------------------------------------------------------------------------------- 1 | import type { ClientToolkit } from "../types"; 2 | import { 3 | Toolkits, 4 | type ServerToolkitNames, 5 | type ServerToolkitParameters, 6 | } from "./shared"; 7 | import { exaClientToolkit } from "./exa/client"; 8 | import { imageClientToolkit } from "./image/client"; 9 | import { githubClientToolkit } from "./github/client"; 10 | import { googleCalendarClientToolkit } from "./google-calendar/client"; 11 | import { googleDriveClientToolkit } from "./google-drive/client"; 12 | import { mem0ClientToolkit } from "./mem0/client"; 13 | import { notionClientToolkit } from "./notion/client"; 14 | import { e2bClientToolkit } from "./e2b/client"; 15 | 16 | type ClientToolkits = { 17 | [K in Toolkits]: ClientToolkit< 18 | ServerToolkitNames[K], 19 | ServerToolkitParameters[K] 20 | >; 21 | }; 22 | 23 | export const clientToolkits: ClientToolkits = { 24 | [Toolkits.E2B]: e2bClientToolkit, 25 | [Toolkits.Memory]: mem0ClientToolkit, 26 | [Toolkits.Image]: imageClientToolkit, 27 | [Toolkits.Exa]: exaClientToolkit, 28 | [Toolkits.Github]: githubClientToolkit, 29 | [Toolkits.GoogleCalendar]: googleCalendarClientToolkit, 30 | [Toolkits.Notion]: notionClientToolkit, 31 | [Toolkits.GoogleDrive]: googleDriveClientToolkit, 32 | }; 33 | 34 | export function getClientToolkit<T extends Toolkits>( 35 | server: T, 36 | ): ClientToolkit<ServerToolkitNames[T], ServerToolkitParameters[T]> { 37 | return clientToolkits[server]; 38 | } 39 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/e2b/base.ts: -------------------------------------------------------------------------------- 1 | import type { ToolkitConfig } from "@/toolkits/types"; 2 | import { baseRunCodeTool } from "./tools/run_code/base"; 3 | import { E2BTools } from "./tools/tools"; 4 | import { z } from "zod"; 5 | 6 | export const e2bParameters = z.object({}); 7 | 8 | export const baseE2BToolkitConfig: ToolkitConfig< 9 | E2BTools, 10 | typeof e2bParameters.shape 11 | > = { 12 | tools: { 13 | [E2BTools.RunCode]: baseRunCodeTool, 14 | }, 15 | parameters: e2bParameters, 16 | }; 17 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/e2b/client.tsx: -------------------------------------------------------------------------------- 1 | import { Terminal } from "lucide-react"; 2 | 3 | import { E2BTools } from "./tools/tools"; 4 | import { createClientToolkit } from "@/toolkits/create-toolkit"; 5 | import { e2bRunCodeToolConfigClient } from "./tools/run_code/client"; 6 | import { baseE2BToolkitConfig } from "./base"; 7 | import { ToolkitGroups } from "@/toolkits/types"; 8 | 9 | export const e2bClientToolkit = createClientToolkit( 10 | baseE2BToolkitConfig, 11 | { 12 | name: "Code Interpreter", 13 | description: "Execute python code in a secure environment", 14 | icon: Terminal, 15 | form: null, 16 | type: ToolkitGroups.Native, 17 | }, 18 | { 19 | [E2BTools.RunCode]: e2bRunCodeToolConfigClient, 20 | }, 21 | ); 22 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/e2b/server.ts: -------------------------------------------------------------------------------- 1 | import { createServerToolkit } from "@/toolkits/create-toolkit"; 2 | import { baseE2BToolkitConfig } from "./base"; 3 | import { e2bRunCodeToolConfigServer } from "./tools/run_code/server"; 4 | import { E2BTools } from "./tools/tools"; 5 | 6 | export const e2bToolkitServer = createServerToolkit( 7 | baseE2BToolkitConfig, 8 | `You have access to the E2B toolkit for secure code execution and development environments. This toolkit provides: 9 | 10 | - **Run Code**: Execute Python code in isolated, secure cloud environments.`, 11 | async () => { 12 | return { 13 | [E2BTools.RunCode]: e2bRunCodeToolConfigServer(), 14 | }; 15 | }, 16 | ); 17 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/e2b/tools/run_code/base.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { createBaseTool } from "@/toolkits/create-tool"; 3 | 4 | import type { Result, Logs } from "@e2b/code-interpreter"; 5 | 6 | export const baseRunCodeTool = createBaseTool({ 7 | description: 8 | "Run Python code in a secure sandbox environment using E2B. Supports Jupyter Notebook syntax and returns execution results and logs.", 9 | inputSchema: z.object({ 10 | code: z.string().describe("The Python code to execute in the sandbox"), 11 | }), 12 | outputSchema: z.object({ 13 | results: z 14 | .array(z.custom<Result>()) 15 | .describe("The execution results from running the code"), 16 | logs: z.custom<Logs>().describe("Execution logs"), 17 | }), 18 | }); 19 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/e2b/tools/tools.ts: -------------------------------------------------------------------------------- 1 | export enum E2BTools { 2 | RunCode = "run-code", 3 | } 4 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/exa/base.ts: -------------------------------------------------------------------------------- 1 | import type { ToolkitConfig } from "@/toolkits/types"; 2 | import { baseSearchTool } from "./tools/search/base"; 3 | import { baseResearchPaperSearchTool } from "./tools/research_paper_search/base"; 4 | import { baseCompanyResearchTool } from "./tools/company_research/base"; 5 | import { baseCrawlingTool } from "./tools/crawling/base"; 6 | import { baseCompetitorFinderTool } from "./tools/competitor_finder/base"; 7 | import { baseLinkedinSearchTool } from "./tools/linkedin_search/base"; 8 | import { baseWikipediaSearchTool } from "./tools/wikipedia_search/base"; 9 | import { baseGithubSearchTool } from "./tools/github_search/base"; 10 | import { ExaTools } from "./tools/tools"; 11 | import { z } from "zod"; 12 | 13 | export const exaParameters = z.object({}); 14 | 15 | export const baseExaToolkitConfig: ToolkitConfig< 16 | ExaTools, 17 | typeof exaParameters.shape 18 | > = { 19 | tools: { 20 | [ExaTools.Search]: baseSearchTool, 21 | [ExaTools.ResearchPaperSearch]: baseResearchPaperSearchTool, 22 | [ExaTools.CompanyResearch]: baseCompanyResearchTool, 23 | [ExaTools.Crawling]: baseCrawlingTool, 24 | [ExaTools.CompetitorFinder]: baseCompetitorFinderTool, 25 | [ExaTools.LinkedinSearch]: baseLinkedinSearchTool, 26 | [ExaTools.WikipediaSearch]: baseWikipediaSearchTool, 27 | [ExaTools.GithubSearch]: baseGithubSearchTool, 28 | }, 29 | parameters: exaParameters, 30 | }; 31 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/exa/client.tsx: -------------------------------------------------------------------------------- 1 | import { Search } from "lucide-react"; 2 | 3 | import { ExaTools } from "./tools/tools"; 4 | import { createClientToolkit } from "@/toolkits/create-toolkit"; 5 | import { exaSearchToolConfigClient } from "./tools/search/client"; 6 | import { exaResearchPaperSearchToolConfigClient } from "./tools/research_paper_search/client"; 7 | import { exaCompanyResearchToolConfigClient } from "./tools/company_research/client"; 8 | import { exaCrawlingToolConfigClient } from "./tools/crawling/client"; 9 | import { exaCompetitorFinderToolConfigClient } from "./tools/competitor_finder/client"; 10 | import { exaLinkedinSearchToolConfigClient } from "./tools/linkedin_search/client"; 11 | import { exaWikipediaSearchToolConfigClient } from "./tools/wikipedia_search/client"; 12 | import { exaGithubSearchToolConfigClient } from "./tools/github_search/client"; 13 | import { baseExaToolkitConfig } from "./base"; 14 | import { ToolkitGroups } from "@/toolkits/types"; 15 | 16 | export const exaClientToolkit = createClientToolkit( 17 | baseExaToolkitConfig, 18 | { 19 | name: "Web Search", 20 | description: "Find articles, research papers, companies, and more", 21 | icon: Search, 22 | form: null, 23 | type: ToolkitGroups.DataSource, 24 | }, 25 | { 26 | [ExaTools.Search]: exaSearchToolConfigClient, 27 | [ExaTools.ResearchPaperSearch]: exaResearchPaperSearchToolConfigClient, 28 | [ExaTools.CompanyResearch]: exaCompanyResearchToolConfigClient, 29 | [ExaTools.Crawling]: exaCrawlingToolConfigClient, 30 | [ExaTools.CompetitorFinder]: exaCompetitorFinderToolConfigClient, 31 | [ExaTools.LinkedinSearch]: exaLinkedinSearchToolConfigClient, 32 | [ExaTools.WikipediaSearch]: exaWikipediaSearchToolConfigClient, 33 | [ExaTools.GithubSearch]: exaGithubSearchToolConfigClient, 34 | }, 35 | ); 36 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/exa/components/index.ts: -------------------------------------------------------------------------------- 1 | export { ToolCallDisplay } from "./tool-call-display"; 2 | export { ResultItem, type ResultData } from "./result-item"; 3 | export { ResultsList } from "./results-list"; 4 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/exa/components/results-list.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ResultItem, type ResultData } from "./result-item"; 3 | 4 | interface ResultsListProps { 5 | results: ResultData[]; 6 | title: string; 7 | emptyMessage: string; 8 | linkText?: string; 9 | } 10 | 11 | export const ResultsList: React.FC<ResultsListProps> = ({ 12 | results, 13 | title, 14 | emptyMessage, 15 | linkText, 16 | }) => { 17 | if (!results.length) { 18 | return <div className="text-gray-500">{emptyMessage}</div>; 19 | } 20 | 21 | return ( 22 | <div className=""> 23 | <h1 className="text-muted-foreground text-sm font-medium">{title}</h1> 24 | <div className="flex flex-col"> 25 | {results.map((result, index) => ( 26 | <ResultItem 27 | key={index} 28 | result={result} 29 | index={index} 30 | linkText={linkText} 31 | /> 32 | ))} 33 | </div> 34 | </div> 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/exa/components/tool-call-display.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { HStack, VStack } from "@/components/ui/stack"; 3 | 4 | interface ToolCallDisplayProps { 5 | icon: React.ComponentType<{ className?: string }>; 6 | label: string; 7 | value: string; 8 | } 9 | 10 | export const ToolCallDisplay: React.FC<ToolCallDisplayProps> = ({ 11 | icon: Icon, 12 | label, 13 | value, 14 | }) => { 15 | return ( 16 | <HStack className="gap-2"> 17 | <Icon className="text-muted-foreground size-4" /> 18 | <VStack className="items-start gap-0"> 19 | <span className="text-muted-foreground/80 text-xs font-medium"> 20 | {label} 21 | </span> 22 | <span className="text-sm">"{value}"</span> 23 | </VStack> 24 | </HStack> 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/exa/tools/company_research/base.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { createBaseTool } from "@/toolkits/create-tool"; 3 | 4 | export const baseCompanyResearchTool = createBaseTool({ 5 | description: "Research companies and gather detailed business information", 6 | inputSchema: z.object({ 7 | company: z.string().min(1).max(100), 8 | }), 9 | outputSchema: z.object({ 10 | results: z.array( 11 | z.object({ 12 | title: z.string().nullable(), 13 | url: z.string().url(), 14 | content: z.string(), 15 | publishedDate: z.string().optional(), 16 | image: z.string().optional(), 17 | favicon: z.string().optional(), 18 | score: z.number().optional(), 19 | author: z.string().optional(), 20 | }), 21 | ), 22 | }), 23 | }); 24 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/exa/tools/company_research/client.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Building } from "lucide-react"; 3 | import type { baseCompanyResearchTool } from "./base"; 4 | import type { ClientToolConfig } from "@/toolkits/types"; 5 | import { ToolCallDisplay, ResultsList } from "../../components"; 6 | 7 | export const exaCompanyResearchToolConfigClient: ClientToolConfig< 8 | typeof baseCompanyResearchTool.inputSchema.shape, 9 | typeof baseCompanyResearchTool.outputSchema.shape 10 | > = { 11 | CallComponent: ({ args }) => { 12 | return ( 13 | <ToolCallDisplay 14 | icon={Building} 15 | label="Company Research" 16 | value={args.company ?? "..."} 17 | /> 18 | ); 19 | }, 20 | ResultComponent: ({ result }) => { 21 | return ( 22 | <ResultsList 23 | results={result.results} 24 | title="Company Research Results" 25 | emptyMessage="No company information found" 26 | linkText="Read more →" 27 | /> 28 | ); 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/exa/tools/company_research/server.ts: -------------------------------------------------------------------------------- 1 | import { type baseCompanyResearchTool } from "./base"; 2 | import { env } from "@/env"; 3 | import { Exa } from "exa-js"; 4 | import type { ServerToolConfig } from "@/toolkits/types"; 5 | 6 | export const exaCompanyResearchToolConfigServer: ServerToolConfig< 7 | typeof baseCompanyResearchTool.inputSchema.shape, 8 | typeof baseCompanyResearchTool.outputSchema.shape 9 | > = { 10 | callback: async ({ company }) => { 11 | if (!env.EXA_API_KEY) { 12 | throw new Error("EXA_API_KEY is not set"); 13 | } 14 | 15 | const exa = new Exa(env.EXA_API_KEY); 16 | 17 | const { results } = await exa.searchAndContents( 18 | `${company} company profile business information`, 19 | { 20 | livecrawl: "always", 21 | numResults: 5, 22 | type: "neural", 23 | useAutoprompt: true, 24 | }, 25 | ); 26 | 27 | return { 28 | results: results.map((result) => ({ 29 | title: result.title, 30 | url: result.url, 31 | content: result.text.slice(0, 1500), 32 | publishedDate: result.publishedDate, 33 | image: result.image, 34 | favicon: result.favicon, 35 | score: result.score, 36 | author: result.author, 37 | })), 38 | }; 39 | }, 40 | message: 41 | "The user is shown comprehensive company research results. Provide a summary of the key business information found and ask if they need more specific details.", 42 | }; 43 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/exa/tools/competitor_finder/base.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { createBaseTool } from "@/toolkits/create-tool"; 3 | 4 | export const baseCompetitorFinderTool = createBaseTool({ 5 | description: "Find competitors of a company", 6 | inputSchema: z.object({ 7 | company: z.string().min(1).max(100), 8 | }), 9 | outputSchema: z.object({ 10 | results: z.array( 11 | z.object({ 12 | title: z.string().nullable(), 13 | url: z.string().url(), 14 | content: z.string(), 15 | publishedDate: z.string().optional(), 16 | image: z.string().optional(), 17 | favicon: z.string().optional(), 18 | score: z.number().optional(), 19 | author: z.string().optional(), 20 | }), 21 | ), 22 | }), 23 | }); 24 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/exa/tools/competitor_finder/client.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Users } from "lucide-react"; 3 | import type { baseCompetitorFinderTool } from "./base"; 4 | import type { ClientToolConfig } from "@/toolkits/types"; 5 | import { ToolCallDisplay, ResultsList } from "../../components"; 6 | 7 | export const exaCompetitorFinderToolConfigClient: ClientToolConfig< 8 | typeof baseCompetitorFinderTool.inputSchema.shape, 9 | typeof baseCompetitorFinderTool.outputSchema.shape 10 | > = { 11 | CallComponent: ({ args }) => { 12 | return ( 13 | <ToolCallDisplay 14 | icon={Users} 15 | label="Competitor Analysis" 16 | value={args.company ?? "..."} 17 | /> 18 | ); 19 | }, 20 | ResultComponent: ({ result }) => { 21 | return ( 22 | <ResultsList 23 | results={result.results} 24 | title="Competitors Found" 25 | emptyMessage="No competitors found" 26 | linkText="Learn more →" 27 | /> 28 | ); 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/exa/tools/competitor_finder/server.ts: -------------------------------------------------------------------------------- 1 | import { type baseCompetitorFinderTool } from "./base"; 2 | import { env } from "@/env"; 3 | import { Exa } from "exa-js"; 4 | import type { ServerToolConfig } from "@/toolkits/types"; 5 | 6 | export const exaCompetitorFinderToolConfigServer: ServerToolConfig< 7 | typeof baseCompetitorFinderTool.inputSchema.shape, 8 | typeof baseCompetitorFinderTool.outputSchema.shape 9 | > = { 10 | callback: async ({ company }) => { 11 | if (!env.EXA_API_KEY) { 12 | throw new Error("EXA_API_KEY is not set"); 13 | } 14 | 15 | const exa = new Exa(env.EXA_API_KEY); 16 | 17 | const { results } = await exa.searchAndContents( 18 | `${company} competitors alternative companies similar services`, 19 | { 20 | livecrawl: "always", 21 | numResults: 5, 22 | type: "neural", 23 | useAutoprompt: true, 24 | }, 25 | ); 26 | 27 | return { 28 | results: results.map((result) => ({ 29 | title: result.title, 30 | url: result.url, 31 | content: result.text.slice(0, 1000), 32 | publishedDate: result.publishedDate, 33 | image: result.image, 34 | favicon: result.favicon, 35 | score: result.score, 36 | author: result.author, 37 | })), 38 | }; 39 | }, 40 | message: 41 | "The user is shown competitor analysis results. Provide a summary of the main competitors found and their key differentiators.", 42 | }; 43 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/exa/tools/crawling/base.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { createBaseTool } from "@/toolkits/create-tool"; 3 | 4 | export const baseCrawlingTool = createBaseTool({ 5 | description: "Extract content from specific URLs", 6 | inputSchema: z.object({ 7 | urls: z.array(z.string()), 8 | }), 9 | outputSchema: z.object({ 10 | results: z.array( 11 | z.object({ 12 | title: z.string().nullable(), 13 | url: z.string().url(), 14 | content: z.string(), 15 | publishedDate: z.string().optional(), 16 | image: z.string().optional(), 17 | favicon: z.string().optional(), 18 | score: z.number().optional(), 19 | author: z.string().optional(), 20 | }), 21 | ), 22 | }), 23 | }); 24 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/exa/tools/crawling/client.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "lucide-react"; 3 | import type { baseCrawlingTool } from "./base"; 4 | import type { ClientToolConfig } from "@/toolkits/types"; 5 | import { ToolCallDisplay, ResultsList } from "../../components"; 6 | 7 | export const exaCrawlingToolConfigClient: ClientToolConfig< 8 | typeof baseCrawlingTool.inputSchema.shape, 9 | typeof baseCrawlingTool.outputSchema.shape 10 | > = { 11 | CallComponent: ({ args }) => { 12 | return ( 13 | <ToolCallDisplay 14 | icon={Link} 15 | label="Crawling URLs" 16 | value={args.urls?.join(", ") ?? "..."} 17 | /> 18 | ); 19 | }, 20 | ResultComponent: ({ result }) => { 21 | return ( 22 | <ResultsList 23 | results={result.results} 24 | title="Extracted Content" 25 | emptyMessage="No content extracted" 26 | linkText="View source →" 27 | /> 28 | ); 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/exa/tools/crawling/server.ts: -------------------------------------------------------------------------------- 1 | import { type baseCrawlingTool } from "./base"; 2 | import { env } from "@/env"; 3 | import { Exa } from "exa-js"; 4 | import type { ServerToolConfig } from "@/toolkits/types"; 5 | 6 | export const exaCrawlingToolConfigServer: ServerToolConfig< 7 | typeof baseCrawlingTool.inputSchema.shape, 8 | typeof baseCrawlingTool.outputSchema.shape 9 | > = { 10 | callback: async ({ urls }) => { 11 | if (!env.EXA_API_KEY) { 12 | throw new Error("EXA_API_KEY is not set"); 13 | } 14 | 15 | const exa = new Exa(env.EXA_API_KEY); 16 | 17 | const { results } = await exa.getContents(urls); 18 | 19 | return { 20 | results: results.map((result) => ({ 21 | title: result.title, 22 | url: result.url, 23 | content: result.text, 24 | publishedDate: result.publishedDate, 25 | image: result.image, 26 | favicon: result.favicon, 27 | score: result.score, 28 | author: result.author, 29 | })), 30 | }; 31 | }, 32 | message: 33 | "The user is shown the extracted content from the URL. Provide a brief summary of what was found.", 34 | }; 35 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/exa/tools/github_search/base.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { createBaseTool } from "@/toolkits/create-tool"; 3 | 4 | export const baseGithubSearchTool = createBaseTool({ 5 | description: "Search GitHub repositories and accounts", 6 | inputSchema: z.object({ 7 | query: z.string().min(1).max(100), 8 | }), 9 | outputSchema: z.object({ 10 | results: z.array( 11 | z.object({ 12 | title: z.string().nullable(), 13 | url: z.string().url(), 14 | content: z.string(), 15 | publishedDate: z.string().optional(), 16 | image: z.string().optional(), 17 | favicon: z.string().optional(), 18 | score: z.number().optional(), 19 | author: z.string().optional(), 20 | }), 21 | ), 22 | }), 23 | }); 24 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/exa/tools/github_search/client.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import type { baseGithubSearchTool } from "./base"; 3 | import type { ClientToolConfig } from "@/toolkits/types"; 4 | import { ToolCallDisplay, ResultsList } from "../../components"; 5 | import { SiGithub } from "@icons-pack/react-simple-icons"; 6 | 7 | export const exaGithubSearchToolConfigClient: ClientToolConfig< 8 | typeof baseGithubSearchTool.inputSchema.shape, 9 | typeof baseGithubSearchTool.outputSchema.shape 10 | > = { 11 | CallComponent: ({ args }) => { 12 | return ( 13 | <ToolCallDisplay 14 | icon={SiGithub} 15 | label="GitHub Search" 16 | value={args.query ?? "..."} 17 | /> 18 | ); 19 | }, 20 | ResultComponent: ({ result }) => { 21 | return ( 22 | <ResultsList 23 | results={result.results} 24 | title="GitHub Repositories" 25 | emptyMessage="No GitHub repositories found" 26 | linkText="View repository →" 27 | /> 28 | ); 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/exa/tools/github_search/server.ts: -------------------------------------------------------------------------------- 1 | import { type baseGithubSearchTool } from "./base"; 2 | import { env } from "@/env"; 3 | import { Exa } from "exa-js"; 4 | import type { ServerToolConfig } from "@/toolkits/types"; 5 | 6 | export const exaGithubSearchToolConfigServer: ServerToolConfig< 7 | typeof baseGithubSearchTool.inputSchema.shape, 8 | typeof baseGithubSearchTool.outputSchema.shape 9 | > = { 10 | callback: async ({ query }) => { 11 | if (!env.EXA_API_KEY) { 12 | throw new Error("EXA_API_KEY is not set"); 13 | } 14 | 15 | const exa = new Exa(env.EXA_API_KEY); 16 | 17 | const { results } = await exa.searchAndContents(query, { 18 | livecrawl: "always", 19 | numResults: 5, 20 | includeDomains: ["github.com"], 21 | }); 22 | 23 | return { 24 | results: results.map((result) => ({ 25 | title: result.title, 26 | url: result.url, 27 | content: result.text.slice(0, 1000), 28 | publishedDate: result.publishedDate, 29 | image: result.image, 30 | favicon: result.favicon, 31 | score: result.score, 32 | author: result.author, 33 | })), 34 | }; 35 | }, 36 | message: 37 | "The user is shown GitHub repositories and accounts. Provide a summary of the development projects and code found.", 38 | }; 39 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/exa/tools/linkedin_search/base.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { createBaseTool } from "@/toolkits/create-tool"; 3 | 4 | export const baseLinkedinSearchTool = createBaseTool({ 5 | description: "Search LinkedIn for companies and people", 6 | inputSchema: z.object({ 7 | query: z.string().min(1).max(100), 8 | }), 9 | outputSchema: z.object({ 10 | results: z.array( 11 | z.object({ 12 | title: z.string().nullable(), 13 | url: z.string().url(), 14 | content: z.string(), 15 | publishedDate: z.string().optional(), 16 | image: z.string().optional(), 17 | favicon: z.string().optional(), 18 | score: z.number().optional(), 19 | author: z.string().optional(), 20 | }), 21 | ), 22 | }), 23 | }); 24 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/exa/tools/linkedin_search/client.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Linkedin } from "lucide-react"; 3 | import type { baseLinkedinSearchTool } from "./base"; 4 | import type { ClientToolConfig } from "@/toolkits/types"; 5 | import { ToolCallDisplay, ResultsList } from "../../components"; 6 | 7 | export const exaLinkedinSearchToolConfigClient: ClientToolConfig< 8 | typeof baseLinkedinSearchTool.inputSchema.shape, 9 | typeof baseLinkedinSearchTool.outputSchema.shape 10 | > = { 11 | CallComponent: ({ args }) => { 12 | return ( 13 | <ToolCallDisplay 14 | icon={Linkedin} 15 | label="LinkedIn Search" 16 | value={args.query ?? "..."} 17 | /> 18 | ); 19 | }, 20 | ResultComponent: ({ result }) => { 21 | return ( 22 | <ResultsList 23 | results={result.results} 24 | title="LinkedIn Results" 25 | emptyMessage="No LinkedIn profiles found" 26 | linkText="View profile →" 27 | /> 28 | ); 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/exa/tools/linkedin_search/server.ts: -------------------------------------------------------------------------------- 1 | import { type baseLinkedinSearchTool } from "./base"; 2 | import { env } from "@/env"; 3 | import { Exa } from "exa-js"; 4 | import type { ServerToolConfig } from "@/toolkits/types"; 5 | 6 | export const exaLinkedinSearchToolConfigServer: ServerToolConfig< 7 | typeof baseLinkedinSearchTool.inputSchema.shape, 8 | typeof baseLinkedinSearchTool.outputSchema.shape 9 | > = { 10 | callback: async ({ query }) => { 11 | if (!env.EXA_API_KEY) { 12 | throw new Error("EXA_API_KEY is not set"); 13 | } 14 | 15 | const exa = new Exa(env.EXA_API_KEY); 16 | 17 | const { results } = await exa.searchAndContents(query, { 18 | livecrawl: "always", 19 | numResults: 3, 20 | includeDomains: ["linkedin.com"], 21 | }); 22 | 23 | return { 24 | results: results.map((result) => ({ 25 | title: result.title, 26 | url: result.url, 27 | content: result.text.slice(0, 1000), 28 | publishedDate: result.publishedDate, 29 | image: result.image, 30 | favicon: result.favicon, 31 | score: result.score, 32 | author: result.author, 33 | })), 34 | }; 35 | }, 36 | message: 37 | "The user is shown LinkedIn profiles and company pages. Provide a summary of the professional information found.", 38 | }; 39 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/exa/tools/research_paper_search/base.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { createBaseTool } from "@/toolkits/create-tool"; 3 | 4 | export const baseResearchPaperSearchTool = createBaseTool({ 5 | description: "Search for research papers and academic content", 6 | inputSchema: z.object({ 7 | query: z.string().min(1).max(100), 8 | }), 9 | outputSchema: z.object({ 10 | results: z.array( 11 | z.object({ 12 | title: z.string().nullable(), 13 | url: z.string().url(), 14 | content: z.string(), 15 | publishedDate: z.string().optional(), 16 | image: z.string().optional(), 17 | favicon: z.string().optional(), 18 | score: z.number().optional(), 19 | author: z.string().optional(), 20 | }), 21 | ), 22 | }), 23 | }); 24 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/exa/tools/research_paper_search/client.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { BookOpen } from "lucide-react"; 3 | 4 | import type { baseResearchPaperSearchTool } from "./base"; 5 | import type { ClientToolConfig } from "@/toolkits/types"; 6 | import { ToolCallDisplay, ResultsList } from "../../components"; 7 | 8 | export const exaResearchPaperSearchToolConfigClient: ClientToolConfig< 9 | typeof baseResearchPaperSearchTool.inputSchema.shape, 10 | typeof baseResearchPaperSearchTool.outputSchema.shape 11 | > = { 12 | CallComponent: ({ args }) => { 13 | return ( 14 | <ToolCallDisplay 15 | icon={BookOpen} 16 | label="Research Paper Search" 17 | value={args.query ?? "..."} 18 | /> 19 | ); 20 | }, 21 | ResultComponent: ({ result }) => { 22 | return ( 23 | <ResultsList 24 | results={result.results} 25 | title="Research Papers" 26 | emptyMessage="No research papers found" 27 | linkText="Read full paper →" 28 | /> 29 | ); 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/exa/tools/research_paper_search/server.ts: -------------------------------------------------------------------------------- 1 | import { type baseResearchPaperSearchTool } from "./base"; 2 | import { env } from "@/env"; 3 | import { Exa } from "exa-js"; 4 | import type { ServerToolConfig } from "@/toolkits/types"; 5 | 6 | export const exaResearchPaperSearchToolConfigServer: ServerToolConfig< 7 | typeof baseResearchPaperSearchTool.inputSchema.shape, 8 | typeof baseResearchPaperSearchTool.outputSchema.shape 9 | > = { 10 | callback: async ({ query }) => { 11 | if (!env.EXA_API_KEY) { 12 | throw new Error("EXA_API_KEY is not set"); 13 | } 14 | 15 | const exa = new Exa(env.EXA_API_KEY); 16 | 17 | const { results } = await exa.searchAndContents(query, { 18 | livecrawl: "always", 19 | numResults: 3, 20 | includeDomains: [ 21 | "arxiv.org", 22 | "scholar.google.com", 23 | "pubmed.ncbi.nlm.nih.gov", 24 | "researchgate.net", 25 | "ieee.org", 26 | "acm.org", 27 | ], 28 | }); 29 | 30 | return { 31 | results: results.map((result) => ({ 32 | title: result.title, 33 | url: result.url, 34 | content: result.text.slice(0, 1000), 35 | publishedDate: result.publishedDate, 36 | image: result.image, 37 | favicon: result.favicon, 38 | score: result.score, 39 | author: result.author, 40 | })), 41 | }; 42 | }, 43 | message: 44 | "The user is shown research papers in three cards. Do not list the sources again. Just give a 1-2 sentence summary response to their question and ask what else they would like to know.", 45 | }; 46 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/exa/tools/search/base.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { createBaseTool } from "@/toolkits/create-tool"; 3 | 4 | export const baseSearchTool = createBaseTool({ 5 | description: "Search the web for up-to-date information", 6 | inputSchema: z.object({ 7 | query: z.string().min(1).max(100), 8 | }), 9 | outputSchema: z.object({ 10 | results: z.array( 11 | z.object({ 12 | title: z.string().nullable(), 13 | url: z.string().url(), 14 | content: z.string(), 15 | publishedDate: z.string().optional(), 16 | image: z.string().optional(), 17 | favicon: z.string().optional(), 18 | score: z.number().optional(), 19 | author: z.string().optional(), 20 | }), 21 | ), 22 | }), 23 | }); 24 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/exa/tools/search/client.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Search } from "lucide-react"; 3 | 4 | import type { baseSearchTool } from "./base"; 5 | import type { ClientToolConfig } from "@/toolkits/types"; 6 | import { ToolCallDisplay, ResultsList } from "../../components"; 7 | 8 | export const exaSearchToolConfigClient: ClientToolConfig< 9 | typeof baseSearchTool.inputSchema.shape, 10 | typeof baseSearchTool.outputSchema.shape 11 | > = { 12 | CallComponent: ({ args }) => { 13 | return ( 14 | <ToolCallDisplay 15 | icon={Search} 16 | label="Search Query" 17 | value={args.query ?? "..."} 18 | /> 19 | ); 20 | }, 21 | ResultComponent: ({ result }) => { 22 | return ( 23 | <ResultsList 24 | results={result.results} 25 | title="Search Results" 26 | emptyMessage="No results found" 27 | linkText="Read full article →" 28 | /> 29 | ); 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/exa/tools/search/server.ts: -------------------------------------------------------------------------------- 1 | import { type baseSearchTool } from "./base"; 2 | import { env } from "@/env"; 3 | import { Exa } from "exa-js"; 4 | import type { ServerToolConfig } from "@/toolkits/types"; 5 | 6 | export const exaSearchToolConfigServer: ServerToolConfig< 7 | typeof baseSearchTool.inputSchema.shape, 8 | typeof baseSearchTool.outputSchema.shape 9 | > = { 10 | callback: async ({ query }) => { 11 | if (!env.EXA_API_KEY) { 12 | throw new Error("EXA_API_KEY is not set"); 13 | } 14 | 15 | const exa = new Exa(env.EXA_API_KEY); 16 | 17 | const { results } = await exa.searchAndContents(query, { 18 | livecrawl: "always", 19 | numResults: 3, 20 | }); 21 | 22 | return { 23 | results: results.map((result) => ({ 24 | title: result.title, 25 | url: result.url, 26 | content: result.text.slice(0, 1000), 27 | publishedDate: result.publishedDate, 28 | image: result.image, 29 | favicon: result.favicon, 30 | score: result.score, 31 | author: result.author, 32 | })), 33 | }; 34 | }, 35 | message: 36 | "The user is shown the article in three cards. Do not list the sources again. Just give a 1-2 sentence summary response to their question and ask what else they would like to know.", 37 | }; 38 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/exa/tools/tools.ts: -------------------------------------------------------------------------------- 1 | export enum ExaTools { 2 | Search = "search", 3 | ResearchPaperSearch = "research-paper-search", 4 | CompanyResearch = "company-research", 5 | Crawling = "crawling", 6 | CompetitorFinder = "competitor-finder", 7 | LinkedinSearch = "linkedin-search", 8 | WikipediaSearch = "wikipedia-search-exa", 9 | GithubSearch = "github-search", 10 | } 11 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/exa/tools/wikipedia_search/base.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { createBaseTool } from "@/toolkits/create-tool"; 3 | 4 | export const baseWikipediaSearchTool = createBaseTool({ 5 | description: "Search Wikipedia articles on specific topics", 6 | inputSchema: z.object({ 7 | query: z.string().min(1).max(100), 8 | }), 9 | outputSchema: z.object({ 10 | results: z.array( 11 | z.object({ 12 | title: z.string().nullable(), 13 | url: z.string().url(), 14 | content: z.string(), 15 | publishedDate: z.string().optional(), 16 | image: z.string().optional(), 17 | favicon: z.string().optional(), 18 | score: z.number().optional(), 19 | author: z.string().optional(), 20 | }), 21 | ), 22 | }), 23 | }); 24 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/exa/tools/wikipedia_search/client.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Globe } from "lucide-react"; 3 | import type { baseWikipediaSearchTool } from "./base"; 4 | import type { ClientToolConfig } from "@/toolkits/types"; 5 | import { ToolCallDisplay, ResultsList } from "../../components"; 6 | 7 | export const exaWikipediaSearchToolConfigClient: ClientToolConfig< 8 | typeof baseWikipediaSearchTool.inputSchema.shape, 9 | typeof baseWikipediaSearchTool.outputSchema.shape 10 | > = { 11 | CallComponent: ({ args }) => { 12 | return ( 13 | <ToolCallDisplay 14 | icon={Globe} 15 | label="Wikipedia Search" 16 | value={args.query ?? "..."} 17 | /> 18 | ); 19 | }, 20 | ResultComponent: ({ result }) => { 21 | return ( 22 | <ResultsList 23 | results={result.results} 24 | title="Wikipedia Articles" 25 | emptyMessage="No Wikipedia articles found" 26 | linkText="Read article →" 27 | /> 28 | ); 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/exa/tools/wikipedia_search/server.ts: -------------------------------------------------------------------------------- 1 | import { type baseWikipediaSearchTool } from "./base"; 2 | import { env } from "@/env"; 3 | import { Exa } from "exa-js"; 4 | import type { ServerToolConfig } from "@/toolkits/types"; 5 | 6 | export const exaWikipediaSearchToolConfigServer: ServerToolConfig< 7 | typeof baseWikipediaSearchTool.inputSchema.shape, 8 | typeof baseWikipediaSearchTool.outputSchema.shape 9 | > = { 10 | callback: async ({ query }) => { 11 | if (!env.EXA_API_KEY) { 12 | throw new Error("EXA_API_KEY is not set"); 13 | } 14 | 15 | const exa = new Exa(env.EXA_API_KEY); 16 | 17 | const { results } = await exa.searchAndContents(query, { 18 | livecrawl: "always", 19 | numResults: 3, 20 | includeDomains: ["wikipedia.org"], 21 | }); 22 | 23 | return { 24 | results: results.map((result) => ({ 25 | title: result.title, 26 | url: result.url, 27 | content: result.text.slice(0, 1500), 28 | publishedDate: result.publishedDate, 29 | image: result.image, 30 | favicon: result.favicon, 31 | score: result.score, 32 | author: result.author, 33 | })), 34 | }; 35 | }, 36 | message: 37 | "The user is shown Wikipedia articles. Provide a concise summary of the encyclopedia information found.", 38 | }; 39 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/github/base.ts: -------------------------------------------------------------------------------- 1 | import type { ToolkitConfig } from "@/toolkits/types"; 2 | import { z } from "zod"; 3 | import { GithubTools } from "./tools"; 4 | import { 5 | searchRepositoriesTool, 6 | repoInfoTool, 7 | searchCodeTool, 8 | searchUsersTool, 9 | } from "./tools"; 10 | 11 | export const githubParameters = z.object({}); 12 | 13 | export const baseGithubToolkitConfig: ToolkitConfig< 14 | GithubTools, 15 | typeof githubParameters.shape 16 | > = { 17 | tools: { 18 | [GithubTools.SearchRepos]: searchRepositoriesTool, 19 | [GithubTools.RepoInfo]: repoInfoTool, 20 | [GithubTools.SearchCode]: searchCodeTool, 21 | [GithubTools.SearchUsers]: searchUsersTool, 22 | }, 23 | parameters: githubParameters, 24 | }; 25 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/github/components/user-avatar.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from "react"; 2 | 3 | import { SiGithub } from "@icons-pack/react-simple-icons"; 4 | 5 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | interface Props { 10 | login: string; 11 | className?: string; 12 | style?: React.CSSProperties; 13 | } 14 | 15 | export const GithubAvatar = memo(function GithubAvatar({ 16 | login, 17 | className, 18 | style, 19 | }: Props) { 20 | return ( 21 | <Avatar className={cn("size-10 rounded-full", className)} style={style}> 22 | <AvatarImage 23 | src={`https://github.com/${login}.png`} 24 | alt={login} 25 | className="object-cover" 26 | /> 27 | <AvatarFallback className="bg-muted dark:bg-muted p-1"> 28 | <SiGithub className="size-full opacity-60" /> 29 | </AvatarFallback> 30 | </Avatar> 31 | ); 32 | }); 33 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/github/lib/commits.ts: -------------------------------------------------------------------------------- 1 | import { type Octokit } from "octokit"; 2 | 3 | interface CommitParams { 4 | since?: string; 5 | until?: string; 6 | author?: string; 7 | sha?: string; 8 | } 9 | 10 | export const getAllCommits = async ( 11 | octokit: Octokit, 12 | owner: string, 13 | repo: string, 14 | additionalParams?: CommitParams, 15 | ) => { 16 | const firstPage = await octokit.rest.repos.listCommits({ 17 | ...additionalParams, 18 | owner, 19 | repo, 20 | per_page: 100, 21 | page: 1, 22 | }); 23 | 24 | const lastPageMatch = firstPage.headers.link?.match(/<([^>]+)>; rel="last"/); 25 | const lastPageUrl = lastPageMatch?.[1]; 26 | const totalPages = lastPageUrl 27 | ? parseInt(new URL(lastPageUrl).searchParams.get("page") ?? "1") 28 | : 1; 29 | 30 | const remainingPages = 31 | totalPages > 1 32 | ? await Promise.all( 33 | Array.from({ length: totalPages - 1 }, (_, i) => 34 | octokit.rest.repos.listCommits({ 35 | owner, 36 | repo, 37 | ...additionalParams, 38 | per_page: 100, 39 | page: i + 2, 40 | }), 41 | ), 42 | ) 43 | : []; 44 | 45 | return [firstPage.data, ...remainingPages.map((page) => page.data)].flat(); 46 | }; 47 | 48 | export const getTotalCommits = async ( 49 | octokit: Octokit, 50 | owner: string, 51 | repo: string, 52 | additionalParams?: CommitParams, 53 | ) => { 54 | const firstPage = await octokit.rest.repos.listCommits({ 55 | owner, 56 | repo, 57 | per_page: 1, 58 | ...additionalParams, 59 | }); 60 | 61 | const lastPageMatch = firstPage.headers.link?.match(/<([^>]+)>; rel="last"/); 62 | const lastPageUrl = lastPageMatch?.[1]; 63 | const totalPages = lastPageUrl 64 | ? parseInt(new URL(lastPageUrl).searchParams.get("page") ?? "1") 65 | : 1; 66 | 67 | return totalPages; 68 | }; 69 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/github/lib/prs.ts: -------------------------------------------------------------------------------- 1 | import { type Octokit } from "octokit"; 2 | 3 | export const getTotalPrs = async (octokit: Octokit, query: string) => { 4 | const { 5 | data: { total_count }, 6 | } = await octokit.rest.search.issuesAndPullRequests({ 7 | q: query, 8 | per_page: 1, 9 | }); 10 | 11 | return total_count; 12 | }; 13 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/github/tools/client.ts: -------------------------------------------------------------------------------- 1 | export * from "./search/client"; 2 | export * from "./repo/client"; 3 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/github/tools/index.ts: -------------------------------------------------------------------------------- 1 | export enum GithubTools { 2 | SearchRepos = "search-repos", 3 | SearchCode = "search-code", 4 | SearchUsers = "search-users", 5 | RepoInfo = "repo-info", 6 | } 7 | 8 | export * from "./search/base"; 9 | export * from "./repo/base"; 10 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/github/tools/server.ts: -------------------------------------------------------------------------------- 1 | export * from "./repo/server"; 2 | export * from "./search/server"; 3 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/google-calendar/base.ts: -------------------------------------------------------------------------------- 1 | import type { ToolkitConfig } from "@/toolkits/types"; 2 | import { z } from "zod"; 3 | import { GoogleCalendarTools } from "./tools"; 4 | import { 5 | listCalendarsTool, 6 | getCalendarTool, 7 | listEventsTool, 8 | getEventTool, 9 | searchEventsTool, 10 | } from "./tools"; 11 | 12 | export const googleCalendarParameters = z.object({}); 13 | 14 | export const baseGoogleCalendarToolkitConfig: ToolkitConfig< 15 | GoogleCalendarTools, 16 | typeof googleCalendarParameters.shape 17 | > = { 18 | tools: { 19 | [GoogleCalendarTools.ListCalendars]: listCalendarsTool, 20 | [GoogleCalendarTools.GetCalendar]: getCalendarTool, 21 | [GoogleCalendarTools.ListEvents]: listEventsTool, 22 | [GoogleCalendarTools.GetEvent]: getEventTool, 23 | [GoogleCalendarTools.SearchEvents]: searchEventsTool, 24 | }, 25 | parameters: googleCalendarParameters, 26 | }; 27 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/google-calendar/components/calendar-card.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Badge } from "@/components/ui/badge"; 3 | import { HStack, VStack } from "@/components/ui/stack"; 4 | import { Calendar, Clock, Shield } from "lucide-react"; 5 | 6 | interface CalendarCardProps { 7 | calendar: { 8 | id: string; 9 | summary: string; 10 | description?: string; 11 | timeZone: string; 12 | accessRole: string; 13 | primary?: boolean; 14 | selected?: boolean; 15 | }; 16 | showDetails?: boolean; 17 | } 18 | 19 | export const CalendarCard: React.FC<CalendarCardProps> = ({ 20 | calendar, 21 | showDetails = false, 22 | }) => { 23 | return ( 24 | <VStack className="w-full items-start gap-1 border-b pb-2 last:border-b-0 last:pb-0"> 25 | <HStack className="w-full items-center gap-2"> 26 | <Calendar className="text-primary size-4" /> 27 | <VStack className="flex-1 items-start gap-0"> 28 | <h3 className="text-sm font-medium">{calendar.summary}</h3> 29 | {calendar.description && ( 30 | <p className="text-muted-foreground line-clamp-2 text-xs"> 31 | {calendar.description} 32 | </p> 33 | )} 34 | </VStack> 35 | {calendar.primary && ( 36 | <Badge variant="default" className="text-xs"> 37 | Primary 38 | </Badge> 39 | )} 40 | </HStack> 41 | 42 | {showDetails && ( 43 | <HStack className="text-muted-foreground items-center gap-4 text-xs"> 44 | <HStack className="items-center gap-1"> 45 | <Clock className="size-3" /> 46 | <span>{calendar.timeZone}</span> 47 | </HStack> 48 | <HStack className="items-center gap-1"> 49 | <Shield className="size-3" /> 50 | <span>{calendar.accessRole}</span> 51 | </HStack> 52 | </HStack> 53 | )} 54 | </VStack> 55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/google-calendar/components/tool-call.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { HStack, VStack } from "@/components/ui/stack"; 3 | import { Search } from "lucide-react"; 4 | 5 | interface ToolCallComponentProps { 6 | action: string; 7 | primaryText: string; 8 | secondaryText?: string; 9 | icon?: React.ComponentType<{ className?: string }>; 10 | } 11 | 12 | export const ToolCallComponent: React.FC<ToolCallComponentProps> = ({ 13 | action, 14 | primaryText, 15 | secondaryText, 16 | icon: Icon = Search, 17 | }) => { 18 | return ( 19 | <HStack className="gap-2"> 20 | <Icon className="text-muted-foreground size-4" /> 21 | <VStack className="items-start gap-0"> 22 | <span className="text-muted-foreground text-xs font-medium"> 23 | {action} 24 | </span> 25 | <span className="text-sm">{primaryText}</span> 26 | {secondaryText && ( 27 | <span className="text-muted-foreground text-xs">{secondaryText}</span> 28 | )} 29 | </VStack> 30 | </HStack> 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/google-calendar/tools/client.ts: -------------------------------------------------------------------------------- 1 | export { googleCalendarListCalendarsToolConfigClient } from "./list-calendars/client"; 2 | export { googleCalendarGetCalendarToolConfigClient } from "./get-calendar/client"; 3 | export { googleCalendarListEventsToolConfigClient } from "./list-events/client"; 4 | export { googleCalendarGetEventToolConfigClient } from "./get-event/client"; 5 | export { googleCalendarSearchEventsToolConfigClient } from "./search-events/client"; 6 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/google-calendar/tools/get-calendar/base.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { createBaseTool } from "@/toolkits/create-tool"; 3 | 4 | export const getCalendarTool = createBaseTool({ 5 | description: "Get details of a specific calendar by ID", 6 | inputSchema: z.object({ 7 | calendarId: z.string().describe("The ID of the calendar to retrieve"), 8 | }), 9 | outputSchema: z.object({ 10 | id: z.string().describe("The calendar ID"), 11 | summary: z.string().describe("The calendar title"), 12 | description: z.string().optional().describe("The calendar description"), 13 | timeZone: z.string().describe("The time zone of the calendar"), 14 | colorId: z.string().optional().describe("The color ID of the calendar"), 15 | backgroundColor: z 16 | .string() 17 | .optional() 18 | .describe("The background color of the calendar"), 19 | foregroundColor: z 20 | .string() 21 | .optional() 22 | .describe("The foreground color of the calendar"), 23 | selected: z 24 | .boolean() 25 | .optional() 26 | .describe("Whether the calendar is selected"), 27 | accessRole: z.string().describe("The access role for the calendar"), 28 | primary: z 29 | .boolean() 30 | .optional() 31 | .describe("Whether this is the primary calendar"), 32 | location: z.string().optional().describe("The location of the calendar"), 33 | }), 34 | }); 35 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/google-calendar/tools/get-calendar/client.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { type getCalendarTool } from "./base"; 3 | import type { ClientToolConfig } from "@/toolkits/types"; 4 | import { VStack } from "@/components/ui/stack"; 5 | import { CalendarCard } from "../../components/calendar-card"; 6 | import { ToolCallComponent } from "../../components/tool-call"; 7 | 8 | export const googleCalendarGetCalendarToolConfigClient: ClientToolConfig< 9 | typeof getCalendarTool.inputSchema.shape, 10 | typeof getCalendarTool.outputSchema.shape 11 | > = { 12 | CallComponent: ({ args }) => { 13 | return ( 14 | <ToolCallComponent 15 | action="Fetching Calendar" 16 | primaryText={args.calendarId ?? ""} 17 | /> 18 | ); 19 | }, 20 | ResultComponent: ({ result }) => { 21 | return ( 22 | <VStack className="items-start gap-2"> 23 | <h3 className="text-sm font-medium">Calendar Details</h3> 24 | <CalendarCard calendar={result} showDetails={true} /> 25 | </VStack> 26 | ); 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/google-calendar/tools/get-calendar/server.ts: -------------------------------------------------------------------------------- 1 | import { type getCalendarTool } from "./base"; 2 | import { google } from "googleapis"; 3 | import type { ServerToolConfig } from "@/toolkits/types"; 4 | 5 | export const googleCalendarGetCalendarToolConfigServer = ( 6 | accessToken: string, 7 | ): ServerToolConfig< 8 | typeof getCalendarTool.inputSchema.shape, 9 | typeof getCalendarTool.outputSchema.shape 10 | > => { 11 | return { 12 | callback: async ({ calendarId }) => { 13 | const auth = new google.auth.OAuth2(); 14 | auth.setCredentials({ access_token: accessToken }); 15 | 16 | const calendar = google.calendar({ version: "v3", auth }); 17 | 18 | const response = await calendar.calendars.get({ 19 | calendarId, 20 | }); 21 | 22 | const cal = response.data; 23 | 24 | return { 25 | id: cal.id!, 26 | summary: cal.summary!, 27 | description: cal.description ?? undefined, 28 | timeZone: cal.timeZone!, 29 | colorId: cal.etag ?? undefined, 30 | backgroundColor: cal.etag ?? undefined, 31 | foregroundColor: cal.etag ?? undefined, 32 | selected: undefined, 33 | accessRole: "owner", 34 | primary: false, 35 | location: cal.location ?? undefined, 36 | }; 37 | }, 38 | }; 39 | }; 40 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/google-calendar/tools/get-event/client.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { type getEventTool } from "./base"; 3 | import type { ClientToolConfig } from "@/toolkits/types"; 4 | import { VStack } from "@/components/ui/stack"; 5 | import { EventCard } from "../../components/event-card"; 6 | import { ToolCallComponent } from "../../components/tool-call"; 7 | 8 | export const googleCalendarGetEventToolConfigClient: ClientToolConfig< 9 | typeof getEventTool.inputSchema.shape, 10 | typeof getEventTool.outputSchema.shape 11 | > = { 12 | CallComponent: ({ args }) => { 13 | return ( 14 | <ToolCallComponent 15 | action="Fetching Event" 16 | primaryText={args.eventId ?? ""} 17 | secondaryText={`From calendar: ${args.calendarId}`} 18 | /> 19 | ); 20 | }, 21 | ResultComponent: ({ result }) => { 22 | return ( 23 | <VStack className="items-start gap-2"> 24 | <h3 className="text-sm font-medium">Event Details</h3> 25 | <EventCard event={result} showDetails={true} /> 26 | </VStack> 27 | ); 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/google-calendar/tools/index.ts: -------------------------------------------------------------------------------- 1 | export enum GoogleCalendarTools { 2 | ListCalendars = "list-calendars", 3 | GetCalendar = "get-calendar", 4 | ListEvents = "list-events", 5 | GetEvent = "get-event", 6 | SearchEvents = "search-events", 7 | } 8 | 9 | export { listCalendarsTool } from "./list-calendars/base"; 10 | export { getCalendarTool } from "./get-calendar/base"; 11 | export { listEventsTool } from "./list-events/base"; 12 | export { getEventTool } from "./get-event/base"; 13 | export { searchEventsTool } from "./search-events/base"; 14 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/google-calendar/tools/list-calendars/base.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { createBaseTool } from "@/toolkits/create-tool"; 3 | 4 | export const listCalendarsTool = createBaseTool({ 5 | description: "List all calendars for the authenticated user", 6 | inputSchema: z.object({ 7 | maxResults: z 8 | .number() 9 | .describe("Maximum number of calendars to return (default: 100)"), 10 | pageToken: z 11 | .string() 12 | .describe("Token for pagination (use empty string for first page)"), 13 | }), 14 | outputSchema: z.object({ 15 | calendars: z.array( 16 | z.object({ 17 | id: z.string().describe("The calendar ID"), 18 | summary: z.string().describe("The calendar title"), 19 | description: z.string().optional().describe("The calendar description"), 20 | timeZone: z.string().describe("The time zone of the calendar"), 21 | colorId: z.string().optional().describe("The color ID of the calendar"), 22 | backgroundColor: z 23 | .string() 24 | .optional() 25 | .describe("The background color of the calendar"), 26 | foregroundColor: z 27 | .string() 28 | .optional() 29 | .describe("The foreground color of the calendar"), 30 | selected: z 31 | .boolean() 32 | .optional() 33 | .describe("Whether the calendar is selected"), 34 | accessRole: z.string().describe("The access role for the calendar"), 35 | primary: z 36 | .boolean() 37 | .optional() 38 | .describe("Whether this is the primary calendar"), 39 | }), 40 | ), 41 | nextPageToken: z 42 | .string() 43 | .optional() 44 | .describe("Token for next page of results"), 45 | }), 46 | }); 47 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/google-calendar/tools/list-calendars/server.ts: -------------------------------------------------------------------------------- 1 | import { type listCalendarsTool } from "./base"; 2 | import { google } from "googleapis"; 3 | import type { ServerToolConfig } from "@/toolkits/types"; 4 | 5 | export const googleCalendarListCalendarsToolConfigServer = ( 6 | accessToken: string, 7 | ): ServerToolConfig< 8 | typeof listCalendarsTool.inputSchema.shape, 9 | typeof listCalendarsTool.outputSchema.shape 10 | > => { 11 | return { 12 | callback: async ({ maxResults, pageToken }) => { 13 | const auth = new google.auth.OAuth2(); 14 | auth.setCredentials({ access_token: accessToken }); 15 | 16 | const calendar = google.calendar({ version: "v3", auth }); 17 | 18 | const response = await calendar.calendarList.list({ 19 | maxResults: maxResults, 20 | pageToken: pageToken, 21 | }); 22 | 23 | const calendars = 24 | response.data.items?.map((cal) => ({ 25 | id: cal.id!, 26 | summary: cal.summary!, 27 | description: cal.description ?? undefined, 28 | timeZone: cal.timeZone!, 29 | colorId: cal.colorId ?? undefined, 30 | backgroundColor: cal.backgroundColor ?? undefined, 31 | foregroundColor: cal.foregroundColor ?? undefined, 32 | selected: cal.selected ?? undefined, 33 | accessRole: cal.accessRole!, 34 | primary: cal.primary ?? undefined, 35 | })) ?? []; 36 | 37 | return { 38 | calendars, 39 | nextPageToken: response.data.nextPageToken ?? undefined, 40 | }; 41 | }, 42 | }; 43 | }; 44 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/google-calendar/tools/search-events/client.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { type searchEventsTool } from "./base"; 3 | import type { ClientToolConfig } from "@/toolkits/types"; 4 | import { HStack, VStack } from "@/components/ui/stack"; 5 | import { EventCard } from "../../components/event-card"; 6 | import { ToolCallComponent } from "../../components/tool-call"; 7 | 8 | export const googleCalendarSearchEventsToolConfigClient: ClientToolConfig< 9 | typeof searchEventsTool.inputSchema.shape, 10 | typeof searchEventsTool.outputSchema.shape 11 | > = { 12 | CallComponent: ({ args }) => { 13 | return ( 14 | <ToolCallComponent 15 | action="Searching Events" 16 | primaryText={`"${args.query}" (${args.maxResults ?? 5} events)`} 17 | secondaryText={`In calendar: ${args.calendarId}`} 18 | /> 19 | ); 20 | }, 21 | ResultComponent: ({ result }) => { 22 | const { events, timeZone } = result; 23 | 24 | if (events.length === 0) { 25 | return ( 26 | <p className="text-muted-foreground text-sm"> 27 | No events found matching your search 28 | </p> 29 | ); 30 | } 31 | 32 | return ( 33 | <VStack className="items-start gap-2"> 34 | <HStack className="items-center justify-between"> 35 | <h3 className="text-sm font-medium"> 36 | Search Results ({events.length}) 37 | </h3> 38 | {timeZone && ( 39 | <span className="text-muted-foreground text-xs">{timeZone}</span> 40 | )} 41 | </HStack> 42 | 43 | <div className="flex w-full flex-col gap-2"> 44 | {events.map((event) => ( 45 | <EventCard key={event.id} event={event} showDetails={true} /> 46 | ))} 47 | </div> 48 | </VStack> 49 | ); 50 | }, 51 | }; 52 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/google-calendar/tools/server.ts: -------------------------------------------------------------------------------- 1 | export { googleCalendarListCalendarsToolConfigServer } from "./list-calendars/server"; 2 | export { googleCalendarGetCalendarToolConfigServer } from "./get-calendar/server"; 3 | export { googleCalendarListEventsToolConfigServer } from "./list-events/server"; 4 | export { googleCalendarGetEventToolConfigServer } from "./get-event/server"; 5 | export { googleCalendarSearchEventsToolConfigServer } from "./search-events/server"; 6 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/google-drive/base.ts: -------------------------------------------------------------------------------- 1 | import type { ToolkitConfig } from "@/toolkits/types"; 2 | import { z } from "zod"; 3 | import { GoogleDriveTools } from "./tools"; 4 | import { searchFilesTool } from "./tools/search-files/base"; 5 | import { readFileTool } from "./tools/read-file/base"; 6 | 7 | export const googleDriveParameters = z.object({}); 8 | 9 | export const baseGoogleDriveToolkitConfig: ToolkitConfig< 10 | GoogleDriveTools, 11 | typeof googleDriveParameters.shape 12 | > = { 13 | tools: { 14 | [GoogleDriveTools.SearchFiles]: searchFilesTool, 15 | [GoogleDriveTools.ReadFile]: readFileTool, 16 | }, 17 | parameters: googleDriveParameters, 18 | }; 19 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/google-drive/components/tool-call.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { HStack, VStack } from "@/components/ui/stack"; 3 | import { Search } from "lucide-react"; 4 | 5 | interface ToolCallComponentProps { 6 | action: string; 7 | primaryText: string; 8 | secondaryText?: string; 9 | icon?: React.ComponentType<{ className?: string }>; 10 | } 11 | 12 | export const ToolCallComponent: React.FC<ToolCallComponentProps> = ({ 13 | action, 14 | primaryText, 15 | secondaryText, 16 | icon: Icon = Search, 17 | }) => { 18 | return ( 19 | <HStack className="gap-2"> 20 | <Icon className="text-muted-foreground size-4" /> 21 | <VStack className="items-start gap-0"> 22 | <span className="text-muted-foreground text-xs font-medium"> 23 | {action} 24 | </span> 25 | <span className="text-sm">{primaryText}</span> 26 | {secondaryText && ( 27 | <span className="text-muted-foreground text-xs">{secondaryText}</span> 28 | )} 29 | </VStack> 30 | </HStack> 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/google-drive/tools/client.ts: -------------------------------------------------------------------------------- 1 | export { googleDriveSearchFilesToolConfigClient } from "./search-files/client"; 2 | export { googleDriveReadFileToolConfigClient } from "./read-file/client"; 3 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/google-drive/tools/index.ts: -------------------------------------------------------------------------------- 1 | export enum GoogleDriveTools { 2 | SearchFiles = "search-files", 3 | ReadFile = "read-file", 4 | } 5 | 6 | export { searchFilesTool } from "./search-files/base"; 7 | export { readFileTool } from "./read-file/base"; 8 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/google-drive/tools/read-file/base.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { createBaseTool } from "@/toolkits/create-tool"; 3 | 4 | export const readFileTool = createBaseTool({ 5 | description: "Read the contents of a file from Google Drive", 6 | inputSchema: z.object({ 7 | fileId: z.string().describe("ID of the file to read"), 8 | exportFormat: z 9 | .string() 10 | .describe( 11 | "Export format for Google Workspace files (e.g., 'text/plain', 'text/csv', 'text/markdown') (leave blank for auto-detection)", 12 | ), 13 | }), 14 | outputSchema: z.object({ 15 | content: z.string().describe("File content as text"), 16 | mimeType: z.string().describe("MIME type of the content"), 17 | fileName: z.string().describe("Name of the file"), 18 | size: z.number().optional().describe("Size of the content in bytes"), 19 | encoding: z.string().optional().describe("Content encoding used"), 20 | }), 21 | }); 22 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/google-drive/tools/server.ts: -------------------------------------------------------------------------------- 1 | export { googleDriveSearchFilesToolConfigServer } from "./search-files/server"; 2 | export { googleDriveReadFileToolConfigServer } from "./read-file/server"; 3 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/image/base.ts: -------------------------------------------------------------------------------- 1 | import type { ToolkitConfig } from "@/toolkits/types"; 2 | import { ImageTools } from "./tools/tools"; 3 | import { baseGenerateTool } from "./tools/generate/base"; 4 | import { z } from "zod"; 5 | import { allImageModels } from "@/ai/models/all"; 6 | import type { ImageModelProvider } from "@/ai/types"; 7 | 8 | export const imageParameters = z.object({ 9 | model: z.enum( 10 | allImageModels.map((model) => `${model.provider}:${model.modelId}`) as [ 11 | `${ImageModelProvider}:${string}`, 12 | ...`${ImageModelProvider}:${string}`[], 13 | ], 14 | ), 15 | }); 16 | 17 | export const baseImageToolkitConfig: ToolkitConfig< 18 | ImageTools, 19 | typeof imageParameters.shape 20 | > = { 21 | tools: { 22 | [ImageTools.Generate]: baseGenerateTool, 23 | }, 24 | parameters: imageParameters, 25 | }; 26 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/image/server.ts: -------------------------------------------------------------------------------- 1 | import { createServerToolkit } from "@/toolkits/create-toolkit"; 2 | import { baseImageToolkitConfig } from "./base"; 3 | import { generateToolConfigServer } from "./tools/generate/server"; 4 | import { ImageTools } from "./tools/tools"; 5 | 6 | export const imageToolkitServer = createServerToolkit( 7 | baseImageToolkitConfig, 8 | `You have access to the Image toolkit for AI-powered image generation. This toolkit contains: 9 | 10 | - **Generate**: Create high-quality images from text descriptions using advanced AI models 11 | 12 | **Usage Guidelines:** 13 | 1. Use detailed, descriptive prompts for better image quality 14 | 2. Include style specifications (e.g., "photorealistic", "digital art", "cartoon style") 15 | 3. Specify composition details (e.g., "close-up", "wide shot", "bird's eye view") 16 | 4. Consider lighting and mood descriptions for enhanced results 17 | 5. For multiple related images, maintain consistent style and theme across generations 18 | 19 | This tool is perfect for creating visual content, illustrations, concept art, and any visual materials needed for your projects.`, 20 | async (parameters) => { 21 | return { 22 | [ImageTools.Generate]: generateToolConfigServer(parameters), 23 | }; 24 | }, 25 | ); 26 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/image/tools/generate/base.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { createBaseTool } from "@/toolkits/create-tool"; 3 | 4 | const inputSchema = z.object({ 5 | prompt: z.string().min(1).max(100).describe("The image generation prompt"), 6 | }); 7 | 8 | const outputSchema = z.object({ 9 | url: z.string().describe("The URL of the generated image"), 10 | }); 11 | 12 | export const baseGenerateTool = createBaseTool({ 13 | description: "Generate an image", 14 | inputSchema, 15 | outputSchema, 16 | }); 17 | 18 | export type GenerateToolArgs = z.infer<typeof inputSchema>; 19 | export type GenerateToolResult = z.infer<typeof outputSchema>; 20 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/image/tools/generate/client.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import Image from "next/image"; 4 | 5 | import type { baseGenerateTool } from "./base"; 6 | 7 | import type { ClientToolConfig } from "@/toolkits/types"; 8 | 9 | export const generateToolConfigClient: ClientToolConfig< 10 | typeof baseGenerateTool.inputSchema.shape, 11 | typeof baseGenerateTool.outputSchema.shape 12 | > = { 13 | CallComponent: ({ args }) => { 14 | return <span className="opacity/60 text-sm font-light">{args.prompt}</span>; 15 | }, 16 | ResultComponent: ({ result }) => { 17 | return ( 18 | <Image src={result.url} alt="Generated Image" width={500} height={500} /> 19 | ); 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/image/tools/generate/server.ts: -------------------------------------------------------------------------------- 1 | import { experimental_generateImage as generateImage } from "ai"; 2 | 3 | import { type baseGenerateTool } from "./base"; 4 | import type { ServerToolConfig } from "@/toolkits/types"; 5 | import { put } from "@vercel/blob"; 6 | import { api } from "@/trpc/server"; 7 | import type { imageParameters } from "../../base"; 8 | import type z from "zod"; 9 | import { registry } from "@/ai/registry"; 10 | 11 | export const generateToolConfigServer = ( 12 | parameters: z.infer<typeof imageParameters>, 13 | ): ServerToolConfig< 14 | typeof baseGenerateTool.inputSchema.shape, 15 | typeof baseGenerateTool.outputSchema.shape 16 | > => { 17 | return { 18 | callback: async ({ prompt }) => { 19 | const { image } = await generateImage({ 20 | model: registry.imageModel(parameters.model), 21 | prompt, 22 | }); 23 | 24 | if (!image) { 25 | console.error("No image generated"); 26 | throw new Error("No image generated"); 27 | } 28 | 29 | const imageId = crypto.randomUUID(); 30 | 31 | const file = new File( 32 | [image.uint8Array], 33 | `images/${imageId}.${image.mimeType.split("/")[1]}`, 34 | { 35 | type: image.mimeType, 36 | }, 37 | ); 38 | 39 | const { url: imageUrl } = await put(file.name, file, { 40 | access: "public", 41 | }); 42 | 43 | await api.images.createImage({ 44 | url: imageUrl, 45 | contentType: image.mimeType, 46 | }); 47 | 48 | return { url: imageUrl }; 49 | }, 50 | }; 51 | }; 52 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/image/tools/tools.ts: -------------------------------------------------------------------------------- 1 | export enum ImageTools { 2 | Generate = "generate", 3 | } 4 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/mem0/base.ts: -------------------------------------------------------------------------------- 1 | import type { ToolkitConfig } from "@/toolkits/types"; 2 | import { baseAddMemoryTool } from "./tools/add_memory/base"; 3 | import { baseSearchMemoriesTool } from "./tools/search_memories/base"; 4 | import { Mem0Tools } from "./tools/tools"; 5 | import { z } from "zod"; 6 | 7 | export const mem0Parameters = z.object({}); 8 | 9 | export const baseMem0ToolkitConfig: ToolkitConfig< 10 | Mem0Tools, 11 | typeof mem0Parameters.shape 12 | > = { 13 | tools: { 14 | [Mem0Tools.AddMemory]: baseAddMemoryTool, 15 | [Mem0Tools.SearchMemories]: baseSearchMemoriesTool, 16 | }, 17 | parameters: mem0Parameters, 18 | }; 19 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/mem0/client.tsx: -------------------------------------------------------------------------------- 1 | import { Brain } from "lucide-react"; 2 | 3 | import { Mem0Tools } from "./tools/tools"; 4 | import { createClientToolkit } from "@/toolkits/create-toolkit"; 5 | import { mem0AddMemoryToolConfigClient } from "./tools/add_memory/client"; 6 | import { mem0SearchMemoriesToolConfigClient } from "./tools/search_memories/client"; 7 | import { baseMem0ToolkitConfig } from "./base"; 8 | import { ToolkitGroups } from "@/toolkits/types"; 9 | 10 | export const mem0ClientToolkit = createClientToolkit( 11 | baseMem0ToolkitConfig, 12 | { 13 | name: "Memory", 14 | description: "Personalize your experience with each conversation.", 15 | icon: ({ className }) => <Brain className={className} />, 16 | form: null, 17 | type: ToolkitGroups.Native, 18 | }, 19 | { 20 | [Mem0Tools.AddMemory]: mem0AddMemoryToolConfigClient, 21 | [Mem0Tools.SearchMemories]: mem0SearchMemoriesToolConfigClient, 22 | }, 23 | ); 24 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/mem0/components/tool-call-display.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { type LucideIcon } from "lucide-react"; 3 | import { HStack } from "@/components/ui/stack"; 4 | 5 | interface ToolCallDisplayProps { 6 | icon: LucideIcon; 7 | label: string; 8 | value: string; 9 | } 10 | 11 | export function ToolCallDisplay({ 12 | icon: Icon, 13 | label, 14 | value, 15 | }: ToolCallDisplayProps) { 16 | return ( 17 | <HStack className="text-muted-foreground flex items-center"> 18 | <Icon className="size-4 shrink-0" /> 19 | <span className="text-sm"> 20 | <strong>{label}:</strong> {value} 21 | </span> 22 | </HStack> 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/mem0/tools/add_memory/base.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { createBaseTool } from "@/toolkits/create-tool"; 3 | 4 | export const baseAddMemoryTool = createBaseTool({ 5 | description: 6 | "Add a new memory. This method is called every time the user informs anything about themselves, their preferences, or anything that has any relevant information which can be useful in the future conversation. This can also be called when the user asks you to remember something.", 7 | inputSchema: z.object({ 8 | content: z.string().describe("The content to store in memory"), 9 | }), 10 | outputSchema: z.object({ 11 | success: z.boolean(), 12 | content: z.string(), 13 | }), 14 | }); 15 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/mem0/tools/add_memory/client.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Plus } from "lucide-react"; 3 | 4 | import type { baseAddMemoryTool } from "./base"; 5 | import type { ClientToolConfig } from "@/toolkits/types"; 6 | import { ToolCallDisplay } from "../../components/tool-call-display"; 7 | import { HStack, VStack } from "@/components/ui/stack"; 8 | 9 | export const mem0AddMemoryToolConfigClient: ClientToolConfig< 10 | typeof baseAddMemoryTool.inputSchema.shape, 11 | typeof baseAddMemoryTool.outputSchema.shape 12 | > = { 13 | CallComponent: ({ args }) => { 14 | return ( 15 | <div className="space-y-2"> 16 | <ToolCallDisplay 17 | icon={Plus} 18 | label="Adding Memory" 19 | value={args.content ?? "..."} 20 | /> 21 | </div> 22 | ); 23 | }, 24 | ResultComponent: ({ result }) => { 25 | return ( 26 | <div className="flex items-center gap-2"> 27 | {result.success ? ( 28 | <HStack className="text-primary flex items-center gap-2"> 29 | <Plus className="size-4 shrink-0" /> 30 | <VStack className="items-start gap-0"> 31 | <span className="text-xs font-medium">Memory Added</span> 32 | <span className="text-muted-foreground text-sm"> 33 | {result.content} 34 | </span> 35 | </VStack> 36 | </HStack> 37 | ) : ( 38 | <HStack className="text-destructive flex items-center gap-2"> 39 | <Plus className="size-4" /> 40 | <VStack className="items-start gap-0"> 41 | <span className="text-xs font-medium">Failed to add memory</span> 42 | <span className="text-muted-foreground text-sm"> 43 | {result.content} 44 | </span> 45 | </VStack> 46 | </HStack> 47 | )} 48 | </div> 49 | ); 50 | }, 51 | }; 52 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/mem0/tools/add_memory/server.ts: -------------------------------------------------------------------------------- 1 | import { type baseAddMemoryTool } from "./base"; 2 | import { env } from "@/env"; 3 | import { MemoryClient, type Message } from "mem0ai"; 4 | import type { ServerToolConfig } from "@/toolkits/types"; 5 | 6 | export const mem0AddMemoryToolConfigServer = ( 7 | userId: string, 8 | ): ServerToolConfig< 9 | typeof baseAddMemoryTool.inputSchema.shape, 10 | typeof baseAddMemoryTool.outputSchema.shape 11 | > => ({ 12 | callback: async ({ content }) => { 13 | if (!env.MEM0_API_KEY) { 14 | throw new Error("MEM0_API_KEY is not set"); 15 | } 16 | 17 | const memoryClient = new MemoryClient({ apiKey: env.MEM0_API_KEY }); 18 | 19 | try { 20 | const messages = [ 21 | { role: "assistant", content: "Memory storage system" }, 22 | { role: "user", content }, 23 | ] as Message[]; 24 | 25 | await memoryClient.add(messages, { user_id: userId }); 26 | 27 | return { 28 | success: true, 29 | content, 30 | }; 31 | } catch (error) { 32 | console.error("Error adding memory:", error); 33 | return { 34 | success: false, 35 | content: `Failed to add memory: ${error instanceof Error ? error.message : String(error)}`, 36 | }; 37 | } 38 | }, 39 | message: 40 | "Memory has been successfully stored and will be used to provide more personalized responses in future conversations.", 41 | }); 42 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/mem0/tools/search_memories/base.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { createBaseTool } from "@/toolkits/create-tool"; 3 | 4 | export const baseSearchMemoriesTool = createBaseTool({ 5 | description: 6 | "Search through stored memories. This method is called ANYTIME the user asks anything.", 7 | inputSchema: z.object({ 8 | query: z 9 | .string() 10 | .describe( 11 | "The search query. This is the query that the user has asked for. Example: 'What did I tell you about the weather last week?' or 'What did I tell you about my friend John?'", 12 | ), 13 | }), 14 | outputSchema: z.object({ 15 | memories: z.array( 16 | z.object({ 17 | memory: z.string(), 18 | score: z.number(), 19 | metadata: z.record(z.any()).optional(), 20 | }), 21 | ), 22 | query: z.string(), 23 | }), 24 | }); 25 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/mem0/tools/search_memories/server.ts: -------------------------------------------------------------------------------- 1 | import { type baseSearchMemoriesTool } from "./base"; 2 | import { env } from "@/env"; 3 | import { MemoryClient } from "mem0ai"; 4 | import type { ServerToolConfig } from "@/toolkits/types"; 5 | 6 | export const mem0SearchMemoriesToolConfigServer = ( 7 | userId: string, 8 | ): ServerToolConfig< 9 | typeof baseSearchMemoriesTool.inputSchema.shape, 10 | typeof baseSearchMemoriesTool.outputSchema.shape 11 | > => ({ 12 | callback: async ({ query }) => { 13 | if (!env.MEM0_API_KEY) { 14 | throw new Error("MEM0_API_KEY is not set"); 15 | } 16 | 17 | const memoryClient = new MemoryClient({ apiKey: env.MEM0_API_KEY }); 18 | 19 | try { 20 | const results = await memoryClient.search(query, { user_id: userId }); 21 | 22 | return { 23 | memories: results.map((result) => ({ 24 | memory: result.memory ?? "", 25 | score: result.score ?? 0, 26 | metadata: result.metadata as object, 27 | })), 28 | query, 29 | }; 30 | } catch (error) { 31 | console.error("Error searching memories:", error); 32 | return { 33 | memories: [], 34 | query, 35 | }; 36 | } 37 | }, 38 | message: 39 | "These are the relevant memories found. Use this information to provide contextual and personalized responses to the user.", 40 | }); 41 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/mem0/tools/tools.ts: -------------------------------------------------------------------------------- 1 | export enum Mem0Tools { 2 | AddMemory = "add-memory", 3 | SearchMemories = "search-memories", 4 | } 5 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/notion/base.ts: -------------------------------------------------------------------------------- 1 | import type { ToolkitConfig } from "@/toolkits/types"; 2 | import { z } from "zod"; 3 | import { NotionTools } from "./tools"; 4 | import { 5 | listDatabasesTool, 6 | queryDatabaseTool, 7 | createDatabaseTool, 8 | getPageTool, 9 | searchPagesTool, 10 | createPageTool, 11 | getBlocksTool, 12 | appendBlocksTool, 13 | listUsersTool, 14 | } from "./tools"; 15 | 16 | export const notionParameters = z.object({}); 17 | 18 | export const baseNotionToolkitConfig: ToolkitConfig< 19 | NotionTools, 20 | typeof notionParameters.shape 21 | > = { 22 | tools: { 23 | [NotionTools.ListDatabases]: listDatabasesTool, 24 | [NotionTools.QueryDatabase]: queryDatabaseTool, 25 | [NotionTools.CreateDatabase]: createDatabaseTool, 26 | [NotionTools.GetPage]: getPageTool, 27 | [NotionTools.SearchPages]: searchPagesTool, 28 | [NotionTools.CreatePage]: createPageTool, 29 | [NotionTools.GetBlocks]: getBlocksTool, 30 | [NotionTools.AppendBlocks]: appendBlocksTool, 31 | [NotionTools.ListUsers]: listUsersTool, 32 | }, 33 | parameters: notionParameters, 34 | }; 35 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/notion/components/database.tsx: -------------------------------------------------------------------------------- 1 | import { HStack, VStack } from "@/components/ui/stack"; 2 | import type { DatabaseObjectResponse } from "@notionhq/client/build/src/api-endpoints"; 3 | import { NotionPageIcon } from "./icon"; 4 | import { Database } from "lucide-react"; 5 | 6 | export const NotionDatabase: React.FC<{ 7 | database: DatabaseObjectResponse; 8 | onClick?: () => void; 9 | }> = ({ database, onClick }) => { 10 | return ( 11 | <HStack 12 | key={database.id} 13 | className="group w-full cursor-pointer justify-between border-b py-2 last:border-b-0 last:pb-0" 14 | onClick={() => { 15 | onClick?.(); 16 | }} 17 | > 18 | <HStack> 19 | <NotionPageIcon 20 | icon={database.icon} 21 | defaultIcon={<Database className="size-4 shrink-0" />} 22 | /> 23 | <VStack className="group flex w-full cursor-pointer items-start gap-0"> 24 | <h3 className="group-hover:text-primary line-clamp-2 transition-colors"> 25 | {database.title?.[0]?.plain_text ?? "Untitled Database"} 26 | </h3> 27 | {database.description?.[0]?.plain_text && ( 28 | <p className="text-muted-foreground text-xs"> 29 | {database.description?.[0]?.plain_text ?? "No description"} 30 | </p> 31 | )} 32 | </VStack> 33 | </HStack> 34 | <p className="text-muted-foreground/60 text-xs"> 35 | Updated {new Date(database.last_edited_time).toLocaleDateString()} 36 | </p> 37 | </HStack> 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/notion/components/icon.tsx: -------------------------------------------------------------------------------- 1 | import type { PageObjectResponse } from "@notionhq/client/build/src/api-endpoints"; 2 | import { FileText } from "lucide-react"; 3 | 4 | interface Props { 5 | icon: PageObjectResponse["icon"]; 6 | defaultIcon?: React.ReactNode; 7 | } 8 | 9 | export const NotionPageIcon: React.FC<Props> = ({ 10 | icon, 11 | defaultIcon = <FileText className="size-4" />, 12 | }) => { 13 | if (!icon) return defaultIcon; 14 | 15 | if (icon.type === "external") { 16 | return ( 17 | // eslint-disable-next-line @next/next/no-img-element 18 | <img src={icon.external.url} alt={icon.external.url} className="size-4" /> 19 | ); 20 | } 21 | 22 | if (icon.type === "file") { 23 | // eslint-disable-next-line @next/next/no-img-element 24 | return <img src={icon.file.url} alt={icon.file.url} className="size-4" />; 25 | } 26 | 27 | if (icon.type === "emoji") { 28 | return <span className="size-4 text-sm">{icon.emoji}</span>; 29 | } 30 | 31 | if (icon.type === "custom_emoji") { 32 | return ( 33 | // eslint-disable-next-line @next/next/no-img-element 34 | <img 35 | src={icon.custom_emoji.url} 36 | alt={icon.custom_emoji.url} 37 | className="size-4" 38 | /> 39 | ); 40 | } 41 | 42 | return null; 43 | }; 44 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/notion/components/index.ts: -------------------------------------------------------------------------------- 1 | export { ToolCallDisplay } from "./tool-call-display"; 2 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/notion/components/page.tsx: -------------------------------------------------------------------------------- 1 | import { HStack } from "@/components/ui/stack"; 2 | import type { PageObjectResponse } from "@notionhq/client/build/src/api-endpoints"; 3 | import { getTitle } from "../lib/title"; 4 | import { NotionPageIcon } from "./icon"; 5 | import Link from "next/link"; 6 | import { ExternalLink } from "lucide-react"; 7 | 8 | export const NotionPage: React.FC<{ 9 | page: PageObjectResponse; 10 | onClick?: () => void; 11 | }> = ({ page, onClick }) => { 12 | return ( 13 | <HStack 14 | className="group w-full cursor-pointer justify-between border-b py-2 last:border-b-0 last:pb-0" 15 | onClick={onClick} 16 | > 17 | <HStack> 18 | <NotionPageIcon icon={page.icon} /> 19 | <HStack> 20 | <h3 className="group-hover:text-primary line-clamp-2 transition-colors"> 21 | {getTitle(page)} 22 | </h3> 23 | <Link 24 | href={page.url} 25 | target="_blank" 26 | className="text-xs" 27 | onClick={(e) => e.stopPropagation()} 28 | > 29 | <ExternalLink className="hover:text-primary size-3" /> 30 | </Link> 31 | </HStack> 32 | </HStack> 33 | 34 | <span className="text-muted-foreground/60 text-xs"> 35 | Created {new Date(page.created_time).toLocaleDateString()} 36 | </span> 37 | </HStack> 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/notion/components/tool-call-display.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { HStack, VStack } from "@/components/ui/stack"; 3 | 4 | interface ToolCallDisplayProps { 5 | icon: React.ComponentType<{ className?: string }>; 6 | label: string; 7 | value: string; 8 | } 9 | 10 | export const ToolCallDisplay: React.FC<ToolCallDisplayProps> = ({ 11 | icon: Icon, 12 | label, 13 | value, 14 | }) => { 15 | return ( 16 | <HStack className="gap-2"> 17 | <Icon className="text-muted-foreground size-4" /> 18 | <VStack className="items-start gap-0"> 19 | <span className="text-muted-foreground/80 text-xs font-medium"> 20 | {label} 21 | </span> 22 | <span className="text-sm">{value}</span> 23 | </VStack> 24 | </HStack> 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/notion/lib/title.ts: -------------------------------------------------------------------------------- 1 | import type { PageObjectResponse } from "@notionhq/client/build/src/api-endpoints"; 2 | 3 | export const getTitle = (page: PageObjectResponse) => { 4 | if ( 5 | "title" in page.properties && 6 | page.properties.title?.type === "title" && 7 | page.properties.title.title?.[0]?.plain_text 8 | ) { 9 | return page.properties.title.title[0].plain_text; 10 | } 11 | 12 | const urlTitle = 13 | page.url.split("/").pop()?.split("-").slice(0, -1).join(" ") ?? ""; 14 | 15 | if (urlTitle) { 16 | return urlTitle; 17 | } 18 | 19 | return "Untitled Page"; 20 | }; 21 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/notion/tools/blocks/base.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { createBaseTool } from "@/toolkits/create-tool"; 3 | import type { BlockObjectResponse } from "@notionhq/client/build/src/api-endpoints"; 4 | 5 | export const getBlocksTool = createBaseTool({ 6 | description: "Retrieve block content from a page or block", 7 | inputSchema: z.object({ 8 | block_id: z 9 | .string() 10 | .describe("The ID of the block or page to retrieve children from"), 11 | start_cursor: z 12 | .string() 13 | .describe("Pagination cursor to start from (blank for first page)"), 14 | page_size: z 15 | .number() 16 | .max(100) 17 | .describe("Number of results per page (max 100, default 10)"), 18 | }), 19 | outputSchema: z.object({ 20 | results: z.array(z.custom<BlockObjectResponse>()), 21 | has_more: z.boolean(), 22 | next_cursor: z.string().optional(), 23 | }), 24 | }); 25 | 26 | export const appendBlocksTool = createBaseTool({ 27 | description: "Append new blocks to a page or existing block", 28 | inputSchema: z.object({ 29 | block_id: z 30 | .string() 31 | .describe("The ID of the parent block or page to append blocks to"), 32 | content: z 33 | .string() 34 | .describe("Markdown content to convert and append as blocks"), 35 | block_type: z 36 | .enum([ 37 | "paragraph", 38 | "heading_1", 39 | "heading_2", 40 | "heading_3", 41 | "bulleted_list_item", 42 | "numbered_list_item", 43 | "to_do", 44 | "code", 45 | ]) 46 | .describe("Type of block to create (defaults to paragraph)"), 47 | }), 48 | outputSchema: z.object({ 49 | results: z.array(z.custom<BlockObjectResponse>()), 50 | }), 51 | }); 52 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/notion/tools/client.ts: -------------------------------------------------------------------------------- 1 | export { 2 | notionListDatabasesToolConfigClient, 3 | notionQueryDatabaseToolConfigClient, 4 | notionCreateDatabaseToolConfigClient, 5 | } from "./databases/client"; 6 | 7 | export { 8 | notionGetPageToolConfigClient, 9 | notionSearchPagesToolConfigClient, 10 | notionCreatePageToolConfigClient, 11 | } from "./pages/client"; 12 | 13 | export { 14 | notionGetBlocksToolConfigClient, 15 | notionAppendBlocksToolConfigClient, 16 | } from "./blocks/client"; 17 | 18 | export { notionListUsersToolConfigClient } from "./users/client"; 19 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/notion/tools/index.ts: -------------------------------------------------------------------------------- 1 | export enum NotionTools { 2 | ListDatabases = "list-databases", 3 | QueryDatabase = "query-database", 4 | CreateDatabase = "create-database", 5 | GetPage = "get-page", 6 | SearchPages = "search-pages", 7 | CreatePage = "create-page", 8 | GetBlocks = "get-blocks", 9 | AppendBlocks = "append-blocks", 10 | ListUsers = "list-users", 11 | } 12 | 13 | export { 14 | listDatabasesTool, 15 | queryDatabaseTool, 16 | createDatabaseTool, 17 | } from "./databases/base"; 18 | export { getPageTool, searchPagesTool, createPageTool } from "./pages/base"; 19 | export { getBlocksTool, appendBlocksTool } from "./blocks/base"; 20 | export { listUsersTool } from "./users/base"; 21 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/notion/tools/server.ts: -------------------------------------------------------------------------------- 1 | export { 2 | notionListDatabasesToolConfigServer, 3 | notionQueryDatabaseToolConfigServer, 4 | notionCreateDatabaseToolConfigServer, 5 | } from "./databases/server"; 6 | 7 | export { 8 | notionGetPageToolConfigServer, 9 | notionSearchPagesToolConfigServer, 10 | notionCreatePageToolConfigServer, 11 | } from "./pages/server"; 12 | 13 | export { 14 | notionGetBlocksToolConfigServer, 15 | notionAppendBlocksToolConfigServer, 16 | } from "./blocks/server"; 17 | 18 | export { notionListUsersToolConfigServer } from "./users/server"; 19 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/notion/tools/users/base.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { createBaseTool } from "@/toolkits/create-tool"; 3 | import type { UserObjectResponse } from "@notionhq/client/build/src/api-endpoints"; 4 | 5 | export const listUsersTool = createBaseTool({ 6 | description: "List all users in the workspace", 7 | inputSchema: z.object({ 8 | start_cursor: z 9 | .string() 10 | .describe("Pagination cursor to start from (blank for first page)"), 11 | page_size: z 12 | .number() 13 | .max(100) 14 | .describe("Number of results per page (max 100, default 10)"), 15 | }), 16 | outputSchema: z.object({ 17 | results: z.array(z.custom<UserObjectResponse>()), 18 | has_more: z.boolean(), 19 | next_cursor: z.string().optional(), 20 | }), 21 | }); 22 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/notion/tools/users/server.ts: -------------------------------------------------------------------------------- 1 | import type { Client } from "@notionhq/client"; 2 | import type { ServerToolConfig } from "@/toolkits/types"; 3 | import type { listUsersTool } from "./base"; 4 | 5 | export const notionListUsersToolConfigServer = ( 6 | notion: Client, 7 | ): ServerToolConfig< 8 | typeof listUsersTool.inputSchema.shape, 9 | typeof listUsersTool.outputSchema.shape 10 | > => { 11 | return { 12 | callback: async ({ start_cursor, page_size = 100 }) => { 13 | try { 14 | const response = await notion.users.list({ 15 | start_cursor: start_cursor || undefined, 16 | page_size, 17 | }); 18 | 19 | return { 20 | results: response.results, 21 | has_more: response.has_more, 22 | next_cursor: response.next_cursor ?? undefined, 23 | }; 24 | } catch (error) { 25 | console.error("Notion API error:", error); 26 | throw new Error("Failed to retrieve users from Notion"); 27 | } 28 | }, 29 | message: 30 | "Successfully retrieved workspace users. The user is shown the responses in the UI. Do not reiterate them. If you called this tool because the user asked a question, answer the question.", 31 | }; 32 | }; 33 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/server.ts: -------------------------------------------------------------------------------- 1 | import type { ServerToolkit } from "../types"; 2 | import { exaToolkitServer } from "./exa/server"; 3 | import { githubToolkitServer } from "./github/server"; 4 | import { googleCalendarToolkitServer } from "./google-calendar/server"; 5 | import { googleDriveToolkitServer } from "./google-drive/server"; 6 | import { imageToolkitServer } from "./image/server"; 7 | import { mem0ToolkitServer } from "./mem0/server"; 8 | import { notionToolkitServer } from "./notion/server"; 9 | import { e2bToolkitServer } from "./e2b/server"; 10 | import { 11 | Toolkits, 12 | type ServerToolkitNames, 13 | type ServerToolkitParameters, 14 | } from "./shared"; 15 | 16 | type ServerToolkits = { 17 | [K in Toolkits]: ServerToolkit< 18 | ServerToolkitNames[K], 19 | ServerToolkitParameters[K] 20 | >; 21 | }; 22 | 23 | export const serverToolkits: ServerToolkits = { 24 | [Toolkits.Exa]: exaToolkitServer, 25 | [Toolkits.Image]: imageToolkitServer, 26 | [Toolkits.Github]: githubToolkitServer, 27 | [Toolkits.GoogleCalendar]: googleCalendarToolkitServer, 28 | [Toolkits.GoogleDrive]: googleDriveToolkitServer, 29 | [Toolkits.Memory]: mem0ToolkitServer, 30 | [Toolkits.Notion]: notionToolkitServer, 31 | [Toolkits.E2B]: e2bToolkitServer, 32 | }; 33 | 34 | export function getServerToolkit<T extends Toolkits>( 35 | server: T, 36 | ): ServerToolkit<ServerToolkitNames[T], ServerToolkitParameters[T]> { 37 | return serverToolkits[server]; 38 | } 39 | -------------------------------------------------------------------------------- /src/toolkits/toolkits/toolkit-types.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonhedman/toolkit.dev/6bb4636d55ffcdf152fa21e668c65ca4a5266a73/src/toolkits/toolkits/toolkit-types.ts -------------------------------------------------------------------------------- /src/trpc/query-client.ts: -------------------------------------------------------------------------------- 1 | import { 2 | defaultShouldDehydrateQuery, 3 | QueryClient, 4 | } from "@tanstack/react-query"; 5 | import SuperJSON from "superjson"; 6 | 7 | export const createQueryClient = () => 8 | new QueryClient({ 9 | defaultOptions: { 10 | queries: { 11 | // With SSR, we usually want to set some default staleTime 12 | // above 0 to avoid refetching immediately on the client 13 | staleTime: 30 * 1000, 14 | }, 15 | dehydrate: { 16 | serializeData: SuperJSON.serialize, 17 | shouldDehydrateQuery: (query) => 18 | defaultShouldDehydrateQuery(query) || 19 | query.state.status === "pending", 20 | }, 21 | hydrate: { 22 | deserializeData: SuperJSON.deserialize, 23 | }, 24 | }, 25 | }); 26 | -------------------------------------------------------------------------------- /src/trpc/server.ts: -------------------------------------------------------------------------------- 1 | import "server-only"; 2 | 3 | import { createHydrationHelpers } from "@trpc/react-query/rsc"; 4 | import { headers } from "next/headers"; 5 | import { cache } from "react"; 6 | 7 | import { createCaller, type AppRouter } from "@/server/api/root"; 8 | import { createTRPCContext } from "@/server/api/trpc"; 9 | import { createQueryClient } from "./query-client"; 10 | 11 | /** 12 | * This wraps the `createTRPCContext` helper and provides the required context for the tRPC API when 13 | * handling a tRPC call from a React Server Component. 14 | */ 15 | const createContext = cache(async () => { 16 | const heads = new Headers(await headers()); 17 | heads.set("x-trpc-source", "rsc"); 18 | 19 | return createTRPCContext({ 20 | headers: heads, 21 | }); 22 | }); 23 | 24 | const getQueryClient = cache(createQueryClient); 25 | const caller = createCaller(createContext); 26 | 27 | export const { trpc: api, HydrateClient } = createHydrationHelpers<AppRouter>( 28 | caller, 29 | getQueryClient, 30 | ); 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Base Options: */ 4 | "esModuleInterop": true, 5 | "skipLibCheck": true, 6 | "target": "es2022", 7 | "allowJs": true, 8 | "resolveJsonModule": true, 9 | "moduleDetection": "force", 10 | "isolatedModules": true, 11 | "verbatimModuleSyntax": true, 12 | 13 | /* Strictness */ 14 | "strict": true, 15 | "noUncheckedIndexedAccess": true, 16 | "checkJs": true, 17 | 18 | /* Bundled projects */ 19 | "lib": ["dom", "dom.iterable", "ES2022"], 20 | "noEmit": true, 21 | "module": "ESNext", 22 | "moduleResolution": "Bundler", 23 | "jsx": "preserve", 24 | "plugins": [{ "name": "next" }], 25 | "incremental": true, 26 | 27 | /* Path Aliases */ 28 | "baseUrl": ".", 29 | "paths": { 30 | "@/*": ["./src/*"] 31 | } 32 | }, 33 | "include": [ 34 | "next-env.d.ts", 35 | "**/*.ts", 36 | "**/*.tsx", 37 | "**/*.cjs", 38 | "**/*.js", 39 | ".next/types/**/*.ts" 40 | ], 41 | "exclude": ["node_modules"] 42 | } 43 | --------------------------------------------------------------------------------