├── .gitignore ├── README.md ├── eslint.config.js ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── _redirects ├── assets │ ├── AI_Clinical_Notes_icon.png │ ├── AI_Doctor_icon.png │ ├── AI_Prompt_Optimizer_icon.png │ ├── AI_Researcher_icon.png │ ├── AI_Stock_Predictor_icon.png │ ├── Add_your_own_icon.png │ └── react.svg ├── icons │ ├── clinic.svg │ ├── doctor.svg │ ├── lawyer.svg │ ├── own-promt.svg │ ├── promt.svg │ ├── researcher.svg │ └── stock.svg ├── images │ ├── avatar.png │ ├── logo1.svg │ └── logo2.svg ├── logo.png └── vite.svg ├── src ├── App.css ├── App.tsx ├── assets │ └── react.svg ├── components │ ├── auth │ │ └── signin-modal.tsx │ ├── chats │ │ ├── bot-message.tsx │ │ ├── chat-input.tsx │ │ ├── chat-list.tsx │ │ ├── chat-messages.tsx │ │ ├── serach-chats.tsx │ │ └── user-message.tsx │ ├── customModal.tsx │ ├── header.tsx │ ├── icons.tsx │ ├── logo.tsx │ ├── primitives.ts │ ├── services │ │ └── services-sidebar-btn.tsx │ ├── setting │ │ ├── APIs.tsx │ │ ├── general.tsx │ │ ├── renderTableCell.tsx │ │ └── security.tsx │ ├── sidebar.tsx │ ├── theme-switch.tsx │ ├── tootip │ │ └── tooltip.tsx │ └── user │ │ └── profile.tsx ├── config │ └── site.ts ├── contexts │ ├── ArchiveContext.tsx │ └── AuthContext.tsx ├── hooks │ ├── use-archive.ts │ ├── use-auth.ts │ ├── use-speech2text.tsx │ └── use-theme.ts ├── html2pdf.d.ts ├── index.css ├── layouts │ └── default.tsx ├── main.tsx ├── pages │ ├── about.tsx │ ├── activate-account.tsx │ ├── blog.tsx │ ├── chat.tsx │ ├── docs.tsx │ ├── index.tsx │ ├── pricing.tsx │ ├── profile.tsx │ ├── setting.tsx │ └── upgradepage.tsx ├── provider.tsx ├── services │ ├── dispatch │ │ ├── chat-dispatch.ts │ │ └── user-dispatch.ts │ ├── service.ts │ └── session.ts ├── store │ ├── actions │ │ └── chatActions.ts │ ├── hooks.ts │ ├── index.ts │ └── slices │ │ └── chatSlice.ts ├── styles │ └── globals.css ├── types │ └── index.ts ├── utils │ ├── PDFdocument.tsx │ └── index.tsx └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node copy.json ├── tsconfig.node.json ├── tsconfig.node.tsbuildinfo ├── vercel.json ├── vite.config.d.ts ├── vite.config.js └── vite.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | Currently, two official plugins are available: 5 | 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: 13 | 14 | - Configure the top-level `parserOptions` property like this: 15 | 16 | ```js 17 | export default tseslint.config({ 18 | languageOptions: { 19 | // other options... 20 | parserOptions: { 21 | project: ['./tsconfig.node.json', './tsconfig.app.json'], 22 | tsconfigRootDir: import.meta.dirname, 23 | }, 24 | }, 25 | }) 26 | ``` 27 | 28 | - Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked` 29 | - Optionally add `...tseslint.configs.stylisticTypeChecked` 30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config: 31 | 32 | ```js 33 | // eslint.config.js 34 | import react from 'eslint-plugin-react' 35 | 36 | export default tseslint.config({ 37 | // Set the react version 38 | settings: { react: { version: '18.3' } }, 39 | plugins: { 40 | // Add the react plugin 41 | react, 42 | }, 43 | rules: { 44 | // other rules... 45 | // Enable its recommended rules 46 | ...react.configs.recommended.rules, 47 | ...react.configs['jsx-runtime'].rules, 48 | }, 49 | }) 50 | ``` 51 | 52 | 53 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js"; 2 | import globals from "globals"; 3 | import reactHooks from "eslint-plugin-react-hooks"; 4 | import reactRefresh from "eslint-plugin-react-refresh"; 5 | import tseslint from "typescript-eslint"; 6 | 7 | export default tseslint.config( 8 | { ignores: ["dist"] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ["**/*.{ts,tsx}"], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | "react-hooks": reactHooks, 18 | "react-refresh": reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | "react-refresh/only-export-components": [ 23 | "warn", 24 | { allowConstantExport: true }, 25 | ], 26 | }, 27 | } 28 | ); 29 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Cerina 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cerina-ai", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite --host 127.0.0.1", 8 | "build": "vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@nextui-org/react": "^2.4.6", 14 | "@react-pdf/renderer": "^4.0.0", 15 | "@reduxjs/toolkit": "^2.2.7", 16 | "axios": "^1.7.6", 17 | "file-saver": "^2.0.5", 18 | "formik": "^2.4.6", 19 | "framer-motion": "^11.3.29", 20 | "html2pdf.js": "^0.10.2", 21 | "lucide-react": "^0.456.0", 22 | "moment": "^2.30.1", 23 | "react": "^18.3.1", 24 | "react-dom": "^18.3.1", 25 | "react-hot-toast": "^2.4.1", 26 | "react-icons": "^5.3.0", 27 | "react-markdown": "^9.0.1", 28 | "react-pdf": "^9.1.1", 29 | "react-query": "^3.39.3", 30 | "react-redux": "^9.1.2", 31 | "react-router-dom": "^6.26.1", 32 | "react-syntax-highlighter": "^15.5.0", 33 | "rehype-raw": "^7.0.0", 34 | "remark-breaks": "^4.0.0", 35 | "remark-gfm": "^4.0.0", 36 | "yup": "^1.4.0" 37 | }, 38 | "devDependencies": { 39 | "@eslint/js": "^9.9.0", 40 | "@types/file-saver": "^2.0.7", 41 | "@types/react": "^18.3.3", 42 | "@types/react-dom": "^18.3.0", 43 | "@types/react-syntax-highlighter": "^15.5.13", 44 | "@vitejs/plugin-react": "^4.3.1", 45 | "autoprefixer": "^10.4.20", 46 | "eslint": "^9.9.0", 47 | "eslint-plugin-react-hooks": "^5.1.0-rc.0", 48 | "eslint-plugin-react-refresh": "^0.4.9", 49 | "globals": "^15.9.0", 50 | "postcss": "^8.4.41", 51 | "tailwindcss": "^3.4.10", 52 | "typescript": "^5.5.3", 53 | "typescript-eslint": "^8.0.1", 54 | "vite": "^5.4.1", 55 | "vite-tsconfig-paths": "^4.3.2" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /public/assets/AI_Clinical_Notes_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeByStella/AI-chatbot/d6d4a40bd2d9efba557c73e32dc1a2801838532f/public/assets/AI_Clinical_Notes_icon.png -------------------------------------------------------------------------------- /public/assets/AI_Doctor_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeByStella/AI-chatbot/d6d4a40bd2d9efba557c73e32dc1a2801838532f/public/assets/AI_Doctor_icon.png -------------------------------------------------------------------------------- /public/assets/AI_Prompt_Optimizer_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeByStella/AI-chatbot/d6d4a40bd2d9efba557c73e32dc1a2801838532f/public/assets/AI_Prompt_Optimizer_icon.png -------------------------------------------------------------------------------- /public/assets/AI_Researcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeByStella/AI-chatbot/d6d4a40bd2d9efba557c73e32dc1a2801838532f/public/assets/AI_Researcher_icon.png -------------------------------------------------------------------------------- /public/assets/AI_Stock_Predictor_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeByStella/AI-chatbot/d6d4a40bd2d9efba557c73e32dc1a2801838532f/public/assets/AI_Stock_Predictor_icon.png -------------------------------------------------------------------------------- /public/assets/Add_your_own_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeByStella/AI-chatbot/d6d4a40bd2d9efba557c73e32dc1a2801838532f/public/assets/Add_your_own_icon.png -------------------------------------------------------------------------------- /public/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/icons/clinic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/icons/doctor.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/icons/lawyer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/icons/own-promt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/icons/promt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /public/icons/researcher.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/icons/stock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/images/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeByStella/AI-chatbot/d6d4a40bd2d9efba557c73e32dc1a2801838532f/public/images/avatar.png -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeByStella/AI-chatbot/d6d4a40bd2d9efba557c73e32dc1a2801838532f/public/logo.png -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Route, Routes } from "react-router-dom"; 2 | 3 | import IndexPage from "@/pages/index"; 4 | import DocsPage from "@/pages/docs"; 5 | import PricingPage from "@/pages/pricing"; 6 | import BlogPage from "@/pages/blog"; 7 | import AboutPage from "@/pages/about"; 8 | import ChatPage from "./pages/chat"; 9 | import ActivateAccountPage from "@/pages/activate-account"; 10 | import ProfilePage from "./pages/profile"; 11 | import Setting from "./pages/setting"; 12 | import Upgrade_Plan from "./pages/upgradepage"; 13 | 14 | function App() { 15 | return ( 16 | 17 | } path="/" /> 18 | } path="/verify-email" /> 19 | } path="/c/:id" /> 20 | } path="/docs" /> 21 | } path="/pricing" /> 22 | } path="/blog" /> 23 | } path="/about" /> 24 | } path="/profile" /> 25 | } path="/setting" /> 26 | } path="/upgrade_plan" /> 27 | 28 | ); 29 | } 30 | 31 | export default App; 32 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/auth/signin-modal.tsx: -------------------------------------------------------------------------------- 1 | import { useAuth } from "@/hooks/use-auth"; 2 | import { login, register, reverifyEmail } from "@/services/dispatch/user-dispatch"; 3 | import { saveSession, setItem } from "@/services/session"; 4 | import { 5 | Button, 6 | Input, 7 | Modal, 8 | ModalBody, 9 | ModalContent, 10 | ModalFooter, 11 | ModalHeader, 12 | Progress, 13 | Tab, 14 | Tabs, 15 | } from "@nextui-org/react"; 16 | import { useFormik } from "formik"; 17 | import { useState } from "react"; 18 | import toast from "react-hot-toast"; 19 | import { IoCheckmarkOutline, IoCloseOutline } from "react-icons/io5"; 20 | import * as Yup from "yup"; 21 | 22 | type P = { 23 | isOpen: boolean; 24 | onClose: () => void; 25 | onSignupSuccess: (message: string) => void; 26 | }; 27 | 28 | export default function SigninModal({ isOpen, onClose, onSignupSuccess }: P) { 29 | const [selectedKey, setSelectedKey] = useState("login"); 30 | const [loading, setLoading] = useState(false); 31 | const [error, setError] = useState(null); 32 | const { setUser, setIsLoggedIn } = useAuth(); 33 | const [showVerificationModal, setShowVerificationModal] = useState(false); 34 | const [unverifiedEmail, setUnverifiedEmail] = useState(null); 35 | const [resendingEmail, setResendingEmail] = useState(false); 36 | 37 | const [password, setPassword] = useState("") 38 | 39 | const validationSchemas = { 40 | login: Yup.object().shape({ 41 | email: Yup.string() 42 | .email("Invalid email format") 43 | .matches( 44 | /@(gmail\.com|outlook\.com|mail\.com|yahoo\.com)$/, 45 | "Invalid Email. Please Give a Valid Personal Email" 46 | ) 47 | .required("Email is required"), 48 | password: Yup.string() 49 | .min(3, "Password must be at least 3 characters long") 50 | .required("Password is required"), 51 | }), 52 | signup: Yup.object().shape({ 53 | full_name: Yup.string() 54 | .min(3, "Full name must be at least 3 characters long") 55 | .required("Full name is required"), 56 | email: Yup.string() 57 | .email("Invalid email format") 58 | .matches( 59 | /@(gmail\.com|outlook\.com|mail\.com|yahoo\.com)$/, 60 | "Invalid Email. Please Give a Valid Personal Email" 61 | ) 62 | .required("Email is required"), 63 | // password: Yup.string() 64 | // .min(3, "Password must be at least 3 characters long") 65 | // .required("Password is required"), 66 | }), 67 | }; 68 | 69 | const handleResendVerification = async (email: string = unverifiedEmail || '') => { 70 | if (!email) return; 71 | 72 | setResendingEmail(true); 73 | try { 74 | await reverifyEmail({ email }); 75 | toast.success("Verification email has been resent. Please check your inbox."); 76 | setShowVerificationModal(true); 77 | } catch (err) { 78 | toast.error("Failed to resend verification email. Please try again."); 79 | } finally { 80 | setResendingEmail(false); 81 | } 82 | }; 83 | 84 | // Enhanced error handling function 85 | const handleApiError = (err: any, context: 'login' | 'signup') => { 86 | setLoading(false); 87 | 88 | if (!err.response) { 89 | setError("Network error. Please check your connection and try again."); 90 | return; 91 | } 92 | 93 | const { status, data } = err.response; 94 | 95 | // Handle email-specific error messages 96 | if (data?.email?.[0]) { 97 | const emailError = data.email[0]; 98 | 99 | // Handle verified user case 100 | if (emailError.includes("already exists and is verified")) { 101 | setError("This email is already registered. Please login instead."); 102 | setTimeout(() => setSelectedKey("login"), 1500); 103 | return; 104 | } 105 | 106 | // Handle unverified user case 107 | if (emailError.includes("already registered but not verified")) { 108 | setUnverifiedEmail(formik.values.email); 109 | setError("This email is registered but not verified. Click below to resend verification email."); 110 | return; 111 | } 112 | 113 | // Handle any other email-specific errors 114 | setError(emailError); 115 | return; 116 | } 117 | 118 | switch (status) { 119 | case 400: 120 | if (context === 'login' && data?.detail?.includes("not found")) { 121 | setError("Email not registered. Please sign up first."); 122 | setTimeout(() => setSelectedKey("signup"), 1500); 123 | return; 124 | } 125 | setError(data.message || "Please check your input and try again."); 126 | break; 127 | case 401: 128 | setError("Incorrect email or password. Please try again."); 129 | break; 130 | case 422: 131 | setError("Invalid input format. Please check your details."); 132 | break; 133 | case 429: 134 | setError("Too many attempts. Please try again later."); 135 | break; 136 | default: 137 | setError("An unexpected error occurred. Please try again later."); 138 | } 139 | 140 | console.error("API Error:", { 141 | status, 142 | data, 143 | context, 144 | error: err 145 | }); 146 | }; 147 | 148 | const formik = useFormik({ 149 | initialValues: 150 | selectedKey === "login" 151 | ? { email: "", password: "" } 152 | : { full_name: "", email: "", password: "" }, 153 | validationSchema: 154 | selectedKey === "login" 155 | ? validationSchemas.login 156 | : validationSchemas.signup, 157 | enableReinitialize: true, 158 | onSubmit: async (values) => { 159 | setLoading(true); 160 | 161 | setError(null); 162 | 163 | try { 164 | if (selectedKey === "login") { 165 | const res = await login(values); 166 | if (res?.user) { 167 | setUser(res.user); 168 | setIsLoggedIn(true); 169 | saveSession({ 170 | accessToken: res.access, 171 | refreshToken: res.refresh, 172 | }); 173 | setItem("user", res.user); 174 | toast.success("Login successful"); 175 | onClose(); 176 | } else { 177 | throw new Error("Invalid response format"); 178 | } 179 | } else { 180 | await register(values); 181 | setLoading(false); 182 | setShowVerificationModal(true); 183 | onSignupSuccess("Registration successful! Please verify your email."); 184 | onClose(); 185 | } 186 | } catch (err: any) { 187 | handleApiError(err, selectedKey as 'login' | 'signup'); 188 | } 189 | }, 190 | }); 191 | 192 | const { getFieldProps, handleSubmit, errors, touched } = formik; 193 | 194 | 195 | 196 | function hasLetter() { 197 | return /[a-zA-Z]/.test(password); 198 | } 199 | 200 | function hasNumber() { 201 | return /[0-9]/.test(password); 202 | } 203 | 204 | function hasMinimumLength() { 205 | return password.length >= 8; 206 | } 207 | 208 | function strongPassword() { 209 | let sum = (hasLetter() ? 1 : 0) + (hasNumber() ? 1 : 0) + (hasMinimumLength() ? 1 : 0); 210 | 211 | return 100 * (sum / 3) 212 | 213 | } 214 | 215 | 216 | 217 | 218 | return ( 219 | <> 220 | {/* Sign-in/Sign-up Modal */} 221 | 228 | 229 | 230 | 231 | { 236 | setSelectedKey(key as string) 237 | setPassword('') 238 | }} 239 | color="primary" 240 | radius="lg" 241 | size="lg" 242 | > 243 | 244 | 245 | 246 | 247 | 248 |
249 | {selectedKey === "signup" && ( 250 | 258 | )} 259 | 267 | { 273 | getFieldProps("password").onChange(e) 274 | setPassword(e.target.value) 275 | }} 276 | isInvalid={touched.password && errors.password ? true : false} 277 | errorMessage={errors.password} 278 | /> 279 | 280 | {selectedKey === 'signup' &&
281 |
282 |
Weak
283 |
Strong
284 |
285 | 297 |
{hasLetter() ? : } Must contain at least one letter (lower or upper case)
298 |
{hasNumber() ? : } Must contain at least one number
299 |
{hasMinimumLength() ? : } Must contain a minimum of 8 characters
300 | 301 |
} 302 | {error && ( 303 |

{error}

304 | )} 305 | 306 | 307 | 308 | {unverifiedEmail && ( 309 | 317 | )} 318 | 321 |
322 |
323 |
324 |
325 |
326 | 327 | {/* Verification Modal */} 328 | {showVerificationModal && ( 329 | setShowVerificationModal(false)} 332 | className="backdrop-blur-xl" 333 | size="lg" 334 | // onClose={() => setShowVerificationModal(false)} 335 | > 336 | 337 | 338 |

Check Your Email

339 |
340 | 341 |

Please check your email and click the verification link to activate your account.

342 |
343 | 344 |
345 | 346 | 347 | 350 | 353 | 354 |
355 |
356 |
357 |
358 | )} 359 | 360 | ); 361 | } -------------------------------------------------------------------------------- /src/components/chats/bot-message.tsx: -------------------------------------------------------------------------------- 1 | import { Message } from "@/store/slices/chatSlice"; 2 | import Markdown from "react-markdown"; 3 | import remarkGfm from "remark-gfm"; 4 | import remarkBreaks from "remark-breaks"; 5 | import rehypeRaw from "rehype-raw"; 6 | import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; 7 | import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism"; 8 | import { useState, useEffect, useRef, memo } from "react"; 9 | import { Button } from "@nextui-org/react"; 10 | import { BiCopy, BiCheck } from "react-icons/bi"; 11 | import { useSelector, useDispatch } from "react-redux"; 12 | import { setCurrentTypingMessageId } from "@/store/slices/chatSlice"; 13 | 14 | type P = { 15 | message: Message; 16 | onTyping?: Function; 17 | parentHeight: number; 18 | parent:HTMLDivElement 19 | }; 20 | 21 | 22 | const MAX_CODE_HEIGHT = 400; 23 | 24 | interface StressButtonProps { 25 | expanded: boolean, 26 | setExpanded:Function 27 | } 28 | 29 | const StressButton = memo(({ expanded,setExpanded }: StressButtonProps) => { 30 | return 37 | }) 38 | 39 | export default function BotMessage(props: P) { 40 | const { message, onTyping,parent } = props; 41 | const dispatch = useDispatch(); 42 | const currentTypingMessageId = useSelector((state: any) => state.chat.currentTypingMessageId); 43 | const [copySuccess, setCopySuccess] = useState(null); 44 | const [displayText, setDisplayText] = useState(""); 45 | const [isCodeExpanded, setIsCodeExpanded] = useState(false); 46 | const [newMessage, setNewMessage] = useState(""); 47 | const isCurrentlyTyping = currentTypingMessageId === message.id; 48 | const ContainerRef=useRef(null) 49 | 50 | const lastScrollY=useRef(0) 51 | const ShouldScroll=useRef(true) 52 | 53 | 54 | const scrollLimited = useRef(false); 55 | const scrollContinRef = useRef(null); 56 | const handleCopy = (code: string) => { 57 | navigator.clipboard.writeText(code); 58 | setCopySuccess("Copied!"); 59 | setTimeout(() => setCopySuccess(null), 2000); 60 | }; 61 | 62 | const messageTyping = (text: string) => { 63 | let currentIndex = 0; 64 | const typingInterval = setInterval(() => { 65 | if (currentIndex < text.length) { 66 | setDisplayText(text.slice(0, currentIndex +5)); 67 | currentIndex += 5; 68 | } else { 69 | dispatch(setCurrentTypingMessageId(null)); 70 | clearInterval(typingInterval); 71 | scrollLimited.current = false; 72 | } 73 | }, 30); 74 | } 75 | 76 | useEffect(() => { 77 | if (message.text !== newMessage) { 78 | setNewMessage(message.text); 79 | } 80 | }, [message?.text]); 81 | 82 | 83 | 84 | 85 | useEffect(() => { 86 | if (newMessage !== "") { 87 | if (!isCurrentlyTyping) { 88 | // Replace `\n` with two spaces for Markdown line breaks 89 | setDisplayText(newMessage); 90 | } else { 91 | scrollLimited.current = true; 92 | setDisplayText(""); 93 | messageTyping(newMessage); 94 | } 95 | } 96 | }, [newMessage]); 97 | 98 | const isNearBottom=()=> { 99 | // return true 100 | if (parent ) { 101 | return /* parent.scrollHeight - parent.scrollTop - parent.clientHeight < 80 &&*/parent.scrollHeight - parent.scrollTop - parent.clientHeight >20; 102 | }else return false 103 | } 104 | 105 | useEffect(() => { 106 | if (scrollContinRef.current) { 107 | if ((( /* scrollContinRef.current?.clientHeight < (parentHeight - 150)||scrollContinRef.current?.clientHeight > (parentHeight - 100)|| */ShouldScroll.current) && isNearBottom() ) || !scrollLimited.current) { 108 | onTyping && onTyping() 109 | } 110 | } 111 | 112 | }, [displayText]) 113 | 114 | 115 | const onshoudscrollChange=(e:any)=>{ 116 | const currentScrollTop = e.target.scrollTop; 117 | 118 | if(currentScrollTop < lastScrollY.current){ 119 | ShouldScroll.current=false 120 | 121 | }else if(parent.scrollHeight - parent.scrollTop - parent.clientHeight < 100) { 122 | ShouldScroll.current=true 123 | } 124 | lastScrollY.current=e.target?.scrollTop 125 | } 126 | 127 | useEffect(()=>{ 128 | if(parent){ 129 | parent.addEventListener('scroll',onshoudscrollChange) 130 | return ()=>{ 131 | parent.removeEventListener('scroll',onshoudscrollChange) 132 | } 133 | } 134 | 135 | },[]) 136 | 137 | 138 | useEffect(() => { 139 | }, [isCodeExpanded]) 140 | 141 | // Guard against undefined message 142 | if (!message?.text) { 143 | return null; 144 | } 145 | 146 | return ( 147 |
148 | cerina 153 |
154 |
155 | {children as string}; 163 | // }, 164 | p: ({node, ...props}) =>

, 165 | h2: ({node, ...props}) =>

, 166 | code({ className, children, ...rest }) { 167 | const match = /language-(\w+)/.exec(className || ""); 168 | const language = match ? match[1] : ""; 169 | const code = String(children).replace(/\n$/, ""); 170 | 171 | if (!match) { 172 | return ( 173 | 174 | {children} 175 | 176 | ); 177 | } 178 | 179 | const codeLines = code.split('\n').length; 180 | const shouldScroll = codeLines > 15; 181 | 182 | return ( 183 |
184 |
185 |
186 | {language} 187 | {shouldScroll && ( 188 | 189 | {codeLines} lines 190 | 191 | )} 192 |
193 |
194 | {shouldScroll && ( 195 | <> 196 | 197 | 198 | 199 | )} 200 | 218 |
219 |
220 |
226 | )} 228 | PreTag="div" 229 | language={language} 230 | style={vscDarkPlus} 231 | customStyle={{ 232 | margin: 0, 233 | borderRadius: 0, 234 | padding: "1.5rem", 235 | }} 236 | showLineNumbers={true} 237 | wrapLines={true} 238 | > 239 | {code} 240 | 241 | {/*
*/} 242 |
243 | {!isCodeExpanded && shouldScroll && ( 244 |
setIsCodeExpanded(true)} 247 | > 248 | Click to show more 249 |
250 | )} 251 |
252 | ); 253 | }, 254 | }} 255 | > 256 | {displayText || ""} 257 | 258 |

259 |
260 | ); 261 | } 262 | -------------------------------------------------------------------------------- /src/components/chats/chat-input.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | chatWithExistingConversation, 3 | newChat, 4 | } from "@/services/dispatch/chat-dispatch"; 5 | import { useAppDispatch, useAppSelector } from "@/store/hooks"; 6 | import { 7 | addBotMessage, 8 | addUserMessage, 9 | setBotResponseLoading, 10 | } from "@/store/slices/chatSlice"; 11 | import { Button, Input, Popover, PopoverContent, PopoverTrigger, Tab, Tabs, Textarea, Tooltip } from "@nextui-org/react"; 12 | import moment from "moment"; 13 | import { ChangeEvent, KeyboardEvent, useEffect, useRef, useState } from "react"; 14 | import { BsMic, BsStopFill } from "react-icons/bs"; 15 | import { IoMdSend } from "react-icons/io"; 16 | import { MdDeleteOutline, MdOutlineAttachFile, MdUnarchive } from "react-icons/md"; 17 | import { RiBook2Line } from "react-icons/ri"; 18 | import { useLocation, useNavigate, useParams } from "react-router-dom"; 19 | import CustomModal from "../customModal"; 20 | import { AiOutlineArrowRight, AiOutlinePlus } from "react-icons/ai"; 21 | import { TiArrowRightOutline } from "react-icons/ti"; 22 | import { useSpeechToText } from "@/hooks/use-speech2text"; 23 | import { getItem, setItem } from "@/services/session"; 24 | import { useArchive } from "@/hooks/use-archive"; 25 | 26 | export default function ChatInput() { 27 | const { botResponseLoading, currentTypingMessageId } = useAppSelector((state) => state.chat); 28 | const dispatch = useAppDispatch(); 29 | const location = useLocation(); 30 | const { id } = useParams(); 31 | const navigate = useNavigate(); 32 | const [modalVisible, setModalVisible] = useState(false) 33 | 34 | const [currentTab, setCurrentTab] = useState(1) 35 | 36 | 37 | const [TooltipOpen, setTooltipOpen] = useState('') 38 | 39 | const { transcript, isListening, isLoading, toggleListening } = useSpeechToText() 40 | 41 | 42 | 43 | const [form, setForm] = useState({ 44 | query: "", 45 | file: null as File | null, 46 | }); 47 | const fileRef = useRef(null); 48 | 49 | const handleTextChange = (e: ChangeEvent) => { 50 | setForm((prev) => ({ ...prev, query: e.target.value })); 51 | }; 52 | 53 | const handleFileSelect = (e: ChangeEvent) => { 54 | const file = e.target.files ? e.target.files[0] : null; 55 | setForm((prev) => ({ ...prev, file })); 56 | }; 57 | 58 | // Input key event 59 | const handleKeyDown = (event: KeyboardEvent) => { 60 | if (event.key === "Enter") { 61 | if (event.shiftKey) { 62 | // Allow shift+enter to insert a new line (default behavior) 63 | return; 64 | } else { 65 | // Prevent default behavior and trigger custom function 66 | event.preventDefault(); 67 | sendMessage(); 68 | } 69 | } 70 | }; 71 | 72 | 73 | const sendMessage = (messageToSend: string = form.query) => { 74 | console.log({ 75 | currentTypingMessageId, 76 | botResponseLoading 77 | }); 78 | 79 | if (currentTypingMessageId || botResponseLoading) { 80 | setTooltipOpen('Cerina is typing, please wait'); 81 | return; 82 | } else if (messageToSend === '') { 83 | setTooltipOpen('Please input text'); 84 | return; 85 | } 86 | 87 | dispatch( 88 | addUserMessage({ 89 | role: "user", 90 | text: messageToSend, 91 | timestamp: moment().toISOString(), 92 | }) 93 | ); 94 | 95 | setForm({ query: "", file: null }); 96 | 97 | // Call chat API here 98 | const formdata = new FormData(); 99 | formdata.append("query", messageToSend); 100 | formdata.append("file", form.file as Blob); 101 | 102 | if (messageToSend.toLowerCase().includes('search')) { 103 | dispatch(setBotResponseLoading('Searching')); 104 | } else { 105 | dispatch(setBotResponseLoading('Analyzing')); 106 | } 107 | 108 | if (location.pathname === "/") { 109 | handleNewChat(formdata); 110 | } else { 111 | if (id) { 112 | handleExistingChat(formdata); 113 | } 114 | } 115 | }; 116 | 117 | // message with existing chat 118 | const handleExistingChat = (formdata: FormData) => { 119 | chatWithExistingConversation({ conversationId: id!, formdata }) 120 | .then((res) => { 121 | console.log(res) 122 | dispatch( 123 | addBotMessage({ 124 | conversation_id: res?.conversation_id, 125 | message: { 126 | role: "model", 127 | text: res?.model_response, 128 | timestamp: moment().toISOString(), 129 | }, 130 | }) 131 | ); 132 | dispatch(setBotResponseLoading('')); 133 | }) 134 | .catch((err) => { 135 | console.log(err); 136 | dispatch(setBotResponseLoading('')); 137 | }); 138 | }; 139 | 140 | // Start New chat 141 | const handleNewChat = (formdata: FormData) => { 142 | newChat(formdata) 143 | .then((res) => { 144 | console.log(res) 145 | dispatch( 146 | addBotMessage({ 147 | conversation_id: res?.conversation_id, 148 | message: { 149 | role: "model", 150 | text: res?.model_response, 151 | timestamp: moment().toISOString(), 152 | }, 153 | }) 154 | ); 155 | navigate("/c/" + res?.conversation_id); 156 | }) 157 | .catch((err) => { 158 | console.log(err); 159 | dispatch(setBotResponseLoading(false)); 160 | }); 161 | }; 162 | 163 | 164 | const [promptValue, setPromptValue] = useState('') 165 | const [prompts, setPrompts] = useState([]) 166 | const [promptSearchKey, setPromptSearchKey] = useState('') 167 | const [Community_prompts, setCommunity_Prompts] = useState([ 168 | { 169 | name: 'Fix grammar Error', 170 | content: 'Fix grammar errors in the text. Source: Tony Dinh' 171 | }, 172 | { 173 | name: 'Fix grammar Error', 174 | content: 'Fix grammar errors in the text. Source:Alpha Star' 175 | }, 176 | { 177 | name: 'Fix grammar Error', 178 | content: 'Fix grammar errors in the text. Source:Ahammed Danish' 179 | } 180 | ]) 181 | const [Community_promptSearchKey, setCommunity_PromptSearchKey] = useState('') 182 | 183 | const { archivedchats,setArchivedChats}=useArchive() 184 | 185 | 186 | const handlePromptAdd = (value: string = promptValue) => { 187 | if (prompts.includes(value) || value === '') { 188 | setPromptValue('') 189 | return 190 | } 191 | setPrompts([value, ...prompts]) 192 | setPromptValue('') 193 | localStorage.setItem('prompts', JSON.stringify([value, ...prompts])) 194 | } 195 | const handlePromptDelete = (value: string) => { 196 | const tt = prompts.filter(val => val !== value) 197 | setPrompts(tt) 198 | localStorage.setItem('prompts', JSON.stringify(tt)) 199 | } 200 | 201 | useEffect(() => { 202 | const prompts = getItem('prompts') 203 | 204 | if (prompts) { 205 | setPrompts(prompts) 206 | } 207 | 208 | }, []) 209 | 210 | const handleUnArchive = () => { 211 | setItem('archivedChats',archivedchats.filter(arch=>arch.id!==id)) 212 | setArchivedChats(archivedchats.filter(arch=>arch.id!==id)) 213 | } 214 | 215 | 216 | 217 | 218 | const handleUsePrompt = (value: string) => { 219 | if (value) { 220 | const newMessage = `I am sharing a prompt template with you! You need to act according to it.\n\n Here is the template.\n\n ${value}`; 221 | setModalVisible(false); 222 | 223 | // Update the form state and pass newMessage to sendMessage directly 224 | setForm((prev) => ({ 225 | ...prev, 226 | query: newMessage, 227 | })); 228 | sendMessage(newMessage); 229 | } 230 | }; 231 | 232 | useEffect(() => { 233 | console.log(transcript) 234 | setForm((prev) => ({ ...prev, query: transcript })); 235 | }, [transcript]) 236 | 237 | return ( 238 |
239 | { archivedchats.map(v=>v.id).includes(id as string)?
240 | 241 |
:
242 | 248 | 251 | 254 |
255 | 256 | 257 |