From f010925381f71ef9c2b3ecec4fe81d7a275cfdc5 Mon Sep 17 00:00:00 2001 From: anunayajoshi Date: Sat, 2 Nov 2024 06:05:09 +0800 Subject: [PATCH 01/11] done --- .env.local | 3 + backend/collaboration/.env.compose | 1 + backend/collaboration/.env.docker | 1 + backend/collaboration/.env.local | 3 +- backend/collaboration/package.json | 1 + .../src/controller/openai-controller.ts | 27 +++ backend/collaboration/src/routes/chat.ts | 9 + backend/collaboration/src/server.ts | 3 + .../src/service/post/openai-service.ts | 37 ++++ .../components/blocks/interview/ai-chat.tsx | 70 +++++++ frontend/src/services/api-clients.ts | 5 + frontend/src/services/collab-service.ts | 28 +++ package-lock.json | 184 ++++++++++++++++++ 13 files changed, 371 insertions(+), 1 deletion(-) create mode 100644 backend/collaboration/src/controller/openai-controller.ts create mode 100644 backend/collaboration/src/routes/chat.ts create mode 100644 backend/collaboration/src/service/post/openai-service.ts create mode 100644 frontend/src/components/blocks/interview/ai-chat.tsx create mode 100644 frontend/src/services/collab-service.ts diff --git a/.env.local b/.env.local index 8212b8ab9b..1be1fad366 100644 --- a/.env.local +++ b/.env.local @@ -12,6 +12,8 @@ COLLAB_SERVICE_NAME=collab-express COLLAB_EXPRESS_PORT=9003 COLLAB_EXPRESS_DB_PORT=5434 COLLAB_PGDATA="/data/collab-db" +OPENAI_API_KEY=PUT_YOUR_OPENAI_API_KEY_HERE + MATCHING_SERVICE_NAME=match-express MATCHING_EXPRESS_PORT=9004 @@ -22,3 +24,4 @@ MATCHING_DB_HOST_MGMT_PORT=3001 FRONTEND_SERVICE_NAME=frontend FRONTEND_PORT=3000 + diff --git a/backend/collaboration/.env.compose b/backend/collaboration/.env.compose index 64db32e1ae..e25ddc5543 100644 --- a/backend/collaboration/.env.compose +++ b/backend/collaboration/.env.compose @@ -8,3 +8,4 @@ POSTGRES_DB="collab" POSTGRES_USER="peerprep-collab-express" POSTGRES_PASSWORD="6rYE0nIzI2ThzDO" PGDATA="/data/collab-db" +OPENAI_API_KEY="PUT_YOUR_OPENAI_API_KEY_HERE" diff --git a/backend/collaboration/.env.docker b/backend/collaboration/.env.docker index 5b27fda54b..5e612bab1f 100644 --- a/backend/collaboration/.env.docker +++ b/backend/collaboration/.env.docker @@ -7,3 +7,4 @@ POSTGRES_DB=collab POSTGRES_USER=peerprep-collab-express POSTGRES_PASSWORD=6rYE0nIzI2ThzDO PGDATA=/data/collab-db +OPENAI_API_KEY=OPENAI_KEY_HERE diff --git a/backend/collaboration/.env.local b/backend/collaboration/.env.local index 129d476a0e..4f298e8cae 100644 --- a/backend/collaboration/.env.local +++ b/backend/collaboration/.env.local @@ -6,4 +6,5 @@ EXPRESS_DB_PORT=5434 POSTGRES_DB="collab" POSTGRES_USER="peerprep-collab-express" POSTGRES_PASSWORD="6rYE0nIzI2ThzDO" -PGDATA="/data/collab-db" \ No newline at end of file +PGDATA="/data/collab-db" +OPENAI_API_KEY=PUT-YOUR-KEYS-HERE diff --git a/backend/collaboration/package.json b/backend/collaboration/package.json index 5a9d2df43c..ca3095e5d8 100644 --- a/backend/collaboration/package.json +++ b/backend/collaboration/package.json @@ -21,6 +21,7 @@ "env-cmd": "^10.1.0", "express": "^4.21.1", "http-status-codes": "^2.3.0", + "openai": "^4.70.2", "pg": "^8.13.0", "pino": "^9.4.0", "pino-http": "^10.3.0", diff --git a/backend/collaboration/src/controller/openai-controller.ts b/backend/collaboration/src/controller/openai-controller.ts new file mode 100644 index 0000000000..e55f0ca3c3 --- /dev/null +++ b/backend/collaboration/src/controller/openai-controller.ts @@ -0,0 +1,27 @@ +import type { Request, Response } from 'express'; +import { StatusCodes } from 'http-status-codes'; + +import { getOpenAIResponse } from '@/service/post/openai-service'; + +export async function queryOpenAI(req: Request, res: Response) { + const { messages } = req.body; + + // Ensure 'messages' array is provided + if (!messages || !Array.isArray(messages)) { + return res.status(StatusCodes.BAD_REQUEST).json({ + error: 'Invalid request: messages array is required.', + }); + } + + try { + const result = await getOpenAIResponse(messages); + + return res.status(StatusCodes.OK).json(result); + } catch (err) { + return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + success: false, + message: 'An error occurred while querying OpenAI', + error: err, + }); + } +} diff --git a/backend/collaboration/src/routes/chat.ts b/backend/collaboration/src/routes/chat.ts new file mode 100644 index 0000000000..083735f5f0 --- /dev/null +++ b/backend/collaboration/src/routes/chat.ts @@ -0,0 +1,9 @@ +import express from 'express'; + +import { queryOpenAI } from '@/controller/openai-controller'; + +const router = express.Router(); + +router.get('/chat', queryOpenAI); + +export default router; diff --git a/backend/collaboration/src/server.ts b/backend/collaboration/src/server.ts index 3c12763d00..dc219167c1 100644 --- a/backend/collaboration/src/server.ts +++ b/backend/collaboration/src/server.ts @@ -9,6 +9,7 @@ import pino from 'pino-http'; import { UI_HOST } from '@/config'; import { config, db } from '@/lib/db'; import { logger } from '@/lib/utils'; +import aiChatRoutes from '@/routes/chat'; import roomRoutes from '@/routes/room'; import { setUpWSServer } from './ws'; @@ -55,6 +56,8 @@ export const dbHealthCheck = async () => { } }; +app.post('/ai', aiChatRoutes); + // Ensure DB service is up before running. app.get('/test-db', async (_req, res) => { await dbHealthCheck(); diff --git a/backend/collaboration/src/service/post/openai-service.ts b/backend/collaboration/src/service/post/openai-service.ts new file mode 100644 index 0000000000..c06138c455 --- /dev/null +++ b/backend/collaboration/src/service/post/openai-service.ts @@ -0,0 +1,37 @@ +import OpenAI from 'openai'; + +const openai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY, +}); + +interface OpenAIMessage { + role: 'system' | 'user' | 'assistant'; + content: string; +} + +export async function getOpenAIResponse(messages: OpenAIMessage[]) { + try { + const response = await openai.chat.completions.create({ + model: 'gpt-3.5-turbo', // or the desired model + messages: [ + { + role: 'system', + content: + 'You are a helpful coding assistant. You are helping a user with a coding problem. Provide tips to the user on solving the problem but do not provide the solution directly.', + }, + ...messages, + ], + }); + + if (response.choices && response.choices[0].message) { + return { + success: true, + data: response.choices[0].message.content, + }; + } else { + throw new Error('No valid response from OpenAI'); + } + } catch (error) { + throw new Error((error as Error)?.message || 'Failed to query OpenAI'); + } +} diff --git a/frontend/src/components/blocks/interview/ai-chat.tsx b/frontend/src/components/blocks/interview/ai-chat.tsx new file mode 100644 index 0000000000..6ef0402ad8 --- /dev/null +++ b/frontend/src/components/blocks/interview/ai-chat.tsx @@ -0,0 +1,70 @@ +import React, { useState } from 'react'; + +import { sendChatMessage } from '@/services/collab-service'; + +import { ChatLayout } from './chat/chat-layout'; +import { ChatMessageType } from './chat/chat-message'; + +// Types for OpenAI API +// interface OpenAIMessage { +// role: 'user' | 'assistant'; +// content: string; +// } + +interface AIChatProps { + isOpen: boolean; + onClose: () => void; +} + +// const API_URL = 'https://api.openai.com/v1/chat/completions'; +// const API_KEY = process.env.OPENAI_API_KEY; + +export const AIChat: React.FC = ({ isOpen, onClose }) => { + const [messages, setMessages] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const handleSend = async (userMessage: string): Promise => { + if (!userMessage.trim() || isLoading) return; + + const updatedMessages = [ + ...messages, + { text: userMessage, isUser: true, timestamp: new Date() }, + ]; + + setMessages(updatedMessages); + setIsLoading(true); + setError(null); + + const inputMessages = updatedMessages.map((message) => ({ + role: message.isUser ? 'user' : 'assistant', + content: message.text, + })); + + try { + const response = await sendChatMessage(inputMessages); + setMessages((prev) => [ + ...prev, + { text: response?.message, isUser: false, timestamp: new Date() }, + ]); + } catch (err) { + setError( + err instanceof Error ? err.message : 'An error occurred while fetching the response' + ); + } finally { + setIsLoading(false); + } + }; + + return ( + + ); +}; diff --git a/frontend/src/services/api-clients.ts b/frontend/src/services/api-clients.ts index 6e33a419d3..e53afe2ac9 100644 --- a/frontend/src/services/api-clients.ts +++ b/frontend/src/services/api-clients.ts @@ -35,4 +35,9 @@ export const matchApiClient = axios.create({ ...basePostHeaders, }); +export const collabApiClient = axios.create({ + ...getApiClientBaseConfig(COLLAB_SERVICE), + ...basePostHeaders, +}); + // define more api clients for other microservices diff --git a/frontend/src/services/collab-service.ts b/frontend/src/services/collab-service.ts new file mode 100644 index 0000000000..96e5ec712f --- /dev/null +++ b/frontend/src/services/collab-service.ts @@ -0,0 +1,28 @@ +import { collabApiClient } from './api-clients'; + +const AI_SERVICE_ROUTES = { + CHAT: '/ai/chat', +}; + +interface IChatResponse { + success: boolean; + message: string; + response?: string; +} + +export const sendChatMessage = ( + messages: Array<{ role: string; content: string }> +): Promise => { + return collabApiClient + .post(AI_SERVICE_ROUTES.CHAT, { messages }) + .then((res) => { + return res.data as IChatResponse; + }) + .catch((err) => { + console.error(err); + return { + success: false, + message: 'An error occurred while processing your request.', + } as IChatResponse; + }); +}; diff --git a/package-lock.json b/package-lock.json index b832a278a7..cfefe3839a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,6 +39,7 @@ "env-cmd": "^10.1.0", "express": "^4.21.1", "http-status-codes": "^2.3.0", + "openai": "^4.70.2", "pg": "^8.13.0", "pino": "^9.4.0", "pino-http": "^10.3.0", @@ -2803,6 +2804,89 @@ "undici-types": "~6.19.2" } }, + "node_modules/@types/node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/pg": { + "version": "8.11.10", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.10.tgz", + "integrity": "sha512-LczQUW4dbOQzsH2RQ5qoeJ6qJPdrcM/DcMLoqWQkMLMsq83J5lAX3LXjdkWdpscFy67JSOWDnh7Ny/sPFykmkg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^4.0.1" + } + }, + "node_modules/@types/pg/node_modules/pg-types": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.2.tgz", + "integrity": "sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "pg-numeric": "1.0.2", + "postgres-array": "~3.0.1", + "postgres-bytea": "~3.0.0", + "postgres-date": "~2.1.0", + "postgres-interval": "^3.0.0", + "postgres-range": "^1.1.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@types/pg/node_modules/postgres-array": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.2.tgz", + "integrity": "sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/pg/node_modules/postgres-bytea": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz", + "integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "obuf": "~1.1.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@types/pg/node_modules/postgres-date": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.1.0.tgz", + "integrity": "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/pg/node_modules/postgres-interval": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz", + "integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/@types/prop-types": { "version": "15.7.13", "license": "MIT" @@ -3780,6 +3864,17 @@ "node": ">= 6.0.0" } }, + "node_modules/agentkeepalive": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "dev": true, @@ -6700,6 +6795,23 @@ "node": ">= 6" } }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==" + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, "node_modules/forwarded": { "version": "0.2.0", "license": "MIT", @@ -7295,6 +7407,14 @@ "node": ">=16.17.0" } }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dependencies": { + "ms": "^2.0.0" + } + }, "node_modules/husky": { "version": "9.1.6", "dev": true, @@ -9559,6 +9679,24 @@ "version": "5.1.0", "license": "MIT" }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "license": "MIT", @@ -9847,6 +9985,44 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openai": { + "version": "4.70.2", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.70.2.tgz", + "integrity": "sha512-Q2ymi/KPUYv+LJ9rFxeYxpkVAhcrZFTVvnJbdF1pUHg9eMC6lY8PU4TO1XOK5UZzOZuuVicouRwVMi1iDrT4qw==", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + }, + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/openai/node_modules/@types/node": { + "version": "18.19.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.63.tgz", + "integrity": "sha512-hcUB7THvrGmaEcPcvUZCZtQ2Z3C+UR/aOcraBLCvTsFMh916Gc1kCCYcfcMuB76HM2pSerxl1PoP3KnmHzd9Lw==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/openai/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, "node_modules/optionator": { "version": "0.9.4", "dev": true, @@ -12998,6 +13174,14 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "engines": { + "node": ">= 14" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "license": "BSD-2-Clause" From e46997dd9aabb1ccbc997572de87c374410ae4af Mon Sep 17 00:00:00 2001 From: SeeuSim Date: Sat, 2 Nov 2024 16:02:59 +0800 Subject: [PATCH 02/11] PEER-226: Update deps Signed-off-by: SeeuSim --- package-lock.json | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index cfefe3839a..9398b00fb9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2817,8 +2817,9 @@ "version": "8.11.10", "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.10.tgz", "integrity": "sha512-LczQUW4dbOQzsH2RQ5qoeJ6qJPdrcM/DcMLoqWQkMLMsq83J5lAX3LXjdkWdpscFy67JSOWDnh7Ny/sPFykmkg==", - "devOptional": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@types/node": "*", "pg-protocol": "*", @@ -2829,8 +2830,9 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.2.tgz", "integrity": "sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==", - "devOptional": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "pg-int8": "1.0.1", "pg-numeric": "1.0.2", @@ -2848,8 +2850,9 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.2.tgz", "integrity": "sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==", - "devOptional": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=12" } @@ -2858,8 +2861,9 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz", "integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==", - "devOptional": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "obuf": "~1.1.2" }, @@ -2871,8 +2875,9 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.1.0.tgz", "integrity": "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==", - "devOptional": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=12" } @@ -2881,8 +2886,9 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz", "integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==", - "devOptional": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=12" } From 6e99813ad62aea60056576db6e316a1808ecfdc3 Mon Sep 17 00:00:00 2001 From: SeeuSim Date: Sat, 2 Nov 2024 17:05:51 +0800 Subject: [PATCH 03/11] PEER-226: Fix bug Signed-off-by: SeeuSim --- .env.local | 8 ++------ backend/collaboration/.env.compose | 2 +- backend/collaboration/.env.docker | 2 +- backend/collaboration/.env.local | 2 +- backend/collaboration/src/routes/chat.ts | 2 +- backend/collaboration/src/server.ts | 3 +-- backend/collaboration/src/service/post/openai-service.ts | 7 +++++-- frontend/.env.docker | 1 - 8 files changed, 12 insertions(+), 15 deletions(-) diff --git a/.env.local b/.env.local index 9f8ab787ac..ae19f6a799 100644 --- a/.env.local +++ b/.env.local @@ -12,8 +12,7 @@ COLLAB_SERVICE_NAME=collab-express COLLAB_EXPRESS_PORT=9003 COLLAB_EXPRESS_DB_PORT=5434 COLLAB_PGDATA="/data/collab-db" -OPENAI_API_KEY=PUT_YOUR_OPENAI_API_KEY_HERE - +OPENAI_API_KEY="" MATCHING_SERVICE_NAME=match-express MATCHING_EXPRESS_PORT=9004 @@ -28,7 +27,4 @@ CHAT_EXPRESS_DB_PORT=5435 CHAT_PGDATA="/data/chat-db" FRONTEND_SERVICE_NAME=frontend -FRONTEND_PORT=3000 - -OPENAI_API_KEY=PUT_YOUR_OPENAI_API_KEY_HERE - +FRONTEND_PORT=3000 \ No newline at end of file diff --git a/backend/collaboration/.env.compose b/backend/collaboration/.env.compose index e25ddc5543..9a964b0f89 100644 --- a/backend/collaboration/.env.compose +++ b/backend/collaboration/.env.compose @@ -8,4 +8,4 @@ POSTGRES_DB="collab" POSTGRES_USER="peerprep-collab-express" POSTGRES_PASSWORD="6rYE0nIzI2ThzDO" PGDATA="/data/collab-db" -OPENAI_API_KEY="PUT_YOUR_OPENAI_API_KEY_HERE" +OPENAI_API_KEY="" diff --git a/backend/collaboration/.env.docker b/backend/collaboration/.env.docker index 5e612bab1f..ffd174e9f7 100644 --- a/backend/collaboration/.env.docker +++ b/backend/collaboration/.env.docker @@ -7,4 +7,4 @@ POSTGRES_DB=collab POSTGRES_USER=peerprep-collab-express POSTGRES_PASSWORD=6rYE0nIzI2ThzDO PGDATA=/data/collab-db -OPENAI_API_KEY=OPENAI_KEY_HERE +OPENAI_API_KEY="" diff --git a/backend/collaboration/.env.local b/backend/collaboration/.env.local index 4f298e8cae..ff5721ae3f 100644 --- a/backend/collaboration/.env.local +++ b/backend/collaboration/.env.local @@ -7,4 +7,4 @@ POSTGRES_DB="collab" POSTGRES_USER="peerprep-collab-express" POSTGRES_PASSWORD="6rYE0nIzI2ThzDO" PGDATA="/data/collab-db" -OPENAI_API_KEY=PUT-YOUR-KEYS-HERE +OPENAI_API_KEY="" diff --git a/backend/collaboration/src/routes/chat.ts b/backend/collaboration/src/routes/chat.ts index 083735f5f0..f929148d95 100644 --- a/backend/collaboration/src/routes/chat.ts +++ b/backend/collaboration/src/routes/chat.ts @@ -4,6 +4,6 @@ import { queryOpenAI } from '@/controller/openai-controller'; const router = express.Router(); -router.get('/chat', queryOpenAI); +router.post('/chat', queryOpenAI); export default router; diff --git a/backend/collaboration/src/server.ts b/backend/collaboration/src/server.ts index dc219167c1..294150d7ea 100644 --- a/backend/collaboration/src/server.ts +++ b/backend/collaboration/src/server.ts @@ -39,6 +39,7 @@ app.use( }) ); +app.use('/ai', aiChatRoutes); app.use('/room', roomRoutes); // Health Check for Docker @@ -56,8 +57,6 @@ export const dbHealthCheck = async () => { } }; -app.post('/ai', aiChatRoutes); - // Ensure DB service is up before running. app.get('/test-db', async (_req, res) => { await dbHealthCheck(); diff --git a/backend/collaboration/src/service/post/openai-service.ts b/backend/collaboration/src/service/post/openai-service.ts index c06138c455..5e38f6fbfa 100644 --- a/backend/collaboration/src/service/post/openai-service.ts +++ b/backend/collaboration/src/service/post/openai-service.ts @@ -17,7 +17,10 @@ export async function getOpenAIResponse(messages: OpenAIMessage[]) { { role: 'system', content: - 'You are a helpful coding assistant. You are helping a user with a coding problem. Provide tips to the user on solving the problem but do not provide the solution directly.', + `You are a helpful coding assistant. ` + + `You are helping a user with a coding problem. ` + + `Provide tips to the user on solving the problem ` + + `but do NOT provide the solution directly.`, }, ...messages, ], @@ -26,7 +29,7 @@ export async function getOpenAIResponse(messages: OpenAIMessage[]) { if (response.choices && response.choices[0].message) { return { success: true, - data: response.choices[0].message.content, + message: response.choices[0].message.content, }; } else { throw new Error('No valid response from OpenAI'); diff --git a/frontend/.env.docker b/frontend/.env.docker index a2388559c5..37b66d141b 100644 --- a/frontend/.env.docker +++ b/frontend/.env.docker @@ -6,4 +6,3 @@ VITE_COLLAB_SERVICE=http://host.docker.internal:9003 VITE_MATCHING_SERVICE=http://host.docker.internal:9004 VITE_CHAT_SERVICE=http://host.docker.internal:9005 FRONTEND_PORT=3000 -OPENAI_API_KEY=PUT_YOUR_OPENAI_API_KEY_HERE \ No newline at end of file From 18457c76d5156851dbe1532d0feabd2f03bfacf1 Mon Sep 17 00:00:00 2001 From: SeeuSim Date: Sat, 2 Nov 2024 17:22:03 +0800 Subject: [PATCH 04/11] PEER-226: Ban committing key Signed-off-by: SeeuSim --- .husky/pre-commit | 2 ++ frontend/.env.local | 1 - scripts/inject-openai-key.sh | 15 +++++++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100755 scripts/inject-openai-key.sh diff --git a/.husky/pre-commit b/.husky/pre-commit index 6b636e2c0c..a466e22d9f 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,2 +1,4 @@ npx lint-staged + +"$(pwd)/scripts/inject-openai-key.sh" diff --git a/frontend/.env.local b/frontend/.env.local index 4e79901707..d2ffc87951 100644 --- a/frontend/.env.local +++ b/frontend/.env.local @@ -5,4 +5,3 @@ VITE_QUESTION_SERVICE=http://localhost:9002 VITE_COLLAB_SERVICE=http://localhost:9003 VITE_MATCHING_SERVICE=http://localhost:9004 VITE_CHAT_SERVICE=http://localhost:9005 -OPENAI_API_KEY=PUT_YOUR_OPENAI_API_KEY_HERE diff --git a/scripts/inject-openai-key.sh b/scripts/inject-openai-key.sh new file mode 100755 index 0000000000..b7dc99b390 --- /dev/null +++ b/scripts/inject-openai-key.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +NEW_KEY="$1" +if [ $# -ne 1 ]; then + NEW_KEY="" +fi + +# Check if we're on macOS +if [[ "$OSTYPE" == "darwin"* ]]; then + # macOS version (using empty '' after -i) + find . -type f -name ".env*" -exec sed -i '' "s/OPENAI_API_KEY=.*/OPENAI_API_KEY=\"${NEW_KEY}\"/g" {} + +else + # Linux version + find . -type f -name ".env*" -exec sed -i "s/OPENAI_API_KEY=.*/OPENAI_API_KEY=\"${NEW_KEY}\"/g" {} + +fi \ No newline at end of file From 51756e256b0563e29c8f4e74a94c74295cbaa2ca Mon Sep 17 00:00:00 2001 From: anunayajoshi Date: Sat, 2 Nov 2024 23:02:50 +0800 Subject: [PATCH 05/11] support streaming and question details and language and code editor info --- .../src/controller/openai-controller.ts | 99 +++++++++++++++++-- backend/collaboration/src/routes/chat.ts | 1 + .../src/service/post/openai-service.ts | 69 ++++++++++--- .../components/blocks/interview/ai-chat.tsx | 94 +++++++++++++++--- .../blocks/interview/chat/chat-message.tsx | 1 + .../components/blocks/interview/editor.tsx | 27 +++-- frontend/src/routes/interview/[room]/main.tsx | 17 +++- frontend/src/services/collab-service.ts | 75 +++++++++++--- 8 files changed, 320 insertions(+), 63 deletions(-) diff --git a/backend/collaboration/src/controller/openai-controller.ts b/backend/collaboration/src/controller/openai-controller.ts index e55f0ca3c3..a2eb4d13e5 100644 --- a/backend/collaboration/src/controller/openai-controller.ts +++ b/backend/collaboration/src/controller/openai-controller.ts @@ -1,12 +1,33 @@ import type { Request, Response } from 'express'; import { StatusCodes } from 'http-status-codes'; +import OpenAI from 'openai'; -import { getOpenAIResponse } from '@/service/post/openai-service'; +const openai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY, +}); + +interface OpenAIMessage { + role: 'system' | 'user' | 'assistant'; + content: string; +} + +const createSystemMessage = ( + editorCode?: string, + language?: string, + questionDetails?: any +): OpenAIMessage => ({ + role: 'system', + content: `You are a helpful coding assistant. +You are helping a user with a coding problem. +${questionDetails ? `\nQuestion Context:\n${JSON.stringify(questionDetails, null, 2)}` : ''} +${editorCode ? `\nCurrent Code (${language || 'unknown'}):\n${editorCode}` : ''} +Provide detailed help while referring to their specific code and question context when available.`, +}); export async function queryOpenAI(req: Request, res: Response) { - const { messages } = req.body; + const { messages, editorCode, language, questionDetails } = req.body; + const isStreaming = req.headers['accept'] === 'text/event-stream'; - // Ensure 'messages' array is provided if (!messages || !Array.isArray(messages)) { return res.status(StatusCodes.BAD_REQUEST).json({ error: 'Invalid request: messages array is required.', @@ -14,14 +35,72 @@ export async function queryOpenAI(req: Request, res: Response) { } try { - const result = await getOpenAIResponse(messages); + const systemMessage = createSystemMessage(editorCode, language, questionDetails); + const allMessages = [systemMessage, ...messages]; + + if (isStreaming) { + // Set up streaming response headers + res.setHeader('Content-Type', 'text/event-stream'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Connection', 'keep-alive'); + + // Create streaming completion + const stream = await openai.chat.completions.create({ + model: 'gpt-3.5-turbo', + messages: allMessages, + stream: true, + }); + + // Handle streaming response + for await (const chunk of stream) { + const content = chunk.choices[0]?.delta?.content || ''; + + if (content) { + // Send the chunk in SSE format + res.write(`data: ${content}\n\n`); + } + } - return res.status(StatusCodes.OK).json(result); + // End the response + res.end(); + } else { + // Non-streaming response + const completion = await openai.chat.completions.create({ + model: 'gpt-3.5-turbo', + messages: allMessages, + }); + + const responseMessage = completion.choices[0]?.message?.content; + + if (!responseMessage) { + throw new Error('No valid response from OpenAI'); + } + + return res.status(StatusCodes.OK).json({ + success: true, + message: responseMessage, + }); + } } catch (err) { - return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ - success: false, - message: 'An error occurred while querying OpenAI', - error: err, - }); + console.error('OpenAI API Error:', err); + + // If headers haven't been sent yet, send error response + if (!res.headersSent) { + return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + success: false, + message: 'An error occurred while querying OpenAI', + error: err instanceof Error ? err.message : 'Unknown error', + }); + } else { + // If we were streaming, end the response + res.end(); + } } + + // Handle client disconnection + req.on('close', () => { + if (isStreaming && !res.writableEnded) { + res.end(); + } + }); } diff --git a/backend/collaboration/src/routes/chat.ts b/backend/collaboration/src/routes/chat.ts index f929148d95..1525da03e1 100644 --- a/backend/collaboration/src/routes/chat.ts +++ b/backend/collaboration/src/routes/chat.ts @@ -4,6 +4,7 @@ import { queryOpenAI } from '@/controller/openai-controller'; const router = express.Router(); +router.post('/chat/stream', queryOpenAI); router.post('/chat', queryOpenAI); export default router; diff --git a/backend/collaboration/src/service/post/openai-service.ts b/backend/collaboration/src/service/post/openai-service.ts index 5e38f6fbfa..67c1eddeab 100644 --- a/backend/collaboration/src/service/post/openai-service.ts +++ b/backend/collaboration/src/service/post/openai-service.ts @@ -1,3 +1,5 @@ +import { EventEmitter } from 'events'; + import OpenAI from 'openai'; const openai = new OpenAI({ @@ -9,21 +11,33 @@ interface OpenAIMessage { content: string; } -export async function getOpenAIResponse(messages: OpenAIMessage[]) { +interface OpenAIRequest { + messages: OpenAIMessage[]; + editorCode?: string; + language?: string; + questionDetails?: string; +} + +// Helper to create system message with context +const createSystemMessage = (editorCode?: string, language?: string, questionDetails?: string) => { + return { + role: 'system' as const, + content: `You are a helpful coding assistant. +You are helping a user with a coding problem. +${questionDetails ? `\nQuestion Context:\n${JSON.stringify(questionDetails, null, 2)}` : ''} +${editorCode ? `\nCurrent Code (${language || 'unknown'}):\n${editorCode}` : ''} +Provide detailed help while referring to their specific code and question context when available.`, + }; +}; + +// Regular response function +export async function getOpenAIResponse(request: OpenAIRequest) { + const { messages, editorCode, language, questionDetails } = request; + try { const response = await openai.chat.completions.create({ - model: 'gpt-3.5-turbo', // or the desired model - messages: [ - { - role: 'system', - content: - `You are a helpful coding assistant. ` + - `You are helping a user with a coding problem. ` + - `Provide tips to the user on solving the problem ` + - `but do NOT provide the solution directly.`, - }, - ...messages, - ], + model: 'gpt-3.5-turbo', + messages: [createSystemMessage(editorCode, language, questionDetails), ...messages], }); if (response.choices && response.choices[0].message) { @@ -38,3 +52,32 @@ export async function getOpenAIResponse(messages: OpenAIMessage[]) { throw new Error((error as Error)?.message || 'Failed to query OpenAI'); } } + +// Streaming response function +export async function getOpenAIStreamResponse(request: OpenAIRequest): Promise { + const { messages, editorCode, language, questionDetails } = request; + const stream = new EventEmitter(); + + try { + const response = await openai.chat.completions.create({ + model: 'gpt-3.5-turbo', + messages: [createSystemMessage(editorCode, language, questionDetails), ...messages], + stream: true, + }); + + // Process the streaming response + for await (const chunk of response) { + const content = chunk.choices[0]?.delta?.content || ''; + + if (content) { + stream.emit('data', content); + } + } + + stream.emit('end'); + } catch (error) { + stream.emit('error', error); + } + + return stream; +} diff --git a/frontend/src/components/blocks/interview/ai-chat.tsx b/frontend/src/components/blocks/interview/ai-chat.tsx index 443385c9fe..dd6d6e5e06 100644 --- a/frontend/src/components/blocks/interview/ai-chat.tsx +++ b/frontend/src/components/blocks/interview/ai-chat.tsx @@ -1,50 +1,112 @@ -import React, { useState } from 'react'; +import { type LanguageName } from '@uiw/codemirror-extensions-langs'; +import React, { useRef,useState } from 'react'; import { sendChatMessage } from '@/services/collab-service'; import { ChatLayout } from './chat/chat-layout'; import { ChatMessageType } from './chat/chat-message'; -// Types for OpenAI API -// interface OpenAIMessage { -// role: 'user' | 'assistant'; -// content: string; -// } - interface AIChatProps { isOpen: boolean; onClose: () => void; + editorCode?: string; + language?: LanguageName; + questionDetails?: string; } -export const AIChat: React.FC = ({ isOpen, onClose }) => { +export const AIChat: React.FC = ({ + isOpen, + onClose, + editorCode = '', + language = 'typescript', + questionDetails = '', +}) => { const [messages, setMessages] = useState([]); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); + const streamingTextRef = useRef(''); const handleSend = async (userMessage: string): Promise => { if (!userMessage.trim() || isLoading) return; - setMessages((prev) => [...prev, { text: userMessage, isUser: true, timestamp: new Date() }]); + // Reset streaming text reference + streamingTextRef.current = ''; + + // Add user message + const newMessage: ChatMessageType = { + text: userMessage, + isUser: true, + timestamp: new Date(), + }; + + setMessages((prev) => [...prev, newMessage]); setIsLoading(true); setError(null); try { - const response = await sendChatMessage( - messages.map((v) => ({ role: v.isUser ? 'user' : 'system', content: v.text })) - ); + const payload = { + messages: [...messages, newMessage].map((v) => ({ + role: v.isUser ? 'user' : 'assistant', + content: v.text, + })), + editorCode, + language, + questionDetails, + }; + + // Add AI response placeholder + setMessages((prev) => [ + ...prev, + { + text: '', + isUser: false, + timestamp: new Date(), + isStreaming: true, + }, + ]); + + const response = await sendChatMessage(payload, (chunk) => { + // Update streaming text + streamingTextRef.current = chunk; + + // Update the last message with the accumulated text + setMessages((prev) => { + const newMessages = [...prev]; + newMessages[newMessages.length - 1] = { + text: streamingTextRef.current, + isUser: false, + timestamp: new Date(), + isStreaming: true, + }; + return newMessages; + }); + }); if (response.success) { - setMessages((prev) => [ - ...prev, - { text: response.message, isUser: false, timestamp: new Date() }, - ]); + setMessages((prev) => { + const newMessages = [...prev]; + newMessages[newMessages.length - 1] = { + text: newMessages[newMessages.length - 1].text, + isUser: false, + timestamp: new Date(), + isStreaming: false, + }; + return newMessages; + }); + } else { + setError('Failed to get response from AI'); + // Remove the streaming message if there was an error + setMessages((prev) => prev.slice(0, -1)); } } catch (err) { setError( err instanceof Error ? err.message : 'An error occurred while fetching the response' ); + // Remove the streaming message if there was an error + setMessages((prev) => prev.slice(0, -1)); } finally { setIsLoading(false); + streamingTextRef.current = ''; } }; diff --git a/frontend/src/components/blocks/interview/chat/chat-message.tsx b/frontend/src/components/blocks/interview/chat/chat-message.tsx index ec4c7d6119..fe9159957a 100644 --- a/frontend/src/components/blocks/interview/chat/chat-message.tsx +++ b/frontend/src/components/blocks/interview/chat/chat-message.tsx @@ -3,6 +3,7 @@ export interface ChatMessageType { isUser: boolean; timestamp: Date; isCode?: boolean; + isStreaming?: boolean; } interface ChatMessageProps { diff --git a/frontend/src/components/blocks/interview/editor.tsx b/frontend/src/components/blocks/interview/editor.tsx index bd6d863d41..d0f4b9e872 100644 --- a/frontend/src/components/blocks/interview/editor.tsx +++ b/frontend/src/components/blocks/interview/editor.tsx @@ -3,7 +3,7 @@ import { useWindowSize } from '@uidotdev/usehooks'; import type { LanguageName } from '@uiw/codemirror-extensions-langs'; import CodeMirror from '@uiw/react-codemirror'; import { Bot, User } from 'lucide-react'; -import { useMemo, useState } from 'react'; +import { useEffect,useMemo, useState } from 'react'; import { Button } from '@/components/ui/button'; import { Label } from '@/components/ui/label'; @@ -25,9 +25,10 @@ type EditorProps = { room: string; onAIClick: () => void; onPartnerClick: () => void; + onCodeChange?: (code: string, language: LanguageName) => void; }; -export const Editor = ({ room, onAIClick, onPartnerClick }: EditorProps) => { +export const Editor = ({ room, onAIClick, onPartnerClick, onCodeChange }: EditorProps) => { const { height } = useWindowSize(); const [theme, setTheme] = useState('vscodeDark'); const { @@ -45,6 +46,21 @@ export const Editor = ({ room, onAIClick, onPartnerClick }: EditorProps) => { return getTheme(theme); }, [theme]); + useEffect(() => { + onCodeChange?.(code, language); + }, [code, language, onCodeChange]); + + const handleLanguageChange = (val: string) => { + const newLanguage = val as LanguageName; + setLanguage(newLanguage); + onCodeChange?.(code, newLanguage); + }; + + const handleCodeChange = (value: string) => { + setCode(value); + onCodeChange?.(value, language); + }; + return (
{isLoading ? ( @@ -63,7 +79,7 @@ export const Editor = ({ room, onAIClick, onPartnerClick }: EditorProps) => {
- @@ -94,7 +110,6 @@ export const Editor = ({ room, onAIClick, onPartnerClick }: EditorProps) => {
- {/* TODO: Get user avatar and display */} {members.map((member, index) => (
{ }} height={`${Math.max((height as number) - EXTENSION_HEIGHT, MIN_EDITOR_HEIGHT)}px`} value={code} - onChange={(value, _viewUpdate) => { - setCode(value); - }} + onChange={handleCodeChange} theme={themePreset} lang={language} basicSetup={{ diff --git a/frontend/src/routes/interview/[room]/main.tsx b/frontend/src/routes/interview/[room]/main.tsx index 86ed68c13e..ee4f02f9a6 100644 --- a/frontend/src/routes/interview/[room]/main.tsx +++ b/frontend/src/routes/interview/[room]/main.tsx @@ -1,4 +1,5 @@ import { QueryClient, useSuspenseQuery } from '@tanstack/react-query'; +import { LanguageName } from '@uiw/codemirror-extensions-langs'; import { useMemo, useState } from 'react'; import { type LoaderFunctionArgs, Navigate, useLoaderData } from 'react-router-dom'; @@ -32,6 +33,13 @@ export const InterviewRoom = () => { const questionDetails = useMemo(() => details.question, [details]); const [isAIChatOpen, setIsAIChatOpen] = useState(false); const [isPartnerChatOpen, setIsPartnerChatOpen] = useState(false); + const [currentCode, setCurrentCode] = useState(''); + const [currentLanguage, setCurrentLanguage] = useState('typescript'); + + const handleCodeChange = (code: string, language: LanguageName) => { + setCurrentCode(code); + setCurrentLanguage(language); + }; const handleAIClick = () => { setIsPartnerChatOpen(false); @@ -57,12 +65,19 @@ export const InterviewRoom = () => { room={roomId as string} onAIClick={handleAIClick} onPartnerClick={handlePartnerClick} + onCodeChange={handleCodeChange} />
{(isAIChatOpen || isPartnerChatOpen) && ( {isAIChatOpen && ( - setIsAIChatOpen(false)} /> + setIsAIChatOpen(false)} + editorCode={currentCode} + language={currentLanguage} + questionDetails={questionDetails.description} + /> )} {isPartnerChatOpen && ( -): Promise => { - return collabApiClient - .post(AI_SERVICE_ROUTES.CHAT, { messages }) - .then((res) => { - return res.data as IChatResponse; - }) - .catch((err) => { - console.error(err); +export const sendChatMessage = async ( + payload: ChatPayload, + onStream?: (chunk: string) => void +): Promise => { + try { + if (onStream) { + // Streaming request + await collabApiClient.post(AI_SERVICE_ROUTES.CHAT, payload, { + headers: { + Accept: 'text/event-stream', + 'Cache-Control': 'no-cache', + Connection: 'keep-alive', + }, + responseType: 'text', + onDownloadProgress: (progressEvent) => { + const rawText = progressEvent.event.target.responseText; + const lines = rawText.split('\n'); + let newContent = ''; + + for (const line of lines) { + if (line.startsWith('data: ')) { + const content = line.slice(6); + newContent += content; + } + } + + if (newContent) { + onStream(newContent); + } + }, + }); + return { - success: false, - message: 'An error occurred while processing your request.', - } as IChatResponse; - }); + success: true, + message: 'Streaming completed successfully', + }; + } else { + const response = await collabApiClient.post(AI_SERVICE_ROUTES.CHAT, payload); + return response.data as ChatResponse; + } + } catch (err) { + console.error('Error in sendChatMessage:', err); + return { + success: false, + message: 'An error occurred while processing your request.', + }; + } }; From 93ea1130fd32c1c95e402b570dda853dafb900f3 Mon Sep 17 00:00:00 2001 From: anunayajoshi Date: Sat, 2 Nov 2024 23:46:54 +0800 Subject: [PATCH 06/11] keep chat history --- .../src/service/post/openai-service.ts | 25 ++++++--- .../components/blocks/interview/ai-chat.tsx | 54 ++++++++++++++++++- .../blocks/interview/chat/chat-layout.tsx | 44 +++++++++++++-- 3 files changed, 112 insertions(+), 11 deletions(-) diff --git a/backend/collaboration/src/service/post/openai-service.ts b/backend/collaboration/src/service/post/openai-service.ts index 67c1eddeab..7b71e5ca61 100644 --- a/backend/collaboration/src/service/post/openai-service.ts +++ b/backend/collaboration/src/service/post/openai-service.ts @@ -22,11 +22,16 @@ interface OpenAIRequest { const createSystemMessage = (editorCode?: string, language?: string, questionDetails?: string) => { return { role: 'system' as const, - content: `You are a helpful coding assistant. + content: `You are a mentor in a coding interview. You are helping a user with a coding problem. ${questionDetails ? `\nQuestion Context:\n${JSON.stringify(questionDetails, null, 2)}` : ''} -${editorCode ? `\nCurrent Code (${language || 'unknown'}):\n${editorCode}` : ''} -Provide detailed help while referring to their specific code and question context when available.`, + +${editorCode ? `\nCurrent Code in the Editor written by the user in language: (${language || 'unknown'}):\n${editorCode}` : ''} + + +If they do not ask for questions related to their code or the question context, you can provide general coding advice anyways. Be very concise and conversational in your responses. + +Your response should only be max 4-5 sentences. Do NOT provide code in your answers, but instead try to guide them and give tips for how to solve it. YOU MUST NOT SOLVE THE PROBLEM FOR THEM, OR WRITE ANY CODE. Guide the user towards the solution, don't just give the solution. MAX 4-5 SENTENCES. Ask questions instead of giving answers. Be conversational and friendly.`, }; }; @@ -36,8 +41,16 @@ export async function getOpenAIResponse(request: OpenAIRequest) { try { const response = await openai.chat.completions.create({ - model: 'gpt-3.5-turbo', - messages: [createSystemMessage(editorCode, language, questionDetails), ...messages], + model: 'gpt-4o', + messages: [ + createSystemMessage(editorCode, language, questionDetails), + ...messages, + { + role: 'assistant', + content: + '', + }, + ], }); if (response.choices && response.choices[0].message) { @@ -60,7 +73,7 @@ export async function getOpenAIStreamResponse(request: OpenAIRequest): Promise = ({ isOpen, onClose, @@ -25,6 +32,50 @@ export const AIChat: React.FC = ({ const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const streamingTextRef = useRef(''); + const prevQuestionRef = useRef(questionDetails); + + useEffect(() => { + const loadMessages = () => { + const stored = localStorage.getItem(STORAGE_KEY); + + if (stored) { + const { messages: storedMessages, questionDetails: storedQuestion } = JSON.parse( + stored + ) as StoredChat; + + // If question has changed, clear the history + if (storedQuestion !== questionDetails) { + localStorage.removeItem(STORAGE_KEY); + setMessages([]); + } else { + // Convert stored timestamps back to Date objects + const messagesWithDates = storedMessages.map((msg) => ({ + ...msg, + timestamp: new Date(msg.timestamp), + })); + setMessages(messagesWithDates); + } + } + }; + + loadMessages(); + prevQuestionRef.current = questionDetails; + }, [questionDetails]); + + const handleClearHistory = () => { + localStorage.removeItem(STORAGE_KEY); + setMessages([]); + }; + + useEffect(() => { + if (messages.length > 0) { + const dataToStore: StoredChat = { + messages, + questionDetails, + }; + localStorage.setItem(STORAGE_KEY, JSON.stringify(dataToStore)); + } + }, [messages, questionDetails]); const handleSend = async (userMessage: string): Promise => { if (!userMessage.trim() || isLoading) return; @@ -119,6 +170,7 @@ export const AIChat: React.FC = ({ isLoading={isLoading} error={error} title='AI Assistant' + onClearHistory={handleClearHistory} /> ); }; diff --git a/frontend/src/components/blocks/interview/chat/chat-layout.tsx b/frontend/src/components/blocks/interview/chat/chat-layout.tsx index e58116894c..cb85f319e5 100644 --- a/frontend/src/components/blocks/interview/chat/chat-layout.tsx +++ b/frontend/src/components/blocks/interview/chat/chat-layout.tsx @@ -1,7 +1,18 @@ -import { Loader2, MessageSquare, Send, X } from 'lucide-react'; +import { Loader2, MessageSquare, Send, Trash2, X } from 'lucide-react'; import React, { ChangeEvent, KeyboardEvent, useEffect, useRef, useState } from 'react'; import { Alert, AlertDescription } from '@/components/ui/alert'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from '@/components/ui/alert-dialog'; import { Button } from '@/components/ui/button'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Textarea } from '@/components/ui/textarea'; @@ -16,6 +27,7 @@ interface ChatLayoutProps { isLoading: boolean; error: string | null; title: string; + onClearHistory?: () => void; } export const ChatLayout: React.FC = ({ @@ -26,6 +38,7 @@ export const ChatLayout: React.FC = ({ isLoading, error, title, + onClearHistory, }) => { const [input, setInput] = useState(''); const inputRef = useRef(null); @@ -35,7 +48,6 @@ export const ChatLayout: React.FC = ({ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }; - // Focus and scroll to bottom on window open useEffect(() => { if (isOpen) { inputRef.current?.focus(); @@ -43,12 +55,10 @@ export const ChatLayout: React.FC = ({ } }, [isOpen]); - // Scroll to bottom on reception of messages useEffect(() => { scrollToBottom(); }, [messages, isLoading]); - // Resize textarea on input, up to a maximum height useEffect(() => { const textAreaEl = inputRef.current; @@ -82,6 +92,32 @@ export const ChatLayout: React.FC = ({

{title}

+ {onClearHistory && ( + + + + + + + Clear Chat History + + Are you sure you want to clear the chat history? This action cannot be undone. + + + + Cancel + Clear History + + + + )} +
+ +
+ ) : ( + + {children} + + ); + }, + }} + > + {children} + + ); +}; diff --git a/frontend/src/components/blocks/interview/chat/chat-message.tsx b/frontend/src/components/blocks/interview/chat/chat-message.tsx index fe9159957a..5f62ed51a2 100644 --- a/frontend/src/components/blocks/interview/chat/chat-message.tsx +++ b/frontend/src/components/blocks/interview/chat/chat-message.tsx @@ -1,3 +1,7 @@ +import { cn } from '@/lib/utils'; + +import { MarkdownComponent } from './chat-markdown'; + export interface ChatMessageType { text: string; isUser: boolean; @@ -25,10 +29,6 @@ const CodeBlock: React.FC<{ content: string }> = ({ content }) => ( ); export const ChatMessage: React.FC = ({ message }) => { - // Detect if the message contains code (basic detection - can be enhanced) - const hasCode = message.text.includes('```'); - const parts = hasCode ? message.text.split('```') : [message.text]; - return (
= ({ message }) => { message.isUser ? 'bg-secondary/50' : 'bg-secondary' }`} > - {parts.map((part, index) => { - if (index % 2 === 1) { - // Code block - return ; - } - - return ( -
- {part.split('\n').map((line, i) => ( -

- {line} -

- ))} -
- ); - })} + + {message.text} +
{message.timestamp.toLocaleTimeString([], { hour: '2-digit', diff --git a/frontend/src/routes/interview/[room]/main.tsx b/frontend/src/routes/interview/[room]/main.tsx index ee4f02f9a6..5321e13c2e 100644 --- a/frontend/src/routes/interview/[room]/main.tsx +++ b/frontend/src/routes/interview/[room]/main.tsx @@ -69,7 +69,7 @@ export const InterviewRoom = () => { />
{(isAIChatOpen || isPartnerChatOpen) && ( - + {isAIChatOpen && ( { - const rawText = progressEvent.event.target.responseText; - const lines = rawText.split('\n'); - let newContent = ''; + const rawText: string = progressEvent.event.target.responseText; - for (const line of lines) { - if (line.startsWith('data: ')) { - const content = line.slice(6); - newContent += content; - } - } - - if (newContent) { - onStream(newContent); + if (rawText) { + onStream(rawText); } }, }); diff --git a/package-lock.json b/package-lock.json index f183f194bf..821b05a2cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -279,6 +279,7 @@ "react-katex": "^3.0.1", "react-markdown": "^9.0.1", "react-router-dom": "^6.26.2", + "react-syntax-highlighter": "^15.6.1", "rehype-katex": "^7.0.1", "remark-gfm": "^4.0.0", "remark-math": "^6.0.0", @@ -299,6 +300,7 @@ "@types/node": "^22.5.5", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", + "@types/react-syntax-highlighter": "^15.5.13", "@types/ws": "^8.5.12", "@vitejs/plugin-react-swc": "^3.5.0", "autoprefixer": "^10.4.20", @@ -2980,6 +2982,16 @@ "@types/react": "*" } }, + "node_modules/@types/react-syntax-highlighter": { + "version": "15.5.13", + "resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz", + "integrity": "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/send": { "version": "0.17.4", "dev": true, @@ -6729,6 +6741,19 @@ "reusify": "^1.0.4" } }, + "node_modules/fault": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", + "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", + "license": "MIT", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "dev": true, @@ -6866,6 +6891,14 @@ "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==" }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/formdata-node": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", @@ -7427,6 +7460,21 @@ "dev": true, "license": "MIT" }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/highlightjs-vue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz", + "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==", + "license": "CC0-1.0" + }, "node_modules/html-url-attributes": { "version": "3.0.1", "license": "MIT", @@ -8641,6 +8689,20 @@ "loose-envify": "cli.js" } }, + "node_modules/lowlight": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", + "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==", + "license": "MIT", + "dependencies": { + "fault": "^1.0.0", + "highlight.js": "~10.7.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/lru-cache": { "version": "10.4.3", "license": "ISC" @@ -10702,6 +10764,15 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/prismjs": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", + "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/process": { "version": "0.11.10", "license": "MIT", @@ -11004,6 +11075,23 @@ } } }, + "node_modules/react-syntax-highlighter": { + "version": "15.6.1", + "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.1.tgz", + "integrity": "sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.3.1", + "highlight.js": "^10.4.1", + "highlightjs-vue": "^1.0.0", + "lowlight": "^1.17.0", + "prismjs": "^1.27.0", + "refractor": "^3.6.0" + }, + "peerDependencies": { + "react": ">= 0.14.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "license": "MIT", @@ -11075,6 +11163,197 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/refractor": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz", + "integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==", + "license": "MIT", + "dependencies": { + "hastscript": "^6.0.0", + "parse-entities": "^2.0.0", + "prismjs": "~1.27.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/@types/hast": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", + "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/refractor/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/refractor/node_modules/character-entities": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", + "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/character-entities-legacy": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", + "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/character-reference-invalid": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", + "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/comma-separated-tokens": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", + "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/hast-util-parse-selector": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz", + "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/refractor/node_modules/hastscript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz", + "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^1.0.0", + "hast-util-parse-selector": "^2.0.0", + "property-information": "^5.0.0", + "space-separated-tokens": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/refractor/node_modules/is-alphabetical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", + "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/is-alphanumerical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", + "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/is-decimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", + "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/is-hexadecimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", + "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/parse-entities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", + "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", + "license": "MIT", + "dependencies": { + "character-entities": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "character-reference-invalid": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-hexadecimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/prismjs": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz", + "integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/refractor/node_modules/property-information": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", + "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/space-separated-tokens": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", + "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/regenerator-runtime": { "version": "0.14.1", "license": "MIT" From 8136163c43efeefa4dcf788fd576b9359eef0593 Mon Sep 17 00:00:00 2001 From: SeeuSim Date: Mon, 4 Nov 2024 01:12:28 +0800 Subject: [PATCH 08/11] PEER-226: Remove unused code Signed-off-by: SeeuSim --- .../blocks/interview/chat/chat-message.tsx | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/frontend/src/components/blocks/interview/chat/chat-message.tsx b/frontend/src/components/blocks/interview/chat/chat-message.tsx index 5f62ed51a2..63c7a797ac 100644 --- a/frontend/src/components/blocks/interview/chat/chat-message.tsx +++ b/frontend/src/components/blocks/interview/chat/chat-message.tsx @@ -14,20 +14,6 @@ interface ChatMessageProps { message: ChatMessageType; } -const CodeBlock: React.FC<{ content: string }> = ({ content }) => ( -
-
-      {content}
-    
- -
-); - export const ChatMessage: React.FC = ({ message }) => { return (
From 571628bc074e45b27a9b386f0293e3f441fb2322 Mon Sep 17 00:00:00 2001 From: SeeuSim Date: Mon, 4 Nov 2024 01:41:25 +0800 Subject: [PATCH 09/11] PEER-226: Add prompts Signed-off-by: SeeuSim --- .../components/blocks/interview/ai-chat.tsx | 30 +++++++++++++++++-- .../blocks/interview/chat/chat-layout.tsx | 28 +++++++++++++---- 2 files changed, 51 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/blocks/interview/ai-chat.tsx b/frontend/src/components/blocks/interview/ai-chat.tsx index b954064ac4..fbcf946e97 100644 --- a/frontend/src/components/blocks/interview/ai-chat.tsx +++ b/frontend/src/components/blocks/interview/ai-chat.tsx @@ -1,11 +1,15 @@ import { type LanguageName } from '@uiw/codemirror-extensions-langs'; +import { MessageSquareIcon } from 'lucide-react'; import React, { useEffect, useRef, useState } from 'react'; +import { Button } from '@/components/ui/button'; import { sendChatMessage } from '@/services/collab-service'; import { ChatLayout } from './chat/chat-layout'; import { ChatMessageType } from './chat/chat-message'; +const STORAGE_KEY = 'ai_chat_history'; + interface AIChatProps { isOpen: boolean; onClose: () => void; @@ -14,13 +18,16 @@ interface AIChatProps { questionDetails?: string; } -const STORAGE_KEY = 'ai_chat_history'; - interface StoredChat { messages: ChatMessageType[]; questionDetails: string; } +const prompts = [ + 'Help me understand the code written.', + 'Give me some suggestions to solve the problem.', +]; + export const AIChat: React.FC = ({ isOpen, onClose, @@ -171,6 +178,25 @@ export const AIChat: React.FC = ({ error={error} title='AI Assistant' onClearHistory={handleClearHistory} + CustomPlaceHolderElem={({ onSend }) => ( +
+ +

No messages yet. Start a conversation, or use one of these prompts:

+
+ {prompts.map((value, index) => ( + + ))} +
+
+ )} /> ); }; diff --git a/frontend/src/components/blocks/interview/chat/chat-layout.tsx b/frontend/src/components/blocks/interview/chat/chat-layout.tsx index cb85f319e5..e5f2b57883 100644 --- a/frontend/src/components/blocks/interview/chat/chat-layout.tsx +++ b/frontend/src/components/blocks/interview/chat/chat-layout.tsx @@ -1,5 +1,11 @@ import { Loader2, MessageSquare, Send, Trash2, X } from 'lucide-react'; -import React, { ChangeEvent, KeyboardEvent, useEffect, useRef, useState } from 'react'; +import React, { + ChangeEvent, + KeyboardEvent, + useEffect, + useRef, + useState, +} from 'react'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { @@ -19,6 +25,10 @@ import { Textarea } from '@/components/ui/textarea'; import { ChatMessage, ChatMessageType } from './chat-message'; +type CustomElemProps = { + onSend: (message: string) => void; +}; + interface ChatLayoutProps { isOpen: boolean; onClose: () => void; @@ -28,9 +38,10 @@ interface ChatLayoutProps { error: string | null; title: string; onClearHistory?: () => void; + CustomPlaceHolderElem?: React.FC; } -export const ChatLayout: React.FC = ({ +export const ChatLayout = ({ isOpen, onClose, messages, @@ -39,7 +50,8 @@ export const ChatLayout: React.FC = ({ error, title, onClearHistory, -}) => { + CustomPlaceHolderElem, +}: ChatLayoutProps) => { const [input, setInput] = useState(''); const inputRef = useRef(null); const messagesEndRef = useRef(null); @@ -132,8 +144,14 @@ export const ChatLayout: React.FC = ({ {messages.length === 0 && (
- -

No messages yet. Start a conversation!

+ {CustomPlaceHolderElem ? ( + + ) : ( + <> + +

No messages yet. Start a conversation!

+ + )}
)} {messages.map((msg, index) => ( From 4808a9e49803310f2f8b992e45bb461650737ab6 Mon Sep 17 00:00:00 2001 From: SeeuSim Date: Mon, 4 Nov 2024 01:45:07 +0800 Subject: [PATCH 10/11] PEER-226: Clean up code Signed-off-by: SeeuSim --- frontend/src/components/ui/multi-select.tsx | 2 +- frontend/src/lib/queries/question-details.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/ui/multi-select.tsx b/frontend/src/components/ui/multi-select.tsx index 842bf737a1..04fd381b96 100644 --- a/frontend/src/components/ui/multi-select.tsx +++ b/frontend/src/components/ui/multi-select.tsx @@ -221,7 +221,7 @@ const MultiSelectorTrigger = forwardRef queryOptions({ queryKey: ['qn', 'details', id], From b66d9a15c3a6347eb1f0075024c2fcf0854f5bcd Mon Sep 17 00:00:00 2001 From: Chu Yi Ting Date: Mon, 4 Nov 2024 02:38:03 +0800 Subject: [PATCH 11/11] Peer-226: Build-fail - Remove unused code, and small ui update --- .../blocks/interview/chat/chat-layout.tsx | 4 +--- .../blocks/interview/chat/chat-message.tsx | 14 -------------- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/frontend/src/components/blocks/interview/chat/chat-layout.tsx b/frontend/src/components/blocks/interview/chat/chat-layout.tsx index cb85f319e5..5aeaa85937 100644 --- a/frontend/src/components/blocks/interview/chat/chat-layout.tsx +++ b/frontend/src/components/blocks/interview/chat/chat-layout.tsx @@ -141,9 +141,7 @@ export const ChatLayout: React.FC = ({ ))} {isLoading && (
-
- -
+
)} {error && ( diff --git a/frontend/src/components/blocks/interview/chat/chat-message.tsx b/frontend/src/components/blocks/interview/chat/chat-message.tsx index 5f62ed51a2..63c7a797ac 100644 --- a/frontend/src/components/blocks/interview/chat/chat-message.tsx +++ b/frontend/src/components/blocks/interview/chat/chat-message.tsx @@ -14,20 +14,6 @@ interface ChatMessageProps { message: ChatMessageType; } -const CodeBlock: React.FC<{ content: string }> = ({ content }) => ( -
-
-      {content}
-    
- -
-); - export const ChatMessage: React.FC = ({ message }) => { return (