├── .dockerignore ├── .env.example ├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ └── docker-image.yml ├── .gitignore ├── .vscode └── settings.json ├── Dockerfile ├── LICENSE.txt ├── README.ja.md ├── README.md ├── README.zh_CN.md ├── README.zh_HK.md ├── app ├── [locale] │ ├── (home) │ │ ├── (chat) │ │ │ └── page.tsx │ │ ├── action.tsx │ │ ├── layout.tsx │ │ ├── provider.tsx │ │ ├── search │ │ │ └── page.tsx │ │ └── sidebar.tsx │ ├── layout.tsx │ └── provider.tsx ├── api │ ├── app │ │ └── latest │ │ │ └── route.ts │ ├── chat │ │ └── messages │ │ │ ├── amazon │ │ │ └── route.ts │ │ │ ├── anthropic │ │ │ └── route.ts │ │ │ ├── azure │ │ │ └── route.ts │ │ │ ├── cohere │ │ │ └── route.ts │ │ │ ├── fireworks │ │ │ └── route.ts │ │ │ ├── google │ │ │ └── route.ts │ │ │ ├── groq │ │ │ └── route.ts │ │ │ ├── huggingface │ │ │ └── route.ts │ │ │ ├── mistral │ │ │ └── route.ts │ │ │ ├── openai │ │ │ └── route.ts │ │ │ ├── perplexity │ │ │ └── route.ts │ │ │ └── route.ts │ └── search │ │ ├── google │ │ └── route.ts │ │ └── route.ts ├── favicon.ico ├── layout.tsx ├── not-found.tsx └── provider.tsx ├── components.json ├── components ├── layout │ ├── add-button.tsx │ ├── brand.tsx │ ├── chat │ │ ├── conversation-window.tsx │ │ └── input-box.tsx │ ├── history-list.tsx │ ├── language-dropdown.tsx │ ├── lightbox.tsx │ ├── message.tsx │ ├── model-select.tsx │ ├── search-select.tsx │ ├── search │ │ ├── block │ │ │ ├── additions.tsx │ │ │ ├── answer.tsx │ │ │ ├── ask-follow-up-question.tsx │ │ │ ├── block-title.tsx │ │ │ ├── clarifier.tsx │ │ │ ├── error.tsx │ │ │ ├── focus-point.tsx │ │ │ ├── images.tsx │ │ │ ├── question.tsx │ │ │ ├── related.tsx │ │ │ ├── searching.tsx │ │ │ ├── sources.tsx │ │ │ └── try-ask.tsx │ │ ├── input-box.tsx │ │ └── search-window.tsx │ ├── settings-dialog.tsx │ ├── settings-drawer.tsx │ ├── settings │ │ ├── general.tsx │ │ ├── provider.tsx │ │ ├── provider │ │ │ ├── amazon.tsx │ │ │ ├── anthropic.tsx │ │ │ ├── azure.tsx │ │ │ ├── cohere.tsx │ │ │ ├── custom.tsx │ │ │ ├── fireworks.tsx │ │ │ ├── google.tsx │ │ │ ├── groq.tsx │ │ │ ├── huggingface.tsx │ │ │ ├── mistral.tsx │ │ │ ├── openai.tsx │ │ │ └── perplexity.tsx │ │ ├── search.tsx │ │ └── searcher │ │ │ └── tavily.tsx │ ├── share-button.tsx │ ├── theme-dropdown.tsx │ ├── user-avatar.tsx │ ├── version-badge.tsx │ └── version-label.tsx └── ui │ ├── avatar.tsx │ ├── badge.tsx │ ├── button.tsx │ ├── checkbox.tsx │ ├── custom │ ├── checkbox.tsx │ ├── dialog.tsx │ ├── drawer.tsx │ ├── dropdown-menu.tsx │ ├── input.tsx │ ├── select.tsx │ ├── switch.tsx │ └── textarea.tsx │ ├── dialog.tsx │ ├── drawer.tsx │ ├── dropdown-menu.tsx │ ├── input.tsx │ ├── label.tsx │ ├── select.tsx │ ├── switch.tsx │ ├── tabs.tsx │ └── textarea.tsx ├── config ├── i18n │ └── index.ts ├── provider │ ├── amazon.ts │ ├── anthropic.ts │ ├── azure.ts │ ├── cohere.ts │ ├── fireworks.ts │ ├── google.ts │ ├── groq.ts │ ├── huggingface.ts │ ├── index.ts │ ├── mistral.ts │ ├── openai.ts │ └── perplexity.ts ├── search │ ├── index.ts │ └── question.ts └── theme │ └── index.ts ├── docker-compose.yml ├── hooks ├── storage.ts ├── store.ts ├── theme.tsx └── window.ts ├── i18n.ts ├── lib ├── prompt │ └── index.ts ├── provider │ ├── Anthropic.ts │ ├── Google.ts │ └── OpenAI.ts ├── search │ ├── challenger.tsx │ ├── clarifier.tsx │ ├── illustrator.tsx │ └── searcher.tsx └── ui │ └── utils.ts ├── locales ├── de.json ├── en.json ├── es.json ├── fr.json ├── it.json ├── ja.json ├── ko.json ├── nl.json ├── pt.json ├── ru.json ├── zh-CN.json ├── zh-HK.json └── zh-TW.json ├── middleware.ts ├── next.config.mjs ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── prettier.config.js ├── public ├── OpenAI.svg ├── favicon.ico ├── hero.png ├── icon.svg ├── icons │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── browserconfig.xml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── mstile-150x150.png │ └── safari-pinned-tab.svg ├── img │ ├── Amazon.png │ ├── Anthropic.png │ ├── Azure.png │ ├── Cohere.png │ ├── Custom.png │ ├── Fireworks.png │ ├── Google.png │ ├── Groq.png │ ├── HuggingFace.png │ ├── Mistral.png │ ├── OpenAI.png │ ├── Perplexity.png │ ├── Replicate.png │ ├── Tavily.png │ └── Team.png └── manifest.json ├── renovate.json ├── styles └── globals.css ├── tailwind.config.ts ├── tsconfig.json ├── types ├── app.ts ├── conversation.ts ├── i18n.ts ├── model.ts ├── search.ts ├── search │ └── resources.ts └── settings.ts └── utils ├── app ├── time.ts ├── uuid.ts └── version.ts ├── provider ├── cohere.tsx └── google.tsx └── search ├── engines ├── google.ts ├── tavily.ts └── you.ts └── image.ts /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.classpath 2 | **/.dockerignore 3 | **/.env 4 | **/.git 5 | **/.gitignore 6 | **/.project 7 | **/.settings 8 | **/.toolstarget 9 | **/.vs 10 | **/.vscode 11 | **/*.*proj.user 12 | **/*.dbmdl 13 | **/*.jfm 14 | **/charts 15 | **/docker-compose* 16 | **/compose* 17 | **/Dockerfile* 18 | **/node_modules 19 | **/npm-debug.log 20 | **/obj 21 | **/secrets.dev.yaml 22 | **/values.dev.yaml 23 | LICENSE 24 | README.md 25 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # --------------- Providers ----------------- 2 | 3 | ## Amazon (not supported so far) 4 | NEXT_PUBLIC_ACCESS_AWS= 5 | AWS_ACCESS_KEY= 6 | AWS_SECRET_KEY= 7 | AWS_REGION= 8 | 9 | ## Anthropic 10 | NEXT_PUBLIC_ACCESS_ANTHROPIC= 11 | ANTHROPIC_API_KEY= 12 | 13 | ## Azure (not supported so far) 14 | NEXT_PUBLIC_ACCESS_AZURE= 15 | AZURE_OPENAI_API_KEY= 16 | AZURE_OPENAI_ENDPOINT= 17 | AZURE_OPENAI_DEPLOY_INSTANCE_NAME= 18 | 19 | ## Cohere 20 | NEXT_PUBLIC_ACCESS_COHERE= 21 | COHERE_API_KEY= 22 | 23 | ## Fireworks 24 | NEXT_PUBLIC_ACCESS_FIREWORKS= 25 | FIREWORKS_API_KEY= 26 | 27 | ## Google 28 | NEXT_PUBLIC_ACCESS_GOOGLE= 29 | GOOGLE_API_KEY= 30 | 31 | ## Groq 32 | NEXT_PUBLIC_ACCESS_GROQ= 33 | GROQ_API_KEY= 34 | 35 | ## Hugging Face 36 | NEXT_PUBLIC_ACCESS_HUGGINGFACE= 37 | HUGGINGFACE_API_KEY= 38 | 39 | ## Mistral 40 | NEXT_PUBLIC_ACCESS_MISTRAL= 41 | MISTRAL_API_KEY= 42 | 43 | ## OpenAI 44 | NEXT_PUBLIC_ACCESS_OPENAI= 45 | OPENAI_API_KEY= 46 | OPENAI_API_ENDPOINT= 47 | 48 | ## Perplexity 49 | NEXT_PUBLIC_ACCESS_PERPLEXITY= 50 | PERPLEXITY_API_KEY= 51 | PERPLEXITY_ENDPOINT= 52 | 53 | # -------------- Search Engines -------------- 54 | 55 | ## Google 56 | NEXT_PUBLIC_ACCESS_GOOGLE_SEARCH= 57 | GOOGLE_SEARCH_API_KEY= 58 | GOOGLE_SEARCH_ENGINE_ID= 59 | 60 | ## Tavily 61 | NEXT_PUBLIC_ACCESS_TAVILY_SEARCH= 62 | TAVILY_SEARCH_API_KEY= 63 | 64 | ## You 65 | NEXT_PUBLIC_ACCESS_YOU_SEARCH= 66 | YOU_SEARCH_API_KEY= 67 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | components/ui/**.tsx 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": [ 4 | "next", 5 | "next/core-web-vitals", 6 | "plugin:tailwindcss/recommended" 7 | ], 8 | "plugins": [ 9 | "react", 10 | "simple-import-sort", 11 | "unused-imports" 12 | ], 13 | "parserOptions": { 14 | "sourceType": "module", 15 | "ecmaVersion": "latest" 16 | }, 17 | "rules": { 18 | "simple-import-sort/imports": "error", 19 | "simple-import-sort/exports": "error", 20 | "unused-imports/no-unused-imports": "error", 21 | "unused-imports/no-unused-vars": [ 22 | "warn", 23 | { 24 | "vars": "all", 25 | "varsIgnorePattern": "^_", 26 | "args": "after-used", 27 | "argsIgnorePattern": "^_" 28 | } 29 | ], 30 | "no-console": "warn", 31 | "react/no-unescaped-entities": "off" 32 | }, 33 | "overrides": [ 34 | { 35 | "files": [ 36 | "*.ts", 37 | "*.tsx", 38 | "*.js" 39 | ], 40 | "parser": "@typescript-eslint/parser" 41 | }, 42 | { 43 | "files": [ 44 | "*.js", 45 | "*.jsx", 46 | "*.ts", 47 | "*.tsx" 48 | ], 49 | "rules": { 50 | "simple-import-sort/imports": [ 51 | "error", 52 | { 53 | "groups": [ 54 | [ 55 | "^react", 56 | "^@?\\w" 57 | ], 58 | [ 59 | "^(@|components)(/.*|$)" 60 | ], 61 | [ 62 | "^\\u0000" 63 | ], 64 | [ 65 | "^\\.\\.(?!/?$)", 66 | "^\\.\\./?$" 67 | ], 68 | [ 69 | "^\\./(?=.*/)(?!/?$)", 70 | "^\\.(?!/?$)", 71 | "^\\./?$" 72 | ], 73 | [ 74 | "^.+\\.?(css)$" 75 | ] 76 | ] 77 | } 78 | ] 79 | } 80 | } 81 | ] 82 | } -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | name: ChatChat Version Docker Image CI 2 | 3 | on: 4 | release: 5 | types: [created] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build-and-publish: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | 16 | - name: Set up Docker Buildx 17 | uses: docker/setup-buildx-action@v3 18 | 19 | - name: Login to GitHub Container Registry 20 | uses: docker/login-action@v3 21 | with: 22 | registry: ghcr.io 23 | username: ${{ github.actor }} 24 | password: ${{ secrets.GHCR_TOKEN }} 25 | 26 | - name: Extract tag name 27 | id: extract_tag 28 | run: echo "##[set-output name=tag;]$(echo ${GITHUB_REF#refs/tags/})" 29 | 30 | - name: Build and Push Docker image to GHCR 31 | uses: docker/build-push-action@v5 32 | with: 33 | context: . 34 | file: ./Dockerfile 35 | push: true 36 | tags: | 37 | ghcr.io/okisdev/chatchat:${{ steps.extract_tag.outputs.tag }} 38 | ghcr.io/okisdev/chatchat:latest 39 | platforms: linux/amd64,linux/arm64 40 | 41 | - name: Login to Docker Hub 42 | uses: docker/login-action@v3 43 | with: 44 | username: ${{ secrets.DOCKERHUB_USERNAME }} 45 | password: ${{ secrets.DOCKERHUB_TOKEN }} 46 | 47 | - name: Tag and Push Docker image to Docker Hub 48 | uses: docker/build-push-action@v5 49 | with: 50 | context: . 51 | file: ./Dockerfile 52 | push: true 53 | tags: | 54 | okisdev/chatchat:${{ steps.extract_tag.outputs.tag }} 55 | okisdev/chatchat:latest 56 | platforms: linux/amd64,linux/arm64 -------------------------------------------------------------------------------- /.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 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env 30 | .env*.local 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | 39 | # pwa 40 | */sw.js 41 | */sw.js.map 42 | */workbox-*.js 43 | */workbox-*.js.map 44 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "buildx", 4 | "DOCKERHUB", 5 | "fastapi", 6 | "Groq", 7 | "huggingface", 8 | "langchain", 9 | "langsmith", 10 | "Lightbox", 11 | "lucide", 12 | "markdownit", 13 | "mistralai", 14 | "mixtral", 15 | "onest", 16 | "rehype", 17 | "sonner", 18 | "Tavily", 19 | "tippyjs" 20 | ], 21 | "python.analysis.typeCheckingMode": "basic", 22 | "python.analysis.autoImportCompletions": true, 23 | "i18n-ally.localesPaths": [ 24 | "locales", 25 | ], 26 | "i18n-ally.keystyle": "flat" 27 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine AS base 2 | 3 | WORKDIR /app 4 | 5 | COPY package.json pnpm-lock.yaml ./ 6 | 7 | RUN npm i -g pnpm@latest 8 | RUN pnpm install 9 | 10 | COPY . . 11 | 12 | RUN pnpm build 13 | 14 | FROM node:lts-alpine AS production 15 | 16 | WORKDIR /app 17 | 18 | COPY --from=base /app/package*.json ./ 19 | COPY --from=base /app/.next ./.next 20 | COPY --from=base /app/public ./public 21 | COPY --from=base /app/node_modules ./node_modules 22 | COPY --from=base /app/next.config.mjs ./next.config.mjs 23 | 24 | RUN npm i -g pnpm@latest 25 | 26 | EXPOSE 3000 27 | 28 | ENV AWS_ACCESS_KEY="" \ 29 | AWS_SECRET_KEY="" \ 30 | AWS_REGION="" \ 31 | ANTHROPIC_API_KEY="" \ 32 | AZURE_OPENAI_API_KEY="" \ 33 | AZURE_OPENAI_ENDPOINT="" \ 34 | AZURE_OPENAI_DEPLOY_INSTANCE_NAME="" \ 35 | COHERE_API_KEY="" \ 36 | FIREWORKS_API_KEY="" \ 37 | GOOGLE_API_KEY="" \ 38 | GROQ_API_KEY="" \ 39 | HUGGINGFACE_API_KEY="" \ 40 | MISTRAL_API_KEY="" \ 41 | OPENAI_API_KEY="" \ 42 | OPENAI_API_ENDPOINT="" \ 43 | PERPLEXITY_API_KEY="" \ 44 | PERPLEXITY_ENDPOINT="" 45 | 46 | CMD ["pnpm", "start"] 47 | -------------------------------------------------------------------------------- /README.ja.md: -------------------------------------------------------------------------------- 1 | # Chat Chat 2 | 3 | > シンプルで使いやすいインターフェイスを備えた、統合されたチャットとAIプラットフォーム。 4 | 5 |

6 | 🇺🇸 | 🇭🇰 | 🇨🇳 | 🇯🇵 7 |

8 | 9 |

10 | 11 | ドキュメント 12 | 13 |

14 | 15 | ## インターフェイス 16 | 17 | ![Search](https://cdn.harrly.com/project/GitHub/Chat-Chat/img/search.png) 18 | 19 | ![Chat](https://cdn.harrly.com/project/GitHub/Chat-Chat/img/chat.png) 20 | 21 | https://github.com/okisdev/ChatChat/assets/66008528/388023d5-b21a-40ee-a856-aab31de3d580 22 | 23 | https://github.com/okisdev/ChatChat/assets/66008528/f8d943d5-77c9-479b-9d5f-1e77eabd47b0 24 | 25 | ## 機能 26 | 27 | - 主要なAIプロバイダーに対応(Anthropic、OpenAI、Cohere、Google Geminiなど) 28 | - 自己ホストが容易 29 | 30 | ## 使用方法 31 | 32 | [ドキュメント](https://docs.okis.dev/docs/chat) 33 | 34 | ## デプロイメント 35 | 36 | [![Vercelでデプロイ](https://vercel.com/button)](https://vercel.com/import/project?template=https://github.com/okisdev/ChatChat) 37 | 38 | [![Railwayでデプロイ](https://railway.app/button.svg)](https://railway.app/template/-WWW5r) 39 | 40 | 詳細なデプロイ方法は[ドキュメント](https://docs.okis.dev/docs/chat)にて 41 | 42 | ## ライセンス 43 | 44 | [AGPL-3.0](./LICENSE) 45 | 46 | ## 技術スタック 47 | 48 | nextjs / tailwindcss / shadcn UI 49 | 50 | ## 注意 51 | 52 | - AIは不適切なコンテンツを生成する可能性がありますので、注意してご使用ください。 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chat Chat 2 | 3 | > Your own unified chat and search to AI platform, with a simple and easy to use interface. 4 | 5 |

6 | 🇺🇸 | 🇭🇰 | 🇨🇳 | 🇯🇵 7 |

8 | 9 |

10 | 11 | Documentation 12 | 13 |

14 | 15 | ## Interface 16 | 17 | ![Search](https://cdn.harrly.com/project/GitHub/Chat-Chat/img/search.png) 18 | 19 | ![Chat](https://cdn.harrly.com/project/GitHub/Chat-Chat/img/chat.png) 20 | 21 | https://github.com/okisdev/ChatChat/assets/66008528/388023d5-b21a-40ee-a856-aab31de3d580 22 | 23 | https://github.com/okisdev/ChatChat/assets/66008528/f8d943d5-77c9-479b-9d5f-1e77eabd47b0 24 | 25 | ## Features 26 | 27 | - Support major AI Providers (Anthropic, OpenAI, Cohere, Google Gemini, etc.) 28 | - Ease self-hosted 29 | 30 | ## Usage 31 | 32 | [docs](https://docs.okis.dev/docs/chat) 33 | 34 | ## Deployment 35 | 36 | [![Deployed in Vercel](https://vercel.com/button)](https://vercel.com/import/project?template=https://github.com/okisdev/ChatChat) 37 | 38 | [![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/-WWW5r) 39 | 40 | more deployment methods in [docs](https://docs.okis.dev/docs/chat) 41 | 42 | ## LICENSE 43 | 44 | [AGPL-3.0](./LICENSE) 45 | 46 | ## Stack 47 | 48 | nextjs / tailwindcss / shadcn UI 49 | 50 | ## Note 51 | 52 | - AI may generate inappropriate content, please use it with caution. 53 | -------------------------------------------------------------------------------- /README.zh_CN.md: -------------------------------------------------------------------------------- 1 | # Chat Chat 2 | 3 | > 您自己的统一聊天和搜索至AI平台,界面简单易用。 4 | 5 |

6 | 🇺🇸 | 🇭🇰 | 🇨🇳 | 🇯🇵 7 |

8 | 9 |

10 | 11 | 文档 12 | 13 |

14 | 15 | ## 界面 16 | 17 | ![Search](https://cdn.harrly.com/project/GitHub/Chat-Chat/img/search.png) 18 | 19 | ![Chat](https://cdn.harrly.com/project/GitHub/Chat-Chat/img/chat.png) 20 | 21 | https://github.com/okisdev/ChatChat/assets/66008528/388023d5-b21a-40ee-a856-aab31de3d580 22 | 23 | https://github.com/okisdev/ChatChat/assets/66008528/f8d943d5-77c9-479b-9d5f-1e77eabd47b0 24 | 25 | ## 特点 26 | 27 | - 支持主要的AI提供商(Anthropic、OpenAI、Cohere、Google Gemini等) 28 | - 方便自托管 29 | 30 | ## 使用方式 31 | 32 | [文档](https://docs.okis.dev/docs/chat) 33 | 34 | ## 部署 35 | 36 | [![在Vercel中部署](https://vercel.com/button)](https://vercel.com/import/project?template=https://github.com/okisdev/ChatChat) 37 | 38 | [![在Railway上部署](https://railway.app/button.svg)](https://railway.app/template/-WWW5r) 39 | 40 | 更多部署方法见[文档](https://docs.okis.dev/docs/chat) 41 | 42 | ## 许可证 43 | 44 | [AGPL-3.0](./LICENSE) 45 | 46 | ## 技术栈 47 | 48 | nextjs / tailwindcss / shadcn UI 49 | 50 | ## 注意事项 51 | 52 | - AI可能会生成不适当的内容,请谨慎使用。 53 | -------------------------------------------------------------------------------- /README.zh_HK.md: -------------------------------------------------------------------------------- 1 | # Chat Chat 2 | 3 | > 你的一體化聊天及搜索人工智能平台,界面簡單易用。 4 | 5 |

6 | 🇺🇸 | 🇭🇰 | 🇨🇳 | 🇯🇵 7 |

8 | 9 |

10 | 11 | 文件 12 | 13 |

14 | 15 | ## 介面 16 | 17 | ![Search](https://cdn.harrly.com/project/GitHub/Chat-Chat/img/search.png) 18 | 19 | ![Chat](https://cdn.harrly.com/project/GitHub/Chat-Chat/img/chat.png) 20 | 21 | https://github.com/okisdev/ChatChat/assets/66008528/388023d5-b21a-40ee-a856-aab31de3d580 22 | 23 | https://github.com/okisdev/ChatChat/assets/66008528/f8d943d5-77c9-479b-9d5f-1e77eabd47b0 24 | 25 | ## 功能 26 | 27 | - 支援主要人工智能供應商(Anthropic、OpenAI、Cohere、Google Gemini 等) 28 | - 方便自行託管 29 | 30 | ## 使用方式 31 | 32 | [文件](https://docs.okis.dev/docs/chat) 33 | 34 | ## 部署 35 | 36 | [![在 Vercel 中部署](https://vercel.com/button)](https://vercel.com/import/project?template=https://github.com/okisdev/ChatChat) 37 | 38 | [![在 Railway 上部署](https://railway.app/button.svg)](https://railway.app/template/-WWW5r) 39 | 40 | 更多部署方法在[文件](https://docs.okis.dev/docs/chat) 41 | 42 | ## 許可證 43 | 44 | [AGPL-3.0](./LICENSE) 45 | 46 | ## 技術棧 47 | 48 | nextjs / tailwindcss / shadcn UI 49 | 50 | ## 注意事項 51 | 52 | - 人工智能可能會生成不當內容,請小心使用。 53 | -------------------------------------------------------------------------------- /app/[locale]/(home)/layout.tsx: -------------------------------------------------------------------------------- 1 | import HomeProvider from '@/app/[locale]/(home)/provider'; 2 | import AppSidebar from '@/app/[locale]/(home)/sidebar'; 3 | 4 | export default async function AppLayout({ 5 | children, 6 | }: Readonly<{ 7 | children: React.ReactNode; 8 | }>) { 9 | return ( 10 | 11 | 12 |
{children}
13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /app/[locale]/(home)/provider.tsx: -------------------------------------------------------------------------------- 1 | import { AI as AiProvider } from '@/app/[locale]/(home)/action'; 2 | 3 | export default function HomeProvider({ children }: Readonly<{ children: React.ReactNode }>) { 4 | return {children}; 5 | } 6 | -------------------------------------------------------------------------------- /app/[locale]/(home)/search/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { AddButton } from '@/components/layout/add-button'; 4 | import { ModelSelect } from '@/components/layout/model-select'; 5 | import { SearchWindow } from '@/components/layout/search/search-window'; 6 | import { SearchSelect } from '@/components/layout/search-select'; 7 | 8 | export const runtime = 'edge'; 9 | 10 | export const dynamic = 'force-dynamic'; 11 | 12 | export default function Search() { 13 | return ( 14 | <> 15 |
16 |
17 | 18 | 19 | 20 |
21 |
22 |
23 | 24 |
25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /app/[locale]/(home)/sidebar.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Suspense } from 'react'; 4 | import { useAtom } from 'jotai'; 5 | 6 | import { Brand } from '@/components/layout/brand'; 7 | import { HistoryList } from '@/components/layout/history-list'; 8 | import { LanguageDropdown } from '@/components/layout/language-dropdown'; 9 | import { SettingsDialog } from '@/components/layout/settings-dialog'; 10 | import { SettingsDrawer } from '@/components/layout/settings-drawer'; 11 | import { ThemeDropdown } from '@/components/layout/theme-dropdown'; 12 | import store from '@/hooks/store'; 13 | import { useMediaQuery } from '@/hooks/window'; 14 | 15 | export default function AppSidebar() { 16 | const [conversations, setConversations] = useAtom(store.conversationsAtom); 17 | 18 | const isDesktop = useMediaQuery('(min-width: 768px)'); 19 | 20 | return ( 21 |
22 | 23 |
24 |
25 | 26 |
27 |
28 | {isDesktop ? : } 29 |
30 | 31 | 32 | 33 | 34 |
35 |
36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /app/[locale]/layout.tsx: -------------------------------------------------------------------------------- 1 | import LocaleProvider from '@/app/[locale]/provider'; 2 | 3 | import '@/styles/globals.css'; 4 | 5 | export default function LocaleLayout({ 6 | children, 7 | }: Readonly<{ 8 | children: React.ReactNode; 9 | }>) { 10 | return ( 11 | 12 | {children} 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /app/[locale]/provider.tsx: -------------------------------------------------------------------------------- 1 | import { NextIntlClientProvider, useMessages } from 'next-intl'; 2 | 3 | export default function LocaleProvider({ children }: Readonly<{ children: React.ReactNode }>) { 4 | const messages = useMessages(); 5 | 6 | return {children}; 7 | } 8 | -------------------------------------------------------------------------------- /app/api/app/latest/route.ts: -------------------------------------------------------------------------------- 1 | import { getLatestVersion } from '@/utils/app/version'; 2 | 3 | export async function GET(request: Request) { 4 | const latestVersion = await getLatestVersion({ owner: 'okisdev', repo: 'ChatChat' }); 5 | 6 | return Response.json( 7 | { 8 | short: { version: latestVersion.tag_name, version_name: latestVersion.name }, 9 | details: latestVersion, 10 | }, 11 | { status: 200 } 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /app/api/chat/messages/amazon/route.ts: -------------------------------------------------------------------------------- 1 | // import { BedrockRuntimeClient, InvokeModelWithResponseStreamCommand } from '@aws-sdk/client-bedrock-runtime'; 2 | // import { AWSBedrockAnthropicStream, StreamingTextResponse } from 'ai'; 3 | // import { experimental_buildAnthropicPrompt } from 'ai/prompts'; 4 | 5 | // import { ApiConfig } from '@/types/app'; 6 | 7 | // export const runtime = 'edge'; 8 | 9 | // export const dynamic = 'force-dynamic'; 10 | 11 | // const amazon = new BedrockRuntimeClient({ 12 | // region: process.env.AWS_REGION ?? 'us-east-1', 13 | // credentials: { 14 | // accessKeyId: process.env.AWS_ACCESS_KEY ?? '', 15 | // secretAccessKey: process.env.AWS_SECRET_KEY ?? '', 16 | // }, 17 | // }); 18 | 19 | // export async function POST(req: Request) { 20 | // const { 21 | // messages, 22 | // config, 23 | // stream, 24 | // }: { 25 | // messages: any[]; 26 | // config: ApiConfig; 27 | // stream: boolean; 28 | // } = await req.json(); 29 | 30 | // const response = await amazon.send( 31 | // new InvokeModelWithResponseStreamCommand({ 32 | // modelId: config.model.model_id, 33 | // contentType: 'application/json', 34 | // accept: 'application/json', 35 | // body: JSON.stringify({ 36 | // prompt: experimental_buildAnthropicPrompt(messages), 37 | // max_tokens_to_sample: 300, 38 | // stream: stream, 39 | // }), 40 | // }) 41 | // ); 42 | 43 | // const output = AWSBedrockAnthropicStream(response); 44 | 45 | // return new StreamingTextResponse(output); 46 | // } 47 | 48 | 49 | export async function GET(req: Request) { 50 | return Response.json({ error: 'Method Not Allowed' }, { status: 405 }); 51 | } 52 | -------------------------------------------------------------------------------- /app/api/chat/messages/anthropic/route.ts: -------------------------------------------------------------------------------- 1 | import Anthropic from '@anthropic-ai/sdk'; 2 | import { AnthropicStream, StreamingTextResponse } from 'ai'; 3 | 4 | import { ApiConfig } from '@/types/app'; 5 | 6 | export const runtime = 'edge'; 7 | 8 | export const dynamic = 'force-dynamic'; 9 | 10 | export async function POST(req: Request) { 11 | const { 12 | messages, 13 | config, 14 | stream, 15 | }: { 16 | messages: any[]; 17 | config: ApiConfig; 18 | stream: boolean; 19 | } = await req.json(); 20 | 21 | const anthropic = new Anthropic({ 22 | apiKey: config.provider?.apiKey ?? process.env.ANTHROPIC_API_KEY ?? '', 23 | }); 24 | 25 | const response = await anthropic.messages.create({ 26 | messages, 27 | model: config.model.model_id, 28 | stream: true, 29 | max_tokens: 4096, 30 | }); 31 | 32 | const output = AnthropicStream(response); 33 | 34 | return new StreamingTextResponse(output); 35 | } 36 | -------------------------------------------------------------------------------- /app/api/chat/messages/azure/route.ts: -------------------------------------------------------------------------------- 1 | // import { SimpleModel } from '@/types/model'; 2 | // import { OpenAIClient, AzureKeyCredential } from '@azure/openai'; 3 | // import { OpenAIStream, StreamingTextResponse } from 'ai'; 4 | 5 | // const client = new OpenAIClient(process.env.AZURE_OPENAI_ENDPOINT ?? '', new AzureKeyCredential(process.env.AZURE_OPENAI_API_KEY ?? '')); 6 | 7 | export const runtime = 'edge'; 8 | 9 | export const dynamic = 'force-dynamic'; 10 | 11 | // export async function POST(req: Request) { 12 | // const { 13 | // messages, 14 | // model, 15 | // }: { stream 16 | // }: { 17 | // messages: any[]; 18 | // model: SimpleModel; 19 | // stream: boolean; 20 | // } = await req.json(); 21 | 22 | // const response = await client.streamChatCompletions(process.env.AZURE_OPENAI_DEPLOY_INSTANCE_NAME || '', messages); 23 | 24 | // const stream = OpenAIStream(response); 25 | 26 | // return new StreamingTextResponse(stream); 27 | // } 28 | 29 | export async function GET(req: Request) { 30 | return Response.json({ error: 'Method Not Allowed' }, { status: 405 }); 31 | } 32 | -------------------------------------------------------------------------------- /app/api/chat/messages/cohere/route.ts: -------------------------------------------------------------------------------- 1 | import { Message } from 'ai'; 2 | import { CohereClient } from 'cohere-ai'; 3 | 4 | import { ApiConfig } from '@/types/app'; 5 | import { toCohereRole } from '@/utils/provider/cohere'; 6 | 7 | export const runtime = 'edge'; 8 | 9 | export const dynamic = 'force-dynamic'; 10 | 11 | export async function POST(req: Request) { 12 | const { 13 | messages, 14 | config, 15 | stream, 16 | }: { 17 | messages: any[]; 18 | config: ApiConfig; 19 | stream: boolean; 20 | } = await req.json(); 21 | 22 | const chatHistory = messages.map((message: Message) => ({ 23 | message: message.content, 24 | role: toCohereRole(message.role), 25 | })); 26 | 27 | const lastMessage = chatHistory.pop()!; 28 | 29 | const cohere = new CohereClient({ 30 | token: config.provider?.apiKey ?? process.env.COHERE_API_KEY ?? '', 31 | }); 32 | 33 | const response = await cohere.chatStream({ 34 | message: lastMessage.message, 35 | chatHistory, 36 | model: config.model.model_id, 37 | }); 38 | 39 | const output = new ReadableStream({ 40 | async start(controller) { 41 | for await (const event of response) { 42 | if (event.eventType === 'text-generation') { 43 | controller.enqueue(event.text); 44 | } 45 | } 46 | controller.close(); 47 | }, 48 | }); 49 | 50 | return new Response(output); 51 | } 52 | -------------------------------------------------------------------------------- /app/api/chat/messages/fireworks/route.ts: -------------------------------------------------------------------------------- 1 | import { OpenAIStream, StreamingTextResponse } from 'ai'; 2 | import OpenAI from 'openai'; 3 | 4 | import { ApiConfig } from '@/types/app'; 5 | 6 | export const runtime = 'edge'; 7 | 8 | export const dynamic = 'force-dynamic'; 9 | 10 | export async function POST(req: Request) { 11 | const { 12 | messages, 13 | config, 14 | stream, 15 | }: { 16 | messages: any[]; 17 | config: ApiConfig; 18 | stream: boolean; 19 | } = await req.json(); 20 | 21 | const fireworks = new OpenAI({ 22 | apiKey: config.provider.apiKey ?? process.env.FIREWORKS_API_KEY ?? '', 23 | baseURL: 'https://api.fireworks.ai/inference/v1', 24 | }); 25 | 26 | const response = await fireworks.chat.completions.create({ 27 | model: config.model.model_id, 28 | stream: true, 29 | max_tokens: 1000, 30 | messages, 31 | }); 32 | 33 | const output = OpenAIStream(response); 34 | 35 | return new StreamingTextResponse(output); 36 | } 37 | -------------------------------------------------------------------------------- /app/api/chat/messages/google/route.ts: -------------------------------------------------------------------------------- 1 | import { GoogleGenerativeAI } from '@google/generative-ai'; 2 | import { GoogleGenerativeAIStream, StreamingTextResponse } from 'ai'; 3 | 4 | import { ApiConfig } from '@/types/app'; 5 | import { toGoogleMessage } from '@/utils/provider/google'; 6 | 7 | export const runtime = 'edge'; 8 | 9 | export const dynamic = 'force-dynamic'; 10 | 11 | export async function POST(req: Request) { 12 | const { 13 | messages, 14 | config, 15 | stream, 16 | }: { 17 | messages: any[]; 18 | config: ApiConfig; 19 | stream: boolean; 20 | } = await req.json(); 21 | 22 | const genAI = new GoogleGenerativeAI(config.provider?.apiKey ?? process.env.GOOGLE_API_KEY ?? ''); 23 | 24 | const response = await genAI.getGenerativeModel({ model: config.model.model_id }).generateContentStream(toGoogleMessage(messages)); 25 | 26 | const output = GoogleGenerativeAIStream(response); 27 | 28 | return new StreamingTextResponse(output); 29 | } 30 | -------------------------------------------------------------------------------- /app/api/chat/messages/groq/route.ts: -------------------------------------------------------------------------------- 1 | import { OpenAIStream, StreamingTextResponse } from 'ai'; 2 | import Groq from 'groq-sdk'; 3 | 4 | import { ApiConfig } from '@/types/app'; 5 | 6 | export const runtime = 'edge'; 7 | 8 | export const dynamic = 'force-dynamic'; 9 | 10 | export async function POST(req: Request) { 11 | const { 12 | messages, 13 | config, 14 | stream, 15 | }: { 16 | messages: any[]; 17 | config: ApiConfig; 18 | stream: boolean; 19 | } = await req.json(); 20 | 21 | const groq = new Groq({ 22 | apiKey: config.provider?.apiKey ?? process.env.GROQ_API_KEY ?? '', 23 | }); 24 | 25 | const response = await groq.chat.completions.create({ 26 | model: config.model.model_id, 27 | stream: true, 28 | messages, 29 | }); 30 | 31 | const output = OpenAIStream(response); 32 | 33 | return new StreamingTextResponse(output); 34 | } 35 | -------------------------------------------------------------------------------- /app/api/chat/messages/huggingface/route.ts: -------------------------------------------------------------------------------- 1 | import { HfInference } from '@huggingface/inference'; 2 | import { HuggingFaceStream, StreamingTextResponse } from 'ai'; 3 | import { experimental_buildOpenAssistantPrompt } from 'ai/prompts'; 4 | 5 | import { ApiConfig } from '@/types/app'; 6 | 7 | export const runtime = 'edge'; 8 | 9 | export const dynamic = 'force-dynamic'; 10 | 11 | export async function POST(req: Request) { 12 | const { 13 | messages, 14 | config, 15 | stream, 16 | }: { 17 | messages: any[]; 18 | config: ApiConfig; 19 | stream: boolean; 20 | } = await req.json(); 21 | 22 | const huggingface = new HfInference(config.provider?.apiKey ?? process.env.HUGGINGFACE_API_KEY ?? ''); 23 | 24 | const response = huggingface.textGenerationStream({ 25 | model: config.model.model_id, 26 | inputs: experimental_buildOpenAssistantPrompt(messages), 27 | parameters: { 28 | max_new_tokens: 200, 29 | typical_p: 0.2, 30 | repetition_penalty: 1, 31 | truncate: 1000, 32 | return_full_text: false, 33 | }, 34 | }); 35 | 36 | const output = HuggingFaceStream(response); 37 | 38 | return new StreamingTextResponse(output); 39 | } 40 | -------------------------------------------------------------------------------- /app/api/chat/messages/mistral/route.ts: -------------------------------------------------------------------------------- 1 | import MistralClient from '@mistralai/mistralai'; 2 | import { MistralStream, StreamingTextResponse } from 'ai'; 3 | 4 | import { ApiConfig } from '@/types/app'; 5 | 6 | export const runtime = 'edge'; 7 | 8 | export const dynamic = 'force-dynamic'; 9 | 10 | export async function POST(req: Request) { 11 | const { 12 | messages, 13 | config, 14 | stream, 15 | }: { 16 | messages: any[]; 17 | config: ApiConfig; 18 | stream: boolean; 19 | } = await req.json(); 20 | 21 | const mistral = new MistralClient(config.provider?.apiKey ?? process.env.MISTRAL_API_KEY ?? ''); 22 | 23 | const response = mistral.chatStream({ 24 | model: config.model.model_id, 25 | maxTokens: 1000, 26 | messages, 27 | }); 28 | 29 | const output = MistralStream(response); 30 | 31 | return new StreamingTextResponse(output); 32 | } 33 | -------------------------------------------------------------------------------- /app/api/chat/messages/openai/route.ts: -------------------------------------------------------------------------------- 1 | import { OpenAIStream, StreamingTextResponse } from 'ai'; 2 | import OpenAI from 'openai'; 3 | 4 | import { ApiConfig } from '@/types/app'; 5 | 6 | export const runtime = 'edge'; 7 | 8 | export const dynamic = 'force-dynamic'; 9 | 10 | export async function POST(req: Request) { 11 | const { 12 | messages, 13 | config, 14 | stream, 15 | }: { 16 | messages: any[]; 17 | config: ApiConfig; 18 | stream: boolean; 19 | } = await req.json(); 20 | 21 | const openai = new OpenAI({ 22 | apiKey: config.provider?.apiKey ?? process.env.OPENAI_API_KEY ?? '', 23 | baseURL: config.provider?.endpoint ?? process.env.OPENAI_API_ENDPOINT ?? 'https://api.openai.com/v1', 24 | }); 25 | 26 | const response = await openai.chat.completions.create({ 27 | model: config.model.model_id, 28 | stream: true, 29 | messages, 30 | }); 31 | 32 | const output = OpenAIStream(response); 33 | 34 | return new StreamingTextResponse(output); 35 | } 36 | -------------------------------------------------------------------------------- /app/api/chat/messages/perplexity/route.ts: -------------------------------------------------------------------------------- 1 | import { OpenAIStream, StreamingTextResponse } from 'ai'; 2 | import OpenAI from 'openai'; 3 | 4 | import { ApiConfig } from '@/types/app'; 5 | 6 | export const runtime = 'edge'; 7 | 8 | export const dynamic = 'force-dynamic'; 9 | 10 | export async function POST(req: Request) { 11 | const { 12 | messages, 13 | config, 14 | stream, 15 | }: { 16 | messages: any[]; 17 | config: ApiConfig; 18 | stream: boolean; 19 | } = await req.json(); 20 | 21 | const perplexity = new OpenAI({ 22 | apiKey: config.provider?.apiKey ?? process.env.PERPLEXITY_API_KEY ?? '', 23 | baseURL: config.provider?.endpoint ?? process.env.PERPLEXITY_ENDPOINT ?? 'https://api.perplexity.ai/', 24 | }); 25 | 26 | const response = await perplexity.chat.completions.create({ 27 | model: config.model.model_id, 28 | stream: true, 29 | max_tokens: 4096, 30 | messages, 31 | }); 32 | 33 | const output = OpenAIStream(response); 34 | 35 | return new StreamingTextResponse(output); 36 | } 37 | -------------------------------------------------------------------------------- /app/api/search/google/route.ts: -------------------------------------------------------------------------------- 1 | import { createOpenAI } from '@ai-sdk/openai'; 2 | import { CoreMessage, StreamingTextResponse, streamText as aiStreamText, ToolCallPart, ToolResultPart } from 'ai'; 3 | import { createStreamableUI, createStreamableValue } from 'ai/rsc'; 4 | 5 | import { searcherPrompt } from '@/lib/prompt'; 6 | import { searcherSchema } from '@/lib/search/searcher'; 7 | import { ApiConfig } from '@/types/app'; 8 | import { withGoogleSearch } from '@/utils/search/engines/google'; 9 | 10 | export const runtime = 'edge'; 11 | 12 | export const dynamic = 'force-dynamic'; 13 | 14 | export async function POST(req: Request) { 15 | const { 16 | messages, 17 | config, 18 | stream, 19 | }: { 20 | messages: CoreMessage[]; 21 | config: ApiConfig; 22 | stream: boolean; 23 | } = await req.json(); 24 | 25 | let fullResponse = ''; 26 | 27 | const streamText = createStreamableValue(); 28 | 29 | const uiStream = createStreamableUI(); 30 | 31 | const openai = createOpenAI({ 32 | apiKey: config.provider?.apiKey ?? process.env.OPENAI_API_KEY ?? '', 33 | baseUrl: config.provider?.endpoint ?? process.env.OPENAI_API_ENDPOINT ?? 'https://api.openai.com/v1', 34 | }); 35 | 36 | const result = await aiStreamText({ 37 | model: openai.chat('gpt-4'), 38 | system: searcherPrompt, 39 | messages, 40 | tools: { 41 | search: { 42 | description: 'Search the web for information.', 43 | parameters: searcherSchema, 44 | execute: async ({ query }: { query: string }) => { 45 | const searchResult = await withGoogleSearch(query); 46 | 47 | return searchResult; 48 | }, 49 | }, 50 | }, 51 | }); 52 | 53 | const toolCalls: ToolCallPart[] = []; 54 | const toolResponses: ToolResultPart[] = []; 55 | for await (const delta of result.fullStream) { 56 | switch (delta.type) { 57 | case 'text-delta': 58 | if (delta.textDelta) { 59 | if (fullResponse.length === 0 && delta.textDelta.length > 0) { 60 | } 61 | 62 | fullResponse += delta.textDelta; 63 | streamText.update(fullResponse); 64 | } 65 | break; 66 | case 'tool-call': 67 | toolCalls.push(delta); 68 | break; 69 | case 'tool-result': 70 | toolResponses.push(delta); 71 | break; 72 | case 'error': 73 | fullResponse += `\nError occurred while executing the tool`; 74 | break; 75 | } 76 | } 77 | messages.push({ 78 | role: 'assistant', 79 | content: [{ type: 'text', text: fullResponse }, ...toolCalls], 80 | }); 81 | 82 | if (toolResponses.length > 0) { 83 | messages.push({ role: 'tool', content: toolResponses }); 84 | } 85 | 86 | return new StreamingTextResponse(result.toAIStream()); 87 | } 88 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okisdev/ChatChat/12674287a96b32ee4f04194b1e44944cf733a084/app/favicon.ico -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata, Viewport } from 'next'; 2 | import { Onest } from 'next/font/google'; 3 | 4 | import RootProvider from '@/app/provider'; 5 | 6 | import '@/styles/globals.css'; 7 | import 'tippy.js/dist/tippy.css'; 8 | 9 | const onest = Onest({ subsets: ['latin'] }); 10 | 11 | export const metadata: Metadata = { 12 | title: 'Chat Chat', 13 | description: 'Chat Chat - Unlock next-level conversations with AI', 14 | 15 | manifest: '/manifest.json', 16 | 17 | appleWebApp: { 18 | capable: true, 19 | statusBarStyle: 'default', 20 | title: 'Chat Chat', 21 | }, 22 | }; 23 | 24 | export const viewport: Viewport = { 25 | width: 'device-width', 26 | initialScale: 1, 27 | minimumScale: 1, 28 | userScalable: false, 29 | }; 30 | 31 | export default function RootLayout({ 32 | children, 33 | params: { locale }, 34 | }: Readonly<{ 35 | children: React.ReactNode; 36 | params: { locale: string }; 37 | }>) { 38 | return ( 39 | 40 | {children} 41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /app/not-found.tsx: -------------------------------------------------------------------------------- 1 | export default function NotFound() { 2 | return ( 3 |
4 |

... not found ...

5 |
6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /app/provider.tsx: -------------------------------------------------------------------------------- 1 | import { Analytics } from '@vercel/analytics/react'; 2 | import { SpeedInsights } from '@vercel/speed-insights/react'; 3 | import { Provider as JotaiProvider } from 'jotai'; 4 | import { Toaster } from 'sonner'; 5 | 6 | import { ThemeProvider } from '@/hooks/theme'; 7 | 8 | export default function RootProvider({ children }: Readonly<{ children: React.ReactNode }>) { 9 | return ( 10 | 11 | 12 | {children} 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "styles/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/ui/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /components/layout/add-button.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { IoMdAdd } from 'react-icons/io'; 4 | import { IoSearch } from 'react-icons/io5'; 5 | import { RiChat1Line } from 'react-icons/ri'; 6 | import Tippy from '@tippyjs/react'; 7 | import { useUIState } from 'ai/rsc'; 8 | import { usePathname, useRouter } from 'next/navigation'; 9 | import { useTranslations } from 'next-intl'; 10 | 11 | import { AI } from '@/app/[locale]/(home)/action'; 12 | import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/custom/dropdown-menu'; 13 | 14 | export const AddButton = () => { 15 | const router = useRouter(); 16 | 17 | const t = useTranslations(); 18 | 19 | const pathname = usePathname(); 20 | 21 | const [messages, setMessages] = useUIState(); 22 | 23 | return ( 24 | 25 | 26 | 27 | 31 | 32 | 33 | 34 | { 36 | if (pathname === '/') { 37 | window.location.reload(); 38 | } else { 39 | router.push('/'); 40 | } 41 | }} 42 | className='flex cursor-pointer items-center justify-between' 43 | > 44 | {t('chat')} 45 | 46 | 47 | { 49 | if (pathname === '/search') { 50 | // router.refresh(); 51 | // window.location.reload(); 52 | setMessages([]); 53 | } else { 54 | router.push('/search'); 55 | } 56 | }} 57 | className='flex cursor-pointer items-center justify-between' 58 | > 59 | {t('search')} 60 | 61 | 62 | 63 | 64 | ); 65 | }; 66 | -------------------------------------------------------------------------------- /components/layout/brand.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | import { VersionBadge } from '@/components/layout/version-badge'; 4 | 5 | export const Brand = () => { 6 | return ( 7 |
8 | 9 | Chat Chat 10 | 11 | 12 |
13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /components/layout/chat/conversation-window.tsx: -------------------------------------------------------------------------------- 1 | import { FaUser } from 'react-icons/fa'; 2 | import { LuClipboardCopy, LuLoader2 } from 'react-icons/lu'; 3 | import { Message } from 'ai'; 4 | import { useAtom } from 'jotai'; 5 | import Image from 'next/image'; 6 | import { useTranslations } from 'next-intl'; 7 | import { toast } from 'sonner'; 8 | 9 | import { renderMarkdownMessage } from '@/components/layout/message'; 10 | import store from '@/hooks/store'; 11 | 12 | export const ConversationWindow = ({ 13 | messages, 14 | isLoading, 15 | }: Readonly<{ 16 | messages: Message[]; 17 | isLoading: boolean; 18 | }>) => { 19 | const t = useTranslations(); 20 | 21 | const [currentUseModel] = useAtom(store.currentUseModelAtom); 22 | 23 | const onCopy = (context: string) => { 24 | navigator.clipboard.writeText(context); 25 | 26 | toast.success(t('copied'), { 27 | position: 'top-right', 28 | }); 29 | }; 30 | 31 | return ( 32 |
33 | {messages.map((m) => ( 34 |
35 |
36 | {m.role == 'user' ? ( 37 | 38 | ) : ( 39 | U 40 | )} 41 | {!isLoading && ( 42 |
43 | 50 |
51 | )} 52 |
53 | {renderMarkdownMessage(m.content)} 54 |
55 | ))} 56 | {isLoading && } 57 |
58 | ); 59 | }; 60 | -------------------------------------------------------------------------------- /components/layout/language-dropdown.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { IoLanguage } from 'react-icons/io5'; 4 | import { usePathname, useRouter } from 'next/navigation'; 5 | import { useLocale } from 'next-intl'; 6 | 7 | import { DropdownMenu, DropdownMenuContent, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuTrigger } from '@/components/ui/custom/dropdown-menu'; 8 | import { LanguageList } from '@/config/i18n'; 9 | 10 | export const LanguageDropdown = () => { 11 | const locale = useLocale(); 12 | 13 | const router = useRouter(); 14 | 15 | const pathname = usePathname(); 16 | 17 | return ( 18 | 19 | 20 | 21 | {LanguageList.find((l) => l.id === locale)?.flag} 22 | 23 | 24 | { 27 | router.push('/' + value + pathname); 28 | }} 29 | > 30 | {LanguageList.map((lang) => { 31 | return ( 32 | 33 | {lang.flag + ' ' + lang.name} 34 | 35 | ); 36 | })} 37 | 38 | 39 | 40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /components/layout/lightbox.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Lightbox from 'react-spring-lightbox'; 4 | import { ImagesList } from 'react-spring-lightbox/dist/types/ImagesList'; 5 | 6 | export const LightBox = ({ 7 | images, 8 | open, 9 | setOpen, 10 | currentIndex, 11 | setCurrentIndex, 12 | }: { 13 | images: ImagesList; 14 | open: boolean; 15 | setOpen: (open: boolean) => void; 16 | currentIndex: number; 17 | setCurrentIndex: (index: number) => void; 18 | }) => { 19 | const gotoPrevious = () => currentIndex > 0 && setCurrentIndex(currentIndex - 1); 20 | 21 | const gotoNext = () => currentIndex + 1 < images.length && setCurrentIndex(currentIndex + 1); 22 | 23 | return setOpen(false)} />; 24 | }; 25 | -------------------------------------------------------------------------------- /components/layout/search-select.tsx: -------------------------------------------------------------------------------- 1 | import { IoChevronDown } from 'react-icons/io5'; 2 | import { useAtom } from 'jotai'; 3 | import Image from 'next/image'; 4 | import Link from 'next/link'; 5 | import { useTranslations } from 'next-intl'; 6 | 7 | import { 8 | DropdownMenu, 9 | DropdownMenuContent, 10 | DropdownMenuItem, 11 | DropdownMenuLabel, 12 | DropdownMenuRadioGroup, 13 | DropdownMenuRadioItem, 14 | DropdownMenuSeparator, 15 | DropdownMenuTrigger, 16 | } from '@/components/ui/custom/dropdown-menu'; 17 | import { SearchEngine } from '@/config/search'; 18 | import store from '@/hooks/store'; 19 | import { SearchEngineSetting } from '@/types/search'; 20 | 21 | export const SearchSelect = () => { 22 | const t = useTranslations(); 23 | 24 | const [currentSearchEngine, setCurrentSearchEngine] = useAtom(store.currentSearchEngineAtom); 25 | const [currentSearchEngineSettings] = useAtom(store.currentSearchEngineSettingsAtom); 26 | 27 | const hasKeyStored = process.env['NEXT_PUBLIC_ACCESS_TAVILY_SEARCH'] == 'true'; 28 | 29 | const isTavilyConfigured = (currentSearchEngineSettings && currentSearchEngineSettings.Tavily !== null) || hasKeyStored; 30 | 31 | return ( 32 | 33 | 34 | {currentSearchEngine} 35 |

{currentSearchEngine}

36 | 37 |
38 | 39 | { 42 | setCurrentSearchEngine(value === 'Tavily' ? SearchEngine.Tavily : SearchEngine.Google); 43 | }} 44 | > 45 | {!isTavilyConfigured ? ( 46 | <> 47 | 48 |

{t('search_engine_not_configured')}

49 |
50 | 51 | 52 | {t('go_to_settings')} 53 | 54 | 55 | 56 | 57 | ) : ( 58 | hasKeyStored && ( 59 | <> 60 | 61 |

{t('search_engine_configured_globally')}

62 |
63 | 64 | 65 | ) 66 | )} 67 | 68 |
69 | Tavily 70 |

Tavily

71 |
72 |
73 |
74 |
75 |
76 | ); 77 | }; 78 | -------------------------------------------------------------------------------- /components/layout/search/block/additions.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { LuCheckCheck } from 'react-icons/lu'; 4 | import { useTranslations } from 'next-intl'; 5 | 6 | import { BlockTitle } from '@/components/layout/search/block/block-title'; 7 | 8 | export const Additions = ({ 9 | content, 10 | input, 11 | }: Readonly<{ 12 | content: { 13 | [key: string]: boolean; 14 | }; 15 | input: string; 16 | }>) => { 17 | const t = useTranslations(); 18 | 19 | let options: string[] = Object.entries(content).reduce((acc: string[], [option, checked]) => { 20 | if (checked) acc.push(option); 21 | return acc; 22 | }, []); 23 | 24 | const additions = options.join(', '); 25 | 26 | return ( 27 |
28 | 29 | {
{t('focus_on') + ': ' + input + t('and') + ' ' + additions}
} 30 |
31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /components/layout/search/block/answer.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { AiOutlineSolution } from 'react-icons/ai'; 4 | import { StreamableValue, useStreamableValue } from 'ai/rsc'; 5 | import { useAtom } from 'jotai'; 6 | import { useTranslations } from 'next-intl'; 7 | 8 | import { renderMarkdownMessage } from '@/components/layout/message'; 9 | import { BlockTitle } from '@/components/layout/search/block/block-title'; 10 | import store from '@/hooks/store'; 11 | 12 | export const Answer = ({ 13 | content, 14 | }: Readonly<{ 15 | content: string | StreamableValue; 16 | }>) => { 17 | const t = useTranslations(); 18 | 19 | const [data, error, pending] = useStreamableValue(content); 20 | 21 | const [sameCitationId, setSameCitationId] = useAtom(store.sameCitationAtom); 22 | 23 | if (error) return
{t('error')}
; 24 | 25 | return ( 26 |
27 | 28 |
{renderMarkdownMessage(data ?? '', sameCitationId, (value) => setSameCitationId(value))}
29 |
30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /components/layout/search/block/ask-follow-up-question.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useRef, useState } from 'react'; 4 | import { LiaReadme } from 'react-icons/lia'; 5 | import { useActions, useUIState } from 'ai/rsc'; 6 | import { useAtom } from 'jotai'; 7 | import { useTranslations } from 'next-intl'; 8 | 9 | import type { AI } from '@/app/[locale]/(home)/action'; 10 | import { BlockTitle } from '@/components/layout/search/block/block-title'; 11 | import { Question } from '@/components/layout/search/block/question'; 12 | import { InputBox } from '@/components/layout/search/input-box'; 13 | import store from '@/hooks/store'; 14 | 15 | export const AskFollowUpQuestion = () => { 16 | const t = useTranslations(); 17 | 18 | const { search } = useActions(); 19 | 20 | const [isProSearch] = useAtom(store.isProSearchAtom); 21 | const [currentUseModel] = useAtom(store.currentUseModelAtom); 22 | const [currentProviderSettings] = useAtom(store.currentProviderSettingsAtom); 23 | const [currentSearchEngineSettings] = useAtom(store.currentSearchEngineSettingsAtom); 24 | 25 | const [input, setInput] = useState(''); 26 | const inputRef = useRef(null); 27 | 28 | const [messages, setMessages] = useUIState(); 29 | 30 | const handleSubmit = async (e: React.FormEvent) => { 31 | e.preventDefault(); 32 | 33 | const formData = new FormData(e.currentTarget as HTMLFormElement); 34 | 35 | const userMessage = { 36 | id: Date.now(), 37 | isGenerating: false, 38 | component: , 39 | }; 40 | 41 | const searchResponse = await search(currentUseModel, currentProviderSettings, currentSearchEngineSettings, formData, isProSearch); 42 | 43 | setMessages((currentMessages) => [...currentMessages, userMessage, searchResponse]); 44 | 45 | setInput(''); 46 | }; 47 | 48 | return ( 49 |
50 | 51 | 52 |
53 | ); 54 | }; 55 | -------------------------------------------------------------------------------- /components/layout/search/block/block-title.tsx: -------------------------------------------------------------------------------- 1 | import { IconType } from 'react-icons/lib'; 2 | 3 | interface BlockTitleProps { 4 | title: string; 5 | icon: IconType; 6 | } 7 | 8 | export const BlockTitle = (props: BlockTitleProps) => { 9 | return ( 10 |
11 | 12 |

{props.title}

13 |
14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /components/layout/search/block/error.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { BiError } from 'react-icons/bi'; 4 | import { useTranslations } from 'next-intl'; 5 | 6 | import { renderMarkdownMessage } from '@/components/layout/message'; 7 | import { BlockTitle } from '@/components/layout/search/block/block-title'; 8 | 9 | type ErrorType = 'not_supported' | 'provider_not_configured' | 'search_engine_not_configured' | 'error'; 10 | 11 | export const BlockError = ({ 12 | content, 13 | type = 'error', 14 | }: Readonly<{ 15 | content: any; 16 | type: ErrorType; 17 | }>) => { 18 | const t = useTranslations(); 19 | 20 | const RenderErrorMessage = () => { 21 | switch (type) { 22 | case 'not_supported': 23 | return
{t('provider_not_supported') ?? content}
; 24 | case 'search_engine_not_configured': 25 | return
{t('search_engine_not_configured') ?? content}
; 26 | case 'provider_not_configured': 27 | return
{t('provider_not_configured') ?? content}
; 28 | case 'error': 29 | default: 30 | return
{renderMarkdownMessage(content ?? '')}
; 31 | } 32 | }; 33 | 34 | return ( 35 |
36 | 37 |
{RenderErrorMessage()}
38 |
39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /components/layout/search/block/focus-point.tsx: -------------------------------------------------------------------------------- 1 | import { LuFocus } from 'react-icons/lu'; 2 | import { useTranslations } from 'next-intl'; 3 | 4 | import { BlockTitle } from '@/components/layout/search/block/block-title'; 5 | 6 | export const FocusPoint = ({ query }: { query: string }) => { 7 | const t = useTranslations(); 8 | 9 | return ( 10 |
11 | 12 |
{query}
13 |
14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /components/layout/search/block/question.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { FaQuestion } from 'react-icons/fa6'; 4 | import { useTranslations } from 'next-intl'; 5 | 6 | import { RenderSimpleMessage } from '@/components/layout/message'; 7 | import { BlockTitle } from '@/components/layout/search/block/block-title'; 8 | 9 | export const Question = ({ content }: Readonly<{ content: string }>) => { 10 | const t = useTranslations(); 11 | 12 | return ( 13 |
14 | 15 | 16 |
17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /components/layout/search/block/related.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | import { LuPlus } from 'react-icons/lu'; 5 | import { PiPlugsConnectedDuotone } from 'react-icons/pi'; 6 | import { useActions, useStreamableValue, useUIState } from 'ai/rsc'; 7 | import { useAtom } from 'jotai'; 8 | import { useTranslations } from 'next-intl'; 9 | 10 | import { AI } from '@/app/[locale]/(home)/action'; 11 | import { BlockTitle } from '@/components/layout/search/block/block-title'; 12 | import { BlockError } from '@/components/layout/search/block/error'; 13 | import { Question } from '@/components/layout/search/block/question'; 14 | import store from '@/hooks/store'; 15 | import { TIllustrator } from '@/types/search'; 16 | 17 | export const Related = ({ relatedQueries }: { relatedQueries: TIllustrator }) => { 18 | const t = useTranslations(); 19 | 20 | const { search } = useActions(); 21 | 22 | const [isProSearch] = useAtom(store.isProSearchAtom); 23 | const [currentUseModel] = useAtom(store.currentUseModelAtom); 24 | const [currentProviderSettings] = useAtom(store.currentProviderSettingsAtom); 25 | const [currentSearchEngineSettings] = useAtom(store.currentSearchEngineSettingsAtom); 26 | 27 | const [messages, setMessages] = useUIState(); 28 | 29 | const [data, error, pending] = useStreamableValue(relatedQueries); 30 | 31 | const handleSubmit = async (e: React.FormEvent) => { 32 | e.preventDefault(); 33 | 34 | const formData = new FormData(e.currentTarget as HTMLFormElement); 35 | 36 | const submitter = (e.nativeEvent as SubmitEvent).submitter as HTMLTextAreaElement; 37 | 38 | let query = ''; 39 | 40 | if (submitter) { 41 | formData.append(submitter.name, submitter.value); 42 | query = submitter.value; 43 | } 44 | 45 | const userMessage = { 46 | id: Date.now(), 47 | isGenerating: false, 48 | component: , 49 | }; 50 | 51 | const searchResponse = await search(currentUseModel, currentProviderSettings, currentSearchEngineSettings, formData, isProSearch); 52 | 53 | setMessages((currentMessages) => [...currentMessages, userMessage, searchResponse]); 54 | }; 55 | 56 | if (error) { 57 | return ; 58 | } 59 | 60 | return ( 61 |
62 | 63 |
64 | {data?.items 65 | ?.filter((item) => item?.query !== '') 66 | .map((item, index) => ( 67 | 76 | ))} 77 |
78 |
79 | ); 80 | }; 81 | -------------------------------------------------------------------------------- /components/layout/search/block/searching.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { AiOutlineSolution } from 'react-icons/ai'; 4 | import { useTranslations } from 'next-intl'; 5 | 6 | import { BlockTitle } from '@/components/layout/search/block/block-title'; 7 | 8 | export const Searching = () => { 9 | const t = useTranslations(); 10 | 11 | return ( 12 |
13 | 14 |
{t('searching_slogan')}
15 |
16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /components/layout/search/block/try-ask.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { memo } from 'react'; 4 | import { PiStarFourBold } from 'react-icons/pi'; 5 | import { useLocale, useTranslations } from 'next-intl'; 6 | 7 | import { BlockTitle } from '@/components/layout/search/block/block-title'; 8 | import { questions } from '@/config/search/question'; 9 | 10 | export const TryAsk = memo(function TryAsk({ 11 | setInput, 12 | }: Readonly<{ 13 | setInput: (value: string) => void; 14 | }>) { 15 | const t = useTranslations(); 16 | 17 | const locale = useLocale(); 18 | 19 | const localeQuestions = questions[locale] ?? questions.en; 20 | const randomQuestions = localeQuestions.sort(() => Math.random() - 0.5).slice(0, 5); 21 | 22 | return ( 23 |
24 | 25 |
26 | {randomQuestions.map((question) => ( 27 | 34 | ))} 35 |
36 |
37 | ); 38 | }); 39 | -------------------------------------------------------------------------------- /components/layout/search/input-box.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { FaArrowUp } from 'react-icons/fa'; 4 | import Tippy from '@tippyjs/react'; 5 | import { useAtom } from 'jotai'; 6 | import { useRouter } from 'next/navigation'; 7 | import { useTranslations } from 'next-intl'; 8 | 9 | import { Switch } from '@/components/ui/custom/switch'; 10 | import { Textarea } from '@/components/ui/custom/textarea'; 11 | import store from '@/hooks/store'; 12 | 13 | export const InputBox = ({ 14 | input, 15 | inputRef, 16 | setInput, 17 | handleSubmit, 18 | }: Readonly<{ 19 | input: string; 20 | inputRef: React.RefObject; 21 | setInput: (value: string) => void; 22 | handleSubmit: (e: React.FormEvent) => void; 23 | }>) => { 24 | const router = useRouter(); 25 | 26 | const t = useTranslations(); 27 | 28 | const [preferences] = useAtom(store.preferencesAtom); 29 | const [conversationSettings] = useAtom(store.conversationSettingsAtom); 30 | 31 | const [currentUseModel] = useAtom(store.currentUseModelAtom); 32 | 33 | const [isProSearch, setIsProSearch] = useAtom(store.isProSearchAtom); 34 | 35 | const handleKeyDown = (e: React.KeyboardEvent) => { 36 | if (preferences.enterSend) { 37 | if (e.key === 'Enter') { 38 | e.preventDefault(); 39 | handleSubmit(e as React.FormEvent); 40 | } 41 | } else if (e.key === 'Enter' && e.shiftKey) { 42 | e.preventDefault(); 43 | handleSubmit(e as React.FormEvent); 44 | } 45 | }; 46 | 47 | return ( 48 |
49 |