Spaces:
Sleeping
Sleeping
Upload 7 files
Browse files- app.py +76 -0
- hoax_detector_model.pkl +3 -0
- index.html +38 -0
- requirements.txt +0 -0
- script.js +38 -0
- style.css +155 -0
- tfidf_vectorizer.pkl +3 -0
app.py
ADDED
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# main.py (UPDATED VERSION)
|
2 |
+
|
3 |
+
import joblib
|
4 |
+
import re
|
5 |
+
from fastapi import FastAPI
|
6 |
+
from pydantic import BaseModel
|
7 |
+
# NEW: Import CORSMiddleware
|
8 |
+
from fastapi.middleware.cors import CORSMiddleware
|
9 |
+
|
10 |
+
# Initialize the FastAPI app
|
11 |
+
app = FastAPI()
|
12 |
+
|
13 |
+
# --- NEW: Add CORS Middleware ---
|
14 |
+
# This allows our front-end (running on a different "origin")
|
15 |
+
# to communicate with our back-end API.
|
16 |
+
origins = ["*"] # Allow all origins for simplicity in development
|
17 |
+
|
18 |
+
app.add_middleware(
|
19 |
+
CORSMiddleware,
|
20 |
+
allow_origins=origins,
|
21 |
+
allow_credentials=True,
|
22 |
+
allow_methods=["*"],
|
23 |
+
allow_headers=["*"],
|
24 |
+
)
|
25 |
+
# --- End of new code block ---
|
26 |
+
|
27 |
+
|
28 |
+
# Define the data model for the request body
|
29 |
+
class NewsTitle(BaseModel):
|
30 |
+
title: str
|
31 |
+
|
32 |
+
# --- Preprocessing Functions and Stopwords ---
|
33 |
+
stopwords_indonesia = [
|
34 |
+
'ada', 'adalah', 'adanya', 'adapun', 'agak', 'agaknya', 'agar', 'akan', 'akankah', 'akhir', 'akhiri', 'akhirnya', 'aku', 'akulah', 'amat', 'amatlah', 'anda', 'andalah', 'antar', 'antara', 'antaranya', 'apa', 'apaan', 'apabila', 'apakah', 'apalagi', 'apatah', 'artinya', 'asal', 'asalkan', 'atas', 'atau', 'ataukah', 'ataupun', 'awal', 'awalnya', 'bagai', 'bagaikan', 'bagaimana', 'bagaimanakah', 'bagaimanapun', 'bagi', 'bagian', 'bahkan', 'bahwa', 'bahwasanya', 'baik', 'bakal', 'bakalan', 'balik', 'banyak', 'bapak', 'baru', 'bawah', 'beberapa', 'begini', 'beginian', 'beginikah', 'beginilah', 'begitu', 'begitukah', 'begitulah', 'begitupun', 'bekerja', 'belakang', 'belakangan', 'belum', 'belumlah', 'benar', 'benarkah', 'benarlah', 'berada', 'berakhir', 'berakhirlah', 'berakhirnya', 'berapa', 'berapakah', 'berapalah', 'berapapun', 'berarti', 'berawal', 'berbagai', 'berdatangan', 'beri', 'berikan', 'berikut', 'berikutnya', 'berjumlah', 'berkali-kali', 'berkata', 'berkehendak', 'berkeinginan', 'berkenaan', 'berlainan', 'berlalu', 'berlangsung', 'berlebihan', 'bermacam', 'bermacam-macam', 'bermaksud', 'bermula', 'bersama', 'bersama-sama', 'bersiap', 'bersiap-siap', 'bertanya', 'bertanya-tanya', 'berturut', 'berturut-turut', 'bertutur', 'berujar', 'berupa', 'besar', 'betul', 'betulkah', 'biasa', 'biasanya', 'bila', 'bilakah', 'bisa', 'bisakah', 'boleh', 'bolehkah', 'bolelah', 'buat', 'bukan', 'bukankah', 'bukanlah', 'bukannya', 'bulan', 'bung', 'cara', 'caranya', 'cukup', 'cukupkah', 'cukuplah', 'cuma', 'dahulu', 'dalam', 'dan', 'dapat', 'dari', 'daripada', 'datang', 'demi', 'demikian', 'demikianlah', 'dengan', 'depan', 'di', 'dia', 'diakhiri', 'diakhirinya', 'dialah', 'diantara', 'diantaranya', 'diberi', 'diberikan', 'diberikannya', 'dibuat', 'dibuatnya', 'didapat', 'didatangkan', 'digunakan', 'diibaratkan', 'diibaratkannya', 'diingat', 'diingatkan', 'diinginkan', 'dijawab', 'dijelaskan', 'dijelaskannya', 'dikarenakan', 'dikatakan', 'dikatakannya', 'dikerjakan', 'diketahui', 'diketahuinya', 'dikiranya', 'dilakukan', 'dilaluinya', 'dilihat', 'dilihatnya', 'dimaksud', 'dimaksudkan', 'dimaksudkannya', 'dimaksudnya', 'diminta', 'dimintai', 'dimisalkan', 'dimulai', 'dimulailah', 'dimulainya', 'dimungkinkan', 'dini', 'dipastikan', 'diperbuat', 'diperbuatnya', 'dipergunakan', 'diperkirakan', 'diperlihatkan', 'diperlukan', 'diperlukannya', 'dipersoalkan', 'dipertanyakan', 'dipunyai', 'diri', 'dirinya', 'disampaikan', 'disebut', 'disebutkan', 'disebutkannya', 'disini', 'disinilah', 'ditambahkan', 'ditandaskan', 'ditanya', 'ditanyai', 'ditanyakan', 'ditegaskan', 'ditujukan', 'ditunjuk', 'ditunjuki', 'ditunjukkan', 'ditunjukkannya', 'dituturkan', 'dituturkannya', 'diucapkan', 'diucapkannya', 'diungkapkan', 'dong', 'dua', 'dulu', 'empat', 'enggak', 'enggaknya', 'entah', 'entahlah', 'guna', 'gunakan', 'hal', 'hampir', 'hanya', 'hanyalah', 'hari', 'harus', 'haruslah', 'harusnya', 'hendak', 'hendaklah', 'hendaknya', 'hingga', 'ia', 'ialah', 'ibarat', 'ibaratnya', 'ibu', 'ikut', 'ingat', 'ingat-ingat', 'ingin', 'inginkah', 'inginkan', 'ini', 'inikah', 'inilah', 'itu', 'itukah', 'itulah', 'jadi', 'jadilah', 'jadinya', 'jangan', 'jangankan', 'janganlah', 'jauh', 'jawab', 'jawaban', 'jawabnya', 'jelas', 'jelaskan', 'jelaslah', 'jelasnya', 'jika', 'jikalau', 'juga', 'jumlah', 'jumlahnya', 'justru', 'kala', 'kalau', 'kalaulah', 'kalaupun', 'kali', 'kalian', 'kami', 'kamilah', 'kamu', 'kamulah', 'kan', 'kapan', 'kapankah', 'kapanpun', 'karena', 'karenanya', 'kasus', 'kata', 'katakan', 'katakanlah', 'katanya', 'ke', 'keadaan', 'kebetulan', 'kecil', 'kedua', 'keduanya', 'keinginan', 'kelak', 'kelima', 'keluar', 'kembali', 'kemudian', 'kemungkinan', 'kemungkinannya', 'kenapa', 'kepada', 'kepadanya', 'kesampaian', 'keseluruhan', 'keseluruhannya', 'keterlaluan', 'ketika', 'khususnya', 'kini', 'kinilah', 'kira', 'kira-kira', 'kiranya', 'kita', 'kitalah', 'kok', 'kurang', 'lagi', 'lagian', 'lah', 'lain', 'lainnya', 'lalu', 'lama', 'lamanya', 'lanjut', 'lanjutnya', 'lebih', 'lewat', 'lima', 'luar', 'macam', 'maka', 'makanya', 'makin', 'malah', 'malahan', 'mampu', 'mampukah', 'mana', 'manakala', 'manalagi', 'masa', 'masalah', 'masalahnya', 'masih', 'masihkah', 'masing', 'masing-masing', 'mau', 'maupun', 'melainkan', 'melakukan', 'melalui', 'melihat', 'melihatnya', 'memang', 'memastikan', 'memberi', 'memberikan', 'membuat', 'memerlukan', 'memihak', 'meminta', 'memintakan', 'memisalkan', 'memperbuat', 'mempergunakan', 'memperkirakan', 'memperlihatkan', 'mempersiapkan', 'mempersoalkan', 'mempertanyakan', 'mempunyai', 'memulai', 'memungkinkan', 'menjadi', 'menjawab', 'menjelaskan', 'menuju', 'menurut', 'menuturkan', 'menyampaikan', 'menyangkut', 'menyatakan', 'menyebutkan', 'menyeluruh', 'menyiapkan', 'merasa', 'mereka', 'merekalah', 'merupakan', 'meski', 'meskipun', 'meyakini', 'minta', 'mirip', 'misal', 'misalkan', 'misalnya', 'mula', 'mulai', 'mulailah', 'mulanya', 'mungkin', 'mungkinkah', 'nah', 'naik', 'namun', 'nanti', 'nantinya', 'nyaris', 'nyatanya', 'oleh', 'olehnya', 'pada', 'padahal', 'padanya', 'pak', 'paling', 'panjang', 'pantas', 'para', 'pasti', 'pastilah', 'penting', 'pentingnya', 'per', 'percuma', 'perlu', 'perlukah', 'perlunya', 'pernah', 'persoalan', 'pertama', 'pertama-tama', 'pertanyaan', 'pertanyakan', 'pihak', 'pihaknya', 'pukul', 'pula', 'pun', 'punya', 'rasa', 'rasanya', 'rata', 'rupanya', 'saat', 'saatnya', 'saja', 'sajalah', 'saling', 'sama', 'sama-sama', 'sambil', 'sampai', 'sampai-sampai', 'sampaikan', 'sana', 'sangat', 'sangatlah', 'sangkut', 'satu', 'saya', 'sayalah', 'se', 'sebab', 'sebabnya', 'sebagai', 'sebagaimana', 'sebagainya', 'sebagian', 'sebaik', 'sebaik-baiknya', 'sebaiknya', 'sebaliknya', 'sebanyak', 'sebelum', 'sebelumnya', 'sebenarnya', 'seberapa', 'sebesar', 'sebetulnya', 'sebisanya', 'sebuah', 'sebut', 'sebutlah', 'sebutnya', 'secara', 'secukupnya', 'sedang', 'sedangkan', 'sedemikian', 'sedikit', 'sedikitnya', 'seenaknya', 'segala', 'segalanya', 'segera', 'seharusnya', 'sehingga', 'seingat', 'sejak', 'sejauh', 'sejenak', 'sejumlah', 'sekadar', 'sekadarnya', 'sekali', 'sekali-kali', 'sekalian', 'sekaligus', 'sekalipun', 'sekarang', 'sekaranglah', 'sekecil', 'seketika', 'sekiranya', 'sekitar', 'sekitarnya', 'sela', 'selain', 'selaku', 'selalu', 'selama', 'selama-lamanya', 'selamanya', 'selanjutnya', 'seluruh', 'seluruhnya', 'semacam', 'semakin', 'semampu', 'semampunya', 'semasa', 'semasih', 'semata', 'semata-mata', 'semaunya', 'sementara', 'sempat', 'semua', 'semuanya', 'semula', 'sendiri', 'sendirian', 'sendirinya', 'seolah', 'seolah-olah', 'seorang', 'sepanjang', 'sepantasnya', 'sepantasnyalah', 'sepatutnya', 'seperti', 'sepertinya', 'sepihak', 'sering', 'seringnya', 'serta', 'serupa', 'sesaat', 'sesampai', 'sesegera', 'sesekali', 'seseorang', 'sesuatu', 'sesuatunya', 'sesudah', 'sesudahnya', 'setelah', 'setempat', 'setengah', 'seterusnya', 'setiap', 'setidaknya', 'setinggi', 'seusai', 'sewaktu', 'siap', 'siapa', 'siapakah', 'siapapun', 'sini', 'sinilah', 'suatu', 'sudah', 'sudahkah', 'sudahlah', 'supaya', 'tadi', 'tadinya', 'tahu', 'tahun', 'tak', 'tanpa', 'tanya', 'tanyakan', 'tanyanya', 'tapi', 'tegas', 'tegasnya', 'telah', 'tempat', 'tengah', 'tentang', 'tentu', 'tentulah', 'tentunya', 'tepat', 'terakhir', 'terasa', 'terbanyak', 'terdahulu', 'terdapat', 'terdiri', 'terhadap', 'terhadapnya', 'teringat', 'teringat-ingat', 'terjadi', 'terjadilah', 'terjadinya', 'terkira', 'terlalu', 'terlebih', 'terlihat', 'termasuk', 'ternyata', 'tersampaikan', 'tersebut', 'tersebutlah', 'tertentu', 'tertuju', 'terus', 'terutama', 'tetap', 'tetapi', 'tiap', 'tidak', 'tidakkah', 'tidaklah', 'tiga', 'tinggi', 'toh', 'tunjuk', 'turut', 'tutur', 'tuturnya', 'ucap', 'ucapnya', 'ujar', 'ujarnya', 'umum', 'umumnya', 'ungkap', 'ungkapnya', 'untuk', 'usah', 'usai', 'waduh', 'wah', 'wahai', 'waktu', 'waktunya', 'walau', 'walaupun', 'while', 'ya', 'yaitu', 'yakin', 'yakni', 'yang'
|
35 |
+
]
|
36 |
+
|
37 |
+
def remove_stopwords(text):
|
38 |
+
return ' '.join([word for word in text.split() if word not in stopwords_indonesia])
|
39 |
+
|
40 |
+
def preprocess_text(text: str) -> str:
|
41 |
+
text = text.lower()
|
42 |
+
text = re.sub(r'[^a-zA-Z\s]', '', text)
|
43 |
+
text = remove_stopwords(text)
|
44 |
+
return text
|
45 |
+
|
46 |
+
try:
|
47 |
+
vectorizer = joblib.load('tfidf_vectorizer.pkl')
|
48 |
+
model = joblib.load('hoax_detector_model.pkl')
|
49 |
+
except FileNotFoundError:
|
50 |
+
print("Error: Model or vectorizer file not found.")
|
51 |
+
vectorizer = None
|
52 |
+
model = None
|
53 |
+
|
54 |
+
@app.get("/")
|
55 |
+
def read_root():
|
56 |
+
return {"message": "Welcome to the Hoax Detector API!"}
|
57 |
+
|
58 |
+
@app.post("/predict")
|
59 |
+
def predict_hoax(news: NewsTitle):
|
60 |
+
if not model or not vectorizer:
|
61 |
+
return {"error": "Model not loaded. Please check server logs."}
|
62 |
+
|
63 |
+
original_title = news.title
|
64 |
+
cleaned_title = preprocess_text(original_title)
|
65 |
+
vectorized_title = vectorizer.transform([cleaned_title])
|
66 |
+
prediction = model.predict(vectorized_title)
|
67 |
+
result = "Hoax" if prediction[0] == 1 else "Bukan Hoax"
|
68 |
+
|
69 |
+
return {
|
70 |
+
"original_title": original_title,
|
71 |
+
"prediction": result
|
72 |
+
}
|
73 |
+
|
74 |
+
if __name__ == "__main__":
|
75 |
+
import uvicorn
|
76 |
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
hoax_detector_model.pkl
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:2df07af473b1f6f6c6d41f27aac67833bfc89e440f38b24ace13d09fbe80f144
|
3 |
+
size 18647
|
index.html
ADDED
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8">
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
+
<title>Detektor Hoax AI</title>
|
7 |
+
<link rel="stylesheet" href="style.css">
|
8 |
+
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700&display=swap" rel="stylesheet">
|
9 |
+
</head>
|
10 |
+
<body>
|
11 |
+
|
12 |
+
<header>
|
13 |
+
<h1>Detektor Hoax AI 🤖</h1>
|
14 |
+
</header>
|
15 |
+
|
16 |
+
<main>
|
17 |
+
<div class="card">
|
18 |
+
<p class="subtitle">Didukung oleh Scikit-learn & FastAPI</p>
|
19 |
+
<p>Masukkan judul berita yang ingin Anda periksa di bawah ini.</p>
|
20 |
+
|
21 |
+
<textarea id="newsTitle" rows="4" placeholder="Contoh: [SALAH] Pemerintah akan bagikan uang 1 Miliar..."></textarea>
|
22 |
+
|
23 |
+
<button id="checkButton">Periksa Sekarang</button>
|
24 |
+
|
25 |
+
<h2>Hasil Prediksi:</h2>
|
26 |
+
<div id="resultContainer">
|
27 |
+
<p id="resultArea">--- Menunggu input ---</p>
|
28 |
+
</div>
|
29 |
+
</div>
|
30 |
+
</main>
|
31 |
+
|
32 |
+
<footer>
|
33 |
+
<p>© 2025 - Dibuat dengan AI</p>
|
34 |
+
</footer>
|
35 |
+
|
36 |
+
<script src="script.js"></script>
|
37 |
+
</body>
|
38 |
+
</html>
|
requirements.txt
ADDED
Binary file (694 Bytes). View file
|
|
script.js
ADDED
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// script.js (Versi Baru)
|
2 |
+
|
3 |
+
document.addEventListener('DOMContentLoaded', () => {
|
4 |
+
|
5 |
+
const checkButton = document.getElementById('checkButton');
|
6 |
+
const newsTitleInput = document.getElementById('newsTitle');
|
7 |
+
const resultContainer = document.getElementById('resultContainer'); // Kita pakai container
|
8 |
+
|
9 |
+
checkButton.addEventListener('click', () => {
|
10 |
+
const titleToPredict = newsTitleInput.value;
|
11 |
+
|
12 |
+
if (titleToPredict.trim() === '') {
|
13 |
+
resultContainer.innerHTML = '<p id="resultArea">Harap masukkan judul berita terlebih dahulu.</p>';
|
14 |
+
return;
|
15 |
+
}
|
16 |
+
|
17 |
+
// Tampilkan spinner
|
18 |
+
resultContainer.innerHTML = '<div class="spinner"></div>';
|
19 |
+
|
20 |
+
fetch('http://127.0.0.1:8000/predict', {
|
21 |
+
/* ... (sisa kode fetch tetap sama) ... */
|
22 |
+
method: 'POST',
|
23 |
+
headers: {
|
24 |
+
'Content-Type': 'application/json',
|
25 |
+
},
|
26 |
+
body: JSON.stringify({ title: titleToPredict }),
|
27 |
+
})
|
28 |
+
.then(response => response.json())
|
29 |
+
.then(data => {
|
30 |
+
// Tampilkan hasil dengan efek animasi
|
31 |
+
resultContainer.innerHTML = `<p id="resultArea">Hasil: ${data.prediction}</p>`;
|
32 |
+
})
|
33 |
+
.catch(error => {
|
34 |
+
console.error('Error:', error);
|
35 |
+
resultContainer.innerHTML = '<p id="resultArea">Terjadi kesalahan. Pastikan server API Anda sudah berjalan.</p>';
|
36 |
+
});
|
37 |
+
});
|
38 |
+
});
|
style.css
ADDED
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/* style.css (Versi Perbaikan Final) */
|
2 |
+
|
3 |
+
/* CSS Variables untuk warna agar mudah diubah */
|
4 |
+
:root {
|
5 |
+
--bg-color: #f0f2f5;
|
6 |
+
--card-bg: white;
|
7 |
+
--text-color: #333;
|
8 |
+
--subtitle-color: #606770;
|
9 |
+
--primary-blue: #1877f2;
|
10 |
+
--blue-hover: #166fe5;
|
11 |
+
--border-color: #dddfe2;
|
12 |
+
}
|
13 |
+
|
14 |
+
body {
|
15 |
+
font-family: 'Poppins', sans-serif;
|
16 |
+
background-color: var(--bg-color);
|
17 |
+
color: var(--text-color);
|
18 |
+
display: flex;
|
19 |
+
flex-direction: column;
|
20 |
+
min-height: 100vh;
|
21 |
+
margin: 0;
|
22 |
+
}
|
23 |
+
|
24 |
+
header {
|
25 |
+
background-color: var(--card-bg);
|
26 |
+
color: var(--primary-blue);
|
27 |
+
text-align: center;
|
28 |
+
padding: 1rem 0;
|
29 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
30 |
+
width: 100%;
|
31 |
+
}
|
32 |
+
|
33 |
+
header h1 {
|
34 |
+
margin: 0;
|
35 |
+
font-weight: 700;
|
36 |
+
}
|
37 |
+
|
38 |
+
/* 'main' HANYA bertugas untuk mengisi ruang dan menengahkan 'card' */
|
39 |
+
main {
|
40 |
+
flex: 1;
|
41 |
+
display: flex;
|
42 |
+
justify-content: center;
|
43 |
+
align-items: center;
|
44 |
+
padding: 2rem;
|
45 |
+
width: 100%;
|
46 |
+
box-sizing: border-box;
|
47 |
+
}
|
48 |
+
|
49 |
+
/* 'card' berisi semua konten dan mengatur dirinya sendiri */
|
50 |
+
.card {
|
51 |
+
background-color: var(--card-bg);
|
52 |
+
padding: 2.5rem 3.5rem;
|
53 |
+
border-radius: 16px;
|
54 |
+
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
55 |
+
text-align: center;
|
56 |
+
max-width: 550px;
|
57 |
+
width: 100%;
|
58 |
+
border: 1px solid #e7e7e7;
|
59 |
+
}
|
60 |
+
|
61 |
+
.card .subtitle {
|
62 |
+
font-size: 0.9rem;
|
63 |
+
color: var(--subtitle-color);
|
64 |
+
margin-top: -1rem;
|
65 |
+
margin-bottom: 2rem;
|
66 |
+
}
|
67 |
+
|
68 |
+
/* --- Desain Form Elements --- */
|
69 |
+
textarea {
|
70 |
+
width: 100%;
|
71 |
+
padding: 1rem;
|
72 |
+
border: 2px solid var(--border-color);
|
73 |
+
border-radius: 8px;
|
74 |
+
font-size: 1rem;
|
75 |
+
font-family: 'Poppins', sans-serif;
|
76 |
+
margin-bottom: 1.5rem;
|
77 |
+
resize: vertical;
|
78 |
+
box-sizing: border-box; /* Important for padding and width */
|
79 |
+
transition: border-color 0.3s, box-shadow 0.3s;
|
80 |
+
}
|
81 |
+
|
82 |
+
textarea:focus {
|
83 |
+
outline: none;
|
84 |
+
border-color: var(--primary-blue);
|
85 |
+
box-shadow: 0 0 0 3px rgba(24, 119, 242, 0.2);
|
86 |
+
}
|
87 |
+
|
88 |
+
button {
|
89 |
+
background-color: var(--primary-blue);
|
90 |
+
color: white;
|
91 |
+
border: none;
|
92 |
+
padding: 1rem 2rem;
|
93 |
+
border-radius: 8px;
|
94 |
+
font-size: 1rem;
|
95 |
+
font-weight: 600;
|
96 |
+
cursor: pointer;
|
97 |
+
transition: all 0.2s ease-in-out;
|
98 |
+
box-shadow: 0 4px 10px rgba(24, 119, 242, 0.3);
|
99 |
+
margin-bottom: 1rem;
|
100 |
+
}
|
101 |
+
|
102 |
+
button:hover {
|
103 |
+
background-color: var(--blue-hover);
|
104 |
+
transform: translateY(-2px);
|
105 |
+
box-shadow: 0 6px 15px rgba(24, 119, 242, 0.4);
|
106 |
+
}
|
107 |
+
|
108 |
+
button:active {
|
109 |
+
transform: translateY(0);
|
110 |
+
box-shadow: 0 2px 5px rgba(24, 119, 242, 0.3);
|
111 |
+
}
|
112 |
+
|
113 |
+
/* --- Area Hasil & Footer --- */
|
114 |
+
#resultContainer {
|
115 |
+
margin-top: 1.5rem;
|
116 |
+
}
|
117 |
+
|
118 |
+
#resultArea {
|
119 |
+
font-size: 1.2rem;
|
120 |
+
font-weight: 600;
|
121 |
+
color: #1c1e21;
|
122 |
+
}
|
123 |
+
|
124 |
+
@keyframes fadeIn {
|
125 |
+
from { opacity: 0; transform: translateY(-10px); }
|
126 |
+
to { opacity: 1; transform: translateY(0); }
|
127 |
+
}
|
128 |
+
|
129 |
+
#resultArea.fade-in {
|
130 |
+
animation: fadeIn 0.5s ease-in-out;
|
131 |
+
}
|
132 |
+
|
133 |
+
.spinner {
|
134 |
+
border: 4px solid rgba(0, 0, 0, 0.1);
|
135 |
+
width: 36px;
|
136 |
+
height: 36px;
|
137 |
+
border-radius: 50%;
|
138 |
+
border-left-color: var(--primary-blue);
|
139 |
+
animation: spin 1s ease infinite;
|
140 |
+
margin: 1.5rem auto; /* Beri margin agar tidak terlalu dekat */
|
141 |
+
}
|
142 |
+
|
143 |
+
@keyframes spin {
|
144 |
+
0% { transform: rotate(0deg); }
|
145 |
+
100% { transform: rotate(360deg); }
|
146 |
+
}
|
147 |
+
|
148 |
+
footer {
|
149 |
+
background-color: #333;
|
150 |
+
color: white;
|
151 |
+
text-align: center;
|
152 |
+
padding: 1rem 0;
|
153 |
+
width: 100%;
|
154 |
+
font-size: 0.9rem;
|
155 |
+
}
|
tfidf_vectorizer.pkl
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:6ede04db3d5a8f0c055020af1834acc07e9a1f5616fb2186c60fb98103945d23
|
3 |
+
size 11874
|