indahPurnamaSarii commited on
Commit ·
f227f03
1
Parent(s): 82244d1
Initial deployment
Browse files- .gitattributes +3 -0
- .gitignore +6 -0
- Dockerfile +8 -0
- app.py +132 -0
- requirements.txt +16 -0
- static/Galeri/hero1.jpg +3 -0
- static/Galeri/hero2.jpg +3 -0
- static/Galeri/hero3.jpg +3 -0
- static/Galeri/logo.png +3 -0
- static/Galeri/logo1.png +3 -0
- static/Galeri/profil.png +3 -0
- static/Galeri/profil1.png +3 -0
- static/css/style.css +204 -0
- static/js/main.js +184 -0
- static/vendor/bootstrap/css/bootstrap.min.css +0 -0
- templates/index.html +663 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
*.pdf filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
*.jpg filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
*.png filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.env
|
| 2 |
+
data/
|
| 3 |
+
source_data/
|
| 4 |
+
.lib/
|
| 5 |
+
venv/
|
| 6 |
+
*.sqlite3
|
Dockerfile
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.9-slim
|
| 2 |
+
WORKDIR /code
|
| 3 |
+
COPY ./requirements.txt /code/requirements.txt
|
| 4 |
+
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
| 5 |
+
COPY . /code/
|
| 6 |
+
RUN mkdir -p /app/.cache
|
| 7 |
+
ENV SENTENCE_TRANSFORMERS_HOME=/app/.cache
|
| 8 |
+
CMD ["gunicorn", "--bind", "0.0.0.0:7860", "--workers", "1", "app:app"]
|
app.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Flask, render_template, request, jsonify, session
|
| 2 |
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
| 3 |
+
from langchain_huggingface import HuggingFaceEmbeddings
|
| 4 |
+
from langchain_chroma import Chroma
|
| 5 |
+
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
| 6 |
+
from langchain.chains import create_history_aware_retriever, create_retrieval_chain
|
| 7 |
+
from langchain.chains.combine_documents import create_stuff_documents_chain
|
| 8 |
+
from langchain_core.messages import HumanMessage, AIMessage
|
| 9 |
+
from dotenv import load_dotenv
|
| 10 |
+
import os
|
| 11 |
+
|
| 12 |
+
# Memuat variabel lingkungan dari file .env
|
| 13 |
+
load_dotenv()
|
| 14 |
+
|
| 15 |
+
app = Flask(__name__, template_folder='templates')
|
| 16 |
+
app.secret_key = os.urandom(24)
|
| 17 |
+
|
| 18 |
+
# --- Inisialisasi Komponen Utama (dilakukan sekali saat server dimulai) ---
|
| 19 |
+
vectorstore = None
|
| 20 |
+
llm = None
|
| 21 |
+
retriever = None
|
| 22 |
+
|
| 23 |
+
try:
|
| 24 |
+
# 1. Memuat Vectorstore (Database Chroma) yang sudah dibuat oleh vector_embeddings.py
|
| 25 |
+
vectorstore = Chroma(
|
| 26 |
+
persist_directory="data",
|
| 27 |
+
embedding_function=HuggingFaceEmbeddings(model_name="sentence-transformers/bert-base-nli-max-tokens")
|
| 28 |
+
)
|
| 29 |
+
# Membuat retriever untuk mengambil informasi dari vectorstore
|
| 30 |
+
retriever = vectorstore.as_retriever(search_kwargs={"k": 5})
|
| 31 |
+
print("Vectorstore berhasil dimuat dan retriever dibuat.")
|
| 32 |
+
except Exception as e:
|
| 33 |
+
print(f"GALAT PENTING: Kesalahan saat memuat vectorstore Chroma: {e}")
|
| 34 |
+
print("Pastikan Anda sudah menjalankan 'vector_embeddings.py' terlebih dahulu.")
|
| 35 |
+
|
| 36 |
+
try:
|
| 37 |
+
# 2. Menyiapkan Model AI (LLM)
|
| 38 |
+
llm = ChatGoogleGenerativeAI(model="gemini-1.5-flash", temperature=0.2)
|
| 39 |
+
print("Model AI (Gemini) berhasil diinisialisasi.")
|
| 40 |
+
except Exception as e:
|
| 41 |
+
print(f"GALAT PENTING: Gagal menginisialisasi model Gemini: {e}")
|
| 42 |
+
print("Pastikan GOOGLE_API_KEY Anda sudah benar di file .env.")
|
| 43 |
+
|
| 44 |
+
# --- Rute Aplikasi Flask ---
|
| 45 |
+
|
| 46 |
+
@app.route('/')
|
| 47 |
+
def home():
|
| 48 |
+
# --- KODE DEBUGGING BARU (Bisa dihapus nanti) ---
|
| 49 |
+
template_folder_path = os.path.join(os.path.dirname(__file__), 'templates')
|
| 50 |
+
print("--- DEBUGGING INFO ---")
|
| 51 |
+
print(f"Mencari folder templates di path: {template_folder_path}")
|
| 52 |
+
if os.path.exists(template_folder_path):
|
| 53 |
+
files_in_templates = os.listdir(template_folder_path)
|
| 54 |
+
print(f"Isi folder templates yang ditemukan: {files_in_templates}")
|
| 55 |
+
else:
|
| 56 |
+
print("Folder 'templates' TIDAK DITEMUKAN oleh aplikasi.")
|
| 57 |
+
print("--- AKHIR DEBUGGING INFO ---")
|
| 58 |
+
# --- Akhir dari kode debugging ---
|
| 59 |
+
|
| 60 |
+
session.pop("chat_history", None)
|
| 61 |
+
return render_template('index.html')
|
| 62 |
+
|
| 63 |
+
@app.route('/get', methods=['GET'])
|
| 64 |
+
def get_response():
|
| 65 |
+
if not llm or not retriever:
|
| 66 |
+
return jsonify({"error": "Server belum siap. Periksa log terminal untuk galat."}), 503
|
| 67 |
+
|
| 68 |
+
user_message = request.args.get('msg')
|
| 69 |
+
if not user_message:
|
| 70 |
+
return jsonify({"error": "Pesan tidak boleh kosong."}), 400
|
| 71 |
+
|
| 72 |
+
# Mengambil riwayat chat dari sesi dan mengubahnya ke format yang benar
|
| 73 |
+
chat_history_from_session = session.get("chat_history", [])
|
| 74 |
+
chat_history = []
|
| 75 |
+
for msg in chat_history_from_session:
|
| 76 |
+
if msg.get("sender") == "user":
|
| 77 |
+
chat_history.append(HumanMessage(content=msg["message"]))
|
| 78 |
+
elif msg.get("sender") == "ai":
|
| 79 |
+
chat_history.append(AIMessage(content=msg["message"]))
|
| 80 |
+
|
| 81 |
+
# 1. Prompt untuk membuat pertanyaan baru yang kontekstual
|
| 82 |
+
contextualize_q_prompt = ChatPromptTemplate.from_messages([
|
| 83 |
+
("system", "Mengingat riwayat percakapan dan pertanyaan terbaru, formulasikan ulang pertanyaan menjadi pertanyaan yang berdiri sendiri."),
|
| 84 |
+
MessagesPlaceholder("chat_history"),
|
| 85 |
+
("human", "{input}"),
|
| 86 |
+
])
|
| 87 |
+
history_aware_retriever = create_history_aware_retriever(
|
| 88 |
+
llm, retriever, contextualize_q_prompt
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
# 2. Prompt utama untuk menjawab pertanyaan berdasarkan konteks dari PDF
|
| 92 |
+
qa_prompt = ChatPromptTemplate.from_messages([
|
| 93 |
+
("system", "Anda adalah asisten AI untuk BPVP Kota Sorong. Gunakan potongan konteks berikut untuk menjawab pertanyaan. Jika tidak tahu jawabannya, katakan saja Anda tidak tahu atau arahkan saja ke Website Resmi BPVP Sorong https://bpvpsorong.kemnaker.go.id/web. Jawab dalam bahasa Indonesia.\n\nKonteks:\n{context}"),
|
| 94 |
+
MessagesPlaceholder("chat_history"),
|
| 95 |
+
("human", "{input}"),
|
| 96 |
+
])
|
| 97 |
+
|
| 98 |
+
# Membuat chain untuk menggabungkan dokumen/konteks
|
| 99 |
+
question_answer_chain = create_stuff_documents_chain(llm, qa_prompt)
|
| 100 |
+
|
| 101 |
+
# Menggabungkan semuanya menjadi satu alur RAG (Retrieval-Augmented Generation)
|
| 102 |
+
rag_chain = create_retrieval_chain(history_aware_retriever, question_answer_chain)
|
| 103 |
+
|
| 104 |
+
try:
|
| 105 |
+
# Menjalankan chain untuk mendapatkan jawaban
|
| 106 |
+
response = rag_chain.invoke({"input": user_message, "chat_history": chat_history})
|
| 107 |
+
answer = response.get("answer", "Maaf, saya tidak dapat menemukan jawaban untuk itu.")
|
| 108 |
+
|
| 109 |
+
# Menyimpan percakapan baru ke dalam sesi dalam format JSON
|
| 110 |
+
new_history = session.get("chat_history", [])
|
| 111 |
+
new_history.append({"sender": "user", "message": user_message})
|
| 112 |
+
new_history.append({"sender": "ai", "message": answer})
|
| 113 |
+
session["chat_history"] = new_history
|
| 114 |
+
|
| 115 |
+
return jsonify(answer)
|
| 116 |
+
|
| 117 |
+
except Exception as e:
|
| 118 |
+
print(f"GALAT saat menjalankan RAG Chain: {e}")
|
| 119 |
+
return jsonify({"error": "Maaf, terjadi masalah internal saat memproses permintaan Anda."}), 500
|
| 120 |
+
|
| 121 |
+
@app.route('/load_history', methods=['GET'])
|
| 122 |
+
def load_history():
|
| 123 |
+
return jsonify(session.get("chat_history", []))
|
| 124 |
+
|
| 125 |
+
@app.route('/clear_history', methods=['POST'])
|
| 126 |
+
def clear_history():
|
| 127 |
+
session.pop("chat_history", None)
|
| 128 |
+
return jsonify({"status": "success"})
|
| 129 |
+
|
| 130 |
+
if __name__ == '__main__':
|
| 131 |
+
app.run(debug=True)
|
| 132 |
+
|
requirements.txt
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gunicorn
|
| 2 |
+
protobuf==3.20.*
|
| 3 |
+
langchain-huggingface==0.0.3
|
| 4 |
+
langchain
|
| 5 |
+
langchain_community
|
| 6 |
+
langchain-google-genai
|
| 7 |
+
python-dotenv
|
| 8 |
+
langchain_experimental
|
| 9 |
+
sentence-transformers
|
| 10 |
+
langchain_chroma
|
| 11 |
+
langchainhub
|
| 12 |
+
pypdf
|
| 13 |
+
rapidocr-onnxruntime
|
| 14 |
+
flask
|
| 15 |
+
pysqlite3-wheels
|
| 16 |
+
chromadb
|
static/Galeri/hero1.jpg
ADDED
|
Git LFS Details
|
static/Galeri/hero2.jpg
ADDED
|
Git LFS Details
|
static/Galeri/hero3.jpg
ADDED
|
Git LFS Details
|
static/Galeri/logo.png
ADDED
|
Git LFS Details
|
static/Galeri/logo1.png
ADDED
|
Git LFS Details
|
static/Galeri/profil.png
ADDED
|
Git LFS Details
|
static/Galeri/profil1.png
ADDED
|
Git LFS Details
|
static/css/style.css
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Efek transisi yang lebih halus untuk semua elemen */
|
| 2 |
+
* {
|
| 3 |
+
transition: all 0.3s ease-in-out;
|
| 4 |
+
}
|
| 5 |
+
/* Menghilangkan scrollbar default */
|
| 6 |
+
::-webkit-scrollbar {
|
| 7 |
+
display: none;
|
| 8 |
+
}
|
| 9 |
+
body {
|
| 10 |
+
-ms-overflow-style: none; /* IE and Edge */
|
| 11 |
+
scrollbar-width: none; /* Firefox */
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
/*
|
| 15 |
+
* File: style.css
|
| 16 |
+
* Description: File ini berisi semua gaya CSS kustom untuk website BPVP Sorong,
|
| 17 |
+
* terutama untuk komponen modal chatbot dan peringatan kustom.
|
| 18 |
+
*/
|
| 19 |
+
|
| 20 |
+
/* Overlay untuk Modal Chat */
|
| 21 |
+
.chat-modal-overlay {
|
| 22 |
+
position: fixed;
|
| 23 |
+
top: 0;
|
| 24 |
+
left: 0;
|
| 25 |
+
width: 100%;
|
| 26 |
+
height: 100%;
|
| 27 |
+
background-color: rgba(0, 0, 0, 0.5);
|
| 28 |
+
display: none; /* Disembunyikan secara default */
|
| 29 |
+
justify-content: center;
|
| 30 |
+
align-items: center;
|
| 31 |
+
z-index: 1000;
|
| 32 |
+
padding: 1rem;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
/* Konten Modal Chat */
|
| 36 |
+
.chat-modal-content {
|
| 37 |
+
background-color: white;
|
| 38 |
+
border-radius: 1rem;
|
| 39 |
+
overflow: hidden;
|
| 40 |
+
width: 100%;
|
| 41 |
+
max-width: 400px;
|
| 42 |
+
height: 90%;
|
| 43 |
+
max-height: 650px;
|
| 44 |
+
display: flex;
|
| 45 |
+
flex-direction: column;
|
| 46 |
+
box-shadow: 0 10px 25px rgba(0,0,0,0.1);
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
#chatbot-widget-container {
|
| 50 |
+
display: flex;
|
| 51 |
+
flex-direction: column;
|
| 52 |
+
height: 100%;
|
| 53 |
+
width: 100%;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
.chatbot-container {
|
| 57 |
+
width: 100%;
|
| 58 |
+
height: 100%;
|
| 59 |
+
display: flex;
|
| 60 |
+
flex-direction: column;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
#chatbot-header {
|
| 64 |
+
background-color: #334155; /* Warna pastel-blue */
|
| 65 |
+
color: white;
|
| 66 |
+
padding: 0.75rem 1rem;
|
| 67 |
+
display: flex;
|
| 68 |
+
justify-content: space-between;
|
| 69 |
+
align-items: center;
|
| 70 |
+
font-family: 'Inter', sans-serif;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
#chatbot-header::before {
|
| 74 |
+
content: 'Asisten Virtual';
|
| 75 |
+
font-weight: 600;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
#end-chat-button {
|
| 79 |
+
background: none;
|
| 80 |
+
border: none;
|
| 81 |
+
color: white;
|
| 82 |
+
font-size: 1.25rem;
|
| 83 |
+
cursor: pointer;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
#chatbot {
|
| 87 |
+
flex-grow: 1;
|
| 88 |
+
display: flex;
|
| 89 |
+
flex-direction: column;
|
| 90 |
+
overflow: hidden;
|
| 91 |
+
background-color: #f1f5f9; /* Warna slate-100 */
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
#conversation {
|
| 95 |
+
flex-grow: 1;
|
| 96 |
+
overflow-y: auto;
|
| 97 |
+
padding: 1rem;
|
| 98 |
+
display: flex;
|
| 99 |
+
flex-direction: column;
|
| 100 |
+
gap: 0.75rem;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
.chatbot-message {
|
| 104 |
+
padding: 0.75rem 1rem;
|
| 105 |
+
border-radius: 1rem;
|
| 106 |
+
max-width: 85%;
|
| 107 |
+
line-height: 1.5;
|
| 108 |
+
word-wrap: break-word;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
.user-message {
|
| 112 |
+
background-color: #334155; /* Warna pastel-blue */
|
| 113 |
+
color: white;
|
| 114 |
+
align-self: flex-end;
|
| 115 |
+
border-bottom-right-radius: 0.25rem;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
.chatbot-message.chatbot {
|
| 119 |
+
background-color: #e2e8f0; /* Warna slate-200 */
|
| 120 |
+
color: #334155;
|
| 121 |
+
align-self: flex-start;
|
| 122 |
+
border-bottom-left-radius: 0.25rem;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
.message-footer {
|
| 126 |
+
font-size: 0.75rem;
|
| 127 |
+
color: #94a3b8;
|
| 128 |
+
text-align: right;
|
| 129 |
+
margin-top: 0.25rem;
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
.user-message .message-footer {
|
| 133 |
+
color: #cbd5e1;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
#input-form {
|
| 137 |
+
padding: 0.75rem;
|
| 138 |
+
border-top: 1px solid #e2e8f0;
|
| 139 |
+
background-color: #ffffff;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
.message-container {
|
| 143 |
+
display: flex;
|
| 144 |
+
border: 1px solid #cbd5e1;
|
| 145 |
+
border-radius: 9999px;
|
| 146 |
+
overflow: hidden;
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
#input-field {
|
| 150 |
+
flex-grow: 1;
|
| 151 |
+
border: none;
|
| 152 |
+
padding: 0.75rem 1rem;
|
| 153 |
+
outline: none;
|
| 154 |
+
background: transparent;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
#submit-button {
|
| 158 |
+
background-color: #334155;
|
| 159 |
+
border: none;
|
| 160 |
+
color: white;
|
| 161 |
+
padding: 0.75rem 1.25rem;
|
| 162 |
+
cursor: pointer;
|
| 163 |
+
transition: background-color 0.2s;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
#submit-button:hover {
|
| 167 |
+
background-color: #475569;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
/* Modal Peringatan Kustom */
|
| 171 |
+
.modal {
|
| 172 |
+
display: none;
|
| 173 |
+
position: fixed;
|
| 174 |
+
z-index: 2000;
|
| 175 |
+
left: 0;
|
| 176 |
+
top: 0;
|
| 177 |
+
width: 100%;
|
| 178 |
+
height: 100%;
|
| 179 |
+
overflow: auto;
|
| 180 |
+
background-color: rgba(0,0,0,0.4);
|
| 181 |
+
justify-content: center;
|
| 182 |
+
align-items: center;
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
.modal-content {
|
| 186 |
+
background-color: #fefefe;
|
| 187 |
+
padding: 20px;
|
| 188 |
+
border: 1px solid #888;
|
| 189 |
+
width: 90%;
|
| 190 |
+
max-width: 400px;
|
| 191 |
+
text-align: center;
|
| 192 |
+
border-radius: 0.5rem;
|
| 193 |
+
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
#custom-alert-ok {
|
| 197 |
+
background-color: #334155;
|
| 198 |
+
color: white;
|
| 199 |
+
padding: 10px 20px;
|
| 200 |
+
border: none;
|
| 201 |
+
border-radius: 0.25rem;
|
| 202 |
+
cursor: pointer;
|
| 203 |
+
margin-top: 15px;
|
| 204 |
+
}
|
static/js/main.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
(function() {
|
| 2 |
+
"use strict";
|
| 3 |
+
|
| 4 |
+
/**
|
| 5 |
+
* Apply .scrolled class to the body as the page is scrolled down
|
| 6 |
+
*/
|
| 7 |
+
function toggleScrolled() {
|
| 8 |
+
const selectBody = document.querySelector('body');
|
| 9 |
+
const selectHeader = document.querySelector('#header');
|
| 10 |
+
if (!selectHeader.classList.contains('scroll-up-sticky') && !selectHeader.classList.contains('sticky-top') && !selectHeader.classList.contains('fixed-top')) return;
|
| 11 |
+
window.scrollY > 100 ? selectBody.classList.add('scrolled') : selectBody.classList.remove('scrolled');
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
document.addEventListener('scroll', toggleScrolled);
|
| 15 |
+
window.addEventListener('load', toggleScrolled);
|
| 16 |
+
|
| 17 |
+
/**
|
| 18 |
+
* Mobile nav toggle
|
| 19 |
+
*/
|
| 20 |
+
const mobileNavToggleBtn = document.querySelector('.mobile-nav-toggle');
|
| 21 |
+
const navMenu = document.getElementById("navmenu").querySelector("ul");
|
| 22 |
+
|
| 23 |
+
function mobileNavToogle() {
|
| 24 |
+
document.querySelector('body').classList.toggle('mobile-nav-active');
|
| 25 |
+
mobileNavToggleBtn.classList.toggle('bi-list');
|
| 26 |
+
mobileNavToggleBtn.classList.toggle('bi-x');
|
| 27 |
+
navMenu.classList.toggle('active'); // Menambahkan toggle menu aktif
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
if (mobileNavToggleBtn) {
|
| 31 |
+
mobileNavToggleBtn.addEventListener('click', mobileNavToogle);
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
/**
|
| 35 |
+
* Hide mobile nav on same-page/hash links
|
| 36 |
+
*/
|
| 37 |
+
document.querySelectorAll('#navmenu a').forEach(navmenu => {
|
| 38 |
+
navmenu.addEventListener('click', () => {
|
| 39 |
+
if (document.querySelector('.mobile-nav-active')) {
|
| 40 |
+
mobileNavToogle();
|
| 41 |
+
}
|
| 42 |
+
});
|
| 43 |
+
});
|
| 44 |
+
|
| 45 |
+
/**
|
| 46 |
+
* Toggle mobile nav dropdowns
|
| 47 |
+
*/
|
| 48 |
+
document.querySelectorAll('.navmenu .toggle-dropdown').forEach(navmenu => {
|
| 49 |
+
navmenu.addEventListener('click', function(e) {
|
| 50 |
+
e.preventDefault();
|
| 51 |
+
this.parentNode.classList.toggle('active');
|
| 52 |
+
this.parentNode.nextElementSibling.classList.toggle('dropdown-active');
|
| 53 |
+
e.stopImmediatePropagation();
|
| 54 |
+
});
|
| 55 |
+
});
|
| 56 |
+
|
| 57 |
+
/**
|
| 58 |
+
* Scroll top button
|
| 59 |
+
*/
|
| 60 |
+
let scrollTop = document.querySelector('.scroll-top');
|
| 61 |
+
|
| 62 |
+
function toggleScrollTop() {
|
| 63 |
+
if (scrollTop) {
|
| 64 |
+
window.scrollY > 100 ? scrollTop.classList.add('active') : scrollTop.classList.remove('active');
|
| 65 |
+
}
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
scrollTop.addEventListener('click', (e) => {
|
| 69 |
+
e.preventDefault();
|
| 70 |
+
window.scrollTo({
|
| 71 |
+
top: 0,
|
| 72 |
+
behavior: 'smooth'
|
| 73 |
+
});
|
| 74 |
+
});
|
| 75 |
+
|
| 76 |
+
window.addEventListener('load', toggleScrollTop);
|
| 77 |
+
document.addEventListener('scroll', toggleScrollTop);
|
| 78 |
+
|
| 79 |
+
/**
|
| 80 |
+
* Animation on scroll function and init
|
| 81 |
+
*/
|
| 82 |
+
function aosInit() {
|
| 83 |
+
AOS.init({
|
| 84 |
+
duration: 600,
|
| 85 |
+
easing: 'ease-in-out',
|
| 86 |
+
once: true,
|
| 87 |
+
mirror: false
|
| 88 |
+
});
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
window.addEventListener('load', aosInit);
|
| 92 |
+
|
| 93 |
+
/**
|
| 94 |
+
* Initiate glightbox
|
| 95 |
+
*/
|
| 96 |
+
const glightbox = GLightbox({
|
| 97 |
+
selector: '.glightbox'
|
| 98 |
+
});
|
| 99 |
+
|
| 100 |
+
/**
|
| 101 |
+
* Initiate Pure Counter
|
| 102 |
+
*/
|
| 103 |
+
new PureCounter();
|
| 104 |
+
|
| 105 |
+
/**
|
| 106 |
+
* Frequently Asked Questions Toggle
|
| 107 |
+
*/
|
| 108 |
+
document.querySelectorAll('.faq-item h3, .faq-item .faq-toggle').forEach((faqItem) => {
|
| 109 |
+
faqItem.addEventListener('click', () => {
|
| 110 |
+
faqItem.parentNode.classList.toggle('faq-active');
|
| 111 |
+
});
|
| 112 |
+
});
|
| 113 |
+
|
| 114 |
+
/**
|
| 115 |
+
* Init isotope layout and filters
|
| 116 |
+
*/
|
| 117 |
+
document.querySelectorAll('.isotope-layout').forEach(function(isotopeItem) {
|
| 118 |
+
let layout = isotopeItem.getAttribute('data-layout') ?? 'masonry';
|
| 119 |
+
let filter = isotopeItem.getAttribute('data-default-filter') ?? '*';
|
| 120 |
+
let sort = isotopeItem.getAttribute('data-sort') ?? 'original-order';
|
| 121 |
+
|
| 122 |
+
let initIsotope;
|
| 123 |
+
imagesLoaded(isotopeItem.querySelector('.isotope-container'), function() {
|
| 124 |
+
initIsotope = new Isotope(isotopeItem.querySelector('.isotope-container'), {
|
| 125 |
+
itemSelector: '.isotope-item',
|
| 126 |
+
layoutMode: layout,
|
| 127 |
+
filter: filter,
|
| 128 |
+
sortBy: sort
|
| 129 |
+
});
|
| 130 |
+
});
|
| 131 |
+
|
| 132 |
+
isotopeItem.querySelectorAll('.isotope-filters li').forEach(function(filters) {
|
| 133 |
+
filters.addEventListener('click', function() {
|
| 134 |
+
isotopeItem.querySelector('.isotope-filters .filter-active').classList.remove('filter-active');
|
| 135 |
+
this.classList.add('filter-active');
|
| 136 |
+
initIsotope.arrange({
|
| 137 |
+
filter: this.getAttribute('data-filter')
|
| 138 |
+
});
|
| 139 |
+
if (typeof aosInit === 'function') {
|
| 140 |
+
aosInit();
|
| 141 |
+
}
|
| 142 |
+
}, false);
|
| 143 |
+
});
|
| 144 |
+
|
| 145 |
+
});
|
| 146 |
+
|
| 147 |
+
/**
|
| 148 |
+
* Init swiper sliders
|
| 149 |
+
*/
|
| 150 |
+
function initSwiper() {
|
| 151 |
+
document.querySelectorAll(".init-swiper").forEach(function(swiperElement) {
|
| 152 |
+
let config = JSON.parse(
|
| 153 |
+
swiperElement.querySelector(".swiper-config").innerHTML.trim()
|
| 154 |
+
);
|
| 155 |
+
|
| 156 |
+
if (swiperElement.classList.contains("swiper-tab")) {
|
| 157 |
+
initSwiperWithCustomPagination(swiperElement, config);
|
| 158 |
+
} else {
|
| 159 |
+
new Swiper(swiperElement, config);
|
| 160 |
+
}
|
| 161 |
+
});
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
window.addEventListener("load", initSwiper);
|
| 165 |
+
|
| 166 |
+
/**
|
| 167 |
+
* Correct scrolling position upon page load for URLs containing hash links.
|
| 168 |
+
*/
|
| 169 |
+
window.addEventListener('load', function(e) {
|
| 170 |
+
if (window.location.hash) {
|
| 171 |
+
if (document.querySelector(window.location.hash)) {
|
| 172 |
+
setTimeout(() => {
|
| 173 |
+
let section = document.querySelector(window.location.hash);
|
| 174 |
+
let scrollMarginTop = getComputedStyle(section).scrollMarginTop;
|
| 175 |
+
window.scrollTo({
|
| 176 |
+
top: section.offsetTop - parseInt(scrollMarginTop),
|
| 177 |
+
behavior: 'smooth'
|
| 178 |
+
});
|
| 179 |
+
}, 100);
|
| 180 |
+
}
|
| 181 |
+
}
|
| 182 |
+
});
|
| 183 |
+
|
| 184 |
+
})();
|
static/vendor/bootstrap/css/bootstrap.min.css
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
templates/index.html
ADDED
|
@@ -0,0 +1,663 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="id" class="scroll-smooth">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>BPVP Sorong - Website Satu Halaman</title>
|
| 7 |
+
|
| 8 |
+
<!-- Memuat Tailwind CSS -->
|
| 9 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 10 |
+
|
| 11 |
+
<!-- Memuat Google Fonts: Inter -->
|
| 12 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 13 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 14 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
| 15 |
+
|
| 16 |
+
<!-- Memuat Font Awesome untuk Ikon -->
|
| 17 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css">
|
| 18 |
+
|
| 19 |
+
<!-- Konfigurasi warna kustom untuk Tailwind -->
|
| 20 |
+
<script>
|
| 21 |
+
tailwind.config = {
|
| 22 |
+
theme: {
|
| 23 |
+
extend: {
|
| 24 |
+
colors: {
|
| 25 |
+
'pastel-blue': '#334155', // Biru pastel
|
| 26 |
+
'pastel-pink': '#f3d6e4', // Pink pastel
|
| 27 |
+
'dark-text': '#334155',
|
| 28 |
+
},
|
| 29 |
+
fontFamily: {
|
| 30 |
+
sans: ['Inter', 'sans-serif'],
|
| 31 |
+
},
|
| 32 |
+
}
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
</script>
|
| 36 |
+
|
| 37 |
+
<!-- Menghubungkan ke file CSS eksternal -->
|
| 38 |
+
<link rel="stylesheet" href="../static/css/style.css">
|
| 39 |
+
</head>
|
| 40 |
+
<body class="bg-gray-50 font-sans text-dark-text">
|
| 41 |
+
|
| 42 |
+
<!-- BAGIAN HEADER -->
|
| 43 |
+
<header id="header" class="fixed top-0 left-0 right-0 z-50 transition-all duration-300 bg-white/50 backdrop-blur-sm shadow-md">
|
| 44 |
+
<nav class="container mx-auto px-6 py-4 flex justify-between items-center">
|
| 45 |
+
<!-- Logo di Kiri -->
|
| 46 |
+
<div class="flex items-center space-x-4">
|
| 47 |
+
<div class="relative h-14 w-14">
|
| 48 |
+
<img id="logo-dark" src="/static/Galeri/logo.png" alt="Logo BPVP Sorong" class="h-14 w-14 absolute top-0 left-0 transition-opacity duration-300 opacity-100">
|
| 49 |
+
<img id="logo-white" src="/static/Galeri/logo1.png" alt="Logo BPVP Sorong Putih" class="h-14 w-14 absolute top-0 left-0 transition-opacity duration-300 opacity-0">
|
| 50 |
+
</div>
|
| 51 |
+
<div>
|
| 52 |
+
<p class="text-sm font-medium text-dark-text tracking-wider">KEMENTERIAN KETENAGAKERJAAN</p>
|
| 53 |
+
<p class="text-2xl font-bold text-dark-text">BPVP SORONG</p>
|
| 54 |
+
</div>
|
| 55 |
+
</div>
|
| 56 |
+
|
| 57 |
+
<!-- Menu di Kanan -->
|
| 58 |
+
<ul class="hidden md:flex items-center space-x-8">
|
| 59 |
+
<li><a href="#beranda" class="text-dark-text hover:text-pastel-blue font-semibold">Beranda</a></li>
|
| 60 |
+
<li><a href="#profil" class="text-dark-text hover:text-pastel-blue font-semibold">Profil</a></li>
|
| 61 |
+
<li><a href="#pelatihan" class="text-dark-text hover:text-pastel-blue font-semibold">Pelatihan</a></li>
|
| 62 |
+
<li><a href="#kontak" class="text-dark-text hover:text-pastel-blue font-semibold">Kontak</a></li>
|
| 63 |
+
</ul>
|
| 64 |
+
<!-- Tombol Menu Mobile -->
|
| 65 |
+
<button id="mobile-menu-button" class="md:hidden text-dark-text">
|
| 66 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
| 67 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16m-7 6h7" />
|
| 68 |
+
</svg>
|
| 69 |
+
</button>
|
| 70 |
+
</nav>
|
| 71 |
+
<!-- Menu Mobile Dropdown -->
|
| 72 |
+
<div id="mobile-menu" class="hidden md:hidden bg-white/90 backdrop-blur-sm rounded-b-xl overflow-hidden">
|
| 73 |
+
<a href="#beranda" class="block py-2 px-6 text-dark-text hover:bg-pastel-blue/50">Beranda</a>
|
| 74 |
+
<a href="#profil" class="block py-2 px-6 text-dark-text hover:bg-pastel-blue/50">Profil</a>
|
| 75 |
+
<a href="#pelatihan" class="block py-2 px-6 text-dark-text hover:bg-pastel-blue/50">Pelatihan</a>
|
| 76 |
+
<a href="#kontak" class="block py-2 px-6 text-dark-text hover:bg-pastel-blue/50">Kontak</a>
|
| 77 |
+
</div>
|
| 78 |
+
</header>
|
| 79 |
+
|
| 80 |
+
<main>
|
| 81 |
+
<!-- BAGIAN BERANDA (HERO SECTION DENGAN SLIDER) -->
|
| 82 |
+
<section id="beranda" class="relative w-full h-screen overflow-hidden">
|
| 83 |
+
<!-- Kontainer untuk gambar slider -->
|
| 84 |
+
<div id="slider-container" class="flex w-full h-full transition-transform duration-500 ease-in-out">
|
| 85 |
+
<!-- Gambar 1 -->
|
| 86 |
+
<div class="w-full h-full flex-shrink-0 bg-cover bg-center" style="background-image: url('/static/Galeri/hero1.jpg');">
|
| 87 |
+
<div class="w-full h-full bg-black/40 flex items-center justify-center">
|
| 88 |
+
<div class="text-center text-white p-4">
|
| 89 |
+
<h1 class="text-4xl md:text-6xl font-bold mb-4">Membangun Kompetensi Unggul</h1>
|
| 90 |
+
<p class="text-lg md:text-xl max-w-2xl">Mencetak tenaga kerja profesional dan berdaya saing tinggi di tanah Papua.</p>
|
| 91 |
+
</div>
|
| 92 |
+
</div>
|
| 93 |
+
</div>
|
| 94 |
+
<!-- Gambar 2 -->
|
| 95 |
+
<div class="w-full h-full flex-shrink-0 bg-cover bg-center" style="background-image: url('/static/Galeri/hero2.jpg');">
|
| 96 |
+
<div class="w-full h-full bg-black/40 flex items-center justify-center">
|
| 97 |
+
<div class="text-center text-white p-4">
|
| 98 |
+
<h1 class="text-4xl md:text-6xl font-bold mb-4">Didukung Fasilitas Modern</h1>
|
| 99 |
+
<p class="text-lg md:text-xl max-w-2xl">Peralatan dan ruang praktik standar industri untuk hasil belajar maksimal.</p>
|
| 100 |
+
</div>
|
| 101 |
+
</div>
|
| 102 |
+
</div>
|
| 103 |
+
<!-- Gambar 3 -->
|
| 104 |
+
<div class="w-full h-full flex-shrink-0 bg-cover bg-center" style="background-image: url('/static/Galeri/hero3.jpg');">
|
| 105 |
+
<div class="w-full h-full bg-black/40 flex items-center justify-center">
|
| 106 |
+
<div class="text-center text-white p-4">
|
| 107 |
+
<h1 class="text-4xl md:text-6xl font-bold mb-4">Lulusan Siap Kerja</h1>
|
| 108 |
+
<p class="text-lg md:text-xl max-w-2xl">Program pelatihan relevan dengan kebutuhan dunia usaha dan industri.</p>
|
| 109 |
+
</div>
|
| 110 |
+
</div>
|
| 111 |
+
</div>
|
| 112 |
+
</div>
|
| 113 |
+
|
| 114 |
+
<!-- Tombol Navigasi Slider -->
|
| 115 |
+
<button id="prevBtn" class="absolute top-1/2 left-4 transform -translate-y-1/2 bg-white/40 p-2 rounded-full hover:bg-white/80">
|
| 116 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-dark-text" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" /></svg>
|
| 117 |
+
</button>
|
| 118 |
+
<button id="nextBtn" class="absolute top-1/2 right-4 transform -translate-y-1/2 bg-white/40 p-2 rounded-full hover:bg-white/80">
|
| 119 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-dark-text" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /></svg>
|
| 120 |
+
</button>
|
| 121 |
+
|
| 122 |
+
<!-- Indikator Dots -->
|
| 123 |
+
<div id="dots-container" class="absolute bottom-8 left-1/2 -translate-x-1/2 flex space-x-3"></div>
|
| 124 |
+
</section>
|
| 125 |
+
|
| 126 |
+
<!-- BAGIAN PROFIL -->
|
| 127 |
+
<section id="profil" class="py-24 bg-white">
|
| 128 |
+
<div class="container mx-auto px-6 grid md:grid-cols-2 gap-16 items-center">
|
| 129 |
+
<!-- Kolom Kiri: Teks -->
|
| 130 |
+
<div>
|
| 131 |
+
<h2 class="text-4xl font-bold mb-8 text-left">Tentang Kami</h2>
|
| 132 |
+
<div class="border-l-4 border-pastel-blue pl-6 space-y-6">
|
| 133 |
+
<p class="text-lg leading-relaxed">
|
| 134 |
+
Balai Pelatihan Vokasi dan Produktivitas Sorong atau di singkat BPVP Sorong didirikan berdasarkan Permenaker No 1 Tahun 2022 tentang Organisasi dan Tata Kerja Unit Pelaksana Teknis di Kementerian Ketenagakerjaan.
|
| 135 |
+
</p>
|
| 136 |
+
<p class="text-lg leading-relaxed">
|
| 137 |
+
Balai Latihan Kerja Sorong yang selanjutnya disebut BLK Sorong telah berubah nama menjadi Balai Pelatihan Vokasi dan Produktivitas Sorong atau di singkat BPVP Sorong yang merupakan Unit Pelaksana Teknis Pusat (UPTP) Bidang Pelatihan Vokasi dan Produktivitas berada di bawah dan bertanggung jawab kepada Direktorat Jenderal Pembinaan Pelatihan Vokasi dan Produktivitas.
|
| 138 |
+
</p>
|
| 139 |
+
</div>
|
| 140 |
+
</div>
|
| 141 |
+
<!-- Kolom Kanan: Gambar -->
|
| 142 |
+
<div class="relative h-96 md:h-auto md:min-h-[30rem]">
|
| 143 |
+
<img src="/static/Galeri/profil.png" alt="Gedung BPVP Sorong" class="absolute top-0 left-1/2 -translate-x-1/2 w-[90%] md:w-full max-w-lg h-auto rounded-xl shadow-2xl object-cover">
|
| 144 |
+
</div>
|
| 145 |
+
</div>
|
| 146 |
+
</section>
|
| 147 |
+
|
| 148 |
+
<!-- BAGIAN PELATIHAN -->
|
| 149 |
+
<section id="pelatihan" class="py-16 md:py-24 bg-gray-100">
|
| 150 |
+
<div class="container mx-auto px-6">
|
| 151 |
+
<h2 class="text-3xl md:text-4xl font-bold mb-12 text-center">Program Pelatihan Kami</h2>
|
| 152 |
+
<!-- Mengubah grid menjadi 2 kolom di HP dan menyesuaikan jarak (gap) -->
|
| 153 |
+
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 md:gap-6">
|
| 154 |
+
|
| 155 |
+
<!-- Kartu Layanan: Teknik Las -->
|
| 156 |
+
<div class="bg-white p-4 rounded-lg shadow-md flex flex-col text-center items-center h-full">
|
| 157 |
+
<div class="flex-shrink-0 mb-3">
|
| 158 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
| 159 |
+
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
| 160 |
+
</svg>
|
| 161 |
+
</div>
|
| 162 |
+
<div class="flex flex-col justify-between flex-grow">
|
| 163 |
+
<h3 class="text-sm font-bold text-dark-text leading-tight">PELATIHAN TEKNIK LAS</h3>
|
| 164 |
+
<p class="text-xs text-gray-500 mt-2">Jumlah Lulusan: 144</p>
|
| 165 |
+
</div>
|
| 166 |
+
</div>
|
| 167 |
+
|
| 168 |
+
<!-- Kartu Layanan: Teknik Listrik -->
|
| 169 |
+
<div class="bg-white p-4 rounded-lg shadow-md flex flex-col text-center items-center h-full">
|
| 170 |
+
<div class="flex-shrink-0 mb-3">
|
| 171 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-yellow-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
| 172 |
+
<path stroke-linecap="round" stroke-linejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
| 173 |
+
</svg>
|
| 174 |
+
</div>
|
| 175 |
+
<div class="flex flex-col justify-between flex-grow">
|
| 176 |
+
<h3 class="text-sm font-bold text-dark-text leading-tight">PELATIHAN TEKNIK LISTRIK</h3>
|
| 177 |
+
<p class="text-xs text-gray-500 mt-2">Jumlah Lulusan: 144</p>
|
| 178 |
+
</div>
|
| 179 |
+
</div>
|
| 180 |
+
|
| 181 |
+
<!-- Kartu Layanan: Teknik Otomotif -->
|
| 182 |
+
<div class="bg-white p-4 rounded-lg shadow-md flex flex-col text-center items-center h-full">
|
| 183 |
+
<div class="flex-shrink-0 mb-3">
|
| 184 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-cyan-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
| 185 |
+
<path stroke-linecap="round" stroke-linejoin="round" d="M9 17a2 2 0 11-4 0 2 2 0 014 0zM19 17a2 2 0 11-4 0 2 2 0 014 0z" />
|
| 186 |
+
<path stroke-linecap="round" stroke-linejoin="round" d="M13 16V6a1 1 0 00-1-1H4a1 1 0 00-1 1v10l2-2h8a1 1 0 001-1z" />
|
| 187 |
+
</svg>
|
| 188 |
+
</div>
|
| 189 |
+
<div class="flex flex-col justify-between flex-grow">
|
| 190 |
+
<h3 class="text-sm font-bold text-dark-text leading-tight">PELATIHAN TEKNIK OTOMOTIF</h3>
|
| 191 |
+
<p class="text-xs text-gray-500 mt-2">Jumlah Lulusan: 240</p>
|
| 192 |
+
</div>
|
| 193 |
+
</div>
|
| 194 |
+
|
| 195 |
+
<!-- Kartu Layanan: Teknik Manufaktur -->
|
| 196 |
+
<div class="bg-white p-4 rounded-lg shadow-md flex flex-col text-center items-center h-full">
|
| 197 |
+
<div class="flex-shrink-0 mb-3">
|
| 198 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
| 199 |
+
<path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
| 200 |
+
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
| 201 |
+
</svg>
|
| 202 |
+
</div>
|
| 203 |
+
<div class="flex flex-col justify-between flex-grow">
|
| 204 |
+
<h3 class="text-sm font-bold text-dark-text leading-tight">PELATIHAN TEKNIK MANUFAKTUR</h3>
|
| 205 |
+
<p class="text-xs text-gray-500 mt-2">Jumlah Lulusan: 64</p>
|
| 206 |
+
</div>
|
| 207 |
+
</div>
|
| 208 |
+
|
| 209 |
+
<!-- Kartu Layanan: Teknik Elektronika -->
|
| 210 |
+
<div class="bg-white p-4 rounded-lg shadow-md flex flex-col text-center items-center h-full">
|
| 211 |
+
<div class="flex-shrink-0 mb-3">
|
| 212 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-indigo-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
| 213 |
+
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" />
|
| 214 |
+
</svg>
|
| 215 |
+
</div>
|
| 216 |
+
<div class="flex flex-col justify-between flex-grow">
|
| 217 |
+
<h3 class="text-sm font-bold text-dark-text leading-tight">PELATIHAN TEKNIK ELEKTRONIKA</h3>
|
| 218 |
+
<p class="text-xs text-gray-500 mt-2">Jumlah Lulusan: 112</p>
|
| 219 |
+
</div>
|
| 220 |
+
</div>
|
| 221 |
+
|
| 222 |
+
<!-- Kartu Layanan: Kejuruan TIK -->
|
| 223 |
+
<div class="bg-white p-4 rounded-lg shadow-md flex flex-col text-center items-center h-full">
|
| 224 |
+
<div class="flex-shrink-0 mb-3">
|
| 225 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-orange-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
| 226 |
+
<path stroke-linecap="round" stroke-linejoin="round" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
| 227 |
+
</svg>
|
| 228 |
+
</div>
|
| 229 |
+
<div class="flex flex-col justify-between flex-grow">
|
| 230 |
+
<h3 class="text-sm font-bold text-dark-text leading-tight">PELATIHAN KEJURUAN TIK</h3>
|
| 231 |
+
<p class="text-xs text-gray-500 mt-2">Jumlah Lulusan: 208</p>
|
| 232 |
+
</div>
|
| 233 |
+
</div>
|
| 234 |
+
|
| 235 |
+
<!-- Kartu Layanan: Kejuruan Processing -->
|
| 236 |
+
<div class="bg-white p-4 rounded-lg shadow-md flex flex-col text-center items-center h-full">
|
| 237 |
+
<div class="flex-shrink-0 mb-3">
|
| 238 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
| 239 |
+
<path stroke-linecap="round" stroke-linejoin="round" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
| 240 |
+
</svg>
|
| 241 |
+
</div>
|
| 242 |
+
<div class="flex flex-col justify-between flex-grow">
|
| 243 |
+
<h3 class="text-sm font-bold text-dark-text leading-tight">PELATIHAN KEJURUAN PROCESSING</h3>
|
| 244 |
+
<p class="text-xs text-gray-500 mt-2">Jumlah Lulusan: 432</p>
|
| 245 |
+
</div>
|
| 246 |
+
</div>
|
| 247 |
+
|
| 248 |
+
<!-- Kartu Layanan: Pariwisata -->
|
| 249 |
+
<div class="bg-white p-4 rounded-lg shadow-md flex flex-col text-center items-center h-full">
|
| 250 |
+
<div class="flex-shrink-0 mb-3">
|
| 251 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-yellow-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
| 252 |
+
<path stroke-linecap="round" stroke-linejoin="round" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2h8a2 2 0 002-2v-1a2 2 0 012-2h1.945M7.88 15.12A3 3 0 015.33 12.033m13.34 0A3 3 0 0118.12 15.12M12 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
| 253 |
+
</svg>
|
| 254 |
+
</div>
|
| 255 |
+
<div class="flex flex-col justify-between flex-grow">
|
| 256 |
+
<h3 class="text-sm font-bold text-dark-text leading-tight">PELATIHAN KEJURUAN PARIWISATA</h3>
|
| 257 |
+
<p class="text-xs text-gray-500 mt-2">Jumlah Lulusan: 96</p>
|
| 258 |
+
</div>
|
| 259 |
+
</div>
|
| 260 |
+
|
| 261 |
+
<!-- Kartu Layanan: Bangunan -->
|
| 262 |
+
<div class="bg-white p-4 rounded-lg shadow-md flex flex-col text-center items-center h-full">
|
| 263 |
+
<div class="flex-shrink-0 mb-3">
|
| 264 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-cyan-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
| 265 |
+
<path stroke-linecap="round" stroke-linejoin="round" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
| 266 |
+
</svg>
|
| 267 |
+
</div>
|
| 268 |
+
<div class="flex flex-col justify-between flex-grow">
|
| 269 |
+
<h3 class="text-sm font-bold text-dark-text leading-tight">PELATIHAN KEJURUAN BANGUNAN</h3>
|
| 270 |
+
<p class="text-xs text-gray-500 mt-2">Jumlah Lulusan: 176</p>
|
| 271 |
+
</div>
|
| 272 |
+
</div>
|
| 273 |
+
|
| 274 |
+
<!-- Kartu Layanan: Tata Rias -->
|
| 275 |
+
<div class="bg-white p-4 rounded-lg shadow-md flex flex-col text-center items-center h-full">
|
| 276 |
+
<div class="flex-shrink-0 mb-3">
|
| 277 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
| 278 |
+
<path stroke-linecap="round" stroke-linejoin="round" d="M14.121 15.536c-1.171 1.952-3.07 1.952-4.242 0-1.172-1.953-1.172-5.119 0-7.072 1.171-1.952 3.07-1.952 4.242 0 1.172 1.953 1.172 5.119 0 7.072z" />
|
| 279 |
+
<path stroke-linecap="round" stroke-linejoin="round" d="M12 10.5v5.25m-2.25-.916a2.25 2.25 0 00-1.99 2.185 2.25 2.25 0 002.25 2.25 2.25 2.25 0 002.25-2.25 2.25 2.25 0 00-1.423-2.065" />
|
| 280 |
+
</svg>
|
| 281 |
+
</div>
|
| 282 |
+
<div class="flex flex-col justify-between flex-grow">
|
| 283 |
+
<h3 class="text-sm font-bold text-dark-text leading-tight">PELATIHAN KEJURUAN TATA RIAS</h3>
|
| 284 |
+
<p class="text-xs text-gray-500 mt-2">Jumlah Lulusan: 160</p>
|
| 285 |
+
</div>
|
| 286 |
+
</div>
|
| 287 |
+
|
| 288 |
+
<!-- Kartu Layanan: Bisnis & Manajemen -->
|
| 289 |
+
<div class="bg-white p-4 rounded-lg shadow-md flex flex-col text-center items-center h-full">
|
| 290 |
+
<div class="flex-shrink-0 mb-3">
|
| 291 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-yellow-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
| 292 |
+
<path stroke-linecap="round" stroke-linejoin="round" d="M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
| 293 |
+
</svg>
|
| 294 |
+
</div>
|
| 295 |
+
<div class="flex flex-col justify-between flex-grow">
|
| 296 |
+
<h3 class="text-sm font-bold text-dark-text leading-tight">PELATIHAN BISNIS DAN MANAJEMEN</h3>
|
| 297 |
+
<p class="text-xs text-gray-500 mt-2">Jumlah Lulusan: 160</p>
|
| 298 |
+
</div>
|
| 299 |
+
</div>
|
| 300 |
+
|
| 301 |
+
<!-- Kartu Layanan: Fashion Technology -->
|
| 302 |
+
<div class="bg-white p-4 rounded-lg shadow-md flex flex-col text-center items-center h-full">
|
| 303 |
+
<div class="flex-shrink-0 mb-3">
|
| 304 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-cyan-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
| 305 |
+
<path stroke-linecap="round" stroke-linejoin="round" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
| 306 |
+
</svg>
|
| 307 |
+
</div>
|
| 308 |
+
<div class="flex flex-col justify-between flex-grow">
|
| 309 |
+
<h3 class="text-sm font-bold text-dark-text leading-tight">PELATIHAN FASHION TECHNOLOGY</h3>
|
| 310 |
+
<p class="text-xs text-gray-500 mt-2">Jumlah Lulusan: 256</p>
|
| 311 |
+
</div>
|
| 312 |
+
</div>
|
| 313 |
+
</div>
|
| 314 |
+
</div>
|
| 315 |
+
</section>
|
| 316 |
+
<section id="kontak" class="py-24 bg-pastel-blue">
|
| 317 |
+
<div class="container mx-auto px-6 grid md:grid-cols-2 gap-16 items-center">
|
| 318 |
+
<!-- Kolom Kiri: Info Kontak -->
|
| 319 |
+
<div class="text-white text-center md:text-left">
|
| 320 |
+
<h2 class="text-4xl font-bold mb-6">Kunjungi Kami</h2>
|
| 321 |
+
<p class="text-lg mb-8 leading-relaxed">
|
| 322 |
+
Punya pertanyaan, saran, atau ingin mendaftar program pelatihan kami? Jangan ragu untuk menghubungi kami atau datang langsung ke alamat kami yang tertera di peta.
|
| 323 |
+
</p>
|
| 324 |
+
<div class="bg-white/20 p-6 rounded-xl inline-block">
|
| 325 |
+
<p class="mb-2"><strong>Alamat:</strong> Jl. Basuki Rahmat, Sawagumu, Kota Sorong, Papua Barat Daya - 98412</p>
|
| 326 |
+
<p class="mb-2"><strong>Email:</strong> blksorong@kemnaker.go.id</p>
|
| 327 |
+
<p><strong>Telepon:</strong> (0951) 324776</p>
|
| 328 |
+
</div>
|
| 329 |
+
</div>
|
| 330 |
+
|
| 331 |
+
<!-- Kolom Kanan: Peta Lokasi -->
|
| 332 |
+
<div class="w-full h-96 md:h-full rounded-xl overflow-hidden shadow-lg">
|
| 333 |
+
<iframe
|
| 334 |
+
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d3989.287893325603!2d131.2902648749652!3d-0.9015993990428178!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x2d595469440307cb%3A0x7055c82432857d42!2sBalai%20Latihan%20Kerja%20Industri%20Sorong!5e0!3m2!1sid!2sid!4v1725944519491!5m2!1sid!2sid"
|
| 335 |
+
width="100%"
|
| 336 |
+
height="100%"
|
| 337 |
+
style="border:0;"
|
| 338 |
+
allowfullscreen=""
|
| 339 |
+
loading="lazy"
|
| 340 |
+
referrerpolicy="no-referrer-when-downgrade">
|
| 341 |
+
</iframe>
|
| 342 |
+
</div>
|
| 343 |
+
</div>
|
| 344 |
+
</section>
|
| 345 |
+
</main>
|
| 346 |
+
|
| 347 |
+
<!-- ICON BANTUAN Pojok Kanan Bawah -->
|
| 348 |
+
<a href="#" id="open-chat-btn" class="fixed bottom-5 right-5 bg-pastel-blue p-3 rounded-full shadow-lg hover:bg-blue-500 transition-transform hover:scale-110">
|
| 349 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
| 350 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.546-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
| 351 |
+
</svg>
|
| 352 |
+
</a>
|
| 353 |
+
|
| 354 |
+
<!-- CHATBOT MODAL -->
|
| 355 |
+
<div id="chat-modal" class="chat-modal-overlay">
|
| 356 |
+
<div class="chat-modal-content">
|
| 357 |
+
<div id="chatbot-widget-container">
|
| 358 |
+
<div class="chatbot-container" id="main-container">
|
| 359 |
+
<div id="chatbot-header">
|
| 360 |
+
<button id="end-chat-button" title="Akhiri & Hapus Sesi"><i class="fas fa-times"></i></button>
|
| 361 |
+
</div>
|
| 362 |
+
<div id="chatbot">
|
| 363 |
+
<div id="conversation"></div>
|
| 364 |
+
<form id="input-form">
|
| 365 |
+
<div class="message-container">
|
| 366 |
+
<input id="input-field" type="text" placeholder="Ketik pertanyaan Anda...">
|
| 367 |
+
<button id="submit-button" type="submit"><i class="fas fa-paper-plane"></i></button>
|
| 368 |
+
</div>
|
| 369 |
+
</form>
|
| 370 |
+
</div>
|
| 371 |
+
</div>
|
| 372 |
+
</div>
|
| 373 |
+
</div>
|
| 374 |
+
</div>
|
| 375 |
+
|
| 376 |
+
<!-- CUSTOM ALERT MODAL -->
|
| 377 |
+
<div id="custom-alert-modal" class="modal">
|
| 378 |
+
<div class="modal-content">
|
| 379 |
+
<p id="custom-alert-message"></p>
|
| 380 |
+
<button id="custom-alert-ok">OK</button>
|
| 381 |
+
</div>
|
| 382 |
+
</div>
|
| 383 |
+
|
| 384 |
+
<script>
|
| 385 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 386 |
+
// --- FUNGSI UNTUK SLIDER GAMBAR ---
|
| 387 |
+
const sliderContainer = document.getElementById('slider-container');
|
| 388 |
+
if (sliderContainer) {
|
| 389 |
+
const prevBtn = document.getElementById('prevBtn');
|
| 390 |
+
const nextBtn = document.getElementById('nextBtn');
|
| 391 |
+
const dotsContainer = document.getElementById('dots-container');
|
| 392 |
+
const images = sliderContainer.children;
|
| 393 |
+
const totalImages = images.length;
|
| 394 |
+
let currentIndex = 0;
|
| 395 |
+
let slideInterval;
|
| 396 |
+
|
| 397 |
+
for (let i = 0; i < totalImages; i++) {
|
| 398 |
+
const dot = document.createElement('div');
|
| 399 |
+
dot.classList.add('w-3', 'h-3', 'rounded-full', 'cursor-pointer', 'bg-white/50', 'transition-colors');
|
| 400 |
+
dot.addEventListener('click', () => goToSlide(i));
|
| 401 |
+
dotsContainer.appendChild(dot);
|
| 402 |
+
}
|
| 403 |
+
const dots = dotsContainer.children;
|
| 404 |
+
|
| 405 |
+
function updateSlider() {
|
| 406 |
+
sliderContainer.style.transform = `translateX(-${currentIndex * 100}%)`;
|
| 407 |
+
Array.from(dots).forEach((dot, i) => {
|
| 408 |
+
dot.classList.toggle('bg-white', i === currentIndex);
|
| 409 |
+
dot.classList.toggle('bg-white/50', i !== currentIndex);
|
| 410 |
+
});
|
| 411 |
+
}
|
| 412 |
+
|
| 413 |
+
function goToSlide(index) {
|
| 414 |
+
currentIndex = index;
|
| 415 |
+
updateSlider();
|
| 416 |
+
resetInterval();
|
| 417 |
+
}
|
| 418 |
+
|
| 419 |
+
function resetInterval() {
|
| 420 |
+
clearInterval(slideInterval);
|
| 421 |
+
slideInterval = setInterval(() => {
|
| 422 |
+
currentIndex = (currentIndex + 1) % totalImages;
|
| 423 |
+
updateSlider();
|
| 424 |
+
}, 5000);
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
nextBtn.addEventListener('click', () => {
|
| 428 |
+
currentIndex = (currentIndex + 1) % totalImages;
|
| 429 |
+
updateSlider();
|
| 430 |
+
resetInterval();
|
| 431 |
+
});
|
| 432 |
+
|
| 433 |
+
prevBtn.addEventListener('click', () => {
|
| 434 |
+
currentIndex = (currentIndex - 1 + totalImages) % totalImages;
|
| 435 |
+
updateSlider();
|
| 436 |
+
resetInterval();
|
| 437 |
+
});
|
| 438 |
+
|
| 439 |
+
updateSlider();
|
| 440 |
+
resetInterval();
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
// --- FUNGSI UNTUK MENU MOBILE ---
|
| 444 |
+
const mobileMenuButton = document.getElementById('mobile-menu-button');
|
| 445 |
+
const mobileMenu = document.getElementById('mobile-menu');
|
| 446 |
+
if(mobileMenuButton && mobileMenu){
|
| 447 |
+
mobileMenuButton.addEventListener('click', () => {
|
| 448 |
+
mobileMenu.classList.toggle('hidden');
|
| 449 |
+
});
|
| 450 |
+
mobileMenu.querySelectorAll('a').forEach(link => {
|
| 451 |
+
link.addEventListener('click', () => {
|
| 452 |
+
mobileMenu.classList.add('hidden');
|
| 453 |
+
});
|
| 454 |
+
});
|
| 455 |
+
}
|
| 456 |
+
|
| 457 |
+
|
| 458 |
+
// --- FUNGSI UNTUK CHATBOT ---
|
| 459 |
+
const openChatBtn = document.getElementById('open-chat-btn');
|
| 460 |
+
const chatModal = document.getElementById('chat-modal');
|
| 461 |
+
let isHistoryLoaded = false;
|
| 462 |
+
const conversation = document.getElementById('conversation');
|
| 463 |
+
const inputForm = document.getElementById('input-form');
|
| 464 |
+
const inputField = document.getElementById('input-field');
|
| 465 |
+
const endChatBtn = document.getElementById('end-chat-button');
|
| 466 |
+
const customAlertModal = document.getElementById('custom-alert-modal');
|
| 467 |
+
const customAlertMessage = document.getElementById('custom-alert-message');
|
| 468 |
+
const customAlertOk = document.getElementById('custom-alert-ok');
|
| 469 |
+
const API_BASE_URL = 'http://127.0.0.1:5000';
|
| 470 |
+
|
| 471 |
+
if (openChatBtn) {
|
| 472 |
+
openChatBtn.addEventListener('click', (e) => {
|
| 473 |
+
e.preventDefault();
|
| 474 |
+
chatModal.style.display = 'flex';
|
| 475 |
+
if (!isHistoryLoaded) {
|
| 476 |
+
showInitialMessage();
|
| 477 |
+
isHistoryLoaded = true;
|
| 478 |
+
}
|
| 479 |
+
});
|
| 480 |
+
}
|
| 481 |
+
|
| 482 |
+
const getCurrentTime = () => new Date().toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' });
|
| 483 |
+
|
| 484 |
+
const showAlert = (message, onOk) => {
|
| 485 |
+
if (customAlertMessage) customAlertMessage.textContent = message;
|
| 486 |
+
if (customAlertModal) customAlertModal.style.display = 'flex';
|
| 487 |
+
|
| 488 |
+
// Clone and replace the OK button to remove old event listeners
|
| 489 |
+
const newOkButton = customAlertOk.cloneNode(true);
|
| 490 |
+
customAlertOk.parentNode.replaceChild(newOkButton, customAlertOk);
|
| 491 |
+
|
| 492 |
+
// Add the new listener
|
| 493 |
+
newOkButton.addEventListener('click', () => {
|
| 494 |
+
if (customAlertModal) customAlertModal.style.display = 'none';
|
| 495 |
+
if (onOk) onOk();
|
| 496 |
+
}, { once: true });
|
| 497 |
+
};
|
| 498 |
+
|
| 499 |
+
// --- FUNGSI createMessage DENGAN FORMATTING YANG LEBIH BAIK ---
|
| 500 |
+
const createMessage = (content, sender, time = getCurrentTime()) => {
|
| 501 |
+
const message = document.createElement('div');
|
| 502 |
+
message.classList.add('chatbot-message', sender);
|
| 503 |
+
|
| 504 |
+
const contentDiv = document.createElement('div');
|
| 505 |
+
contentDiv.classList.add('chatbot-text');
|
| 506 |
+
|
| 507 |
+
// --- Improved Markdown-like Parser ---
|
| 508 |
+
|
| 509 |
+
// 1. First, process inline formatting like **bold** across the entire content.
|
| 510 |
+
// This is more robust than processing line-by-line.
|
| 511 |
+
// The regex /\*\*(.*?)\*\*/g correctly finds text surrounded by double asterisks.
|
| 512 |
+
let processedContent = String(content).replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
| 513 |
+
|
| 514 |
+
// 2. Normalize newlines. Server responses might have escaped newlines ('\\n').
|
| 515 |
+
// We replace them with actual newline characters ('\n') for consistent splitting.
|
| 516 |
+
processedContent = processedContent.replace(/\\n/g, '\n');
|
| 517 |
+
const lines = processedContent.split('\n');
|
| 518 |
+
|
| 519 |
+
let currentList = null;
|
| 520 |
+
let listType = null; // Can be 'ul' or 'ol'
|
| 521 |
+
|
| 522 |
+
// Helper function to append the current list to the main content div.
|
| 523 |
+
// This avoids duplicating code and ensures any open list is added before a new element.
|
| 524 |
+
const appendCurrentList = () => {
|
| 525 |
+
if (currentList) {
|
| 526 |
+
contentDiv.appendChild(currentList);
|
| 527 |
+
currentList = null;
|
| 528 |
+
listType = null;
|
| 529 |
+
}
|
| 530 |
+
};
|
| 531 |
+
|
| 532 |
+
lines.forEach(line => {
|
| 533 |
+
const trimmedLine = line.trim();
|
| 534 |
+
// ### for Subheadings (h3)
|
| 535 |
+
if (trimmedLine.startsWith('### ')) {
|
| 536 |
+
appendCurrentList();
|
| 537 |
+
const h3 = document.createElement('h3');
|
| 538 |
+
// The text already has <strong> tags from the initial processing.
|
| 539 |
+
h3.innerHTML = trimmedLine.substring(4);
|
| 540 |
+
h3.className = 'text-lg font-semibold mt-4 mb-2';
|
| 541 |
+
contentDiv.appendChild(h3);
|
| 542 |
+
}
|
| 543 |
+
// * or - for Unordered Lists (ul)
|
| 544 |
+
else if (trimmedLine.startsWith('* ') || trimmedLine.startsWith('- ')) {
|
| 545 |
+
if (listType !== 'ul') {
|
| 546 |
+
appendCurrentList();
|
| 547 |
+
currentList = document.createElement('ul');
|
| 548 |
+
// Using Tailwind classes for clean styling.
|
| 549 |
+
currentList.className = 'list-disc list-inside my-2 pl-4 space-y-1';
|
| 550 |
+
listType = 'ul';
|
| 551 |
+
}
|
| 552 |
+
const li = document.createElement('li');
|
| 553 |
+
li.innerHTML = trimmedLine.substring(2);
|
| 554 |
+
currentList.appendChild(li);
|
| 555 |
+
}
|
| 556 |
+
// 1., 2., etc. for Ordered Lists (ol)
|
| 557 |
+
// The regex /^\d+\.\s/ matches lines starting with a number, a dot, and a space.
|
| 558 |
+
else if (trimmedLine.match(/^\d+\.\s/)) {
|
| 559 |
+
if (listType !== 'ol') {
|
| 560 |
+
appendCurrentList();
|
| 561 |
+
currentList = document.createElement('ol');
|
| 562 |
+
currentList.className = 'list-decimal list-inside my-2 pl-4 space-y-1';
|
| 563 |
+
listType = 'ol';
|
| 564 |
+
}
|
| 565 |
+
const li = document.createElement('li');
|
| 566 |
+
li.innerHTML = trimmedLine.replace(/^\d+\.\s/, '');
|
| 567 |
+
currentList.appendChild(li);
|
| 568 |
+
}
|
| 569 |
+
// Any other non-empty line is a paragraph
|
| 570 |
+
else if (trimmedLine) {
|
| 571 |
+
appendCurrentList();
|
| 572 |
+
const p = document.createElement('p');
|
| 573 |
+
p.innerHTML = trimmedLine;
|
| 574 |
+
p.className = 'mb-3 last:mb-0'; // Increased margin for better paragraph separation
|
| 575 |
+
contentDiv.appendChild(p);
|
| 576 |
+
} else {
|
| 577 |
+
// An empty line signifies a paragraph break, so we just end any current list.
|
| 578 |
+
appendCurrentList();
|
| 579 |
+
}
|
| 580 |
+
});
|
| 581 |
+
|
| 582 |
+
// After the loop, append the last list if it exists.
|
| 583 |
+
appendCurrentList();
|
| 584 |
+
message.appendChild(contentDiv);
|
| 585 |
+
|
| 586 |
+
const footer = document.createElement('div');
|
| 587 |
+
footer.className = 'message-footer';
|
| 588 |
+
footer.innerHTML = `<span class="time">${time}</span>`;
|
| 589 |
+
message.appendChild(footer);
|
| 590 |
+
|
| 591 |
+
return message;
|
| 592 |
+
};
|
| 593 |
+
|
| 594 |
+
const addMessageToConversation = (message) => {
|
| 595 |
+
if (conversation) {
|
| 596 |
+
conversation.appendChild(message);
|
| 597 |
+
conversation.scrollTop = conversation.scrollHeight;
|
| 598 |
+
}
|
| 599 |
+
};
|
| 600 |
+
|
| 601 |
+
const showInitialMessage = async () => {
|
| 602 |
+
try {
|
| 603 |
+
const response = await fetch(`${API_BASE_URL}/load_history`);
|
| 604 |
+
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
| 605 |
+
const history = await response.json();
|
| 606 |
+
if (history && history.length > 0) {
|
| 607 |
+
history.forEach(item => {
|
| 608 |
+
const sender = item.sender === 'user' ? 'user-message' : 'chatbot';
|
| 609 |
+
addMessageToConversation(createMessage(item.message, sender));
|
| 610 |
+
});
|
| 611 |
+
} else {
|
| 612 |
+
addMessageToConversation(createMessage('Halo! Ada yang bisa saya bantu terkait dengan informasi di BPVP Sorong?', 'chatbot'));
|
| 613 |
+
}
|
| 614 |
+
} catch (error) {
|
| 615 |
+
console.error("Could not load history:", error);
|
| 616 |
+
addMessageToConversation(createMessage('Halo! Ada yang bisa saya bantu terkait dengan informasi di BPVP Sorong?', 'chatbot'));
|
| 617 |
+
}
|
| 618 |
+
};
|
| 619 |
+
|
| 620 |
+
if (inputForm) {
|
| 621 |
+
inputForm.addEventListener('submit', async (event) => {
|
| 622 |
+
event.preventDefault();
|
| 623 |
+
const input = inputField.value.trim();
|
| 624 |
+
if (!input) return;
|
| 625 |
+
inputField.value = '';
|
| 626 |
+
addMessageToConversation(createMessage(input, 'user-message'));
|
| 627 |
+
const typingMessage = createMessage('Sedang berpikir...', 'chatbot');
|
| 628 |
+
addMessageToConversation(typingMessage);
|
| 629 |
+
try {
|
| 630 |
+
const response = await fetch(`${API_BASE_URL}/get?msg=${encodeURIComponent(input)}`);
|
| 631 |
+
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
| 632 |
+
const result = await response.json();
|
| 633 |
+
typingMessage.remove();
|
| 634 |
+
addMessageToConversation(createMessage(result.error || result, 'chatbot'));
|
| 635 |
+
} catch (error) {
|
| 636 |
+
console.error('Error fetching AI response:', error);
|
| 637 |
+
typingMessage.remove();
|
| 638 |
+
const errorMessage = "Aduh, sepertinya ada kendala. Silahkan mencoba beberapa saat lagi.";
|
| 639 |
+
addMessageToConversation(createMessage(errorMessage, 'chatbot'));
|
| 640 |
+
}
|
| 641 |
+
});
|
| 642 |
+
}
|
| 643 |
+
|
| 644 |
+
const clearChatAction = async () => {
|
| 645 |
+
try {
|
| 646 |
+
await fetch(`${API_BASE_URL}/clear_history`, { method: 'POST' });
|
| 647 |
+
if (conversation) conversation.innerHTML = '';
|
| 648 |
+
if (chatModal) chatModal.style.display = 'none';
|
| 649 |
+
isHistoryLoaded = false;
|
| 650 |
+
} catch (error) {
|
| 651 |
+
showAlert("Gagal menghapus riwayat di server.");
|
| 652 |
+
}
|
| 653 |
+
};
|
| 654 |
+
|
| 655 |
+
if (endChatBtn) {
|
| 656 |
+
endChatBtn.addEventListener('click', () => {
|
| 657 |
+
showAlert("Anda yakin ingin mengakhiri sesi ini? Riwayat percakapan akan dihapus.", clearChatAction);
|
| 658 |
+
});
|
| 659 |
+
}
|
| 660 |
+
});
|
| 661 |
+
</script>
|
| 662 |
+
</body>
|
| 663 |
+
</html>
|