Spaces:
Build error
Build error
Commit ·
0d13ade
0
Parent(s):
Upload bersih ke Hugging Face
Browse files- .gitignore +12 -0
- Dockerfile +26 -0
- README.md +23 -0
- ai-backend/chat_app/__init__.py +0 -0
- ai-backend/chat_app/admin.py +6 -0
- ai-backend/chat_app/apps.py +5 -0
- ai-backend/chat_app/migrations/0001_initial.py +48 -0
- ai-backend/chat_app/migrations/__init__.py +0 -0
- ai-backend/chat_app/models.py +32 -0
- ai-backend/chat_app/tests.py +3 -0
- ai-backend/chat_app/views.py +110 -0
- ai-backend/core/__init__.py +0 -0
- ai-backend/core/asgi.py +16 -0
- ai-backend/core/settings.py +124 -0
- ai-backend/core/urls.py +10 -0
- ai-backend/core/wsgi.py +16 -0
- ai-backend/manage.py +22 -0
- ai-frontend/.gitignore +42 -0
- ai-frontend/README.md +36 -0
- ai-frontend/app/api/auth/[...nextauth]/route.js +15 -0
- ai-frontend/app/favicon.ico +0 -0
- ai-frontend/app/globals.css +26 -0
- ai-frontend/app/layout.tsx +15 -0
- ai-frontend/app/page.tsx +253 -0
- ai-frontend/components/SessionWrapper.js +7 -0
- ai-frontend/eslint.config.mjs +18 -0
- ai-frontend/next.config.mjs +13 -0
- ai-frontend/next.config.ts +7 -0
- ai-frontend/package-lock.json +0 -0
- ai-frontend/package.json +28 -0
- ai-frontend/postcss.config.mjs +7 -0
- ai-frontend/public/file.svg +1 -0
- ai-frontend/public/globe.svg +1 -0
- ai-frontend/public/next.svg +1 -0
- ai-frontend/public/vercel.svg +1 -0
- ai-frontend/public/window.svg +1 -0
- ai-frontend/tsconfig.json +34 -0
- prepare_dataset.py +93 -0
- requirements.txt +3 -0
- test_api.py +36 -0
- train_local.py +82 -0
- translate_dataset.py +89 -0
.gitignore
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
venv/
|
| 2 |
+
node_modules/
|
| 3 |
+
__pycache__/
|
| 4 |
+
*.env
|
| 5 |
+
.env.local
|
| 6 |
+
.next/
|
| 7 |
+
*.gguf
|
| 8 |
+
*.bin
|
| 9 |
+
models/
|
| 10 |
+
*.sqlite3
|
| 11 |
+
db.sqlite3
|
| 12 |
+
ai-backend/db.sqlite3
|
Dockerfile
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.13-slim
|
| 2 |
+
|
| 3 |
+
# Aturan wajib Hugging Face: Buat user dengan ID 1000
|
| 4 |
+
RUN useradd -m -u 1000 user
|
| 5 |
+
|
| 6 |
+
# Install compiler C++ untuk menjalankan llama.cpp
|
| 7 |
+
RUN apt-get update && apt-get install -y build-essential gcc g++
|
| 8 |
+
|
| 9 |
+
# Gunakan user yang baru dibuat
|
| 10 |
+
USER user
|
| 11 |
+
ENV PATH="/home/user/.local/bin:$PATH"
|
| 12 |
+
|
| 13 |
+
WORKDIR /app
|
| 14 |
+
|
| 15 |
+
# Copy requirements dan install library Python
|
| 16 |
+
COPY --chown=user ./requirements.txt requirements.txt
|
| 17 |
+
RUN pip install --no-cache-dir --upgrade -r requirements.txt
|
| 18 |
+
|
| 19 |
+
# Copy seluruh file project dari laptop Anda ke server HF
|
| 20 |
+
COPY --chown=user . /app
|
| 21 |
+
|
| 22 |
+
# Buka pintu komunikasi wajib Hugging Face
|
| 23 |
+
EXPOSE 7860
|
| 24 |
+
|
| 25 |
+
# Jalankan mesin Django di port 7860
|
| 26 |
+
CMD ["python", "manage.py", "runserver", "0.0.0.0:7860"]
|
README.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🤖 Proyek AI_Kan (Local Qwen 3B Chatbot)
|
| 2 |
+
|
| 3 |
+
Proyek ini adalah aplikasi *Full-Stack* AI Chatbot yang berjalan sepenuhnya secara lokal menggunakan GPU (GTX). Aplikasi ini memiliki fitur memori obrolan (history), sistem *login* menggunakan akun Google (NextAuth), dan panel admin untuk mengatur kepribadian AI secara spesifik untuk setiap pengguna.
|
| 4 |
+
|
| 5 |
+
## 🏗️ Arsitektur Proyek
|
| 6 |
+
* **Backend:** Django (Python) + `llama_cpp` (Pemroses AI) + SQLite
|
| 7 |
+
* **Frontend:** Next.js (App Router) + Tailwind CSS + NextAuth.js
|
| 8 |
+
* **Model AI:** Qwen 2.5 3B Instruct (Format GGUF)
|
| 9 |
+
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
## 🚀 Cara Menjalankan Proyek (Local Development)
|
| 13 |
+
|
| 14 |
+
Karena proyek ini terpisah antara *Backend* dan *Frontend*, Anda harus membuka **dua terminal** secara bersamaan.
|
| 15 |
+
|
| 16 |
+
### Tahap 1: Menyalakan Mesin AI (Backend Django)
|
| 17 |
+
1. Buka terminal (PowerShell/CMD) dan arahkan ke folder utama proyek (`D:\Project\AI_Kan`).
|
| 18 |
+
2. Aktifkan *Virtual Environment*:
|
| 19 |
+
.\venv\Scripts\activate
|
| 20 |
+
python manage.py runserver
|
| 21 |
+
3. Menyalakan Antarmuka Web (Frontend Next.js):
|
| 22 |
+
cd ai-frontend
|
| 23 |
+
npm run dev
|
ai-backend/chat_app/__init__.py
ADDED
|
File without changes
|
ai-backend/chat_app/admin.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.contrib import admin
|
| 2 |
+
from .models import UserProfile, ChatSession, Message
|
| 3 |
+
|
| 4 |
+
admin.site.register(UserProfile)
|
| 5 |
+
admin.site.register(ChatSession)
|
| 6 |
+
admin.site.register(Message)
|
ai-backend/chat_app/apps.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.apps import AppConfig
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class ChatAppConfig(AppConfig):
|
| 5 |
+
name = 'chat_app'
|
ai-backend/chat_app/migrations/0001_initial.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Generated by Django 6.0.2 on 2026-02-23 15:42
|
| 2 |
+
|
| 3 |
+
import django.db.models.deletion
|
| 4 |
+
import uuid
|
| 5 |
+
from django.conf import settings
|
| 6 |
+
from django.db import migrations, models
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class Migration(migrations.Migration):
|
| 10 |
+
|
| 11 |
+
initial = True
|
| 12 |
+
|
| 13 |
+
dependencies = [
|
| 14 |
+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
| 15 |
+
]
|
| 16 |
+
|
| 17 |
+
operations = [
|
| 18 |
+
migrations.CreateModel(
|
| 19 |
+
name='ChatSession',
|
| 20 |
+
fields=[
|
| 21 |
+
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
| 22 |
+
('title', models.CharField(default='Percakapan Baru', max_length=255)),
|
| 23 |
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
| 24 |
+
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='chat_sessions', to=settings.AUTH_USER_MODEL)),
|
| 25 |
+
],
|
| 26 |
+
),
|
| 27 |
+
migrations.CreateModel(
|
| 28 |
+
name='Message',
|
| 29 |
+
fields=[
|
| 30 |
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
| 31 |
+
('role', models.CharField(max_length=10)),
|
| 32 |
+
('content', models.TextField()),
|
| 33 |
+
('timestamp', models.DateTimeField(auto_now_add=True)),
|
| 34 |
+
('session', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='chat_app.chatsession')),
|
| 35 |
+
],
|
| 36 |
+
options={
|
| 37 |
+
'ordering': ['timestamp'],
|
| 38 |
+
},
|
| 39 |
+
),
|
| 40 |
+
migrations.CreateModel(
|
| 41 |
+
name='UserProfile',
|
| 42 |
+
fields=[
|
| 43 |
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
| 44 |
+
('system_prompt', models.TextField(default='Kamu adalah asisten AI berbahasa Indonesia yang cerdas, ramah, dan sopan.')),
|
| 45 |
+
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
| 46 |
+
],
|
| 47 |
+
),
|
| 48 |
+
]
|
ai-backend/chat_app/migrations/__init__.py
ADDED
|
File without changes
|
ai-backend/chat_app/models.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import uuid
|
| 2 |
+
from django.db import models
|
| 3 |
+
from django.contrib.auth.models import User
|
| 4 |
+
|
| 5 |
+
# 1. Tabel Profil untuk mengatur Kepribadian AI per User
|
| 6 |
+
class UserProfile(models.Model):
|
| 7 |
+
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
| 8 |
+
# Default sifat AI, bisa diedit oleh Admin di panel Django
|
| 9 |
+
system_prompt = models.TextField(default="Kamu adalah asisten AI berbahasa Indonesia yang cerdas, ramah, dan sopan.")
|
| 10 |
+
|
| 11 |
+
def __str__(self):
|
| 12 |
+
return f"Pengaturan AI untuk: {self.user.username}"
|
| 13 |
+
|
| 14 |
+
# 2. Tabel Sesi Obrolan (Riwayat per User)
|
| 15 |
+
class ChatSession(models.Model):
|
| 16 |
+
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
| 17 |
+
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='chat_sessions')
|
| 18 |
+
title = models.CharField(max_length=255, default="Percakapan Baru")
|
| 19 |
+
created_at = models.DateTimeField(auto_now_add=True)
|
| 20 |
+
|
| 21 |
+
def __str__(self):
|
| 22 |
+
return f"{self.user.username} - {self.title}"
|
| 23 |
+
|
| 24 |
+
# 3. Tabel Pesan Obrolan
|
| 25 |
+
class Message(models.Model):
|
| 26 |
+
session = models.ForeignKey(ChatSession, on_delete=models.CASCADE, related_name='messages')
|
| 27 |
+
role = models.CharField(max_length=10) # 'user' atau 'assistant'
|
| 28 |
+
content = models.TextField()
|
| 29 |
+
timestamp = models.DateTimeField(auto_now_add=True)
|
| 30 |
+
|
| 31 |
+
class Meta:
|
| 32 |
+
ordering = ['timestamp'] # Urutkan dari yang terlama ke terbaru
|
ai-backend/chat_app/tests.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.test import TestCase
|
| 2 |
+
|
| 3 |
+
# Create your tests here.
|
ai-backend/chat_app/views.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.http import JsonResponse
|
| 2 |
+
from django.views.decorators.csrf import csrf_exempt
|
| 3 |
+
from django.contrib.auth.models import User
|
| 4 |
+
from .models import ChatSession, Message, UserProfile
|
| 5 |
+
from llama_cpp import Llama
|
| 6 |
+
import json
|
| 7 |
+
import os
|
| 8 |
+
|
| 9 |
+
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 10 |
+
MODEL_PATH = os.path.join(BASE_DIR, 'chat_app', 'models', 'qwen2.5-3b-instruct-q5_k_m.gguf')
|
| 11 |
+
|
| 12 |
+
print("Sedang memuat model AI ke dalam memori... Mohon tunggu.")
|
| 13 |
+
try:
|
| 14 |
+
llm = Llama(
|
| 15 |
+
model_path=MODEL_PATH,
|
| 16 |
+
n_ctx=2048, # Diperbesar untuk menampung riwayat chat
|
| 17 |
+
n_gpu_layers=-1
|
| 18 |
+
)
|
| 19 |
+
print("Model AI berhasil dimuat!")
|
| 20 |
+
except Exception as e:
|
| 21 |
+
print(f"Gagal memuat model: {e}")
|
| 22 |
+
|
| 23 |
+
@csrf_exempt
|
| 24 |
+
def api_chat(request):
|
| 25 |
+
if request.method == 'POST':
|
| 26 |
+
try:
|
| 27 |
+
data = json.loads(request.body)
|
| 28 |
+
user_message = data.get('message', '')
|
| 29 |
+
session_id = data.get('session_id')
|
| 30 |
+
user_email = data.get('email') # Dari login Google di React
|
| 31 |
+
|
| 32 |
+
if not user_message or not user_email:
|
| 33 |
+
return JsonResponse({'error': 'Pesan dan Email wajib ada'}, status=400)
|
| 34 |
+
|
| 35 |
+
# 1. Cari atau Buat User & Profilnya (Berdasarkan email Gmail)
|
| 36 |
+
user, _ = User.objects.get_or_create(username=user_email, email=user_email)
|
| 37 |
+
user_profile, _ = UserProfile.objects.get_or_create(user=user)
|
| 38 |
+
|
| 39 |
+
# 2. Cari atau Buat Sesi Chat (Isolasi antar user)
|
| 40 |
+
if session_id:
|
| 41 |
+
session = ChatSession.objects.get(id=session_id, user=user)
|
| 42 |
+
else:
|
| 43 |
+
session = ChatSession.objects.create(user=user, title=user_message[:30])
|
| 44 |
+
|
| 45 |
+
# 3. Simpan pesan User
|
| 46 |
+
Message.objects.create(session=session, role='user', content=user_message)
|
| 47 |
+
|
| 48 |
+
# 4. Ambil 4 riwayat pesan terakhir (Agar VRAM tidak kepenuhan)
|
| 49 |
+
past_messages = session.messages.all().order_by('-timestamp')[:4]
|
| 50 |
+
past_messages = reversed(past_messages)
|
| 51 |
+
|
| 52 |
+
# 5. Susun Prompt dengan KEPRIBADIAN DARI ADMIN + RIWAYAT CHAT
|
| 53 |
+
prompt = f"<|im_start|>system\n{user_profile.system_prompt}<|im_end|>\n"
|
| 54 |
+
for msg in past_messages:
|
| 55 |
+
prompt += f"<|im_start|>{msg.role}\n{msg.content}<|im_end|>\n"
|
| 56 |
+
prompt += "<|im_start|>assistant\n"
|
| 57 |
+
|
| 58 |
+
# 6. Proses ke Model Qwen
|
| 59 |
+
output = llm(
|
| 60 |
+
prompt, max_tokens=256, stop=["<|im_end|>"], echo=False
|
| 61 |
+
)
|
| 62 |
+
ai_response = output['choices'][0]['text'].strip()
|
| 63 |
+
|
| 64 |
+
# 7. Simpan balasan AI
|
| 65 |
+
Message.objects.create(session=session, role='assistant', content=ai_response)
|
| 66 |
+
|
| 67 |
+
return JsonResponse({
|
| 68 |
+
'status': 'success',
|
| 69 |
+
'session_id': str(session.id),
|
| 70 |
+
'response': ai_response
|
| 71 |
+
})
|
| 72 |
+
|
| 73 |
+
except Exception as e:
|
| 74 |
+
return JsonResponse({'error': str(e)}, status=500)
|
| 75 |
+
|
| 76 |
+
return JsonResponse({'error': 'Hanya menerima POST'}, status=405)
|
| 77 |
+
|
| 78 |
+
# Tambahkan di paling bawah views.py
|
| 79 |
+
@csrf_exempt
|
| 80 |
+
def get_history(request):
|
| 81 |
+
"""Mengambil daftar judul riwayat chat milik user"""
|
| 82 |
+
if request.method == 'GET':
|
| 83 |
+
email = request.GET.get('email')
|
| 84 |
+
if not email:
|
| 85 |
+
return JsonResponse({'error': 'Email wajib ada'}, status=400)
|
| 86 |
+
|
| 87 |
+
try:
|
| 88 |
+
user = User.objects.get(email=email)
|
| 89 |
+
# Ambil semua sesi urut dari yang paling baru
|
| 90 |
+
sessions = ChatSession.objects.filter(user=user).order_by('-created_at')
|
| 91 |
+
data = [{'id': str(s.id), 'title': s.title} for s in sessions]
|
| 92 |
+
return JsonResponse({'status': 'success', 'data': data})
|
| 93 |
+
except User.DoesNotExist:
|
| 94 |
+
return JsonResponse({'status': 'success', 'data': []})
|
| 95 |
+
|
| 96 |
+
return JsonResponse({'error': 'Hanya menerima GET'}, status=405)
|
| 97 |
+
|
| 98 |
+
@csrf_exempt
|
| 99 |
+
def get_session_messages(request, session_id):
|
| 100 |
+
"""Mengambil isi pesan saat riwayat di-klik"""
|
| 101 |
+
if request.method == 'GET':
|
| 102 |
+
try:
|
| 103 |
+
session = ChatSession.objects.get(id=session_id)
|
| 104 |
+
messages = session.messages.all().order_by('timestamp')
|
| 105 |
+
data = [{'role': m.role, 'content': m.content} for m in messages]
|
| 106 |
+
return JsonResponse({'status': 'success', 'data': {'messages': data}})
|
| 107 |
+
except ChatSession.DoesNotExist:
|
| 108 |
+
return JsonResponse({'error': 'Sesi tidak ditemukan'}, status=404)
|
| 109 |
+
|
| 110 |
+
return JsonResponse({'error': 'Hanya menerima GET'}, status=405)
|
ai-backend/core/__init__.py
ADDED
|
File without changes
|
ai-backend/core/asgi.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
ASGI config for core project.
|
| 3 |
+
|
| 4 |
+
It exposes the ASGI callable as a module-level variable named ``application``.
|
| 5 |
+
|
| 6 |
+
For more information on this file, see
|
| 7 |
+
https://docs.djangoproject.com/en/6.0/howto/deployment/asgi/
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import os
|
| 11 |
+
|
| 12 |
+
from django.core.asgi import get_asgi_application
|
| 13 |
+
|
| 14 |
+
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
|
| 15 |
+
|
| 16 |
+
application = get_asgi_application()
|
ai-backend/core/settings.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Django settings for core project.
|
| 3 |
+
|
| 4 |
+
Generated by 'django-admin startproject' using Django 6.0.2.
|
| 5 |
+
|
| 6 |
+
For more information on this file, see
|
| 7 |
+
https://docs.djangoproject.com/en/6.0/topics/settings/
|
| 8 |
+
|
| 9 |
+
For the full list of settings and their values, see
|
| 10 |
+
https://docs.djangoproject.com/en/6.0/ref/settings/
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
|
| 15 |
+
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
| 16 |
+
BASE_DIR = Path(__file__).resolve().parent.parent
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
# Quick-start development settings - unsuitable for production
|
| 20 |
+
# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/
|
| 21 |
+
|
| 22 |
+
# SECURITY WARNING: keep the secret key used in production secret!
|
| 23 |
+
SECRET_KEY = 'django-insecure--_an^du4%(4rng$^18e-u)fu-x@&kepr3t7*qx615l@*bg1*hn'
|
| 24 |
+
|
| 25 |
+
# SECURITY WARNING: don't run with debug turned on in production!
|
| 26 |
+
DEBUG = True
|
| 27 |
+
|
| 28 |
+
ALLOWED_HOSTS = []
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
# Application definition
|
| 32 |
+
|
| 33 |
+
INSTALLED_APPS = [
|
| 34 |
+
'django.contrib.admin',
|
| 35 |
+
'django.contrib.auth',
|
| 36 |
+
'django.contrib.contenttypes',
|
| 37 |
+
'django.contrib.sessions',
|
| 38 |
+
'django.contrib.messages',
|
| 39 |
+
'django.contrib.staticfiles',
|
| 40 |
+
'corsheaders',
|
| 41 |
+
'chat_app',
|
| 42 |
+
]
|
| 43 |
+
|
| 44 |
+
MIDDLEWARE = [
|
| 45 |
+
'corsheaders.middleware.CorsMiddleware',
|
| 46 |
+
'django.middleware.security.SecurityMiddleware',
|
| 47 |
+
'django.contrib.sessions.middleware.SessionMiddleware',
|
| 48 |
+
'django.middleware.common.CommonMiddleware',
|
| 49 |
+
'django.middleware.csrf.CsrfViewMiddleware',
|
| 50 |
+
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
| 51 |
+
'django.contrib.messages.middleware.MessageMiddleware',
|
| 52 |
+
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
| 53 |
+
]
|
| 54 |
+
|
| 55 |
+
ROOT_URLCONF = 'core.urls'
|
| 56 |
+
|
| 57 |
+
TEMPLATES = [
|
| 58 |
+
{
|
| 59 |
+
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
| 60 |
+
'DIRS': [],
|
| 61 |
+
'APP_DIRS': True,
|
| 62 |
+
'OPTIONS': {
|
| 63 |
+
'context_processors': [
|
| 64 |
+
'django.template.context_processors.request',
|
| 65 |
+
'django.contrib.auth.context_processors.auth',
|
| 66 |
+
'django.contrib.messages.context_processors.messages',
|
| 67 |
+
],
|
| 68 |
+
},
|
| 69 |
+
},
|
| 70 |
+
]
|
| 71 |
+
|
| 72 |
+
WSGI_APPLICATION = 'core.wsgi.application'
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
# Database
|
| 76 |
+
# https://docs.djangoproject.com/en/6.0/ref/settings/#databases
|
| 77 |
+
|
| 78 |
+
DATABASES = {
|
| 79 |
+
'default': {
|
| 80 |
+
'ENGINE': 'django.db.backends.sqlite3',
|
| 81 |
+
'NAME': BASE_DIR / 'db.sqlite3',
|
| 82 |
+
}
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
# Password validation
|
| 87 |
+
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
|
| 88 |
+
|
| 89 |
+
AUTH_PASSWORD_VALIDATORS = [
|
| 90 |
+
{
|
| 91 |
+
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
| 92 |
+
},
|
| 93 |
+
{
|
| 94 |
+
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
| 95 |
+
},
|
| 96 |
+
{
|
| 97 |
+
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
| 98 |
+
},
|
| 99 |
+
{
|
| 100 |
+
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
| 101 |
+
},
|
| 102 |
+
]
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
# Internationalization
|
| 106 |
+
# https://docs.djangoproject.com/en/6.0/topics/i18n/
|
| 107 |
+
|
| 108 |
+
LANGUAGE_CODE = 'en-us'
|
| 109 |
+
|
| 110 |
+
TIME_ZONE = 'UTC'
|
| 111 |
+
|
| 112 |
+
USE_I18N = True
|
| 113 |
+
|
| 114 |
+
USE_TZ = True
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
# Static files (CSS, JavaScript, Images)
|
| 118 |
+
# https://docs.djangoproject.com/en/6.0/howto/static-files/
|
| 119 |
+
|
| 120 |
+
STATIC_URL = 'static/'
|
| 121 |
+
|
| 122 |
+
CORS_ALLOW_ALL_ORIGINS = True
|
| 123 |
+
|
| 124 |
+
APPEND_SLASH = False
|
ai-backend/core/urls.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.contrib import admin
|
| 2 |
+
from django.urls import path
|
| 3 |
+
from chat_app.views import api_chat, get_history, get_session_messages
|
| 4 |
+
|
| 5 |
+
urlpatterns = [
|
| 6 |
+
path('admin/', admin.site.urls),
|
| 7 |
+
path('api/chat', api_chat, name='api_chat'),
|
| 8 |
+
path('api/history', get_history, name='get_history'),
|
| 9 |
+
path('api/history/<uuid:session_id>', get_session_messages, name='get_session_messages'),
|
| 10 |
+
]
|
ai-backend/core/wsgi.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
WSGI config for core project.
|
| 3 |
+
|
| 4 |
+
It exposes the WSGI callable as a module-level variable named ``application``.
|
| 5 |
+
|
| 6 |
+
For more information on this file, see
|
| 7 |
+
https://docs.djangoproject.com/en/6.0/howto/deployment/wsgi/
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import os
|
| 11 |
+
|
| 12 |
+
from django.core.wsgi import get_wsgi_application
|
| 13 |
+
|
| 14 |
+
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
|
| 15 |
+
|
| 16 |
+
application = get_wsgi_application()
|
ai-backend/manage.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
"""Django's command-line utility for administrative tasks."""
|
| 3 |
+
import os
|
| 4 |
+
import sys
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def main():
|
| 8 |
+
"""Run administrative tasks."""
|
| 9 |
+
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
|
| 10 |
+
try:
|
| 11 |
+
from django.core.management import execute_from_command_line
|
| 12 |
+
except ImportError as exc:
|
| 13 |
+
raise ImportError(
|
| 14 |
+
"Couldn't import Django. Are you sure it's installed and "
|
| 15 |
+
"available on your PYTHONPATH environment variable? Did you "
|
| 16 |
+
"forget to activate a virtual environment?"
|
| 17 |
+
) from exc
|
| 18 |
+
execute_from_command_line(sys.argv)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
if __name__ == '__main__':
|
| 22 |
+
main()
|
ai-frontend/.gitignore
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
| 2 |
+
|
| 3 |
+
# dependencies
|
| 4 |
+
/node_modules
|
| 5 |
+
/.pnp
|
| 6 |
+
.pnp.*
|
| 7 |
+
.yarn/*
|
| 8 |
+
!.yarn/patches
|
| 9 |
+
!.yarn/plugins
|
| 10 |
+
!.yarn/releases
|
| 11 |
+
!.yarn/versions
|
| 12 |
+
|
| 13 |
+
# testing
|
| 14 |
+
/coverage
|
| 15 |
+
|
| 16 |
+
# next.js
|
| 17 |
+
/.next/
|
| 18 |
+
/out/
|
| 19 |
+
|
| 20 |
+
# production
|
| 21 |
+
/build
|
| 22 |
+
|
| 23 |
+
# misc
|
| 24 |
+
.DS_Store
|
| 25 |
+
*.pem
|
| 26 |
+
|
| 27 |
+
# debug
|
| 28 |
+
npm-debug.log*
|
| 29 |
+
yarn-debug.log*
|
| 30 |
+
yarn-error.log*
|
| 31 |
+
.pnpm-debug.log*
|
| 32 |
+
|
| 33 |
+
# env files (can opt-in for committing if needed)
|
| 34 |
+
.env*
|
| 35 |
+
.env.local
|
| 36 |
+
|
| 37 |
+
# vercel
|
| 38 |
+
.vercel
|
| 39 |
+
|
| 40 |
+
# typescript
|
| 41 |
+
*.tsbuildinfo
|
| 42 |
+
next-env.d.ts
|
ai-frontend/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
| 2 |
+
|
| 3 |
+
## Getting Started
|
| 4 |
+
|
| 5 |
+
First, run the development server:
|
| 6 |
+
|
| 7 |
+
```bash
|
| 8 |
+
npm run dev
|
| 9 |
+
# or
|
| 10 |
+
yarn dev
|
| 11 |
+
# or
|
| 12 |
+
pnpm dev
|
| 13 |
+
# or
|
| 14 |
+
bun dev
|
| 15 |
+
```
|
| 16 |
+
|
| 17 |
+
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
| 18 |
+
|
| 19 |
+
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
| 20 |
+
|
| 21 |
+
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
| 22 |
+
|
| 23 |
+
## Learn More
|
| 24 |
+
|
| 25 |
+
To learn more about Next.js, take a look at the following resources:
|
| 26 |
+
|
| 27 |
+
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
| 28 |
+
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
| 29 |
+
|
| 30 |
+
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
| 31 |
+
|
| 32 |
+
## Deploy on Vercel
|
| 33 |
+
|
| 34 |
+
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
| 35 |
+
|
| 36 |
+
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
ai-frontend/app/api/auth/[...nextauth]/route.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// File: app/api/auth/[...nextauth]/route.js
|
| 2 |
+
import NextAuth from "next-auth";
|
| 3 |
+
import GoogleProvider from "next-auth/providers/google";
|
| 4 |
+
|
| 5 |
+
const handler = NextAuth({
|
| 6 |
+
providers: [
|
| 7 |
+
GoogleProvider({
|
| 8 |
+
clientId: process.env.GOOGLE_CLIENT_ID,
|
| 9 |
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
| 10 |
+
}),
|
| 11 |
+
],
|
| 12 |
+
secret: process.env.NEXTAUTH_SECRET,
|
| 13 |
+
});
|
| 14 |
+
|
| 15 |
+
export { handler as GET, handler as POST };
|
ai-frontend/app/favicon.ico
ADDED
|
|
ai-frontend/app/globals.css
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@import "tailwindcss";
|
| 2 |
+
|
| 3 |
+
:root {
|
| 4 |
+
--background: #ffffff;
|
| 5 |
+
--foreground: #171717;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
@theme inline {
|
| 9 |
+
--color-background: var(--background);
|
| 10 |
+
--color-foreground: var(--foreground);
|
| 11 |
+
--font-sans: var(--font-geist-sans);
|
| 12 |
+
--font-mono: var(--font-geist-mono);
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
@media (prefers-color-scheme: dark) {
|
| 16 |
+
:root {
|
| 17 |
+
--background: #0a0a0a;
|
| 18 |
+
--foreground: #ededed;
|
| 19 |
+
}
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
body {
|
| 23 |
+
background: var(--background);
|
| 24 |
+
color: var(--foreground);
|
| 25 |
+
font-family: Arial, Helvetica, sans-serif;
|
| 26 |
+
}
|
ai-frontend/app/layout.tsx
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// File: app/layout.js
|
| 2 |
+
import "./globals.css";
|
| 3 |
+
import SessionWrapper from "../components/SessionWrapper";
|
| 4 |
+
|
| 5 |
+
export const metadata = { title: "Kans AI" };
|
| 6 |
+
|
| 7 |
+
export default function RootLayout({ children }) {
|
| 8 |
+
return (
|
| 9 |
+
<html lang="id">
|
| 10 |
+
<body className="bg-[#131314] text-white">
|
| 11 |
+
<SessionWrapper>{children}</SessionWrapper>
|
| 12 |
+
</body>
|
| 13 |
+
</html>
|
| 14 |
+
);
|
| 15 |
+
}
|
ai-frontend/app/page.tsx
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
import React, { useState, useRef, useEffect } from "react";
|
| 3 |
+
import { useSession, signIn, signOut } from "next-auth/react";
|
| 4 |
+
import { Plus, Menu, MessageSquare, X } from "lucide-react"; // Tambahkan icon X
|
| 5 |
+
|
| 6 |
+
export default function Home() {
|
| 7 |
+
const { data: session } = useSession();
|
| 8 |
+
const [messages, setMessages] = useState([]);
|
| 9 |
+
const [input, setInput] = useState("");
|
| 10 |
+
const [isLoading, setIsLoading] = useState(false);
|
| 11 |
+
const [sessionId, setSessionId] = useState(null);
|
| 12 |
+
|
| 13 |
+
const [chatHistory, setChatHistory] = useState([]);
|
| 14 |
+
|
| 15 |
+
// State baru KHUSUS untuk mengatur menu sidebar di Mobile
|
| 16 |
+
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
| 17 |
+
|
| 18 |
+
const messagesEndRef = useRef(null);
|
| 19 |
+
const scrollToBottom = () => {
|
| 20 |
+
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
| 21 |
+
};
|
| 22 |
+
useEffect(() => { scrollToBottom(); }, [messages]);
|
| 23 |
+
|
| 24 |
+
useEffect(() => {
|
| 25 |
+
if (session?.user?.email) {
|
| 26 |
+
fetchHistory();
|
| 27 |
+
}
|
| 28 |
+
}, [session]);
|
| 29 |
+
|
| 30 |
+
const fetchHistory = async () => {
|
| 31 |
+
try {
|
| 32 |
+
const response = await fetch(`/api/django/history?email=${session.user.email}`);
|
| 33 |
+
const data = await response.json();
|
| 34 |
+
if (data.status === "success") {
|
| 35 |
+
setChatHistory(data.data);
|
| 36 |
+
}
|
| 37 |
+
} catch (error) {
|
| 38 |
+
console.error("Gagal mengambil riwayat", error);
|
| 39 |
+
}
|
| 40 |
+
};
|
| 41 |
+
|
| 42 |
+
// Fungsi saat user mengklik salah satu riwayat di sidebar
|
| 43 |
+
const loadSession = async (id) => {
|
| 44 |
+
try {
|
| 45 |
+
const response = await fetch(`/api/django/history/${id}`);
|
| 46 |
+
const data = await response.json();
|
| 47 |
+
if (data.status === "success") {
|
| 48 |
+
setSessionId(id);
|
| 49 |
+
setMessages(data.data.messages);
|
| 50 |
+
}
|
| 51 |
+
} catch (error) {
|
| 52 |
+
console.error("Gagal memuat sesi", error);
|
| 53 |
+
}
|
| 54 |
+
};
|
| 55 |
+
|
| 56 |
+
const sendMessage = async (e) => {
|
| 57 |
+
e.preventDefault();
|
| 58 |
+
if (!input.trim() || !session) return;
|
| 59 |
+
|
| 60 |
+
const userMsg = input.trim();
|
| 61 |
+
setMessages((prev) => [...prev, { role: "user", content: userMsg }]);
|
| 62 |
+
setInput("");
|
| 63 |
+
setIsLoading(true);
|
| 64 |
+
|
| 65 |
+
try {
|
| 66 |
+
const response = await fetch("/api/django/chat", {
|
| 67 |
+
method: "POST",
|
| 68 |
+
headers: { "Content-Type": "application/json" },
|
| 69 |
+
body: JSON.stringify({
|
| 70 |
+
message: userMsg,
|
| 71 |
+
email: session.user.email,
|
| 72 |
+
session_id: sessionId,
|
| 73 |
+
}),
|
| 74 |
+
});
|
| 75 |
+
|
| 76 |
+
const data = await response.json();
|
| 77 |
+
if (data.status === "success") {
|
| 78 |
+
// Jika ini adalah chat pertama (sebelumnya tidak ada sessionId), refresh riwayat di sidebar
|
| 79 |
+
if (!sessionId) {
|
| 80 |
+
fetchHistory();
|
| 81 |
+
}
|
| 82 |
+
setSessionId(data.session_id);
|
| 83 |
+
setMessages((prev) => [...prev, { role: "assistant", content: data.response }]);
|
| 84 |
+
} else {
|
| 85 |
+
console.error("Error dari Django:", data.error);
|
| 86 |
+
}
|
| 87 |
+
} catch (error) {
|
| 88 |
+
console.error("Gagal terhubung ke server Django", error);
|
| 89 |
+
setMessages((prev) => [...prev, { role: "assistant", content: "Maaf, gagal terhubung ke server lokal." }]);
|
| 90 |
+
} finally {
|
| 91 |
+
setIsLoading(false);
|
| 92 |
+
}
|
| 93 |
+
};
|
| 94 |
+
|
| 95 |
+
const startNewChat = () => {
|
| 96 |
+
setMessages([]);
|
| 97 |
+
setSessionId(null);
|
| 98 |
+
setIsSidebarOpen(false); // Tutup sidebar di HP saat buat chat baru
|
| 99 |
+
};
|
| 100 |
+
|
| 101 |
+
const isChatEmpty = messages.length === 0;
|
| 102 |
+
|
| 103 |
+
return (
|
| 104 |
+
<div className="flex h-screen font-sans bg-[#131314] overflow-hidden">
|
| 105 |
+
|
| 106 |
+
{/* Overlay Hitam Transparan untuk Mobile saat Sidebar terbuka */}
|
| 107 |
+
{isSidebarOpen && (
|
| 108 |
+
<div
|
| 109 |
+
className="fixed inset-0 bg-black/60 z-40 md:hidden transition-opacity"
|
| 110 |
+
onClick={() => setIsSidebarOpen(false)}
|
| 111 |
+
/>
|
| 112 |
+
)}
|
| 113 |
+
|
| 114 |
+
{/* Sidebar - Diubah agar responsif (Slide di HP, Tetap di Laptop) */}
|
| 115 |
+
{session && (
|
| 116 |
+
<aside
|
| 117 |
+
className={`fixed md:relative z-50 inset-y-0 left-0 w-64 bg-[#1e1f20] p-4 flex flex-col border-r border-gray-800 transition-transform duration-300 ease-in-out md:translate-x-0 ${
|
| 118 |
+
isSidebarOpen ? "translate-x-0" : "-translate-x-full"
|
| 119 |
+
}`}
|
| 120 |
+
>
|
| 121 |
+
{/* Tombol Close Menu (Khusus Mobile) */}
|
| 122 |
+
<div className="flex justify-between items-center mb-6 md:hidden">
|
| 123 |
+
<span className="text-gray-300 font-medium">Menu Navigasi</span>
|
| 124 |
+
<button onClick={() => setIsSidebarOpen(false)} className="text-gray-400 hover:text-white">
|
| 125 |
+
<X size={24} />
|
| 126 |
+
</button>
|
| 127 |
+
</div>
|
| 128 |
+
|
| 129 |
+
<button
|
| 130 |
+
onClick={startNewChat}
|
| 131 |
+
className="flex items-center gap-3 bg-[#131314] hover:bg-gray-800 p-3 rounded-full transition w-full md:w-fit border border-gray-700"
|
| 132 |
+
>
|
| 133 |
+
<Plus size={20} className="text-gray-200" />
|
| 134 |
+
<span className="font-medium text-sm text-gray-200">Chat baru</span>
|
| 135 |
+
</button>
|
| 136 |
+
|
| 137 |
+
{/* Tampilan Daftar Riwayat Percakapan */}
|
| 138 |
+
<div className="mt-8 flex-1 overflow-y-auto pr-1">
|
| 139 |
+
<p className="text-xs text-gray-500 font-semibold mb-3 px-2">Riwayat Percakapan</p>
|
| 140 |
+
<div className="flex flex-col gap-1">
|
| 141 |
+
{chatHistory.map((chat) => (
|
| 142 |
+
<button
|
| 143 |
+
key={chat.id}
|
| 144 |
+
onClick={() => loadSession(chat.id)}
|
| 145 |
+
className={`flex items-center gap-3 text-left px-3 py-2.5 rounded-lg text-sm transition-colors w-full ${
|
| 146 |
+
sessionId === chat.id
|
| 147 |
+
? 'bg-[#2a2b2c] text-gray-200'
|
| 148 |
+
: 'text-gray-400 hover:bg-[#2a2b2c] hover:text-gray-200'
|
| 149 |
+
}`}
|
| 150 |
+
>
|
| 151 |
+
<MessageSquare size={16} className="min-w-fit" />
|
| 152 |
+
<span className="truncate">{chat.title}</span>
|
| 153 |
+
</button>
|
| 154 |
+
))}
|
| 155 |
+
</div>
|
| 156 |
+
</div>
|
| 157 |
+
</aside>
|
| 158 |
+
)}
|
| 159 |
+
|
| 160 |
+
{/* Main Content Area */}
|
| 161 |
+
<main className="flex-1 flex flex-col relative w-full">
|
| 162 |
+
<header className="h-16 flex items-center justify-between px-4 md:px-6 pt-2">
|
| 163 |
+
<div className="flex items-center gap-3">
|
| 164 |
+
{/* Tombol Hamburger di HP untuk membuka Sidebar */}
|
| 165 |
+
<Menu
|
| 166 |
+
className="md:hidden cursor-pointer text-gray-400 hover:text-white"
|
| 167 |
+
onClick={() => setIsSidebarOpen(true)}
|
| 168 |
+
/>
|
| 169 |
+
<h1 className="text-xl font-medium text-gray-300 tracking-wide">MyNameIsKans</h1>
|
| 170 |
+
</div>
|
| 171 |
+
|
| 172 |
+
{session ? (
|
| 173 |
+
<div className="flex items-center gap-2 md:gap-4 bg-[#1e1f20] py-1.5 px-2 md:px-3 rounded-full border border-gray-700">
|
| 174 |
+
<span className="text-sm text-gray-300 hidden md:block">{session.user.name}</span>
|
| 175 |
+
<img src={session.user.image} alt="Profile" className="w-7 h-7 rounded-full" />
|
| 176 |
+
<button onClick={() => signOut()} className="text-sm font-medium text-red-400 hover:text-red-300 ml-1 md:ml-2 pr-2">
|
| 177 |
+
Keluar
|
| 178 |
+
</button>
|
| 179 |
+
</div>
|
| 180 |
+
) : (
|
| 181 |
+
<button
|
| 182 |
+
onClick={() => signIn("google")}
|
| 183 |
+
className="flex items-center gap-2 bg-white text-black px-4 md:px-5 py-2 rounded-full hover:bg-gray-200 transition font-medium shadow-md text-sm md:text-base"
|
| 184 |
+
>
|
| 185 |
+
<svg className="w-5 h-5" viewBox="0 0 24 24">
|
| 186 |
+
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4" />
|
| 187 |
+
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853" />
|
| 188 |
+
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05" />
|
| 189 |
+
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335" />
|
| 190 |
+
</svg>
|
| 191 |
+
Login
|
| 192 |
+
</button>
|
| 193 |
+
)}
|
| 194 |
+
</header>
|
| 195 |
+
|
| 196 |
+
{!session ? (
|
| 197 |
+
<div className="flex-1 flex items-center justify-center flex-col gap-4 p-4">
|
| 198 |
+
<h2 className="text-2xl md:text-3xl font-semibold text-gray-300 text-center">Akses Terkunci</h2>
|
| 199 |
+
<p className="text-gray-500 text-center max-w-md text-sm md:text-base">Silakan login menggunakan akun Google Anda untuk memulai percakapan dan menyimpan riwayat obrolan.</p>
|
| 200 |
+
</div>
|
| 201 |
+
) : (
|
| 202 |
+
<div className={`flex-1 overflow-y-auto px-4 md:px-32 flex flex-col ${isChatEmpty ? "justify-center items-center" : "pt-4 pb-32"}`}>
|
| 203 |
+
|
| 204 |
+
{isChatEmpty ? (
|
| 205 |
+
<div className="w-full max-w-3xl text-left mb-8 px-2 md:px-0">
|
| 206 |
+
{/* Ukuran teks dikecilkan untuk layar HP (text-3xl) dan besar di Laptop (md:text-5xl) */}
|
| 207 |
+
<h1 className="text-3xl md:text-5xl font-semibold mb-2">
|
| 208 |
+
<span className="bg-gradient-to-r from-blue-400 to-purple-400 text-transparent bg-clip-text">
|
| 209 |
+
Halo, {session.user.name.split(' ')[0]}
|
| 210 |
+
</span>
|
| 211 |
+
</h1>
|
| 212 |
+
<h2 className="text-3xl md:text-5xl font-semibold text-[#444746] leading-tight">Ada yang bisa saya bantu hari ini?</h2>
|
| 213 |
+
</div>
|
| 214 |
+
) : (
|
| 215 |
+
<div className="w-full max-w-3xl mx-auto flex flex-col gap-6">
|
| 216 |
+
{messages.map((msg, index) => (
|
| 217 |
+
<div key={index} className={`flex ${msg.role === "user" ? "justify-end" : "justify-start"}`}>
|
| 218 |
+
<div className={`p-3 md:p-4 rounded-2xl max-w-[90%] md:max-w-[85%] leading-relaxed text-sm md:text-base ${
|
| 219 |
+
msg.role === "user" ? "bg-[#2a2b2c] text-gray-200 rounded-br-sm" : "bg-transparent text-gray-100"
|
| 220 |
+
}`}>
|
| 221 |
+
<div className="whitespace-pre-wrap">{msg.content}</div>
|
| 222 |
+
</div>
|
| 223 |
+
</div>
|
| 224 |
+
))}
|
| 225 |
+
{isLoading && (
|
| 226 |
+
<div className="flex justify-start text-gray-500 italic text-sm animate-pulse ml-4">
|
| 227 |
+
Kans sedang mengetik...
|
| 228 |
+
</div>
|
| 229 |
+
)}
|
| 230 |
+
<div ref={messagesEndRef} />
|
| 231 |
+
</div>
|
| 232 |
+
)}
|
| 233 |
+
|
| 234 |
+
<div className={`w-full max-w-3xl mx-auto px-4 md:px-0 transition-all duration-500 ease-in-out ${isChatEmpty ? "" : "absolute bottom-4 md:bottom-6 left-1/2 -translate-x-1/2"}`}>
|
| 235 |
+
<form onSubmit={sendMessage} className="relative bg-[#1e1f20] rounded-full flex items-center p-1.5 md:p-2 border border-gray-700 shadow-xl focus-within:bg-[#2a2b2c] transition-colors">
|
| 236 |
+
<input
|
| 237 |
+
type="text"
|
| 238 |
+
value={input}
|
| 239 |
+
onChange={(e) => setInput(e.target.value)}
|
| 240 |
+
placeholder="Ketik pesan..."
|
| 241 |
+
className="w-full bg-transparent text-gray-200 outline-none px-4 md:px-6 py-2 md:py-3 placeholder-gray-500 text-sm md:text-base"
|
| 242 |
+
/>
|
| 243 |
+
<button type="submit" disabled={isLoading} className="bg-blue-600 hover:bg-blue-500 text-white font-medium px-4 md:px-6 py-2 md:py-3 rounded-full transition disabled:opacity-50 disabled:bg-gray-700 text-sm md:text-base">
|
| 244 |
+
Kirim
|
| 245 |
+
</button>
|
| 246 |
+
</form>
|
| 247 |
+
</div>
|
| 248 |
+
</div>
|
| 249 |
+
)}
|
| 250 |
+
</main>
|
| 251 |
+
</div>
|
| 252 |
+
);
|
| 253 |
+
}
|
ai-frontend/components/SessionWrapper.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// File: components/SessionWrapper.js
|
| 2 |
+
"use client";
|
| 3 |
+
import { SessionProvider } from "next-auth/react";
|
| 4 |
+
|
| 5 |
+
export default function SessionWrapper({ children }) {
|
| 6 |
+
return <SessionProvider>{children}</SessionProvider>;
|
| 7 |
+
}
|
ai-frontend/eslint.config.mjs
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig, globalIgnores } from "eslint/config";
|
| 2 |
+
import nextVitals from "eslint-config-next/core-web-vitals";
|
| 3 |
+
import nextTs from "eslint-config-next/typescript";
|
| 4 |
+
|
| 5 |
+
const eslintConfig = defineConfig([
|
| 6 |
+
...nextVitals,
|
| 7 |
+
...nextTs,
|
| 8 |
+
// Override default ignores of eslint-config-next.
|
| 9 |
+
globalIgnores([
|
| 10 |
+
// Default ignores of eslint-config-next:
|
| 11 |
+
".next/**",
|
| 12 |
+
"out/**",
|
| 13 |
+
"build/**",
|
| 14 |
+
"next-env.d.ts",
|
| 15 |
+
]),
|
| 16 |
+
]);
|
| 17 |
+
|
| 18 |
+
export default eslintConfig;
|
ai-frontend/next.config.mjs
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/** @type {import('next').NextConfig} */
|
| 2 |
+
const nextConfig = {
|
| 3 |
+
async rewrites() {
|
| 4 |
+
return [
|
| 5 |
+
{
|
| 6 |
+
// Jika ada request ke /api/django/..., teruskan ke Django lokal Anda
|
| 7 |
+
source: '/api/django/:path*',
|
| 8 |
+
destination: 'http://127.0.0.1:8000/api/:path*',
|
| 9 |
+
},
|
| 10 |
+
]
|
| 11 |
+
},
|
| 12 |
+
}
|
| 13 |
+
export default nextConfig;
|
ai-frontend/next.config.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { NextConfig } from "next";
|
| 2 |
+
|
| 3 |
+
const nextConfig: NextConfig = {
|
| 4 |
+
/* config options here */
|
| 5 |
+
};
|
| 6 |
+
|
| 7 |
+
export default nextConfig;
|
ai-frontend/package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
ai-frontend/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "ai-frontend",
|
| 3 |
+
"version": "0.1.0",
|
| 4 |
+
"private": true,
|
| 5 |
+
"scripts": {
|
| 6 |
+
"dev": "next dev",
|
| 7 |
+
"build": "next build",
|
| 8 |
+
"start": "next start",
|
| 9 |
+
"lint": "eslint"
|
| 10 |
+
},
|
| 11 |
+
"dependencies": {
|
| 12 |
+
"lucide-react": "^0.575.0",
|
| 13 |
+
"next": "16.1.6",
|
| 14 |
+
"next-auth": "^4.24.13",
|
| 15 |
+
"react": "19.2.3",
|
| 16 |
+
"react-dom": "19.2.3"
|
| 17 |
+
},
|
| 18 |
+
"devDependencies": {
|
| 19 |
+
"@tailwindcss/postcss": "^4",
|
| 20 |
+
"@types/node": "^20",
|
| 21 |
+
"@types/react": "^19",
|
| 22 |
+
"@types/react-dom": "^19",
|
| 23 |
+
"eslint": "^9",
|
| 24 |
+
"eslint-config-next": "16.1.6",
|
| 25 |
+
"tailwindcss": "^4",
|
| 26 |
+
"typescript": "^5"
|
| 27 |
+
}
|
| 28 |
+
}
|
ai-frontend/postcss.config.mjs
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const config = {
|
| 2 |
+
plugins: {
|
| 3 |
+
"@tailwindcss/postcss": {},
|
| 4 |
+
},
|
| 5 |
+
};
|
| 6 |
+
|
| 7 |
+
export default config;
|
ai-frontend/public/file.svg
ADDED
|
|
ai-frontend/public/globe.svg
ADDED
|
|
ai-frontend/public/next.svg
ADDED
|
|
ai-frontend/public/vercel.svg
ADDED
|
|
ai-frontend/public/window.svg
ADDED
|
|
ai-frontend/tsconfig.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"target": "ES2017",
|
| 4 |
+
"lib": ["dom", "dom.iterable", "esnext"],
|
| 5 |
+
"allowJs": true,
|
| 6 |
+
"skipLibCheck": true,
|
| 7 |
+
"strict": true,
|
| 8 |
+
"noEmit": true,
|
| 9 |
+
"esModuleInterop": true,
|
| 10 |
+
"module": "esnext",
|
| 11 |
+
"moduleResolution": "bundler",
|
| 12 |
+
"resolveJsonModule": true,
|
| 13 |
+
"isolatedModules": true,
|
| 14 |
+
"jsx": "react-jsx",
|
| 15 |
+
"incremental": true,
|
| 16 |
+
"plugins": [
|
| 17 |
+
{
|
| 18 |
+
"name": "next"
|
| 19 |
+
}
|
| 20 |
+
],
|
| 21 |
+
"paths": {
|
| 22 |
+
"@/*": ["./*"]
|
| 23 |
+
}
|
| 24 |
+
},
|
| 25 |
+
"include": [
|
| 26 |
+
"next-env.d.ts",
|
| 27 |
+
"**/*.ts",
|
| 28 |
+
"**/*.tsx",
|
| 29 |
+
".next/types/**/*.ts",
|
| 30 |
+
".next/dev/types/**/*.ts",
|
| 31 |
+
"**/*.mts"
|
| 32 |
+
],
|
| 33 |
+
"exclude": ["node_modules"]
|
| 34 |
+
}
|
prepare_dataset.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import random
|
| 3 |
+
import os
|
| 4 |
+
|
| 5 |
+
def process_datasets():
|
| 6 |
+
all_formatted_data = []
|
| 7 |
+
|
| 8 |
+
print("Memulai proses membaca dan menyeragamkan format...")
|
| 9 |
+
|
| 10 |
+
# 1. File 1: broad-general-dataset.jsonl
|
| 11 |
+
file1 = "dataset/broad-general-dataset.jsonl"
|
| 12 |
+
if os.path.exists(file1):
|
| 13 |
+
with open(file1, 'r', encoding='utf-8') as f:
|
| 14 |
+
for line in f:
|
| 15 |
+
if not line.strip(): continue
|
| 16 |
+
data = json.loads(line)
|
| 17 |
+
messages = []
|
| 18 |
+
|
| 19 |
+
# Masukkan system prompt jika ada (berguna untuk persona)
|
| 20 |
+
if "system" in data and data["system"].strip():
|
| 21 |
+
messages.append({"role": "system", "content": data["system"]})
|
| 22 |
+
|
| 23 |
+
# Masukkan instruction sebagai user, dan response sebagai assistant
|
| 24 |
+
if "instruction" in data and "response" in data:
|
| 25 |
+
messages.append({"role": "user", "content": data["instruction"]})
|
| 26 |
+
messages.append({"role": "assistant", "content": data["response"]})
|
| 27 |
+
|
| 28 |
+
all_formatted_data.append({"messages": messages})
|
| 29 |
+
print(f" [+] Berhasil membaca {file1}")
|
| 30 |
+
|
| 31 |
+
# 2. File 2: casual-conversation-poo.json
|
| 32 |
+
file2 = "dataset/casual-conversation-poo.json"
|
| 33 |
+
if os.path.exists(file2):
|
| 34 |
+
with open(file2, 'r', encoding='utf-8') as f:
|
| 35 |
+
data_list = json.load(f) # Ini file .json biasa (list of objects)
|
| 36 |
+
for item in data_list:
|
| 37 |
+
if "prompt" in item and "chosen" in item:
|
| 38 |
+
messages = [
|
| 39 |
+
{"role": "user", "content": item["prompt"]},
|
| 40 |
+
{"role": "assistant", "content": item["chosen"]}
|
| 41 |
+
]
|
| 42 |
+
all_formatted_data.append({"messages": messages})
|
| 43 |
+
print(f" [+] Berhasil membaca {file2}")
|
| 44 |
+
|
| 45 |
+
# 3. File 3: dataset.jsonl (Dataset curhat)
|
| 46 |
+
file3 = "dataset/dataset.jsonl"
|
| 47 |
+
if os.path.exists(file3):
|
| 48 |
+
with open(file3, 'r', encoding='utf-8') as f:
|
| 49 |
+
for line in f:
|
| 50 |
+
if not line.strip(): continue
|
| 51 |
+
data = json.loads(line)
|
| 52 |
+
# Karena formatnya sudah {"messages": [...]}, kita tinggal ambil utuh
|
| 53 |
+
if "messages" in data:
|
| 54 |
+
all_formatted_data.append(data)
|
| 55 |
+
print(f" [+] Berhasil membaca {file3}")
|
| 56 |
+
|
| 57 |
+
total_data = len(all_formatted_data)
|
| 58 |
+
if total_data == 0:
|
| 59 |
+
print("[-] Tidak ada data yang berhasil diproses. Cek kembali path/nama folder.")
|
| 60 |
+
return
|
| 61 |
+
|
| 62 |
+
print(f"\nTotal data terkumpul: {total_data} baris percakapan.")
|
| 63 |
+
|
| 64 |
+
# 4. MENGACAK DATA (Shuffling)
|
| 65 |
+
print("Mengacak data agar pelatihan seimbang...")
|
| 66 |
+
random.seed(42) # Seed agar acakannya konsisten jika dijalankan ulang
|
| 67 |
+
random.shuffle(all_formatted_data)
|
| 68 |
+
|
| 69 |
+
# 5. MEMBAGI DATA (Train/Test Split - 90% Train, 10% Test)
|
| 70 |
+
print("Membagi data menjadi Train dan Test set...")
|
| 71 |
+
split_idx = int(total_data * 0.9)
|
| 72 |
+
train_data = all_formatted_data[:split_idx]
|
| 73 |
+
test_data = all_formatted_data[split_idx:]
|
| 74 |
+
|
| 75 |
+
# 6. MENYIMPAN KE FILE BARU
|
| 76 |
+
train_file = "dataset/train.jsonl"
|
| 77 |
+
test_file = "dataset/test.jsonl"
|
| 78 |
+
|
| 79 |
+
with open(train_file, 'w', encoding='utf-8') as f:
|
| 80 |
+
for item in train_data:
|
| 81 |
+
f.write(json.dumps(item, ensure_ascii=False) + '\n')
|
| 82 |
+
|
| 83 |
+
with open(test_file, 'w', encoding='utf-8') as f:
|
| 84 |
+
for item in test_data:
|
| 85 |
+
f.write(json.dumps(item, ensure_ascii=False) + '\n')
|
| 86 |
+
|
| 87 |
+
print("\n✅ PROSES SELESAI!")
|
| 88 |
+
print(f" - Data Pelatihan (Train): {len(train_data)} baris -> {train_file}")
|
| 89 |
+
print(f" - Data Evaluasi (Test): {len(test_data)} baris -> {test_file}")
|
| 90 |
+
print("Dataset sudah 100% siap digunakan untuk Fine-Tuning LLM.")
|
| 91 |
+
|
| 92 |
+
# Jalankan script
|
| 93 |
+
process_datasets()
|
requirements.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
django
|
| 2 |
+
django-cors-headers
|
| 3 |
+
llama-cpp-python
|
test_api.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import requests
|
| 2 |
+
import time
|
| 3 |
+
|
| 4 |
+
# Alamat API Django Anda
|
| 5 |
+
url = 'http://127.0.0.1:8000/api/chat/'
|
| 6 |
+
|
| 7 |
+
# Pesan yang akan dikirim ke bot (Saya buatkan pertanyaan seputar IoT)
|
| 8 |
+
payload = {
|
| 9 |
+
"message": "saya punya teman bernama rosyy, coba berikan dia pencerahan agar tidak begadang terus, menjaga pola makan, menjaga kesehatan."
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
print("Sedang mengirim pesan ke server Django...\n")
|
| 13 |
+
start_time = time.time()
|
| 14 |
+
|
| 15 |
+
try:
|
| 16 |
+
# Menembak API dengan method POST
|
| 17 |
+
response = requests.post(url, json=payload)
|
| 18 |
+
|
| 19 |
+
# Menghitung waktu respons
|
| 20 |
+
end_time = time.time()
|
| 21 |
+
durasi = round(end_time - start_time, 2)
|
| 22 |
+
|
| 23 |
+
# Mengecek apakah server membalas dengan status sukses (200)
|
| 24 |
+
if response.status_code == 200:
|
| 25 |
+
data = response.json()
|
| 26 |
+
print(f"⏱Waktu respons: {durasi} detik\n")
|
| 27 |
+
print("Jawaban Bot Qwen:")
|
| 28 |
+
print("-" * 50)
|
| 29 |
+
print(data['response'])
|
| 30 |
+
print("-" * 50)
|
| 31 |
+
else:
|
| 32 |
+
print(f"❌ Server membalas dengan Error {response.status_code}:")
|
| 33 |
+
print(response.text)
|
| 34 |
+
|
| 35 |
+
except Exception as e:
|
| 36 |
+
print(f"❌ Gagal terhubung ke server: {e}")
|
train_local.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# train_local.py
|
| 2 |
+
import torch
|
| 3 |
+
from datasets import load_dataset
|
| 4 |
+
from unsloth import FastLanguageModel
|
| 5 |
+
from unsloth.chat_templates import get_chat_template
|
| 6 |
+
from trl import SFTTrainer
|
| 7 |
+
from transformers import TrainingArguments
|
| 8 |
+
|
| 9 |
+
# 1. KONFIGURASI MEMORI SANGAT KETAT
|
| 10 |
+
max_seq_length = 512 # Panjang maksimal kalimat dipangkas drastis agar tidak OOM
|
| 11 |
+
dtype = None
|
| 12 |
+
load_in_4bit = True # Wajib True untuk hemat VRAM
|
| 13 |
+
|
| 14 |
+
print("Memuat model Qwen-2.5-1.5B...")
|
| 15 |
+
# Menggunakan Qwen 1.5B yang sangat ringan tapi pintar
|
| 16 |
+
model, tokenizer = FastLanguageModel.from_pretrained(
|
| 17 |
+
model_name = "unsloth/Qwen2.5-1.5B-Instruct-bnb-4bit",
|
| 18 |
+
max_seq_length = max_seq_length,
|
| 19 |
+
dtype = dtype,
|
| 20 |
+
load_in_4bit = load_in_4bit,
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
# Menyiapkan LoRA (Melatih hanya sebagian kecil otak AI)
|
| 24 |
+
model = FastLanguageModel.get_peft_model(
|
| 25 |
+
model,
|
| 26 |
+
r = 8, # Diperkecil dari 16 ke 8 agar VRAM lebih hemat
|
| 27 |
+
target_modules = ["q_proj", "k_proj", "v_proj", "o_proj",
|
| 28 |
+
"gate_proj", "up_proj", "down_proj",],
|
| 29 |
+
lora_alpha = 8,
|
| 30 |
+
lora_dropout = 0,
|
| 31 |
+
bias = "none",
|
| 32 |
+
use_gradient_checkpointing = "unsloth",
|
| 33 |
+
random_state = 3407,
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
# 2. PERSIAPAN DATASET
|
| 37 |
+
print("Membaca dataset train.jsonl...")
|
| 38 |
+
tokenizer = get_chat_template(tokenizer, chat_template = "chatml")
|
| 39 |
+
|
| 40 |
+
def formatting_prompts_func(examples):
|
| 41 |
+
convos = examples["messages"]
|
| 42 |
+
texts = [tokenizer.apply_chat_template(convo, tokenize = False, add_generation_prompt = False) for convo in convos]
|
| 43 |
+
return { "text" : texts, }
|
| 44 |
+
|
| 45 |
+
dataset = load_dataset("json", data_files="dataset/train.jsonl", split="train")
|
| 46 |
+
dataset = dataset.map(formatting_prompts_func, batched = True)
|
| 47 |
+
|
| 48 |
+
# 3. PROSES PELATIHAN (TRAINING)
|
| 49 |
+
print("Memulai pelatihan...")
|
| 50 |
+
trainer = SFTTrainer(
|
| 51 |
+
model = model,
|
| 52 |
+
tokenizer = tokenizer,
|
| 53 |
+
train_dataset = dataset,
|
| 54 |
+
dataset_text_field = "text",
|
| 55 |
+
max_seq_length = max_seq_length,
|
| 56 |
+
dataset_num_proc = 2,
|
| 57 |
+
packing = False,
|
| 58 |
+
args = TrainingArguments(
|
| 59 |
+
per_device_train_batch_size = 1, # HARGA MATI 1 untuk lokal
|
| 60 |
+
gradient_accumulation_steps = 4,
|
| 61 |
+
warmup_steps = 5,
|
| 62 |
+
max_steps = 60, # Uji coba awal 60 langkah dulu
|
| 63 |
+
learning_rate = 2e-4,
|
| 64 |
+
fp16 = not torch.cuda.is_bf16_supported(),
|
| 65 |
+
bf16 = torch.cuda.is_bf16_supported(),
|
| 66 |
+
logging_steps = 10,
|
| 67 |
+
optim = "adamw_8bit", # Optimizer khusus hemat memori
|
| 68 |
+
weight_decay = 0.01,
|
| 69 |
+
lr_scheduler_type = "linear",
|
| 70 |
+
seed = 3407,
|
| 71 |
+
output_dir = "outputs",
|
| 72 |
+
report_to="none", # Mematikan notifikasi wandb secara otomatis
|
| 73 |
+
),
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
trainer_stats = trainer.train()
|
| 77 |
+
|
| 78 |
+
# 4. SIMPAN MODEL
|
| 79 |
+
print("Pelatihan selesai! Menyimpan model...")
|
| 80 |
+
model.save_pretrained("model_curhat_lokal") # Simpan hasil LoRA
|
| 81 |
+
tokenizer.save_pretrained("model_curhat_lokal")
|
| 82 |
+
print("Semua proses berhasil!")
|
translate_dataset.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
import time
|
| 4 |
+
from deep_translator import GoogleTranslator
|
| 5 |
+
|
| 6 |
+
# Inisialisasi translator (Inggris ke Indonesia)
|
| 7 |
+
translator = GoogleTranslator(source='en', target='id')
|
| 8 |
+
|
| 9 |
+
def translate_text(text):
|
| 10 |
+
"""Fungsi untuk menerjemahkan teks."""
|
| 11 |
+
try:
|
| 12 |
+
# Jika teks terlalu pendek atau kosong, lewati saja
|
| 13 |
+
if not text or len(text.strip()) < 2:
|
| 14 |
+
return text
|
| 15 |
+
|
| 16 |
+
# Translate dan beri jeda agar tidak diblokir (Rate Limit)
|
| 17 |
+
translated = translator.translate(text)
|
| 18 |
+
time.sleep(0.5)
|
| 19 |
+
return translated
|
| 20 |
+
except Exception as e:
|
| 21 |
+
print(f"Error saat menerjemahkan: {e}")
|
| 22 |
+
return text
|
| 23 |
+
|
| 24 |
+
def translate_recursive(data):
|
| 25 |
+
"""Fungsi rekursif untuk mencari dan menerjemahkan semua string."""
|
| 26 |
+
if isinstance(data, dict):
|
| 27 |
+
return {key: translate_recursive(value) for key, value in data.items()}
|
| 28 |
+
elif isinstance(data, list):
|
| 29 |
+
return [translate_recursive(item) for item in data]
|
| 30 |
+
elif isinstance(data, str):
|
| 31 |
+
return translate_text(data)
|
| 32 |
+
else:
|
| 33 |
+
return data
|
| 34 |
+
|
| 35 |
+
def process_and_merge(filepaths, output_filepath):
|
| 36 |
+
"""Membaca banyak file, menerjemahkan, dan menyatukannya ke satu file."""
|
| 37 |
+
print(f"Memulai proses... Hasil akhir akan disimpan di: {output_filepath}")
|
| 38 |
+
|
| 39 |
+
# Buka file output utama dengan mode 'w' (write)
|
| 40 |
+
with open(output_filepath, 'w', encoding='utf-8') as outfile:
|
| 41 |
+
|
| 42 |
+
for filepath in filepaths:
|
| 43 |
+
if not os.path.exists(filepath):
|
| 44 |
+
print(f"\n[!] File tidak ditemukan, melewati: {filepath}")
|
| 45 |
+
continue
|
| 46 |
+
|
| 47 |
+
print(f"\n[+] Memproses file: {filepath}")
|
| 48 |
+
ext = os.path.splitext(filepath)[1]
|
| 49 |
+
|
| 50 |
+
with open(filepath, 'r', encoding='utf-8') as infile:
|
| 51 |
+
if ext == '.jsonl':
|
| 52 |
+
# Proses file .jsonl baris demi baris
|
| 53 |
+
for line_num, line in enumerate(infile, 1):
|
| 54 |
+
if not line.strip(): continue # Lewati baris kosong jika ada
|
| 55 |
+
data = json.loads(line)
|
| 56 |
+
translated_data = translate_recursive(data)
|
| 57 |
+
|
| 58 |
+
# Tulis langsung ke file master
|
| 59 |
+
outfile.write(json.dumps(translated_data, ensure_ascii=False) + '\n')
|
| 60 |
+
print(f" -> Baris {line_num} selesai diterjemahkan & digabung")
|
| 61 |
+
|
| 62 |
+
elif ext == '.json':
|
| 63 |
+
# Proses file .json (biasanya berupa list of objects)
|
| 64 |
+
data_list = json.load(infile)
|
| 65 |
+
if isinstance(data_list, list):
|
| 66 |
+
for i, data in enumerate(data_list, 1):
|
| 67 |
+
translated_data = translate_recursive(data)
|
| 68 |
+
|
| 69 |
+
# Tulis langsung ke file master dalam format jsonl
|
| 70 |
+
outfile.write(json.dumps(translated_data, ensure_ascii=False) + '\n')
|
| 71 |
+
print(f" -> Item {i} selesai diterjemahkan & digabung")
|
| 72 |
+
else:
|
| 73 |
+
print(f"Format .json pada {filepath} tidak didukung. Harus berupa list.")
|
| 74 |
+
|
| 75 |
+
print(f"\n✅ PROSES SELESAI! Semua dataset berhasil diterjemahkan dan digabung menjadi {output_filepath}")
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
# 1. Daftar file sumber Anda yang ada di dalam folder 'dataset'
|
| 79 |
+
files_to_translate = [
|
| 80 |
+
"dataset/broad-general-dataset.jsonl",
|
| 81 |
+
"dataset/casual-conversation-poo.json",
|
| 82 |
+
"dataset/dataset.jsonl"
|
| 83 |
+
]
|
| 84 |
+
|
| 85 |
+
# 2. Nama file gabungan hasil terjemahan
|
| 86 |
+
output_file = "dataset/master_dataset_id.jsonl"
|
| 87 |
+
|
| 88 |
+
# 3. Eksekusi program
|
| 89 |
+
process_and_merge(files_to_translate, output_file)
|