laluarkan commited on
Commit
0d13ade
·
0 Parent(s):

Upload bersih ke Hugging Face

Browse files
.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)