diff --git a/README.md b/README.md index c7a2a7d2..07c09450 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,6 @@ Build your own AI real-time collaborative markdown editor in just 5 minutes. 스크린샷 2024-02-02 오후 4 35 29 - - ## Overview CodePair is an open-source real-time collaborative markdown editor with AI intelligence, built using React, NestJS, and LangChain. @@ -32,62 +30,84 @@ This repository contains multiple packages/modules that make up our project. Eac ## Getting Started with Development -### Configuration and Setup +### 1. Set Up GitHub OAuth Key + +For the Social Login feature, you need to obtain a GitHub OAuth key before running the project. Please refer to [this document](./docs/1_Set_Up_GitHub_OAuth_Key.md) for guidance. + +After completing this step, you should have the `GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET` values. + +### 2. Choose Running Mode + +We offer two options. Choose the one that best suits your needs: + +- **Frontend Development Only Mode**: Use this option if you only want to develop the frontend. +- **Full Stack Development Mode**: Use this option if you want to develop both the frontend and backend together. + +### 3-1. Frontend Development Only Mode -Before running the Frontend and Backend applications, you need to fill in the required API Keys. -Follow these steps: +1. Update your `GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET` to `./backend/docker/docker-compose-full.yml`. + + ```bash + vi ./backend/docker/docker-compose-full.yml + + # In the file, update the following values: + # GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET + GITHUB_CLIENT_ID: "your_github_client_id_here" + GITHUB_CLIENT_SECRET: "your_github_client_secret_here" + ``` + +2. Run `./backend/dockerdocker-compose-full.yml`. + + ```bash + docker-compose -f ./backend/docker/docker-compose-full.yml up -d + ``` -**Frontend Environment Configuration** +3. Run the Frontend application: -1. Navigate to the `frontend` directory. - ```bash cd frontend + npm install + npm run dev ``` -2. Copy the `.env.example` file to create a `.env.development` file. + +4. Visit http://localhost:5173 to enjoy your CodePair. + +### 3-2. Full Stack Development Mode + +1. Update your `GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET` to `./backend/.env.development`. + ```bash - cp .env.example .env.development + vi ./backend/.env.development + + # In the file, update the following values: + # GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET + GITHUB_CLIENT_ID=your_github_client_id_here + GITHUB_CLIENT_SECRET=your_github_client_secret_here ``` -3. Edit the `.env.development` file and fill in the necessary environment variable values. Refer to the comments for the meaning and examples of each value. -**Backend Environment Configuration** +2. Run `.backend/docker/docker-compose.yml`. -1. Navigate to the `frontend` directory. - ```bash - cd backend + docker-compose -f ./backend/docker/docker-compose.yml up -d ``` -2. Copy the `.env.example` file to create a `.env.development` file. + +3. Run the Backend application: + ```bash - cp .env.example .env.development + cd backend + npm install + npm run start:dev ``` -3. Edit the `.env.development` file and fill in the necessary environment variable values. Refer to the comments for the meaning and examples of each value. - -### Run Application - -1. Run the Dockerfile for MongoDB, the database used by CodePair: - - ```bash - docker-compose up -f ./backend/docker/mongodb_replica/docker-compose.yml -d - ``` - -2. Run the Backend application: - - ```bash - cd backend - npm install - npm run start:dev - ``` -3. Run the Frontend application: - - ```bash - cd frontend - npm install - npm run dev - ``` +4. Run the Frontend application: -4. Visit http://localhost:5173 to enjoy your CodePair. + ```bash + cd ../frontend + npm install + npm run dev + ``` + +5. Visit http://localhost:5173 to enjoy your CodePair. ## Contributing diff --git a/backend/.env.example b/backend/.env.development similarity index 75% rename from backend/.env.example rename to backend/.env.development index c92061fe..236e5076 100644 --- a/backend/.env.example +++ b/backend/.env.development @@ -1,10 +1,7 @@ -# This file serves as a template for configuring environment variables. -# Copy this file to `.env` and fill in the necessary values. - # DATABASE_URL: URL for connecting to the database. # Format: mongodb://:@:/ # Example: mongodb://localhost:27017/codepair (For development mode) -DATABASE_URL=your_mongodb_database_url_here +DATABASE_URL=mongodb://localhost:27017/codepair # GITHUB_CLIENT_ID: Client ID for authenticating with GitHub. # To obtain a client ID, create an OAuth app at: https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app @@ -15,29 +12,34 @@ GITHUB_CLIENT_SECRET=your_github_client_secret_here # GITHUB_CALLBACK_URL: Callback URL for handling GitHub authentication response. # Format: https:///auth/login/github # Example: http://localhost:3000/auth/login/github (For development mode) -GITHUB_CALLBACK_URL=your_github_callback_url_here +GITHUB_CALLBACK_URL=http://localhost:3000/auth/login/github # JWT_AUTH_SECRET: Secret key for JWT authentication. # This key is used to sign and verify JWT tokens. -JWT_AUTH_SECRET=your_jwt_auth_secret_here +JWT_AUTH_SECRET=you_should_change_this_secret_key_in_production # FRONTEND_BASE_URL: Base URL of the frontend application. # This URL is used for redirecting after authentication, etc. # Example: http://localhost:5173 (For development mode) -FRONTEND_BASE_URL=your_frontend_base_url_here - +FRONTEND_BASE_URL=http://localhost:5173 # YORKIE_API_ADDR: URL of the Yorkie Server # This URL is used for using collaborative editing CRDT server -# Example: https://api.yorkie.dev (For development mode) -YORKIE_API_ADDR=your_yorkie_api_addr_here +# Example: http://localhost:8080 (For development mode) +YORKIE_API_ADDR=http://localhost:8080 # YORKIE_PROJECT_NAME: Name of the Yorkie project # Create Yorkie project at: https://yorkie.dev -YORKIE_PROJECT_NAME=your_yorkie_project_name_here +# Example: default (For development mode) +YORKIE_PROJECT_NAME=default # YORKIE_PROJECT_SECRET_KEY: Secret key of the Yorkie project # To obtain a project secret key, visit your project dashboard: https://yorkie.dev/dashboard/projects -YORKIE_PROJECT_SECRET_KEY=your_yorkie_project_secret_key_here +# You can leave this empty if you are using the default project +YORKIE_PROJECT_SECRET_KEY="" +# YORKIE_INTELLIGENCE: Whether to enable Yorkie Intelligence for collaborative editing. +# Set to true if Yorkie Intelligence is required. +# If set to false, OPENAI_API_KEY is not required. +YORKIE_INTELLIGENCE=false # OPENAI_API_KEY: API key for using the gpt-3.5-turbo model by Yorkie Intelligence. # To obtain an API key, visit OpenAI: https://help.openai.com/en/articles/4936850-where-do-i-find-my-api-key OPENAI_API_KEY=your_openai_api_key_here @@ -45,12 +47,12 @@ OPENAI_API_KEY=your_openai_api_key_here # LANGCHAIN_TRACING_V2: Whether LangSmith monitoring for YorkieIntelligence is needed. # Set to true if LangSmith monitoring is required. # If set to false, LANGCHAIN_ENDPOINT, LANGCHAIN_API_KEY, and LANGCHAIN_PROJECT are not required. -LANGCHAIN_TRACING_V2=true +LANGCHAIN_TRACING_V2=false # LANGCHAIN_ENDPOINT: LangSmith API URL. # This URL is used to communicate with LangSmith. # Example: https://api.smith.langchain.com # To obtain the LangSmith endpoint, visit LangSmith: https://www.langchain.com/langsmith -LANGCHAIN_ENDPOINT=your_langsmith_api_key_here +LANGCHAIN_ENDPOINT=https://www.langchain.com/langsmith # LANGCHAIN_API_KEY: LangSmith API Key. # This key is required for authenticating with LangSmith. # To obtain an API key, visit LangSmith: https://www.langchain.com/langsmith @@ -60,6 +62,11 @@ LANGCHAIN_API_KEY=your_langsmith_api_key_here # To create a LangSmith project, visit LangSmith: https://www.langchain.com/langsmith LANGCHAIN_PROJECT=your_langsmith_project_name_here + +# FILE_UPLOAD: Whether to enable file upload to storage +# Set to true if file upload is required. +# If set to false, AWS_S3_BUCKET_NAME is not required. +FILE_UPLOAD=false # AWS_S3_BUCKET_NAME: S3 Bucket name # This is the name of the S3 Bucket AWS_S3_BUCKET_NAME="your_s3_bucket_name" \ No newline at end of file diff --git a/backend/README.md b/backend/README.md index ccb70ed5..9db6d20d 100644 --- a/backend/README.md +++ b/backend/README.md @@ -4,25 +4,38 @@ This project is the backend part of the CodePair service developed using NestJS. ## Getting Started -1. Navigate to the project directory. +1. Set Up GitHub OAuth Key + + For the Social Login feature, you need to obtain a GitHub OAuth key before running the project. Please refer to [this document](../docs/1_Set_Up_GitHub_OAuth_Key.md) for guidance. + + After completing this step, you should have the `GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET` values. + +2. Update your `GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET` to `./backend/.env.development`. ```bash - cd backend + vi ./backend/.env.development + + # In the file, update the following values: + # GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET + GITHUB_CLIENT_ID=your_github_client_id_here + GITHUB_CLIENT_SECRET=your_github_client_secret_here ``` -2. Install dependencies. +3. Run `.backend/docker/docker-compose.yml`. ```bash - npm install + docker-compose -f ./backend/docker/docker-compose.yml up -d ``` -3. Start the server in development mode. +4. Run the Backend application: ```bash + cd backend + npm install npm run start:dev ``` -4. Open [http://localhost:3000](http://localhost:3000) in your browser to access the backend. +5. Visit http://localhost:3000 to enjoy your CodePair. ## API Specification @@ -92,4 +105,4 @@ backend/ ## Contributing Please see the [CONTRIBUTING.md](../CONTRIBUTING.md) file for details on how to contribute to this project. -If you are interested in internal design, please refer to the [Design Document](./design/). \ No newline at end of file +If you are interested in internal design, please refer to the [Design Document](./design/). diff --git a/backend/docker/docker-compose-full.yml b/backend/docker/docker-compose-full.yml index 77e6730d..5bec6f56 100644 --- a/backend/docker/docker-compose-full.yml +++ b/backend/docker/docker-compose-full.yml @@ -7,14 +7,22 @@ services: environment: DATABASE_URL: "mongodb://mongo:27017/codepair" # Environment variables need to be passed to the container - GITHUB_CLIENT_ID: "GITHUB_CLIENT_ID" - GITHUB_CLIENT_SECRET: "GITHUB_CLIENT_SECRET" - GITHUB_CALLBACK_URL: "/auth/login/github" - JWT_AUTH_SECRET: "JWT_AUTH_SECRET" - FRONTEND_BASE_URL: "FRONTEND_BASE_URL" - YORKIE_API_ADDR: "http://localhost:8080" - YORKIE_PROJECT_NAME: "admin" # If you want to use the other project, you should change this value - YORKIE_PROJECT_SECRET_KEY: "admin" # If you want to use the other project, you should change this value + GITHUB_CLIENT_ID: "your_github_client_id_here" + GITHUB_CLIENT_SECRET: "your_github_client_secret_here" + GITHUB_CALLBACK_URL: "http://localhost:3000/auth/login/github" + JWT_AUTH_SECRET: "you_should_change_this_secret_key_in_production" + FRONTEND_BASE_URL: "http://localhost:5173" + YORKIE_API_ADDR: "http://yorkie:8080" + YORKIE_PROJECT_NAME: "default" + YORKIE_PROJECT_SECRET_KEY: "" + YORKIE_PROJECT_TOKEN: "" + YORKIE_INTELLIGENCE: false + OPENAI_API_KEY: "your_openai_api_key_here" + LANGCHAIN_ENDPOINT: "https://www.langchain.com/langsmith" + LANGCHAIN_API_KEY: "your_langsmith_api_key_here" + LANGCHAIN_PROJECT: "your_langsmith_project_name_here" + FILE_UPLOAD: false + AWS_S3_BUCKET_NAME: "your_s3_bucket_name" ports: - "3000:3000" depends_on: @@ -22,6 +30,7 @@ services: restart: unless-stopped links: - "mongo:mongo" + - "yorkie:yorkie" yorkie: image: "yorkieteam/yorkie:latest" diff --git a/backend/docker/docker-compose.yml b/backend/docker/docker-compose.yml index a99bc9ac..7b191618 100644 --- a/backend/docker/docker-compose.yml +++ b/backend/docker/docker-compose.yml @@ -1,21 +1,30 @@ version: "3.8" services: - codepair-backend: + yorkie: + image: "yorkieteam/yorkie:latest" + command: ["server", "--enable-pprof"] + restart: always + ports: + - "8080:8080" + - "8081:8081" + + mongo: build: - context: ../ + context: ./mongodb_replica + args: + MONGO_VERSION: 4 environment: - # Environment variables need to be passed to the container - DATABASE_URL: "DATABASE_URL" - GITHUB_CLIENT_ID: "GITHUB_CLIENT_ID" - GITHUB_CLIENT_SECRET: "GITHUB_CLIENT_SECRET" - GITHUB_CALLBACK_URL: "/auth/login/github" - JWT_AUTH_SECRET: "JWT_AUTH_SECRET" - FRONTEND_BASE_URL: "FRONTEND_BASE_URL" - YORKIE_API_ADDR: "YORKIE_API_ADDR" - YORKIE_PROJECT_NAME: "YORKIE_PROJECT_NAME" - YORKIE_PROJECT_SECRET_KEY: "YORKIE_PROJECT_SECRET_KEY" - AWS_S3_BUCKET_NAME: "YOUR_S3_BUCKET_NAME" + MONGO_REPLICA_HOST: 127.0.0.1 + MONGO_REPLICA_PORT: 27017 + MONGO_INITDB_DATABASE: "codepair" + MONGO_COMMAND: "mongo" ports: - - "3000:3000" + - "27017:27017" restart: unless-stopped + healthcheck: + test: + ["CMD", "mongo", "admin", "--port", "27017", "--eval", "db.adminCommand('ping').ok"] + interval: 5s + timeout: 2s + retries: 20 diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 3897794e..688bf78d 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -13,10 +13,15 @@ import { CheckModule } from "./check/check.module"; import { IntelligenceModule } from "./intelligence/intelligence.module"; import { LangchainModule } from "./langchain/langchain.module"; import { FilesModule } from "./files/files.module"; +import { SettingsModule } from "./settings/settings.module"; @Module({ imports: [ - ConfigModule.forRoot({ isGlobal: true }), + ConfigModule.forRoot({ + isGlobal: true, + envFilePath: + process.env.NODE_ENV === "production" ? ".env.production" : ".env.development", + }), UsersModule, AuthModule, WorkspacesModule, @@ -27,6 +32,8 @@ import { FilesModule } from "./files/files.module"; IntelligenceModule, LangchainModule, FilesModule, + ConfigModule, + SettingsModule, ], controllers: [], providers: [ diff --git a/backend/src/settings/settings.controller.spec.ts b/backend/src/settings/settings.controller.spec.ts new file mode 100644 index 00000000..02670cb8 --- /dev/null +++ b/backend/src/settings/settings.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from "@nestjs/testing"; +import { SettingsController } from "./settings.controller"; + +describe("SettingsController", () => { + let controller: SettingsController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [SettingsController], + }).compile(); + + controller = module.get(SettingsController); + }); + + it("should be defined", () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/backend/src/settings/settings.controller.ts b/backend/src/settings/settings.controller.ts new file mode 100644 index 00000000..eb18926b --- /dev/null +++ b/backend/src/settings/settings.controller.ts @@ -0,0 +1,22 @@ +import { Controller, Get } from "@nestjs/common"; +import { SettingsService } from "./settings.service"; +import { ApiFoundResponse, ApiOperation, ApiTags } from "@nestjs/swagger"; +import { GetSettingsResponse } from "./types/get-settings-response.type"; +import { Public } from "src/utils/decorators/auth.decorator"; + +@ApiTags("Settings") +@Controller("settings") +export class SettingsController { + constructor(private settingsService: SettingsService) {} + + @Public() + @Get() + @ApiOperation({ + summary: "Get Settings", + description: "Get Settings of CodePair", + }) + @ApiFoundResponse({ type: GetSettingsResponse }) + async getSettings(): Promise { + return this.settingsService.getSettings(); + } +} diff --git a/backend/src/settings/settings.module.ts b/backend/src/settings/settings.module.ts new file mode 100644 index 00000000..4b8e6847 --- /dev/null +++ b/backend/src/settings/settings.module.ts @@ -0,0 +1,9 @@ +import { Module } from "@nestjs/common"; +import { SettingsController } from "./settings.controller"; +import { SettingsService } from "./settings.service"; + +@Module({ + controllers: [SettingsController], + providers: [SettingsService], +}) +export class SettingsModule {} diff --git a/backend/src/settings/settings.service.spec.ts b/backend/src/settings/settings.service.spec.ts new file mode 100644 index 00000000..a4961c0b --- /dev/null +++ b/backend/src/settings/settings.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from "@nestjs/testing"; +import { SettingsService } from "./settings.service"; + +describe("SettingsService", () => { + let service: SettingsService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [SettingsService], + }).compile(); + + service = module.get(SettingsService); + }); + + it("should be defined", () => { + expect(service).toBeDefined(); + }); +}); diff --git a/backend/src/settings/settings.service.ts b/backend/src/settings/settings.service.ts new file mode 100644 index 00000000..a2532705 --- /dev/null +++ b/backend/src/settings/settings.service.ts @@ -0,0 +1,23 @@ +import { Injectable } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; +import { generateFeatureList } from "src/utils/constants/yorkie-intelligence"; +import { GetSettingsResponse } from "./types/get-settings-response.type"; + +@Injectable() +export class SettingsService { + constructor(private configService: ConfigService) {} + + async getSettings(): Promise { + return { + yorkieIntelligence: { + enable: this.configService.get("YORKIE_INTELLIGENCE") === "true", + config: { + features: generateFeatureList(this.configService), + }, + }, + fileUpload: { + enable: this.configService.get("FILE_UPLOAD") === "true", + }, + }; + } +} diff --git a/backend/src/settings/types/get-settings-response.type.ts b/backend/src/settings/types/get-settings-response.type.ts new file mode 100644 index 00000000..56cc9986 --- /dev/null +++ b/backend/src/settings/types/get-settings-response.type.ts @@ -0,0 +1,28 @@ +import { ApiProperty } from "@nestjs/swagger"; + +class YorkieIntelligenceConfig { + @ApiProperty({ type: Boolean, description: "Enable Yorkie Intelligence" }) + enable: boolean; + + @ApiProperty({ type: Object, description: "Yorkie Intelligence Config" }) + config: { + features: Array<{ + title: string; + icon: string; + feature: string; + }>; + }; +} + +class FileUploadConfig { + @ApiProperty({ type: Boolean, description: "Enable File Upload" }) + enable: boolean; +} + +export class GetSettingsResponse { + @ApiProperty({ type: YorkieIntelligenceConfig, description: "Yorkie Intelligence Config" }) + yorkieIntelligence: YorkieIntelligenceConfig; + + @ApiProperty({ type: FileUploadConfig, description: "File Upload Config" }) + fileUpload: FileUploadConfig; +} diff --git a/backend/src/utils/constants/yorkie-intelligence.ts b/backend/src/utils/constants/yorkie-intelligence.ts new file mode 100644 index 00000000..e7a40999 --- /dev/null +++ b/backend/src/utils/constants/yorkie-intelligence.ts @@ -0,0 +1,25 @@ +import { ConfigService } from "@nestjs/config"; + +export enum IntelligenceFeature { + GITHUB_ISSUE = "github-issue", + GITHUB_PR = "github-pr", +} + +export const generateFeatureList = (configService: ConfigService) => { + const generateIconUrl = (icon: string) => { + return `${configService.get("FRONTEND_BASE_URL")}/yorkie_intelligence/${icon}`; + }; + + return [ + { + title: "Write GitHub Issue", + icon: generateIconUrl("github.svg"), + feature: "github-issue", + }, + { + title: "Write GitHub Pull Request", + icon: generateIconUrl("github.svg"), + feature: "github-pr", + }, + ]; +}; diff --git a/docs/1_Set_Up_GitHub_OAuth_Key.md b/docs/1_Set_Up_GitHub_OAuth_Key.md new file mode 100644 index 00000000..a6abc447 --- /dev/null +++ b/docs/1_Set_Up_GitHub_OAuth_Key.md @@ -0,0 +1,36 @@ +# Set Up GitHub OAuth + +For the Social Login feature, you need to obtain a GitHub OAuth key before running the project. After completing this step, you should have the `GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET` values. + +## 1. Visit GitHub and Sign In + +Visit [GitHub](https://github.com/) and sign in to your account. +You should have a GitHub account to create an OAuth application. + +## 2.Visit Developer Settings + +You can access the Developer Setting by [this link](https://github.com/settings/apps). + +Or, you can access the Developer Settings page by clicking on your profile icon in the top right corner of the page and selecting `Settings`. Then, click on the `Developer settings` tab. + +## 3. Create a New GitHub App + +![Create a New GitHub App](./images/create_new_github_app.png) + +Click on the `New GitHub App` button to create a new OAuth application. + +## 4. Fill Out the Form + +![Fill Out the Form](./images/github_form.png) + +You should fill out the form with the following information (In Development Mode): + +- Authorization callback URL: `http://localhost:3000/auth/login/github` + +Other fields can be filled out according to your needs. + +## 5. Get Your Client ID and Client Secret + +![Get Your Client ID and Client Secret](./images/get_your_key.png) + +After creating the application, you will see your `Client ID` and `Client Secret`. Copy these values and save them in a safe place. Paste the `GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET` values into the `backend/.env.development` or `backend/docker/docker-compose-full.yml` file. diff --git a/docs/images/create_new_github_app.png b/docs/images/create_new_github_app.png new file mode 100644 index 00000000..97537a80 Binary files /dev/null and b/docs/images/create_new_github_app.png differ diff --git a/docs/images/create_new_oauth_app.png b/docs/images/create_new_oauth_app.png new file mode 100644 index 00000000..d0f2f86d Binary files /dev/null and b/docs/images/create_new_oauth_app.png differ diff --git a/docs/images/get_your_key.png b/docs/images/get_your_key.png new file mode 100644 index 00000000..8aaf617f Binary files /dev/null and b/docs/images/get_your_key.png differ diff --git a/docs/images/github_form.png b/docs/images/github_form.png new file mode 100644 index 00000000..8e968437 Binary files /dev/null and b/docs/images/github_form.png differ diff --git a/frontend/.env.development b/frontend/.env.development index be08dbc1..855fc6af 100644 --- a/frontend/.env.development +++ b/frontend/.env.development @@ -1,3 +1,3 @@ VITE_API_ADDR="http://localhost:3000" -VITE_YORKIE_API_ADDR="https://api.yorkie.dev" -VITE_YORKIE_API_KEY="cmftp10ksk14av0kc7gg" +VITE_YORKIE_API_ADDR="http://localhost:8080" +VITE_YORKIE_API_KEY="" diff --git a/frontend/.env.example b/frontend/.env.example deleted file mode 100644 index ccc70e05..00000000 --- a/frontend/.env.example +++ /dev/null @@ -1,15 +0,0 @@ -# This file serves as a template for configuring environment variables. -# Copy this file to `.env` and fill in the necessary values. - -# VITE_API_ADDR: URL of backend application -# This URL is used for backend API -# Example: http://localhost:3000 (For development mode) -VITE_API_ADDR=your_api_addr_here - -# YORKIE_API_ADDR: URL of the Yorkie Server -# This URL is used for using collaborative editing CRDT server -# Example: https://api.yorkie.dev (For development mode) -VITE_YORKIE_API_ADDR=your_yorkie_api_addr_here -# VITE_YORKIE_API_KEY: API key of the Yorkie project -# To obtain a API key, visit your project dashboard: https://yorkie.dev/dashboard/projects -VITE_YORKIE_API_KEY=your_yorkie_api_key_here diff --git a/frontend/README.md b/frontend/README.md index ed562d10..2442ffc0 100755 --- a/frontend/README.md +++ b/frontend/README.md @@ -4,25 +4,38 @@ This project is the frontend part of the CodePair service developed using Vite a ## Getting Started -1. Navigate to the project directory. +1. Set Up GitHub OAuth Key -```bash -cd frontend -``` + For the Social Login feature, you need to obtain a GitHub OAuth key before running the project. Please refer to [this document](../docs/1_Set_Up_GitHub_OAuth_Key.md) for guidance. -2. Install dependencies. + After completing this step, you should have the `GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET` values. -```bash -npm install -``` +2. Update your `GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET` to `./backend/dockerdocker-compose-full.yml`. -3. Start the development server. + ```bash + vi ./backend/docker/docker-compose-full.yml -```bash -npm run dev -``` + # In the file, update the following values: + # GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET + GITHUB_CLIENT_ID: "your_github_client_id_here" + GITHUB_CLIENT_SECRET: "your_github_client_secret_here" + ``` + +3. Run `./backend/dockerdocker-compose-full.yml`. + + ```bash + docker-compose -f ./backend/docker/docker-compose-full.yml up -d + ``` + +4. Run the Frontend application: + + ```bash + cd frontend + npm install + npm run dev + ``` -4. Open [http://localhost:5173](http://localhost:5173) in your browser to view the app. +5. Visit [http://localhost:5173](http://localhost:5173) to enjoy your CodePair. ## Commands diff --git a/frontend/public/yorkie_intelligence/github.svg b/frontend/public/yorkie_intelligence/github.svg new file mode 100644 index 00000000..326755c1 --- /dev/null +++ b/frontend/public/yorkie_intelligence/github.svg @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/frontend/src/components/common/PrivateRoute.tsx b/frontend/src/components/common/PrivateRoute.tsx index ba56620d..cc60b859 100644 --- a/frontend/src/components/common/PrivateRoute.tsx +++ b/frontend/src/components/common/PrivateRoute.tsx @@ -2,6 +2,7 @@ import { ReactNode, useContext } from "react"; import { useLocation, Navigate } from "react-router-dom"; import { AuthContext } from "../../contexts/AuthContext"; import { Backdrop, CircularProgress } from "@mui/material"; +import { useGetSettingsQuery } from "../../hooks/api/settings"; interface PrivateRouteProps { children?: ReactNode; @@ -12,6 +13,8 @@ const PrivateRoute = (props: PrivateRouteProps) => { const { isLoggedIn, isLoading } = useContext(AuthContext); const location = useLocation(); + useGetSettingsQuery(); + if (isLoading) { return ( diff --git a/frontend/src/components/editor/Editor.tsx b/frontend/src/components/editor/Editor.tsx index eb019248..825b0515 100644 --- a/frontend/src/components/editor/Editor.tsx +++ b/frontend/src/components/editor/Editor.tsx @@ -14,12 +14,14 @@ import { imageUploader } from "../../utils/imageUploader"; import { useCreateUploadUrlMutation, useUploadFileMutation } from "../../hooks/api/file"; import { selectWorkspace } from "../../store/workspaceSlice"; import { ScrollSyncPane } from "react-scroll-sync"; +import { selectSetting } from "../../store/settingSlice"; function Editor() { const dispatch = useDispatch(); const themeMode = useCurrentTheme(); const [element, setElement] = useState(); const editorStore = useSelector(selectEditor); + const settingStore = useSelector(selectSetting); const workspaceStore = useSelector(selectWorkspace); const { mutateAsync: createUploadUrl } = useCreateUploadUrlMutation(); const { mutateAsync: uploadFile } = useUploadFileMutation(); @@ -31,7 +33,12 @@ function Editor() { useEffect(() => { let view: EditorView | undefined = undefined; - if (!element || !editorStore.doc || !editorStore.client) { + if ( + !element || + !editorStore.doc || + !editorStore.client || + typeof settingStore.fileUpload?.enable !== "boolean" + ) { return; } @@ -62,7 +69,9 @@ function Editor() { EditorView.lineWrapping, keymap.of([indentWithTab]), intelligencePivot, - imageUploader(handleUploadImage, editorStore.doc), + ...(settingStore.fileUpload.enable + ? [imageUploader(handleUploadImage, editorStore.doc)] + : []), ], }); @@ -85,6 +94,7 @@ function Editor() { workspaceStore.data, createUploadUrl, uploadFile, + settingStore.fileUpload?.enable, ]); return ( diff --git a/frontend/src/components/editor/YorkieIntelligenceFeature.tsx b/frontend/src/components/editor/YorkieIntelligenceFeature.tsx index f45c280d..405cbfa9 100644 --- a/frontend/src/components/editor/YorkieIntelligenceFeature.tsx +++ b/frontend/src/components/editor/YorkieIntelligenceFeature.tsx @@ -10,7 +10,7 @@ import { Typography, useTheme, } from "@mui/material"; -import { INTELLIGENCE_FOOTER_ID, IntelligenceFeature } from "../../constants/intelligence"; +import { INTELLIGENCE_FOOTER_ID } from "../../constants/intelligence"; import ContentCopyIcon from "@mui/icons-material/ContentCopy"; import RefreshIcon from "@mui/icons-material/Refresh"; import { FormContainer, TextFieldElement, useForm } from "react-hook-form-mui"; @@ -27,7 +27,7 @@ import { selectEditor } from "../../store/editorSlice"; interface YorkieIntelligenceFeatureProps { title: string; - feature: IntelligenceFeature; + feature: string; onClose: () => void; } diff --git a/frontend/src/components/editor/YorkieIntelligenceFeatureList.tsx b/frontend/src/components/editor/YorkieIntelligenceFeatureList.tsx index feb9c640..8ac46502 100644 --- a/frontend/src/components/editor/YorkieIntelligenceFeatureList.tsx +++ b/frontend/src/components/editor/YorkieIntelligenceFeatureList.tsx @@ -1,32 +1,30 @@ -import { ListItemIcon, ListItemText, MenuItem, MenuList, Stack, TextField } from "@mui/material"; +import { + Icon, + ListItemIcon, + ListItemText, + MenuItem, + MenuList, + Stack, + TextField, +} from "@mui/material"; import { useMemo, useState } from "react"; -import GitHubIcon from "@mui/icons-material/GitHub"; import { matchSorter } from "match-sorter"; -import { IntelligenceFeature } from "../../constants/intelligence"; - -const featureInfoList = [ - { - title: "Write GitHub Issue", - icon: , - feature: IntelligenceFeature.GITHUB_ISSUE, - }, - { - title: "Write GitHub Pull Request", - icon: , - feature: IntelligenceFeature.GITHUB_PR, - }, -]; +import { selectSetting } from "../../store/settingSlice"; +import { useSelector } from "react-redux"; interface YorkieIntelligenceFeatureListProps { - onSelectFeature: (feature: IntelligenceFeature, title: string) => void; + onSelectFeature: (feature: string, title: string) => void; } function YorkieIntelligenceFeatureList(props: YorkieIntelligenceFeatureListProps) { const { onSelectFeature } = props; + const settingStore = useSelector(selectSetting); const [featureText, setFeatureText] = useState(""); const filteredFeatureInfoList = useMemo(() => { - return matchSorter(featureInfoList, featureText, { keys: ["title", "feature"] }); - }, [featureText]); + return matchSorter(settingStore.yorkieIntelligence?.config.features ?? [], featureText, { + keys: ["title", "feature"], + }); + }, [featureText, settingStore.yorkieIntelligence?.config.features]); const handleFeatureTextChange: React.ChangeEventHandler< HTMLInputElement | HTMLTextAreaElement @@ -51,7 +49,11 @@ function YorkieIntelligenceFeatureList(props: YorkieIntelligenceFeatureListProps key={featureInfo.feature} onClick={() => onSelectFeature(featureInfo.feature, featureInfo.title)} > - {featureInfo.icon} + + + {featureInfo.title} + + {featureInfo.title} ))} diff --git a/frontend/src/components/editor/YorkieIntelligenceFooter.tsx b/frontend/src/components/editor/YorkieIntelligenceFooter.tsx index 3f757c40..33d62e32 100644 --- a/frontend/src/components/editor/YorkieIntelligenceFooter.tsx +++ b/frontend/src/components/editor/YorkieIntelligenceFooter.tsx @@ -1,7 +1,6 @@ import { Box, Card, Popover, useTheme } from "@mui/material"; import YorkieIntelligenceFeatureList from "./YorkieIntelligenceFeatureList"; import { useEffect, useMemo, useRef, useState } from "react"; -import { IntelligenceFeature } from "../../constants/intelligence"; import YorkieIntelligenceFeature from "./YorkieIntelligenceFeature"; import { useSelector } from "react-redux"; import { selectEditor } from "../../store/editorSlice"; @@ -17,7 +16,7 @@ function YorkieIntelligenceFooter(props: YorkieIntelligenceFooterProps) { const editorStore = useSelector(selectEditor); const anchorRef = useRef(null); const [selectedTitle, setSelectedTitle] = useState(null); - const [selectedFeature, setSelectedFeature] = useState(null); + const [selectedFeature, setSelectedFeature] = useState(null); const [anchorEl, setAnchorEl] = useState(); const [closeModalOpen, setCloseModalOpen] = useState(false); const cardRef = useRef(null); @@ -37,7 +36,7 @@ function YorkieIntelligenceFooter(props: YorkieIntelligenceFooterProps) { }; }, []); - const handleSelectFeature = (feature: IntelligenceFeature, title: string) => { + const handleSelectFeature = (feature: string, title: string) => { setSelectedFeature(feature); setSelectedTitle(title); }; diff --git a/frontend/src/constants/intelligence.ts b/frontend/src/constants/intelligence.ts index fc278995..c73c0b97 100644 --- a/frontend/src/constants/intelligence.ts +++ b/frontend/src/constants/intelligence.ts @@ -1,7 +1,2 @@ export const INTELLIGENCE_HEADER_ID = "yorkie-intelligence-header"; export const INTELLIGENCE_FOOTER_ID = "yorkie-intelligence-footer"; - -export enum IntelligenceFeature { - GITHUB_ISSUE = "github-issue", - GITHUB_PR = "github-pr", -} diff --git a/frontend/src/hooks/api/settings.ts b/frontend/src/hooks/api/settings.ts new file mode 100644 index 00000000..884d9bf8 --- /dev/null +++ b/frontend/src/hooks/api/settings.ts @@ -0,0 +1,34 @@ +import { useQuery } from "@tanstack/react-query"; +import axios from "axios"; +import { GetSettingsResponse } from "./types/settings"; +import { useDispatch, useSelector } from "react-redux"; +import { selectSetting, setFileUpload, setYorkieIntelligence } from "../../store/settingSlice"; +import { useEffect } from "react"; + +export const generateGetSettingsQueryKey = () => { + return ["settings"]; +}; + +export const useGetSettingsQuery = () => { + const dispatch = useDispatch(); + const settingStore = useSelector(selectSetting); + const query = useQuery({ + queryKey: generateGetSettingsQueryKey(), + enabled: settingStore.yorkieIntelligence === null && settingStore.fileUpload === null, + queryFn: async () => { + const res = await axios.get("/settings"); + return res.data; + }, + staleTime: 1000 * 60 * 60 * 24, // 24 hours + }); + + useEffect(() => { + if (!query.isSuccess) return; + + const data = query.data; + dispatch(setYorkieIntelligence(data.yorkieIntelligence)); + dispatch(setFileUpload(data.fileUpload)); + }, [dispatch, query.data, query.isSuccess]); + + return query; +}; diff --git a/frontend/src/hooks/api/types/settings.d.ts b/frontend/src/hooks/api/types/settings.d.ts new file mode 100644 index 00000000..8e590d7a --- /dev/null +++ b/frontend/src/hooks/api/types/settings.d.ts @@ -0,0 +1,20 @@ +class YorkieIntelligenceSetting { + enable: boolean; + config: { + features: Array<{ + title: string; + icon: string; + feature: string; + }>; + }; +} + +class FileUploadSetting { + enable: boolean; +} + +export class GetSettingsResponse { + yorkieIntelligence: YorkieIntelligenceSetting; + + fileUpload: FileUploadSetting; +} diff --git a/frontend/src/pages/workspace/document/Index.tsx b/frontend/src/pages/workspace/document/Index.tsx index afc2de2d..6ee3ca29 100644 --- a/frontend/src/pages/workspace/document/Index.tsx +++ b/frontend/src/pages/workspace/document/Index.tsx @@ -9,11 +9,13 @@ import { useGetWorkspaceQuery } from "../../../hooks/api/workspace"; import DocumentView from "../../../components/editor/DocumentView"; import { useYorkieDocument } from "../../../hooks/useYorkieDocument"; import YorkieIntelligence from "../../../components/editor/YorkieIntelligence"; +import { selectSetting } from "../../../store/settingSlice"; function DocumentIndex() { const dispatch = useDispatch(); const params = useParams(); const userStore = useSelector(selectUser); + const settingStore = useSelector(selectSetting); const { data: workspace } = useGetWorkspaceQuery(params.workspaceSlug); const { data: document } = useGetDocumentQuery(workspace?.id, params.documentId); const { doc, client } = useYorkieDocument(document?.yorkieDocumentId, userStore.data?.nickname); @@ -33,7 +35,7 @@ function DocumentIndex() { return ( - + {settingStore.yorkieIntelligence?.enable && } ); } diff --git a/frontend/src/store/settingSlice.ts b/frontend/src/store/settingSlice.ts new file mode 100644 index 00000000..d4754766 --- /dev/null +++ b/frontend/src/store/settingSlice.ts @@ -0,0 +1,47 @@ +import { createSlice } from "@reduxjs/toolkit"; +import type { PayloadAction } from "@reduxjs/toolkit"; +import { RootState } from "./store"; + +interface YorkieIntelligenceSetting { + enable: boolean; + config: { + features: Array<{ + title: string; + icon: string; + feature: string; + }>; + }; +} + +interface FileUploadSetting { + enable: boolean; +} + +export interface SettingState { + yorkieIntelligence: YorkieIntelligenceSetting | null; + fileUpload: FileUploadSetting | null; +} + +const initialState: SettingState = { + yorkieIntelligence: null, + fileUpload: null, +}; + +export const settingSlice = createSlice({ + name: "setting", + initialState, + reducers: { + setYorkieIntelligence: (state, action: PayloadAction) => { + state.yorkieIntelligence = action.payload; + }, + setFileUpload: (state, action: PayloadAction) => { + state.fileUpload = action.payload; + }, + }, +}); + +export const { setYorkieIntelligence, setFileUpload } = settingSlice.actions; + +export const selectSetting = (state: RootState) => state.setting; + +export default settingSlice.reducer; diff --git a/frontend/src/store/store.ts b/frontend/src/store/store.ts index 79710107..81fb07f7 100644 --- a/frontend/src/store/store.ts +++ b/frontend/src/store/store.ts @@ -7,6 +7,7 @@ import authSlice from "./authSlice"; import userSlice from "./userSlice"; import workspaceSlice from "./workspaceSlice"; import documentSlice from "./documentSlice"; +import settingSlice from "./settingSlice"; const reducers = combineReducers({ // Persistence @@ -17,6 +18,7 @@ const reducers = combineReducers({ editor: editorSlice, workspace: workspaceSlice, document: documentSlice, + setting: settingSlice, }); const persistConfig = {