├── .npmrc ├── apps ├── chatbot │ ├── app │ │ ├── page.module.scss │ │ ├── (chat) │ │ │ ├── page.tsx │ │ │ └── layout.tsx │ │ ├── api │ │ │ └── chat │ │ │ │ └── route.ts │ │ ├── layout.tsx │ │ └── global.css │ ├── postcss.config.js │ ├── next.config.js │ ├── vercel.json │ ├── next-env.d.ts │ ├── .eslintrc.js │ ├── components │ │ ├── markdown.tsx │ │ ├── footer.tsx │ │ ├── provider.tsx │ │ ├── chat-scroll-anchor.tsx │ │ ├── chat-list.tsx │ │ ├── external-link.tsx │ │ ├── ui │ │ │ ├── input.tsx │ │ │ ├── separator.tsx │ │ │ ├── tooltip.tsx │ │ │ ├── button.tsx │ │ │ ├── card.tsx │ │ │ ├── dialog.tsx │ │ │ ├── codeblock.tsx │ │ │ └── icon.tsx │ │ ├── header.tsx │ │ ├── button-scroll-to-bottom.tsx │ │ ├── chat-message-actions.tsx │ │ ├── empty-screen.tsx │ │ ├── chat.tsx │ │ ├── chat-message.tsx │ │ ├── prompt-form.tsx │ │ └── codeblock.tsx │ ├── lib │ │ ├── utils.ts │ │ └── hooks │ │ │ ├── use-enter-submit.tsx │ │ │ ├── use-local-storage.ts │ │ │ ├── use-copy-to-clipboard.tsx │ │ │ └── use-sidebar.tsx │ ├── tsconfig.json │ ├── containers │ │ ├── messages-list-section.tsx │ │ ├── access-key-dialog-section.tsx │ │ └── chat-prompt-section.tsx │ ├── public │ │ └── next.svg │ ├── README.md │ ├── package.json │ └── tailwind.config.js └── brain-updater │ ├── package.json │ └── src │ ├── index.ts │ └── downloadAndProcessCairoBook.ts ├── pnpm-workspace.yaml ├── tsconfig.json ├── packages ├── eslint-config │ ├── README.md │ ├── package.json │ ├── library.js │ ├── next.js │ └── react-internal.js ├── ai │ ├── utils │ │ ├── constants.ts │ │ └── validateKey.utils.ts │ ├── error │ │ └── invalidKeyError.error.ts │ ├── .eslintrc.js │ ├── tsconfig.lint.json │ ├── features │ │ ├── core │ │ │ ├── mongodb.ts │ │ │ └── vectorStore.ts │ │ ├── augmentedPromptChat │ │ │ ├── formatChatHistory.ts │ │ │ ├── formatDocumentAsString.ts │ │ │ ├── findBookChunk.infrastructure.ts │ │ │ ├── buildChatHistory.ts │ │ │ ├── types.ts │ │ │ ├── augmentedPrompt.usecase.ts │ │ │ └── ragChain.ts │ │ ├── cairoBookUpdate │ │ │ ├── removeBookPages.infrastructure.ts │ │ │ ├── getStoredBookPagesHashes.infrastructure.ts │ │ │ ├── types.ts │ │ │ ├── cairoBookUpdate.infrastructure.ts │ │ │ ├── bookPage.entity.ts │ │ │ ├── splitPagesIntoChunks.infrastructure.ts │ │ │ ├── cairoBookUpdate.usecase.test.ts │ │ │ ├── cairoBookUpdate.usecase.ts │ │ │ └── __mocks__ │ │ │ │ └── content.ts │ │ ├── findBookChunksToRemoveUseCase │ │ │ └── findBookChunksToRemove.usecase.ts │ │ └── findBookChunksToUpdateUseCase │ │ │ └── findBookChunksToUpdateUseCase.usecase.ts │ ├── tsconfig.json │ └── package.json └── typescript-config │ ├── package.json │ ├── react-library.json │ ├── nextjs.json │ └── base.json ├── .vscode └── settings.json ├── jest.config.js ├── .env.example ├── docs └── finetuning.md ├── .eslintrc.js ├── turbo.json ├── LICENSE ├── .gitignore ├── package.json └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/chatbot/app/page.module.scss: -------------------------------------------------------------------------------- 1 | .page { 2 | } 3 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "apps/*" 3 | - "packages/*" 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@repo/typescript-config/base.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/eslint-config/README.md: -------------------------------------------------------------------------------- 1 | # `@turbo/eslint-config` 2 | 3 | Collection of internal eslint configurations. 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.workingDirectories": [ 3 | { 4 | "mode": "auto" 5 | } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /apps/chatbot/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /apps/chatbot/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | module.exports = { 3 | transpilePackages: ["@repo/ai"], 4 | }; 5 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | }; -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY= 2 | MONGODB_ATLAS_URI= 3 | QUESTION_MODEL_NAME=gpt-3.5-turbo 4 | ANSWER_MODEL_NAME=gpt-4-turbo-preview 5 | -------------------------------------------------------------------------------- /packages/ai/utils/constants.ts: -------------------------------------------------------------------------------- 1 | export class Constants { 2 | static readonly IS_PREVIEW = process.env.ENV !== 'production'; 3 | static readonly LOCAL_STORAGE_KEY = 'ai-chatbot-token'; 4 | } -------------------------------------------------------------------------------- /apps/chatbot/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "redirects": [ 3 | { 4 | "source": "/(.*)", 5 | "destination": "https://agent.starknet.id", 6 | "permanent": true 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /docs/finetuning.md: -------------------------------------------------------------------------------- 1 | # Finetuning parameters and their results 2 | 3 | 4 | 5 | ## Optimal chunk sizes 6 | -------------------------------------------------------------------------------- /packages/ai/error/invalidKeyError.error.ts: -------------------------------------------------------------------------------- 1 | export class InvalidKeyError extends Error { 2 | constructor(message: string) { 3 | super(message); 4 | this.name = "InvalidKeyError"; 5 | } 6 | } -------------------------------------------------------------------------------- /packages/ai/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import("eslint").Linter.Config} */ 2 | module.exports = { 3 | root: true, 4 | extends: ["@repo/eslint-config/react-internal.js"], 5 | parser: "@typescript-eslint/parser", 6 | }; 7 | -------------------------------------------------------------------------------- /packages/typescript-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@repo/typescript-config", 3 | "version": "0.0.0", 4 | "private": true, 5 | "license": "MIT", 6 | "publishConfig": { 7 | "access": "public" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/ai/tsconfig.lint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@repo/typescript-config/react-library.json", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | }, 6 | "include": ["src", "turbo"], 7 | "exclude": ["node_modules", "dist"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/typescript-config/react-library.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "React Library", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "jsx": "react-jsx" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /apps/chatbot/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /apps/chatbot/app/(chat)/page.tsx: -------------------------------------------------------------------------------- 1 | import { nanoid } from '../../lib/utils' 2 | import { Chat } from '../../components/chat' 3 | 4 | export default function IndexPage() { 5 | const id = nanoid() 6 | return 7 | } 8 | -------------------------------------------------------------------------------- /apps/chatbot/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import("eslint").Linter.Config} */ 2 | module.exports = { 3 | root: true, 4 | extends: ["@repo/eslint-config/next.js"], 5 | parser: "@typescript-eslint/parser", 6 | parserOptions: { 7 | project: true, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /packages/ai/features/core/mongodb.ts: -------------------------------------------------------------------------------- 1 | import { MongoClient } from "mongodb"; 2 | 3 | export const client = new MongoClient(process.env.MONGODB_ATLAS_URI || ""); 4 | 5 | const dbName = "langchain"; 6 | const collectionName = "store"; 7 | export const collection = client.db(dbName).collection(collectionName); 8 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // This configuration only applies to the package manager root. 2 | /** @type {import("eslint").Linter.Config} */ 3 | module.exports = { 4 | ignorePatterns: ["apps/**", "packages/**"], 5 | extends: ["@repo/eslint-config/library.js"], 6 | parser: "@typescript-eslint/parser", 7 | parserOptions: { 8 | project: true, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /apps/chatbot/components/markdown.tsx: -------------------------------------------------------------------------------- 1 | import { FC, memo } from 'react' 2 | import ReactMarkdown, { Options } from 'react-markdown' 3 | 4 | export const MemoizedReactMarkdown: FC = memo( 5 | ReactMarkdown, 6 | (prevProps, nextProps) => 7 | prevProps.children === nextProps.children && 8 | prevProps.className === nextProps.className 9 | ) 10 | -------------------------------------------------------------------------------- /packages/ai/features/augmentedPromptChat/formatChatHistory.ts: -------------------------------------------------------------------------------- 1 | const formatChatHistory = (chatHistory: [string, string][]) => { 2 | const formattedDialogueTurns = chatHistory.map( 3 | (dialogueTurn) => 4 | `Human: ${dialogueTurn[0]}\nAssistant: ${dialogueTurn[1]}`, 5 | ); 6 | return formattedDialogueTurns.join("\n"); 7 | }; 8 | 9 | export default formatChatHistory; 10 | -------------------------------------------------------------------------------- /packages/typescript-config/nextjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Next.js", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "plugins": [{ "name": "next" }], 7 | "module": "ESNext", 8 | "moduleResolution": "Bundler", 9 | "allowJs": true, 10 | "jsx": "preserve", 11 | "noEmit": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /apps/chatbot/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | import { customAlphabet } from 'nanoid' 4 | 5 | 6 | export function cn(...inputs: ClassValue[]) { 7 | return twMerge(clsx(inputs)) 8 | } 9 | 10 | export const nanoid = customAlphabet( 11 | '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', 12 | 7 13 | ) -------------------------------------------------------------------------------- /apps/chatbot/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@repo/typescript-config/nextjs.json", 3 | "compilerOptions": { 4 | "plugins": [ 5 | { 6 | "name": "next" 7 | } 8 | ] 9 | }, 10 | "include": [ 11 | "next-env.d.ts", 12 | "next.config.js", 13 | "**/*.ts", 14 | "**/*.tsx", 15 | ".next/types/**/*.ts" 16 | ], 17 | "exclude": ["node_modules"] 18 | } 19 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "globalDependencies": ["**/.env.*local"], 4 | "pipeline": { 5 | "build": { 6 | "dependsOn": ["^build"], 7 | "outputs": [".next/**", "!.next/cache/**"] 8 | }, 9 | "lint": { 10 | "dependsOn": ["^lint"] 11 | }, 12 | "dev": { 13 | "cache": false, 14 | "persistent": true 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/ai/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "esModuleInterop": true, 5 | "forceConsistentCasingInFileNames": true, 6 | "isolatedModules": true, 7 | "module": "ESNext", 8 | "moduleResolution": "Bundler", 9 | "preserveWatchOutput": true, 10 | "skipLibCheck": true, 11 | "noEmit": true, 12 | "strict": true 13 | }, 14 | "exclude": ["node_modules"] 15 | } 16 | -------------------------------------------------------------------------------- /packages/ai/features/cairoBookUpdate/removeBookPages.infrastructure.ts: -------------------------------------------------------------------------------- 1 | import { RemoveBookPages } from "./types"; 2 | import { collection } from "../core/mongodb"; 3 | import { ObjectId } from "mongodb"; 4 | 5 | export const removeBookPages: RemoveBookPages = async (pageNames) => { 6 | console.log("Removing book pages with names ", pageNames); 7 | await collection.deleteMany({ 8 | _id: { $in: pageNames.map((name) => name as unknown as ObjectId) }, 9 | }); 10 | }; 11 | -------------------------------------------------------------------------------- /apps/chatbot/app/(chat)/layout.tsx: -------------------------------------------------------------------------------- 1 | interface ChatLayoutProps { 2 | children: React.ReactNode 3 | } 4 | 5 | export default async function ChatLayout({ children }: ChatLayoutProps) { 6 | return ( 7 |
8 |
9 | {children} 10 |
11 |
12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /packages/ai/features/augmentedPromptChat/formatDocumentAsString.ts: -------------------------------------------------------------------------------- 1 | import { Document } from "@langchain/core/documents"; 2 | 3 | /** 4 | * Given a list of documents, this util formats their contents 5 | * into a string, separated by newlines. 6 | * 7 | * @param documents 8 | * @returns A string of the documents page content, separated by newlines. 9 | */ 10 | const formatDocumentsAsString = (documents: Document[]): string => { 11 | return documents.map((doc) => doc.pageContent).join("\n\n"); 12 | }; 13 | 14 | export default formatDocumentsAsString; 15 | -------------------------------------------------------------------------------- /packages/eslint-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@repo/eslint-config", 3 | "version": "0.0.0", 4 | "private": true, 5 | "files": [ 6 | "library.js", 7 | "next.js", 8 | "react-internal.js" 9 | ], 10 | "devDependencies": { 11 | "@vercel/style-guide": "^5.1.0", 12 | "eslint-config-turbo": "^1.11.3", 13 | "eslint-config-prettier": "^9.1.0", 14 | "eslint-plugin-only-warn": "^1.1.0", 15 | "@typescript-eslint/parser": "^6.17.0", 16 | "@typescript-eslint/eslint-plugin": "^6.17.0", 17 | "typescript": "^5.3.3" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/ai/features/core/vectorStore.ts: -------------------------------------------------------------------------------- 1 | import { MongoDBAtlasVectorSearch } from "@langchain/mongodb"; 2 | import { OpenAIEmbeddings } from "@langchain/openai"; 3 | import { collection } from "../core/mongodb"; 4 | 5 | export const vectorStore = new MongoDBAtlasVectorSearch( 6 | new OpenAIEmbeddings({ 7 | openAIApiKey: process.env.OPENAI_API_KEY, 8 | batchSize: 512, 9 | modelName: "text-embedding-3-large", 10 | dimensions: 2048, 11 | }), 12 | { 13 | collection, 14 | indexName: "default", 15 | textKey: "text", 16 | embeddingKey: "embedding", 17 | } 18 | ); 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 Vercel, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /.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 | # Local env files 9 | .env 10 | .env.local 11 | .env.development.local 12 | .env.test.local 13 | .env.production.local 14 | 15 | # Testing 16 | coverage 17 | 18 | # Turbo 19 | .turbo 20 | 21 | # Vercel 22 | .vercel 23 | 24 | # Build Outputs 25 | .next/ 26 | out/ 27 | build 28 | dist 29 | 30 | 31 | # Debug 32 | npm-debug.log* 33 | yarn-debug.log* 34 | yarn-error.log* 35 | 36 | # Misc 37 | .DS_Store 38 | *.pem 39 | 40 | # IDEs 41 | .idea 42 | -------------------------------------------------------------------------------- /packages/ai/utils/validateKey.utils.ts: -------------------------------------------------------------------------------- 1 | export class ValidateKeyUtils { 2 | 3 | static isKeyValid(key: string | null): boolean { 4 | const isAuthenticationEnabled = process.env.AUTHENTICATION_ENABLED === 'true'; 5 | 6 | if (!isAuthenticationEnabled) { 7 | return true; 8 | } 9 | 10 | const validKey = process.env.KEY; 11 | if (validKey === null ) { 12 | console.log('An error occurred while validating the key. Please check the environment variables.'); 13 | return false; 14 | } 15 | return key === validKey; 16 | } 17 | } -------------------------------------------------------------------------------- /packages/ai/features/augmentedPromptChat/findBookChunk.infrastructure.ts: -------------------------------------------------------------------------------- 1 | import { ObjectId } from "mongodb"; 2 | import { collection } from "../core/mongodb"; 3 | 4 | export const findBookChunk = async (name: string) => { 5 | try { 6 | const match = await collection.findOne({ 7 | _id: name as unknown as ObjectId, 8 | }); 9 | if (match) { 10 | return { 11 | metadata: { _id: name, contentHash: match.contentHash }, 12 | pageContent: match.text, 13 | }; 14 | } 15 | } catch (error) { 16 | console.error("Error finding book chunk:", error); 17 | throw error; 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /apps/chatbot/components/footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { cn } from '../lib/utils' 4 | import { ExternalLink } from '../components/external-link' 5 | 6 | export function FooterText({ className, ...props }: React.ComponentProps<'p'>) { 7 | return ( 8 |

15 | Open source AI chatbot based on{' '} 16 | Vercel Ai ChatBot 17 | . 18 |

19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /apps/chatbot/components/provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 5 | import { ThemeProviderProps } from "next-themes/dist/types"; 6 | import { SidebarProvider } from "../lib/hooks/use-sidebar"; 7 | import { TooltipProvider } from "./ui/tooltip"; 8 | 9 | export function Providers({ children, ...props }: ThemeProviderProps) { 10 | return ( 11 | 12 | 13 | {children} 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /packages/typescript-config/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Default", 4 | "compilerOptions": { 5 | "declaration": true, 6 | "declarationMap": true, 7 | "esModuleInterop": true, 8 | "incremental": false, 9 | "isolatedModules": true, 10 | "lib": ["es2022", "DOM", "DOM.Iterable"], 11 | "module": "NodeNext", 12 | "moduleDetection": "force", 13 | "moduleResolution": "NodeNext", 14 | "noUncheckedIndexedAccess": true, 15 | "resolveJsonModule": true, 16 | "skipLibCheck": true, 17 | "strict": true, 18 | "target": "ES2022" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/ai/features/cairoBookUpdate/getStoredBookPagesHashes.infrastructure.ts: -------------------------------------------------------------------------------- 1 | import { GetStoredBookPagesHashes } from "./types"; 2 | import { collection } from "../core/mongodb"; 3 | 4 | export const getStoredBookPagesHashes: GetStoredBookPagesHashes = async () => { 5 | const documents = await collection 6 | .find( 7 | {}, 8 | { 9 | projection: { _id: 1, contentHash: 1 }, 10 | } 11 | ) 12 | .toArray(); 13 | 14 | // Transform documents into an array of BookChunkHashDto 15 | return documents.map((doc) => ({ 16 | name: doc._id.toString(), 17 | contentHash: doc.contentHash, 18 | })); 19 | }; 20 | -------------------------------------------------------------------------------- /packages/ai/features/augmentedPromptChat/buildChatHistory.ts: -------------------------------------------------------------------------------- 1 | import { ChatMessage, ChatHistory } from "./types"; 2 | 3 | const buildChatHistory = (messages: ChatMessage[]) => { 4 | let userMessage = '' 5 | let assistantMessage = '' 6 | const chatHistory: ChatHistory = [] 7 | 8 | messages.forEach((message) => { 9 | if (message.role === "user") { 10 | userMessage = message.content 11 | } else { 12 | assistantMessage = message.content 13 | chatHistory.push([userMessage, assistantMessage]) 14 | } 15 | });; 16 | 17 | return chatHistory; 18 | }; 19 | 20 | export default buildChatHistory; 21 | -------------------------------------------------------------------------------- /packages/ai/features/findBookChunksToRemoveUseCase/findBookChunksToRemove.usecase.ts: -------------------------------------------------------------------------------- 1 | import { BookChunk } from "../cairoBookUpdate/bookPage.entity"; 2 | import { BookChunkHashDto } from "../cairoBookUpdate/types"; 3 | 4 | export const findBookChunksToRemoveUseCase = ( 5 | pages: BookChunk[], 6 | storedPages: BookChunkHashDto[] 7 | ) => { 8 | // Find stored pages missing in the fresh pages based on their names 9 | const freshPageNames = pages.map((page) => page.name); 10 | const missingPages = storedPages.filter( 11 | (storedPage) => !freshPageNames.includes(storedPage.name) 12 | ); 13 | return missingPages.map((page) => page.name); 14 | }; 15 | -------------------------------------------------------------------------------- /packages/ai/features/augmentedPromptChat/types.ts: -------------------------------------------------------------------------------- 1 | import { RunnableSequence } from "@langchain/core/runnables"; 2 | import { BaseMessageChunk } from "@langchain/core/messages"; 3 | 4 | type HumanMessage = string; 5 | type AssistantMessage = string; 6 | export type ChatHistory = [HumanMessage, AssistantMessage][]; 7 | 8 | export type ChatMessage = { 9 | role: "user" | "assistant"; 10 | content: string; 11 | } 12 | 13 | export type ConversationalRetrievalQAChainInput = { 14 | question: string; 15 | chat_history: ChatHistory; 16 | }; 17 | 18 | export type RagChatAgent = RunnableSequence< 19 | ConversationalRetrievalQAChainInput, 20 | BaseMessageChunk 21 | >; 22 | -------------------------------------------------------------------------------- /apps/chatbot/lib/hooks/use-enter-submit.tsx: -------------------------------------------------------------------------------- 1 | import { useRef, type RefObject } from 'react' 2 | 3 | export function useEnterSubmit(): { 4 | formRef: RefObject 5 | onKeyDown: (event: React.KeyboardEvent) => void 6 | } { 7 | const formRef = useRef(null) 8 | 9 | const handleKeyDown = ( 10 | event: React.KeyboardEvent 11 | ): void => { 12 | if ( 13 | event.key === 'Enter' && 14 | !event.shiftKey && 15 | !event.nativeEvent.isComposing 16 | ) { 17 | formRef.current?.requestSubmit() 18 | event.preventDefault() 19 | } 20 | } 21 | 22 | return { formRef, onKeyDown: handleKeyDown } 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cairo-chatbot", 3 | "private": true, 4 | "scripts": { 5 | "build": "turbo build", 6 | "dev": "turbo dev", 7 | "lint": "turbo lint", 8 | "format": "prettier --write \"**/*.{ts,tsx,md}\"" 9 | }, 10 | "devDependencies": { 11 | "@repo/eslint-config": "workspace:*", 12 | "@repo/typescript-config": "workspace:*", 13 | "@types/jest": "^29.5.12", 14 | "jest": "^29.7.0", 15 | "prettier": "^3.1.1", 16 | "ts-jest": "^29.1.2", 17 | "turbo": "latest" 18 | }, 19 | "packageManager": "pnpm@8.9.0", 20 | "engines": { 21 | "node": ">=18" 22 | }, 23 | "dependencies": { 24 | "aws4": "^1.12.0", 25 | "posthog-js": "^1.131.4" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/ai/features/cairoBookUpdate/types.ts: -------------------------------------------------------------------------------- 1 | import { BookChunk } from "./bookPage.entity"; 2 | import { Document } from "@langchain/core/documents"; 3 | 4 | export type BookPageDto = { 5 | name: string; 6 | content: string; 7 | }; 8 | 9 | export type BookChunkHashDto = { 10 | name: string; 11 | contentHash: string; 12 | }; 13 | 14 | export type GetFreshBookPages = () => Promise; 15 | export type GetStoredBookPagesHashes = () => Promise; 16 | export type RemoveBookPages = (pageNames: BookChunk["name"][]) => Promise; 17 | export type UpdateBookPages = (pages: BookChunk[]) => Promise; 18 | export type SplitBookPages = (pages: BookPageDto[]) => Promise; 19 | -------------------------------------------------------------------------------- /packages/ai/features/findBookChunksToUpdateUseCase/findBookChunksToUpdateUseCase.usecase.ts: -------------------------------------------------------------------------------- 1 | import { BookChunk } from "../cairoBookUpdate/bookPage.entity"; 2 | import { BookChunkHashDto } from "../cairoBookUpdate/types"; 3 | 4 | export function findBookChunksToUpdateUseCase( 5 | freshPages: BookChunk[], 6 | storedPageHashes: BookChunkHashDto[] 7 | ): BookChunk[] { 8 | const storedHashesMap = new Map(); 9 | for (const hashDto of storedPageHashes) { 10 | storedHashesMap.set(hashDto.name, hashDto.contentHash); 11 | } 12 | 13 | return freshPages.filter((page) => { 14 | const storedHash = storedHashesMap.get(page.name); 15 | return storedHash !== page.contentHash; 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /packages/ai/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@repo/ai", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "lint": "eslint . --max-warnings 0" 7 | }, 8 | "devDependencies": { 9 | "@repo/eslint-config": "workspace:*", 10 | "@repo/typescript-config": "workspace:*", 11 | "@types/eslint": "^8.56.1", 12 | "@types/node": "^20.10.6", 13 | "eslint": "^8.56.0", 14 | "typescript": "^5.3.3" 15 | }, 16 | "dependencies": { 17 | "@langchain/community": "^0.0.40", 18 | "@langchain/core": "^0.1.48", 19 | "@langchain/mongodb": "^0.0.1", 20 | "@langchain/openai": "^0.0.21", 21 | "aws4": "^1.12.0", 22 | "langchain": "^0.1.26", 23 | "mongodb": "^6.5.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /apps/chatbot/lib/hooks/use-local-storage.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | 3 | export const useLocalStorage = ( 4 | key: string, 5 | initialValue: T 6 | ): [T, (value: T) => void] => { 7 | const [storedValue, setStoredValue] = useState(initialValue) 8 | 9 | useEffect(() => { 10 | // Retrieve from localStorage 11 | const item = window.localStorage.getItem(key) 12 | if (item) { 13 | setStoredValue(JSON.parse(item)) 14 | } 15 | }, [key]) 16 | 17 | const setValue = (value: T) => { 18 | // Save state 19 | setStoredValue(value) 20 | // Save to localStorage 21 | window.localStorage.setItem(key, JSON.stringify(value)) 22 | } 23 | return [storedValue, setValue] 24 | } 25 | -------------------------------------------------------------------------------- /apps/brain-updater/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "brain-updater", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "ts-node src/index.ts", 7 | "lint": "eslint . --max-warnings 0" 8 | }, 9 | "dependencies": { 10 | "@repo/ai": "workspace:*", 11 | "simple-git": "^3.22.0", 12 | "ts-node": "^10.9.2", 13 | "adm-zip": "^0.5.12", 14 | "axios": "^1.6.8" 15 | }, 16 | "devDependencies": { 17 | "@next/eslint-plugin-next": "^14.1.1", 18 | "@repo/eslint-config": "workspace:*", 19 | "@repo/typescript-config": "workspace:*", 20 | "@types/eslint": "^8.56.5", 21 | "@types/node": "^20.11.24", 22 | "eslint": "^8.57.0", 23 | "typescript": "^5.3.3" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /apps/chatbot/components/chat-scroll-anchor.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | import { useInView } from 'react-intersection-observer' 5 | 6 | interface ChatScrollAnchorProps { 7 | trackVisibility?: boolean 8 | } 9 | 10 | export function ChatScrollAnchor({ trackVisibility }: ChatScrollAnchorProps) { 11 | const { ref, entry, inView } = useInView({ 12 | trackVisibility, 13 | delay: 100, 14 | rootMargin: '0px 0px -150px 0px' 15 | }) 16 | 17 | React.useEffect(() => { 18 | if (trackVisibility && !inView) { 19 | entry?.target.scrollIntoView({ 20 | block: 'start' 21 | }) 22 | } 23 | }, [inView, entry, trackVisibility]) 24 | 25 | return
26 | } 27 | -------------------------------------------------------------------------------- /apps/chatbot/components/chat-list.tsx: -------------------------------------------------------------------------------- 1 | import { type Message } from 'ai' 2 | 3 | import { Separator } from './ui/separator' 4 | import { ChatMessage } from '../components/chat-message' 5 | 6 | interface ChatListProps { 7 | messages: Message[] 8 | } 9 | 10 | export function ChatList({ messages }: ChatListProps) { 11 | if (!messages.length) { 12 | return null 13 | } 14 | 15 | return ( 16 |
17 | {messages.map((message, index) => ( 18 |
19 | 20 | {index < messages.length - 1 && ( 21 | 22 | )} 23 |
24 | ))} 25 |
26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /packages/eslint-config/library.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require("node:path"); 2 | 3 | const project = resolve(process.cwd(), "tsconfig.json"); 4 | 5 | /** @type {import("eslint").Linter.Config} */ 6 | module.exports = { 7 | extends: ["eslint:recommended", "prettier", "eslint-config-turbo"], 8 | plugins: ["only-warn"], 9 | globals: { 10 | React: true, 11 | JSX: true, 12 | }, 13 | env: { 14 | node: true, 15 | }, 16 | settings: { 17 | "import/resolver": { 18 | typescript: { 19 | project, 20 | }, 21 | }, 22 | }, 23 | ignorePatterns: [ 24 | // Ignore dotfiles 25 | ".*.js", 26 | "node_modules/", 27 | "dist/", 28 | ], 29 | overrides: [ 30 | { 31 | files: ["*.js?(x)", "*.ts?(x)"], 32 | }, 33 | ], 34 | }; 35 | -------------------------------------------------------------------------------- /apps/chatbot/components/external-link.tsx: -------------------------------------------------------------------------------- 1 | export function ExternalLink({ 2 | href, 3 | children 4 | }: { 5 | href: string 6 | children: React.ReactNode 7 | }) { 8 | return ( 9 | 14 | {children} 15 | 27 | 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /apps/chatbot/lib/hooks/use-copy-to-clipboard.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | 5 | export interface useCopyToClipboardProps { 6 | timeout?: number 7 | } 8 | 9 | export function useCopyToClipboard({ 10 | timeout = 2000 11 | }: useCopyToClipboardProps) { 12 | const [isCopied, setIsCopied] = React.useState(false) 13 | 14 | const copyToClipboard = (value: string) => { 15 | if (typeof window === 'undefined' || !navigator.clipboard?.writeText) { 16 | return 17 | } 18 | 19 | if (!value) { 20 | return 21 | } 22 | 23 | navigator.clipboard.writeText(value).then(() => { 24 | setIsCopied(true) 25 | 26 | setTimeout(() => { 27 | setIsCopied(false) 28 | }, timeout) 29 | }) 30 | } 31 | 32 | return { isCopied, copyToClipboard } 33 | } 34 | -------------------------------------------------------------------------------- /packages/ai/features/cairoBookUpdate/cairoBookUpdate.infrastructure.ts: -------------------------------------------------------------------------------- 1 | import { UpdateBookPages } from "./types"; 2 | import { Document } from "@langchain/core/documents"; 3 | import { BookChunk } from "./bookPage.entity"; 4 | import { client } from "../core/mongodb"; 5 | import { vectorStore } from "../core/vectorStore"; 6 | 7 | export const updateBookPages: UpdateBookPages = async (pages: BookChunk[]) => { 8 | console.log("Updating book pages with ", pages); 9 | const documents: Document[] = pages.map((page) => { 10 | return { 11 | pageContent: page.content, 12 | metadata: { 13 | contentHash: page.contentHash, 14 | }, 15 | }; 16 | }); 17 | const ids = pages.map((page) => page.name); 18 | await vectorStore.addDocuments(documents, { 19 | ids: ids, 20 | }); 21 | await client.close(); 22 | }; 23 | -------------------------------------------------------------------------------- /packages/eslint-config/next.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require("node:path"); 2 | 3 | const project = resolve(process.cwd(), "tsconfig.json"); 4 | 5 | /** @type {import("eslint").Linter.Config} */ 6 | module.exports = { 7 | extends: [ 8 | "eslint:recommended", 9 | "prettier", 10 | require.resolve("@vercel/style-guide/eslint/next"), 11 | "eslint-config-turbo", 12 | ], 13 | globals: { 14 | React: true, 15 | JSX: true, 16 | }, 17 | env: { 18 | node: true, 19 | browser: true, 20 | }, 21 | plugins: ["only-warn"], 22 | settings: { 23 | "import/resolver": { 24 | typescript: { 25 | project, 26 | }, 27 | }, 28 | }, 29 | ignorePatterns: [ 30 | // Ignore dotfiles 31 | ".*.js", 32 | "node_modules/", 33 | ], 34 | overrides: [{ files: ["*.js?(x)", "*.ts?(x)"] }], 35 | }; 36 | -------------------------------------------------------------------------------- /apps/chatbot/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from '../../lib/utils' 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /apps/chatbot/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 | const Separator = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >( 12 | ( 13 | { className, orientation = 'horizontal', decorative = true, ...props }, 14 | ref 15 | ) => ( 16 | 27 | ) 28 | ) 29 | Separator.displayName = SeparatorPrimitive.Root.displayName 30 | 31 | export { Separator } 32 | -------------------------------------------------------------------------------- /apps/chatbot/app/api/chat/route.ts: -------------------------------------------------------------------------------- 1 | import { augmentedPromptChatUseCase } from "@repo/ai/features/augmentedPromptChat/augmentedPrompt.usecase" 2 | import { StreamingTextResponse } from 'ai' 3 | import { InvalidKeyError } from '@repo/ai/error/invalidKeyError.error' 4 | 5 | export const maxDuration = 60; 6 | 7 | export async function POST(req: Request) { 8 | console.log('Handling POST /api/chat'); 9 | 10 | try { 11 | const json = await req.json(); 12 | const { messages, previewToken } = json; 13 | 14 | const stream = await augmentedPromptChatUseCase(messages, previewToken); 15 | return new StreamingTextResponse(stream); 16 | } catch (error) { 17 | console.error(error); 18 | 19 | let message = 'Internal Server Error'; 20 | let statusCode = 500; 21 | if (error instanceof InvalidKeyError) { 22 | message = error.message; 23 | statusCode = 401; 24 | } 25 | 26 | return new Response(message, { status: statusCode }); 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /packages/ai/features/cairoBookUpdate/bookPage.entity.ts: -------------------------------------------------------------------------------- 1 | import { createHash } from "node:crypto"; 2 | import { BookPageDto } from "./types"; 3 | 4 | import { Document } from "@langchain/core/documents"; 5 | 6 | export class BookChunkFactory { 7 | static fromDto(pageDto: BookPageDto): BookChunk { 8 | return new BookChunk(pageDto.name, pageDto.content); 9 | } 10 | 11 | static fromDocument(doc: Document): BookChunk { 12 | return new BookChunk( 13 | `${doc.metadata.name}-${doc.metadata.chunkNumber}`, 14 | doc.pageContent 15 | ); 16 | } 17 | } 18 | 19 | export class BookChunk { 20 | name: string; 21 | content: string; 22 | contentHash: string; 23 | 24 | constructor(name: string, content: string) { 25 | this.name = name; 26 | this.content = content; 27 | this.contentHash = BookChunk.calculateHash(content); 28 | } 29 | 30 | static calculateHash(content: string): string { 31 | return createHash("md5").update(content).digest("hex"); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/ai/features/cairoBookUpdate/splitPagesIntoChunks.infrastructure.ts: -------------------------------------------------------------------------------- 1 | import { BookPageDto, SplitBookPages } from "./types"; 2 | import { Document } from "@langchain/core/documents"; 3 | import { BookChunk } from "./bookPage.entity"; 4 | import { RecursiveCharacterTextSplitter } from "langchain/text_splitter"; 5 | 6 | export const splitBookPages: SplitBookPages = async (pages) => { 7 | const textSplitter = RecursiveCharacterTextSplitter.fromLanguage("markdown", { 8 | chunkSize: 4096, 9 | chunkOverlap: 512, 10 | }); 11 | 12 | const documents: Document[] = []; 13 | 14 | for (const page of pages) { 15 | const pageDocuments = await textSplitter.createDocuments( 16 | [page.content], 17 | [ 18 | { 19 | name: page.name, 20 | }, 21 | ] 22 | ); 23 | 24 | // Assign unique number to each page 25 | pageDocuments.forEach((doc, index) => { 26 | doc.metadata.chunkNumber = index; 27 | }); 28 | documents.push(...pageDocuments); 29 | } 30 | return documents; 31 | }; 32 | -------------------------------------------------------------------------------- /apps/brain-updater/src/index.ts: -------------------------------------------------------------------------------- 1 | import { cairoBookUpdateUseCase } from "@repo/ai/features/cairoBookUpdate/cairoBookUpdate.usecase"; 2 | import { updateBookPages } from "@repo/ai/features/cairoBookUpdate/cairoBookUpdate.infrastructure"; 3 | import { getStoredBookPagesHashes } from "@repo/ai/features/cairoBookUpdate/getStoredBookPagesHashes.infrastructure"; 4 | import { removeBookPages } from "@repo/ai/features/cairoBookUpdate/removeBookPages.infrastructure"; 5 | import { downloadAndProcessCairoBook } from "./downloadAndProcessCairoBook"; 6 | 7 | console.log("Brain updater started..."); 8 | 9 | cairoBookUpdateUseCase({ 10 | getFreshBookPages: async () => { 11 | const pages = await downloadAndProcessCairoBook(); 12 | console.log( 13 | `Downloaded and identified ${pages.length} pages to learn from.`, 14 | ); 15 | return pages; 16 | }, 17 | removeBookPages: removeBookPages, 18 | updateBookPages: updateBookPages, 19 | getStoredBookPagesHashes: getStoredBookPagesHashes, 20 | }).then((_) => console.log("Brain learned much today!")); 21 | -------------------------------------------------------------------------------- /apps/chatbot/components/header.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { cn } from '../lib/utils' 4 | import { buttonVariants } from './ui/button' 5 | import { IconGitHub } from './ui/icon' 6 | 7 | export function Header() { 8 | return ( 9 |
10 |
11 |

The Cairo Programming Language Chatbot

12 |
13 | 24 |
25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /apps/chatbot/containers/messages-list-section.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Message } from 'ai/react' 4 | import { cn } from '../lib/utils' 5 | import { ChatList } from '../components/chat-list' 6 | import { ChatScrollAnchor } from '../components/chat-scroll-anchor' 7 | import { EmptyScreen } from '../components/empty-screen' 8 | import { Dispatch, SetStateAction } from 'react' 9 | 10 | export interface MessagesListSectionProps extends React.ComponentProps<'div'> { 11 | messages: Message[], 12 | isLoading: boolean, 13 | setInput: Dispatch> 14 | } 15 | 16 | export function MessagesListSection( 17 | { messages, isLoading, setInput, className }: 18 | MessagesListSectionProps 19 | ) { 20 | return ( 21 |
22 | {messages.length ? ( 23 | <> 24 | 25 | 26 | 27 | ) : ( 28 | 29 | )} 30 |
31 | ) 32 | } -------------------------------------------------------------------------------- /packages/eslint-config/react-internal.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require("node:path"); 2 | 3 | const project = resolve(process.cwd(), "tsconfig.json"); 4 | 5 | /* 6 | * This is a custom ESLint configuration for use with 7 | * internal (bundled by their consumer) libraries 8 | * that utilize React. 9 | * 10 | * This config extends the Vercel Engineering Style Guide. 11 | * For more information, see https://github.com/vercel/style-guide 12 | * 13 | */ 14 | 15 | /** @type {import("eslint").Linter.Config} */ 16 | module.exports = { 17 | extends: ["eslint:recommended", "prettier", "eslint-config-turbo"], 18 | plugins: ["only-warn"], 19 | globals: { 20 | React: true, 21 | JSX: true, 22 | }, 23 | env: { 24 | browser: true, 25 | }, 26 | settings: { 27 | "import/resolver": { 28 | typescript: { 29 | project, 30 | }, 31 | }, 32 | }, 33 | ignorePatterns: [ 34 | // Ignore dotfiles 35 | ".*.js", 36 | "node_modules/", 37 | "dist/", 38 | ], 39 | overrides: [ 40 | // Force ESLint to detect .tsx files 41 | { files: ["*.js?(x)", "*.ts?(x)"] }, 42 | ], 43 | }; 44 | -------------------------------------------------------------------------------- /apps/chatbot/components/button-scroll-to-bottom.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | 5 | import { cn } from '../lib/utils' 6 | import { Button, type ButtonProps } from './ui/button' 7 | import { IconArrowDown } from './ui/icon' 8 | import { Message } from 'ai' 9 | 10 | interface ButtonScrollToBottomProps extends ButtonProps { 11 | messages: Message[], 12 | scrollToBottom: () => void 13 | } 14 | 15 | export function ButtonScrollToBottom({ messages, scrollToBottom, className, ...props }: ButtonScrollToBottomProps) { 16 | return ( 17 | <> 18 | 34 | 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /packages/ai/features/cairoBookUpdate/cairoBookUpdate.usecase.test.ts: -------------------------------------------------------------------------------- 1 | import { cairoBookUpdateUseCase } from "./cairoBookUpdate.usecase"; 2 | import { BookChunk } from "./bookPage.entity"; 3 | import { BookPageDto } from "./types"; 4 | import { splitBookPages } from "./splitPagesIntoChunks.infrastructure"; 5 | import { mockedPages } from "./__mocks__/content"; 6 | import { assert } from "console"; 7 | 8 | // Mock dependencies 9 | const getFreshBookPages = jest.fn(); 10 | const getStoredBookPagesHashes = jest.fn(); 11 | const removeBookPages = jest.fn(); 12 | const updateBookPages = jest.fn(); 13 | 14 | describe("cairoBookUpdateUseCase", () => { 15 | it("should split the book pages into chunks", async () => { 16 | const freshPages: BookPageDto[] = mockedPages; 17 | getFreshBookPages.mockResolvedValue(freshPages); 18 | const split = await splitBookPages(freshPages); 19 | 20 | // Check that each page was split into chunks and that these chunks 21 | split.forEach((chunk) => { 22 | expect(chunk.pageContent.length).toBeLessThan(4096); 23 | expect(chunk.metadata.name).toBeDefined(); 24 | expect(chunk.metadata.chunkNumber).toBeDefined(); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /apps/chatbot/components/ui/tooltip.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | import * as TooltipPrimitive from '@radix-ui/react-tooltip' 5 | 6 | import { cn } from '../../lib/utils' 7 | 8 | const TooltipProvider = TooltipPrimitive.Provider 9 | 10 | const Tooltip = TooltipPrimitive.Root 11 | 12 | const TooltipTrigger = TooltipPrimitive.Trigger 13 | 14 | const TooltipContent = React.forwardRef< 15 | React.ElementRef, 16 | React.ComponentPropsWithoutRef 17 | >(({ className, sideOffset = 4, ...props }, ref) => ( 18 | 27 | )) 28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName 29 | 30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } 31 | -------------------------------------------------------------------------------- /packages/ai/features/augmentedPromptChat/augmentedPrompt.usecase.ts: -------------------------------------------------------------------------------- 1 | import { ChatMessage } from "./types"; 2 | import { ragChatAgent } from "./ragChain"; 3 | import buildChatHistory from "./buildChatHistory"; 4 | import { ValidateKeyUtils } from "../../utils/validateKey.utils"; 5 | import { InvalidKeyError } from "../../error/invalidKeyError.error"; 6 | import { HttpResponseOutputParser } from "langchain/output_parsers"; 7 | import { IterableReadableStream } from "@langchain/core/utils/stream"; 8 | 9 | export async function augmentedPromptChatUseCase( 10 | messages: ChatMessage[], 11 | previewToken: string 12 | ) : Promise> { 13 | console.log("Running augmented prompt chat use case..."); 14 | 15 | if (!ValidateKeyUtils.isKeyValid(previewToken)) { 16 | throw new InvalidKeyError('Invalid Access Key'); 17 | } 18 | 19 | let prompt = '' 20 | if (messages && messages.length > 0) { 21 | prompt = messages![messages.length - 1]!.content; 22 | messages.pop(); 23 | } 24 | 25 | const parser = new HttpResponseOutputParser(); 26 | 27 | return await ragChatAgent.pipe(parser).stream({ 28 | question: prompt, 29 | chat_history: buildChatHistory(messages), 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /apps/chatbot/components/chat-message-actions.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { type Message } from 'ai' 4 | 5 | import { Button } from './ui/button' 6 | import { IconCheck, IconCopy } from './ui/icon' 7 | import { useCopyToClipboard } from '../lib/hooks/use-copy-to-clipboard' 8 | import { cn } from '../lib/utils' 9 | 10 | interface ChatMessageActionsProps extends React.ComponentProps<'div'> { 11 | message: Message 12 | } 13 | 14 | export function ChatMessageActions({ 15 | message, 16 | className, 17 | ...props 18 | }: ChatMessageActionsProps) { 19 | const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 }) 20 | 21 | const onCopy = () => { 22 | if (isCopied) return 23 | copyToClipboard(message.content) 24 | } 25 | 26 | return ( 27 |
34 | 38 |
39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /apps/chatbot/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cairo chatbot 2 | 3 | This is a chatbot to help you learn and code with the [Cairo language](https://cairo-lang.org/). 4 | 5 | Archived in favor of the [Starknet Agent](https://github.com/cairo-book/starknet-agent) 6 | 7 | ## What's inside? 8 | 9 | This Turborepo includes the following packages/apps: 10 | 11 | ### Apps and Packages 12 | 13 | - `chatbot`: a [Next.js](https://nextjs.org/) app. The application is highly inspired by the vercel ai chatbot template (https://github.com/supabase-community/vercel-ai-chatbot). 14 | - `@repo/ai`: a package for the AI use cases 15 | - `@repo/eslint-config`: `eslint` configurations (includes `eslint-config-next` and `eslint-config-prettier`) 16 | - `@repo/typescript-config`: `tsconfig.json`s used throughout the monorepo 17 | 18 | ### Build 19 | 20 | To build all apps and packages, run the following command: 21 | 22 | ``` 23 | pnpm build 24 | ``` 25 | 26 | ### Develop 27 | 28 | You will need `OPENAI_API_KEY` and `MONGODB_ATLAS_URI` environment set properly. 29 | ```shell 30 | export OPENAI_API_KEY="sk-<...>" 31 | export MONGODB_ATLAS_URI="mongodb+srv:<...>" 32 | ``` 33 | 34 | Add a `.env.local` file at the root of the repo with the following content (replace the values with your own): 35 | ```shell 36 | QUESTION_MODEL_NAME="gpt-3.5-turbo" 37 | ANSWER_MODEL_NAME="gpt-4-turbo-preview" 38 | ``` 39 | 40 | To develop all apps and packages, run the following command: 41 | 42 | ``` 43 | pnpm dev 44 | ``` 45 | -------------------------------------------------------------------------------- /apps/chatbot/README.md: -------------------------------------------------------------------------------- 1 | ## Getting Started 2 | 3 | First, run the development server: 4 | 5 | ```bash 6 | yarn dev 7 | ``` 8 | 9 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 10 | 11 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 12 | 13 | To create [API routes](https://nextjs.org/docs/app/building-your-application/routing/router-handlers) add an `api/` directory to the `app/` directory with a `route.ts` file. For individual endpoints, create a subfolder in the `api` directory, like `api/hello/route.ts` would map to [http://localhost:3000/api/hello](http://localhost:3000/api/hello). 14 | 15 | ## Learn More 16 | 17 | To learn more about Next.js, take a look at the following resources: 18 | 19 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 20 | - [Learn Next.js](https://nextjs.org/learn/foundations/about-nextjs) - an interactive Next.js tutorial. 21 | 22 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 23 | 24 | ## Deploy on Vercel 25 | 26 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_source=github.com&utm_medium=referral&utm_campaign=turborepo-readme) from the creators of Next.js. 27 | 28 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 29 | -------------------------------------------------------------------------------- /packages/ai/features/cairoBookUpdate/cairoBookUpdate.usecase.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GetFreshBookPages, 3 | GetStoredBookPagesHashes, 4 | RemoveBookPages, 5 | UpdateBookPages, 6 | } from "./types"; 7 | import { BookChunkFactory } from "./bookPage.entity"; 8 | import { findBookChunksToUpdateUseCase } from "../findBookChunksToUpdateUseCase/findBookChunksToUpdateUseCase.usecase"; 9 | import { findBookChunksToRemoveUseCase } from "../findBookChunksToRemoveUseCase/findBookChunksToRemove.usecase"; 10 | import { splitBookPages } from "./splitPagesIntoChunks.infrastructure"; 11 | 12 | export async function cairoBookUpdateUseCase(context: { 13 | getFreshBookPages: GetFreshBookPages; 14 | getStoredBookPagesHashes: GetStoredBookPagesHashes; 15 | removeBookPages: RemoveBookPages; 16 | updateBookPages: UpdateBookPages; 17 | }) { 18 | console.log("Running cairoBookUpdate"); 19 | 20 | const pagesDto = await context.getFreshBookPages(); 21 | const pagesSplit = await splitBookPages(pagesDto); 22 | const chunks = pagesSplit.map(BookChunkFactory.fromDocument); 23 | 24 | const storedChunks = await context.getStoredBookPagesHashes(); 25 | const chunksToRemove = findBookChunksToRemoveUseCase(chunks, storedChunks); 26 | const chunksToUpdate = findBookChunksToUpdateUseCase(chunks, storedChunks); 27 | 28 | await context.removeBookPages(chunksToRemove); 29 | await context.updateBookPages(chunksToUpdate); 30 | 31 | console.log("Removed book pages with names ", chunksToRemove); 32 | console.log( 33 | "Updated book pages with names ", 34 | chunksToUpdate.map((chunk) => chunk.name) 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /apps/chatbot/lib/hooks/use-sidebar.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | 5 | const LOCAL_STORAGE_KEY = 'sidebar' 6 | 7 | interface SidebarContextProps { 8 | isSidebarOpen: boolean 9 | toggleSidebar: () => void 10 | isLoading: boolean 11 | } 12 | 13 | const SidebarContext = React.createContext( 14 | undefined 15 | ) 16 | 17 | export function useSidebar() { 18 | const context = React.useContext(SidebarContext) 19 | if (!context) { 20 | throw new Error('useSidebarContext must be used within a SidebarProvider') 21 | } 22 | return context 23 | } 24 | 25 | interface SidebarProviderProps { 26 | children: React.ReactNode 27 | } 28 | 29 | export function SidebarProvider({ children }: SidebarProviderProps) { 30 | const [isSidebarOpen, setSidebarOpen] = React.useState(true) 31 | const [isLoading, setLoading] = React.useState(true) 32 | 33 | React.useEffect(() => { 34 | const value = localStorage.getItem(LOCAL_STORAGE_KEY) 35 | if (value) { 36 | setSidebarOpen(JSON.parse(value)) 37 | } 38 | setLoading(false) 39 | }, []) 40 | 41 | const toggleSidebar = () => { 42 | setSidebarOpen(value => { 43 | const newState = !value 44 | localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(newState)) 45 | return newState 46 | }) 47 | } 48 | 49 | if (isLoading) { 50 | return null 51 | } 52 | 53 | return ( 54 | 57 | {children} 58 | 59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /apps/chatbot/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Toaster } from 'react-hot-toast'; 2 | import { GeistSans } from 'geist/font/sans'; 3 | import { GeistMono } from 'geist/font/mono'; 4 | import type { Metadata } from "next"; 5 | 6 | import '../app/global.css'; 7 | 8 | import { cn } from '../lib/utils'; 9 | import { Header } from '../components/header'; 10 | import { Providers } from '../components/provider'; 11 | 12 | export const metadata: Metadata = { 13 | title: "Cairo chatbot", 14 | description: "Discuss and learn about Cairo with our AI-powered chatbot", 15 | }; 16 | 17 | export const viewport = { 18 | themeColor: [ 19 | { media: '(prefers-color-scheme: light)', color: 'white' }, 20 | { media: '(prefers-color-scheme: dark)', color: 'black' }, 21 | ], 22 | }; 23 | 24 | interface RootLayoutProps { 25 | children: React.ReactNode; 26 | } 27 | 28 | export default function RootLayout({ children }: RootLayoutProps) { 29 | return ( 30 | 31 | 38 | 44 | 45 |
46 |
47 |
{children}
48 |
49 |
50 | 51 | 52 | ); 53 | } -------------------------------------------------------------------------------- /apps/chatbot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatbot", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "eslint . --max-warnings 0" 10 | }, 11 | "dependencies": { 12 | "@radix-ui/react-dialog": "^1.0.5", 13 | "@radix-ui/react-separator": "^1.0.3", 14 | "@radix-ui/react-slot": "^1.0.2", 15 | "@radix-ui/react-tooltip": "^1.0.7", 16 | "@repo/ai": "workspace:*", 17 | "ai": "^3.0.2", 18 | "class-variance-authority": "^0.7.0", 19 | "clsx": "^2.1.0", 20 | "geist": "^1.2.2", 21 | "nanoid": "^5.0.6", 22 | "next": "^14.1.1", 23 | "next-themes": "^0.2.1", 24 | "openai": "^4.24.7", 25 | "posthog-js": "^1.131.4", 26 | "react": "^18.2.0", 27 | "react-dom": "^18.2.0", 28 | "react-hot-toast": "^2.4.1", 29 | "react-intersection-observer": "^9.8.1", 30 | "react-markdown": "^9.0.1", 31 | "react-syntax-highlighter": "^15.5.0", 32 | "react-textarea-autosize": "^8.5.3", 33 | "remark-gfm": "^4.0.0", 34 | "remark-math": "^6.0.0", 35 | "tailwind-merge": "^2.2.1", 36 | "tailwindcss-animate": "^1.0.7" 37 | }, 38 | "devDependencies": { 39 | "@next/eslint-plugin-next": "^14.1.1", 40 | "@repo/eslint-config": "workspace:*", 41 | "@repo/typescript-config": "workspace:*", 42 | "@tailwindcss/typography": "^0.5.10", 43 | "@types/eslint": "^8.56.5", 44 | "@types/node": "^20.11.24", 45 | "@types/react": "^18.2.61", 46 | "@types/react-dom": "^18.2.19", 47 | "@types/react-syntax-highlighter": "^15.5.11", 48 | "autoprefixer": "^10.4.18", 49 | "eslint": "^8.57.0", 50 | "postcss": "^8.4.35", 51 | "tailwindcss": "^3.4.1", 52 | "typescript": "^5.3.3" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /apps/chatbot/app/global.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 240 10% 3.9%; 9 | 10 | --muted: 240 4.8% 95.9%; 11 | --muted-foreground: 240 3.8% 46.1%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 240 10% 3.9%; 15 | 16 | --card: 0 0% 100%; 17 | --card-foreground: 240 10% 3.9%; 18 | 19 | --border: 240 5.9% 90%; 20 | --input: 240 5.9% 90%; 21 | 22 | --primary: 240 5.9% 10%; 23 | --primary-foreground: 0 0% 98%; 24 | 25 | --secondary: 240 4.8% 95.9%; 26 | --secondary-foreground: 240 5.9% 10%; 27 | 28 | --accent: 240 4.8% 95.9%; 29 | --accent-foreground: ; 30 | 31 | --destructive: 0 84.2% 60.2%; 32 | --destructive-foreground: 0 0% 98%; 33 | 34 | --ring: 240 5% 64.9%; 35 | 36 | --radius: 0.5rem; 37 | } 38 | 39 | .dark { 40 | --background: 240 10% 3.9%; 41 | --foreground: 0 0% 98%; 42 | 43 | --muted: 240 3.7% 15.9%; 44 | --muted-foreground: 240 5% 64.9%; 45 | 46 | --popover: 240 10% 3.9%; 47 | --popover-foreground: 0 0% 98%; 48 | 49 | --card: 240 10% 3.9%; 50 | --card-foreground: 0 0% 98%; 51 | 52 | --border: 240 3.7% 15.9%; 53 | --input: 240 3.7% 15.9%; 54 | 55 | --primary: 0 0% 98%; 56 | --primary-foreground: 240 5.9% 10%; 57 | 58 | --secondary: 240 3.7% 15.9%; 59 | --secondary-foreground: 0 0% 98%; 60 | 61 | --accent: 240 3.7% 15.9%; 62 | --accent-foreground: ; 63 | 64 | --destructive: 0 62.8% 30.6%; 65 | --destructive-foreground: 0 85.7% 97.3%; 66 | 67 | --ring: 240 3.7% 15.9%; 68 | } 69 | } 70 | 71 | @layer base { 72 | * { 73 | @apply border-border; 74 | } 75 | body { 76 | @apply bg-background text-foreground; 77 | } 78 | } -------------------------------------------------------------------------------- /apps/chatbot/components/empty-screen.tsx: -------------------------------------------------------------------------------- 1 | import { UseChatHelpers } from 'ai/react' 2 | 3 | import { Button } from './ui/button' 4 | import { ExternalLink } from '../components/external-link' 5 | import { IconArrowRight } from './ui/icon' 6 | 7 | const exampleMessages = [ 8 | { 9 | heading: 'Discover Cairo', 10 | message: `What is the Cairo Programming Language?` 11 | }, 12 | { 13 | heading: 'Write a simple program', 14 | message: `How do I write a simple program in Cairo?` 15 | }, 16 | { 17 | heading: 'Create a Starknet contract', 18 | message: `Write a basic counter contract.` 19 | } 20 | ] 21 | 22 | export function EmptyScreen({ setInput }: Pick) { 23 | return ( 24 |
25 |
26 |

27 | Welcome to The Cairo Programming Language Chatbot! 28 |

29 |

30 | This is an open source AI chatbot app based on{' '} 31 | Vercel Ai ChatBot 32 | . This chatbot can answer questions about the Cairo Language and help you understand specific concepts. 33 |

34 |

35 | You can start a conversation here or try the following examples: 36 |

37 |
38 | {exampleMessages.map((message, index) => ( 39 | 48 | ))} 49 |
50 |
51 |
52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /apps/chatbot/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from '../../lib/utils'; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ) 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button" 46 | return ( 47 | 52 | ) 53 | } 54 | ) 55 | Button.displayName = "Button" 56 | 57 | export { Button, buttonVariants } 58 | -------------------------------------------------------------------------------- /apps/chatbot/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from '../../lib/utils'; 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

41 | )) 42 | CardTitle.displayName = "CardTitle" 43 | 44 | const CardDescription = React.forwardRef< 45 | HTMLParagraphElement, 46 | React.HTMLAttributes 47 | >(({ className, ...props }, ref) => ( 48 |

53 | )) 54 | CardDescription.displayName = "CardDescription" 55 | 56 | const CardContent = React.forwardRef< 57 | HTMLDivElement, 58 | React.HTMLAttributes 59 | >(({ className, ...props }, ref) => ( 60 |

61 | )) 62 | CardContent.displayName = "CardContent" 63 | 64 | const CardFooter = React.forwardRef< 65 | HTMLDivElement, 66 | React.HTMLAttributes 67 | >(({ className, ...props }, ref) => ( 68 |
73 | )) 74 | CardFooter.displayName = "CardFooter" 75 | 76 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 77 | -------------------------------------------------------------------------------- /apps/brain-updater/src/downloadAndProcessCairoBook.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | import axios from "axios"; 4 | import AdmZip from "adm-zip"; 5 | import { BookPageDto } from "@repo/ai/features/cairoBookUpdate/types"; 6 | 7 | const REPO_OWNER = "cairo-book"; 8 | const REPO_NAME = "cairo-book"; 9 | const MD_FILE_EXTENSION = ".md"; 10 | 11 | export async function downloadAndProcessCairoBook(): Promise { 12 | try { 13 | const latestReleaseUrl = `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest`; 14 | const response = await axios.get(latestReleaseUrl); 15 | const latestRelease = response.data; 16 | 17 | const zipAsset = latestRelease.assets.find( 18 | (asset: any) => asset.name === "markdown-output.zip" 19 | ); 20 | 21 | if (!zipAsset) { 22 | throw new Error("ZIP asset not found in the latest release."); 23 | } 24 | 25 | const zipUrl = zipAsset.browser_download_url; 26 | const zipResponse = await axios.get(zipUrl, { 27 | responseType: "arraybuffer", 28 | }); 29 | const zipData = zipResponse.data; 30 | 31 | const zipFile = new AdmZip(zipData); 32 | const extractDir = path.join(__dirname, "cairo-book"); 33 | zipFile.extractAllTo(extractDir, true); 34 | 35 | console.log("ZIP file downloaded and extracted successfully."); 36 | 37 | const srcDir = path.join(extractDir, "book/markdown"); 38 | return processMarkdownFiles(srcDir); 39 | } catch (error) { 40 | console.error("Error downloading and processing Cairo Book:", error); 41 | throw new Error("Failed to download and process Cairo Book"); 42 | } 43 | } 44 | 45 | function processMarkdownFiles(directory: string): Promise { 46 | return new Promise((resolve, reject) => { 47 | fs.readdir(directory, (err, files) => { 48 | if (err) { 49 | console.error("Error reading directory:", err); 50 | return reject(err); 51 | } 52 | 53 | const pages: BookPageDto[] = []; 54 | files.forEach((file) => { 55 | const filePath = path.join(directory, file); 56 | if (path.extname(file).toLowerCase() === MD_FILE_EXTENSION) { 57 | const content = fs.readFileSync(filePath, "utf8"); 58 | pages.push({ 59 | name: path.basename(file, MD_FILE_EXTENSION), 60 | content, 61 | }); 62 | } 63 | }); 64 | 65 | resolve(pages); 66 | }); 67 | }); 68 | } 69 | -------------------------------------------------------------------------------- /apps/chatbot/containers/access-key-dialog-section.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useState } from "react"; 4 | import { 5 | Dialog, 6 | DialogContent, 7 | DialogHeader, 8 | DialogTitle, 9 | DialogDescription, 10 | DialogFooter, 11 | } from "../components/ui/dialog"; 12 | import { Button } from "../components/ui/button"; 13 | import { Input } from "../components/ui/input"; 14 | import toast from "react-hot-toast"; 15 | import { ValidateKeyUtils } from "@repo/ai/utils/validateKey.utils"; 16 | 17 | export interface AccessKeyDialogSectionProps extends React.ComponentProps<'div'> { 18 | previewToken: string | null 19 | setPreviewToken: (value: string) => void 20 | previewTokenDialog: boolean 21 | setPreviewTokenDialog: (value: boolean) => void 22 | } 23 | 24 | export function AccessKeyDialogSection({ 25 | previewToken, 26 | setPreviewToken, 27 | previewTokenDialog, 28 | setPreviewTokenDialog 29 | }: AccessKeyDialogSectionProps) { 30 | const [previewTokenInput, setPreviewTokenInput] = useState(previewToken ?? '') 31 | 32 | const submitAccessKey = () => { 33 | if (ValidateKeyUtils.isKeyValid(previewTokenInput)) { 34 | setPreviewToken(previewTokenInput); 35 | setPreviewTokenDialog(false); 36 | } else { 37 | toast.error('Access key is not valid. Please try again.'); 38 | } 39 | }; 40 | 41 | return ( 42 | 43 | 44 | 45 | Enter your Access Key 46 | 47 | This site is still a work in progress. 48 | You should have an access key to access it in preview. 49 | If you have not obtained your key, you can do so by contact us{' '} 50 | 54 | visit us. 55 | {' '} 56 | This is only necessary for preview 57 | environments so that the open source community can test the app. 58 | 59 | 60 | setPreviewTokenInput(e.target.value)} 64 | /> 65 | 66 | 71 | 72 | 73 | 74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /apps/chatbot/containers/chat-prompt-section.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { type UseChatHelpers } from 'ai/react' 3 | 4 | import { Button } from '../components/ui/button' 5 | import { PromptForm } from '../components/prompt-form' 6 | import { ButtonScrollToBottom } from '../components/button-scroll-to-bottom' 7 | import { IconRefresh, IconStop } from '../components/ui/icon' 8 | import { FooterText } from '../components/footer' 9 | 10 | export interface ChatPromptSectionProps 11 | extends Pick< 12 | UseChatHelpers, 13 | | 'append' 14 | | 'isLoading' 15 | | 'reload' 16 | | 'messages' 17 | | 'stop' 18 | | 'input' 19 | | 'setInput' 20 | > { 21 | id?: string, 22 | scrollToBottom: () => void 23 | } 24 | 25 | export function ChatPromptSection({ 26 | id, 27 | isLoading, 28 | stop, 29 | append, 30 | reload, 31 | input, 32 | setInput, 33 | messages, 34 | scrollToBottom 35 | }: ChatPromptSectionProps) { 36 | return ( 37 |
38 | 39 |
40 | 41 |
42 | {isLoading ? ( 43 | 51 | ) : ( 52 | messages?.length >= 2 && ( 53 |
54 | 58 |
59 | ) 60 | )} 61 |
62 |
63 | { 65 | await append({ 66 | id, 67 | content: value, 68 | role: 'user' 69 | }) 70 | }} 71 | input={input} 72 | setInput={setInput} 73 | isLoading={isLoading} 74 | /> 75 | 76 |
77 |
78 |
79 | ) 80 | } 81 | -------------------------------------------------------------------------------- /apps/chatbot/components/chat.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useChat, type Message } from 'ai/react' 4 | import { toast } from 'react-hot-toast' 5 | 6 | import { AccessKeyDialogSection } from "../containers/access-key-dialog-section"; 7 | import { ChatPromptSection } from "../containers/chat-prompt-section"; 8 | 9 | import { Constants } from '@repo/ai/utils/constants'; 10 | import { useLocalStorage } from '../lib/hooks/use-local-storage'; 11 | import { MessagesListSection } from '../containers/messages-list-section'; 12 | import { useRef, useState } from 'react'; 13 | import { ValidateKeyUtils } from '@repo/ai/utils/validateKey.utils'; 14 | 15 | export interface ChatProps extends React.ComponentProps<'div'> { 16 | initialMessages?: Message[] 17 | id?: string 18 | } 19 | export function Chat({ id, initialMessages, className }: ChatProps) { 20 | const [previewToken, setPreviewToken] = useLocalStorage( 21 | Constants.LOCAL_STORAGE_KEY, 22 | window.localStorage.getItem(Constants.LOCAL_STORAGE_KEY) !== null ? 23 | JSON.parse(window.localStorage.getItem(Constants.LOCAL_STORAGE_KEY) ?? '') : null 24 | ) 25 | 26 | const [previewTokenDialog, setPreviewTokenDialog] = useState(Constants.IS_PREVIEW && !ValidateKeyUtils.isKeyValid(previewToken)); 27 | 28 | const { messages, append, reload, stop, isLoading, input, setInput } = 29 | useChat({ 30 | initialMessages, 31 | id, 32 | body: { 33 | id, 34 | previewToken 35 | }, 36 | onResponse(response) { 37 | if (response.status === 401) { 38 | toast.error('Invalid Access Key') 39 | setPreviewToken(null) 40 | setPreviewTokenDialog(true) 41 | } else if (response.status !== 200) { 42 | toast.error(response.statusText) 43 | } 44 | }, 45 | }) 46 | 47 | const chatRef = useRef(null); 48 | const scrollToBottom = () => { 49 | if (chatRef.current) { 50 | (chatRef.current as HTMLElement).scrollIntoView({ behavior: "smooth", block: "end" }); 51 | } 52 | } 53 | 54 | return ( 55 | <> 56 |
57 | 63 | 74 | 80 |
81 | 82 | ) 83 | } 84 | -------------------------------------------------------------------------------- /apps/chatbot/components/chat-message.tsx: -------------------------------------------------------------------------------- 1 | // Inspired by Chatbot-UI and modified to fit the needs of this project 2 | // @see https://github.com/mckaywrigley/chatbot-ui/blob/main/components/Chat/ChatMessage.tsx 3 | 4 | import { Message } from 'ai' 5 | import remarkGfm from 'remark-gfm' 6 | import remarkMath from 'remark-math' 7 | 8 | import { cn } from '../lib/utils' 9 | import { CodeBlock } from '../components/codeblock' 10 | import { MemoizedReactMarkdown } from '../components/markdown' 11 | import { IconOpenAI, IconUser } from './ui/icon' 12 | import { ChatMessageActions } from '../components/chat-message-actions' 13 | 14 | export interface ChatMessageProps { 15 | message: Message 16 | } 17 | 18 | export function ChatMessage({ message, ...props }: ChatMessageProps) { 19 | return ( 20 |
24 |
32 | {message.role === 'user' ? : } 33 |
34 |
35 | {children}

41 | }, 42 | code({ node, className, children, ...props }) { 43 | 44 | const isInline = node?.position?.start?.line === node?.position?.end?.line; 45 | 46 | if (Array.isArray(children) && children.length) { 47 | if (children[0] == '▍') { 48 | return ( 49 | 50 | ) 51 | } 52 | 53 | children[0] = (children[0] as string).replace('`▍`', '▍') 54 | } 55 | 56 | const match = /language-(\w+)/.exec(className || '') 57 | 58 | if (isInline) { 59 | return ( 60 | 61 | {children} 62 | 63 | ) 64 | } 65 | 66 | return ( 67 | 73 | ) 74 | } 75 | }} 76 | > 77 | {message.content} 78 |
79 | 80 |
81 |
82 | ) 83 | } 84 | -------------------------------------------------------------------------------- /apps/chatbot/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: ['class'], 4 | content: ['app/**/*.{ts,tsx}', 'components/**/*.{ts,tsx}', 'containers/**/*.{ts,tsx}'], 5 | theme: { 6 | container: { 7 | center: true, 8 | padding: '2rem', 9 | screens: { 10 | '2xl': '1400px' 11 | } 12 | }, 13 | extend: { 14 | fontFamily: { 15 | sans: ['var(--font-geist-sans)'], 16 | mono: ['var(--font-geist-mono)'] 17 | }, 18 | colors: { 19 | border: 'hsl(var(--border))', 20 | input: 'hsl(var(--input))', 21 | ring: 'hsl(var(--ring))', 22 | background: 'hsl(var(--background))', 23 | foreground: 'hsl(var(--foreground))', 24 | primary: { 25 | DEFAULT: 'hsl(var(--primary))', 26 | foreground: 'hsl(var(--primary-foreground))' 27 | }, 28 | secondary: { 29 | DEFAULT: 'hsl(var(--secondary))', 30 | foreground: 'hsl(var(--secondary-foreground))' 31 | }, 32 | destructive: { 33 | DEFAULT: 'hsl(var(--destructive))', 34 | foreground: 'hsl(var(--destructive-foreground))' 35 | }, 36 | muted: { 37 | DEFAULT: 'hsl(var(--muted))', 38 | foreground: 'hsl(var(--muted-foreground))' 39 | }, 40 | accent: { 41 | DEFAULT: 'hsl(var(--accent))', 42 | foreground: 'hsl(var(--accent-foreground))' 43 | }, 44 | popover: { 45 | DEFAULT: 'hsl(var(--popover))', 46 | foreground: 'hsl(var(--popover-foreground))' 47 | }, 48 | card: { 49 | DEFAULT: 'hsl(var(--card))', 50 | foreground: 'hsl(var(--card-foreground))' 51 | } 52 | }, 53 | borderRadius: { 54 | lg: `var(--radius)`, 55 | md: `calc(var(--radius) - 2px)`, 56 | sm: 'calc(var(--radius) - 4px)' 57 | }, 58 | keyframes: { 59 | 'accordion-down': { 60 | from: { height: 0 }, 61 | to: { height: 'var(--radix-accordion-content-height)' } 62 | }, 63 | 'accordion-up': { 64 | from: { height: 'var(--radix-accordion-content-height)' }, 65 | to: { height: 0 } 66 | }, 67 | 'slide-from-left': { 68 | '0%': { 69 | transform: 'translateX(-100%)' 70 | }, 71 | '100%': { 72 | transform: 'translateX(0)' 73 | } 74 | }, 75 | 'slide-to-left': { 76 | '0%': { 77 | transform: 'translateX(0)' 78 | }, 79 | '100%': { 80 | transform: 'translateX(-100%)' 81 | } 82 | } 83 | }, 84 | animation: { 85 | 'slide-from-left': 86 | 'slide-from-left 0.3s cubic-bezier(0.82, 0.085, 0.395, 0.895)', 87 | 'slide-to-left': 88 | 'slide-to-left 0.25s cubic-bezier(0.82, 0.085, 0.395, 0.895)', 89 | 'accordion-down': 'accordion-down 0.2s ease-out', 90 | 'accordion-up': 'accordion-up 0.2s ease-out' 91 | } 92 | } 93 | }, 94 | plugins: [require('tailwindcss-animate'), require('@tailwindcss/typography')] 95 | } -------------------------------------------------------------------------------- /apps/chatbot/components/prompt-form.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import Textarea from 'react-textarea-autosize' 3 | import { UseChatHelpers } from 'ai/react' 4 | import { useEnterSubmit } from '../lib/hooks/use-enter-submit' 5 | import { cn } from '../lib/utils' 6 | import { Button, buttonVariants } from './ui/button' 7 | import { 8 | Tooltip, 9 | TooltipContent, 10 | TooltipTrigger 11 | } from './ui/tooltip' 12 | import { IconArrowElbow, IconPlus } from './ui/icon' 13 | import { useRouter } from 'next/navigation' 14 | 15 | export interface PromptProps 16 | extends Pick { 17 | onSubmit: (value: string) => void 18 | isLoading: boolean 19 | } 20 | 21 | export function PromptForm({ 22 | onSubmit, 23 | input, 24 | setInput, 25 | isLoading 26 | }: PromptProps) { 27 | const { formRef, onKeyDown } = useEnterSubmit() 28 | const inputRef = React.useRef(null) 29 | const router = useRouter() 30 | React.useEffect(() => { 31 | if (inputRef.current) { 32 | inputRef.current.focus() 33 | } 34 | }, []) 35 | 36 | return ( 37 |
{ 39 | e.preventDefault() 40 | if (!input?.trim()) { 41 | return 42 | } 43 | setInput('') 44 | await onSubmit(input) 45 | }} 46 | ref={formRef} 47 | > 48 |
49 | 50 | 51 | 64 | 65 | New Chat 66 | 67 |