parent
0c16c70a9e
commit
003ebd1de1
|
@ -0,0 +1,147 @@
|
|||
from flask import Flask, request, jsonify
|
||||
from flask_cors import CORS
|
||||
from sklearn.neighbors import KNeighborsClassifier
|
||||
import pandas as pd
|
||||
import os
|
||||
|
||||
app = Flask(__name__)
|
||||
CORS(app)
|
||||
|
||||
|
||||
DATASET_FILE = os.path.join(os.path.dirname(__file__), "dataset.xlsx")
|
||||
|
||||
# Mapping angka ke kategori jawaban
|
||||
ANSWER_MAPPING = {
|
||||
0: "Tidak Mengandung",
|
||||
1: "Sangat Sedikit Mengandung",
|
||||
2: "Agak Mengandung",
|
||||
3: "Cukup Mengandung",
|
||||
4: "Banyak Mengandung",
|
||||
5: "Sangat Banyak Mengandung"
|
||||
}
|
||||
|
||||
REVERSE_MAPPING = {v: k for k, v in ANSWER_MAPPING.items()}
|
||||
|
||||
# Fungsi untuk membaca dataset
|
||||
def load_dataset():
|
||||
if os.path.exists(DATASET_FILE):
|
||||
try:
|
||||
data = pd.read_excel(DATASET_FILE)
|
||||
|
||||
if data.empty:
|
||||
data = pd.DataFrame(columns=["Nama Game", "Kekerasan", "Bahasa Kasar", "Tema Seksual", "Darah/Gore", "Elemen Perjudian", "Rating"])
|
||||
data.to_excel(DATASET_FILE, index=False)
|
||||
except Exception as e:
|
||||
print("Error saat membaca dataset:", e)
|
||||
data = pd.DataFrame(columns=["Nama Game", "Kekerasan", "Bahasa Kasar", "Tema Seksual", "Darah/Gore", "Elemen Perjudian", "Rating"])
|
||||
data.to_excel(DATASET_FILE, index=False)
|
||||
else:
|
||||
data = pd.DataFrame(columns=["Nama Game", "Kekerasan", "Bahasa Kasar", "Tema Seksual", "Darah/Gore", "Elemen Perjudian", "Rating"])
|
||||
data.to_excel(DATASET_FILE, index=False)
|
||||
|
||||
print("Kolom dalam dataset:", data.columns.tolist())
|
||||
|
||||
required_columns = ["Nama Game", "Kekerasan", "Bahasa Kasar", "Tema Seksual", "Darah/Gore", "Elemen Perjudian", "Rating"]
|
||||
for col in required_columns:
|
||||
if col not in data.columns:
|
||||
raise ValueError(f"Kolom '{col}' tidak ditemukan dalam dataset!")
|
||||
|
||||
X = data.iloc[:, 1:-1].values
|
||||
y = data.iloc[:, -1].values
|
||||
|
||||
return X, y, data
|
||||
|
||||
# Fungsi untuk menambahkan data ke dataset
|
||||
def add_to_dataset(name, features, rating):
|
||||
data = pd.read_excel(DATASET_FILE)
|
||||
|
||||
if name not in data['Nama Game'].values:
|
||||
new_row = [name] + features + [rating]
|
||||
data.loc[len(data)] = new_row
|
||||
data.to_excel(DATASET_FILE, index=False)
|
||||
|
||||
# Endpoint untuk mengecek apakah game sudah ada dalam dataset
|
||||
@app.route('/check_game', methods=['POST'])
|
||||
def check_game():
|
||||
try:
|
||||
data = request.json
|
||||
game_name = data['game_name']
|
||||
|
||||
_, _, dataset = load_dataset()
|
||||
|
||||
if game_name in dataset["Nama Game"].values:
|
||||
rating = dataset.loc[dataset["Nama Game"] == game_name, "Rating"].values[0]
|
||||
return jsonify({'exists': True, 'rating': rating})
|
||||
|
||||
return jsonify({'exists': False})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
# Endpoint untuk menerima jawaban dan memberikan rating
|
||||
@app.route('/submit_answers', methods=['POST'])
|
||||
def submit_answers():
|
||||
try:
|
||||
data = request.json
|
||||
print("=== DATA DITERIMA DARI FRONTEND ===")
|
||||
print(data)
|
||||
|
||||
game_name = data.get('game_name', '').strip()
|
||||
answers = data.get('answers', [])
|
||||
|
||||
if not game_name or not answers:
|
||||
print("❌ ERROR: Data tidak lengkap")
|
||||
return jsonify({"error": "Data tidak lengkap"}), 400
|
||||
|
||||
print("✔ Nama Game:", game_name)
|
||||
print("✔ Jawaban:", answers)
|
||||
|
||||
# Cek apakah jawaban valid
|
||||
features = []
|
||||
for answer in answers:
|
||||
if answer not in REVERSE_MAPPING:
|
||||
print(f"❌ ERROR: Jawaban tidak valid -> {answer}")
|
||||
return jsonify({"error": f"Jawaban tidak valid: {answer}"}), 400
|
||||
features.append(REVERSE_MAPPING[answer])
|
||||
|
||||
print("✔ Features (dikonversi ke angka):", features)
|
||||
|
||||
|
||||
X, y, dataset = load_dataset()
|
||||
|
||||
# Jika game sudah ada di dataset, kembalikan rating yang sudah ada
|
||||
if game_name in dataset["Nama Game"].values:
|
||||
existing_rating = dataset.loc[dataset["Nama Game"] == game_name, "Rating"].values[0]
|
||||
print("✔ Game ditemukan dalam dataset. Rating:", existing_rating)
|
||||
return jsonify({'rating': existing_rating})
|
||||
|
||||
# Prediksi rating dengan KNN jika dataset cukup besar
|
||||
if len(X) > 3:
|
||||
knn = KNeighborsClassifier(n_neighbors=3)
|
||||
knn.fit(X, y)
|
||||
predicted_rating = knn.predict([features])[0]
|
||||
else:
|
||||
total_score = sum(features)
|
||||
if total_score <= 5:
|
||||
predicted_rating = "SU"
|
||||
elif total_score <= 10:
|
||||
predicted_rating = "3+"
|
||||
elif total_score <= 15:
|
||||
predicted_rating = "7+"
|
||||
elif total_score <= 20:
|
||||
predicted_rating = "13+"
|
||||
else:
|
||||
predicted_rating = "18+"
|
||||
|
||||
print("✔ Prediksi Rating:", predicted_rating)
|
||||
|
||||
add_to_dataset(game_name, features, predicted_rating)
|
||||
return jsonify({'rating': predicted_rating})
|
||||
|
||||
except Exception as e:
|
||||
print("❌ ERROR di backend:", str(e))
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True)
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"name": "frontend",
|
||||
"version": "1.0.0",
|
||||
"main": "script.js",
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"react-dom": "^19.0.0",
|
||||
"react-scripts": "^5.0.1"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.5.2"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Rating Umur Game</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,38 @@
|
|||
document.getElementById("game-form").addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(e.target);
|
||||
const name = formData.get("name");
|
||||
const features = [
|
||||
parseInt(formData.get("kekerasan")),
|
||||
parseInt(formData.get("bahasa_kasar")),
|
||||
parseInt(formData.get("tema_seksual")),
|
||||
parseInt(formData.get("darah_gore")),
|
||||
parseInt(formData.get("elemen_perjudian")),
|
||||
];
|
||||
|
||||
try {
|
||||
// Kirim data ke backend
|
||||
const response = await fetch("http://127.0.0.1:5000/predict", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ name, features }),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.status === "exists") {
|
||||
document.getElementById("result").textContent =
|
||||
`Game "${name}" sudah ada. Rating: ${result.predicted_rating}`;
|
||||
} else {
|
||||
document.getElementById("result").textContent =
|
||||
`Game "${name}" berhasil diprediksi. Rating: ${result.predicted_rating}`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
alert("Gagal memproses data.");
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,230 @@
|
|||
import React, { useState } from "react";
|
||||
import "./index.css";
|
||||
import PopUp from "./components/PopUp";
|
||||
|
||||
const questions = [
|
||||
"Seberapa banyak game ini mengandung kekerasan?",
|
||||
"Seberapa sering bahasa kasar digunakan dalam game ini?",
|
||||
"Seberapa banyak game ini mengandung konten dewasa?",
|
||||
"Seberapa sering game ini menampilkan penggunaan obat-obatan atau alkohol?",
|
||||
"Apakah game ini memiliki unsur simulasi perjudian?",
|
||||
];
|
||||
|
||||
const options = [
|
||||
"Tidak Mengandung",
|
||||
"Sangat Sedikit Mengandung",
|
||||
"Agak Mengandung",
|
||||
"Cukup Mengandung",
|
||||
"Banyak Mengandung",
|
||||
"Sangat Banyak Mengandung"
|
||||
];
|
||||
|
||||
const ratingDescriptions = {
|
||||
"SU": "Semua Umur - Dapat dimainkan oleh semua orang tanpa batasan konten.",
|
||||
"3+": "Usia 3 Tahun Keatas - Tidak ada konten dewasa, perjudian, atau interaksi online.",
|
||||
"7+": "Usia 7 Tahun Keatas - Ada unsur ringan seperti kekerasan kartun atau bahasa ringan.",
|
||||
"13+": "Usia 13 Tahun Keatas - Ada kekerasan moderat, simulasi perjudian, atau tema horor.",
|
||||
"18+": "Usia 18 Tahun Keatas - Mengandung konten dewasa, kekerasan realistis, atau humor kasar."
|
||||
};
|
||||
|
||||
const App = () => {
|
||||
const [gameName, setGameName] = useState("");
|
||||
const [step, setStep] = useState(0);
|
||||
const [answers, setAnswers] = useState([]);
|
||||
const [rating, setRating] = useState(null);
|
||||
const [isPopUpOpen, setIsPopUpOpen] = useState(false);
|
||||
const [isConfirmPopupOpen, setIsConfirmPopupOpen] = useState(false);
|
||||
const [selectedOption, setSelectedOption] = useState("");
|
||||
|
||||
const checkGame = async () => {
|
||||
const response = await fetch("http://127.0.0.1:5000/check_game", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ game_name: gameName }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (data.exists) {
|
||||
setRating(data.rating);
|
||||
} else {
|
||||
setStep(1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOptionClick = (option) => {
|
||||
setSelectedOption(option);
|
||||
setIsPopUpOpen(true);
|
||||
};
|
||||
|
||||
const confirmSelection = () => {
|
||||
setAnswers([...answers, selectedOption]);
|
||||
setStep(step + 1);
|
||||
setIsPopUpOpen(false);
|
||||
};
|
||||
|
||||
const submitAnswers = async () => {
|
||||
setIsConfirmPopupOpen(true);
|
||||
};
|
||||
|
||||
const confirmSubmit = async () => {
|
||||
setIsConfirmPopupOpen(false);
|
||||
const response = await fetch("http://localhost:5000/submit_answers", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ game_name: gameName, answers }),
|
||||
});
|
||||
const data = await response.json();
|
||||
setRating(data.rating);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen bg-gray-100 relative">
|
||||
<div className="w-full max-w-lg bg-white p-6 rounded-lg shadow-md text-center relative">
|
||||
<h1 className="text-2xl font-bold">Rating Umur Game</h1>
|
||||
|
||||
{/* Tombol kembali hanya muncul saat berada di pertanyaan */}
|
||||
{step > 0 && step <= questions.length && (
|
||||
<div className="absolute top-4 right-4 flex space-x-2">
|
||||
{step > 1 && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setAnswers(answers.slice(0, -1));
|
||||
setStep(step - 1);
|
||||
}}
|
||||
className="w-6 h-6 bg-yellow-500 rounded-full hover:bg-yellow-700"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Tombol merah: Kembali ke input nama game */}
|
||||
<button
|
||||
onClick={() => {
|
||||
setStep(0);
|
||||
setAnswers([]);
|
||||
setGameName("");
|
||||
}}
|
||||
className="w-6 h-6 bg-red-500 rounded-full hover:bg-red-700"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{!rating && step === 0 && (
|
||||
<>
|
||||
<p className="mt-4">Masukkan nama game:</p>
|
||||
<input
|
||||
type="text"
|
||||
value={gameName}
|
||||
onChange={(e) => setGameName(e.target.value)}
|
||||
className="border border-gray-600 bg-gray-100 p-2 w-full mt-2 focus:bg-gray-200 focus:border-black rounded mt-6"
|
||||
/>
|
||||
<button
|
||||
onClick={checkGame}
|
||||
className="bg-blue-500 text-white px-4 py-2 rounded mt-4"
|
||||
>
|
||||
Cek
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!rating && step > 0 && step <= questions.length && (
|
||||
<>
|
||||
<p className="mt-4">{questions[step - 1]}</p>
|
||||
{options.map((option) => (
|
||||
<button
|
||||
key={option}
|
||||
onClick={() => handleOptionClick(option)}
|
||||
className="block bg-gray-300 text-black px-4 py-2 rounded mt-2 hover:bg-gray-400 w-full"
|
||||
>
|
||||
{option}
|
||||
</button>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
{!rating && step > questions.length && (
|
||||
<>
|
||||
{/* Menampilkan tabel hasil jawaban */}
|
||||
<div className="mt-4">
|
||||
<h2 className="text-lg font-semibold">Hasil Jawaban Anda:</h2>
|
||||
<table className="table-auto w-full border mt-4">
|
||||
<thead>
|
||||
<tr className="bg-gray-200">
|
||||
<th className="border px-4 py-2">Pertanyaan</th>
|
||||
<th className="border px-4 py-2">Jawaban</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{questions.map((question, index) => (
|
||||
<tr key={index}>
|
||||
<td className="border px-4 py-2">{question}</td>
|
||||
<td className="border px-4 py-2">{answers[index]}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Tombol kembali ke input game */}
|
||||
<button
|
||||
onClick={() => {
|
||||
setStep(0);
|
||||
setAnswers([]);
|
||||
setGameName("");
|
||||
}}
|
||||
className="bg-red-500 text-white px-4 py-2 rounded mt-4 w-full"
|
||||
>
|
||||
Kembali
|
||||
</button>
|
||||
|
||||
{/* Tombol untuk konfirmasi hasil */}
|
||||
<button
|
||||
onClick={submitAnswers}
|
||||
className="bg-green-500 text-white px-4 py-2 rounded mt-4 w-full"
|
||||
>
|
||||
Cek Hasil
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{rating && (
|
||||
<>
|
||||
<h2 className="text-xl font-semibold mt-4">
|
||||
Game <span className="font-bold text-blue-500">{gameName}</span> memiliki rating
|
||||
<span className="font-bold text-red-500"> {rating}</span>
|
||||
</h2>
|
||||
<p className="mt-2"><strong>Penjelasan:</strong> {ratingDescriptions[rating]}</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
setStep(0);
|
||||
setAnswers([]);
|
||||
setRating(null);
|
||||
setGameName("");
|
||||
}}
|
||||
className="bg-red-500 text-white px-4 py-2 rounded mt-4 w-full"
|
||||
>
|
||||
Coba Lagi
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Pop-up konfirmasi pilihan jawaban */}
|
||||
<PopUp
|
||||
isOpen={isPopUpOpen}
|
||||
onClose={() => setIsPopUpOpen(false)}
|
||||
onConfirm={confirmSelection}
|
||||
selectedOption={selectedOption}
|
||||
/>
|
||||
|
||||
{/* Pop-up konfirmasi cek hasil */}
|
||||
<PopUp
|
||||
isOpen={isConfirmPopupOpen}
|
||||
onClose={() => setIsConfirmPopupOpen(false)}
|
||||
onConfirm={confirmSubmit}
|
||||
selectedOption="Apakah Anda yakin ingin melihat hasil rating?"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
|
@ -0,0 +1,30 @@
|
|||
import React from "react";
|
||||
|
||||
|
||||
const PopUp = ({ isOpen, onClose, onConfirm, selectedOption }) => {
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50">
|
||||
<div className="bg-white p-6 rounded-lg shadow-lg text-center">
|
||||
<p>Apakah Anda yakin memilih: <strong>{selectedOption}</strong>?</p>
|
||||
<div className="mt-4 flex justify-center gap-4">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="bg-gray-500 text-white px-4 py-2 rounded hover:bg-gray-700"
|
||||
>
|
||||
Kembali
|
||||
</button>
|
||||
<button
|
||||
onClick={onConfirm}
|
||||
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-700"
|
||||
>
|
||||
Lanjut
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PopUp;
|
|
@ -0,0 +1,3 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
|
@ -0,0 +1,15 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
reportWebVitals();
|
|
@ -0,0 +1,6 @@
|
|||
const reportWebVitals = (metric) => {
|
||||
console.log(metric);
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ["./src/**/*.{js,jsx,ts,tsx,css}"],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
Flask
|
||||
Flask-Cors
|
||||
pandas
|
||||
scikit-learn
|
||||
openpyxl
|
Loading…
Reference in New Issue