diff --git a/README.md b/README.md index 0cf1221..bb479af 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,20 @@ * First download gguf from https://huggingface.co/TheBloke/Llama-2-13B-chat-GGUF , `llama-2-13b-chat.Q5_K_S.gguf` * move this model to `model/llama-2-13b-chat.Q5_K_S.gguf` -* to locally develop the frontend, you need to: - 1. Navigate to the frontend directory: `cd frontend` - 2. Install PNPM: `npm install -g pnpm` - 3. Install Dependencies: `npm start` - 4. start the application in dev mode: `pnpm dev` - 5. production build: `pnpm build` - -# To run the backend -* run docker containers: docker-compose up -d -* after making sure that the database and the frontend are running, run the LlmServiceApplication + + +## Running the frontend: +1. Navigate to the frontend directory: `cd frontend` +2. Install node `node:18.17.1` +3. Install PNPM: `npm install -g pnpm` `pnpm:>=8.0.0` +4. Install Dependencies: `pnpm install` +5. start the application in dev mode: `pnpm dev` +6. production build: `pnpm build` +7. any linter problem `pnpm lint:fix` or to check `pnpm lint` + + +## Running the backend +* run docker containers +* after making sure that the database and the frontend are running, run the `LlmServiceApplication` +* to run the Backend linter `mvn spotless:check` to fix `mvn spotless:check` +* Backend uses cpp plugin to run the modal , `C++11` `g++` `cmake` diff --git a/backend/src/main/java/com/llm_service/llm_service/controller/user/UserApiMapper.java b/backend/src/main/java/com/llm_service/llm_service/controller/user/UserApiMapper.java index 5282295..bb2d109 100644 --- a/backend/src/main/java/com/llm_service/llm_service/controller/user/UserApiMapper.java +++ b/backend/src/main/java/com/llm_service/llm_service/controller/user/UserApiMapper.java @@ -10,5 +10,6 @@ public interface UserApiMapper { LoginResponse map(AuthenticationResponse response); @Mapping(target = "name", source = "username") + @Mapping(target = "role", source = "role") UserResponse map(User user); } diff --git a/backend/src/main/java/com/llm_service/llm_service/controller/user/UserController.java b/backend/src/main/java/com/llm_service/llm_service/controller/user/UserController.java index 8dc6631..dadbe34 100644 --- a/backend/src/main/java/com/llm_service/llm_service/controller/user/UserController.java +++ b/backend/src/main/java/com/llm_service/llm_service/controller/user/UserController.java @@ -1,6 +1,7 @@ package com.llm_service.llm_service.controller.user; import com.llm_service.llm_service.dto.User; +import com.llm_service.llm_service.exception.UnAuthorizedException; import com.llm_service.llm_service.exception.user.UserAlreadyExistsException; import com.llm_service.llm_service.exception.user.UserNotFoundException; import com.llm_service.llm_service.exception.user.UsernameAlreadyExistsException; @@ -26,6 +27,20 @@ public UserController(AuthenticationService authenticationService, UserApiMapper this.userApiMapper = userApiMapper; } + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "Get Current User Data", + content = {@Content(mediaType = "application/json")}) + }) + @Operation(summary = "get the current user") + @GetMapping("/me") + public ResponseEntity register() throws UnAuthorizedException { + User user = authenticationService.getUser().orElseThrow(UnAuthorizedException::new); + return ResponseEntity.status(HttpStatus.OK).body(userApiMapper.map(user)); + } + @ApiResponses( value = { @ApiResponse( diff --git a/backend/src/main/java/com/llm_service/llm_service/controller/user/UserResponse.java b/backend/src/main/java/com/llm_service/llm_service/controller/user/UserResponse.java index b70bf58..d7af55e 100644 --- a/backend/src/main/java/com/llm_service/llm_service/controller/user/UserResponse.java +++ b/backend/src/main/java/com/llm_service/llm_service/controller/user/UserResponse.java @@ -1,5 +1,6 @@ package com.llm_service.llm_service.controller.user; +import com.llm_service.llm_service.persistance.entities.Role; import lombok.Value; import lombok.experimental.SuperBuilder; import lombok.extern.jackson.Jacksonized; @@ -9,4 +10,5 @@ @SuperBuilder public class UserResponse { String name; + Role role; } diff --git a/backend/src/main/java/com/llm_service/llm_service/service/jwt/AuthenticationService.java b/backend/src/main/java/com/llm_service/llm_service/service/jwt/AuthenticationService.java index e2bc2a5..fe8c072 100644 --- a/backend/src/main/java/com/llm_service/llm_service/service/jwt/AuthenticationService.java +++ b/backend/src/main/java/com/llm_service/llm_service/service/jwt/AuthenticationService.java @@ -9,7 +9,9 @@ import com.llm_service.llm_service.persistance.repositories.token.TokenPersistenceManager; import com.llm_service.llm_service.persistance.repositories.user.UserEntityMapper; import com.llm_service.llm_service.persistance.repositories.user.UserPersistenceManager; +import com.llm_service.llm_service.service.user.UserContext; import java.util.List; +import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -32,6 +34,12 @@ public class AuthenticationService { private final UserEntityMapper userEntityMapper; + private final UserContext userContext; + + public Optional getUser() { + return userContext.getUserFromContext(); + } + public User register(UserRequest userRequest) throws UsernameAlreadyExistsException { User user = User.builder() .role(userRequest.getRole()) diff --git a/frontend/.eslintrc b/frontend/.eslintrc index 8e4e90c..220412a 100644 --- a/frontend/.eslintrc +++ b/frontend/.eslintrc @@ -40,6 +40,7 @@ } }, "rules": { + "implicit-arrow-linebreak": "off", "@typescript-eslint/no-unused-vars": "off", "no-unused-vars": "off", "object-curly-newline": "off", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 06a41e9..983a999 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,61 +1,17 @@ -import { createBrowserRouter, RouterProvider } from "react-router-dom"; -import Welcome from "pages/Welcome/Welcome.tsx"; -import Conversation from "pages/Conversation/Conversation.tsx"; -import Login from "pages/Auth/Login.tsx"; -import Register from "pages/Auth/Register.tsx"; -import ForgotPassword from "pages/Auth/ForgotPassword.tsx"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; -import ConversationRedirect from "./pages/Conversation/ConversationRedirect.tsx"; -import Error500 from "./pages/Error/Error500.tsx"; -import Error404 from "./pages/Error/Error404.tsx"; - -const router = createBrowserRouter([ - { - path: "/", - Component: Welcome, - }, - { - path: "/conversation", - Component: ConversationRedirect, - }, - { - path: "/conversation/:id", - Component: Conversation, - }, - { - path: "/login", - Component: Login, - }, - { - path: "/register", - Component: Register, - }, - { - path: "/forgot-password", - Component: ForgotPassword, - }, - { - path: "/404", - Component: Error404, - }, - { - path: "/500", - Component: Error500, - }, - { - path: "*", - Component: Error404, - }, -]); +import { AuthProvider } from "./context/AuthContext.tsx"; +import Routes from "./router/Routes.tsx"; const queryClient = new QueryClient(); function App() { return ( - - + + + + ); } diff --git a/frontend/src/context/AuthContext.tsx b/frontend/src/context/AuthContext.tsx new file mode 100644 index 0000000..7514579 --- /dev/null +++ b/frontend/src/context/AuthContext.tsx @@ -0,0 +1,41 @@ +import { createContext, PropsWithChildren, useEffect, useMemo, useState } from "react"; +import { useQueryClient } from "@tanstack/react-query"; + +type AuthContextType = { token: string | null; setToken: (token: string | null) => void }; + +export const AuthContext = createContext({ + token: null, + setToken: () => {}, +}); + +export function AuthProvider({ children }: PropsWithChildren) { + // State to hold the authentication token + const [token, setToken_] = useState(localStorage.getItem("token")); + const queryClient = useQueryClient(); + + // Function to set the authentication token + const setToken = (newToken: string | null) => { + setToken_(newToken); + }; + + useEffect(() => { + if (token) { + localStorage.setItem("token", token); + } else { + localStorage.removeItem("token"); + queryClient.clear(); + } + }, [queryClient, token]); + + // Memoized value of the authentication context + const contextValue = useMemo( + () => ({ + token, + setToken, + }), + [token], + ); + + // Provide the authentication context to the children components + return {children}; +} diff --git a/frontend/src/context/useAuth.ts b/frontend/src/context/useAuth.ts new file mode 100644 index 0000000..d8a1bae --- /dev/null +++ b/frontend/src/context/useAuth.ts @@ -0,0 +1,6 @@ +import { useContext } from "react"; +import { AuthContext } from "./AuthContext.tsx"; + +export default function useAuth() { + return useContext(AuthContext); +} diff --git a/frontend/src/hooks/api/constants.ts b/frontend/src/hooks/api/constants.ts index 55424df..185ffc5 100644 --- a/frontend/src/hooks/api/constants.ts +++ b/frontend/src/hooks/api/constants.ts @@ -2,10 +2,17 @@ import { ConversationId } from "models/Id.ts"; export const Queries = { Conversation: "Conversation", + User: "User", } as const; const basePath = import.meta.env.VITE_APP_BASE_URL; +// Conversation export const ConversationsPath = `${basePath}/conversation`; export const getConversationPath = (id: ConversationId) => `${basePath}/conversation/${id}`; export const getContinueConversationPath = (id: ConversationId) => `${getConversationPath(id)}/continue`; + +// User +export const UserPath = `${basePath}/me`; +export const LoginPath = `${basePath}/login`; +export const RegisterPath = `${basePath}/register`; diff --git a/frontend/src/hooks/api/useGetUser.ts b/frontend/src/hooks/api/useGetUser.ts new file mode 100644 index 0000000..16ddf97 --- /dev/null +++ b/frontend/src/hooks/api/useGetUser.ts @@ -0,0 +1,14 @@ +import { useQuery } from "@tanstack/react-query"; +import { User } from "models/User.ts"; +import useApi from "hooks/useApi.ts"; +import { Queries, UserPath } from "./constants.ts"; + +export default function useGetUser() { + const callApi = useApi(); + + return useQuery({ + queryKey: [Queries.User], + queryFn: () => callApi({ url: UserPath }), + staleTime: Infinity, + }); +} diff --git a/frontend/src/hooks/api/useLogout.ts b/frontend/src/hooks/api/useLogout.ts new file mode 100644 index 0000000..c72d160 --- /dev/null +++ b/frontend/src/hooks/api/useLogout.ts @@ -0,0 +1,9 @@ +import useAuth from "context/useAuth.ts"; + +export default function useLogout() { + const { setToken } = useAuth(); + + return () => { + setToken(null); + }; +} diff --git a/frontend/src/hooks/useApi.ts b/frontend/src/hooks/useApi.ts index 642e034..8a9ced7 100644 --- a/frontend/src/hooks/useApi.ts +++ b/frontend/src/hooks/useApi.ts @@ -1,8 +1,11 @@ import { useCallback } from "react"; import { api, ApiRequest } from "utils/api.ts"; +import useAuth from "context/useAuth.ts"; type CallApiArgs = Omit; export default function useApi() { - return useCallback(async (args: CallApiArgs) => api(args), []); + const { token } = useAuth(); + + return useCallback(async (args: CallApiArgs) => api({ ...args, token }), [token]); } diff --git a/frontend/src/hooks/useLogin.ts b/frontend/src/hooks/useLogin.ts new file mode 100644 index 0000000..4366ec7 --- /dev/null +++ b/frontend/src/hooks/useLogin.ts @@ -0,0 +1,22 @@ +import { useMutation } from "@tanstack/react-query"; +import useAuth from "context/useAuth.ts"; +import useApi from "./useApi.ts"; +import { LoginPath } from "./api/constants.ts"; + +interface LoginRequest { + username: string; + password: string; +} + +export default function useLogin() { + const callApi = useApi(); + const { setToken } = useAuth(); + + return useMutation({ + mutationFn: ({ username, password }: LoginRequest) => + callApi<{ token: string }>({ url: LoginPath, body: { username, password }, method: "POST" }), + onSuccess: (res) => { + setToken(res.token); + }, + }); +} diff --git a/frontend/src/hooks/useRegister.ts b/frontend/src/hooks/useRegister.ts new file mode 100644 index 0000000..e400411 --- /dev/null +++ b/frontend/src/hooks/useRegister.ts @@ -0,0 +1,24 @@ +import { useMutation } from "@tanstack/react-query"; +import { RolesType } from "models/User.ts"; +import useApi from "./useApi.ts"; +import { RegisterPath } from "./api/constants.ts"; + +interface RegisterRequest { + username: string; + firstName: string; + lastName: string; + role: RolesType; + password: string; +} + +export default function useRegister() { + const callApi = useApi(); + return useMutation({ + mutationFn: ({ username, password, firstName, lastName, role }: RegisterRequest) => + callApi<{ name: string }>({ + url: RegisterPath, + body: { username, password, firstName, lastName, role }, + method: "POST", + }), + }); +} diff --git a/frontend/src/models/User.ts b/frontend/src/models/User.ts new file mode 100644 index 0000000..cfe8fea --- /dev/null +++ b/frontend/src/models/User.ts @@ -0,0 +1,13 @@ +import { ObjectValues } from "utils/types/index.ts"; + +export const Roles = { + FREE: "FREE", + PAID: "PAID", +} as const; + +export type RolesType = ObjectValues; + +export interface User { + username: string; + role: RolesType; +} diff --git a/frontend/src/pages/Auth/Login.tsx b/frontend/src/pages/Auth/Login.tsx index b0603d6..0a64aec 100644 --- a/frontend/src/pages/Auth/Login.tsx +++ b/frontend/src/pages/Auth/Login.tsx @@ -1,37 +1,59 @@ import InputWithLabel from "ui/InputWithLabel.tsx"; -import Checkbox from "ui/Checkbox.tsx"; import LinkText from "ui/LinkText.tsx"; -import FormSubmitButton from "./components/FormSubmitButton.tsx"; +import { useState } from "react"; +import useLogin from "hooks/useLogin.ts"; +import { useNavigate } from "react-router-dom"; +import Button from "ui/Button.tsx"; function Login() { + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const { mutateAsync, isPending } = useLogin(); + const navigate = useNavigate(); + + const onSubmit = async () => { + await mutateAsync({ username, password }); + navigate("/conversation"); + }; + return (

Welcome Back :)

-
+
- + setUsername(e.currentTarget.value)} + />
- + setPassword(e.currentTarget.value)} + />
-
- - Remember me -
Forgot password ?
- Sign In +
Not a member? Register
- +
); diff --git a/frontend/src/pages/Auth/Register.tsx b/frontend/src/pages/Auth/Register.tsx index 38c833f..d44b171 100644 --- a/frontend/src/pages/Auth/Register.tsx +++ b/frontend/src/pages/Auth/Register.tsx @@ -1,8 +1,33 @@ import InputWithLabel from "ui/InputWithLabel.tsx"; import LinkText from "ui/LinkText.tsx"; -import FormSubmitButton from "./components/FormSubmitButton.tsx"; +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import Button from "ui/Button.tsx"; +import useRegister from "hooks/useRegister.ts"; +import Checkbox from "ui/Checkbox.tsx"; +import { Roles } from "models/User.ts"; function Register() { + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [passwordCheck, setCheckPassword] = useState(""); + const [firstName, setFirstName] = useState(""); + const [lastName, setLastName] = useState(""); + const [premium, setPremium] = useState(false); + const { mutateAsync, isPending } = useRegister(); + const navigate = useNavigate(); + + const onSubmit = async () => { + await mutateAsync({ + username, + password, + firstName, + lastName, + role: premium ? Roles.PAID : Roles.FREE, + }); + navigate("/login"); + }; + return (

@@ -11,15 +36,62 @@ function Register() {
- + setUsername(e.currentTarget.value)} + /> +
+
+ setPassword(e.currentTarget.value)} + />
- + setCheckPassword(e.currentTarget.value)} + />
- + setFirstName(e.currentTarget.value)} + /> +
+
+ setLastName(e.currentTarget.value)} + /> +
+
+ Premium + setPremium((value) => !value)} checked={premium} />
- Register +
Already have an account Log In diff --git a/frontend/src/pages/Auth/components/FormSubmitButton.tsx b/frontend/src/pages/Auth/components/FormSubmitButton.tsx index fd41bde..039cfcc 100644 --- a/frontend/src/pages/Auth/components/FormSubmitButton.tsx +++ b/frontend/src/pages/Auth/components/FormSubmitButton.tsx @@ -1,13 +1,20 @@ import { PropsWithChildren } from "react"; -function FormSubmitButton({ children }: PropsWithChildren) { +interface FormSubmitButtonProps extends PropsWithChildren { + disabled: boolean; +} + +function FormSubmitButton({ children, disabled }: FormSubmitButtonProps) { return ( diff --git a/frontend/src/pages/Conversation/Conversation.tsx b/frontend/src/pages/Conversation/Conversation.tsx index 0d4c92c..96c98bb 100644 --- a/frontend/src/pages/Conversation/Conversation.tsx +++ b/frontend/src/pages/Conversation/Conversation.tsx @@ -5,7 +5,6 @@ import Navbar from "./Navbar/Navbar.tsx"; import Discussions from "./Discussions/Discussions.tsx"; import Prompt from "./Prompt/Prompt.tsx"; -// TODO this might need an id in the future function Conversation() { const { id } = useParams(); diff --git a/frontend/src/pages/Conversation/Navbar/Conversations.tsx b/frontend/src/pages/Conversation/Navbar/Conversations.tsx new file mode 100644 index 0000000..29597da --- /dev/null +++ b/frontend/src/pages/Conversation/Navbar/Conversations.tsx @@ -0,0 +1,59 @@ +import { useState } from "react"; +import { clsx } from "clsx"; +import SpinLoader from "ui/SpinLoader.tsx"; +import Button from "ui/Button.tsx"; +import useGetConversations from "hooks/api/useGetConversations.ts"; +import { ConversationId } from "models/Id.ts"; +import { useNavigate } from "react-router-dom"; +import EditConversationModal from "./EditConversationModal.tsx"; + +interface ConversationProps { + id: ConversationId; +} + +function Conversations({ id }: ConversationProps) { + const navigate = useNavigate(); + const onConversationClick = (conversationId: ConversationId) => { + navigate(`/conversation/${conversationId}`); + }; + + const { data, isFetching, isError } = useGetConversations(); + const [modalConversationId, setModalConversationId] = useState(""); + return ( + <> +
+ {isError &&

Something Went Wrong

} + {isFetching && } + {!isFetching && data && data.length === 0 &&
No Conversation
} + {data?.map((conversation) => ( + + + ))} +
+ setModalConversationId("")} /> + + ); +} + +export default Conversations; diff --git a/frontend/src/pages/Conversation/Navbar/Navbar.tsx b/frontend/src/pages/Conversation/Navbar/Navbar.tsx index b0069bd..3b9503f 100644 --- a/frontend/src/pages/Conversation/Navbar/Navbar.tsx +++ b/frontend/src/pages/Conversation/Navbar/Navbar.tsx @@ -1,61 +1,29 @@ -import useGetConversations from "hooks/api/useGetConversations.ts"; -import SpinLoader from "ui/SpinLoader.tsx"; import Button from "ui/Button.tsx"; -import { clsx } from "clsx"; import { ConversationId } from "models/Id.ts"; -import { useState } from "react"; import { useNavigate } from "react-router-dom"; -import EditConversationModal from "./EditConversationModal.tsx"; +import useGetUser from "hooks/api/useGetUser.ts"; +import { Roles } from "models/User.ts"; +import useLogout from "hooks/api/useLogout.ts"; +import Conversations from "./Conversations.tsx"; interface NavbarProps { id: ConversationId; } function Navbar({ id }: NavbarProps) { - const { data, isFetching, isError } = useGetConversations(); const navigate = useNavigate(); - const [modalConversationId, setModalConversationId] = useState(""); - - const onConversationClick = (conversationId: ConversationId) => { - navigate(`/conversation/${conversationId}`); - }; + const { data } = useGetUser(); + const logout = useLogout(); return (
-
- {isError &&

Something Went Wrong

} - {isFetching && } - {!isFetching && data && data.length === 0 &&
No Conversation
} - {data?.map((conversation) => ( - - - ))} -
- setModalConversationId("")} /> + + {data?.role === Roles.PAID && }
); } diff --git a/frontend/src/router/ProtectedRoute.tsx b/frontend/src/router/ProtectedRoute.tsx new file mode 100644 index 0000000..7fa37a0 --- /dev/null +++ b/frontend/src/router/ProtectedRoute.tsx @@ -0,0 +1,15 @@ +import { Navigate, Outlet } from "react-router-dom"; +import useAuth from "context/useAuth.ts"; + +export default function ProtectedRoute() { + const { token } = useAuth(); + + // Check if the user is authenticated + if (!token) { + // If not authenticated, redirect to the login page + return ; + } + + // If authenticated, render the child routes + return ; +} diff --git a/frontend/src/router/Routes.tsx b/frontend/src/router/Routes.tsx new file mode 100644 index 0000000..85b3426 --- /dev/null +++ b/frontend/src/router/Routes.tsx @@ -0,0 +1,75 @@ +import { createBrowserRouter, RouterProvider } from "react-router-dom"; +import { useMemo } from "react"; +import Login from "pages/Auth/Login.tsx"; +import Register from "pages/Auth/Register.tsx"; +import ForgotPassword from "pages/Auth/ForgotPassword.tsx"; +import Error404 from "pages/Error/Error404.tsx"; +import Error500 from "pages/Error/Error500.tsx"; +import Conversation from "pages/Conversation/Conversation.tsx"; +import useAuth from "context/useAuth.ts"; +import Welcome from "pages/Welcome/Welcome.tsx"; +import ConversationRedirect from "pages/Conversation/ConversationRedirect.tsx"; +import ProtectedRoute from "./ProtectedRoute.tsx"; + +const routesForPublic = [ + { + path: "/", + Component: Welcome, + }, + { + path: "/404", + Component: Error404, + }, + { + path: "/500", + Component: Error500, + }, + { + path: "*", + Component: Error404, + }, +]; + +const routesForProtected = [ + { + path: "/", + Component: ProtectedRoute, + children: [ + { + path: "/conversation", + Component: ConversationRedirect, + }, + { + path: "/conversation/:id", + Component: Conversation, + }, + ], + }, +]; + +const routesForNotAuthenticatedOnly = [ + { + path: "/login", + Component: Login, + }, + { + path: "/register", + Component: Register, + }, + { + path: "/forgot-password", + Component: ForgotPassword, + }, +]; + +export default function Routes() { + const { token } = useAuth(); + + const router = useMemo(() => { + const routes = [...routesForPublic, ...(!token ? routesForNotAuthenticatedOnly : []), ...routesForProtected]; + return createBrowserRouter(routes); + }, [token]); + + // Provide the router configuration using RouterProvider + return ; +} diff --git a/frontend/src/ui/Input.tsx b/frontend/src/ui/Input.tsx index ae44811..8fd65ab 100644 --- a/frontend/src/ui/Input.tsx +++ b/frontend/src/ui/Input.tsx @@ -1,13 +1,15 @@ import { ComponentProps } from "react"; import { twMerge } from "tailwind-merge"; -interface InputProps extends Omit, "type"> {} +interface InputProps extends Omit, "type"> { + type: "text" | "password"; +} -export default function Input({ className, ...rest }: InputProps) { +export default function Input({ type = "text", className, ...rest }: InputProps) { return ( { label: string; + type: "text" | "password"; } function InputWithLabel({ label, name, ...rest }: InputWithLabelProps) { diff --git a/frontend/src/utils/api.ts b/frontend/src/utils/api.ts index fd01571..c253464 100644 --- a/frontend/src/utils/api.ts +++ b/frontend/src/utils/api.ts @@ -2,7 +2,7 @@ import { ApiError, NetworkError } from "models/Errors.ts"; export interface ApiRequest { url: string; - token?: string; + token: string | null; method?: Request["method"]; params?: Record; headers?: HeadersInit; diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 8a4772b..21dd79e 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -12,6 +12,7 @@ export default defineConfig({ 'pages': path.resolve(__dirname, './src/pages'), 'utils': path.resolve(__dirname, './src/utils'), 'models': path.resolve(__dirname, './src/models'), + 'context': path.resolve(__dirname, './src/context'), } }, server: {