From 268a8de478a8214cf5ae70092c0080aaff974695 Mon Sep 17 00:00:00 2001 From: salmanabdurrahman Date: Tue, 6 Jan 2026 11:48:43 +0700 Subject: [PATCH 1/4] feat(ai): integrate OpenAI API for chat and content generation --- .env.example | 5 ++ package-lock.json | 94 +++++++++++++++++++++++--------------- package.json | 1 + src/config/index.ts | 5 ++ src/core/ai.ts | 108 ++++++++++++++++++++++++++++---------------- 5 files changed, 138 insertions(+), 75 deletions(-) diff --git a/.env.example b/.env.example index 4c10d40..a366c15 100644 --- a/.env.example +++ b/.env.example @@ -15,6 +15,11 @@ GOOGLE_CLIENT_ID=YOUR_GOOGLE_CLIENT_ID_HERE GEMINI_API_KEY=YOUR_GEMINI_API_KEY_HERE GEMINI_MODEL=YOUR_GEMINI_MODEL_HERE # e.g., "gemini-2.0-flash" +# Open AI API +OPENAI_API_KEY=YOUR_OPENAI_API_KEY_HERE +OPENAI_MODEL=YOUR_OPENAI_MODEL_HERE +OPENAI_BASE_URL=YOUR_OPENAI_BASE_URL_HERE + # Database Configuration (For Docker) DATABASE_USER=YOUR_DATABASE_USER_HERE DATABASE_PASSWORD=YOUR_DATABASE_PASSWORD_HERE diff --git a/package-lock.json b/package-lock.json index 181ddb5..90d8249 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "google-auth-library": "^10.3.0", "jsonwebtoken": "^9.0.2", "node-cron": "^4.2.1", + "openai": "^6.15.0", "zod": "^4.1.5" }, "devDependencies": { @@ -1049,7 +1050,6 @@ "integrity": "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.10.0" } @@ -1134,7 +1134,6 @@ "integrity": "sha512-n1H6IcDhmmUEG7TNVSspGmiHHutt7iVKtZwRppD7e04wha5MrkV1h3pti9xQLcCMt6YWsncpoT0HMjkH1FNwWQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.0", "@typescript-eslint/types": "8.46.0", @@ -1484,7 +1483,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1640,23 +1638,27 @@ } }, "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", - "debug": "^4.4.0", + "debug": "^4.4.3", "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", + "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" + "raw-body": "^3.0.1", + "type-is": "^2.0.1" }, "engines": { "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/brace-expansion": { @@ -1997,9 +1999,9 @@ } }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2246,7 +2248,6 @@ "integrity": "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -2308,7 +2309,6 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -2531,7 +2531,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", @@ -3124,15 +3123,19 @@ } }, "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz", + "integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/ignore": { @@ -3285,15 +3288,14 @@ "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", "devOptional": true, "license": "MIT", - "peer": true, "bin": { "jiti": "lib/jiti-cli.mjs" } }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -3367,12 +3369,12 @@ } }, "node_modules/jsonwebtoken/node_modules/jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz", + "integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==", "license": "MIT", "dependencies": { - "jwa": "^1.4.1", + "jwa": "^1.4.2", "safe-buffer": "^5.0.1" } }, @@ -3388,12 +3390,12 @@ } }, "node_modules/jws": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", "license": "MIT", "dependencies": { - "jwa": "^2.0.0", + "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, @@ -3806,6 +3808,27 @@ "wrappy": "1" } }, + "node_modules/openai": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.15.0.tgz", + "integrity": "sha512-F1Lvs5BoVvmZtzkUEVyh8mDQPPFolq4F+xdsx/DO8Hee8YF3IGAlZqUIsF+DVGhqf4aU0a3bTghsxB6OIsRy1g==", + "license": "Apache-2.0", + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -3969,7 +3992,6 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -4000,7 +4022,6 @@ "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@prisma/config": "6.15.0", "@prisma/engines": "6.15.0" @@ -4068,9 +4089,9 @@ "license": "MIT" }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -4627,7 +4648,6 @@ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index 15b83e4..98da1a8 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "google-auth-library": "^10.3.0", "jsonwebtoken": "^9.0.2", "node-cron": "^4.2.1", + "openai": "^6.15.0", "zod": "^4.1.5" }, "devDependencies": { diff --git a/src/config/index.ts b/src/config/index.ts index f8d67ea..54eb74c 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -17,6 +17,11 @@ const config = { apiKey: process.env.GEMINI_API_KEY || '', model: process.env.GEMINI_MODEL || '', }, + openai: { + apiKey: process.env.OPENAI_API_KEY || '', + model: process.env.OPENAI_MODEL || '', + baseUrl: process.env.OPENAI_BASE_URL || '', + }, }; export default config; diff --git a/src/core/ai.ts b/src/core/ai.ts index 331c6fb..f1e9931 100644 --- a/src/core/ai.ts +++ b/src/core/ai.ts @@ -1,57 +1,89 @@ -import { GoogleGenerativeAI, type Content } from '@google/generative-ai'; +import OpenAI from 'openai'; import config from '../config/index.js'; -const genAI = new GoogleGenerativeAI(config.gemini.apiKey); -const model = genAI.getGenerativeModel({ model: config.gemini.model }); +const openai = new OpenAI({ + apiKey: config.openai.apiKey, + baseURL: config.openai.baseUrl, +}); + +interface ChatHistoryMessage { + role: 'user' | 'model'; + parts: Array<{ text: string }>; +} + +interface CoachChatResponse { + response: { + text: () => string; + }; +} + +interface CoachChat { + sendMessage: (userMessage: string) => Promise; +} export function startCoachChat( systemPrompt: string, nickname: string, - chatHistory: Content[] = [] -) { - return model.startChat({ - history: [ - { - role: 'user', - parts: [ - { - text: systemPrompt, - }, - ], - }, - { - role: 'model', - parts: [ - { - text: `Tentu, aku siap mendengarkan ${nickname}. Apa yang sedang kamu rasakan?`, - }, - ], - }, - ...chatHistory, - ], - }); + chatHistory: ChatHistoryMessage[] = [] +): CoachChat { + const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [ + { + role: 'system', + content: systemPrompt, + }, + { + role: 'assistant', + content: `Tentu, aku siap mendengarkan ${nickname}. Apa yang sedang kamu rasakan?`, + }, + ...chatHistory.map( + (message): OpenAI.Chat.ChatCompletionMessageParam => ({ + role: message.role === 'model' ? 'assistant' : 'user', + content: message.parts[0]?.text || '', + }) + ), + ]; + + return { + sendMessage: async (userMessage: string): Promise => { + messages.push({ role: 'user', content: userMessage }); + + const response = await openai.chat.completions.create({ + model: config.openai.model, + messages: messages, + }); + + const aiText = response.choices[0]?.message?.content || ''; + + return { + response: { + text: (): string => aiText, + }, + }; + }, + }; } export async function generateContent(prompt: string): Promise { - const result = await model.generateContent(prompt); - const response = result.response; + const response = await openai.chat.completions.create({ + model: config.openai.model, + messages: [{ role: 'user', content: prompt }], + }); - return response.text(); + return response.choices[0]?.message?.content || ''; } export async function generateJsonContent(prompt: string): Promise { - const result = await model.generateContent(prompt); - const rawResponse = result.response.text(); + const response = await openai.chat.completions.create({ + model: config.openai.model, + messages: [{ role: 'user', content: prompt }], + response_format: { type: 'json_object' }, + }); - const jsonString = rawResponse - .replace(/```json/g, '') - .replace(/```/g, '') - .trim(); + const rawResponse = response.choices[0]?.message?.content || '{}'; try { - const parsedJson = JSON.parse(jsonString); - return parsedJson; + return JSON.parse(rawResponse); } catch (error) { - throw new Error('AI returned an invalid JSON format'); + throw new Error('AI mengembalikan format JSON yang tidak valid.'); } } From 0445854ba6b7a09db320b1dd3964cb99973b3d40 Mon Sep 17 00:00:00 2001 From: salmanabdurrahman Date: Tue, 6 Jan 2026 12:08:08 +0700 Subject: [PATCH 2/4] feat(ai): add chat history retrieval functionality --- src/api/ai/ai.controller.ts | 22 +++++++++++++++++++++- src/api/ai/ai.routes.ts | 10 +++++++++- src/api/ai/ai.service.ts | 13 +++++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/api/ai/ai.controller.ts b/src/api/ai/ai.controller.ts index 2010694..7ade89d 100644 --- a/src/api/ai/ai.controller.ts +++ b/src/api/ai/ai.controller.ts @@ -1,5 +1,10 @@ import type { Request, Response } from 'express'; -import { analyzeOnboardingAnswers, getCoachResponse, getLatestSummary } from './ai.service.js'; +import { + analyzeOnboardingAnswers, + findAllChatHistory, + getCoachResponse, + getLatestSummary, +} from './ai.service.js'; import { asyncHandler } from '../../handler/async.handler.js'; import { errorResponse, successResponse } from '../../core/response.js'; @@ -22,6 +27,21 @@ export const askCoachHandler = asyncHandler(async (req: Request, res: Response) }); }); +export const getChatHistoryHandler = asyncHandler(async (req: Request, res: Response) => { + const userId = req.user?.id || req.body?.userId; // Temporary support for userId in body for testing purposes + if (!userId) { + return errorResponse( + res, + 401, + 'Tidak diizinkan', + 'ID pengguna tidak ditemukan dalam permintaan' + ); + } + + const chatHistory = await findAllChatHistory(userId); + return successResponse(res, 200, 'Riwayat chat berhasil diambil', chatHistory); +}); + export const getSummaryHandler = asyncHandler(async (req: Request, res: Response) => { const userId = req.user?.id || req.body?.userId; // Temporary support for userId in body for testing purposes if (!userId) { diff --git a/src/api/ai/ai.routes.ts b/src/api/ai/ai.routes.ts index 1ffb36a..829b61d 100644 --- a/src/api/ai/ai.routes.ts +++ b/src/api/ai/ai.routes.ts @@ -1,5 +1,10 @@ import { Router } from 'express'; -import { askCoachHandler, getSummaryHandler, onboardingAnalysisHandler } from './ai.controller.js'; +import { + askCoachHandler, + getChatHistoryHandler, + getSummaryHandler, + onboardingAnalysisHandler, +} from './ai.controller.js'; import { requireAuth } from '../../middleware/auth.middleware.js'; import { validate } from '../../middleware/validate.middleware.js'; import { askCoachSchema } from './ai.validation.js'; @@ -10,6 +15,9 @@ const router = Router(); // router.post('/ask-coach', requireAuth, validate(askCoachSchema), askCoachHandler); router.post('/ask-coach', validate(askCoachSchema), askCoachHandler); +// router.get('/chat-history', requireAuth, getChatHistoryHandler); +router.get('/chat-history', getChatHistoryHandler); + // router.get('/summary', requireAuth, getSummaryHandler); router.get('/summary', getSummaryHandler); diff --git a/src/api/ai/ai.service.ts b/src/api/ai/ai.service.ts index 363ce62..2f44832 100644 --- a/src/api/ai/ai.service.ts +++ b/src/api/ai/ai.service.ts @@ -70,6 +70,19 @@ export async function getCoachResponse(userId: string, userMessage: string): Pro return aiResponseText; } +export async function findAllChatHistory(userId: string) { + const history = await prisma.aiChatHistory.findMany({ + where: { + userId, + }, + orderBy: { + createdAt: 'asc', + }, + }); + + return history; +} + export async function getLatestSummary(userId: string): Promise { const userProfile = await prisma.userProfile.findUnique({ where: { From f2498f57db0e502354154af58c59ecee8f8dd227 Mon Sep 17 00:00:00 2001 From: salmanabdurrahman Date: Tue, 6 Jan 2026 12:28:50 +0700 Subject: [PATCH 3/4] docs: enhance README with detailed environment variable tables and a new AI chat history endpoint, and update the dev database port in Docker Compose. --- README.md | 72 ++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 48 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 26e5133..07f8f73 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Selamat datang di **Recova Backend API** 👋 Proyek ini menyediakan layanan backend untuk aplikasi **Recova**, yang dirancang untuk membantu pengguna dalam perjalanan pemulihan dan pembentukan kebiasaan positif. -## 🚀 Fitur Utama +## Fitur Utama - **Autentikasi Pengguna** - Login aman menggunakan Google OAuth & JWT. - **Manajemen Profil** - Atur profil (nama panggilan, alasan pemulihan, waktu check-in harian). @@ -16,7 +16,7 @@ Proyek ini menyediakan layanan backend untuk aplikasi **Recova**, yang dirancang - **AI Coach** - Pendamping virtual yang memberikan dukungan emosional dan motivasi. - **Konten Harian** - Motivasi dan tantangan harian untuk menginspirasi pengguna. -## 🛠️ Teknologi yang Digunakan +## Teknologi yang Digunakan - **Runtime**: [Node.js](https://nodejs.org/) - **Framework**: [Express.js](https://expressjs.com/) @@ -27,7 +27,7 @@ Proyek ini menyediakan layanan backend untuk aplikasi **Recova**, yang dirancang - **Validasi**: [Zod](https://zod.dev/) - **Autentikasi**: [JWT](https://www.jwt.io/) & [Google OAuth](https://developers.google.com/identity/protocols/oauth2?hl=id) -## 📦 Prasyarat +## Prasyarat Sebelum mulai, pastikan sudah install: @@ -36,7 +36,7 @@ Sebelum mulai, pastikan sudah install: - [PostgreSQL](https://www.postgresql.org/) - [Docker](https://www.docker.com/) & [Docker Compose](https://docs.docker.com/compose/) (Opsional, untuk menjalankan dengan container) -## ⚡ Instalasi +## Instalasi 1. **Clone repositori** @@ -66,22 +66,45 @@ Sebelum mulai, pastikan sudah install: npm run db:migrate ``` -## ⚙️ Variabel Lingkungan (.env) +## Variabel Lingkungan (.env) File `.env` digunakan untuk mengkonfigurasi aplikasi. Berikut adalah penjelasan untuk setiap variabel yang ada di `.env.example`: -- `PORT`: Port tempat server akan berjalan (contoh: `3000`). -- `DOCS_URL`: URL untuk dokumentasi API (contoh: `/docs`). -- `JWT_SECRET`: Kunci rahasia acak untuk menandatangani token JWT. -- `GOOGLE_CLIENT_ID`: Client ID dari Google Cloud Console untuk otentikasi Google OAuth. -- `GEMINI_API_KEY`: Kunci API untuk layanan Google Gemini yang digunakan oleh AI Coach. -- `GEMINI_MODEL`: Model AI Gemini yang akan digunakan (contoh: `gemini-2.0-flash`). -- `DATABASE_USER`: Nama pengguna untuk database PostgreSQL. -- `DATABASE_PASSWORD`: Kata sandi untuk database PostgreSQL. -- `DATABASE_NAME`: Nama database yang akan digunakan. -- `DATABASE_URL`: URL koneksi lengkap ke database PostgreSQL. Format: `postgresql://USER:PASSWORD@HOST:PORT/DATABASE`. +### Application -## ▶️ Menjalankan Aplikasi +| Variabel | Deskripsi | Contoh | +| ---------- | ------------------------------------ | ------------------------------- | +| `PORT` | Port tempat server akan berjalan. | `3000` | +| `NODE_ENV` | Mode environment aplikasi. | `development` atau `production` | +| `DOCS_URL` | URL untuk dokumentasi API eksternal. | `https://docs.example.com` | + +### JWT & Authentication + +| Variabel | Deskripsi | Contoh | +| ------------------ | ------------------------------------------------------------------------------------------------------- | ------------------------------------------ | +| `JWT_SECRET` | Kunci rahasia acak untuk menandatangani token JWT. Gunakan string acak yang kuat. | `my-super-secret-key-123` | +| `GOOGLE_CLIENT_ID` | Client ID dari [Google Cloud Console](https://console.cloud.google.com/) untuk otentikasi Google OAuth. | `123456789-abc.apps.googleusercontent.com` | + +### AI Configuration + +| Variabel | Deskripsi | Contoh | +| ----------------- | --------------------------------------------------------------------------------------------- | --------------------------- | +| `GEMINI_API_KEY` | Kunci API untuk layanan [Google Gemini](https://ai.google.dev/) yang digunakan oleh AI Coach. | `AIzaSy...` | +| `GEMINI_MODEL` | Model AI Gemini yang akan digunakan. | `gemini-2.0-flash` | +| `OPENAI_API_KEY` | Kunci API untuk layanan OpenAI (opsional, sebagai alternatif Gemini). | `sk-...` | +| `OPENAI_MODEL` | Model OpenAI yang akan digunakan. | `gpt-4o` | +| `OPENAI_BASE_URL` | Base URL untuk API OpenAI (untuk API yang kompatibel dengan OpenAI). | `https://api.openai.com/v1` | + +### Database Configuration + +| Variabel | Deskripsi | Contoh | +| ------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------ | +| `DATABASE_USER` | Nama pengguna untuk database PostgreSQL (digunakan oleh Docker). | `postgres` | +| `DATABASE_PASSWORD` | Kata sandi untuk database PostgreSQL. | `password123` | +| `DATABASE_NAME` | Nama database yang akan digunakan. | `recova_db` | +| `DATABASE_URL` | URL koneksi lengkap ke database PostgreSQL. | `postgresql://postgres:password123@localhost:5432/recova_db` | + +## Menjalankan Aplikasi ### Mode Development (Lokal) @@ -98,7 +121,7 @@ npm run build npm start ``` -### 🌱 Database Seeding +### Database Seeding Proyek ini dilengkapi dengan mekanisme _seeding_ untuk mengisi database dengan data awal untuk keperluan development dan testing. @@ -120,7 +143,7 @@ Untuk menjalankan proses seeding, gunakan perintah: npm run db:seed ``` -## 🐳 Menjalankan dengan Docker +## Menjalankan dengan Docker Proyek ini menyediakan konfigurasi Docker untuk mempermudah proses setup di lingkungan development dan deployment di production. @@ -147,7 +170,7 @@ Buat file `.env.production` sebelum menjalankan di mode production. docker-compose -f docker-compose.yml up -d --build ``` -## 📜 Skrip NPM +## Skrip NPM - `npm run dev` - Jalankan server development (hot reload). - `npm run build` - Build TypeScript ke JavaScript. @@ -162,7 +185,7 @@ docker-compose -f docker-compose.yml up -d --build - `npm run db:studio` - Buka Prisma Studio. - `npm run db:seed` - Isi database dengan data awal (seeding). -## 📂 Struktur Proyek +## Struktur Proyek ``` src/ @@ -179,7 +202,7 @@ src/ ├── config/ # Konfigurasi (env, app settings) ├── core/ # Setup inti server Express ├── database/ # Konfigurasi Prisma & koneksi DB -├── handler/ # Error handling & response standar +├── handler/ # Error handling & response standar ├── middleware/ # Middleware kustom (auth, validation, dsb.) ├── routes/ # Routing API ├── types/ # Definisi tipe global (TypeScript) @@ -187,7 +210,7 @@ src/ └── views/ # Tampilan Views ``` -## 📡 Rute & Endpoint API +## Rute & Endpoint API Semua endpoint berada di bawah prefix: **`/api/v1`**. Pengaturan rute utama terdapat di `src/routes/index.ts` yang menggabungkan semua modul API. @@ -202,6 +225,7 @@ Semua endpoint berada di bawah prefix: **`/api/v1`**. Pengaturan rute utama terd - **`/api/v1/ai`**: Rute untuk fitur berbasis AI. - `POST /ask-coach` - Kirim pesan ke AI Coach. + - `GET /chat-history` - Ambil riwayat percakapan dengan AI Coach. - `GET /summary` - Dapatkan ringkasan check-in harian. - `POST /onboarding-analysis` - Analisis data onboarding. @@ -226,7 +250,7 @@ Semua endpoint berada di bawah prefix: **`/api/v1`**. Pengaturan rute utama terd - **`/api/v1/content`**: Rute untuk konten dinamis. - `GET /daily` - Ambil konten harian. -## 🤝 Kontribusi +## Kontribusi Kontribusi terbuka untuk siapa saja. @@ -236,6 +260,6 @@ Kontribusi terbuka untuk siapa saja. - Push ke branch (`git push origin feat/new-feature`) - Buat **Pull Request** -## 📄 Lisensi +## Lisensi Proyek ini dilisensikan di bawah lisensi [MIT](LICENSE). From 803d2a1a4abf848ef942f487fd5847f1adffa09c Mon Sep 17 00:00:00 2001 From: salmanabdurrahman Date: Tue, 6 Jan 2026 12:36:51 +0700 Subject: [PATCH 4/4] fix: filter invalid AI chat history messages and update dev database port mapping --- src/core/ai.ts | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/core/ai.ts b/src/core/ai.ts index f1e9931..3867cd0 100644 --- a/src/core/ai.ts +++ b/src/core/ai.ts @@ -35,12 +35,20 @@ export function startCoachChat( role: 'assistant', content: `Tentu, aku siap mendengarkan ${nickname}. Apa yang sedang kamu rasakan?`, }, - ...chatHistory.map( - (message): OpenAI.Chat.ChatCompletionMessageParam => ({ - role: message.role === 'model' ? 'assistant' : 'user', - content: message.parts[0]?.text || '', - }) - ), + ...chatHistory + .filter( + (message): message is ChatHistoryMessage => + Array.isArray(message.parts) && + message.parts.length > 0 && + typeof message.parts[0]?.text === 'string' && + message.parts[0]?.text.trim() !== '' + ) + .map( + (message): OpenAI.Chat.ChatCompletionMessageParam => ({ + role: message.role === 'model' ? 'assistant' : 'user', + content: message.parts[0]?.text || '', + }) + ), ]; return {