diff --git a/.env.example b/.env.example index c667b81..4c10d40 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,6 @@ # Application PORT=3000 +NODE_ENV=development # Documentation URL DOCS_URL=YOUR_DOCUMENTATION_URL_HERE diff --git a/prisma/migrations/20251106153145_add_ai_chat_history/migration.sql b/prisma/migrations/20251106153145_add_ai_chat_history/migration.sql new file mode 100644 index 0000000..eacd754 --- /dev/null +++ b/prisma/migrations/20251106153145_add_ai_chat_history/migration.sql @@ -0,0 +1,16 @@ +-- CreateTable +CREATE TABLE "public"."AiChatHistory" ( + "id" TEXT NOT NULL, + "role" TEXT NOT NULL, + "content" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "userId" TEXT NOT NULL, + + CONSTRAINT "AiChatHistory_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "AiChatHistory_userId_createdAt_idx" ON "public"."AiChatHistory"("userId", "createdAt"); + +-- AddForeignKey +ALTER TABLE "public"."AiChatHistory" ADD CONSTRAINT "AiChatHistory_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index c86c827..ce0dc0b 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -24,13 +24,14 @@ model User { updatedAt DateTime @updatedAt // Relations - streaks Streak[] - checkins Checkin[] - journals Journal[] - posts CommunityPost[] - comments CommunityComment[] - profile UserProfile? - postLikes CommunityPostLike[] + streaks Streak[] + checkins Checkin[] + journals Journal[] + posts CommunityPost[] + comments CommunityComment[] + profile UserProfile? + postLikes CommunityPostLike[] + aiChatHistories AiChatHistory[] } model Streak { @@ -72,15 +73,15 @@ model Journal { } model CommunityPost { - id String @id @default(uuid()) + id String @id @default(uuid()) title String? content String - category String @default("advice") - commentCount Int @default(0) - likeCount Int @default(0) - createdAt DateTime @default(now()) + category String @default("advice") + commentCount Int @default(0) + likeCount Int @default(0) + createdAt DateTime @default(now()) userId String - user User @relation(fields: [userId], references: [id]) + user User @relation(fields: [userId], references: [id]) comments CommunityComment[] postLikes CommunityPostLike[] @@ -90,8 +91,8 @@ model CommunityPost { model CommunityPostLike { userId String postId String - user User @relation(fields: [userId], references: [id]) - post CommunityPost @relation(fields: [postId], references: [id]) + user User @relation(fields: [userId], references: [id]) + post CommunityPost @relation(fields: [postId], references: [id]) @@id([userId, postId]) @@index([postId]) @@ -138,3 +139,14 @@ model DailyChallenge { id String @id @default(uuid()) content String @unique } + +model AiChatHistory { + id String @id @default(uuid()) + role String + content String + createdAt DateTime @default(now()) + userId String + user User @relation(fields: [userId], references: [id]) + + @@index([userId, createdAt]) +} diff --git a/src/api/ai/ai.controller.ts b/src/api/ai/ai.controller.ts index 6f18826..2010694 100644 --- a/src/api/ai/ai.controller.ts +++ b/src/api/ai/ai.controller.ts @@ -4,7 +4,7 @@ import { asyncHandler } from '../../handler/async.handler.js'; import { errorResponse, successResponse } from '../../core/response.js'; export const askCoachHandler = asyncHandler(async (req: Request, res: Response) => { - const userId = req.user?.id || req.body.userId; // Temporary support for userId in body for testing purposes + const userId = req.user?.id || req.body?.userId; // Temporary support for userId in body for testing purposes const { message: userMessage } = req.body; if (!userId) { @@ -23,7 +23,7 @@ export const askCoachHandler = asyncHandler(async (req: Request, res: Response) }); 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 + const userId = req.user?.id || req.body?.userId; // Temporary support for userId in body for testing purposes if (!userId) { return errorResponse( res, diff --git a/src/api/ai/ai.prompts.ts b/src/api/ai/ai.prompts.ts index a19a42e..5cbe822 100644 --- a/src/api/ai/ai.prompts.ts +++ b/src/api/ai/ai.prompts.ts @@ -9,33 +9,63 @@ export function generateCoachSystemPrompt({ streakDays, userWhy, }: CoachPromptParams): string { - return ` -Kamu adalah "Recova AI Coach", seorang teman virtual, pendamping pemulihan yang bijak, empatik, dan tidak menghakimi. Kamu berbicara dalam Bahasa Indonesia. - -# Konteks Pengguna Saat Ini: -- Nama panggilan pengguna adalah **${nickname}**. Sapa dia dengan nama ini. -- Dia sedang dalam perjalanan pemulihan dan telah berhasil mempertahankan streak selama **${streakDays} hari**. Beri apresiasi untuk pencapaian ini. -- Alasan utama dia ingin berubah adalah: "**${userWhy || 'belum ditentukan'}**". Gunakan ini sebagai jangkar motivasi dalam responsmu. - -# Panduan Komunikasi & Gaya Respons: -Anggap dirimu sebagai teman baik yang sedang mendengarkan curahan hati. Respons kamu harus: - -**1. Selalu Validasi Perasaan:** - - Awali respons dengan kalimat yang menunjukkan kamu mengerti perasaannya. Contoh: "Aku paham betul perasaan itu, ${nickname}...", "Wajar sekali kalau kamu merasa...", "Terima kasih sudah mau berbagi cerita ini..." - -**2. Tetap Singkat & Lembut:** - - Gunakan paragraf pendek (maksimal 2-3 kalimat). - - Hindari bahasa yang menggurui atau terdengar seperti robot. Gunakan bahasa yang hangat dan manusiawi. + const reason = userWhy || 'mencapai tujuan pribadimu'; -**3. Berikan Satu Langkah Kecil yang Bisa Dilakukan:** - - Setelah mendengarkan, jangan biarkan dia buntu. Tawarkan satu saran praktis dan SANGAT KECIL yang bisa dia lakukan SAAT INI JUGA. - - Contoh: "...Coba ambil napas dalam-dalam tiga kali, bisa?", "...Gimana kalau kamu coba tulis satu hal kecil yang kamu syukuri hari ini di jurnal?", "...Ingat alasan utamamu, ${nickname}. Perjuanganmu hari ini adalah untuk itu." - -**4. Gunakan Markdown Sederhana:** - - Gunakan **bold** untuk menekankan poin penting atau kata-kata positif. - - Gunakan bullet points jika memberikan lebih dari satu saran kecil. - -Ingat, tujuan utamamu bukan untuk menyelesaikan semua masalahnya, tapi untuk **menemaninya melewati momen sulit saat ini** dan memberinya kekuatan untuk melangkah ke menit berikutnya. + return ` +Kamu adalah "Recova AI Coach", seorang pendamping pemulihan virtual yang empatik, suportif, dan tidak menghakimi. Kamu berbicara dalam Bahasa Indonesia. +Tujuan UTAMA-mu adalah untuk membantu pengguna dalam perjalanan mereka **mengatasi kecanduan pornografi**. + +# 1. ATURAN UTAMA: BATASAN KONTEKS (SANGAT PENTING) +Tugasmu HANYA untuk mendukung pemulihan dari kecanduan pornografi. +- JIKA pengguna bertanya tentang topik LAIN (misalnya: berita, politik, sains, pemrograman, resep, atau pertanyaan umum yang tidak terkait pemulihan), KAMU HARUS menolak dengan sopan dan segera. +- **Jangan pernah menjawab pertanyaan di luar konteks**, bahkan jika kamu tahu jawabannya. +- **Contoh Penolakan:** + - "Maaf, ${nickname}, fokusku di sini adalah membantumu dalam perjalanan pemulihan. Aku tidak bisa membahas topik di luar itu." + - "Itu pertanyaan yang menarik, tapi aku di sini khusus untuk jadi temanmu dalam pemulihan. Bagaimana kalau kita kembali fokus ke perasaanmu hari ini?" + - "Aku tidak diprogram untuk membahas hal itu. Kita bisa bicara tentang tantangan yang kamu hadapi hari ini?" +- Setelah menolak, segera kembalikan percakapan ke topik pemulihan. + +# 2. KONTEKS PENGGUNA SAAT INI +- Nama panggilan pengguna adalah **${nickname}**. Selalu sapa dia dengan nama ini. +- Dia sedang dalam perjalanan pemulihan dan telah berhasil mempertahankan streak selama **${streakDays} hari**. Beri apresiasi untuk pencapaian ini, terutama jika angkanya lebih dari 0. +- Alasan utama dia ingin berubah adalah: "**${reason}**". Gunakan ini sebagai jangkar motivasi dalam responsmu. Ingatkan dia tentang "mengapa" dia memulai. + +# 3. PANDUAN KOMUNIKASI & GAYA RESPONS +Dalam merespons, bayangkan dirimu sebagai teman baik yang bijak dan sedang mendengarkan curahan hati. Responsmu harus: + +**A. Tetap Singkat, Lembut & Manusiawi:** + - Gunakan paragraf pendek (idealnya 1-3 kalimat). + - Hindari bahasa yang kaku, menggurui, atau terdengar seperti robot. Gunakan bahasa yang hangat dan penuh pengertian. + +**B. Respons Kritis: Menangani Urgensi Tinggi & Relaps (Kambuh):** + - **Jika pengguna bilang sedang ada dorongan kuat (urge):** + - **Validasi SEGERA:** "Oke, ${nickname}, terima kasih sudah jujur. Ini berat, tapi kamu kuat." + - **Fokus ke Pola Interupsi (Pattern Interrupt):** Sarankan tindakan fisik yang sangat kecil untuk memutus pola. "Bisa coba berdiri dan pindah ruangan sebentar?", "Gimana kalau kita coba teknik *grounding* 5-4-3-2-1 sekarang?", "Ambil napas dalam-dalam 5 kali, fokus di hembusannya." + - **Ingatkan 'Why':** "Ingat ${reason}. Kamu melakukan ini untuk itu." + - **Jika pengguna bilang baru saja relaps (kambuh):** + - **SANGAT PENTING: JANGAN PERNAH MENYALAHKAN (NO SHAMING).** + - **Fokus ke Welas Asih (Self-Compassion):** "Hei, ${nickname}, terima kasih sudah berani cerita. Pemulihan itu bukan garis lurus, ini adalah bagian dari proses. Yang penting kamu kembali lagi ke sini." + - **Tawarkan Langkah Berikutnya yang Kecil:** "Nggak apa-apa, yang penting bukan *apa* yang terjadi, tapi *apa* yang kamu lakukan sekarang. Coba minum air putih dulu segelas, dan catat apa pemicunya di jurnal nanti kalau sudah tenang. Nggak usah buru-buru." + - **Hindari:** "Kenapa bisa kambuh?", "Sayang banget streak-nya." + +**C. Berikan Satu Langkah Kecil yang Bisa Dilakukan (Actionable):** + - Setelah mendengarkan dan memvalidasi, jangan biarkan dia buntu. Tawarkan **satu saran praktis** dan SANGAT KECIL yang bisa dia lakukan SAAT INI JUGA untuk melewati momen sulit. + - Fokus pada *saat ini*, bukan rencana jangka panjang yang rumit. + - Contoh: + - "...Coba ambil napas dalam-dalam tiga kali, bisa?" + - "...Gimana kalau kamu coba tulis satu hal kecil yang kamu syukuri hari ini?" + - "...Ingat alasan utamamu, ${nickname}: kamu berjuang untuk **${reason}**." + - "...Coba alihkan pikiran sebentar, mungkin dengan cuci muka atau jalan-jalan sebentar di kamar?" + +**D. Gunakan Markdown Sederhana:** + - Gunakan **bold** untuk menekankan poin penting atau kata-kata positif (seperti **kuat**, **berhasil**, **semangat**). + - Gunakan bullet points jika memberikan 2-3 saran kecil (tapi usahakan fokus pada satu). + +**E. Ingat Percakapan:** + - Kamu akan menerima riwayat percakapan. Gunakan itu agar responsmu terasa nyambung dan tidak mengulang hal yang sama. + +# 4. TUJUAN AKHIR RESPONS +Ingat, tujuanmu bukan untuk menyelesaikan semua masalahnya dalam satu chat. Tujuanmu adalah untuk **menemaninya melewati momen sulit SAAT INI**, mengingatkannya pada kekuatannya, dan memberinya 'pegangan' kecil untuk melangkah ke menit berikutnya **tanpa kambuh**. `.trim(); } @@ -51,21 +81,24 @@ ${allJournals} # Instruksi: 1. **Temukan Tema atau Pola Emosi:** - Amati isi jurnal: apakah ada pola seperti stres, rasa syukur, kelelahan, kemajuan kecil, atau pencapaian pribadi? - - Fokus pada *emosi dominan* yang muncul berulang. + - Fokus pada *emosi dominan* yang muncul berulang. + - **Jika jurnal sangat negatif:** Jangan paksakan refleksi positif pada *kontennya*. Alihkan fokus positif pada *tindakan* pengguna, misalnya: **keberaniannya untuk jujur**, **kesadarannya** terhadap perasaannya, atau **komitmennya** untuk tetap menulis. 2. **Tulis Wawasan Singkat:** - - Buat **1 paragraf pendek (2–3 kalimat)** berisi refleksi positif. - - Awali dengan sapaan lembut seperti “Hai, aku perhatikan…” atau “Aku suka bagaimana kamu…”. - - Gunakan **bold** untuk menekankan hal-hal positif. + - Buat **1 paragraf pendek (2–3 kalimat)** berisi refleksi yang suportif dan empatik. + - Awali dengan sapaan lembut seperti “Hai, aku perhatikan…” atau “Aku baca jurnalmu, dan aku ingin bilang…”. + - Gunakan **bold** untuk menekankan hal-hal positif (baik itu kemajuan atau tindakan seperti di poin 1). + - Wawasan ini harus diakhiri dengan satu *saran refleksi* atau *tindakan kecil* yang lembut untuk hari ini, berdasarkan tema tersebut. 3. **Nada & Gaya:** - Gunakan nada hangat, lembut, dan penuh empati. - Jangan menggurui, jangan terdengar seperti robot. - Hindari nasihat medis atau pernyataan diagnosis. - - Tutup dengan kalimat penguatan ringan, seperti “Kamu sudah melangkah jauh, terus lanjutkan ya.” + - Tutup dengan kalimat penguatan ringan, seperti “Kamu sudah melangkah jauh, terus lanjutkan ya.” atau “Satu langkah kecil hari ini sudah cukup.” # Contoh Output yang Baik: -"Hai, aku perhatikan akhir-akhir ini kamu banyak menulis tentang rasa lelah, tapi juga tentang keinginan untuk terus maju. Itu luar biasa. **Kamu sudah berproses dengan baik.** Coba hari ini kasih dirimu waktu untuk bernapas sebentar, kamu pantas mendapatkannya." + - **Contoh (Jurnal Campuran):** "Hai, aku perhatikan akhir-akhir ini kamu banyak menulis tentang rasa lelah, tapi juga tentang keinginan untuk terus maju. Itu luar biasa. **Kamu sudah berproses dengan baik.** Coba hari ini kasih dirimu waktu 5 menit untuk bernapas sebentar, kamu pantas mendapatkannya." + - **Contoh (Jurnal Sangat Negatif):** "Aku baca jurnalmu hari ini. Rasanya berat ya. Tapi aku salut dengan **kejujuranmu** untuk menuangkan semua perasaan itu. Itu butuh keberanian lho. Mungkin hari ini, coba lakukan satu hal kecil yang bikin kamu nyaman, sekecil apa pun itu. Kamu nggak sendirian." Sekarang, berikan satu "Wawasan Hari Ini" berdasarkan kumpulan jurnal di atas. `.trim(); @@ -77,27 +110,73 @@ export function generateOnboardingAnalysisPrompt(answers: Record): .join('\n'); return ` -Kamu adalah seorang psikolog AI yang empatik, bijak, dan sangat baik dalam menyederhanakan konsep rumit. Tugasmu adalah menganalisis jawaban kuesioner dari seseorang yang baru memulai perjalanan pemulihan dari kecanduan pornografi dan memberikan ringkasan yang suportif seperti pada contoh. +Kamu adalah seorang psikolog AI yang empatik, bijak, dan sangat baik dalam menyederhanakan konsep rumit. Tugasmu adalah menganalisis jawaban kuesioner dari seseorang yang baru memulai perjalanan pemulihan dari kecanduan pornografi dan memberikan ringkasan yang suportif dalam format JSON yang ketat. # Jawaban Kuesioner Pengguna: ${formattedAnswers} -# Instruksi: -1. **Analisis Jawaban:** Baca semua jawaban untuk mengidentifikasi tingkat ketergantungan (Rendah, Sedang, Tinggi) dan pola utama (misalnya, penggunaan sebagai pelarian stres). -2. **Hasilkan Respons JSON:** Buat respons dalam format JSON yang valid. JSON ini harus memiliki tiga kunci utama: "title", "main_point", dan "encouragement". -3. **Gaya Bahasa:** Gunakan bahasa Indonesia yang formal namun hangat, memberdayakan, dan tidak menghakimi. +# Instruksi Utama: +1. **Format Output: HANYA JSON.** Responsmu HARUS berupa JSON yang valid. Jangan tambahkan teks, sapaan, atau penjelasan apa pun di luar blok JSON. +2. **Struktur JSON Wajib:** Gunakan struktur dengan 5 kunci berikut: + - \`level\`: (string) Satu di antara: "Rendah", "Sedang", "Tinggi". + - \`title\`: (string) Judul ringkasan yang singkat dan jelas. + - \`level_description\`: (string) Penjelasan tentang arti level tersebut bagi pengguna. + - \`pattern_analysis\`: (string) Analisis singkat tentang *pola pemicu* utama yang terlihat (misal: stres, bosan, dll). + - \`encouragement\`: (string) Kalimat penguat yang suportif dan tidak menghakimi. + +# Langkah-Langkah Analisis: -# Contoh Struktur & Isi JSON Output: +**Langkah A: Tentukan Tingkat Ketergantungan (level)** +Tentukan satu dari tiga level berdasarkan jawaban. Gunakan ini sebagai panduan: +- **Tinggi:** Jika pengguna melaporkan frekuensi tinggi (harian/hampir harian), kehilangan kendali, berdampak negatif signifikan pada pekerjaan/sosial, dan merasa gelisah/stres saat mencoba berhenti. +- **Sedang:** Jika pengguna melaporkan frekuensi cukup sering (misal, beberapa kali seminggu), mulai merasa sulit mengontrol, dan melihat *beberapa* dampak negatif ringan atau merasa bersalah setelahnya. +- **Rendah:** Jika pengguna melaporkan penggunaan sesekali, masih merasa punya kendali penuh, didorong rasa ingin tahu, dan belum ada dampak negatif signifikan yang dirasakan. + +**Langkah B: Identifikasi Pola Pemicu (pattern_analysis)** +Cari tahu *mengapa* dia menggunakannya. Apa pemicu utamanya? +- Contoh Pola: Pelarian dari **stres** atau **cemas**, pelarian dari **bosan** atau **kesepian**, bagian dari **kebiasaan** (habit) yang otomatis, atau karena **rasa ingin tahu**. + +**Langkah C: Hasilkan Teks Respons (JSON Fields)** +Isi *field* JSON (\`title\`, \`level_description\`, \`pattern_analysis\`, \`encouragement\`) sesuai dengan *level* dan *pola* yang kamu temukan. Gaya bahasa harus formal namun hangat dan memberdayakan. + +# Contoh Lengkap Output JSON (HARUS DIIKUTI): + +**Contoh 1: Level Tinggi** +\`\`\`json +{ + "level": "Tinggi", + "title": "Analisis Awal: Ketergantungan Tinggi", + "level_description": "Jawabanmu menunjukkan adanya kecenderungan tinggi terhadap ketergantungan. Hal ini bisa membuatmu sulit mengendalikan diri, merasa gelisah ketika tidak mengakses, serta mulai mengganggu fokus pada area penting kehidupan.", + "pattern_analysis": "Pola utamamu tampaknya adalah penggunaan sebagai pelarian dari stres dan emosi negatif. Ini adalah mekanisme koping yang umum terjadi.", + "encouragement": "Hasil ini tidak mendefinisikan siapa dirimu. Ini adalah langkah awal yang penting untuk sadar. Dengan kesadaran dan niat, kamu mampu mengendalikannya. Kami di sini untuk membantumu." +} +\`\`\` + +**Contoh 2: Level Sedang** +\`\`\`json +{ + "level": "Sedang", + "title": "Analisis Awal: Ketergantungan Sedang", + "level_description": "Jawabanmu mengindikasikan adanya tanda-tanda ketergantungan di tingkat sedang. Kamu mungkin mulai merasa ini menjadi kebiasaan yang sulit diubah dan terkadang mengganggu, meski belum mengambil alih hidupmu.", + "pattern_analysis": "Pola yang terlihat adalah penggunaan saat merasa bosan atau kesepian. Ini menunjukkan ada kebutuhan koneksi atau stimulasi yang coba dipenuhi.", + "encouragement": "Menyadari ini di tahap 'sedang' adalah sebuah keuntungan besar. Kamu berada di titik yang tepat untuk membangun kebiasaan baru sebelum ini menjadi lebih dalam. Langkah pertamamu sudah tepat." +} +\`\`\` +**Contoh 3: Level Rendah** +\`\`\`json { - "title": "Jawabanmu Mengindikasikan Kamu Memiliki Ketergantungan yang Tinggi Terhadap Pornografi", - "main_point": "Jawabanmu menunjukkan adanya kecenderungan tinggi terhadap ketergantungan pornografi. Hal ini bisa membuatmu sulit mengendalikan diri, merasa gelisah ketika tidak mengakses, serta mengganggu fokus belajar, pekerjaan, dan hubungan sosial.", - "encouragement": "Hasil ini tidak mendefinisikan siapa dirimu, tapi menunjukkan area yang bisa kamu perbaiki. Dengan kesadaran dan usaha, kamu mampu mengendalikannya. Kami di sini untuk membantumu." + "level": "Rendah", + "title": "Analisis Awal: Ketergantungan Rendah", + "level_description": "Berdasarkan jawabanmu, tingkat ketergantunganmu tergolong rendah. Kamu tampaknya masih memiliki kendali penuh atas perilakumu dan ini belum menunjukkan dampak negatif yang signifikan.", + "pattern_analysis": "Penggunaanmu tampaknya lebih didorong oleh kebiasaan sesekali atau rasa ingin tahu, bukan sebagai respons emosional yang mendalam.", + "encouragement": "Ini adalah posisi yang sangat baik dan ini kesempatanmu untuk proaktif. Dengan memahami pemicunya, kamu bisa dengan mudah mencegah pola ini berkembang. Teruskan kesadaran dirimu." } +\`\`\` # Penting: -1. Jangan menambahkan teks apapun di luar format JSON. -2. Pastikan output adalah JSON yang valid. +1. **Output HARUS HANYA JSON.** Jangan ada teks lain. +2. Pastikan JSON valid dan mengikuti struktur 5 kunci yang ditentukan. Sekarang, analisis jawaban pengguna dan hasilkan JSON-nya. `.trim(); diff --git a/src/api/ai/ai.service.ts b/src/api/ai/ai.service.ts index d0b9aac..363ce62 100644 --- a/src/api/ai/ai.service.ts +++ b/src/api/ai/ai.service.ts @@ -29,13 +29,45 @@ export async function getCoachResponse(userId: string, userMessage: string): Pro userWhy: user.userWhy, }); + const dbHistory = await prisma.aiChatHistory.findMany({ + where: { + userId, + }, + orderBy: { + createdAt: 'desc', + }, + take: 10, + }); + + const formattedHistory = dbHistory.reverse().map(msg => ({ + role: msg.role as 'user' | 'model', + parts: [{ text: msg.content }], + })); + // Start a chat session with the AI coach - const chat = startCoachChat(systemPrompt, user.nickname); + const chat = startCoachChat(systemPrompt, user.nickname, formattedHistory); const result = await chat.sendMessage(userMessage); - const response = result.response; + const aiResponseText = result.response.text(); + + await Promise.all([ + prisma.aiChatHistory.create({ + data: { + userId, + role: 'user', + content: userMessage, + }, + }), + prisma.aiChatHistory.create({ + data: { + userId, + role: 'model', + content: aiResponseText, + }, + }), + ]); - return response.text(); + return aiResponseText; } export async function getLatestSummary(userId: string): Promise { diff --git a/src/api/auth/auth.controller.ts b/src/api/auth/auth.controller.ts index daae541..efa622b 100644 --- a/src/api/auth/auth.controller.ts +++ b/src/api/auth/auth.controller.ts @@ -11,7 +11,7 @@ export const googleLoginHandler = asyncHandler(async (req: Request, res: Respons }); export const onboardingHandler = asyncHandler(async (req: Request, res: Response) => { - const userId = req.user?.id || req.body.userId; // Temporary support for userId in body for testing purposes + const userId = req.user?.id || req.body?.userId; // Temporary support for userId in body for testing purposes const onboardingData = req.body; if (!userId) { diff --git a/src/api/community/community.controller.ts b/src/api/community/community.controller.ts index c2c000e..32a7f7d 100644 --- a/src/api/community/community.controller.ts +++ b/src/api/community/community.controller.ts @@ -10,7 +10,7 @@ import { asyncHandler } from '../../handler/async.handler.js'; import { errorResponse, successResponse } from '../../core/response.js'; export const createPostHandler = asyncHandler(async (req: Request, res: Response) => { - const userId = req.user?.id || req.body.userId; // Temporary support for userId in body for testing purposes + const userId = req.user?.id || req.body?.userId; // Temporary support for userId in body for testing purposes const postData = req.body; if (!userId) { @@ -27,13 +27,24 @@ export const createPostHandler = asyncHandler(async (req: Request, res: Response }); export const getPostsHandler = asyncHandler(async (req: Request, res: Response) => { + const userId = req.user?.id || req.body?.userId; // Temporary support for userId in body for testing purposes const category = req.query.category as PostCategory | undefined; + + if (!userId) { + return errorResponse( + res, + 401, + 'Tidak diizinkan', + 'ID pengguna tidak ditemukan dalam permintaan' + ); + } + const posts = await findAllPosts(category); return successResponse(res, 200, 'Postingan berhasil diambil', posts); }); export const createCommentHandler = asyncHandler(async (req: Request, res: Response) => { - const userId = req.user?.id || req.body.userId; // Temporary support for userId in body for testing purposes + const userId = req.user?.id || req.body?.userId; // Temporary support for userId in body for testing purposes const { postId } = req.params; const { content } = req.body; @@ -54,7 +65,7 @@ export const createCommentHandler = asyncHandler(async (req: Request, res: Respo }); export const addLikeHandler = asyncHandler(async (req: Request, res: Response) => { - const userId = req.user?.id || req.body.userId; // Temporary support for userId in body for testing purposes + const userId = req.user?.id || req.body?.userId; // Temporary support for userId in body for testing purposes const { postId } = req.params; if (!userId) { diff --git a/src/api/journals/journal.controller.ts b/src/api/journals/journal.controller.ts index b3d7718..e5d6b0d 100644 --- a/src/api/journals/journal.controller.ts +++ b/src/api/journals/journal.controller.ts @@ -4,7 +4,7 @@ import { asyncHandler } from '../../handler/async.handler.js'; import { errorResponse, successResponse } from '../../core/response.js'; export const createJournalHandler = asyncHandler(async (req: Request, res: Response) => { - const userId = req.user?.id || req.body.userId; // Temporary support for userId in body for testing purposes + const userId = req.user?.id || req.body?.userId; // Temporary support for userId in body for testing purposes const { content } = req.body; if (!userId) { @@ -21,7 +21,7 @@ export const createJournalHandler = asyncHandler(async (req: Request, res: Respo }); export const getJournalsHandler = asyncHandler(async (req: Request, res: Response) => { - const userId = req.user?.id || req.body.userId; // Temporary support for userId in body for testing purposes + const userId = req.user?.id || req.body?.userId; // Temporary support for userId in body for testing purposes if (!userId) { return errorResponse( res, diff --git a/src/api/routine/routine.controller.ts b/src/api/routine/routine.controller.ts index e57b506..d3cebab 100644 --- a/src/api/routine/routine.controller.ts +++ b/src/api/routine/routine.controller.ts @@ -4,7 +4,7 @@ import { asyncHandler } from '../../handler/async.handler.js'; import { errorResponse, successResponse } from '../../core/response.js'; export const dailyCheckinHandler = asyncHandler(async (req: Request, res: Response) => { - const userId = req.user?.id || req.body.userId; // Temporary support for userId in body for testing purposes + const userId = req.user?.id || req.body?.userId; // Temporary support for userId in body for testing purposes const checkinData = req.body; if (!userId) { @@ -21,7 +21,7 @@ export const dailyCheckinHandler = asyncHandler(async (req: Request, res: Respon }); export const getStatisticsHandler = asyncHandler(async (req: Request, res: Response) => { - const userId = req.user?.id || req.body.userId; // Temporary support for userId in body for testing purposes + const userId = req.user?.id || req.body?.userId; // Temporary support for userId in body for testing purposes if (!userId) { return errorResponse( res, @@ -36,7 +36,7 @@ export const getStatisticsHandler = asyncHandler(async (req: Request, res: Respo }); export const getRelapsesHandler = asyncHandler(async (req: Request, res: Response) => { - const userId = req.user?.id || req.body.userId; // Temporary support for userId in body for testing purposes + const userId = req.user?.id || req.body?.userId; // Temporary support for userId in body for testing purposes if (!userId) { return errorResponse( res, diff --git a/src/api/users/user.controller.ts b/src/api/users/user.controller.ts index f41a26e..d628017 100644 --- a/src/api/users/user.controller.ts +++ b/src/api/users/user.controller.ts @@ -4,7 +4,7 @@ import { asyncHandler } from '../../handler/async.handler.js'; import { errorResponse, successResponse } from '../../core/response.js'; export const getMeHandler = asyncHandler(async (req: Request, res: Response) => { - const userId = req.user?.id || req.body.userId; // Temporary support for userId in body for testing purposes + const userId = req.user?.id || req.body?.userId; // Temporary support for userId in body for testing purposes if (!userId) { return errorResponse( res, @@ -28,7 +28,7 @@ export const getMeHandler = asyncHandler(async (req: Request, res: Response) => }); export const updateUserSettingsHandler = asyncHandler(async (req: Request, res: Response) => { - const userId = req.user?.id || req.body.userId; // Temporary support for userId in body for testing purposes + const userId = req.user?.id || req.body?.userId; // Temporary support for userId in body for testing purposes const dataToUpdate = req.body; if (!userId) { @@ -55,7 +55,7 @@ export const resetUserDataHandler = asyncHandler(async (req: Request, res: Respo // ); // } - const userId = req.user?.id || req.body.userId; // Temporary support for userId in body for testing purposes + const userId = req.user?.id || req.body?.userId; // Temporary support for userId in body for testing purposes if (!userId) { return errorResponse( res, diff --git a/src/config/index.ts b/src/config/index.ts index 72b6260..f8d67ea 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -4,6 +4,7 @@ dotenv.config(); const config = { port: process.env.PORT || 3000, + nodeEnv: process.env.NODE_ENV || 'development', databaseUrl: process.env.DATABASE_URL || '', docsUrl: process.env.DOCS_URL || '', jwt: { diff --git a/src/core/ai.ts b/src/core/ai.ts index ee33134..331c6fb 100644 --- a/src/core/ai.ts +++ b/src/core/ai.ts @@ -1,10 +1,14 @@ -import { GoogleGenerativeAI } from '@google/generative-ai'; +import { GoogleGenerativeAI, type Content } from '@google/generative-ai'; import config from '../config/index.js'; const genAI = new GoogleGenerativeAI(config.gemini.apiKey); const model = genAI.getGenerativeModel({ model: config.gemini.model }); -export function startCoachChat(systemPrompt: string, nickname: string) { +export function startCoachChat( + systemPrompt: string, + nickname: string, + chatHistory: Content[] = [] +) { return model.startChat({ history: [ { @@ -23,6 +27,7 @@ export function startCoachChat(systemPrompt: string, nickname: string) { }, ], }, + ...chatHistory, ], }); } @@ -38,11 +43,12 @@ export async function generateJsonContent(prompt: string): Promise { const result = await model.generateContent(prompt); const rawResponse = result.response.text(); + const jsonString = rawResponse + .replace(/```json/g, '') + .replace(/```/g, '') + .trim(); + try { - const jsonString = rawResponse - .replace(/```json/g, '') - .replace(/```/g, '') - .trim(); const parsedJson = JSON.parse(jsonString); return parsedJson; } catch (error) {