├── .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 |
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 | handleResendVerification(formik.values['email'])}>
348 | Resend
349 |
350 | window.open("https://mail.google.com", "_blank")}>
351 | Open Email
352 |
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 setExpanded(!expanded) }
33 | className="bg-default-200 hover:bg-default-300"
34 | >
35 | {expanded ? 'Collapse' : 'Expand'}
36 |
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 |
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 |
handleCopy(code)}
204 | className="bg-default-200 hover:bg-default-300"
205 | >
206 | {copySuccess ? (
207 |
208 |
209 | Copied!
210 |
211 | ) : (
212 |
213 |
214 | Copy Code
215 |
216 | )}
217 |
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 | Unarchive Chat
241 |
:
242 |
248 |
setModalVisible(true)}>
249 |
250 |
251 |
252 | {isListening ? : }
253 |
254 |
255 |
256 |
257 |
267 |
268 |
setTooltipOpen('')} placement='top-end' isOpen={(!!TooltipOpen)} >
269 |
270 | sendMessage()}
276 | >
277 |
278 |
279 |
280 |
281 |
282 | {TooltipOpen}
283 |
284 |
285 |
286 |
287 |
fileRef.current?.click()}
291 | >
292 |
293 |
294 |
}
295 |
setModalVisible(false)} >
296 |
297 |
Prompt Library
298 | Prompts are message templates that you can quickly fill in the chat input. Some prompts come with variables.
299 |
300 | {
306 | setCurrentTab(key as number)
307 | }}
308 | classNames={{
309 | tabList: "gap-6 w-full flex justify-between relative rounded-none p-0 border-b border-divider",
310 | cursor: "w-full bg-[#3CFF00]",
311 | tab: "max-w-fit px-8 h-12 w-1/2",
312 | tabContent: "group-data-[selected=true]:text-[#3CFF00]"
313 | }}
314 | >
315 |
320 | Your Prompts
321 |
322 | }
323 | />
324 |
329 | Community Prompts
330 |
331 | }
332 | />
333 |
334 |
335 | {currentTab == 1 ?
336 |
337 |
setPromptValue(e.target.value)} classNames={{
338 | inputWrapper: 'bg-default focus:bg-default'
339 | }} autoFocus color="default" />
340 |
handlePromptAdd()} className="flex whitespace-nowrap bg-transparent items-center px-2 text-sm" > Add Prompts
341 |
342 |
343 |
344 | setPromptSearchKey(e.target.value)} classNames={{
345 | inputWrapper: 'bg-default focus:bg-default'
346 | }} />
347 |
348 |
349 | {prompts.length ?
350 | {
351 | prompts.filter(val => val.toLocaleLowerCase().includes(promptSearchKey)).map((val: string, id) =>
352 |
{val}
353 |
354 |
355 |
356 | handleUsePrompt(val)} >
357 |
358 |
359 |
360 |
361 | handlePromptDelete(val)} >
362 |
363 |
364 |
365 |
366 |
367 |
368 |
)
369 | }
370 |
:
371 |
372 | You have no saved prompts. Tap “Add Prompt” to add new Prompts
}
373 |
374 |
:
375 |
376 |
377 |
setCommunity_PromptSearchKey(e.target.value)} autoFocus />
378 |
379 |
380 | {Community_prompts
381 | .filter(value => value.content.toLocaleLowerCase().includes(Community_promptSearchKey))
382 | .map((v, i) =>
383 |
384 |
385 |
386 | {v.name}
387 |
388 |
389 | {v.content}
390 |
391 |
392 |
393 |
handleUsePrompt(v.content)} className="font-bold m-1 " >
394 | Use
395 |
396 | {prompts.includes(v.content) ?
handlePromptDelete(v.content)} className="font-bold m-1">
397 | Added
398 | :
handlePromptAdd(v.content)} className="font-bold m-1">
399 | Add
400 | }
401 |
402 |
)}
403 |
404 |
405 |
406 |
407 | }
408 |
409 |
410 |
411 | );
412 | }
413 |
--------------------------------------------------------------------------------
/src/components/chats/chat-list.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 | import { useArchive } from "@/hooks/use-archive";
3 | import { useAuth } from "@/hooks/use-auth";
4 | import { deleteChatHistory, getChatHistory } from "@/services/dispatch/chat-dispatch";
5 | import { setItem } from "@/services/session";
6 | import { useAppDispatch } from "@/store/hooks";
7 | import { setMessages } from "@/store/slices/chatSlice";
8 | import { downloadHistory } from "@/utils";
9 | import { Dropdown, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, useDisclosure, DropdownTrigger, DropdownMenu, DropdownItem, Button, Listbox, ListboxItem } from "@nextui-org/react";
10 | import { useState } from "react";
11 | import { BsThreeDots, BsTrash } from "react-icons/bs";
12 | import { IoArchiveOutline, IoSaveOutline } from "react-icons/io5";
13 | import { useQuery } from "react-query";
14 | import { useNavigate, useParams } from "react-router-dom";
15 |
16 | export interface ArchivedChat{
17 | name:string;
18 | id:string;
19 | created:Date
20 | }
21 |
22 | export default function ChatList() {
23 | const { isLoggedIn } = useAuth();
24 | const { isLoading, data } = useQuery({
25 | queryKey: ["chat-list"],
26 | queryFn: getChatHistory,
27 | enabled: isLoggedIn,
28 | });
29 | const navigate = useNavigate()
30 | const { isOpen, onOpen, onOpenChange } = useDisclosure();
31 | const [selectedHistory, selecteHistory] = useState('')
32 | const { id } = useParams()
33 | const dispatch = useAppDispatch()
34 |
35 | const { archivedchats,setArchivedChats }=useArchive()
36 |
37 |
38 | const handleDivClick = (url: string) => {
39 | navigate(url)
40 | // window.location.href = url; // Navigate to the specified page
41 | };
42 |
43 | const handleDelete = async () => {
44 | if (!selectedHistory) return
45 | const res = await deleteChatHistory(selectedHistory)
46 | if (res) {
47 | data.conversations = data?.conversations.filter((item: any) => item?.conversation_id !== selectedHistory)
48 |
49 | if (id === selectedHistory) {
50 | dispatch(setMessages([]));
51 |
52 | navigate('/')
53 | }
54 | selecteHistory('')
55 | }
56 | };
57 |
58 |
59 |
60 | return (<>
61 |
62 | {isLoading ? (
63 |
64 | Loading...
65 |
66 | ) : (
67 | data?.conversations?.filter((val:any)=>archivedchats?!archivedchats.map(arch=>arch.id).includes(val?.conversation_id):false).map((item: any) => (
68 | handleDivClick(`/c/${item?.conversation_id}`)}
73 | >
74 |
75 |
{item?.conversation_name || "Unknown"}
76 |
77 |
81 |
82 | {
83 | e.stopPropagation()
84 | // handleDelete(item?.conversation_id)
85 | }} >
86 |
87 |
88 |
89 |
90 | {
91 | setArchivedChats(([...archivedchats, { name:item.conversation_name||'Unknown',id:item.conversation_id,created:Date.now()}] as ArchivedChat[]))
92 | setItem('archivedChats', [...archivedchats, { name:item.conversation_name||'Unknown',id:item.conversation_id,created:Date.now()}] as ArchivedChat[])
93 |
94 | if (id === item.conversation_id) {
95 | dispatch(setMessages([]));
96 |
97 | navigate('/')
98 | }
99 | selecteHistory('')
100 | }} className=" text-center " >
101 |
102 | Archive
103 |
104 |
105 |
106 | {
107 | downloadHistory(item.conversation_id)
108 | }} className=" text-center " >
109 |
110 | Download
111 |
112 |
113 |
114 |
115 | {
116 | onOpen()
117 | selecteHistory(item.conversation_id)
118 | }} className="text-danger bg-background text-center " color="danger" >
119 |
120 | Delete
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 | ))
129 | )}
130 |
131 |
132 |
133 | {(onClose) => (
134 | <>
135 | Delete chat?
136 |
137 |
138 | Are you sure you want to delete this chat history? This action cannot be undone.
139 |
140 |
141 |
142 |
143 |
144 | Close
145 |
146 | {
147 | onClose()
148 | handleDelete()
149 | }}>
150 | Delete
151 |
152 |
153 | >
154 | )}
155 |
156 |
157 | >
158 | );
159 | }
160 |
--------------------------------------------------------------------------------
/src/components/chats/chat-messages.tsx:
--------------------------------------------------------------------------------
1 | import { Chat } from "@/store/slices/chatSlice";
2 | import { useEffect, useRef, useState } from "react";
3 | import BotMessage from "./bot-message";
4 | import UserMessage from "./user-message";
5 | import { Spacer } from "@nextui-org/react";
6 | import { AiOutlineArrowDown } from "react-icons/ai";
7 |
8 | type P = {
9 | data: Chat;
10 | };
11 |
12 | export default function ChatMessages(props: P) {
13 | const { data } = props;
14 | const bottomRef = useRef(null);
15 |
16 | const [isFirstRender, setIsFirstRender] = useState(true);
17 | const containRef = useRef(null);
18 | const scrollButton = useRef(null);
19 |
20 |
21 |
22 | const handleScrollDwon = () => {
23 | if (containRef.current) {
24 | containRef.current.scrollTo({
25 | left: 0,
26 | top: containRef.current.scrollHeight,
27 | behavior: "smooth"
28 | })
29 | }
30 | }
31 |
32 |
33 | useEffect(() => {
34 | if (!isFirstRender) {
35 | handleScrollDwon()
36 | }
37 | setIsFirstRender(false);
38 | }, [data?.messages]);
39 |
40 | useEffect(() => {
41 | let interval: any;
42 | const observer = new IntersectionObserver(
43 | (entries) => {
44 | entries.forEach((entry) => {
45 | if (entry.target === bottomRef.current && scrollButton.current) {
46 | if (interval)
47 | clearTimeout(interval);
48 | if (scrollButton.current.hidden) {
49 | interval = setTimeout(() => {
50 | if (scrollButton.current)
51 | scrollButton.current.hidden = entry.isIntersecting;
52 | }, 600)
53 | } else {
54 | scrollButton.current.hidden = entry.isIntersecting;
55 | }
56 | }
57 | });
58 | },
59 | {
60 | root: null,
61 | threshold: 0.1,
62 | }
63 | );
64 |
65 | if (bottomRef.current) {
66 | observer.observe(bottomRef.current);
67 | }
68 |
69 | return () => {
70 | if (bottomRef.current) observer.unobserve(bottomRef.current);
71 | };
72 | }, []);
73 |
74 |
75 | return (
76 | <>
77 |
78 |
79 |
80 | {data?.loading ? (
81 |
82 |
Loading messages...
83 |
84 | ) : (
85 | <>
86 | {data?.messages?.map((item, index) => {
87 | if (item.role === "model") {
88 | return (
89 |
96 | );
97 | } else {
98 | return (
99 |
103 | );
104 | }
105 | })}
106 | {data?.botResponseLoading && (
107 |
108 |
113 |
114 |
{data?.botResponseLoading}...
115 |
116 |
117 | )}
118 | {data?.isError && (
119 |
120 |
125 |
126 |
127 | {data?.errorMessage || "An error occurred. Please try again."}
128 |
129 |
130 |
131 | )}
132 | >
133 | )}
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 | >
142 | );
143 | }
--------------------------------------------------------------------------------
/src/components/chats/serach-chats.tsx:
--------------------------------------------------------------------------------
1 | export default function SearchChats() {
2 | return serach-chats
;
3 | }
4 |
--------------------------------------------------------------------------------
/src/components/chats/user-message.tsx:
--------------------------------------------------------------------------------
1 | import { chatWithExistingConversation } from "@/services/dispatch/chat-dispatch";
2 | import { useAppDispatch, useAppSelector } from "@/store/hooks";
3 | import { addBotMessage, addUserMessage, Message, setBotResponseLoading } from "@/store/slices/chatSlice";
4 | import { Button, Textarea } from "@nextui-org/react";
5 | import moment from "moment";
6 | import { ChangeEvent, useState, KeyboardEvent } from "react";
7 | import { MdModeEditOutline } from "react-icons/md";
8 | import { useParams } from "react-router-dom";
9 |
10 | type P = {
11 | message: Message;
12 | };
13 | export default function UserMessage(props: P) {
14 | const { message } = props;
15 | const [buttonVis, setButtonVis] = useState(false)
16 | const [editing, setEditing] = useState(false)
17 | const [inputValue, setInputValue] = useState('')
18 |
19 | const { botResponseLoading, currentTypingMessageId } = useAppSelector((state) => state.chat);
20 | const dispatch = useAppDispatch();
21 |
22 | const { id } = useParams();
23 |
24 |
25 | const handleTextChange = (e: ChangeEvent) => {
26 | setInputValue(e.target.value)
27 | };
28 |
29 |
30 | // Input key event
31 | const handleKeyDown = (event: KeyboardEvent) => {
32 | if (event.key === "Enter") {
33 | if (event.shiftKey) {
34 | return;
35 | } else {
36 | // sendMessage();
37 | }
38 | }
39 | };
40 |
41 |
42 | const handleSend = () => {
43 | setEditing(false)
44 | setButtonVis(false)
45 | if (currentTypingMessageId || botResponseLoading) {
46 | console.log('Cerina is typing, please wait');
47 | return;
48 | } else if (inputValue === '') {
49 | console.log('Please input text');
50 | return;
51 | }
52 |
53 | dispatch(
54 | addUserMessage({
55 | role: "user",
56 | text: inputValue,
57 | timestamp: moment().toISOString(),
58 | })
59 | );
60 |
61 |
62 | // Call chat API here
63 | const formdata = new FormData();
64 | formdata.append("query", inputValue);
65 | formdata.append("file", (null as File|null) as Blob);
66 |
67 | if (inputValue.toLowerCase().includes('search')) {
68 | dispatch(setBotResponseLoading('Searching'));
69 | } else {
70 | dispatch(setBotResponseLoading('Analyzing'));
71 | }
72 | handleExistingChat(formdata);
73 | }
74 |
75 | const handleExistingChat = (formdata: FormData) => {
76 | chatWithExistingConversation({ conversationId: id!, formdata })
77 | .then((res) => {
78 | console.log(res)
79 | dispatch(
80 | addBotMessage({
81 | conversation_id: res?.conversation_id,
82 | message: {
83 | role: "model",
84 | text: res?.model_response,
85 | timestamp: moment().toISOString(),
86 | },
87 | })
88 | );
89 | dispatch(setBotResponseLoading(''));
90 | })
91 | .catch((err) => {
92 | console.log(err);
93 | dispatch(setBotResponseLoading(''));
94 | });
95 | };
96 |
97 | return (
98 | <>
99 | {editing ?
102 |
120 |
121 |
122 | {
123 | setEditing(false)
124 | setButtonVis(false)
125 | }} >Cancel
126 | Send
127 |
128 |
: setButtonVis(true)} onMouseLeave={() => setButtonVis(false)} className=" relative text-right max-w-full py-2 px-3 bg-default w-fit self-end rounded-lg break-words">
129 |
130 |
') }} >
131 |
132 |
{
133 | setEditing(true)
134 | setInputValue(message.text)
135 | }} size="sm" isIconOnly className={`${buttonVis ? ' visible ' : ' invisible'} absolute bg-default-400 right-0 bottom-0`} >
136 |
137 |
138 |
139 |
140 |
}
141 | >
142 | );
143 | }
144 |
--------------------------------------------------------------------------------
/src/components/customModal.tsx:
--------------------------------------------------------------------------------
1 | import React, { MouseEventHandler } from "react"
2 |
3 |
4 | interface props {
5 | isOpen?: boolean,
6 | onClose?: MouseEventHandler,
7 | children?: React.ReactNode
8 | height?:string
9 | width?:string
10 | }
11 |
12 | const CustomModal = (p: props) => {
13 | const { children = <>>, isOpen = false, onClose = () => { },height='',width='' } = p
14 | return isOpen?(
15 | <>
16 |
17 |
18 |
19 |
20 |
21 | {children}
22 |
23 |
24 | >
25 |
26 | ):<>>
27 | }
28 |
29 | export default CustomModal
--------------------------------------------------------------------------------
/src/components/header.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Avatar,
3 | Button,
4 | Dropdown,
5 | DropdownItem,
6 | DropdownMenu,
7 | DropdownTrigger,
8 | Select,
9 | SelectItem,
10 | } from "@nextui-org/react";
11 | import { BsLayoutSidebar } from "react-icons/bs";
12 | import { ThemeSwitch } from "./theme-switch";
13 | import { useAuth } from "@/hooks/use-auth";
14 | import { MdLogout } from "react-icons/md";
15 | import { deleteSession } from "@/services/session";
16 | import { useNavigate } from "react-router-dom";
17 |
18 | type P = {
19 | toggleSideBar: () => void;
20 | isOpen:boolean
21 | };
22 | export default function Header(props: P) {
23 | const { toggleSideBar } = props;
24 | const { loading, user, isLoggedIn, setIsLoggedIn, setUser } = useAuth();
25 | const navigate = useNavigate();
26 |
27 | const handlelogout = () => {
28 | deleteSession();
29 | setUser(null);
30 | setIsLoggedIn(false);
31 | navigate("/");
32 | };
33 |
34 | if (loading) {
35 | return null;
36 | }
37 | return (
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | Cerina
46 |
47 |
48 |
49 |
50 |
51 |
52 | {isLoggedIn && (
53 |
54 |
55 |
60 |
61 |
62 | navigate("/profile")}
65 | >
66 | Profile
67 |
68 | navigate(0)} >Docs
69 | navigate('/setting?id=Your_APIs')} >Your API's
70 | }
74 | >
75 | Logout
76 |
77 |
78 |
79 | )}
80 |
81 |
82 |
83 | );
84 | }
85 |
--------------------------------------------------------------------------------
/src/components/icons.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { IconSvgProps } from "@/types";
4 |
5 | export const Logo: React.FC = ({
6 | size = 36,
7 | height,
8 | ...props
9 | }) => (
10 |
17 |
23 |
24 | );
25 |
26 | export const DiscordIcon: React.FC = ({
27 | size = 24,
28 | width,
29 | height,
30 | ...props
31 | }) => {
32 | return (
33 |
39 |
43 |
44 | );
45 | };
46 |
47 | export const TwitterIcon: React.FC = ({
48 | size = 24,
49 | width,
50 | height,
51 | ...props
52 | }) => {
53 | return (
54 |
60 |
64 |
65 | );
66 | };
67 |
68 | export const GithubIcon: React.FC = ({
69 | size = 24,
70 | width,
71 | height,
72 | ...props
73 | }) => {
74 | return (
75 |
81 |
87 |
88 | );
89 | };
90 |
91 | export const MoonFilledIcon = ({
92 | size = 24,
93 | width,
94 | height,
95 | ...props
96 | }: IconSvgProps) => (
97 |
106 |
110 |
111 | );
112 |
113 | export const SunFilledIcon = ({
114 | size = 24,
115 | width,
116 | height,
117 | ...props
118 | }: IconSvgProps) => (
119 |
128 |
129 |
130 |
131 |
132 |
133 | );
134 |
135 | export const HeartFilledIcon = ({
136 | size = 24,
137 | width,
138 | height,
139 | ...props
140 | }: IconSvgProps) => (
141 |
150 |
157 |
158 | );
159 |
160 | export const SearchIcon = (props: IconSvgProps) => (
161 |
171 |
178 |
185 |
186 | );
187 |
188 | export const NextUILogo: React.FC = (props) => {
189 | const { width, height = 40 } = props;
190 |
191 | return (
192 |
200 |
204 |
208 |
212 |
213 | );
214 | };
215 |
--------------------------------------------------------------------------------
/src/components/logo.tsx:
--------------------------------------------------------------------------------
1 | export default function Logo() {
2 | return (
3 |
4 | {/* First Image */}
5 |
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/src/components/primitives.ts:
--------------------------------------------------------------------------------
1 | import { tv } from "tailwind-variants";
2 |
3 | export const title = tv({
4 | base: "tracking-tight inline font-semibold",
5 | variants: {
6 | color: {
7 | violet: "from-[#FF1CF7] to-[#b249f8]",
8 | yellow: "from-[#FF705B] to-[#FFB457]",
9 | blue: "from-[#5EA2EF] to-[#0072F5]",
10 | cyan: "from-[#00b7fa] to-[#01cfea]",
11 | green: "from-[#6FEE8D] to-[#17c964]",
12 | pink: "from-[#FF72E1] to-[#F54C7A]",
13 | foreground: "dark:from-[#FFFFFF] dark:to-[#4B4B4B]",
14 | },
15 | size: {
16 | sm: "text-3xl lg:text-4xl",
17 | md: "text-[2.3rem] lg:text-5xl leading-9",
18 | lg: "text-4xl lg:text-6xl",
19 | },
20 | fullWidth: {
21 | true: "w-full block",
22 | },
23 | },
24 | defaultVariants: {
25 | size: "md",
26 | },
27 | compoundVariants: [
28 | {
29 | color: [
30 | "violet",
31 | "yellow",
32 | "blue",
33 | "cyan",
34 | "green",
35 | "pink",
36 | "foreground",
37 | ],
38 | class: "bg-clip-text text-transparent bg-gradient-to-b",
39 | },
40 | ],
41 | });
42 |
43 | export const subtitle = tv({
44 | base: "w-full md:w-1/2 my-2 text-lg lg:text-xl text-default-600 block max-w-full",
45 | variants: {
46 | fullWidth: {
47 | true: "!w-full",
48 | },
49 | },
50 | defaultVariants: {
51 | fullWidth: true,
52 | },
53 | });
54 |
--------------------------------------------------------------------------------
/src/components/services/services-sidebar-btn.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Button,
3 | Dropdown,
4 | DropdownItem,
5 | DropdownMenu,
6 | DropdownTrigger,
7 | } from "@nextui-org/react";
8 | import { Key, useState } from "react";
9 | import { MdOutlineMiscellaneousServices } from "react-icons/md";
10 |
11 | export default function ServicesSidebarBtn() {
12 | const [showMore, setShowMore] = useState(false);
13 | const [isOpen, setIsOpen] = useState(false);
14 |
15 | const handleMenuClick = (key: Key) => {
16 | if (key === "more") {
17 | setShowMore(true);
18 | } else {
19 | setIsOpen(false);
20 | }
21 | };
22 |
23 | return (
24 | {
31 | setShowMore(false);
32 | setIsOpen(false);
33 | }}
34 | isOpen={isOpen}
35 | placement="right"
36 | >
37 | {
39 | setIsOpen((prev) => !prev);
40 | }}
41 | >
42 |
47 |
48 | Services
49 |
50 |
51 |
63 | }
66 | >
67 | AI Stock Predictor
68 |
69 | }
72 | >
73 | AI Doctor
74 |
75 | }
78 | >
79 | AI Researcher
80 |
81 |
85 | More...
86 |
87 | }
90 | className={`${showMore ? "flex" : "hidden"}`}
91 | >
92 | AI Lawyer
93 |
94 | }
97 | className={`${showMore ? "flex" : "hidden"}`}
98 | >
99 | AI Prompt Optimizer
100 |
101 | }
104 | className={`${showMore ? "flex" : "hidden"}`}
105 | >
106 | AI Clinical Notes
107 |
108 | }
111 | className={`${showMore ? "flex" : "hidden"}`}
112 | >
113 | Add your own
114 |
115 |
116 |
117 | );
118 | }
--------------------------------------------------------------------------------
/src/components/setting/APIs.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Button,
3 | Divider,
4 | Input,
5 | Radio,
6 | RadioGroup,
7 | Tab,
8 | Table,
9 | TableBody,
10 | TableCell,
11 | TableColumn,
12 | TableHeader,
13 | TableRow,
14 | Tabs,
15 | } from "@nextui-org/react";
16 | import { useState } from "react";
17 | import { RenderAPITableCell } from "./renderTableCell";
18 | import CustomModal from "../customModal";
19 |
20 |
21 |
22 | const APIsetting = () => {
23 | const [createModal, setCreateModal] = useState(false);
24 |
25 | return <>
26 | Create new API key
27 |
32 | You
33 | Service Account
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | New
43 |
44 |
45 | Permissions
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
62 | {
66 | setCreateModal(true);
67 | }}
68 | >
69 | Create API Key
70 |
71 |
72 |
73 |
74 |
75 |
76 |
82 |
92 | {(column) => (
93 |
94 | {column.name}
95 |
96 | )}
97 |
98 |
136 | {(item) => (
137 |
138 | {(columnKey) => {RenderAPITableCell(item, columnKey)} }
139 |
140 | )}
141 |
142 |
143 | {
147 | setCreateModal(false);
148 | }}
149 | >
150 |
151 | Your Generated API Key
152 |
153 |
154 | Lorem ipsum dolor sit amet consectetur. Aliquam eu praesent faucibus
155 | morbi dolor mi. Feugiat id at ornare at donec ante massa. Sit volutpat
156 | elementum et consequat amet aliquet scelerisque. Hendrerit amet mauris
157 | quis quis faucibus scelerisque risus.
158 |
159 |
160 |
161 |
170 |
171 | Copy
172 |
173 |
174 |
175 | >
176 | }
177 |
178 | export default APIsetting
--------------------------------------------------------------------------------
/src/components/setting/general.tsx:
--------------------------------------------------------------------------------
1 | import { useTheme } from "@/hooks/use-theme";
2 | import { deleteChatHistory, getChatHistory } from "@/services/dispatch/chat-dispatch";
3 | import { getItem, setItem } from "@/services/session";
4 | import {
5 | Button,
6 | Divider,
7 | Popover,
8 | PopoverContent,
9 | PopoverTrigger,
10 | Select,
11 | SelectItem,
12 | Switch,
13 | Table,
14 | TableBody,
15 | TableCell,
16 | TableColumn,
17 | TableHeader,
18 | TableRow
19 | } from "@nextui-org/react";
20 | import CustomModal from "../customModal";
21 | import { useState } from "react";
22 | import { ArchivedChat } from "../chats/chat-list";
23 | import { RenderArchivedChatsTableCell } from "./renderTableCell";
24 | import { useNavigate } from "react-router-dom";
25 | import { useArchive } from "@/hooks/use-archive";
26 |
27 |
28 | const GeneralSetting=()=>{
29 |
30 | const { theme, setLightTheme,setDarkTheme } = useTheme()
31 |
32 | const [manageModal,setManageModal]=useState(false)
33 |
34 | const [tooltip,setTooltip]=useState(false)
35 |
36 | const { archivedchats ,setArchivedChats } = useArchive()
37 |
38 | const navigate=useNavigate()
39 |
40 |
41 | const handleArchieveAll = async() => {
42 | const histories=await getChatHistory()
43 |
44 | setItem('archivedChats',histories?.conversations.map((v:any)=>({id:v.conversation_id,name:v.conversation_name,created:Date.now()})))
45 |
46 | }
47 |
48 |
49 | const handleUnarchive = (current:string) => {
50 | const chats:ArchivedChat[]=getItem('archivedChats')
51 | setItem('archivedChats',chats.filter(chat=>chat.id!==current))
52 | setArchivedChats(chats.filter(chat=>chat.id!==current))
53 | }
54 | const handleDeleteConversation =async (current:string) => {
55 | const chats:ArchivedChat[]=getItem('archivedChats')
56 | setItem('archivedChats',chats.filter(chat=>chat.id!==current))
57 | setArchivedChats(chats.filter(chat=>chat.id!==current))
58 | await deleteChatHistory(current)
59 | }
60 |
61 |
62 | return <>
63 |
Theme
64 |
65 | {
69 | if([...key][0]==='light'){
70 | setLightTheme()
71 | }else{
72 | setDarkTheme()
73 | }
74 | }}
75 | items={[
76 | { key: "light", label: "Light" },
77 | { key: "dark", label: "Dark" },
78 | ]}
79 | // label="Favorite Animal"
80 |
81 | size="lg"
82 | placeholder="Select an theme"
83 | className=" w-40 max-xs:w-full"
84 | >
85 | {(opt: { key: string; label: string }) => (
86 | {opt.label}
87 | )}
88 |
89 |
90 |
91 |
92 |
93 |
Always show code while using data analytics
94 |
95 |
100 |
101 |
102 |
103 |
104 |
Language
105 |
106 |
124 | {(opt: { key: string; label: string }) => (
125 | {opt.label}
126 | )}
127 |
128 |
129 |
130 |
131 |
132 |
Archieved chat
133 |
134 | setManageModal(true)} className="w-full">Manage
135 |
136 |
137 |
138 |
Archive all chats
139 |
140 |
setTooltip(false)} placement="top-end" showArrow >
141 |
142 | setTooltip(true)} className="w-full">Archieve all
143 |
144 |
145 |
146 |
Are you sure?
147 |
Archive all chat history.
148 |
{
149 | handleArchieveAll()
150 | setTooltip(false)
151 | }} size="sm" color="danger" >Ok
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
Delete chat
161 |
162 | Delete all
163 |
164 |
165 |
166 | {
171 | setManageModal(false);
172 | }}
173 | >
174 | Archeved Chats
175 |
176 |
183 |
192 | {(column) => (
193 |
194 | {column.name}
195 |
196 | )}
197 |
198 |
199 | {(item) => (
200 |
201 | {(columnKey) => {RenderArchivedChatsTableCell(item,columnKey,handleUnarchive,handleDeleteConversation,navigate)} }
202 |
203 | )}
204 |
205 |
206 |
207 |
208 |
209 | >
210 | }
211 |
212 | export default GeneralSetting
--------------------------------------------------------------------------------
/src/components/setting/renderTableCell.tsx:
--------------------------------------------------------------------------------
1 | import { Popover, PopoverContent, PopoverTrigger, Tooltip } from "@nextui-org/react";
2 | import React, { useState } from "react";
3 | import { IoCheckmarkOutline, IoCopyOutline, IoEyeOutline } from "react-icons/io5";
4 | import { MdDeleteOutline, MdUnarchive } from "react-icons/md";
5 | import { ArchivedChat } from "../chats/chat-list";
6 | import { BiConversation } from "react-icons/bi";
7 |
8 |
9 |
10 | interface APIProps {
11 | name?: string,
12 | key?: string,
13 | permission?: string
14 | }
15 |
16 |
17 |
18 |
19 | const CopyButton = (props: { copyvalue: string }) => {
20 | const [copying, setCopying] = useState(false)
21 | const copy = () => {
22 | setCopying(true)
23 | window.navigator.clipboard.writeText(props.copyvalue)
24 | setTimeout(() => {
25 | setCopying(false)
26 | }, 2000);
27 | }
28 |
29 | return <>
30 | {copying ? : }
31 | >
32 |
33 | }
34 |
35 |
36 |
37 |
38 | export const RenderAPITableCell = (value: APIProps, columnKey: React.Key) => {
39 | const cellValue = value[columnKey as keyof APIProps] || '';
40 |
41 | switch (columnKey) {
42 | case 'key':
43 | return (
44 |
45 | {cellValue.slice(0, 6)}...............{cellValue.slice(-5)}
46 |
47 | )
48 |
49 | case "actions":
50 | return (
51 |
52 |
53 |
54 | {/* */}
55 |
56 |
57 |
58 | {/* */}
59 |
60 |
61 |
62 | {value['key']}
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | );
75 | default:
76 | return cellValue;
77 | }
78 | }
79 |
80 |
81 |
82 | export const RenderArchivedChatsTableCell = (value: any, columnKey: React.Key,handleUnarchive:Function,handleDeleteConversation:Function,navigate:Function) => {
83 | const cellValue = value[columnKey as keyof ArchivedChat] || '';
84 |
85 |
86 |
87 | switch (columnKey) {
88 | case 'name':
89 | return navigate('/c/'+value.id)} >
92 | {cellValue}
93 |
94 | case 'created':
95 | return (
96 |
97 | {new Date(cellValue).toLocaleDateString()}
98 |
99 | )
100 |
101 | case "actions":
102 | return (
103 |
104 |
105 |
106 | handleUnarchive(value.id)}/>
107 |
108 |
109 |
110 |
111 |
112 | handleDeleteConversation(value.id)} />
113 |
114 |
115 |
116 |
117 | );
118 | default:
119 | return cellValue;
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/src/components/setting/security.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Button,
3 | Divider,
4 | Popover,
5 | PopoverContent,
6 | PopoverTrigger,
7 | Tooltip
8 | } from "@nextui-org/react";
9 | // import { Tooltip } from "../tootip/tooltip";
10 | import { Monitor, Smartphone, Laptop, Terminal, X, Info } from "lucide-react"
11 | import { useNavigate } from "react-router-dom";
12 | import { useEffect, useState } from "react";
13 | import { getConnectedDevices, logoutAllDevices } from "@/services/dispatch/user-dispatch";
14 | import { deleteSession } from "@/services/session";
15 | import { useAuth } from "@/hooks/use-auth";
16 | import toast from "react-hot-toast";
17 | import { formatDate } from "@/utils";
18 |
19 | interface Device {
20 | device_id: string,
21 | device_name: string,
22 | ip_address: string,
23 | last_active: Date,
24 | created_at: Date
25 | }
26 |
27 |
28 |
29 | const getDeviceInfo = (deviceName: string) => {
30 | const userAgent = deviceName.toLowerCase()
31 |
32 | if (userAgent.includes("postman")) {
33 | return {
34 | type: "API Client",
35 | icon: Terminal,
36 | name: "Postman",
37 | os: "Cross-platform",
38 | browser: "Postman Runtime",
39 | version: userAgent.split("/")[1]
40 | }
41 | }
42 |
43 | if (userAgent.includes("mozilla")) {
44 | const info = {
45 | type: "Browser",
46 | icon: Laptop,
47 | name: "Web Browser",
48 | os: userAgent.includes("windows") ? "Windows" :
49 | userAgent.includes("mac") ? "macOS" :
50 | userAgent.includes("linux") ? "Linux" : "Unknown",
51 | browser: userAgent.includes("chrome") ? "Chrome" :
52 | userAgent.includes("firefox") ? "Firefox" :
53 | userAgent.includes("safari") ? "Safari" : "Unknown",
54 | version: userAgent.match(/chrome\/([0-9.]+)/i)?.[1] ||
55 | userAgent.match(/firefox\/([0-9.]+)/i)?.[1] ||
56 | userAgent.match(/safari\/([0-9.]+)/i)?.[1] || "Unknown"
57 | }
58 | return info
59 | }
60 |
61 | return {
62 | type: "Unknown Device",
63 | icon: Monitor,
64 | name: deviceName,
65 | os: "Unknown",
66 | browser: "Unknown",
67 | version: "Unknown"
68 | }
69 | }
70 |
71 |
72 | const SecuritySetting = () => {
73 | const [devices, setDevices] = useState([])
74 |
75 |
76 | const [infoToolTip,setInfoTooltip]=useState(0)
77 |
78 | const { setIsLoggedIn, setUser } = useAuth();
79 |
80 | useEffect(() => {
81 | getConnectedDevices().then(res => {
82 | console.log(res)
83 | setDevices(res.connected_devices)
84 | })
85 | }, []);
86 |
87 | const navigate = useNavigate();
88 |
89 |
90 | const handlelogoutAll = () => {
91 |
92 | logoutAllDevices().then(res => {
93 | toast.success('All the devices have been logged out including you. Kindly login back...')
94 | console.log(res)
95 | deleteSession();
96 | setUser(null);
97 | setIsLoggedIn(false);
98 | navigate("/");
99 | })
100 |
101 | };
102 |
103 | const handleDeleteAccount = () => {
104 | toast.success('Your account is deleted successfully')
105 | deleteSession();
106 | setUser(null);
107 | setIsLoggedIn(false);
108 | navigate("/");
109 | }
110 |
111 | return <>
112 |
113 |
Delete account
114 |
115 |
116 |
117 |
118 | Delete current account
119 |
120 |
121 |
122 |
Are you sure?
123 |
Your current account will be deleted. This action cannot be undone.
124 |
Ok
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
Log out of all devices
133 |
134 |
135 |
136 |
137 | Log out all
138 |
139 |
140 |
141 |
Are you sure?
142 |
All the devices will be logged out including you
143 |
Ok
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
Connected devices
152 |
153 | {true ? devices.map((v, id) => {
154 | const deviceInfo = getDeviceInfo(v.device_name)
155 | const DeviceIcon = deviceInfo.icon
156 | return
157 | {/*
158 | {true ? : }
159 | {v.device_name.split(' ')[0]}
160 |
161 |
162 | {v.ip_address}
163 |
*/}
164 |
165 |
166 |
167 |
168 |
169 |
170 |
{deviceInfo.name}
171 |
173 | Type: {deviceInfo.type}
174 | OS: {deviceInfo.os}
175 | Browser: {deviceInfo.browser}
176 | Version: {deviceInfo.version}
177 | Last Active: {formatDate(v.last_active)}
178 | First Seen: {formatDate(v.created_at)}
179 |
180 | }>
181 |
setInfoTooltip(0)} onMouseEnter={()=>setInfoTooltip(id+1)} variant="ghost" isIconOnly className="h-6 w-6">
182 |
183 |
184 |
185 |
186 |
187 |
{deviceInfo.type}
188 |
189 |
190 |
191 |
{v.ip_address}
192 |
193 | {new Date(v.last_active).toLocaleDateString()}
194 |
195 |
196 |
197 |
198 |
199 | Remove device
200 |
201 |
202 | })
203 | :
There is no any connected device.
}
204 |
205 |
206 |
207 | >
208 | }
209 |
210 | export default SecuritySetting
--------------------------------------------------------------------------------
/src/components/sidebar.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@nextui-org/react";
2 | import { BsChatLeftDots,BsLayoutSidebar } from "react-icons/bs";
3 | import { CiSettings } from "react-icons/ci";
4 | import ChatList from "./chats/chat-list";
5 | import { useEffect, useRef, useState, lazy, Suspense } from "react";
6 | import { useAuth } from "@/hooks/use-auth";
7 | import { useNavigate } from "react-router-dom";
8 | import { useAppDispatch } from "@/store/hooks";
9 | import { setMessages } from "@/store/slices/chatSlice";
10 |
11 | const SigninModal = lazy(() => import("@/components/auth/signin-modal"));
12 |
13 | type P = {
14 | isOpen: boolean;
15 | onClose: () => void;
16 | };
17 |
18 | export default function Sidebar({ isOpen, onClose }: P) {
19 | const sidebarRef = useRef(null);
20 | const [isSigninOpen, setIsSigninOpen] = useState(false);
21 | const { loading, isLoggedIn } = useAuth();
22 | const navigate = useNavigate();
23 | const dispatch = useAppDispatch();
24 |
25 | useEffect(() => {
26 | function handleClickOutside(event: MouseEvent) {
27 | if (sidebarRef.current && !sidebarRef.current.contains(event.target as Node)) {
28 | if (window.innerWidth < 768) onClose();
29 | }
30 | }
31 |
32 | if (isOpen) {
33 | document.addEventListener("mousedown", handleClickOutside);
34 | } else {
35 | document.removeEventListener("mousedown", handleClickOutside);
36 | }
37 |
38 | return () => {
39 | document.removeEventListener("mousedown", handleClickOutside);
40 | };
41 | }, [isOpen, onClose]);
42 |
43 | return (
44 |
45 |
62 | {!loading && (
63 |
64 | {/* Top Section */}
65 |
66 |
67 |
68 |
69 | {
73 | navigate("/");
74 | dispatch(setMessages([]));
75 | }}
76 | >
77 |
78 | New Chat
79 |
80 |
81 |
82 |
83 | {/* Chat List Section */}
84 |
91 |
92 |
93 |
94 |
95 | {/* Bottom Section */}
96 |
97 | {isLoggedIn ? (
98 | navigate('/upgrade_plan')}
104 | >
105 | Upgrade to Pro
106 |
107 | ) : (
108 | setIsSigninOpen(true)}
114 | >
115 | Login / Sign Up
116 |
117 | )}
118 |
119 | navigate('/setting')}
124 | >
125 |
126 | Settings
127 |
128 |
129 |
130 | )}
131 |
132 |
133 |
134 | setIsSigninOpen(false)}
137 | onSignupSuccess={() => {}}
138 | />
139 |
140 |
141 | );
142 | }
--------------------------------------------------------------------------------
/src/components/theme-switch.tsx:
--------------------------------------------------------------------------------
1 | import { FC, useState, useEffect } from "react";
2 | import { VisuallyHidden } from "@react-aria/visually-hidden";
3 | import { SwitchProps, useSwitch } from "@nextui-org/switch";
4 | import clsx from "clsx";
5 |
6 | import { useTheme } from "@/hooks/use-theme";
7 | import { SunFilledIcon, MoonFilledIcon } from "@/components/icons";
8 |
9 | export interface ThemeSwitchProps {
10 | className?: string;
11 | classNames?: SwitchProps["classNames"];
12 | }
13 |
14 | export const ThemeSwitch: FC = ({
15 | className,
16 | classNames,
17 | }) => {
18 | const [isMounted, setIsMounted] = useState(false);
19 |
20 | const { theme, toggleTheme } = useTheme("dark");
21 |
22 | const onChange = toggleTheme;
23 |
24 | const {
25 | Component,
26 | slots,
27 | isSelected,
28 | getBaseProps,
29 | getInputProps,
30 | getWrapperProps,
31 | } = useSwitch({
32 | isSelected: theme === "light",
33 | onChange,
34 | });
35 |
36 | useEffect(() => {
37 | setIsMounted(true);
38 | }, [isMounted]);
39 |
40 | // Prevent Hydration Mismatch
41 | if (!isMounted) return
;
42 |
43 | return (
44 |
53 |
54 |
55 |
56 |
75 | {isSelected ? (
76 |
77 | ) : (
78 |
79 | )}
80 |
81 |
82 | );
83 | };
84 |
--------------------------------------------------------------------------------
/src/components/tootip/tooltip.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 |
3 | type P = {
4 | children: any;
5 | content: any
6 | };
7 | export function Tooltip(props: P) {
8 |
9 | const [hover, setHover] = useState(false);
10 | return (
11 |
12 |
13 |
14 | {props.content}
15 |
16 |
17 |
[
18 | setHover(false)
19 | ]}
20 | onMouseOver={() => {
21 | setHover(true)
22 | }}
23 | >
24 | {
25 | props.children
26 | }
27 |
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/src/components/user/profile.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef, useState } from "react";
2 | import CustomModal from "../customModal";
3 | import { Avatar, Divider, Switch } from "@nextui-org/react";
4 | import { AiOutlineUser } from "react-icons/ai";
5 | import { BiEdit } from "react-icons/bi";
6 | import { useNavigate } from "react-router-dom";
7 | import { updateProfile } from "@/services/dispatch/user-dispatch";
8 | import { useAuth } from "@/hooks/use-auth";
9 | import { getItem, setItem } from "@/services/session";
10 | import { convertToBase64 } from "@/utils";
11 | import toast from "react-hot-toast";
12 | import { User } from "@/contexts/AuthContext";
13 |
14 | const Profile = () => {
15 | //to-do: userdata fetching
16 | const { user, setUser } = useAuth();
17 |
18 | const [UserInfo, setUserInfo] = useState({
19 | avatar: getItem('avatar') || '',
20 | full_name: user?.full_name || '',
21 | email: user?.email || '',
22 | retention: true,
23 | });
24 |
25 | const save = async () => {
26 | const result = await updateProfile({ full_name: UserInfo.full_name, email: UserInfo.email })
27 | if(result.message !=="No changes were made to the profile"){
28 | toast.success(result.message)
29 | setUser({...user,full_name:UserInfo.full_name,email:UserInfo.email} as User )
30 | setItem("user", {...user,full_name:UserInfo.full_name,email:UserInfo.email} as User);
31 | }
32 | }
33 |
34 | const [isNameChanging, setIsnameChaning] = useState(false);
35 | const [isEmailChanging, setIsEmailChanging] = useState(false);
36 |
37 |
38 | const FileInputRef = useRef(null);
39 |
40 | const handleSeleteFile = (e: React.ChangeEvent) => {
41 | if (e.target?.files) {
42 | convertToBase64(e.target.files[0]).then(base64 => {
43 |
44 | setUserInfo({
45 | ...UserInfo,
46 | avatar: base64 as string,
47 | });
48 | setItem('avatar', base64 as string)
49 | toast.success('Profile updated successfully!')
50 | })
51 |
52 | }
53 | };
54 | const handleChangeUserName = (e: React.ChangeEvent) => {
55 | setUserInfo({ ...UserInfo, full_name: e.target.value });
56 | };
57 | const handleChangeUserEmail = (e: React.ChangeEvent) => {
58 | setUserInfo({ ...UserInfo, email: e.target.value });
59 | };
60 |
61 | const navigate = useNavigate();
62 |
63 | return (
64 |
65 |
navigate(-1)}>
66 |
67 |
68 |
Avatar
69 |
FileInputRef.current?.click()}
72 | >
73 | {" "}
74 |
}
79 | />{" "}
80 |
81 |
82 |
83 |
84 |
85 |
User Name
86 |
87 | {isNameChanging ? (
88 |
{
95 |
96 | setIsnameChaning(false)
97 | save()
98 | }}
99 | />
100 | ) : (
101 |
{UserInfo.full_name}
102 | )}
103 |
104 |
setIsnameChaning(true)}
106 | className="w-1/2 z-[50] opacity-80 hover:opacity-100 active:opacity-50"
107 | />
108 |
109 |
110 |
111 |
112 |
Email
113 |
114 | {isEmailChanging ? (
115 |
{
121 | setIsEmailChanging(false)
122 | save()
123 | }}
124 | />
125 | ) : (
126 |
{UserInfo.email}
127 | )}
128 |
129 |
setIsEmailChanging(true)}
131 | className="w-1/2 z-[50] opacity-80 hover:opacity-100 active:opacity-50"
132 | />
133 |
134 |
135 |
136 |
137 |
138 |
AI Data Retention
139 |
140 | AI Data Retention allows Cerina to use your searches to improve
141 | AI models. Turn this setting off if you wish to exclude your
142 | data from this process
143 |
144 |
145 |
146 |
152 | setUserInfo({ ...UserInfo, retention: isSelected })
153 | }
154 | isSelected={UserInfo.retention}
155 | >
156 |
157 |
158 |
164 |
165 |
166 |
167 | );
168 | };
169 |
170 | export default Profile;
171 |
--------------------------------------------------------------------------------
/src/config/site.ts:
--------------------------------------------------------------------------------
1 | export type SiteConfig = typeof siteConfig;
2 |
3 | export const siteConfig = {
4 | name: "Vite + NextUI",
5 | description: "Make beautiful websites regardless of your design experience.",
6 | navItems: [
7 | {
8 | label: "Home",
9 | href: "/",
10 | },
11 | {
12 | label: "Docs",
13 | href: "/docs",
14 | },
15 | {
16 | label: "Pricing",
17 | href: "/pricing",
18 | },
19 | {
20 | label: "Blog",
21 | href: "/blog",
22 | },
23 | {
24 | label: "About",
25 | href: "/about",
26 | },
27 | ],
28 | navMenuItems: [
29 | {
30 | label: "Profile",
31 | href: "/profile",
32 | },
33 | {
34 | label: "Dashboard",
35 | href: "/dashboard",
36 | },
37 | {
38 | label: "Projects",
39 | href: "/projects",
40 | },
41 | {
42 | label: "Team",
43 | href: "/team",
44 | },
45 | {
46 | label: "Calendar",
47 | href: "/calendar",
48 | },
49 | {
50 | label: "Settings",
51 | href: "/settings",
52 | },
53 | {
54 | label: "Help & Feedback",
55 | href: "/help-feedback",
56 | },
57 | {
58 | label: "Logout",
59 | href: "/logout",
60 | },
61 | ],
62 | links: {
63 | github: "https://github.com/nextui-org/nextui",
64 | twitter: "https://twitter.com/getnextui",
65 | docs: "https://nextui-docs-v2.vercel.app",
66 | discord: "https://discord.gg/9b6yyZKmH4",
67 | sponsor: "https://patreon.com/jrgarciadev",
68 | },
69 | };
70 |
--------------------------------------------------------------------------------
/src/contexts/ArchiveContext.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode, createContext, useEffect, useState } from "react";
2 |
3 | import { getItem } from "../services/session";
4 |
5 | export interface ArchivedChat{
6 | name:string;
7 | id:string;
8 | created:Date
9 | }
10 |
11 | interface ArchiveContextProps {
12 | archivedchats: ArchivedChat[];
13 | setArchivedChats: React.Dispatch>;
14 | currentLookingAt:string,
15 | setCrurentLookingAt:React.Dispatch>
16 | }
17 |
18 | export const ArchiveContext = createContext({
19 | archivedchats:[],
20 | setArchivedChats:()=>{},
21 | currentLookingAt:'',
22 | setCrurentLookingAt:()=>{}
23 | });
24 |
25 | export const ArchiveProvider = ({ children }: { children: ReactNode }) => {
26 | const [archivedchats, setArchivedChats] = useState([]);
27 | const [currentLookingAt,setCrurentLookingAt]=useState('')
28 |
29 | // check session
30 | useEffect(() => {
31 |
32 | const archived = getItem('archivedChats')
33 | if (archived) {
34 | setArchivedChats(archived)
35 | }
36 | }, []);
37 |
38 |
39 | return (
40 |
48 | {children}
49 |
50 | );
51 | };
52 |
--------------------------------------------------------------------------------
/src/contexts/AuthContext.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode, createContext, useEffect, useState } from "react";
2 |
3 | import { getItem } from "../services/session";
4 |
5 | interface User {
6 | email: string;
7 | full_name: string;
8 | username: string;
9 | unique_id: string;
10 | }
11 |
12 | interface AuthContextProps {
13 | loading: boolean;
14 | isLoggedIn: boolean;
15 | setIsLoggedIn: React.Dispatch>;
16 | user: User | null;
17 | setUser: React.Dispatch>;
18 | isUserUpdated: boolean;
19 | setIsUserUpdated: React.Dispatch>;
20 | }
21 |
22 | export const AuthContext = createContext({
23 | loading: true,
24 | isLoggedIn: false,
25 | setIsLoggedIn: () => {},
26 | user: null,
27 | setUser: () => {},
28 | isUserUpdated: false,
29 | setIsUserUpdated: () => {},
30 | });
31 |
32 | export const AuthProvider = ({ children }: { children: ReactNode }) => {
33 | const [isLoggedIn, setIsLoggedIn] = useState(false);
34 | const [loading, setLoading] = useState(true);
35 | const [user, setUser] = useState(null);
36 | const [isUserUpdated, setIsUserUpdated] = useState(false);
37 |
38 | // check session
39 | useEffect(() => {
40 | if (getItem("access_token")) {
41 | setUser(getItem("user"));
42 | setIsLoggedIn(true);
43 | setLoading(false);
44 | } else {
45 | setLoading(false);
46 | }
47 | }, []);
48 |
49 | return (
50 |
61 | {children}
62 |
63 | );
64 | };
65 |
--------------------------------------------------------------------------------
/src/hooks/use-archive.ts:
--------------------------------------------------------------------------------
1 | import { ArchiveContext } from "@/contexts/ArchiveContext";
2 | import { useContext } from "react";
3 |
4 | export const useArchive = () => {
5 | const contextValue = useContext(ArchiveContext);
6 | return contextValue;
7 | };
8 |
--------------------------------------------------------------------------------
/src/hooks/use-auth.ts:
--------------------------------------------------------------------------------
1 | import { AuthContext } from "@/contexts/AuthContext";
2 | import { useContext } from "react";
3 |
4 | export const useAuth = () => {
5 | const contextValue = useContext(AuthContext);
6 | return contextValue;
7 | };
8 |
--------------------------------------------------------------------------------
/src/hooks/use-speech2text.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import toast from 'react-hot-toast';
3 |
4 | // Inline SpeechRecognition Type Declarations
5 | interface SpeechRecognition extends EventTarget {
6 | continuous: boolean;
7 | interimResults: boolean;
8 | lang: string;
9 | start: () => void;
10 | stop: () => void;
11 | onresult: (event: SpeechRecognitionEvent) => void;
12 | onerror: (event: SpeechRecognitionErrorEvent) => void;
13 | onend: () => void;
14 | }
15 |
16 | interface SpeechRecognitionEvent extends Event {
17 | results: SpeechRecognitionResultList;
18 | resultIndex: number;
19 | }
20 |
21 | interface SpeechRecognitionErrorEvent extends Event {
22 | error: string;
23 | }
24 |
25 | type SpeechToTextHook = {
26 | transcript: string;
27 | isListening: boolean;
28 | toggleListening: () => void;
29 | isLoading: boolean
30 | };
31 |
32 | // Speech recognition utility function
33 | const getSpeechRecognition = (): SpeechRecognition | null => {
34 | const SpeechRecognition =
35 | (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition;
36 | return SpeechRecognition ? new SpeechRecognition() : null;
37 | };
38 |
39 | export const useSpeechToText = (): SpeechToTextHook => {
40 | const [transcript, setTranscript] = useState('');
41 | const [isListening, setIsListening] = useState(false);
42 | const [isLoading, setIsLoading] = useState(false);
43 | const [recognition] = useState(getSpeechRecognition);
44 |
45 | useEffect(() => {
46 | if (!recognition) {
47 | console.warn('Speech Recognition API is not supported in this browser');
48 | return;
49 | }
50 |
51 |
52 | // Configure the SpeechRecognition instance
53 | recognition.continuous = true;
54 | recognition.interimResults = true;
55 | recognition.lang = 'en-US';
56 |
57 | // Event handler for speech results
58 | recognition.onresult = (event: SpeechRecognitionEvent) => {
59 | // Only update the transcript if `isListening` is true
60 | console.log('first')
61 | let interimTranscript = '';
62 | for (let i = event.resultIndex; i < event.results.length; i++) {
63 | const transcript = event.results[i][0].transcript;
64 | interimTranscript += transcript;
65 | }
66 | console.log(interimTranscript)
67 | setTranscript(interimTranscript);
68 | };
69 |
70 | // Error handling
71 | recognition.onerror = (event: SpeechRecognitionErrorEvent) => {
72 | console.log(event.error)
73 | if(event.error==='no-speech')
74 | toast.error("We're having trouble hearing you. Check your microphone settings.")
75 |
76 | };
77 |
78 | // Restart recognition if `isListening` is true
79 | recognition.onend = () => {
80 | setIsListening(false)
81 | };
82 |
83 | // Cleanup on unmount
84 | return () => {
85 | recognition.stop();
86 | };
87 | }, [recognition]);
88 |
89 | const toggleListening = () => {
90 | if (recognition && !isLoading) {
91 |
92 | if (isListening) {
93 | setIsLoading(true)
94 | recognition.stop();
95 | setIsListening(false);
96 | setIsLoading(false)
97 | } else {
98 | setIsLoading(true)
99 | setIsListening(true);
100 | recognition.start();
101 | setIsLoading(false)
102 | }
103 |
104 |
105 | }
106 | };
107 | return { transcript, isListening, isLoading, toggleListening };
108 | };
109 |
--------------------------------------------------------------------------------
/src/hooks/use-theme.ts:
--------------------------------------------------------------------------------
1 | // originally written by @imoaazahmed
2 |
3 | import { useEffect, useMemo, useState } from "react";
4 |
5 | const ThemeProps = {
6 | key: "theme",
7 | light: "light",
8 | dark: "dark",
9 | } as const;
10 |
11 | type Theme = typeof ThemeProps.light | typeof ThemeProps.dark;
12 |
13 | export const useTheme = (defaultTheme?: Theme) => {
14 | const [theme, setTheme] = useState(() => {
15 | const storedTheme = localStorage.getItem(ThemeProps.key) as Theme | null;
16 |
17 | return storedTheme || (defaultTheme ?? ThemeProps.light);
18 | });
19 |
20 | const isDark = useMemo(() => {
21 | return theme === ThemeProps.dark;
22 | }, [theme]);
23 |
24 | const isLight = useMemo(() => {
25 | return theme === ThemeProps.light;
26 | }, [theme]);
27 |
28 | const _setTheme = (theme: Theme) => {
29 | localStorage.setItem(ThemeProps.key, theme);
30 | document.documentElement.classList.remove(
31 | ThemeProps.light,
32 | ThemeProps.dark,
33 | );
34 | document.documentElement.classList.add(theme);
35 | setTheme(theme);
36 | };
37 |
38 | const setLightTheme = () => _setTheme(ThemeProps.light);
39 |
40 | const setDarkTheme = () => _setTheme(ThemeProps.dark);
41 |
42 | const toggleTheme = () =>
43 | theme === ThemeProps.dark ? setLightTheme() : setDarkTheme();
44 |
45 | useEffect(() => {
46 | _setTheme(theme);
47 | });
48 |
49 | return { theme, isDark, isLight, setLightTheme, setDarkTheme, toggleTheme };
50 | };
51 |
--------------------------------------------------------------------------------
/src/html2pdf.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'html2pdf.js' {
2 | const html2pdf: any;
3 | export default html2pdf;
4 | }
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | .markdown-content a {
6 | @apply text-blue-600 underline hover:text-blue-800 transition-colors duration-300;
7 | }
8 |
9 | .cus-modal{
10 | background-color: transparent !important;
11 | background: transparent !important;
12 | margin: 0 !important;
13 | }
--------------------------------------------------------------------------------
/src/layouts/default.tsx:
--------------------------------------------------------------------------------
1 | import Header from "@/components/header";
2 | import Sidebar from "@/components/sidebar";
3 | import { useState } from "react";
4 |
5 | export default function DefaultLayout({
6 | children,
7 | }: {
8 | children: React.ReactNode;
9 | }) {
10 | const [isSidebarOpen, setIsSidebarOpen] = useState(
11 | () => window.screen.width >= 768
12 | );
13 |
14 | return (
15 |
16 |
setIsSidebarOpen(false)} />
17 |
18 |
19 | setIsSidebarOpen((prev) => !prev)} isOpen={isSidebarOpen} />
20 | {children}
21 |
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import { StrictMode } from "react";
2 | import { createRoot } from "react-dom/client";
3 | import App from "./App.tsx";
4 | import "./index.css";
5 | import { BrowserRouter } from "react-router-dom";
6 | import { Provider } from "./provider.tsx";
7 |
8 | createRoot(document.getElementById("root")!).render(
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | );
17 |
--------------------------------------------------------------------------------
/src/pages/about.tsx:
--------------------------------------------------------------------------------
1 | import { title } from "@/components/primitives";
2 | import DefaultLayout from "@/layouts/default";
3 |
4 | export default function DocsPage() {
5 | return (
6 |
7 |
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/src/pages/activate-account.tsx:
--------------------------------------------------------------------------------
1 | import { useAuth } from "@/hooks/use-auth";
2 | import { verifyEmail } from "@/services/dispatch/user-dispatch";
3 | import { saveSession, setItem } from "@/services/session";
4 | import { Button, CircularProgress } from "@nextui-org/react";
5 | import { useCallback, useEffect, useState } from "react";
6 | import toast from "react-hot-toast";
7 | import { FcApproval } from "react-icons/fc";
8 | import { MdError } from "react-icons/md";
9 | import { useNavigate, useSearchParams } from "react-router-dom";
10 |
11 | export default function ActivateAccountPage() {
12 | const [isLoading, setIsLoading] = useState(false);
13 | const [error, setError] = useState("");
14 | const [searchParams] = useSearchParams();
15 | const uidb64 = searchParams.get("uidb64");
16 | const token = searchParams.get("token");
17 | const { setUser, setIsLoggedIn } = useAuth();
18 | const navigate = useNavigate();
19 |
20 | const handleVerifyEmail = useCallback(() => {
21 | if (!uidb64 || !token) {
22 | setError("Missing uidb64 or token");
23 | return;
24 | }
25 |
26 | setIsLoading(true);
27 | verifyEmail({ uidb64, token })
28 | .then((res) => {
29 | setIsLoading(false);
30 | setUser(res?.user);
31 | setIsLoggedIn(true);
32 | saveSession({
33 | accessToken: res?.access_token,
34 | refreshToken: res?.refresh_token,
35 | });
36 | setItem("user", res?.user);
37 | toast.success("Login successful");
38 | navigate("/");
39 | })
40 | .catch((error) => {
41 | setIsLoading(false);
42 | console.log(error);
43 | });
44 | }, [navigate, setIsLoggedIn, setUser, token, uidb64]);
45 |
46 | useEffect(() => {
47 | if (uidb64 && token) {
48 | handleVerifyEmail();
49 | }
50 | }, [handleVerifyEmail, token, uidb64]);
51 |
52 | return (
53 |
54 | {isLoading ? (
55 |
56 |
57 | Please wait while activating your account
58 |
59 |
60 |
61 | ) : (
62 | <>
63 | {error ? (
64 |
65 |
66 |
Failed to activate account
67 | location.reload()}>Retry
68 |
69 | ) : (
70 |
71 |
72 |
73 | Your account is activated successfully
74 |
75 |
76 | )}
77 | >
78 | )}
79 |
80 | );
81 | }
--------------------------------------------------------------------------------
/src/pages/blog.tsx:
--------------------------------------------------------------------------------
1 | import { title } from "@/components/primitives";
2 | import DefaultLayout from "@/layouts/default";
3 |
4 | export default function DocsPage() {
5 | return (
6 |
7 |
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/src/pages/chat.tsx:
--------------------------------------------------------------------------------
1 | import ChatInput from "@/components/chats/chat-input";
2 | import ChatMessages from "@/components/chats/chat-messages";
3 | import DefaultLayout from "@/layouts/default";
4 | import { getChatMessagesById } from "@/store/actions/chatActions";
5 | import { useAppDispatch, useAppSelector } from "@/store/hooks";
6 | import { useEffect } from "react";
7 | import { useParams } from "react-router-dom";
8 |
9 | export default function ChatPage() {
10 | const { id } = useParams();
11 | const chatData = useAppSelector((state) => state.chat);
12 | const dispatch = useAppDispatch();
13 | useEffect(() => {
14 | dispatch(getChatMessagesById(id!));
15 | }, [dispatch, id]);
16 |
17 | return (
18 |
19 |
20 | {/* chat messages */}
21 |
22 |
23 |
24 | {/* Chat input container */}
25 |
26 |
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/src/pages/docs.tsx:
--------------------------------------------------------------------------------
1 | import { title } from "@/components/primitives";
2 | import DefaultLayout from "@/layouts/default";
3 |
4 | export default function DocsPage() {
5 | return (
6 |
7 |
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import ChatInput from "@/components/chats/chat-input";
3 | import ChatMessages from "@/components/chats/chat-messages";
4 | import Logo from "@/components/logo";
5 | import DefaultLayout from "@/layouts/default";
6 | import { useAppSelector } from "@/store/hooks";
7 | import { useAuth } from "@/hooks/use-auth";
8 | import { lazy, Suspense } from "react";
9 |
10 | // Lazy load the SigninModal component
11 | const SigninModal = lazy(() => import("@/components/auth/signin-modal"));
12 |
13 | export default function IndexPage() {
14 | const chatData = useAppSelector((state) => state.chat);
15 | const { isLoggedIn } = useAuth(); // Use the login state
16 | const [isSigninOpen, setIsSigninOpen] = useState(!isLoggedIn); // Open modal if not logged in
17 | const [signupMessage, setSignupMessage] = useState(""); // State to hold success message
18 |
19 | useEffect(() => {
20 | if (!isLoggedIn) {
21 | setIsSigninOpen(true); // Ensure modal remains open until login/signup
22 | } else {
23 | setIsSigninOpen(false); // Close modal if the user is already logged in
24 | }
25 | }, [isLoggedIn]);
26 |
27 | return (
28 |
29 | {/* Apply blur only to the main content when the modal is open */}
30 |
35 | {/* Chat messages */}
36 | {chatData?.messages.length ? (
37 |
38 | ) : (
39 |
40 |
41 |
42 | )}
43 | {/* Chat input container */}
44 |
45 |
46 |
47 | {/* Display signup success message */}
48 | {signupMessage && (
49 |
50 | {signupMessage}
51 |
52 | )}
53 |
54 | {/* Sign-in modal */}
55 | Loading...}>
56 | {!isLoggedIn && (
57 | setIsSigninOpen(false)}
60 | onSignupSuccess={(message) => setSignupMessage(message)}
61 | />
62 | )}
63 |
64 |
65 | );
66 | }
67 |
--------------------------------------------------------------------------------
/src/pages/pricing.tsx:
--------------------------------------------------------------------------------
1 | import { title } from "@/components/primitives";
2 | import DefaultLayout from "@/layouts/default";
3 |
4 | export default function DocsPage() {
5 | return (
6 |
7 |
8 |
9 |
Pricing
10 |
11 |
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/src/pages/profile.tsx:
--------------------------------------------------------------------------------
1 | import Profile from "@/components/user/profile";
2 | import DefaultLayout from "@/layouts/default";
3 |
4 | export default function ProfilePage() {
5 |
6 | return (
7 |
8 |
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/src/pages/setting.tsx:
--------------------------------------------------------------------------------
1 | import DefaultLayout from "@/layouts/default";
2 | import {
3 | Divider,
4 | Tab,
5 | Tabs,
6 | } from "@nextui-org/react";
7 | import {
8 | AiOutlineApi,
9 | AiOutlineClose,
10 | AiOutlineSecurityScan,
11 | } from "react-icons/ai";
12 | import { useLocation, useNavigate } from "react-router-dom";
13 | import { FiSettings } from "react-icons/fi";
14 | import { useEffect, useState } from "react";
15 | import CustomModal from "@/components/customModal";
16 | import GeneralSetting from "@/components/setting/general";
17 | import SecuritySetting from "@/components/setting/security";
18 | import APIsetting from "@/components/setting/APIs";
19 |
20 |
21 |
22 | const SettingModal = () => {
23 |
24 | const [isHorizontal, setHorizontalMode] = useState(false);
25 |
26 |
27 |
28 | const location = useLocation();
29 | const queryParams = new URLSearchParams(location.search);
30 | const id = queryParams.get('id');
31 |
32 |
33 | useEffect(() => {
34 | const resizeHandler = () => {
35 | if (
36 | document.body.clientWidth < 950
37 | ) {
38 | setHorizontalMode(true);
39 | } else {
40 | setHorizontalMode(false);
41 | }
42 | };
43 | resizeHandler();
44 | window.addEventListener("resize", resizeHandler);
45 | return () => {
46 | window.removeEventListener("resize", resizeHandler);
47 | };
48 | }, []);
49 |
50 | const navigate = useNavigate();
51 |
52 | return (
53 |
54 |
navigate(-1)}
59 | >
60 |
61 | Settings
62 |
navigate(-1)} />
63 |
64 |
65 |
66 |
67 |
68 |
69 |
80 |
85 |
86 | {isHorizontal || <> General>}
87 |
88 | }
89 | >
90 |
91 |
92 |
93 |
97 | {" "}
98 |
99 | {isHorizontal || <> Security>}{" "}
100 |
101 | }
102 | >
103 |
104 |
105 |
109 |
110 | {isHorizontal || <> Your APIs>}
111 |
112 | }
113 | >
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 | );
123 | };
124 |
125 |
126 |
127 |
128 | export default function Setting() {
129 |
130 | return (
131 |
132 |
133 |
134 |
135 |
136 | );
137 | }
--------------------------------------------------------------------------------
/src/pages/upgradepage.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@nextui-org/react"
2 | import { BsCheck2All, BsStarFill } from "react-icons/bs"
3 | import { IoCloseOutline } from "react-icons/io5"
4 | import { useNavigate } from "react-router-dom"
5 |
6 | const Upgrade_Plan = () => {
7 | const navigate = useNavigate()
8 | return (
9 | <>
10 |
11 |
12 |
13 |
14 |
navigate(-1)} />
15 | Upgrade Your Plan
16 |
17 |
18 |
19 |
Free
20 |
Rs. 0/month
21 |
Your Current Plan
22 |
23 |
Lorem ipsum dolor sit amet consectetur.
24 |
25 |
26 |
27 | Sapien in magnis nam at eget ipsum.
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | Plus
40 |
Rs. 5,000/month
41 |
42 |
43 |
44 | Upgrade Plan
45 |
46 |
Lorem ipsum dolor sit amet consectetur.
47 |
48 |
49 |
50 | Sapien in magnis nam at eget ipsum.
51 |
52 |
53 |
54 | At arcu semper amet massa elementum dignissim.
55 |
56 |
57 |
58 | Faucibus maecenas facilisis aliquam accumsan m.
59 |
60 |
61 | Massa sit condimentum nulla dolor.
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | >
70 | )
71 | }
72 |
73 | export default Upgrade_Plan
--------------------------------------------------------------------------------
/src/provider.tsx:
--------------------------------------------------------------------------------
1 | import { NextUIProvider } from "@nextui-org/system";
2 | import { useNavigate } from "react-router-dom";
3 | import { AuthProvider } from "./contexts/AuthContext";
4 | import { Toaster } from "react-hot-toast";
5 | import { Provider as StoreProvider } from "react-redux";
6 | import { store } from "./store";
7 | import { QueryClient, QueryClientProvider } from "react-query";
8 | import { ArchiveProvider } from "./contexts/ArchiveContext";
9 |
10 | const queryClient = new QueryClient();
11 | export function Provider({ children }: { children: React.ReactNode }) {
12 | const navigate = useNavigate();
13 |
14 | return (
15 |
16 |
17 |
18 |
19 |
20 |
21 | {children}
22 |
23 |
24 |
25 |
26 |
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/src/services/dispatch/chat-dispatch.ts:
--------------------------------------------------------------------------------
1 | import { deleteService, getService, postService } from "../service";
2 |
3 | // Start new chat
4 | export const newChat = async (param: unknown) => {
5 | const { data } = await postService("/chat/", param);
6 | return data;
7 | };
8 |
9 | // Get chat details
10 | export const getChatMessages = async (conversationId: string) => {
11 | const { data } = await getService(`/chat/${conversationId}/`);
12 | return data;
13 | };
14 |
15 | export const deleteChatHistory= async(conversationId:string)=>{
16 | const { data } = await deleteService(`/chat/delete/${conversationId}/`);
17 | return data;
18 | }
19 |
20 | // Chat with existing conversation
21 | export const chatWithExistingConversation = async ({
22 | conversationId,
23 | formdata,
24 | }: {
25 | conversationId: string;
26 | formdata: unknown;
27 | }) => {
28 | const { data } = await postService(`/chat/${conversationId}/`, formdata);
29 | return data;
30 | };
31 |
32 | // Get all conversations
33 | export const getChatHistory = async () => {
34 | const { data } = await getService("/conversations/");
35 | return data;
36 | };
37 |
--------------------------------------------------------------------------------
/src/services/dispatch/user-dispatch.ts:
--------------------------------------------------------------------------------
1 | import { getService, postService, putService } from "../service";
2 |
3 | // Login
4 | export const login = async (params: unknown) => {
5 | const { data } = await postService("/login/", params as object);
6 | return data;
7 | };
8 |
9 | // Register
10 | export const register = async (params: unknown) => {
11 | const { data } = await postService("/register/", params as object);
12 | return data;
13 | };
14 |
15 | // Verify email
16 | export const verifyEmail = async (params: { uidb64: string; token: string }) => {
17 | const { data } = await postService("/verify-email/", params);
18 | return data;
19 | };
20 |
21 | // ReVerify email
22 | export const reverifyEmail = async (params: { email: string }) => {
23 | const { data } = await postService("/resend-verification/", params);
24 | return data;
25 | };
26 |
27 | export const updateProfile= async (params:{full_name:string,email:string})=>{
28 | const {data}= await putService('/profile/',params)
29 | return data
30 | }
31 |
32 | export const getConnectedDevices= async()=>{
33 | const {data} = await getService('/devices/')
34 | return data
35 | }
36 |
37 | export const logoutAllDevices = async ()=>{
38 | const {data} =await postService('/logout-all/',{})
39 | return data
40 | }
41 |
42 |
--------------------------------------------------------------------------------
/src/services/service.ts:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import { getItem } from "./session";
3 |
4 | const makeRequest = async (
5 | method: "GET" | "POST" | "PATCH" | "PUT" | "DELETE",
6 | endPoint: string,
7 | data: unknown
8 | ) => {
9 | try {
10 | let headers = {};
11 | if (getItem("access_token")) {
12 | headers = {
13 | Authorization: `Bearer ${getItem("access_token")}`,
14 | };
15 | }
16 |
17 | return await axios({
18 | baseURL: "https://beta-api.tecosys.ai",
19 | url: endPoint,
20 | method,
21 | data,
22 | headers: {
23 | ...headers,
24 | },
25 | });
26 | } catch (error) {
27 | console.log(error);
28 | throw error;
29 | }
30 | };
31 |
32 | // GET
33 | export const getService = async (endPoint: string) =>
34 | makeRequest("GET", endPoint, null);
35 |
36 | // POST
37 | export const postService = async (endPoint: string, reqest: unknown) =>
38 | makeRequest("POST", endPoint, reqest);
39 |
40 | // PUT
41 | export const putService = async (endPoint: string, reqest: unknown) =>
42 | makeRequest("PUT", endPoint, reqest);
43 |
44 | // PATCH
45 | export const patchService = async (endPoint: string, reqest: unknown) =>
46 | makeRequest("PATCH", endPoint, reqest);
47 |
48 | // DELETE
49 | export const deleteService = async (endPoint: string) =>
50 | makeRequest("DELETE", endPoint, null);
51 |
--------------------------------------------------------------------------------
/src/services/session.ts:
--------------------------------------------------------------------------------
1 | // GET ITEM BY KEY
2 | export const getItem = (key: string) => {
3 | return JSON.parse(localStorage.getItem(key)!);
4 | };
5 |
6 | // SET ITEM (KEY, VALUE)
7 | export const setItem = (key: string, value: any) => {
8 | localStorage.setItem(key, JSON.stringify(value));
9 | };
10 |
11 | // DELETE ITEM BY KEY
12 | export const deleteItem = (key: string) => {
13 | localStorage.removeItem(key);
14 | };
15 |
16 | // Get token details
17 | const parseJwt = (token: string): Record => {
18 | const base64Url = token.split(".")[1];
19 | const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
20 | const jsonPayload = decodeURIComponent(
21 | atob(base64)
22 | .split("")
23 | .map((c) => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2))
24 | .join("")
25 | );
26 |
27 | return JSON.parse(jsonPayload) as Record;
28 | };
29 |
30 | // Store token into localStorage
31 | export const saveSession = ({
32 | accessToken,
33 | refreshToken,
34 | }: {
35 | accessToken: string;
36 | refreshToken: string;
37 | }) => {
38 | setItem("access_token", accessToken);
39 | setItem("refresh_token", refreshToken);
40 | };
41 | // Delete token from localStorage
42 | export const deleteSession = () => {
43 | deleteItem("access_token");
44 | deleteItem("refresh_token");
45 | };
46 |
47 | // Get user from token
48 |
49 | export const getUserInfo = (): unknown => {
50 | return parseJwt(getItem("access_token"));
51 | };
52 |
--------------------------------------------------------------------------------
/src/store/actions/chatActions.ts:
--------------------------------------------------------------------------------
1 | import { getChatMessages } from "@/services/dispatch/chat-dispatch";
2 | import { createAsyncThunk } from "@reduxjs/toolkit";
3 | import { Message } from "../slices/chatSlice";
4 |
5 | // Get chat messages by converstaion id
6 | export const getChatMessagesById = createAsyncThunk, string>(
7 | "/chat/messagesById",
8 | async (id: string, thunkAPI) => {
9 | try {
10 | const res = await getChatMessages(id);
11 | return res?.messages;
12 | } catch (err) {
13 | return thunkAPI.rejectWithValue(err);
14 | }
15 | }
16 | );
17 |
--------------------------------------------------------------------------------
/src/store/hooks.ts:
--------------------------------------------------------------------------------
1 | import { useDispatch, useSelector } from "react-redux";
2 | import type { RootState, AppDispatch } from "./index";
3 |
4 | // Use throughout your app instead of plain `useDispatch` and `useSelector`
5 | export const useAppDispatch = useDispatch.withTypes();
6 | export const useAppSelector = useSelector.withTypes();
7 |
--------------------------------------------------------------------------------
/src/store/index.ts:
--------------------------------------------------------------------------------
1 | import { configureStore } from "@reduxjs/toolkit";
2 | import chatSlice from "./slices/chatSlice";
3 | // ...
4 |
5 | export const store = configureStore({
6 | reducer: {
7 | chat: chatSlice,
8 | },
9 | });
10 |
11 | // Infer the `RootState` and `AppDispatch` types from the store itself
12 | export type RootState = ReturnType;
13 | // Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
14 | export type AppDispatch = typeof store.dispatch;
15 |
--------------------------------------------------------------------------------
/src/store/slices/chatSlice.ts:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 | import { getChatMessagesById } from "../actions/chatActions";
3 |
4 | export type Message = {
5 | role: "user" | "model";
6 | text: string;
7 | timestamp: string;
8 | id: string;
9 | };
10 |
11 | export type Chat = {
12 | chatId?: string | null;
13 | loading: boolean;
14 | botResponseLoading: string;
15 | messages: Array;
16 | isError: boolean;
17 | errorMessage: string | null;
18 | currentTypingMessageId: string | null;
19 | };
20 |
21 | const initialState: Chat = {
22 | chatId: null,
23 | loading: false,
24 | botResponseLoading: '',
25 | messages: [],
26 | isError: false,
27 | errorMessage: null,
28 | currentTypingMessageId: null,
29 | };
30 |
31 | const chatSlice = createSlice({
32 | name: "chat",
33 | initialState: initialState,
34 | reducers: {
35 | setMessages: (state, action) => {
36 | return { ...state, messages: action.payload };
37 | },
38 | addBotMessage: (state, action) => {
39 | const messageId = `msg-${Date.now()}`; // Generate a unique ID for the message
40 | const newMessage = {
41 | ...action.payload?.message,
42 | id: messageId,
43 | };
44 | const newMessages = [...state.messages, newMessage];
45 |
46 | return {
47 | ...state,
48 | messages: newMessages,
49 | botResponseLoading: '',
50 | chatId: action.payload?.conversation_id,
51 | currentTypingMessageId: messageId, // Set this message as the currently typing one
52 | };
53 | },
54 | addUserMessage: (state, action) => {
55 | const messageId = `msg-${Date.now()}`;
56 | state.messages.push({
57 | ...action.payload,
58 | id: messageId,
59 | });
60 | },
61 | setBotResponseLoading: (state, action) => {
62 | return { ...state, botResponseLoading: action.payload };
63 | },
64 | setCurrentTypingMessageId: (state, action) => {
65 | state.currentTypingMessageId = action.payload;
66 | },
67 | },
68 | extraReducers: (builder) => {
69 | builder
70 | .addCase(getChatMessagesById.pending, (state) => {
71 | if (state.messages.length) {
72 | return state;
73 | } else {
74 | return { ...state, loading: true };
75 | }
76 | })
77 | .addCase(getChatMessagesById.fulfilled, (state, action) => {
78 | // Add IDs to existing messages if they don't have them
79 | const messagesWithIds = action.payload.map((msg: Message) => ({
80 | ...msg,
81 | id: msg.id || `msg-${Date.now()}-${Math.random()}`,
82 | }));
83 | return { ...state, loading: false, messages: messagesWithIds };
84 | })
85 | .addCase(getChatMessagesById.rejected, (state) => {
86 | return {
87 | ...state,
88 | isError: true,
89 | loading: false,
90 | errorMessage: "Something went wrong try again",
91 | };
92 | });
93 | },
94 | });
95 |
96 | export const {
97 | setMessages,
98 | addBotMessage,
99 | addUserMessage,
100 | setBotResponseLoading,
101 | setCurrentTypingMessageId,
102 | } = chatSlice.actions;
103 | export default chatSlice.reducer;
--------------------------------------------------------------------------------
/src/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/src/types/index.ts:
--------------------------------------------------------------------------------
1 | import { SVGProps } from "react";
2 |
3 | export type IconSvgProps = SVGProps & {
4 | size?: number;
5 | };
6 |
--------------------------------------------------------------------------------
/src/utils/PDFdocument.tsx:
--------------------------------------------------------------------------------
1 | import { Document, Page, Text, StyleSheet, View, Font } from '@react-pdf/renderer';
2 |
3 | interface Message {
4 | role: string;
5 | text: string;
6 | timestamp: string;
7 | }
8 |
9 | interface PdfDocumentProps {
10 | content: {
11 | conversation_id: string;
12 | messages: Message[];
13 | };
14 | }
15 |
16 | Font.register({
17 | family: 'Courier',
18 | src: 'https://fonts.gstatic.com/s/courierprime/v1/u-450q2lgwslOqpF2pIMou3L3MBPgyo.ttf',
19 | });
20 |
21 | const styles = StyleSheet.create({
22 | page: { padding: 20, fontSize: 12 },
23 | title: { fontSize: 24, marginBottom: 20 },
24 | messageContainer: { marginBottom: 10, padding: 10, borderRadius: 8 },
25 | userMessage: { backgroundColor: '#e3f2fd', alignSelf: 'flex-end' },
26 | modelMessage: { backgroundColor: '#f1f1f1', alignSelf: 'flex-start' },
27 | role: { fontSize: 10, fontWeight: 'bold', marginBottom: 4 },
28 | text: { fontSize: 12, marginBottom: 4 },
29 | boldText: { fontSize: 12, fontWeight: 'bold' },
30 | italicText: { fontSize: 12, fontStyle: 'italic' },
31 | codeBlock: {
32 | fontFamily: 'Courier',
33 | backgroundColor: '#f5f5f5',
34 | padding: 10,
35 | borderRadius: 4,
36 | fontSize: 10,
37 | },
38 | timestamp: { fontSize: 8, color: '#888888', textAlign: 'right' },
39 | });
40 |
41 | const formatTimestamp = (timestamp: string) => new Date(timestamp).toLocaleString();
42 |
43 | const parseMarkdown = (text: string) => {
44 | const elements: JSX.Element[] = [];
45 | const regex = /(\*\*([^*]+)\*\*)|(\*([^*]+)\*)|(`([^`]+)`)/g;
46 | let lastIndex = 0;
47 |
48 | text.replace(regex, (match, bold, boldText, italic, italicText, inlineCode, codeText, index) => {
49 | if (index > lastIndex) {
50 | elements.push({text.slice(lastIndex, index)} );
51 | }
52 |
53 | if (bold) {
54 | elements.push({boldText} );
55 | } else if (italic) {
56 | elements.push({italicText} );
57 | } else if (inlineCode) {
58 | elements.push({codeText} );
59 | }
60 |
61 | lastIndex = index + match.length;
62 | return '';
63 | });
64 |
65 | if (lastIndex < text.length) {
66 | elements.push({text.slice(lastIndex)} );
67 | }
68 |
69 | return elements;
70 | };
71 |
72 | const parseMessage = (text: string) => {
73 | const segments = text.split(/(```[\w]*\n|```)/g); // Split by code block markers
74 | let isCode = false; // Track whether the current segment is code
75 | const parsedSegments: JSX.Element[] = [];
76 |
77 | segments.forEach((segment, index) => {
78 | if (segment.startsWith("```")) {
79 | isCode = !isCode; // Toggle code state on each code marker
80 | } else if (isCode) {
81 | parsedSegments.push(
82 | {segment}
83 | );
84 | } else {
85 | parsedSegments.push(
86 | {parseMarkdown(segment)}
87 | );
88 | }
89 | });
90 |
91 | return parsedSegments;
92 | };
93 |
94 | const PdfDocument = ({ content }: PdfDocumentProps) => (
95 |
96 |
97 | Chat History
98 | {content.messages.map((message, index) => (
99 |
106 | {message.role === 'model' ? 'Cerina' : 'User'}
107 | {parseMessage(message.text)}
108 | {formatTimestamp(message.timestamp)}
109 |
110 | ))}
111 |
112 |
113 | );
114 |
115 | export default PdfDocument;
116 |
--------------------------------------------------------------------------------
/src/utils/index.tsx:
--------------------------------------------------------------------------------
1 | // utilities.ts
2 | import { saveAs } from 'file-saver';
3 | import { pdf } from '@react-pdf/renderer';
4 | import { getChatMessages } from '@/services/dispatch/chat-dispatch';
5 | import PdfDocument from './PDFdocument';
6 |
7 | // Convert file to base64
8 | export const convertToBase64 = async (file: File): Promise => {
9 | return new Promise((resolve, reject) => {
10 | const reader = new FileReader();
11 | reader.onload = () => resolve(reader.result);
12 | reader.onerror = (error) => reject(error);
13 | reader.readAsDataURL(file);
14 | });
15 | };
16 |
17 | // Function to download chat history as a PDF
18 | export const downloadHistory = async (id: string): Promise => {
19 | try {
20 | // Fetch chat messages based on id
21 | const history = await getChatMessages(id);
22 | console.log(history);
23 |
24 | // Convert history to a single string to pass as content
25 |
26 | // Create a PDF blob from the PdfDocument component
27 | const blob = await pdf( ).toBlob();
28 |
29 | const fileName = `${history.conversation_id}.pdf`;
30 | console.log(blob);
31 | saveAs(blob, fileName);
32 | } catch (error) {
33 | console.error('Error downloading history:', error);
34 | }
35 | };
36 |
37 | export const formatDate=(dateString: any)=> {
38 | return new Date(dateString as string).toLocaleString()
39 | }
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | const { nextui } = require("@nextui-org/react");
3 |
4 | export default {
5 | content: [
6 | "./index.html",
7 | "./src/**/*.{js,ts,jsx,tsx}",
8 | "./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}",
9 | ],
10 | theme: {
11 | extend: {
12 | keyframes: {
13 | modal: {
14 | 'from': { transform: 'scale(1.2)',opacity:0 },
15 | 'to': { transform: 'scale(1)',opacity:1 },
16 | }
17 | },
18 | screens: {
19 | 'xs': '460px', // Define custom breakpoint at 320px
20 | },
21 | boxShadow: {
22 | custom: "1px 2px 20px 2px rgba(0, 0, 0, 0.35)",
23 | },
24 | },
25 | },
26 | darkMode: "class",
27 | plugins: [
28 | nextui({
29 | // addCommonColors: true,
30 | themes: {
31 | dark: {
32 | colors: {
33 | background: "#252525",
34 | foreground: "#ffffff",
35 | primary: {
36 | DEFAULT: "#0FA37F",
37 | foreground: "#ffffff",
38 | },
39 | danger: {
40 | DEFAULT: "#FC506D",
41 | foreground: "#ffffff",
42 | },
43 | success: {
44 | DEFAULT: "#4CD19F",
45 | foreground: "#ffffff",
46 | },
47 | secondary: {
48 | DEFAULT: "#494949",
49 | foreground: "#ffffff",
50 | },
51 | },
52 | },
53 | light: {
54 | colors: {
55 | background: "#E4E3E8",
56 | foreground: "#000000",
57 | primary: {
58 | DEFAULT: "#000000",
59 | foreground: "#ffffff",
60 | },
61 | danger: {
62 | DEFAULT: "#FC506D",
63 | foreground: "#ffffff",
64 | },
65 | success: {
66 | DEFAULT: "#4CD19F",
67 | foreground: "#ffffff",
68 | },
69 | secondary: {
70 | DEFAULT: "#78778B",
71 | foreground: "#ffffff",
72 | },
73 | },
74 | },
75 | },
76 | }),
77 | ],
78 | };
79 |
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "isolatedModules": true,
13 | "moduleDetection": "force",
14 | "noEmit": true,
15 | "jsx": "react-jsx",
16 |
17 | /* Linting */
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noFallthroughCasesInSwitch": true
22 | },
23 | "include": ["src"]
24 | }
25 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 | "paths": {
9 | "@/*": ["./src/*"]
10 | },
11 |
12 | /* Bundler mode */
13 | "moduleResolution": "bundler",
14 | "allowImportingTsExtensions": true,
15 | "resolveJsonModule": true,
16 | "isolatedModules": true,
17 | "noEmit": true,
18 | "jsx": "react-jsx",
19 |
20 | /* Linting */
21 | "strict": true,
22 | "noUnusedLocals": true,
23 | "noUnusedParameters": true,
24 | "noFallthroughCasesInSwitch": true
25 | },
26 | "include": ["src"],
27 | "references": [{ "path": "./tsconfig.node.json" }]
28 | }
29 |
--------------------------------------------------------------------------------
/tsconfig.node copy.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true,
8 | "strict": true
9 | },
10 | "include": ["vite.config.ts"]
11 | }
12 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true,
8 | "strict": true
9 | },
10 | "include": ["vite.config.ts"]
11 | }
12 |
--------------------------------------------------------------------------------
/tsconfig.node.tsbuildinfo:
--------------------------------------------------------------------------------
1 | {"program":{"fileNames":["./node_modules/typescript/lib/lib.d.ts","./node_modules/typescript/lib/lib.es5.d.ts","./node_modules/typescript/lib/lib.dom.d.ts","./node_modules/typescript/lib/lib.webworker.importscripts.d.ts","./node_modules/typescript/lib/lib.scripthost.d.ts","./node_modules/typescript/lib/lib.decorators.d.ts","./node_modules/typescript/lib/lib.decorators.legacy.d.ts","./node_modules/@types/estree/index.d.ts","./node_modules/rollup/dist/rollup.d.ts","./node_modules/rollup/dist/parseast.d.ts","./node_modules/vite/types/hmrpayload.d.ts","./node_modules/vite/types/customevent.d.ts","./node_modules/vite/types/hot.d.ts","./node_modules/vite/dist/node/types.d-agj9qkwt.d.ts","./node_modules/esbuild/lib/main.d.ts","./node_modules/source-map-js/source-map.d.ts","./node_modules/postcss/lib/previous-map.d.ts","./node_modules/postcss/lib/input.d.ts","./node_modules/postcss/lib/css-syntax-error.d.ts","./node_modules/postcss/lib/declaration.d.ts","./node_modules/postcss/lib/root.d.ts","./node_modules/postcss/lib/warning.d.ts","./node_modules/postcss/lib/lazy-result.d.ts","./node_modules/postcss/lib/no-work-result.d.ts","./node_modules/postcss/lib/processor.d.ts","./node_modules/postcss/lib/result.d.ts","./node_modules/postcss/lib/document.d.ts","./node_modules/postcss/lib/rule.d.ts","./node_modules/postcss/lib/node.d.ts","./node_modules/postcss/lib/comment.d.ts","./node_modules/postcss/lib/container.d.ts","./node_modules/postcss/lib/at-rule.d.ts","./node_modules/postcss/lib/list.d.ts","./node_modules/postcss/lib/postcss.d.ts","./node_modules/postcss/lib/postcss.d.mts","./node_modules/vite/dist/node/runtime.d.ts","./node_modules/vite/types/importglob.d.ts","./node_modules/vite/types/metadata.d.ts","./node_modules/vite/dist/node/index.d.ts","./node_modules/@babel/types/lib/index.d.ts","./node_modules/@types/babel__generator/index.d.ts","./node_modules/@babel/parser/typings/babel-parser.d.ts","./node_modules/@types/babel__template/index.d.ts","./node_modules/@types/babel__traverse/index.d.ts","./node_modules/@types/babel__core/index.d.ts","./node_modules/@vitejs/plugin-react/dist/index.d.mts","./node_modules/vite-tsconfig-paths/dist/index.d.ts","./node_modules/vite-tsconfig-paths/dist/index.d.mts","./vite.config.ts","./node_modules/@types/ms/index.d.ts","./node_modules/@types/debug/index.d.ts","./node_modules/@types/estree-jsx/index.d.ts","./node_modules/@types/unist/index.d.ts","./node_modules/@types/hast/index.d.ts","./node_modules/@types/react/global.d.ts","./node_modules/csstype/index.d.ts","./node_modules/@types/prop-types/index.d.ts","./node_modules/@types/react/index.d.ts","./node_modules/@types/hoist-non-react-statics/index.d.ts","./node_modules/@types/lodash/common/common.d.ts","./node_modules/@types/lodash/common/array.d.ts","./node_modules/@types/lodash/common/collection.d.ts","./node_modules/@types/lodash/common/date.d.ts","./node_modules/@types/lodash/common/function.d.ts","./node_modules/@types/lodash/common/lang.d.ts","./node_modules/@types/lodash/common/math.d.ts","./node_modules/@types/lodash/common/number.d.ts","./node_modules/@types/lodash/common/object.d.ts","./node_modules/@types/lodash/common/seq.d.ts","./node_modules/@types/lodash/common/string.d.ts","./node_modules/@types/lodash/common/util.d.ts","./node_modules/@types/lodash/index.d.ts","./node_modules/@types/lodash.debounce/index.d.ts","./node_modules/@types/mdast/index.d.ts","./node_modules/@types/react-dom/index.d.ts","./node_modules/@types/react-syntax-highlighter/index.d.ts","./node_modules/@types/use-sync-external-store/index.d.ts"],"fileInfos":["a7297ff837fcdf174a9524925966429eb8e5feecc2cc55cc06574e6b092c1eaa",{"version":"44e584d4f6444f58791784f1d530875970993129442a847597db702a073ca68c","affectsGlobalScope":true},{"version":"4af6b0c727b7a2896463d512fafd23634229adf69ac7c00e2ae15a09cb084fad","affectsGlobalScope":true},{"version":"80e18897e5884b6723488d4f5652167e7bb5024f946743134ecc4aa4ee731f89","affectsGlobalScope":true},{"version":"cd034f499c6cdca722b60c04b5b1b78e058487a7085a8e0d6fb50809947ee573","affectsGlobalScope":true},{"version":"33358442698bb565130f52ba79bfd3d4d484ac85fe33f3cb1759c54d18201393","affectsGlobalScope":true},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","affectsGlobalScope":true},"ee7d8894904b465b072be0d2e4b45cf6b887cdba16a467645c4e200982ece7ea",{"version":"f5ed3fcde4f8e724ad213e1deddd8a52adaeca132025b613f32bfe7ace31f42b","affectsGlobalScope":true},"a660aa95476042d3fdcc1343cf6bb8fdf24772d31712b1db321c5a4dcc325434","282f98006ed7fa9bb2cd9bdbe2524595cfc4bcd58a0bb3232e4519f2138df811","6222e987b58abfe92597e1273ad7233626285bc2d78409d4a7b113d81a83496b","cbe726263ae9a7bf32352380f7e8ab66ee25b3457137e316929269c19e18a2be","8b96046bf5fb0a815cba6b0880d9f97b7f3a93cf187e8dcfe8e2792e97f38f87",{"version":"bacf2c84cf448b2cd02c717ad46c3d7fd530e0c91282888c923ad64810a4d511","affectsGlobalScope":true},"858d0d831826c6eb563df02f7db71c90e26deadd0938652096bea3cc14899700","8885cf05f3e2abf117590bbb951dcf6359e3e5ac462af1c901cfd24c6a6472e2","bb37a0638c082c58b9c2743213e11f1bf21ee127c4e9d491d9a6ea5ff51820c6","e61df3640a38d535fd4bc9f4a53aef17c296b58dc4b6394fd576b808dd2fe5e6","80781460eca408fe8d2937d9fdbbb780d6aac35f549621e6200c9bee1da5b8fe","352706de457583c883af57408149007f73ad2f42f5951b2b0de9603af2d4fb9e","7ec359bbc29b69d4063fe7dad0baaf35f1856f914db16b3f4f6e3e1bca4099fa","b9261ac3e9944d3d72c5ee4cf888ad35d9743a5563405c6963c4e43ee3708ca4","c84fd54e8400def0d1ef1569cafd02e9f39a622df9fa69b57ccc82128856b916","c7a38c1ef8d6ae4bf252be67bd9a8b012b2cdea65bd6225a3d1a726c4f0d52b6","2ed6489ef46eb61442d067c08e87e3db501c0bfb2837eee4041a27bf3e792bb0","54916fb53d13d06476a63992ca6f0e217b7479aa81373f00e628dd923bf88ccf","da710103337668d6b63f2fd74329c1a5d29e92b51b345b568819fbe65499af30","ee2bc8d5a6885d88ebb59fc1a80167a8f96f82cedce2fb6367fe77ec8a87d3a9","774b783046ba3d473948132d28a69f52a295b2f378f2939304118ba571b1355e","bad9c34f6923020692d8bc9ac12148e95c6bf740c5c3e5b2e84ebcc95a8f12de","14ba97f0907144771331e1349fdccb5a13526eba0647e6b447e572376d811b6f","fd630a97a81e535d943456555ce5a8007572714dd5e1bb08a0d1e425ac25b633","d1a4bc0f6c2fa03bcdbfcd9ea3ab9af78bcd29089d1344761d0a5b868dcf9ea6","26e629be9bbd94ea1d465af83ce5a3306890520695f07be6eb016f8d734d02be","82e687ebd99518bc63ea04b0c3810fb6e50aa6942decd0ca6f7a56d9b9a212a6","7f698624bbbb060ece7c0e51b7236520ebada74b747d7523c7df376453ed6fea","8f07f2b6514744ac96e51d7cb8518c0f4de319471237ea10cf688b8d0e9d0225","9ae0ca65717af0d3b554a26fd333ad9c78ad3910ad4b22140ff02acb63076927","e74998d5cefc2f29d583c10b99c1478fb810f1e46fbb06535bfb0bbba3c84aa5","2c8e55457aaf4902941dfdba4061935922e8ee6e120539c9801cd7b400fae050","43d058146b002d075f5d0033a6870321048297f1658eb0db559ba028383803a6","670a76db379b27c8ff42f1ba927828a22862e2ab0b0908e38b671f0e912cc5ed","9e0cf651e8e2c5b9bebbabdff2f7c6f8cedd91b1d9afcc0a854cdff053a88f1b","069bebfee29864e3955378107e243508b163e77ab10de6a5ee03ae06939f0bb9","f5e8546cfe500116aba8a6cb7ee171774b14a6db30d4bcd6e0aa5073e919e739","d6838763d96ca56f56a7acdefb550e929f8a0c25d4d1e8b01a1bcc5ecfcad2cd","b943e4cfae007bf0e8b5aa9cbb979865505e89586fd1e45bb7aabf0f855ed1d5",{"version":"8692a3ee912792ce9573fed379649606e54511fcbddf331303367316749bcfda","signature":"4b96dd19fd2949d28ce80e913412b0026dc421e5bf6c31d87c7b5eb11b5753b4"},"68cc8d6fcc2f270d7108f02f3ebc59480a54615be3e09a47e14527f349e9d53e","3eb11dbf3489064a47a2e1cf9d261b1f100ef0b3b50ffca6c44dd99d6dd81ac1","5d08a179b846f5ee674624b349ebebe2121c455e3a265dc93da4e8d9e89722b4","89121c1bf2990f5219bfd802a3e7fc557de447c62058d6af68d6b6348d64499a","79b4369233a12c6fa4a07301ecb7085802c98f3a77cf9ab97eee27e1656f82e6",{"version":"36a2e4c9a67439aca5f91bb304611d5ae6e20d420503e96c230cf8fcdc948d94","affectsGlobalScope":true},"8a8eb4ebffd85e589a1cc7c178e291626c359543403d58c9cd22b81fab5b1fb9","247a952efd811d780e5630f8cfd76f495196f5fa74f6f0fee39ac8ba4a3c9800",{"version":"d145d9c91e20758369b8e4b14cd6d70c86a3fbe480da51bb3b82598c8bbbec7e","affectsGlobalScope":true},"a7ca2a9e61286d74bc37fe64e5dcd7da04607f7f5432f7c651b47b573fc76cef","ff81bffa4ecfceae2e86b5920c3fcb250b66b1d6ed72944dffdf58123be2481b","458111fc89d11d2151277c822dfdc1a28fa5b6b2493cf942e37d4cd0a6ee5f22","da2b6356b84a40111aaecb18304ea4e4fcb43d70efb1c13ca7d7a906445ee0d3","187119ff4f9553676a884e296089e131e8cc01691c546273b1d0089c3533ce42","aa2c18a1b5a086bbcaae10a4efba409cc95ba7287d8cf8f2591b53704fea3dea","6f294731b495c65ecf46a5694f0082954b961cf05463bea823f8014098eaffa0","0aaef8cded245bf5036a7a40b65622dd6c4da71f7a35343112edbe112b348a1e","00baffbe8a2f2e4875367479489b5d43b5fc1429ecb4a4cc98cfc3009095f52a","bdf0ed7d9ebae6175a5d1b4ec4065d07f8099379370a804b1faff05004dc387d","3c92b6dfd43cc1c2485d9eba5ff0b74a19bb8725b692773ef1d66dac48cda4bd","b03afe4bec768ae333582915146f48b161e567a81b5ebc31c4d78af089770ac9","df996e25faa505f85aeb294d15ebe61b399cf1d1e49959cdfaf2cc0815c203f9","4f6a12044ee6f458db11964153830abbc499e73d065c51c329ec97407f4b13dd","7605dd065ecbd2d8ff5f80a0b3813fc163ed593f4f24f3b6f6a7e98ac0e2157f","d4a22007b481fe2a2e6bfd3a42c00cd62d41edb36d30fc4697df2692e9891fc8","adb17fea4d847e1267ae1241fa1ac3917c7e332999ebdab388a24d82d4f58240",{"version":"3cef134032da5e1bfabba59a03a58d91ed59f302235034279bb25a5a5b65ca62","affectsGlobalScope":true},"61f41da9aaa809e5142b1d849d4e70f3e09913a5cb32c629bf6e61ef27967ff7"],"root":[49],"options":{"allowSyntheticDefaultImports":true,"composite":true,"module":99,"skipLibCheck":true,"strict":true},"fileIdsList":[[40],[40,41,42,43,44],[40,42],[50],[8,9,52],[53],[58],[72],[60,62,63,64,65,66,67,68,69,70,71,72],[60,61,63,64,65,66,67,68,69,70,71,72],[61,62,63,64,65,66,67,68,69,70,71,72],[60,61,62,64,65,66,67,68,69,70,71,72],[60,61,62,63,65,66,67,68,69,70,71,72],[60,61,62,63,64,66,67,68,69,70,71,72],[60,61,62,63,64,65,67,68,69,70,71,72],[60,61,62,63,64,65,66,68,69,70,71,72],[60,61,62,63,64,65,66,67,69,70,71,72],[60,61,62,63,64,65,66,67,68,70,71,72],[60,61,62,63,64,65,66,67,68,69,71,72],[60,61,62,63,64,65,66,67,68,69,70,72],[60,61,62,63,64,65,66,67,68,69,70,71],[58,76],[55,56,57],[39,45],[31],[29,31],[20,28,29,30,32],[18],[21,26,31,34],[17,34],[21,22,25,26,27,34],[21,22,23,25,26,34],[18,19,20,21,22,26,27,28,30,31,32,34],[34],[16,18,19,20,21,22,23,25,26,27,28,29,30,31,32,33],[16,34],[21,23,24,26,27,34],[25,34],[26,27,31,34],[19,29],[9,38],[16],[39,47],[39],[9,10,11,12,13,14,15,35,36,37,38],[11,12,13,14],[11,12,13],[11],[12],[9],[39,46,48]],"referencedMap":[[42,1],[45,2],[41,1],[43,3],[44,1],[51,4],[52,5],[54,6],[59,7],[73,8],[61,9],[62,10],[60,11],[63,12],[64,13],[65,14],[66,15],[67,16],[68,17],[69,18],[70,19],[71,20],[72,21],[74,6],[75,7],[76,22],[58,23],[46,24],[32,25],[30,26],[31,27],[19,28],[20,26],[27,29],[18,30],[23,31],[24,32],[29,33],[35,34],[34,35],[17,36],[25,37],[26,38],[21,39],[28,25],[22,40],[10,41],[9,5],[16,42],[48,43],[47,44],[39,45],[36,46],[14,47],[12,48],[13,49],[38,50],[49,51]],"latestChangedDtsFile":"./vite.config.d.ts"},"version":"5.5.4"}
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
3 | }
--------------------------------------------------------------------------------
/vite.config.d.ts:
--------------------------------------------------------------------------------
1 | declare const _default: import("vite").UserConfig;
2 | export default _default;
3 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import react from "@vitejs/plugin-react";
3 | import tsconfigPaths from "vite-tsconfig-paths";
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react(), tsconfigPaths()],
7 | optimizeDeps: {
8 | include: ["remark-breaks"], // Force Vite to pre-bundle this module
9 | }
10 | });
11 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import react from "@vitejs/plugin-react";
3 | import tsconfigPaths from "vite-tsconfig-paths";
4 |
5 | // https://vitejs.dev/config/
6 | export default defineConfig({
7 | plugins: [react(), tsconfigPaths(),],
8 | optimizeDeps: {
9 | include: ["remark-breaks"], // Force Vite to pre-bundle this module
10 | }
11 | });
12 |
--------------------------------------------------------------------------------