From 7de0f819e288608c49f10c4790cc5b33f6e1d4e8 Mon Sep 17 00:00:00 2001 From: tanskwacu <132917311+tanskwacu@users.noreply.github.com> Date: Tue, 3 Nov 2026 11:37:39 +0700 Subject: [PATCH] Add S-Learn frontend backend and model --- .gitignore | 16 + api_slearn/artifacts_slearn/README.txt | 46 + .../artifacts_slearn/kamus_normalisasi.joblib | Bin 0 -> 75358 bytes .../artifacts_slearn/metadata_model.json | 145 + .../model_logistic_regression.joblib | Bin 0 -> 6271 bytes .../preprocessing_config.json | 5419 +++++++++++++++++ .../artifacts_slearn/preprocessing_slearn.py | 124 + api_slearn/artifacts_slearn/requirements.txt | 7 + .../artifacts_slearn/stopwords_final.joblib | Bin 0 -> 7623 bytes .../artifacts_slearn/tfidf_vectorizer.joblib | Bin 0 -> 13944 bytes api_slearn/database.py | 1021 ++++ api_slearn/export_research_tfidf.py | 681 +++ api_slearn/main.py | 487 ++ api_slearn/migrate_model_versions.py | 24 + api_slearn/model_registry.py | 259 + api_slearn/model_service.py | 274 + api_slearn/model_store/v2/metadata_model.json | 93 + .../v2/model_logistic_regression.joblib | Bin 0 -> 6271 bytes .../model_store/v2/tfidf_vectorizer.joblib | Bin 0 -> 13947 bytes api_slearn/model_store/v3/metadata_model.json | 100 + .../v3/model_logistic_regression.joblib | Bin 0 -> 6895 bytes .../model_store/v3/tfidf_vectorizer.joblib | Bin 0 -> 15522 bytes api_slearn/model_store/v4/metadata_model.json | 100 + .../v4/model_logistic_regression.joblib | Bin 0 -> 7231 bytes .../model_store/v4/tfidf_vectorizer.joblib | Bin 0 -> 16385 bytes api_slearn/model_store/v5/metadata_model.json | 100 + .../v5/model_logistic_regression.joblib | Bin 0 -> 7231 bytes .../model_store/v5/tfidf_vectorizer.joblib | Bin 0 -> 16385 bytes api_slearn/preprocessing.py | 193 + api_slearn/pulihkan_data_prodi.py | 73 + api_slearn/requirements.txt | 11 + api_slearn/tests/test_retraining.py | 87 + api_slearn/training_input.py | 14 + web_slearn/.gitignore | 24 + web_slearn/README.md | 16 + .../src/pages/PenelitianModel.jsx | 1821 ++++++ web_slearn/eslint.config.js | 21 + web_slearn/index.html | 13 + web_slearn/package-lock.json | 2512 ++++++++ web_slearn/package.json | 29 + web_slearn/public/favicon.svg | 1 + web_slearn/public/icons.svg | 24 + web_slearn/src/App.css | 184 + web_slearn/src/App.jsx | 27 + web_slearn/src/assets/hero.png | Bin 0 -> 13057 bytes web_slearn/src/assets/react.svg | 1 + web_slearn/src/assets/vite.svg | 1 + .../src/components/EvaluasiPembanding.jsx | 89 + ...iPembanding.jsx.backup-20261028-180124-636 | 91 + web_slearn/src/components/ModelContext.jsx | 6 + web_slearn/src/components/Sidebar.jsx | 141 + web_slearn/src/components/TrainingDetails.jsx | 137 + web_slearn/src/index.css | 18 + web_slearn/src/layouts/MainLayout.jsx | 18 + web_slearn/src/main.jsx | 13 + web_slearn/src/pages/AnalisisCSV.jsx | 917 +++ web_slearn/src/pages/Dashboard.jsx | 50 + web_slearn/src/pages/HasilAnalisis.jsx | 1204 ++++ web_slearn/src/pages/PenelitianModel.jsx | 1824 ++++++ web_slearn/src/pages/PrediksiUlasan.jsx | 567 ++ web_slearn/src/pages/RiwayatAnalisis.jsx | 1554 +++++ web_slearn/src/pages/RiwayatModel.jsx | 413 ++ web_slearn/src/services/api.js | 191 + web_slearn/src/styles/analisis-csv.css | 661 ++ web_slearn/src/styles/dashboard.css | 210 + web_slearn/src/styles/evaluasi-pembanding.css | 21 + web_slearn/src/styles/hasil-analisis.css | 1391 +++++ web_slearn/src/styles/layout.css | 171 + web_slearn/src/styles/penelitian-model.css | 370 ++ web_slearn/src/styles/prediksi-ulasan.css | 897 +++ web_slearn/src/styles/riwayat-analisis.css | 1222 ++++ web_slearn/src/styles/riwayat-model.css | 413 ++ web_slearn/vite.config.js | 7 + 73 files changed, 26544 insertions(+) create mode 100644 .gitignore create mode 100644 api_slearn/artifacts_slearn/README.txt create mode 100644 api_slearn/artifacts_slearn/kamus_normalisasi.joblib create mode 100644 api_slearn/artifacts_slearn/metadata_model.json create mode 100644 api_slearn/artifacts_slearn/model_logistic_regression.joblib create mode 100644 api_slearn/artifacts_slearn/preprocessing_config.json create mode 100644 api_slearn/artifacts_slearn/preprocessing_slearn.py create mode 100644 api_slearn/artifacts_slearn/requirements.txt create mode 100644 api_slearn/artifacts_slearn/stopwords_final.joblib create mode 100644 api_slearn/artifacts_slearn/tfidf_vectorizer.joblib create mode 100644 api_slearn/database.py create mode 100644 api_slearn/export_research_tfidf.py create mode 100644 api_slearn/main.py create mode 100644 api_slearn/migrate_model_versions.py create mode 100644 api_slearn/model_registry.py create mode 100644 api_slearn/model_service.py create mode 100644 api_slearn/model_store/v2/metadata_model.json create mode 100644 api_slearn/model_store/v2/model_logistic_regression.joblib create mode 100644 api_slearn/model_store/v2/tfidf_vectorizer.joblib create mode 100644 api_slearn/model_store/v3/metadata_model.json create mode 100644 api_slearn/model_store/v3/model_logistic_regression.joblib create mode 100644 api_slearn/model_store/v3/tfidf_vectorizer.joblib create mode 100644 api_slearn/model_store/v4/metadata_model.json create mode 100644 api_slearn/model_store/v4/model_logistic_regression.joblib create mode 100644 api_slearn/model_store/v4/tfidf_vectorizer.joblib create mode 100644 api_slearn/model_store/v5/metadata_model.json create mode 100644 api_slearn/model_store/v5/model_logistic_regression.joblib create mode 100644 api_slearn/model_store/v5/tfidf_vectorizer.joblib create mode 100644 api_slearn/preprocessing.py create mode 100644 api_slearn/pulihkan_data_prodi.py create mode 100644 api_slearn/requirements.txt create mode 100644 api_slearn/tests/test_retraining.py create mode 100644 api_slearn/training_input.py create mode 100644 web_slearn/.gitignore create mode 100644 web_slearn/README.md create mode 100644 web_slearn/backup-pembanding-20261028-175022-231/src/pages/PenelitianModel.jsx create mode 100644 web_slearn/eslint.config.js create mode 100644 web_slearn/index.html create mode 100644 web_slearn/package-lock.json create mode 100644 web_slearn/package.json create mode 100644 web_slearn/public/favicon.svg create mode 100644 web_slearn/public/icons.svg create mode 100644 web_slearn/src/App.css create mode 100644 web_slearn/src/App.jsx create mode 100644 web_slearn/src/assets/hero.png create mode 100644 web_slearn/src/assets/react.svg create mode 100644 web_slearn/src/assets/vite.svg create mode 100644 web_slearn/src/components/EvaluasiPembanding.jsx create mode 100644 web_slearn/src/components/EvaluasiPembanding.jsx.backup-20261028-180124-636 create mode 100644 web_slearn/src/components/ModelContext.jsx create mode 100644 web_slearn/src/components/Sidebar.jsx create mode 100644 web_slearn/src/components/TrainingDetails.jsx create mode 100644 web_slearn/src/index.css create mode 100644 web_slearn/src/layouts/MainLayout.jsx create mode 100644 web_slearn/src/main.jsx create mode 100644 web_slearn/src/pages/AnalisisCSV.jsx create mode 100644 web_slearn/src/pages/Dashboard.jsx create mode 100644 web_slearn/src/pages/HasilAnalisis.jsx create mode 100644 web_slearn/src/pages/PenelitianModel.jsx create mode 100644 web_slearn/src/pages/PrediksiUlasan.jsx create mode 100644 web_slearn/src/pages/RiwayatAnalisis.jsx create mode 100644 web_slearn/src/pages/RiwayatModel.jsx create mode 100644 web_slearn/src/services/api.js create mode 100644 web_slearn/src/styles/analisis-csv.css create mode 100644 web_slearn/src/styles/dashboard.css create mode 100644 web_slearn/src/styles/evaluasi-pembanding.css create mode 100644 web_slearn/src/styles/hasil-analisis.css create mode 100644 web_slearn/src/styles/layout.css create mode 100644 web_slearn/src/styles/penelitian-model.css create mode 100644 web_slearn/src/styles/prediksi-ulasan.css create mode 100644 web_slearn/src/styles/riwayat-analisis.css create mode 100644 web_slearn/src/styles/riwayat-model.css create mode 100644 web_slearn/vite.config.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..20fa2d4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +node_modules/ +dist/ +__pycache__/ +*.pyc +.venv/ +venv/ +.env +.env.* +!.env.example +*.db +*.sqlite +*.sqlite3 +*.csv +web_slearn/src/data/ +web_slearn/public/data/ +api_slearn/model_store_backup_sebelum_perbaikan/ diff --git a/api_slearn/artifacts_slearn/README.txt b/api_slearn/artifacts_slearn/README.txt new file mode 100644 index 0000000..9a81b12 --- /dev/null +++ b/api_slearn/artifacts_slearn/README.txt @@ -0,0 +1,46 @@ +ARTEFAK FINAL ANALISIS SENTIMEN S-LEARN +======================================= + +1. Model yang digunakan: + Logistic Regression. + +2. Ekstraksi fitur: + TF-IDF yang sudah di-fit pada data training penelitian. + +3. Website TIDAK BOLEH melakukan: + - model.fit() + - tfidf.fit() + - tfidf.fit_transform() + +4. Untuk data baru: + preprocessing_slearn.preprocessing_kalimat_baru(teks) + -> tfidf.transform() + -> model.predict() + +5. Confidence: + Gunakan model.predict_proba(). + + Probabilitas kelas 0 = Negatif. + Probabilitas kelas 1 = Positif. + + Confidence adalah probabilitas kelas + yang dipilih sebagai hasil prediksi. + + Confidence BUKAN akurasi dan bukan + jaminan bahwa prediksi pasti benar. + +6. Untuk analisis CSV: + Seluruh komentar menggunakan preprocessing + yang sama, kemudian tfidf.transform(), + model.predict(), dan model.predict_proba(). + +7. Jangan melakukan training ulang pada website. + +8. Gunakan versi library pada requirements.txt. + +9. Siapkan resource NLTK: + - punkt + - punkt_tab + +10. preprocessing_config.json merupakan + konfigurasi preprocessing final penelitian. diff --git a/api_slearn/artifacts_slearn/kamus_normalisasi.joblib b/api_slearn/artifacts_slearn/kamus_normalisasi.joblib new file mode 100644 index 0000000000000000000000000000000000000000..634512f21e7ae8f11724fcee80b64e62a1cf94d2 GIT binary patch literal 75358 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", + "encoding": "'utf-8'", + "input": "'content'", + "lowercase": "True", + "max_df": "1.0", + "max_features": "None", + "min_df": "1", + "ngram_range": "(1, 1)", + "norm": "None", + "preprocessor": "None", + "smooth_idf": "True", + "stop_words": "None", + "strip_accents": "None", + "sublinear_tf": "False", + "token_pattern": "'(?u)\\\\b\\\\w\\\\w+\\\\b'", + "tokenizer": "None", + "use_idf": "True", + "vocabulary": "None" + }, + "logistic_regression_parameters": { + "C": 1.0, + "class_weight": null, + "dual": false, + "fit_intercept": true, + "intercept_scaling": 1, + "l1_ratio": null, + "max_iter": 1000, + "multi_class": "deprecated", + "n_jobs": null, + "penalty": "l2", + "random_state": null, + "solver": "lbfgs", + "tol": 0.0001, + "verbose": 0, + "warm_start": false + }, + "confidence": { + "tersedia": true, + "sumber": "LogisticRegression.predict_proba", + "definisi": "Confidence adalah probabilitas pada kelas yang dipilih sebagai hasil prediksi model.", + "bukan": "Confidence bukan tingkat akurasi atau jaminan prediksi benar.", + "kelas": { + "0": "Probabilitas Negatif", + "1": "Probabilitas Positif" + } + }, + "website": { + "training_ulang": false, + "tfidf_fit_ulang": false, + "gunakan_tfidf_transform": true, + "mendukung_prediksi_batch_csv": true, + "mendukung_predict_proba": true + }, + "python_version": "3.13.16", + "library_versions": { + "scikit-learn": "1.6.1", + "numpy": "2.1.3", + "scipy": "1.16.3", + "pandas": "2.2.3", + "nltk": "3.9.1", + "Sastrawi": "1.0.1", + "joblib": "1.6.0" + }, + "dibuat_pada_utc": "2026-10-08T23:04:35.996842+00:00", + "kamus_sumber_sha256": "ff82815802208fefc93f5ec2582b8b9d0137efb85719da559a6bd4be06f2638f", + "sha256_files": { + "requirements.txt": "669c113634d1c30c04e3a859d2cdfe4da162c1b6926d7248e44bab29608d3adb", + "README.txt": "95a299ee72388f64f2002c91536d770b090aa414b4d771b13d4b8bae2954a0ef", + "stopwords_final.joblib": "86fde7096c19c3e139e3afaecc97d558211001520ee2daef6c72a60df7d58bb0", + "preprocessing_slearn.py": "6797aef8198520720fe8db837346c97304aee2bc1f3870bbf4ac1fdb8b77ec44", + "kamus_alay_sumber.csv": "ff82815802208fefc93f5ec2582b8b9d0137efb85719da559a6bd4be06f2638f", + "tfidf_vectorizer.joblib": "59669b82757eca691cd48d33d39dd52c2ede5bc17724b882bd242782bdbca1d1", + "preprocessing_config.json": "704fa2d507aa2fc9ed5382dc675210dca80c816af921bd16d8ae9b4d4ba18ad0", + "kamus_normalisasi.joblib": "60b7e79bab04401cbd3cbbf2b0920512a37f6e5066aee729710157d16e1203e2", + "model_logistic_regression.joblib": "a7813f5b31ed2aa5e5b413a38bed0ec76e75dde714af93a0f7d1b9741c47f1fc" + } +} \ No newline at end of file diff --git a/api_slearn/artifacts_slearn/model_logistic_regression.joblib b/api_slearn/artifacts_slearn/model_logistic_regression.joblib new file mode 100644 index 0000000000000000000000000000000000000000..62aefd4a490775dd893840edc3e80c7a26ec6a60 GIT binary patch literal 6271 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 literal 0 HcmV?d00001 diff --git a/api_slearn/artifacts_slearn/preprocessing_config.json b/api_slearn/artifacts_slearn/preprocessing_config.json new file mode 100644 index 0000000..69dd0de --- /dev/null +++ b/api_slearn/artifacts_slearn/preprocessing_config.json @@ -0,0 +1,5419 @@ +{ + "kamus_normalisasi": { + "woww": "wow", + "aminn": "amin", + "met": "selamat", + "netaas": "menetas", + "keberpa": "keberapa", + "eeeehhhh": "eh", + "kata2nyaaa": "kata-katanya", + "hallo": "halo", + "kaka": "kakak", + "ka": "kak", + "daah": "dah", + "aaaaahhhh": "ah", + "yaa": "ya", + "smga": "semoga", + "slalu": "selalu", + "amiin": "amin", + "kk": "kakak", + "trus": "terus", + "sii": "sih", + "nyenengin": "menyenangkan", + "bgt": "banget", + "gemess": "gemas", + "akuuu": "aku", + "jgn": "jangan", + "udah": "sudah", + "gitu": "begitu", + "aja": "saja", + "gemesiin": "menggemaskan", + "menyenangkn": "menyenangkan", + "rb": "ribu", + "akau": "aku", + "saranin": "menyarankan", + "nemuin": "menemukan", + "yah": "ya", + "mnis2nya": "manis-manisnya", + "gtu": "begitu", + "tu": "itu", + "idolaa": "idola", + "bangett": "banget", + "kaakk": "kak", + "baaiikkk": "baik", + "telaatt": "telat", + "kaak": "kak", + "yg": "yang", + "lg": "lagi", + "skarang": "sekarang", + "sempet": "sempat", + "karna": "karena", + "ngga": "tidak", + "mantep": "mantap", + "loohhh": "loh", + "yeee": "ya", + "baguuuuus": "bagus", + "sampe": "sampai", + "ngikutin": "mengikuti", + "gayany": "gayanya", + "bnk": "bang", + "awaas": "awas", + "siip": "sip", + "abizzzz": "habis", + "kerrrrennnn": "keren", + "sayaa": "saya", + "pengen": "ingin", + "kpan": "kapan", + "sma": "sama", + "sdkit": "sedikit", + "bngt": "banget", + "lgu": "lagu", + "bener2": "benar-benar", + "kharismanya": "karismanya", + "aq": "aku", + "jdi": "jadi", + "lgi": "lagi", + "gk": "tidak", + "kek": "seperti", + "lgunya": "lagunya", + "kereeeeennnn": "keren", + "nyamain": "menyamakan", + "lakik": "laki", + "siippp": "sip", + "bkn": "bukan", + "nyangka": "menyangka", + "bangettt": "banget", + "kereeen": "keren", + "wihhhhhh": "wih", + "tp": "tapi", + "inget": "ingat", + "bingitsss": "banget", + "dmn": "dimana", + "bird": "biru", + "kaloo": "kalo", + "gimna": "bagaimana", + "tuuh": "tuh", + "nyebur": "mencebur", + "yng": "yang", + "dbuat": "dibuat", + "kn": "kan", + "org": "orang", + "pingin": "pengin", + "bangcat": "bangsat", + "yha": "ya", + "waoohhh": "wah", + "pngn": "pengin", + "tmpt": "tempat", + "dhe": "deh", + "indahhh": "indah", + "bgttt": "banget", + "tuhh": "tuh", + "berarto": "berarti", + "ga": "tidak", + "aamiin": "amin", + "amiiinn": "amin", + "dgn": "dengan", + "aminnnn2": "amin-amin", + "amiiiin": "amin", + "aminnn": "amin", + "amiend": "amien", + "yaaa": "ya", + "ameen": "amin", + "aminnnn": "amin", + "amiiinnn": "amin", + "ngomong": "mengomong", + "mamer": "memamerkan", + "keles": "kali", + "jelase": "jelas", + "liat": "lihat", + "uwakk": "uwak", + "ntar": "entar", + "bs": "bisa", + "gini": "begini", + "salfok": "salah fokus", + "njir": "anjir", + "piiink": "pink", + "astgfrullah": "astagfirullah", + "ko": "kok", + "cd": "celana dalam", + "gbs": "enggak bisa", + "bhs": "bahasa", + "yaaaaaa": "ya", + "gak": "tidak", + "bangshad": "bangsat", + "stagahhhh": "astaga", + "kpn": "kapan", + "iooo": "lo", + "dy": "dia", + "d": "di", + "mslh": "masalah", + "qt": "kita", + "plampiasannn": "pelampiasan", + "masalahh": "masalah", + "tohh": "toh", + "inghinn": "ingin", + "dhi": "di", + "mengherthii": "mengerti", + "yanckh": "yang", + "chelalyyu": "selalu", + "therchakitty": "tersakiti", + "mauuuuu": "mau", + "kaliii": "kali", + "bangat": "banget", + "bkin": "bikin", + "dongg": "dong", + "yokkk": "yuk", + "yu": "yuk", + "sweger": "segar", + "gantengg": "ganteng", + "bingiit": "banget", + "kebukaaa": "terbuka", + "tpi": "tetapi", + "keliatan": "terlihat", + "yakk": "ya", + "kayakk": "kayak", + "asliii": "asli", + "kyk": "seperti", + "cntiknya": "cantiknya", + "bat": "banget", + "ngeliatnya": "melihatnya", + "sll": "selalu", + "bet": "banget", + "njerr": "anjir", + "bingiiittttzzzz": "banget", + "k": "ke", + "klo": "kalau", + "dooong": "dong", + "masi": "masih", + "gni": "begini", + "hdk": "hendak", + "jugaa": "juga", + "mksud": "maksud", + "aj": "saja", + "tetep": "tetap", + "mbakx": "mbaknya", + "syantik": "cantik", + "kite": "kita", + "pa": "apa", + "mksd": "maksud", + "bejilbbnya": "berjilbabnya", + "masio": "masih", + "yahhh": "ya", + "makasih": "terima kasih", + "leh": "boleh", + "ugaaa": "juga", + "jerewi": "jerawat", + "ilang": "hilang", + "jilbabnyaah": "jilbabnya", + "yuuuk": "yuk", + "warnaaa": "warna", + "punyaaa": "punya", + "cantiik": "cantik", + "sukaaak": "suka", + "kebayanyaa": "kebayanya", + "mantes": "memantas", + "merekaaaa": "mereka", + "jugaaa": "juga", + "gilaaaa": "gila", + "tiga2nya": "tiga-tiganya", + "lucuu": "lucu", + "emessss": "gemas", + "ucullllll": "lucu", + "males": "malas", + "bangeettt": "banget", + "gemesshh": "gemas", + "apaaa": "apa", + "ampuunn": "ampun", + "ciuuuummmm": "cium", + "mbil": "sambil", + "ngmng": "mengomong", + "ngemeng": "mengomong", + "epe": "apa", + "seehh": "sih", + "yank": "sayang", + "ucul": "lucu", + "yahh": "ya", + "gw": "saya", + "ngakak": "mengakak", + "astagfirrullah": "astagfirullah", + "suerrr": "suer", + "mnta": "minta", + "syg": "sayang", + "ntik": "cantik", + "pny": "punya", + "dapet": "dapat", + "suru": "suruh", + "lucuuuu": "lucu", + "ketawakkkk": "ketawa", + "kyanya": "kayanya", + "bnget": "banget", + "nakutin": "menakutkan", + "malingg": "maling", + "cocokk": "cocok", + "niihh": "nih", + "imutttttt": "imut", + "satuuuuu": "satu", + "sesekk": "sesak", + "nafass": "nafas", + "bgtt": "banget", + "cintaaaaaaa": "cinta", + "lucuk": "lucu", + "ma": "sama", + "duluu": "dulu", + "megang": "memegang", + "kawinin": "mengawini", + "liatin": "perlihatkan", + "ny": "nya", + "bdan": "badan", + "nyaaa": "nya", + "gemesin": "menggemaskan", + "fotoin": "memfoto", + "ngaku": "mengaku", + "kaaa": "kak", + "ganteng2": "ganteng-ganteng", + "pcarny": "pacarnya", + "kri": "kiri", + "knpa": "kenapa", + "pda": "pada", + "cmn": "cuman", + "gara2": "gara-gara", + "garuk2": "garuk-garuk", + "tembokk": "tembok", + "namax": "namanya", + "kiriii": "kiri", + "bungkusin": "bungkuskan", + "atu": "satu", + "dehhhh": "deh", + "kakk": "kak", + "ngegantiin": "menggantikan", + "alloh": "allah", + "smpe": "sampai", + "msh": "masih", + "trsa": "terasa", + "khadirannya": "kehadirannya", + "fikiran": "pikiran", + "hayalanku": "khayalanku", + "bru": "baru", + "nton": "menonton", + "ngapain": "mengapai", + "surgaa": "surga", + "kangenn": "kangen", + "kgn": "kangen", + "kakkkkk": "kak", + "rinduuuuuuuuu": "rindu", + "ninggalin": "meninggalkan", + "smg": "semoga", + "terangkhn": "terangkan", + "bgtttt": "banget", + "knp": "kenapa", + "tuk": "untuk", + "slama": "selama", + "orng": "orang", + "bngunlah": "bangunlah", + "bngun": "bangun", + "ea": "ya", + "qu": "ku", + "hiri": "hari", + "teman\"ny": "teman-temannya", + "aza": "saja", + "dr": "dari", + "alm": "almarhum", + "skrg": "sekarang", + "sie": "sih", + "benet": "benar", + "bangetttt": "banget", + "tuu": "tuh", + "skrang": "sekarang", + "gda": "enggak ada", + "gantiin": "menggantikan", + "kta": "kita", + "kklau": "kalau", + "acra": "acara", + "kasian": "kasihan", + "yaaa ": "ya", + "bnerr": "benar", + "emk": "emak", + "temenn": "teman", + "pngenn": "pengin", + "terkenall": "terkenal", + "ajhh": "saja", + "sungguh2": "sungguh-sungguh", + "mnjadi": "menjadi", + "shbt": "sahabat", + "bner": "benar", + "ktny": "katanya", + "dimusiumin": "dimuseumkan", + "bntu": "bantu", + "dlu": "dulu", + "udh": "sudah", + "kangem": "kangen", + "amiiin": "amin", + "smua": "semua", + "slmt": "selamat", + "jln": "jalan", + "mudh\"an": "mudah-mudahan", + "tenny": "tenang", + "sdh": "sudah", + "tnya": "tanya", + "blm": "belum", + "td": "tadi", + "sblm": "sebelum", + "bole": "boleh", + "lahhh": "lah", + "maap": "maaf", + "cm": "cuma", + "kmrn": "kemarin", + "wkt": "waktu", + "ama": "sama", + "ak": "aku", + "lht": "lihat", + "biasa2": "biasa-biasa", + "mnrtkuu": "menurutku", + "trs": "terus", + "ikutin": "mengikuti", + "jdwl": "jadwal", + "wajiiib": "wajib", + "aaaammmiiiinnnn": "amin", + "dtnggu": "ditunggu", + "jan": "jangan", + "kunjungin": "kunjungi", + "lagiiii": "lagi", + "waktunyaaa": "waktunya", + "diluangin": "diluangkan", + "jga": "juga", + "mimin": "admin", + "istimewaaa": "istimewa", + "blom": "belum", + "nangis": "menangis", + "ngaku\"": "mengaku-ngaku", + "diaa": "dia", + "donkk": "dong", + "kejawab": "terjawab", + "yaaaa": "ya", + "seru2an": "seru-seruan", + "mohom": "mohon", + "kebalas": "terbalas", + "nyempetin": "menyempatkan", + "wktunya": "waktunya", + "kaaaa": "kak", + "ngobrol": "mengobrol", + "namain": "namai", + "bls": "balas", + "ijap": "ijab", + "kobul": "kabul", + "co": "cowok", + "pst": "pasti", + "inggat": "ingat", + "brdua": "berdua", + "masing2": "masing-masing", + "sanggat": "sangat", + "siipp": "sip", + "laa": "lah", + "dlm": "dalam", + "hdup": "hidup", + "hdp": "hidup", + "kedpan": "kedepan", + "smoga": "semoga", + "mndaptkan": "mendapatkan", + "nutup\"in": "menutup-nutupi", + "iyah": "iya", + "sja": "saja", + "kakkk": "kak", + "za": "saja", + "mulut2": "mulut-mulut", + "peengn": "pengin", + "tuch": "tuh", + "muantteepp": "mantap", + "cpt": "cepat", + "mrit": "merit", + "y": "ya", + "ngapasih": "mengapa sih", + "ni": "nih", + "segitunya": "sebegitunya", + "fto": "foto", + "pke": "pakai", + "sisain": "menyisakan", + "slamt": "selamat", + "nantiii": "nanti", + "edaaan": "edan", + "emg": "memang", + "tunggu2": "tunggu-tunggu", + "sehat2": "sehat-sehat", + "ciyeeeeeee": "cie", + "pantes": "pantas", + "lairannya": "kelahirannya", + "jmnya": "jamnya", + "ajha": "saja", + "nyaaaa": "nya", + "nyari": "mencari", + "amiien": "amien", + "terbaikkk": "terbaik", + "ayahh": "ayah", + "ae": "saja", + "ndak": "indak", + "ngerti": "mengerti", + "mrekaaaa": "mereka", + "yaampuuuunnnn": "ya ampun", + "nyanyiin": "menyanyikan", + "ahhh": "ah", + "pgen": "pengin", + "tempt": "tempat", + "kh": "kah", + "bgini": "begini", + "brani": "berani", + "km": "kamu", + "jalan2": "jalan-jalan", + "asyikkkkk": "asyik", + "iriiiii": "iri", + "ngajak": "mengajak", + "naaaaahh": "nah", + "kka": "kakak", + "yak": "ya", + "ttep": "tetap", + "sperti": "seperti", + "jrang": "jarang", + "msuk": "masuk", + "gossip2.an": "gosip-gosipan", + "makkk": "mak", + "guee": "gue", + "pengennn": "pengin", + "gilzz": "gila", + "nyaru": "menyaru", + "gilaaa": "gila", + "jd": "jadi", + "kebarat2an": "kebarat-baratan", + "ohh": "oh", + "mantabs": "mantab", + "jgan": "jangan", + "mlu": "melulu", + "payahh": "payah", + "ntaf": "mantap", + "beneran": "benar", + "mudah\"n": "mudah-mudahan", + "gada": "enggak ada", + "milikin": "memiliki", + "kamo": "kamu", + "inih": "ini", + "sukaaaa": "suka", + "sukaaaaaa": "suka", + "kirain": "mengira", + "mehoonggggg": "mahal", + "apikkkk": "apik", + "yach": "ya", + "kereenn": "keren", + "ginii": "begini", + "ayoook": "ayo", + "yoiii": "yoi", + "gituuu": "begitu", + "iyaaa": "iya", + "pgn": "pengin", + "liptsk": "lipstik", + "nyeplak": "menjiplak", + "cw": "cewek", + "kmarin": "kemarin", + "mengncang": "mengencang", + "tauu": "tau", + "ksh": "kasih", + "skrng": "sekarang", + "mksh": "terima kasih", + "ank": "anak", + "skitar": "sekitar", + "ktmu": "ketemu", + "tmen": "teman", + "ats": "atas", + "donk": "dong", + "heii": "hai", + "diputusin": "diputuskan", + "pcrnya": "pacarnya", + "mnjual": "menjual", + "ngadain": "mengadakan", + "kkak": "kakak", + "rekomendasiin": "rekomendasikan", + "sihhh": "sih", + "cooocok": "cocok", + "mrip": "mirip", + "mama.nyy": "mamanya", + "bosenin": "membosankan", + "kmu": "kamu", + "bukn": "bukan", + "sey": "sih", + "cantikkk": "cantik", + "banger": "banget", + "kaa": "kak", + "eakyu": "waktu", + "cntik": "cantik", + "kakanya": "kakaknya", + "doeloe": "dulu", + "cantikkkk": "cantik", + "bngtt": "banget", + "iyak": "iya", + "malh": "malah", + "mlah": "malah", + "ngak": "tidak", + "laki2": "laki-laki", + "gag": "tidak", + "fhotonya": "fotonya", + "bleh": "boleh", + "brp": "berapa", + "tnggl": "tanggal", + "tanteny": "tantenya", + "ky": "kaya", + "sklinyaa": "sekalinya", + "ayoo": "ayo", + "w": "gue", + "dramaa": "drama", + "niiih": "nih", + "bisaa": "bisa", + "lha": "lah", + "dunk": "dong", + "lihaat": "lihat", + "yuwkkk": "yuk", + "nnti": "nanti", + "editannn": "editan", + "mw": "mau", + "thu": "itu", + "mai": "mau", + "kamuu": "kamu", + "pastii": "pasti", + "bisaaa": "bisa", + "kejuu": "keju", + "kereen": "keren", + "nama.a": "namanya", + "kmauan": "kemauan", + "ahhhh": "ah", + "yuuuuk": "yuk", + "dmana": "dimana", + "disiniii": "disini", + "cobaa": "coba", + "bayangin": "bayangkan", + "yaudh": "ya sudah", + "dl": "dulu", + "ajaaa": "saja", + "tuhhh": "tuh", + "amploppp": "amplop", + "mo": "mau", + "sayangg''": "sayang-sayang", + "nantii": "nanti", + "aminnnnn": "amin", + "gakuat": "enggak kuat", + "yukkkk": "yuk", + "nged": "banget", + "nantik": "nanti", + "ksitu": "kesitu", + "bangeett": "banget", + "liad": "lihat", + "kluarga": "keluarga", + "biasahhh": "biasa", + "tutorialx": "tutorialnya", + "dpke": "dipakai", + "fhoto": "foto", + "kerenn": "keren", + "binggo": "banget", + "rekomndd": "rekomendasi", + "bli2n": "beli-belikan", + "bwt": "buat", + "wado": "waduh", + "guaa": "gua", + "yaakkk": "ya", + "bangetttttyt": "banget", + "bangettttt": "banget", + "bangg": "bang", + "kangeeeeeeeeennnnnnnn": "kangen", + "kaliannnnnn": "kalian", + "rinduuuuuuuuuu": "rindu", + "kangennnnnmnnnnnnnnnnnnnnnnn": "kangen", + "kaliannnnnm": "kalian", + "kangeeeeeeeeeeennnnn": "kangen", + "kangennnnnnnnnnnnnnnn": "kangen", + "kangennn": "kangen", + "kaliannnnnnn": "kalian", + "th2": "tahun-tahun", + "smpt": "sempat", + "blg": "bilang", + "trserah": "terserah", + "ad": "ada", + "cwo": "cowok", + "prnah": "pernah", + "wlwpun": "walaupun", + "trpsah": "terpisah", + "klw": "kalau", + "mmg": "memang", + "prsatukn": "persatukan", + "g": "tidak", + "slh": "salah", + "ngomng": "mengomong", + "mdh2an": "mudah-mudahan", + "sm": "sama", + "skedar": "sekadar", + "kangeeerrn": "kangen", + "pulak": "pula", + "terrrrbaik": "terbaik", + "jorookknya": "joroknya", + "ihhhhh": "ih", + "padahall": "padahal", + "jorokkkk": "jorok", + "mualll": "mual", + "klow": "kalau", + "muke": "muka", + "ajh": "saja", + "taiiii": "tahi", + "seneeeeeng": "senang", + "nyesel": "menyesal", + "besarin": "membesarkan", + "tanyang": "tayang", + "ber2": "berdua", + "jngan": "jangan", + "ngelarang": "melarang", + "kerreeen": "keren", + "pantess": "pantas", + "bngettt": "banget", + "baperr": "bawa perasaan", + "sudh": "sudah", + "sbr": "sabar", + "pngen": "pengin", + "nntn": "menonton", + "ht": "hati", + "pemainy": "pemainnya", + "jg": "juga", + "aqu": "aku", + "ngeliat": "melihat", + "baper": "bawa perasaan", + "akn": "akan", + "nyampe": "sampai", + "kbawa": "terbawa", + "bradegan": "beradegan", + "amiiinnnn": "amin", + "mlm": "malam", + "nikahin": "menikahi", + "hadeuh": "aduh", + "nyambung": "menyambung", + "liyat": "lihat", + "krn": "karena", + "sk": "suka", + "adengan": "adegan", + "engga": "tidak", + "bingit": "banget", + "dapetin": "mendapatkan", + "model2": "model-model", + "sitayngin": "ditayangkan", + "cocokkk": "cocok", + "mahhhh": "mah", + "cocokny": "cocoknya", + "iniw": "ini", + "q": "ku", + "setju": "setuju", + "bnern": "benaran", + "pcrn": "pacaran", + "klian": "kalian", + "cocokkkk": "cocok", + "bngett": "banget", + "abes": "habis", + "br": "baru", + "mksdny": "maksudnya", + "brpa": "berapa", + "ihh": "ih", + "kesampean": "kesampaian", + "beginiiii": "begini", + "ntr": "entar", + "beliin": "belikan", + "kya": "kaya", + "keq": "kayak", + "ngerasa": "merasa", + "sygkuu": "sayangku", + "ngeselin": "menyebalkan", + "baju'a": "bajunya", + "gara''": "gara-gara", + "teruss": "terus", + "pokok'a": "pokoknya", + "bg": "bang", + "ubur2": "ubur-ubur", + "kuyy": "yuk", + "bentarr": "bentar", + "dureen": "duren", + "ajibb": "ajib", + "mahall": "mahal", + "yukk": "yuk", + "niiihh": "nih", + "imuuttt": "imut", + "imuttzz": "imut", + "nha": "nya", + "konten2": "konten-konten", + "rasain": "merasai", + "daaaah": "deh", + "asliiii": "asli", + "cantiknnnnyaaaaaaa": "cantiknya", + "yuuk": "yuk", + "ngelihatnya": "melihatnya", + "bdn": "badan", + "na": "nya", + "ibu2": "ibu-ibu", + "lau": "kau", + "cantikk": "cantik", + "sexxi": "seksi", + "bulet": "bulat", + "ituu": "itu", + "ngocok": "mengocok", + "nyoba": "mencoba", + "gn": "begini", + "yaahhh": "ya", + "kencqng": "kencang", + "kra": "kira", + "diginikan": "dibeginikan", + "coret\"": "coret-coret", + "lahh": "lah", + "gambarin": "gambarkan", + "ngerii": "ngeri", + "digambarin": "digambarkan", + "klen": "kalian", + "bangke": "bangkai", + "kog": "kok", + "liatnya": "lihatnya", + "bedanyaaa": "bedanya", + "negri": "negeri", + "mbk": "mbak", + "pgin": "pengin", + "krudungan": "kerudungan", + "ajaa": "saja", + "maksutku": "maksudku", + "kerennnnn": "keren", + "njirr": "anjir", + "jd'y": "jadinya", + "manisnyaaaa": "manisnya", + "thun": "tahun", + "mbg": "mbak", + "bangeet": "banget", + "ngaco": "mengaco", + "ngomen": "mengomentari", + "kaaakk": "kak", + "gituh": "begitu", + "tenggah": "tengah", + "uuuuuaaaaaaaaaappppppppppiiiiiiiiiiiiiiiiiikkkkkkkkkkkkkkkk": "apik", + "kocakkkkkkkkkkkkkk": "kocak", + "ksukaanmuuu": "kesukaanmu", + "bagiii": "bagi", + "ayooo": "ayo", + "nontonx": "menontonnya", + "dpn": "depan", + "pi": "tapi", + "penasarannnn": "penasaran", + "gilakkkkkk": "gila", + "mauuu": "mau", + "nontoonnn": "menonton", + "cepet2": "cepat-cepat", + "kepingin": "pengin", + "nontonnnn": "menonton", + "deeek": "dek", + "ntappp": "mantap", + "deg2an": "deg-degan", + "cyiiin": "cinta", + "iyha": "iya", + "kureeeen": "keren", + "muantaap": "mantap", + "pake": "menggunakan", + "ngantri": "mengantri", + "bli": "beli", + "nraktir": "mentraktir", + "nonton": "menonton", + "sbnyak": "sebanyak", + "penasaraaann": "penasaran", + "ayoooo": "ayo", + "nii": "ini", + "nontonya": "menontonnya", + "kl": "kalo", + "nunggu": "menunggu", + "dpt": "dapat", + "qta": "kita", + "peen": "pengin", + "lw": "kalau", + "gga": "tidak", + "yaah": "ya", + "asikkk": "asik", + "gilak": "gila", + "mcm": "macam", + "ceritax": "ceritanya", + "yoooiii": "yoi", + "kuy": "yuk", + "udahh": "sudah", + "tauuuuu": "tau", + "sabarrrrr": "sabar", + "kqmu": "kamu", + "banggain": "banggakan", + "thn": "tahun", + "pengan": "pengin", + "sendeng": "senang", + "simpen": "simpan", + "gakk": "tidak", + "bagiin": "membagikan", + "nanggepin": "menanggapi", + "kapn": "kapan", + "barang2": "barang-barang", + "kirimin": "mengirimi", + "senengin": "senangi", + "ngasih": "memberikan", + "ampunnnn": "ampun", + "bln": "bulan", + "senengg": "senang", + "teruzzz": "terus", + "mantabb": "mantab", + "sumpahh": "sumpah", + "diluarann": "diluaran", + "trbaik": "terbaik", + "dlam": "dalam", + "anjirrrr": "anjir", + "jendess": "janda", + "dh": "dah", + "sichh": "sih", + "namnya": "namanya", + "geh": "gih", + "nggak": "tidak", + "22nya": "dua-duanya", + "aduuhhh": "aduh", + "miripp": "mirip", + "silauuu": "silau", + "waww": "wow", + "uga": "juga", + "njiirr": "anjir", + "bgs": "bagus", + "kesinii": "kesini", + "gemay": "gemas", + "dstu": "disitu", + "wiiiihhhh": "wih", + "samoga": "semoga", + "kereennn": "keren", + "wahh": "wah", + "nyangkaa": "menyangka", + "hasilnyaaa": "hasilnya", + "dahhhh": "dah", + "mudah2": "mudah-mudah", + "jugaakk": "juga", + "prnh": "pernah", + "diapa2in": "diapa-apai", + "yaak": "ya", + "lhoo": "loh", + "donx": "dong", + "gthu": "begitu", + "nda": "indak", + "maauuu": "mau", + "pacarrr": "pacar", + "idamann": "idaman", + "naturalllll": "natural", + "bnyak": "banyak", + "berntung": "beruntung", + "merka": "mereka", + "jln2": "jalan-jalan", + "romntis": "romantis", + "slamat": "selamat", + "ulan": "ulang", + "th": "tahu", + "mngkin": "mungkin", + "klau": "kalau", + "diksi": "dikasih", + "sipp": "sip", + "taun": "tahun", + "jugak": "juga", + "nungguin": "menunggu", + "ngeluarin": "mengeluarkan", + "keciill": "kecil", + "ughaa": "juga", + "tante\"": "tante-tante", + "banged": "banget", + "abeees": "habis", + "kapan2": "kapan-kapan", + "adain": "adakan", + "bsa": "bisa", + "bosen2": "bosan-bosan", + "sempurnaaaaa": "sempurna", + "kaan": "kan", + "kece2": "kece-kece", + "badaaiii": "badai", + "beneraan": "benaran", + "cantiiik": "cantik", + "kuatttt": "kuat", + "cantiiiiiiik": "cantik", + "cantiknyaaa": "cantiknya", + "bae2": "baik-baik", + "bgd": "banget", + "mb": "mbak", + "catiiiiik": "cantik", + "subahanallah": "subhanallah", + "jaman2nya": "jaman-jamannya", + "padet": "padat", + "buktiin": "membuktikan", + "cantiq": "cantik", + "pasien\"nya": "pasien-pasiennya", + "lohh": "loh", + "utk": "untuk", + "keeeerrrreeeennnnnnnnn": "keren", + "woow": "wow", + "choker2": "choker-choker", + "hadirrr": "hadir", + "hadirr": "hadir", + "jelashh": "jelas", + "sekali2": "sekali-kali", + "mbaaa": "mbak", + "temen": "teman", + "hidupppp": "hidup", + "nyebelin": "menyebalkan", + "diliatnya": "dilihatnya", + "abis2": "habis-habis", + "gpp": "enggak apa-apa", + "para2": "parah-parah", + "bangeeet": "banget", + "allahhhhhhh": "allah", + "gemesnya": "gemasnya", + "ilang2": "hilang-hilang", + "kmi": "kami", + "ngilangin": "menghilangkan", + "tbuh": "tubuh", + "itungan": "hitungan", + "ledekin": "ledeki", + "tmn2": "teman-teman", + "repot2": "repot-repot", + "oles2": "oles-oles", + "ckup": "cukup", + "dptkan": "dapatkan", + "artis2": "artis-artis", + "kalii": "kali", + "ninggiin": "meninggikan", + "baget": "banget", + "dri": "dari", + "kira2": "kira-kira", + "lgsg": "langsung", + "ngk": "tidak", + "ahh": "ah", + "pas2an": "pas-pasan", + "balesin": "membalas", + "gausah": "enggak usah", + "sukaaa": "suka", + "lutuna": "lucunya", + "ciapa": "siapa", + "gemesss": "gemas", + "duhh": "duh", + "terharuuu": "terharu", + "alamii": "alami", + "sampek": "sampai", + "ckrng": "sekarang", + "ngegemesin": "menggemaskan", + "imoet": "imut", + "imut2": "imut-imut", + "jagain": "menjaga", + "yahbh": "ya", + "msih": "masih", + "macem2": "macam-macam", + "neko2": "neko-neko", + "sktika": "seketika", + "lutu": "lucu", + "wowwww": "wow", + "massss": "mas", + "kesiniii": "kesini", + "keren\"": "keren-keren", + "jalan\"nya": "jalan-jalannya", + "masyaallahhh": "masyaallah", + "kerennn": "keren", + "ksni": "kesini", + "begok": "bego", + "bkan": "bukan", + "jgk": "juga", + "bawak": "bawa", + "it": "itu", + "nti": "nanti", + "pgi": "pagi", + "keeeeeren": "keren", + "baguusss": "bagus", + "mna": "mana", + "nahannnn": "tahan", + "bagaatt": "banget", + "gayannxa": "gayanya", + "kereeeen": "keren", + "uda": "sudah", + "ngekeh": "kekeh", + "xlian": "kalian", + "b3": "bertiga", + "ihhh": "ih", + "lcu": "lucu", + "pait": "pahit", + "koplakkk": "koplak", + "kerass": "keras", + "yachhh": "ya", + "gokil": "gila", + "ngelihat": "melihat", + "ngelawak": "melawak", + "iiihhhhh": "ih", + "nyolotinnnn": "menyolot", + "beratttttttt": "berat", + "cantiikkkk": "cantik", + "hijaabbb": "hijab", + "sekalii": "sekali", + "smnagatin": "menyemangati", + "pakek": "pakai", + "kanapa": "kenapa", + "cans": "cantik", + "banhet": "banget", + "bgttttttt": "banget", + "blng": "bilang", + "mngasi": "mengasih", + "kmbang": "kembang", + "pg": "pagi", + "nie": "ini", + "pla": "pula", + "kw": "kau", + "blakon": "berlakon", + "sy": "saya", + "nmpk": "nampak", + "bngtzt": "banget", + "lucukkkkk": "lucu", + "sbelah": "sebelah", + "baek": "baik", + "ciusss": "serius", + "palinf": "paling", + "fav": "favorit", + "maak": "mak", + "ngakaaaakkk": "mengakak", + "gokill": "gila", + "kocakkkkkk": "kocak", + "ngekek": "mengekek", + "capee": "cape", + "pegangin": "memegang", + "nyender": "menyandar", + "bener": "benar", + "manjain": "memanjakan", + "peleotin": "meleot", + "bng": "bang", + "termurahhhh": "termurah", + "nyatakan": "menyatakan", + "graaatiiiis": "gratis", + "meriahh": "meriah", + "cantikkkm": "cantik", + "parahh": "parah", + "bingits": "banget", + "yuppss": "ya", + "nyindir": "menyindir", + "onoh": "sono", + "bnyk": "banyak", + "anakny": "anaknya", + "staf2nya": "staf-stafnya", + "begete": "banget", + "aduhh": "aduh", + "manteppp": "mantap", + "hati2": "hati-hati", + "jangan2": "jangan-jangan", + "kata2nya": "kata-katanya", + "ynk": "sayang", + "nangis\"": "menangis-nangis", + "hati\"": "hati-hati", + "jangan\"": "jangan-jangan", + "emank": "memang", + "cr": "cara", + "diemin": "diamkan", + "hancurin": "menghancurkan", + "ckp": "cukup", + "mgkn": "mungkin", + "ato": "atau", + "diapain": "diapakan", + "ms": "masih", + "haragain": "menghargai", + "cb": "coba", + "nyakitin": "menyakiti", + "jgn2": "jangan-jangan", + "sbaiknya": "sebaiknya", + "mngerti": "mengerti", + "blajar": "belajar", + "fakta2": "fakta-fakta", + "spt": "seperti", + "mnding": "mending", + "nilai2": "nilai-nilai", + "buka2": "buka-buka", + "nyinyirin": "nyinyir", + "pdhl": "padahal", + "pd": "pada", + "nyerang": "menyerang", + "hrs": "harus", + "mrk": "mereka", + "trima": "terima", + "ngomomg": "mengomong", + "mang": "memang", + "kbetulan": "kebetulan", + "orgtuanya": "orang tuanya", + "sbelum": "sebelum", + "ngamatin": "mengamati", + "ngintip": "mengintip", + "stelah": "setelah", + "sndiri": "sendiri", + "ngintilin": "mengintil", + "smnjak": "semenjak", + "trbukti": "terbukti", + "nyambet": "menyambat", + "msk": "masuk", + "krmh": "ke rumah", + "rmh": "rumah", + "bo'ong": "bohong", + "eloo": "lo", + "apa2nya": "apa-apanya", + "goblokkk": "goblok", + "sesatt": "sesat", + "ibliss": "iblis", + "elo": "lo", + "kbiasaan": "kebiasaan", + "hdpnya": "hidupnya", + "lbh": "lebih", + "byk": "banyak", + "anknya": "anaknya", + "tdr": "tidur", + "sbuah": "sebuah", + "anak2nya": "anak-anaknya", + "tdnya": "tadinya", + "ank2nya": "anak-anaknya", + "milih": "memilih", + "branjak": "beranjak", + "ank2": "anak-anak", + "masyarakay": "masyarakat", + "mudah2an": "mudah-mudahan", + "menegerti": "mengerti", + "apa2": "apa-apa", + "sok2": "sok-sok", + "nasehatin": "menasihati", + "ngaca": "mengaca", + "ajah": "saja", + "kakkkk": "kak", + "slamet": "selamat", + "ampe": "sampai", + "nari": "menari", + "ehh": "eh", + "mf": "maaf", + "ngcpin": "mengucapkan", + "woowwww": "wow", + "iaa": "iya", + "kdg": "kadang", + "kebagi": "terbagi", + "nerusin": "meneruskan", + "indo": "indonesia", + "univ": "universitas", + "maen": "main", + "wihh": "wih", + "tetapppp": "tetap", + "gantenggg": "ganteng", + "jakety": "jaketnya", + "nggk": "tidak", + "skli": "sekali", + "kli": "kali", + "ttp": "tetap", + "pala": "kepala", + "pusingggg": "pusing", + "pucing": "pusing", + "ihhhh": "ih", + "maho": "manusia homo", + "gantng": "ganteng", + "alak": "kepala", + "hilangv": "hilang", + "bungggg": "bung", + "gapap": "enggak apa-apa", + "kyak": "kayak", + "pling": "paling", + "gokiiill": "gila", + "busedh": "buset", + "gokilnya": "gilanya", + "duluuu": "dulu", + "keceee": "kece", + "nyaa": "nya", + "klu": "kalau", + "gilaa": "gila", + "duuhhh": "duh", + "amiiiiinnnnn": "amin", + "dg": "dengan", + "tullus": "tulus", + "tw": "tau", + "pluk": "peluk", + "durhaka2": "durhaka-durhaka", + "kpada": "kepada", + "seburuk\"": "seburuk-buruk", + "ortu": "orang tua", + "ktaa": "kita", + "yup": "ya", + "tdk": "tidak", + "tetaplaah": "tetaplah", + "ajja": "saja", + "dech": "deh", + "oprasi": "operasi", + "gya": "gaya", + "kndung": "kandung", + "sbg": "sebagai", + "u": "lu", + "ksenangan": "kesenangan", + "ddpt": "didapat", + "jwab": "jawab", + "mmbodohkan": "membodohkan", + "orlaen": "orang lain", + "bgtu": "begitu", + "drpd": "daripada", + "malem": "malam", + "aplikasiin": "mengaplikasikan", + "berangat": "berangkat", + "praktekin": "mempraktikkan", + "dahh": "dah", + "gmn": "bagaimana", + "omdo": "omong doang", + "kate": "kata", + "cabe2": "cabe-cabe", + "prjuangan": "perjuangan", + "kmewahan": "kemewahan", + "stimpal": "setimpal", + "ddpat": "didapat", + "sprt": "seperti", + "bc": "baca", + "mnilai": "menilai", + "kata2": "kata-kata", + "usahh": "usah", + "segala2nya": "segala-galanya", + "sekarng": "sekarang", + "sprti": "seperti", + "mkanya": "makanya", + "hay": "hai", + "mreka": "mereka", + "bhagia": "bahagia", + "korek2": "korek-korek", + "ngebales": "membalas", + "krna": "karena", + "sihh": "sih", + "sasama": "sesama", + "dehh": "deh", + "cabe2an": "cabe-cabean", + "terong2an": "terong-terongan", + "seklian": "sekalian", + "kluar": "keluar", + "tuuu": "itu", + "ngurusan": "mengurusi", + "campurin": "mencampuri", + "emnknya": "memangnya", + "py": "punya", + "unyuu": "unyu", + "mrangkul": "merangkul", + "adlh": "adalah", + "wlo": "walau", + "lh": "boleh", + "manusiy": "manusia", + "tx": "tak", + "samara": "sakinah mawadah rahman", + "teringt": "teringat", + "trmsuk": "termasuk", + "dn": "dan", + "pilihnmu": "pilihanmu", + "swmntara": "sementara", + "trmasuk": "termasuk", + "orang2": "orang-orang", + "sgt": "sangat", + "liht": "lihat", + "suaminy": "suaminya", + "jadulnya": "jaman dulunya", + "mantaff": "mantap", + "ngata2in": "mengatai-ngatai", + "bagaimna": "bagaimana", + "mulusssss": "mulus", + "bgus": "bagus", + "gmana": "bagaimana", + "kpleset": "kepleset", + "yawlah": "ya allah", + "kepeleset": "kepleset", + "kayanye": "kayanya", + "amiinnnn": "amin", + "komen2an": "komen-komenan", + "wewww": "wow", + "dumsss": "dong", + "tar": "entar", + "perbaikin": "perbaiki", + "permasalhkan": "permasalahkan", + "betul2": "betul-betul", + "jauhin": "jauhi", + "dateng": "datang", + "adiik": "adik", + "manisss": "manis", + "besar\"": "besar-besar", + "masing\"": "masing-masing", + "ddlm": "di dalam", + "kmbali": "kembali", + "dkuburnya": "dikuburnya", + "fi": "di", + "ttg": "tentang", + "rata2": "rata-rata", + "lbih": "lebih", + "critamu": "ceritamu", + "ja": "saja", + "bres": "beres", + "cmkn": "camkan", + "diurusin": "diurusi", + "biarin": "biarkan", + "kale": "kali", + "donks": "dong", + "membanding\"": "membanding-banding", + "mengperbolehkan": "memperbolehkan", + "krena": "karena", + "ngri": "negeri", + "dingin\"": "dingin-dingin", + "lma": "lama", + "dibangga'in": "dibangga-banggakan", + "ngebolehin": "membolehkan", + "denger": "dengar", + "jelek\"in": "menjelek-jelekkan", + "stiap": "setiap", + "gausa": "enggak usah", + "maksain": "memaksakan", + "smog": "semoga", + "mnerima": "menerima", + "baikxa": "baiknya", + "aaamin": "amin", + "amienn": "amien", + "sekangen2": "sekangen-kangen", + "kt": "kita", + "amiiiiiiiiiinnnnnnn": "amin", + "nolongin": "menolong", + "amiiiiiiiiiiin": "amin", + "seluru": "seluruh", + "amiennn": "amien", + "tebir": "ribet", + "doangam": "doang", + "tebir\"": "ribet-ribet", + "amad": "amat", + "mkst": "maksud", + "wong2": "wong-wong", + "rempong": "repot", + "ngurusin": "mengurusi", + "kerjaan": "pekerjaan", + "bingittzz": "banget", + "bangets": "banget", + "ikh": "ih", + "dwh": "deh", + "nengok": "menengok", + "bante": "bantai", + "la": "lah", + "komenannya": "komentarnya", + "hadapin": "menghadapi", + "dengarin": "mendengarkan", + "ush": "usah", + "cock": "cocok", + "widihh": "idih", + "mantepp": "mantap", + "cita2": "cita-cita", + "temen-temen": "teman-teman", + "stu": "satu", + "syratnya": "syaratnya", + "pnjang": "panjang", + "shat": "sehat", + "inginkn": "inginkan", + "belanjaa": "belanja", + "burem": "buram", + "tlp": "telepon", + "dngan": "dengan", + "ngerasain": "merasai", + "kancang": "kencang", + "tmbh": "tambah", + "mansyoallah": "masyaallah", + "skali": "sekali", + "canttttikkk": "cantik", + "subhanalloh": "subhanallah", + "lkuin": "lakukan", + "langgenh": "langgeng", + "foto2nya": "foto-fotonya", + "tanggung2": "tanggung-tanggung", + "salluutt": "salut", + "pkai": "pakai", + "org2": "orang-orang", + "mkn": "makin", + "seneng": "senang", + "pdng": "padang", + "warbiyasah": "luar biasa", + "pk": "pakai", + "keedaannya": "keadaannya", + "lagu2": "lagu-lagu", + "saya2": "saya-saya", + "sekrng": "sekarang", + "lagu2nya": "lagu-lagunya", + "ttap": "tetap", + "spertinya": "sepertinya", + "mrah": "marah", + "gra2": "gara-gara", + "sngaja": "sengaja", + "jlek": "jelek", + "udah2": "sudah-sudah", + "ungkit2": "ungkit-ungkit", + "pura2": "pura-pura", + "gatau": "tidak tahu", + "curhat2": "curhat-curhat", + "matany": "matanya", + "dteng": "datang", + "mata'y": "matanya", + "baju2": "baju-baju", + "buruannn": "buruan", + "emng": "memang", + "perubut": "perebut", + "sumai": "suami", + "tk": "tak", + "urusin": "urus", + "apah": "apa", + "bagus2": "bagus-bagus", + "say": "sayang", + "mmbela": "membela", + "mslhnya": "masalahnya", + "hnya": "hanya", + "sndri": "sendiri", + "sj": "saja", + "llki": "lelaki", + "dluar": "diluar", + "sna": "sana", + "boleh2": "boleh-boleh", + "dya": "dia", + "maluuuu": "malu", + "beda2": "beda-beda", + "nyaranin": "menyarankan", + "x": "kali", + "tongolin": "menongol", + "cabol": "cabul", + "ape": "apa", + "nyet": "monyet", + "muna": "munafik", + "jing": "anjing", + "tot": "entot", + "nihh": "nih", + "hidiyah": "hidayah", + "percya": "percaya", + "bgi": "bagi", + "tlg": "tolong", + "sya": "saya", + "sdri": "sendiri", + "tdi": "tadi", + "mgkin": "mungkin", + "bhwa": "bahwa", + "skaligus": "sekaligus", + "cek2": "cek-cek", + "spti": "seperti", + "smakin": "semakin", + "sbgai": "sebagai", + "mgk": "mungkin", + "bgmn": "bagaimana", + "sapa2": "sapa-sapa", + "sama2": "sama-sama", + "kyknya": "kayaknya", + "pnya": "punya", + "dng": "dengan", + "termurahhh": "termurah", + "bbrp": "beberapa", + "bhw": "bahwa", + "banyaaaaaak": "banyak", + "baik2nya": "baik-baiknya", + "tunjukan": "tunjukkan", + "anak2mu": "anak-anakmu", + "anak2": "anak-anak", + "ngasih2": "mengasih-ngasih", + "berfikir": "berpikir", + "amit2": "amit-amit", + "mba": "mbak", + "elu": "lu", + "ngajuin": "mengajukan", + "tiba2": "tiba-tiba", + "jkt": "jakarta", + "terbaeq": "terbaik", + "namany": "namanya", + "jels": "jelas", + "diakibtkn": "diakibatkan", + "suk": "suka", + "jelek2": "jelek-jelek", + "dsrny": "dasarnya", + "jkrta": "jakarta", + "mantaap": "mantap", + "benar2": "benar-benar", + "main2": "main-main", + "lanjutkaaaaan": "lanjutkan", + "hiduppp": "hidup", + "ncek": "encek", + "majuuu": "maju", + "teruuss": "terus", + "kdg2": "kadang-kadang", + "looohh": "loh", + "sebelumbya": "sebelumnya", + "tunjukin": "tunjukkan", + "tahun\"": "tahun-tahun", + "koment": "komentar", + "nyampein": "menyampaikan", + "hokiii": "hoki", + "naksir": "menaksir", + "bangt\"": "banget-banget", + "nafkahin": "menafkahi", + "ngitem": "menghitam", + "banyaaaak": "banyak", + "sama\"": "sama-sama", + "gx": "tidak", + "lhat": "lihat", + "cantika": "cantik", + "cwek": "cewek", + "kngen": "kangen", + "fillm": "film", + "sinet": "sinetron", + "jendral": "jenderal", + "nanya": "tanya", + "maenya": "mainnya", + "cantiks": "cantik", + "wktu": "waktu", + "pilm": "film", + "taon": "tahun", + "nd": "indak", + "slah": "salah", + "cini": "sini", + "jaaangan": "jangan", + "nambah": "menambah", + "naek": "naik", + "terimaksih": "terima kasih", + "inii": "ini", + "kaannnn": "kan", + "tidakkkk": "tidak", + "kemuh": "kamu", + "da": "dah", + "keren2": "keren-keren", + "sby": "surabaya", + "tgl": "tanggal", + "pinginnn": "pengin", + "buanget": "banget", + "ketemuuu": "ketemu", + "kebawa": "terbawa", + "mimpiii": "mimpi", + "wanjerrr": "anjir", + "satupunn": "satupun", + "motret": "memotret", + "temen2": "teman-teman", + "lapakk": "lapak", + "nyeniii": "menyeni", + "bget": "banget", + "seeh": "sih", + "abis": "habis", + "deuhh": "duh", + "b2an": "berduaan", + "dihargain": "dihargai", + "engga\"": "enggak-enggak", + "tahun2": "tahun-tahun", + "lebayy": "lebay", + "gilsss": "gila", + "nganbek": "mengambek", + "sukaa": "suka", + "enk": "enak", + "mendam": "memendam", + "manta": "mantan", + "relaaaa": "rela", + "bgun": "bangun", + "tdur": "tidur", + "cntk": "cantik", + "knl": "kenal", + "anugrah": "anugerah", + "slm": "salam", + "kliatan": "kelihatan", + "awoohhh": "allah", + "cantiikk": "cantik", + "gt": "begitu", + "sbbyak": "sebanyak", + "apun": "apapun", + "nyangkutnya": "menyangkutnya", + "untk": "untuk", + "ampunnnnn": "ampun", + "hy": "hai", + "wadaaw": "waduh", + "paraah": "parah", + "sampw": "sampai", + "dijelekin": "dijelekkan", + "napa": "kenapa", + "azeeeek": "asik", + "ntikk": "cantik", + "uhh": "uh", + "mayan": "lumayan", + "mnisss": "manis", + "tambahim": "tambahkan", + "tambahin": "tambahkan", + "bener\"": "benar-benar", + "make": "menggunakan", + "canttikkk": "cantik", + "baanggeettt": "banget", + "ap": "apa", + "siiihh": "sih", + "krpoooo": "kepo", + "pengn": "pengin", + "nyusul": "menyusul", + "nyok": "yuk", + "dekkk": "dek", + "ngiri": "mengiri", + "mmang": "memang", + "nentuin": "menentukan", + "mnjmput": "menjemput", + "ygs": "sayang", + "mblooo": "jomblo", + "ber'expresi": "berekspresi", + "jdulnya": "judulnya", + "ciumman": "ciuman", + "aurot": "aurat", + "laginy": "laginya", + "nyaaaaaaaaaaaaa": "nya", + "alkhamdulilah": "alhamdulillah", + "ktnya": "katanya", + "selaluu": "selalu", + "anjirrr": "anjir", + "ngeh": "engah", + "amen": "amin", + "qm": "kamu", + "nahhh": "nah", + "ngmngnya": "mengomongnya", + "sampai2": "sampai-sampai", + "hal2": "hal-hal", + "komunitas2": "komunitas-komunitas", + "fotony": "fotonya", + "halllo": "halo", + "ngiritya": "mengiritnya", + "hematt": "hemat", + "ciyeeee": "cie", + "yeeee": "ya", + "nget": "banget", + "kususnya": "khususnya", + "krm": "kirim", + "sam": "sama", + "ajaaahhhhh": "saja", + "syediiihhh": "sedih", + "malangg": "malang", + "belooomm": "belum", + "yaakk": "ya", + "kebukti": "terbukti", + "anak2e": "anak-anaknya", + "tuwanya": "tuanya", + "comen2nya": "komen-komennya", + "pedesssss": "pedas", + "jelas2": "jelas-jelas", + "skg": "sekarang", + "nyebut2": "menyebut-nyebut", + "plg": "pulang", + "smbil": "sambil", + "hellooooooo": "halo", + "ngrebut": "merebut", + "mnjdi": "menjadi", + "taek": "tahi", + "lupain": "melupakan", + "bangkret": "bangkrut", + "taekk": "tahi", + "lahhhhhh": "lah", + "kedokk": "kedok", + "mati\"an": "mati-matian", + "nyimak": "menyimak", + "komenx": "komennya", + "serem2": "seram-seram", + "ngumpet\"": "mengumpet-ngumpet", + "blim": "belum", + "sekrang": "sekarang", + "ngerebut": "merebut", + "mnjijikaaaaaan": "menjijikkan", + "astgfrlh": "astagfirullah", + "sahabatt": "sahabat", + "gnyngka": "enggak menyangka", + "sendirir": "sendiri", + "brengsekk": "brengsek", + "ngomentnya": "mengomentarinya", + "nusuk": "menusuk", + "skr": "sekarang", + "dpotong": "dipotong", + "helo": "halo", + "kmrin": "kemarin", + "ngmgin": "mengomongkan", + "bilangg": "bilang", + "gkhianati": "mengkhianati", + "kpd": "kepada", + "hr": "hari", + "sp": "sapa", + "pljaran": "pelajaran", + "spy": "supaya", + "mngikuti": "mengikuti", + "klkuan": "kelakuan", + "dibikin2": "dibikin-bikin", + "gimana": "bagaimana", + "nnt": "nanti", + "cew": "cewek", + "cow": "cowok", + "kasiannn": "kasihan", + "syekaliii": "sekali", + "hgga": "hingga", + "cieeeee": "cie", + "sholihah": "salihah", + "adeuh": "aduh", + "halahh": "alah", + "luu": "lu", + "yuuks": "yuk", + "yoo": "ya", + "ginian": "beginian", + "judulnyaa": "judulnya", + "ayoookkkkkk": "ayo", + "artix": "artinya", + "cewe": "cewek", + "cowo": "cowok", + "lt": "lantai", + "nama\"": "nama-nama", + "cwe": "cewek", + "serem": "seram", + "kepoin": "kepo", + "keluarin": "keluarkan", + "gajelas": "tidak jelas", + "mbak2": "mbak-mbak", + "pedes": "pedas", + "komentnya": "komentarnya", + "omongin": "omongkan", + "ngalirrrrr": "mengalir", + "idup": "hidup", + "nmbah": "menambah", + "terossss": "terus", + "ngehujat": "menghujat", + "dosaa": "dosa", + "ngebela": "membela", + "seindonesiyah": "seindonesia", + "seh": "sih", + "senyumin": "senyumi", + "mh": "mah", + "ngalir": "mengalir", + "ngrusak": "merusak", + "px": "punya", + "kslhn": "kesalahan", + "skrngnya": "sekarangnya", + "emang": "memang", + "kalik": "kali", + "bilangin": "bilang", + "nyambungin": "menyambung", + "sllu": "selalu", + "scra": "secara", + "nggali": "menggali", + "cmpur": "campur", + "dgr": "dengar", + "kurng": "kurang", + "nangiiiiis": "menangis", + "nangiiis": "menangis", + "ngomentarin": "mengomentari", + "idupnya": "hidupnya", + "ortunya": "orang tuanya", + "woey": "woy", + "bloghhh": "goblok", + "ngomongin": "mengomongkan", + "doank": "doang", + "abal\"": "abal-abal", + "dikata2in": "dikata-katai", + "ngehina": "menghina", + "gmna": "bagaimana", + "kebanggan": "kebanggaan", + "dah": "deh", + "jngn": "jangan", + "mnjawab": "menjawab", + "woooiiiiiiii": "woy", + "dosaaaaaaaaaa": "dosa", + "ingt": "ingat", + "karmaaa": "karma", + "woooiiiii": "woy", + "ngkkkkkk": "mengakak", + "makny": "maknya", + "wdooohhh": "waduh", + "kasihn": "kasihan", + "lhirr": "lahir", + "hujatann": "hujatan", + "palgi": "apalagi", + "wihhh": "wih", + "kakakkk": "kakak", + "bagusss": "bagus", + "jt": "juta", + "ngets": "banget", + "stp": "setiap", + "penbelian": "pembelian", + "cuaantik": "cantik", + "sukakkk": "suka", + "m": "sama", + "cucokkk": "cocok", + "lyt": "lihat", + "expresi": "ekspresi", + "natap": "menatap", + "ccok": "cocok", + "helloo": "halo", + "dripada": "daripada", + "difotoin": "difoto", + "psangan": "pasangan", + "cakepp": "cakep", + "ngiriii": "mengiri", + "coxok": "cocok", + "buatin": "membuat", + "nihhh": "nih", + "statusx": "statusnya", + "cem": "macam", + "yee": "ya", + "sunggu": "sungguh", + "lagiii": "lagi", + "ammiiin": "amin", + "haddeh": "aduh", + "maaasss": "mas", + "segerrrr": "segar", + "kemn": "kemana", + "suipit": "sipit", + "sipiit": "sipit", + "kedengeran": "kedengaran", + "semangaatt": "semangat", + "promil": "program hamil", + "lancr": "lancar", + "keputihn": "keputihan", + "gratisss": "gratis", + "kebayang": "terbayang", + "mdh\"": "mudah-mudah", + "bz": "bisa", + "amiinn": "amin", + "ammin": "amin", + "mendapatkn": "mendapatkan", + "lucuuuk": "lucu", + "amaattt": "amat", + "aje": "saja", + "yaoloh": "yaallah", + "ngerayain": "merayakan", + "sotoy": "sok tau", + "yok": "yuk", + "d.bikinin": "dibikin", + "mmz": "mas", + "bikinin": "membikin", + "bapeeeer": "bawa perasaan", + "keliatannya": "kelihatannya", + "siih": "sih", + "diginiin": "dibeginikan", + "hadeehh": "aduh", + "pu": "punya", + "suamiiiii": "suami", + "krenn": "keren", + "kebykan": "kebanyakan", + "tingal": "tinggal", + "pernikahn": "pernikahan", + "ngtain": "mengatai", + "macem": "macam", + "bagusnyeee": "bagusnya", + "kemaren": "kemarin", + "hmpir": "hampir", + "liatnyaaa": "lihatnya", + "waw": "wow", + "mbuh": "embuh", + "bsk": "besok", + "waaa": "wah", + "berbiiiiiihhh": "barbie", + "canteek": "cantik", + "teru": "terus", + "nyatok": "mencatok", + "nyampenya": "sampainya", + "goncengin": "boncengkan", + "kasi": "kasih", + "dianterin": "diantar", + "gebleg": "geblek", + "bosen": "bosan", + "anterin": "antarkan", + "ngantar": "mengantar", + "nganterin": "mengantarkan", + "mz": "mas", + "nerima": "menerima", + "ati2": "hati-hati", + "yeuh": "ya", + "kmn": "kemana", + "dianter": "diantar", + "disupirin": "disopiri", + "jidad": "jidat", + "ciee": "cie", + "novembr": "november", + "hri": "hari", + "bedain": "membedakan", + "genduut": "gendut", + "ehhh": "eh", + "bertatto": "bertato", + "brrt": "berarti", + "kana": "kanan", + "dtg": "datang", + "doong": "dong", + "sblah": "sebelah", + "tauuu": "tau", + "tengaj": "tengah", + "ps": "pas", + "gmpg": "gampang", + "engk": "tidak", + "apal": "hapal", + "dak": "indak", + "rupa2nya": "rupa-rupanya", + "kembaq": "kembar", + "tngh": "tengah", + "mknya": "makanya", + "prmpuan": "perempuan", + "ketuker": "tertukar", + "drimnanya": "darimananya", + "cntikan": "cantikan", + "edaaaan": "edan", + "syp": "siapa", + "bcnda": "bercanda", + "uhhh": "uh", + "bazengg": "bajingan", + "haiii": "hai", + "bosaan": "bosan", + "gitu2": "begitu-gitu", + "kmana2": "kemana-mana", + "hrsnya": "harusnya", + "sdg": "sedang", + "komentar2": "komentar-komentar", + "kenyataan2": "kenyataan-kenyataan", + "silahkan": "silakan", + "lwt": "lewat", + "sukur2": "sukur-sukur", + "baek2": "baik-baik", + "cma": "cuma", + "berbeda2": "berbeda-beda", + "dibkatakan": "dikatakan", + "doonk": "dong", + "pengikuy": "pengikut", + "ngmngin": "mengomongkan", + "sblmnya": "sebelumnya", + "maksd": "maksud", + "adkah": "adakah", + "menbelah": "membelah", + "ayuk": "ayo", + "mngira": "mengira", + "bertolernsi": "bertoleransi", + "slahkan": "salahkan", + "tolernsi": "toleransi", + "sbnarnya": "sebenarnya", + "ingi": "ingin", + "oalaaaa": "walah", + "menjelekan": "menjelekkan", + "hadeuuh": "aduh", + "dikemanain": "dikemanakan", + "aneh2": "aneh-aneh", + "bpknya": "bapaknya", + "baca2": "baca-baca", + "jwb": "jawab", + "komen2": "komen-komen", + "sebelum2nya": "sebelum-sebelumnya", + "sbagai": "sebagai", + "sedihh": "sedih", + "kangennnn": "kangen", + "gndakan": "gandakan", + "kangeeeeeeeenya": "kangen", + "gaa": "tidak", + "ganti'kn": "menggantikan", + "tnang": "tenang", + "amiiiiiiiiiiiiiiiiin": "amin", + "kgeeeen": "kangen", + "kangeenn": "kangen", + "bangeeeet": "banget", + "bsok": "besok", + "hbis": "habis", + "mti": "mati", + "bngetttt": "banget", + "kangeen": "kangen", + "bgect": "banget", + "kebaikan\"y": "kebaikannya", + "dssi": "disisi", + "kngenn": "kangen", + "kangeeeeennn": "kangen", + "lagii": "lagi", + "dasyat": "dahsyat", + "aer": "air", + "bgni": "begini", + "skalii": "sekali", + "gg": "tidak", + "dtinggl": "ditinggal", + "terbaiiikkkkk": "terbaik", + "murahhh": "murah", + "pecahhg": "pecah", + "kakakgh": "kakak", + "terbaeeeek": "terbaik", + "prempuan": "perempuan", + "manteep": "mantap", + "nggeh": "engah", + "indonesiaaa": "indonesia", + "inia": "ini", + "maluu": "malu", + "cobaaaa": "coba", + "dehhh": "deh", + "cepetan": "cepatan", + "ntapsss": "mantap", + "kamiiii": "kami", + "smangat": "semangat", + "qmu": "kamu", + "sdkit2": "sedikit-sedikit", + "tntg": "tentang", + "penyakitny": "penyakitnya", + "ngajarin": "mengajari", + "laenny": "lainnya", + "amiinnn": "amin", + "scpatny": "secepatnya", + "sampein": "sampaikan", + "tuany": "tuanya", + "doa2": "doa-doa", + "pjg": "panjang", + "adeeek": "adek", + "bca": "baca", + "netes": "menetes", + "mataq": "mataku", + "byangin": "membayangkan", + "trjadi": "terjadi", + "dposisi": "diposisi", + "tanda2nya": "tanda-tandanya", + "dijwb": "dijawab", + "tmn2nya": "teman-temannya", + "sayy": "sayang", + "salaam": "salam", + "ngajar": "mengajar", + "fto2": "foto-foto", + "laen": "lain", + "dallam": "dalam", + "tentanh": "tentang", + "nangiss": "menangis", + "nnton": "menonton", + "kejadianny": "kejadiannya", + "klg": "keluarga", + "sayaang": "sayang", + "msg2": "masing-masing", + "pgnnya": "penginnya", + "meluk": "memeluk", + "siiihhh": "sih", + "mupeng": "muka pengin", + "caya": "percaya", + "aduhhhhhhhhh": "aduh", + "buseett": "buset", + "mblo": "jomblo", + "lucuuu": "lucu", + "gagahx": "gagahnya", + "miriiiip": "mirip", + "kaliiii": "kali", + "niru": "meniru", + "bpkmu": "bapakmu", + "sangt": "sangat", + "baang": "bang", + "gasih": "enggak sih", + "yuu": "yuk", + "yuuu": "yuk", + "samaa": "sama", + "ngademin": "mengademkan", + "aahh": "ah", + "makasi": "terima kasih", + "looo": "loh", + "yokk": "yuk", + "gils": "gila", + "ono": "sono", + "weeeh": "wih", + "suksea": "sukses", + "temenan": "berteman", + "sehh": "sih", + "deket": "dekat", + "dekt": "dekat", + "genggg": "geng", + "yawloh": "ya allah", + "bhnya": "bahannya", + "loo": "lo", + "samaaaa": "sama", + "abanggku": "abangku", + "sayanggg": "sayang", + "dsni": "disini", + "ciieeeeeee": "cie", + "kokohh": "engkoh", + "pdhal": "padahal", + "takuttt": "takut", + "yuks": "yuk", + "pnjual": "penjual", + "yh": "ya", + "dung": "dong", + "latian": "latihan", + "aaaaaaakkk": "ah", + "mauuuu": "mau", + "soga": "moga", + "tteh": "teteh", + "sintron": "sinetron", + "astagaa": "astaga", + "ancurrr": "hancur", + "dblkang": "dibelakang", + "yayang": "sayang", + "gokillllll": "gila", + "aplks": "aplikasi", + "ska": "suka", + "ktawa": "ketawa", + "baguuus": "bagus", + "kaaan": "kan", + "ngepoin": "mengepoi", + "uuhh": "uh", + "beut": "banget", + "njirrr": "anjir", + "pengeeenn": "pengin", + "dongggg": "dong", + "hatii": "hati", + "besook": "besok", + "mauu": "mau", + "gtuu": "begitu", + "njayyy": "anjay", + "maacih": "terima kasih", + "tokayyy": "tahi", + "to": "tapi", + "mksdku": "maksudku", + "gituin": "begitukan", + "lgsung": "langsung", + "cba": "coba", + "nh": "nih", + "ilang\"": "hilang-hilang", + "giniin": "beginikan", + "ksini": "kesini", + "kubarikan": "kuberikan", + "ayank": "sayang", + "anyeng": "anjing", + "yankkk": "sayang", + "cukurrr": "cukur", + "gilaaaaaa": "gila", + "yhaaa": "ya", + "qaqaaaa": "kakak", + "sayangkuuuuu": "sayangku", + "kakakuuu": "kakakku", + "banx": "bang", + "edannn": "edan", + "bape": "bawa perasaan", + "artis\"": "artis-artis", + "ngelahirin": "melahirkan", + "paksain": "memaksa", + "biasaa": "biasa", + "hub": "hubungan", + "hubunggi": "hubungi", + "loooh": "loh", + "telimakacih": "terima kasih", + "nerawang": "menerawang", + "cantiiiik": "cantik", + "didapet": "didapat", + "tanya2": "tanya-tanya", + "pede": "percaya diri", + "kuningggg": "kuning", + "nyolot": "menyolot", + "elahh": "alah", + "pnting": "penting", + "bela2in": "membela-bela", + "marah2an": "marah-marahan", + "wlaupun": "walaupun", + "syantiiiiikkkkkkkkkkkk": "cantik", + "bwa\"": "bawa-bawa", + "agma": "agama", + "hijabb": "hijab", + "kerrennn": "keren", + "ngetik": "mengetik", + "ntah": "entah", + "pan": "kapan", + "mateeeek": "mati", + "geetoo": "begitu", + "sewain": "menyewa", + "kokkk": "kok", + "muluuu": "melulu", + "yaaang": "yang", + "iyaa": "iya", + "gituu": "begitu", + "yappp": "ya", + "uwahhhhh": "wah", + "bnr2": "benar-benar", + "psgn": "pasangan", + "ujg": "ujung", + "rmbut": "rambut", + "smp": "sampai", + "kki": "kaki", + "hayukk": "hayo", + "ntab": "mantab", + "atuhhh": "atuh", + "yukss": "yuk", + "lihaatttt": "lihat", + "niiiihh": "nih", + "liattt": "lihat", + "cuyyy": "cuy", + "gapernah": "enggak pernah", + "liatiii": "lihat", + "jombsss": "jomblo", + "emesshhh": "gemas", + "gemeessss": "gemas", + "poollll": "pol", + "gemesyak": "gemas ya", + "bngttt": "banget", + "ngegmesin": "menggemaskan", + "hndak": "hendak", + "hndk": "hendak", + "iiih": "ih", + "lutunaa": "lucunya", + "abiz": "habis", + "gemes": "gemas", + "ngareppppp": "mengharap", + "aamiinin": "amin", + "imuutt": "imut", + "abizzz": "habis", + "tay": "tahi", + "toketnya": "teteknya", + "tumpe2": "tumpah-tumpah", + "lenje": "lenjeh", + "tuuuuh": "tuh", + "cantinya": "cantiknya", + "macam2": "macam-macam", + "mbak\"": "mbak-mbak", + "ngatain": "mengatai", + "mbaknyaahh": "mbaknya", + "pyudara": "payudara", + "ditutupin": "ditutupi", + "bgitu": "begitu", + "ngomongnya": "mengomongnya", + "sich": "sih", + "cantiik2": "cantik-cantik", + "hati.a": "hatinya", + "mk.a": "makanya", + "mlt.a": "mulutnya", + "nyerocooooos": "mencerocos", + "kyk.a": "kayaknya", + "sklh": "sekolah", + "pljrn": "pelajaran", + "srng": "sering", + "ngilang": "menghilang", + "dikatain": "dikatai", + "maluuu": "malu", + "ngalahin": "mengalahkan", + "brisi": "berisi", + "pkoknya": "pokoknya", + "canti": "cantik", + "astagfirullh": "astagfirullah", + "nutupin": "menutupi", + "ori": "orisinal", + "kakakaaaaaaaaaaaaaa": "kakak", + "cantikkkkkkkk": "cantik", + "batwc": "banget", + "kamyuhhh": "kamu", + "dbwh": "dibawah", + "bukti2": "bukti-bukti", + "senenggg": "senang", + "mao": "mau", + "murhan": "murahan", + "prwtn": "perawatan", + "wjh": "wajah", + "bgettt": "banget", + "mereka2": "mereka-mereka", + "pakke": "pakai", + "dipake": "dipakai", + "ktergantungan": "ketergantungan", + "prwatn": "perawatan", + "ngebuktiin": "membuktikan", + "hslnya": "hasilnya", + "awal2": "awal-awal", + "setabil": "stabil", + "maksih": "terima kasih", + "gratiss": "gratis", + "bbrpa": "beberapa", + "gara\"": "gara-gara", + "benar\"": "benar-benar", + "ajaibb": "ajaib", + "pokonya": "pokoknya", + "setujuuu": "setuju", + "digituin": "dibegitukan", + "sekolahin": "sekolahkan", + "ajarin": "ajarkan", + "sendirii": "sendiri", + "mrnghadapi": "menghadapi", + "apalg": "apalagi", + "naka": "nakal", + "sdkt": "sedikit", + "besar2in": "besar-besarkan", + "bgmana": "bagaimana", + "s*inetr*n": "sinetron", + "mrusak": "merusak", + "mndpt": "mendapat", + "skt": "sakit", + "sebnyak": "sebanyak", + "terburu2": "terburu-buru", + "baik2": "baik-baik", + "ngumpat2": "mengumpat-ngumpat", + "nantangin": "menantang", + "ngegampar": "menggampar", + "ngelaporin": "melaporkan", + "salahin": "salahkan", + "gabisa": "tidak bisa", + "konsekwensi": "konsekuensi", + "muliany": "mulianya", + "mungki": "mungkin", + "lngsung": "langsung", + "tsb": "tersebut", + "t4": "tempat", + "bacahalah": "bacalah", + "dikerasin": "dikerasi", + "gkkan": "enggak akan", + "ngulangin": "mengulangi", + "bedaa": "beda", + "pnyaa": "punya", + "bol3h": "boleh", + "ngadu": "mengadu", + "ketauan": "ketahuan", + "dimarahin": "dimarahi", + "nyelesain": "menyelesaikan", + "troma": "trauma", + "kerasin": "kerasi", + "kamu2": "kamu-kamu", + "cewer": "jewer", + "bilanh": "bilang", + "jwban": "jawaban", + "nuduh": "menuduh", + "ngatur": "mengatur", + "rajin2": "rajin-rajin", + "bacaaaa": "baca", + "kekk": "kayak", + "jungkiir": "jungkir", + "gunaa": "guna", + "bales2in": "balas-membalas", + "orangg": "orang", + "tauuuu": "tau", + "byak": "banyak", + "kepad": "kepada", + "bantuanny": "bantuannya", + "ttinggi": "tinggi", + "perna": "pernah", + "grgr": "gara-gara", + "jari2": "jari-jari", + "pukulin": "pukuli", + "bengkak2": "bengkak-bengkak", + "komentarin": "mengomentari", + "bijak2": "bijak-bijak", + "lama2": "lama-lama", + "menurur": "menurut", + "tipe2": "tipe-tipe", + "diliat": "dilihat", + "liatlah": "lihatlah", + "teguran2": "teguran-teguran", + "gr2": "gara-gara", + "gamau": "enggak mau", + "skolah": "sekolah", + "pdahal": "padahal", + "klas": "kelas", + "tkut": "takut", + "grunya": "gurunya", + "pndah": "pindah", + "liat2": "lihat-lihat", + "masukin": "memasukkan", + "dilindungin": "dilindungi", + "kasiaannn": "kasihan", + "miring2": "miring-miring", + "tinggalin": "tinggalkan", + "kaitannha": "kaitannya", + "sud": "sudah", + "pinter": "pintar", + "rana": "ranah", + "thdp": "terhadap", + "scr": "secara", + "bhkn": "bahkan", + "justeru": "justru", + "ngatai": "mengatai", + "kln": "kalian", + "sukak": "suka", + "termuraaahhhh": "termurah", + "brkunjun": "berkunjung", + "suarranya": "suaranya", + "bner\"": "benar-benar", + "iu": "itu", + "bgussss": "bagus", + "kmaren": "kemarin", + "ambilin": "mengambil", + "biasaaaa": "biasa", + "luuuaar": "luar", + "redho": "ridho", + "rezki": "rezeki", + "kepd": "kepada", + "luaarrr": "luar", + "biasaaa": "biasa", + "jadilh": "jadilah", + "bnr": "benar", + "terhaaru": "terharu", + "orrunya": "orang tuanya", + "flm": "film", + "kalok": "kalo", + "adek2": "adek-adek", + "loncatt": "loncat", + "hampasss": "hempas", + "tehh": "teh", + "bunga2": "bunga-bunga", + "apaaan": "apaan", + "ampuuunn": "ampun", + "daaah": "dah", + "bangetttttt": "banget", + "ituh": "itu", + "cantiiikk": "cantik", + "suaranyaaaaaa": "suaranya", + "idoolaaa": "idola", + "pulaa": "pula", + "penyanyi2": "penyanyi-penyanyi", + "wiih": "wih", + "apaa": "apa", + "miriip": "mirip", + "allaahh": "allah", + "ajahhh": "saja", + "trims": "terima kasih", + "dehat": "sehat", + "salammmm": "salam", + "hari2": "hari-hari", + "sihat": "sehat", + "buruann": "buruan", + "rekomendain": "rekomendasikan", + "trussss": "terus", + "bangt": "banget", + "cepet": "cepat", + "pesen": "pesan", + "boong": "bohong", + "prawatan": "perawatan", + "mkin": "makin", + "dimao": "dimau", + "dijadiin": "dijadikan", + "silahkn": "silakan", + "kasiatnya": "khasiatnya", + "minyak\"an": "minyak-minyakan", + "suksess": "sukses", + "prodak": "produk", + "trasa": "terasa", + "minpi": "mimpi", + "bner2": "benar-benar", + "mutihin": "memutihkan", + "smster": "semester", + "kmren": "kemarin", + "ucull": "lucu", + "ngomong\"": "ngomong-ngomong", + "udehh": "sudah", + "syngnya": "sayangnya", + "dipermainin": "dipermainkan", + "kocakkk": "kocak", + "dasarrr": "dasar", + "waaaaw": "wow", + "cannnnntikkkkkkk": "cantik", + "mukax": "mukanya", + "cantikx": "cantiknya", + "fatanik": "fanatik", + "protestin": "protestan", + "kakau": "kalau", + "marh": "marah", + "emngnya": "memangnya", + "cantiknyaa": "cantiknya", + "darpada": "daripada", + "cooooooocok": "cocok", + "pkk": "pakai", + "sling": "saling", + "agama'ny": "agamanya", + "bilng": "bilang", + "prcy": "percaya", + "lurusin": "meluruskan", + "biri": "biru", + "luco": "lucu", + "ultha": "ultah", + "batikk": "batik", + "arab2": "arab-arab", + "luchu": "lucu", + "baguss": "bagus", + "sedihhh": "sedih", + "syang": "sayang", + "ngasi": "mengasih", + "naaaak": "nak", + "astafirullah": "astagfirullah", + "peragain": "peragakan", + "shayyy": "say", + "anjaayy": "anjay", + "shay": "say", + "ngilerrrr": "mengiler", + "woyyy": "woy", + "shaaaay": "say", + "hempaass": "hempas", + "sayyy": "say", + "shayy": "say", + "syantiek": "cantik", + "njay": "anjay", + "hehh": "eh", + "cantik2": "cantik-cantik", + "kam": "kamu", + "ingeet": "ingat", + "sblh": "sebelah", + "nawarin": "menawari", + "amiiiiin": "amin", + "jauhhh": "jauh", + "waaahhh": "wah", + "ajakin": "mengajak", + "mntn": "mantan", + "ade": "adek", + "luhhh": "lu", + "yooookkk": "yuk", + "jrnk": "jarang", + "jelas\"": "jelas-jelas", + "kayax": "kayak", + "dblkng": "dibelakang", + "anjirr": "anjir", + "mirippp": "mirip", + "sangattt": "sangat", + "iyap": "iya", + "mayann": "lumayan", + "autuhh": "tau tuh", + "atutt": "takut", + "ketara": "tertara", + "giniii": "begini", + "mbaaass": "mbak", + "bund": "bunda", + "bangeeettt": "banget", + "almh": "almarhumah", + "temenkuuh": "temanku", + "suami'y": "suaminya", + "bngeeeetttt": "banget", + "entr": "entar", + "kyax": "kayak", + "cuantikkk": "cantik", + "mksdnya": "maksudnya", + "kaaaaaaaaaakkkkkkkkkkk": "kak", + "kucing'y": "kucingnya", + "nama'in": "namai", + "panggilan'ya": "panggilannya", + "panggilan'y": "panggilannya", + "z": "saja", + "kalw": "kalau", + "tips2": "tips-tips", + "alu": "aku", + "ngerrrriiii": "ngeri", + "ngriii": "mengiri", + "amatt": "amat", + "smpai": "sampai", + "kox": "kok", + "tmbah": "tambah", + "cntikk": "cantik", + "pkee": "pakai", + "smkin": "semakin", + "amiiiiinnn": "amin", + "bgmna": "bagaimana", + "ssorg": "seseorang", + "hdpn": "hadapan", + "foto2": "foto-foto", + "mnambh": "menambah", + "ktaatan": "ketaatan", + "stju": "setuju", + "cantikkkkkk": "cantik", + "dilepas2": "dilepas-lepas", + "tehhh": "teh", + "istiqomahhh": "istikamah", + "tehhhh": "teh", + "prodaknya": "produknya", + "imutt": "imut", + "amu": "kamu", + "nahh": "nah", + "mauuuuuuu": "mau", + "jilbb": "jilbab", + "tmn": "teman", + "ndut": "gendut", + "ngecek": "mengecek", + "psti": "pasti", + "penasarn": "penasaran", + "cuyy": "cuy", + "astauviruloh": "astagfirullah", + "paaak": "pak", + "diajarin": "diajari", + "gawl": "gaul", + "kecr": "kece", + "iih": "ih", + "laaaah": "lah", + "aned": "banget", + "gingsungny": "gingsulnya", + "blh": "boleh", + "cariin": "carikan", + "kenceng": "kencang", + "cuanti": "cantik", + "mbanya": "mbaknya", + "knlan": "kenalan", + "mbaakk": "mbak", + "mrka": "mereka", + "nyusahhin": "menyusahkan", + "byaran": "bayaran", + "wlawpun": "walaupun", + "sedangkn": "sedangkan", + "tnpa": "tanpa", + "bnggain": "banggakan", + "nyusahin": "menyusahkan", + "malu2in": "malu-malui", + "dibanggain": "dibanggakan", + "wlau": "walau", + "komenan": "komentar", + "bangga\"in": "membangga-banggakan", + "krya": "karya", + "bilang\"": "bilang-bilang", + "tinggall": "tinggal", + "dmna": "dimana", + "dripda": "daripada", + "bangga2in": "membangga-banggakan", + "dibnggain": "dibanggakan", + "karya2": "karya-karya", + "drimana": "darimana", + "ngepake'": "memakai", + "gksalah": "enggak salah", + "nuntut": "menuntut", + "ngebiarin": "membiarkan", + "bhas": "bahas", + "lagu'y": "lagunya", + "jadiin": "jadikan", + "diem": "diam", + "nyampah": "menyampah", + "mba\"": "mbak-mbak", + "bukan'a": "bukannya", + "tappi": "tapi", + "kalli": "kali", + "pacara": "pacaran", + "ngerebut\"": "merebut-rebut", + "lluu": "lu", + "mbaa": "mbak", + "ngerussak": "merusak", + "bela\"in": "membela-bela", + "bellain": "membela", + "kallo": "kalo", + "coment": "komentar", + "dewassa": "dewasa", + "comnet": "komen", + "pikir\"": "pikir-pikir", + "kaya'a": "kayanya", + "kuyyy": "yuk", + "kitaa": "kita", + "sgtu'y": "sebegitunya", + "ngebelain": "membela", + "tpii": "tapi", + "karya\"": "karya-karya", + "do": "di", + "cali": "caci", + "penghrgaan": "penghargaan", + "ajjh": "saja", + "lbihh": "lebih", + "baikk": "baik", + "drii": "dari", + "artist\"": "artis-artis", + "maki\"": "maki-maki", + "sendirinnya": "sendirinya", + "stidaknya": "setidaknya", + "may": "mau", + "trbaikk": "terbaik", + "punn": "pun", + "mengikutii": "mengikuti", + "atassan": "atasan", + "klauu": "kalau", + "jngnn": "jangan", + "so": "sok", + "sendirinyya": "sendirinya", + "frestasiii": "prestasi", + "mst": "mesti", + "add": "ada", + "see": "sih", + "sibukx": "sibuknya", + "in": "ini", + "katanyaa": "katanya", + "lawaaaas": "lawas", + "kocaaaak": "kocak", + "syng": "sayang", + "adenya": "adeknya", + "pkoknyaa": "pokoknya", + "dsana": "disana", + "dua2nya": "dua-duanya", + "sichhhhhhhhhh": "sih", + "nampak": "menampak", + "tetehh": "teteh", + "wooowww": "wow", + "cobain": "mencoba", + "muga": "moga", + "kenalin": "memperkenalkan", + "janka": "jangka", + "wawwwww": "wow", + "waaaww": "wow", + "waaawwww": "wow", + "mantapp": "mantap", + "cobaaa": "coba", + "kageet": "kaget", + "bangettttttt": "banget", + "oplas": "operasi plastik", + "nambahh": "bertambah", + "paripurnahh": "paripurna", + "wanitah": "wanita", + "indonesiaah": "indonesia", + "cumn": "cuman", + "gegra": "gegara", + "brita": "berita", + "fotbar": "foto bareng", + "wajarrr": "wajar", + "sngt": "sangat", + "wk": "waktu", + "bingiittt": "banget", + "nampar": "menampar", + "tukan": "tukang", + "jatoh": "jatuh", + "muka\"": "muka-muka", + "cantik2nya": "cantik-cantiknya", + "sdgkn": "sedangkan", + "apa\"": "apa-apa", + "mancungin": "memancungkan", + "idung": "hidung", + "kusem": "kusam", + "drpada": "daripada", + "kmar": "kamar", + "diemm": "diam", + "tnggu": "tunggu", + "ketawain": "mentertawakan", + "yaelahhh": "alah", + "sikap\"": "sikap-sikap", + "bales": "balas", + "ampuun": "ampun", + "ngejatuhin": "manjatuhkan", + "ngelakuin": "melakukan", + "gedeg": "gedek", + "muka2": "muka-muka", + "eey": "eh", + "kslahan": "kesalahan", + "perhatiam": "perhatian", + "dibiarin": "dibiarkan", + "sbnrny": "sebenarnya", + "trjd": "terjadi", + "mkir": "memikir", + "hny": "hanya", + "apalai": "apalagi", + "bkal": "bakal", + "saat2": "saat-saat", + "nyadar": "menyadar", + "bka": "buka", + "berani2": "berani-berani", + "lucuuuuu": "lucu", + "pipi'y": "pipinya", + "gemmeeezzz": "gemas", + "sukam": "suka", + "menghilangkn": "menghilangkan", + "mnjdkan": "menjadikan", + "mudah2n": "mudah-mudahan", + "stlh": "setelah", + "soalx": "soalnya", + "keche": "kece", + "badaii": "badai", + "btuh": "butuh", + "ktanya": "katanya", + "bebrp": "beberapa", + "sblum": "sebelum", + "santab": "santap", + "salehah": "salihah", + "mikirin": "memikirkan", + "subhannallah": "subhanallah", + "konsul": "konsultasi", + "pengalam": "pengalaman", + "poko": "pokok", + "trun": "turun", + "kuninggg": "kuning", + "pinkkkk": "pink", + "warnany": "warnanya", + "seger": "segar", + "senangat": "semangat", + "msknya": "masaknya", + "kerennnn": "keren", + "ciptain": "menciptakan", + "bagussss": "bagus", + "sayangkuh": "sayangku", + "gans": "ganteng", + "kren": "keren", + "babget": "banget", + "dihh": "dih", + "jadu": "jadi", + "ksyngn": "kesayangan", + "haru": "harus", + "ngingetin": "mengingatkan", + "narik": "menarik", + "orang\"": "orang-orang", + "gokilll": "gila", + "astagaaaa": "astaga", + "hancuuuuuy": "hancur", + "mattihhh": "mati", + "beudd": "banget", + "termiribkan": "termiripkan", + "dngerin": "mendengarkan", + "brrti": "berarti", + "wenakk": "enak", + "nganteng": "ganteng", + "abanggg": "abang", + "seksii": "seksi", + "gtuh": "begitu", + "jadii": "jadi", + "emeezzz": "gemas", + "amohhh": "kamu", + "maau": "mau", + "eyke": "aku", + "gilakkkkk": "gila", + "awk": "awak", + "duhhhhh": "duh", + "wowww": "wow", + "liaatt": "lihat", + "iniiiii": "ini", + "aloh": "allah", + "ngettt": "banget", + "maso": "masa", + "brubah": "berubah", + "pelastik": "plastik", + "keak": "kayak", + "dengerin": "mendengarkan", + "beud": "banget", + "pahamin": "pahami", + "takuuuuut": "takut", + "ahahahanjirrr": "anjir", + "jogetin": "berjoget", + "shaay": "say", + "boker": "berak", + "pacarnyaa": "pacarnya", + "siapaa": "siapa", + "matii": "mati", + "ketawaa": "ketawa", + "wanjiir": "anjir", + "bw": "bawa", + "minta": "meminta", + "bnran": "benaran", + "sgla": "segala", + "uitnya": "duitnya", + "beliiin": "belikan", + "yaaah": "ya", + "sekaeang": "sekarang", + "tgk": "tengok", + "tanyain": "tanyakan", + "jatuhnyaa": "jatuhnya", + "banyakin": "membanyaki", + "tipu2nya": "tipu-tipunya", + "doaang": "doang", + "gituan": "begituan", + "kaj": "kak", + "wahhhhhh": "wah", + "iiiiiiiii": "ih", + "kaliiiiiii": "kali", + "melebihin": "melebihi", + "gakpapa": "enggak apa-apa", + "kalogak": "kalo enggak", + "sm2": "sama-sama", + "ndukung": "mendukung", + "kaaaaa": "kak", + "aamiinnn": "amin", + "skluarga": "sekeluarga", + "suaminga": "suaminya", + "berhijap": "berhijab", + "smgaa": "semoga", + "istiqamah": "istikamah", + "cuaaantikk": "cantik", + "muraaaaah": "murah", + "baraang": "barang", + "aamiiin": "amin", + "bets": "banget", + "alhamdulillaaaaah": "alhamdulillah", + "sembuhin": "menyembuhkan", + "pengrusak": "perusak", + "ngoceh": "mengoceh", + "aduhhh": "aduh", + "hpus": "hapus", + "pjang": "pajang", + "bdampingan": "berdampingan", + "kaga": "kagak", + "uih": "uh", + "serem\"": "seram-seram", + "happi\"": "hepi-hepi", + "bgtuu": "begitu", + "mff": "maaf", + "ktwa": "ketawa", + "trlalu": "terlalu", + "jjur": "jujur", + "ngmg": "mengomong", + "semogaa": "semoga", + "kepelesett": "terpeleset", + "geeelaakk": "gila", + "bolang": "bocah petualang", + "pkek": "pakai", + "lagihh": "lagi", + "hedeuuhh": "aduh", + "stress": "stres", + "saltummmm": "salah kostum", + "mboizzz": "bois", + "suda": "sudah", + "kayaq": "kayak", + "ema": "emak", + "dirumahny": "dirumahnya", + "yuh": "tuh", + "rmbt": "rambut", + "apus": "hapus", + "siapayahh": "siapa ya", + "kostom": "kostum", + "gnii": "begini", + "atuu": "satu", + "pipinyaaa": "pipinya", + "bungkuuuuuuuuusss": "bungkus", + "iiiihhhhhj": "ih", + "emeessss": "gemas", + "pipine": "pipinya", + "glembeeeer": "gelambir", + "lucunaaa": "lucunya", + "cubanget": "lucu banget", + "pengenn": "pengin", + "unyuuk": "unyu", + "dedek": "adek", + "lucunyah": "lucunya", + "mantaaap": "mantap", + "keduaa": "kedua", + "trsrh": "terserah", + "ksih": "kasih", + "seterong": "setrong", + "teyuss": "terus", + "dimasukin": "dimasukkan", + "jelekin": "menjelekkan", + "jahatin": "menjahati", + "tai": "tahi", + "gtw": "enggak tau", + "qo": "kok", + "nulis": "menulis", + "nyebutin": "menyebutkan", + "ngertiin": "mengerti", + "rasaiin": "rasai", + "gakusah": "enggak usah", + "disni": "disini", + "sabarrr": "sabar", + "semangaatttt": "semangat", + "nyalahin": "menyalahkan", + "bedainnya": "membedakannya", + "keknya": "kayaknya", + "krus": "kurus", + "elaaaahhhh": "alah", + "cogan": "cowok ganteng", + "tap8": "tapi", + "pandanginya": "memandanginya", + "kenaliiin": "kenalkan", + "banyak2": "banyak-banyak", + "yo": "ya", + "suko": "suka", + "niih": "nih", + "sihhhh": "sih", + "sebaik\"": "sebaik-baik", + "segtu": "sebegitu", + "ngebenci": "membenci", + "bakar2in": "bakar-bakari", + "padhe": "pakde", + "yya": "ya", + "atw": "atau", + "dosa2mu": "dosa-dosamu", + "maafin": "memaafi", + "gile": "gila", + "ujung2": "ujung-ujung", + "dalem2": "dalam-dalam", + "kgak": "kagak", + "bahas2": "bahas-bahas", + "ud": "sudah", + "woles": "selow", + "hianatin": "khianati", + "halangin": "halangi", + "iyuh": "ih", + "iyuuuhhhh": "ih", + "gaje": "enggak jelas", + "gabakal": "enggak bakal", + "adapin": "menghadapi", + "ngehakimi": "menghakimi", + "ngejuangin": "memperjuangkan", + "dipojokin": "dipojokkan", + "bacotin": "bacoti", + "berati": "berarti", + "kebangetak": "kebangetan", + "tangkep": "tangkap", + "bt": "buat", + "sadis2": "sadis-sadis", + "maksir": "menaksir", + "hebatt": "hebat", + "pndang": "pandang", + "pmbelajaran": "pembelajaran", + "ngejelek": "menjelekkan", + "maslah": "masalah", + "mmperbaiki": "memperbaiki", + "nahan": "menahan", + "kafeh": "kafe", + "remang2": "remang-remang", + "mlh": "malah", + "tinggl": "tinggal", + "dirmh": "dirumah", + "iyuuhhh": "ih", + "ktika": "ketika", + "dipuja2": "dipuja-puja", + "ngrasain": "merasai", + "ityl": "itil", + "hbs": "habis", + "marah2": "marah-marah", + "wenak": "enak", + "ngarang": "mengarang", + "kelas'a": "kelasnya", + "hadeeehhh": "aduh", + "kecil\"": "kecil-kecil", + "gtu\"an": "begitu-gituan", + "gmanapun": "bagaimanapun", + "kr": "kira", + "nenek2": "nenek-nenek", + "taux": "taunya", + "urat2nya": "urat-uratnya", + "ketarik": "tertarik", + "ngorbanin": "mengorbankan", + "eksprsi": "ekspresi", + "gitulah": "begitulah", + "foto\"": "foto-foto", + "mamas": "mas", + "bandingin": "bandingkan", + "tua2": "tua-tua", + "terus2an": "terus-terusan", + "waduhh": "waduh", + "bawa\"": "bawa-bawa", + "badingkan": "membandingkan", + "ngurus": "mengurus", + "lanjutin": "lanjutkan", + "ributin": "meributkan", + "brantem": "berantem", + "belain": "membela", + "lipetan": "lipatan", + "nenek\"": "nenek-nenek", + "bneraan": "benaran", + "kageeet": "kaget", + "sangaaattt": "sangat", + "rmbuut": "rambut", + "nt": "nanti", + "walik": "balik", + "oma2": "oma-oma", + "kellen": "kalian", + "nyerocos": "mencerocos", + "ibuk2": "ibu-ibu", + "ngomenin": "mengomentari", + "iseng2": "iseng-iseng", + "niyyyyhhhh": "nih", + "sexi2": "seksi-seksi", + "blik": "balik", + "kenape": "kenapa", + "ladenin": "meladeni", + "skit": "sakit", + "hat": "hati", + "jilbabnye": "jilbabnya", + "ledkin": "meledeki", + "digabtikan": "digantikan", + "jng": "jangan", + "ngumung": "mengomong", + "ilang-in": "hilangkan", + "prinadi": "pribadi", + "luh": "lu", + "yep": "ya", + "helooooo": "halo", + "loe": "lo", + "palingann": "palingan", + "berjinah": "berzina", + "munaafikk": "munafik", + "urusn": "urusan", + "saudra": "saudara", + "trganggu": "terganggu", + "bhijab": "berhijab", + "lepasin": "melepas", + "smentara": "sementara", + "waduhhh": "waduh", + "ursan": "urusan", + "sudab": "sudah", + "dwsa": "dewasa", + "rpt2": "repot-repot", + "ngaji": "mengaji", + "nmr": "nomor", + "romantiz": "romantis", + "murmer": "murah meriah", + "pensaran": "penasaran", + "baru2": "baru-baru", + "yuukk": "yuk", + "kitaaa": "kita", + "lainnyaa": "lainnya", + "perbulannn": "perbulan", + "penyebab2": "penyebab-penyebab", + "nyupir": "menyopir", + "steess": "stres", + "kwatir": "kawatir", + "tengah2": "tengah-tengah", + "duetin": "duet", + "mudaa": "muda", + "sebenernya": "sebenarnya", + "silsturahmi": "silaturahmi", + "dijkt": "dijakarta", + "cums": "cuma", + "undangan2": "undangan-undangan", + "past": "pasti", + "ayukk": "yuk", + "ngucapin": "mengucapkan", + "filmny": "filmnya", + "ntn": "menonton", + "nobar": "menonton bareng", + "kemariin": "kemarin", + "knpp": "kenapa", + "lggi": "lagi", + "gaprgi": "enggak pergi", + "aaahh": "ah", + "pa2": "apa-apa", + "dtunggu": "ditunggu", + "masa2": "masa-masa", + "kepikir": "terpikir", + "seruuu": "seru", + "emeshh": "gemas", + "kaya": "seperti", + "astagaaaaaa": "astaga", + "pacarn": "pacaran", + "ktg": "kita", + "pacaranya": "pacarannya", + "mbaak": "mbak", + "ngalah": "mengalah", + "de": "deh", + "pacarane": "pacarannya", + "rusakkk": "rusak", + "yaaampunnn": "ya ampun", + "tuwek": "tua", + "mukae": "mukanya", + "ultahe": "ultahnya", + "kliatanya": "kelihatannya", + "pcaranya": "pacarannya", + "bermaksut": "bermaksud", + "tampialannya": "tampilannya", + "nikaah": "nikah", + "anak\"": "anak-anak", + "pankapan": "kapan-kapan", + "ngumbar": "mengumbar", + "dhpusin": "dihapuskan", + "istiqomah": "istikamah", + "mnutup": "menutup", + "bngatt": "banget", + "dii": "di", + "ooh": "oh", + "hapusin": "hapuskan", + "lohhh": "loh", + "ngeuh": "engah", + "sneng": "senang", + "mutusin": "memutuskan", + "ngapus": "menghapus", + "awl": "awal", + "hrus": "harus", + "bngkit": "bangkit", + "mncoba": "mencoba", + "wahhh": "wah", + "mutuain": "memutuskan", + "smw": "semua", + "geuliiiiiiis": "geulis", + "euyyyyy": "euy", + "uuh": "uh", + "ngirim": "mengirim", + "ngidam": "mengidam", + "waah": "wah", + "sampis": "sampah habis", + "maapin": "maafi", + "ngebala": "bala", + "mampuussss": "mampus", + "diihhhh": "dih", + "hrusnya": "harusnya", + "sabal": "sabar", + "udahlah": "sudahlah", + "kauuuu": "kau", + "sabaro": "sabar", + "eehhh": "eh", + "iiii": "ih", + "kreative": "kreatif", + "bangrt": "banget", + "azekkkk": "asik", + "kuaaatt": "kuat", + "bhet": "banget", + "inggrisny": "inggrisnya", + "dsna": "disana", + "iyaap": "iya", + "mksih": "terima kasih", + "nga": "tidak", + "canyk": "cantik", + "prodac": "produk", + "nyesell": "menyesal", + "ciyusss": "serius", + "cyin": "cinta", + "bangetttttttt": "banget", + "sexi": "seksi", + "bundaaaa": "bunda", + "bangget": "banget", + "bettt": "banget", + "cantiiillx": "cantik", + "brapa": "berapa", + "alaami": "alami", + "sabaarrr": "sabar", + "cantiiiikk": "cantik", + "bangeeeeetttt": "banget", + "cannntikk": "cantik", + "banggett": "banget", + "thnan": "tahunan", + "cuantik": "cantik", + "segerr": "segar", + "wouww": "wow", + "cantiiikkk": "cantik", + "pen": "pengin", + "catokin": "catok", + "ikut2an": "ikut-ikutan", + "comment2nya": "komentar-komentarnya", + "teman2": "teman-teman", + "besar2an": "besar-besaran", + "knsul": "konsultasi", + "brhasil": "berhasil", + "hilangin": "menghilangkan", + "kontak2": "kontak-kontak", + "dptnya": "dapatnya", + "applikasi": "aplikasi", + "kereeeeen": "keren", + "bngeet": "banget", + "gemesinnnnn": "menggemaskan", + "ongkiir": "ongkir", + "ajaibbbbb": "ajaib", + "saayyyyy": "say", + "lohhhh": "loh", + "ganteung": "ganteng", + "bhgia": "bahagia", + "iiiiiiiiii": "ih", + "lucuukk": "lucu", + "keciil": "kecil", + "yakinn": "yakin", + "pentass": "pentas", + "ngarep": "mengharap", + "lucu2": "lucu-lucu", + "testinya": "testimoninya", + "mengirusi": "mengurusi", + "khdpn": "kehidupan", + "ngelawan": "melawan", + "betuuuuulllllllllllllllll": "betul", + "bisax": "bisanya", + "klmbutan": "kelembutan", + "seburuk2": "seburuk-buruk", + "diingetin": "diingatkan", + "ngeriiiiii": "ngeri", + "nich": "nih", + "akhlah": "akhlak", + "elo2": "lo-lo", + "mandiin": "memandikan", + "nguburin": "menguburkan", + "ngebedain": "membedakan", + "urusanny": "urusannya", + "tuhanny": "tuhannya", + "brtngkar": "bertengkar", + "pndapat": "pendapat", + "looh": "loh", + "sharusnya": "seharusnya", + "mngkannya": "mangkanya", + "nyia2in": "menyia-nyiakan", + "brarti": "berarti", + "mndpat": "mendapat", + "kpuasan": "kepuasan", + "sgampang": "segampang", + "mlepaskan": "melepaskan", + "trll": "terlalu", + "sndr": "sendiri", + "sellow": "selow", + "sbbrnya": "sebenarnya", + "kasar2": "kasar-kasar", + "semangattt": "semangat", + "testi": "testimoni", + "banyaak": "banyak", + "bujukk": "bujuk", + "d.apa": "diapa", + "apain": "apai", + "dsar": "dasar", + "benetaran": "benaran", + "bneran": "benaran", + "si": "sih", + "kraya": "karya", + "brg": "bareng", + "ngebantu": "membantu", + "janjutin": "lanjutkan", + "kepppoin": "kepoi", + "recommendasi": "rekomendasi", + "btulan": "betulan", + "kha": "kah", + "s": "si", + "tutupin": "menutupi", + "wik": "wig", + "ithu": "itu", + "botakin": "botaki", + "botakkkk": "botak", + "bnrn": "benaran", + "week": "wig", + "gawul": "gaul", + "modes": "modis", + "brayyy": "bray", + "kcantikan": "kecantikan", + "bronis": "brondong manis", + "gapunya": "enggak punya", + "bkln": "bakalan", + "kecantikanga": "kecantikannya", + "hiii": "hai", + "piiiilih": "pilih", + "deeh": "deh", + "zama": "zaman", + "gnti": "ganti", + "nmnya": "namanya", + "tapo": "tapi", + "gkk": "tidak", + "petinggi2": "petinggi-petinggi", + "wota2": "wota-wota", + "indefendet": "independen", + "tempat2": "tempat-tempat", + "taik": "tahi", + "yuuuukkk": "yuk", + "dituutup": "ditutup", + "ngayal": "mengkhayal", + "smvk": "sempak", + "knyataan": "kenyataan", + "njrittt": "anjir", + "selmt": "selamat", + "taz": "atas", + "klhran": "kelahiran", + "pertma": "pertama", + "kelahuran": "kelahiran", + "pertamany": "pertamanya", + "mamax": "mamanya", + "ankx": "anaknya", + "cantip": "cantik", + "klahiran": "kelahiran", + "slmat": "selamat", + "mmbanggakan": "membanggakan", + "ortuny": "orang tuanya", + "selmat": "selamat", + "bnda": "bunda", + "nyah": "nya", + "prtamax": "pertamanya", + "moqa": "moga", + "yq": "yang", + "brbakti": "berbakti", + "gewlaaaaa": "gila", + "basodaraa": "bersaudara", + "hargs": "harga", + "perhatiin": "perhatikan", + "abal2": "abal-abal", + "bantuim": "bantu", + "adl": "adalah", + "pelwak": "pelawak", + "suces": "sukses", + "sucses": "sukses", + "garemin": "garami", + "gapake": "enggak pakai", + "ngiringan": "mengiringi", + "karir.a": "karirnya", + "aamin": "amin", + "teruz": "terus", + "kemarn": "kemarin", + "recomendasinya": "rekomendasinya", + "promoo": "promo", + "ongkirr": "ongkir", + "mskasih": "terima kasih", + "wowwwww": "wow", + "seeeemua": "semua", + "batesin": "batasi", + "bijin": "bikin", + "ngeidealin": "mengidealkan", + "bhn": "bahan", + "gasalah": "enggak salah", + "brsma": "bersama", + "le": "boleh", + "ugha": "juga", + "semuaaa": "semua", + "heeee": "hai", + "rame2": "rame-rame", + "yam": "ya", + "kartunin": "kartun", + "kyaknya": "kayaknya", + "abez": "habis", + "baaaaaang": "bang", + "ganteeeeng": "ganteng", + "entahhh": "entah", + "gimanaaaa": "bagaimana", + "duuhh": "duh", + "gantengnyaaaa": "gantengnya", + "kpn2": "kapan-kapan", + "pans": "fan", + "kuliyh": "kuliah", + "gantengkuuuuuu": "gantengku", + "atassss": "atas", + "orgny": "orangnya", + "owalaah": "walah", + "aslinyo": "aslinya", + "alissnyaa": "alisnya", + "masaaaa": "masa", + "komuk": "muka", + "kalahnye": "kalahnya", + "iyaaaa": "iya", + "fakultass": "fakultas", + "smrg": "semarang", + "unyu2": "unyu-unyu", + "koq": "kok", + "yyaaa": "ya", + "insonniaaa": "insomnia", + "adekx": "adeknya", + "azaab": "azab", + "gksi": "enggak sih", + "greged": "greget", + "alamiii": "alami", + "ktemu": "ketemu", + "ditayangin": "ditayangkan", + "ms\"": "masa-masa", + "meranin": "memerankan", + "nontonny": "menontonnya", + "rekomendasiiin": "merekomendasikan", + "sista2": "sista-sista", + "agan2": "agan-agan", + "ngg": "tidak", + "nanget": "banget", + "ngeditnya": "mengeditnya", + "himana": "bagaimana", + "kekgini": "kayak begini", + "subhanaallahh": "subhanallah", + "sangat2": "sangat-sangat", + "terharuuuu": "terharu", + "kata\"": "kata-kata", + "subhanallahhh": "subhanallah", + "tlsn": "tulisan", + "nagiss": "menangis", + "aneut": "banget", + "trrharu": "terharu", + "subhnallah": "subhanallah", + "allahh": "allah", + "suamiii": "suami", + "dcintai": "dicintai", + "klrga": "keluarga", + "liat'y": "lihatnya", + "lggeng": "langgeng", + "romantisny": "romatisnya", + "duhhh": "duh", + "pcar": "pacar", + "kakanga": "kakaknya", + "slw": "selow", + "cpa": "siapa", + "akuu": "aku", + "aelah": "alah", + "orabg": "orang", + "mukanyaa": "mukanya", + "kakakny": "kakaknya", + "woooyy": "woy", + "ngelucu": "melucu", + "bukak": "buka", + "mato": "mata", + "kareno": "karena", + "cino": "cina", + "munafikk": "munafik", + "meker": "mikir", + "indoneaia": "indonesia", + "benarin": "membenarkan", + "jaadii": "jadi", + "apo": "apa", + "nyo": "nya", + "miara": "memiara", + "jalanani": "menjalani", + "dalil2": "dalil-dalil", + "sunah2": "sunah-sunah", + "sdah": "sudah", + "benda2": "benda-benda", + "bilang2": "bilang-bilang", + "emangnya": "memangnya", + "rumpiin": "rumpi", + "hotak": "botak", + "ikut2": "ikut-ikut", + "jawabanny": "jawabannya", + "ajj": "saja", + "mikir2": "memikir-mikir", + "melihara": "memelihara", + "oooh": "oh", + "hidupx": "hidupnya", + "ngefans": "fan", + "lsg": "langsung", + "uztad": "ustad", + "anjingnyaa": "anjingnya", + "pegang2": "pegang-pegang", + "ilmh": "ilmu", + "menasehati": "menasihati", + "suka2": "suka-suka", + "kelus": "kali", + "hubngan": "hubungan", + "koclak": "kocak", + "bju": "baju", + "cinggirnyaaaa": "cinggirnya", + "yahhhh": "ya", + "miripppp": "mirip", + "sedikitlaahh": "sedikitlah", + "miriplahhhhhh": "miriplah", + "syediih": "sedih", + "mirp": "mirip", + "baju.a": "bajunya", + "wiiiihhhhhhhhh": "wih", + "gantheng": "ganteng", + "synk": "sayang", + "ngfans": "fan", + "miriiippp": "mirip", + "idungnya": "hidungnya", + "wooow": "wow", + "panglingg": "pangling", + "artisnyaa": "artisnya", + "dtggu": "ditunggu", + "trnyata": "ternyata", + "bgtttttttt": "banget", + "rapih": "rapi", + "buuun": "bun", + "iyaappps": "iya", + "sukse": "sukses", + "nih2": "nih-nih", + "baruu": "baru", + "kecptan": "kecepatan", + "rmbutnya": "rambutnya", + "giti": "begitu", + "penganten": "pengantin", + "wrn": "warna", + "ne": "ini", + "cantikkkkk": "cantik", + "fans": "fan", + "loksyut": "lokasi syuting", + "amfun": "ampun", + "k'": "kak", + "asuuu": "asu", + "kuu": "ku", + "dsini": "disini", + "dibls": "dibalas", + "ceklist": "ceklis", + "terusss": "terus", + "tlvn": "telepon", + "trkrim": "terkirim", + "kelaluan": "kelakuan", + "guanteng": "ganteng", + "cucok": "cocok", + "c": "sih", + "gueee": "gue", + "semangatt": "semangat", + "haduh": "aduh", + "mlz": "malas", + "emak2": "emak-emak", + "tante2": "tante-tante", + "kepoh": "kepo", + "jalanin": "menjalani", + "repong": "repot", + "ngrusin": "mengurusi", + "iy": "iya", + "dnk": "dong", + "senyumna": "senyumnya", + "ngejelek2in": "menjelek-jelekkan", + "hotx": "hotnya", + "cwnya": "ceweknya", + "mbaaahhh": "mbah", + "pasangn": "pasangan", + "sttus": "status", + "aama": "sama", + "hadeuuhhhh": "aduh", + "lebar2": "lebar-lebar", + "sstu": "sesuatu", + "serasiii": "serasi", + "cocokx": "cocoknya", + "emakx": "emaknya", + "iniiiiiiiiiiiiiii": "ini", + "cocookk": "cocok", + "kll": "kalo", + "arghhh": "ah", + "ungkapin": "ungkapkan", + "merrid": "merit", + "neh": "nih", + "cocokkkkk": "cocok", + "yaolohh": "ya allah", + "aee": "saja", + "naggung": "menanggung", + "battt": "banget", + "am": "sama", + "tah": "teh", + "luthuna": "lucunya", + "astagaaa": "astaga", + "suaraynyaa": "suaranya", + "duanyaa": "duanya", + "anjayyyy": "anjay", + "coeggg": "coeg", + "ancur": "hancur", + "koplak": "kocak", + "uaaasssiiiik": "asik", + "boss": "bos", + "nyubit": "mencubit", + "dede": "adek", + "gemez": "gemas", + "tium": "cium", + "gemeeezzzz": "gemas", + "ppa": "papa", + "papahhhnya": "papahnya", + "duuuhh": "duh", + "ginting": "ganteng", + "bingiiittzz": "banget", + "iiihh": "ih", + "dedeknya": "adeknya", + "hadeeuh": "aduh", + "geneeee": "begini", + "yooow": "ya", + "gemezz": "gemas", + "gayax": "gayanya", + "infpyunnnn": "ampun", + "paraaah": "parah", + "papiii": "papi", + "kamunyaa": "kamunya", + "ngambek": "mengambek", + "haduuuh": "aduh", + "kapaan": "kapan", + "nginjek": "menginjak", + "putihnyaa": "putihnya", + "ituuuu": "itu", + "omaaaaaaakk": "oma", + "loncattt": "loncat", + "didatengin": "didatangi", + "abg": "abang", + "kmariiin": "kemarin", + "ayoklaa": "ayolah", + "jelasssss": "jelas", + "kaliiin": "kalian", + "ujunggg": "ujung", + "kaakaaaa": "kakak", + "srruu": "seru", + "kannn": "kan", + "jmpaaaa": "jumpa", + "yukkk": "yuk", + "haii": "hai", + "syukaaakkk": "suka", + "dengeeriin": "dengarkan", + "yakaliiiii": "yakali", + "atuhh": "atuh", + "ngertii": "mengerti", + "apalgi": "apalagi", + "akuh": "aku", + "mesrany": "mesranya", + "jendes": "janda", + "ngggk": "tidak", + "coccokk": "cocok", + "msa": "masa", + "jendesss": "janda", + "klepek2": "klepek-klepek", + "bngtttt": "banget", + "ponakn": "ponakan", + "tantee": "tante", + "gadiss": "gadis", + "tpy": "tapi", + "jnda": "janda", + "bngy": "banget", + "bujngn": "bujangan", + "ngp": "mengapa", + "padetan": "padatan", + "bawa2": "bawa-bawa", + "nyobain": "mencoba", + "pcaran": "pacaran", + "or": "orang", + "tdak": "tidak", + "prcuma": "percuma", + "langgengggg": "langgeng", + "sekaliiiii": "sekali", + "anehhh": "aneh", + "hayal": "khayal", + "bedua": "berdua", + "bapeerr": "bawa perasaan", + "keinget": "teringat", + "merekak": "mereka", + "kreeenn": "keren", + "abisss": "habis", + "kocaaaakkk": "kocak", + "abiiiss": "habis", + "pass": "pas", + "personill": "personil", + "lengkapp": "lengkap", + "ampuuun": "ampun", + "iiihhhh": "ih", + "flim.a": "filmnya", + "trusan.a": "terusannya", + "tdung": "tudung", + "gangguin": "mengganggu", + "lgkah2": "langkah-langkah", + "taw": "tau", + "cntiiikkk": "cantik", + "tegaar": "tegar", + "sktlrang": "sekarang", + "kangeeeennnnn": "kangen", + "sjk": "sejak", + "tambh": "tambah", + "nangiis": "menangis", + "nyaaaaaaaaa": "nya", + "bundaa": "bunda", + "duuhhhhh": "duh", + "gaada": "enggak ada", + "malsudnya": "maksudnya", + "lluuccuu": "lucu", + "siappa": "siapa", + "martabakk": "martabak", + "skali2": "sekali-kali", + "kueren2": "keren-keren", + "abgnya": "abangnya", + "warkop": "warung kopi", + "hayuu": "hayo", + "kenapaa": "kenapa", + "ilang2an": "hilang-hilangan", + "ayooook": "ayo", + "astagaaaaaaaa": "astaga", + "inihhh": "ini", + "oom": "om", + "mansetan": "bermanset", + "cadasss": "cadas", + "kereeeeennn": "keren", + "temen\"": "teman-teman", + "telefisi": "televisi", + "nikmatin": "menikmati", + "mantappp": "mantap", + "mbahh": "mbah", + "giitu": "begitu", + "mtanya": "matanya", + "cpit": "sipit", + "dee": "dek", + "sumvahh": "sumpah", + "tamvann": "tampan", + "yap": "ya", + "guaaaaaaa": "gua", + "capa": "sapa", + "ngakakkk": "mengakak", + "liaat": "lihat", + "gantweng": "ganteng", + "anjjjjjjjj": "anjing", + "gakwad": "enggak kuat", + "seru2": "seru-seru", + "iniii": "ini", + "favoriteeee": "favorit", + "adaa": "ada", + "dinyanyiin": "dinyanyikan", + "seruu": "seru", + "nyengir": "menyengir", + "temennya": "temannya", + "klk": "kelak", + "brjdh": "berjodoh", + "akhrt": "akhirat", + "kangennnnn": "kangen", + "kanggen": "kangen", + "kangennnnnnn": "kangen", + "ysh": "ya", + "sypa": "siapa", + "nangisss": "menangis", + "brjodoh": "berjodoh", + "kaliann": "kalian", + "barengg": "bareng", + "kpnpun": "kapanpun", + "mendj": "menjadi", + "allahhhh": "allah", + "merindinggg": "merinding", + "amiennnn": "amien", + "iihh": "ih", + "lhir": "lahir", + "udeh": "sudah", + "menjdi": "menjadi", + "debay": "adek bayi", + "bangeer": "banget", + "lafazd": "lafaz", + "pmbyran": "pembayaran", + "plsa": "pulsa", + "dimana\"": "dimana-mana", + "sokk": "sok", + "ngambekan": "ambekan", + "sma2": "sama-sama", + "ngom": "mengomong", + "toloollll": "tolol", + "bodhooo": "bodoh", + "peliharaaaa": "pelihara", + "makanyaa": "makanya", + "ati\"": "hati-hati", + "kemakan": "termakan", + "bodhoo": "bodoh", + "siihhh": "sih", + "andaaa": "anda", + "buktinyaaa": "buktinya", + "smuaaa": "semua", + "ngambeknya": "mengambeknya", + "kebongkar": "terbongkar", + "tapii": "tapi", + "kasiian": "kasihan", + "diladenin": "diladeni", + "comen": "komen", + "ngtif": "negatif", + "comntr": "komentar", + "kaliannn": "kalian", + "brdiri": "berdiri", + "mulutny": "mulutnya", + "msalah": "masalah", + "trserh": "terserah", + "waduuuuh": "waduh", + "ngedukung": "mendukung", + "dihpus": "dihapus", + "mrekaa": "mereka", + "cakeeeeppp": "cakep", + "benerr": "benar", + "brg\"": "bareng-bareng", + "baanget": "banget", + "anjaaaayyyyyy": "anjay", + "ngetsz": "banget", + "utkmu": "untukmu", + "anj": "anjing", + "merekaa": "mereka", + "kann": "kan", + "dieee": "dia", + "yaahh": "ya", + "hijap": "hijab", + "ternodain": "ternodai", + "ngeri2": "ngeri-ngeri", + "mn": "mana", + "kt2": "kata-kata", + "agk": "agak", + "slhkan": "salahkan", + "jilbabny": "jilbabnya", + "perilakuny": "perilakunya", + "ketaatanny": "ketaatannya", + "kepda": "kepada", + "sblmny": "sebelumnya", + "bljr": "belajar", + "pedulilaaaah": "pedulilah", + "ingetin": "mengingatkan", + "terjrumus": "terjerumus", + "stuju": "setuju", + "sbb": "sebab", + "igtlah": "ingatlah", + "dtgkan": "datangkan", + "benahin": "membenahi", + "ohhhh": "oh", + "yapp": "ya", + "jauhh": "jauh", + "bingitt": "banget", + "buka2an": "buka-bukaan", + "ngembali": "kembali", + "semngat": "semangat", + "lekong": "laki", + "kacauuuu": "kacau", + "ngimpi": "mimpi", + "owh": "oh", + "ngerampas": "merampas", + "ngehancurin": "menghancurkan", + "ngungkit2": "mengungkit-ungkit", + "kaleee": "kali", + "caranga": "caranya", + "usa": "usah", + "dipikirin": "dipikirkan", + "nunduk": "menunduk", + "tembemmm": "tembem", + "lucuuuuuuuuuu": "lucu", + "bundaaa": "bunda", + "nyubitt": "mencubit", + "jls2": "jelas-jelas", + "supeer": "super", + "inni": "ini", + "appa": "apa", + "hayooo": "hayo", + "adyem": "adem", + "mitnah": "memfitnah", + "cantiknyaaaaa": "cantiknya", + "tmbm": "tembam", + "cantix": "cantik", + "kriput": "keriput", + "jelex": "jelek", + "drmn": "darimana", + "laku\"": "laku-laku", + "orang\"nya": "orang-orangnya", + "kacian": "kasihan", + "cape": "capek", + "mancing2": "memancing-mancing", + "eeeeh": "eh", + "kpancing": "terpancing", + "lakuin": "melakukan", + "cri": "cari", + "sbenernya": "sebenarnya", + "mikirinnn": "memikirkan", + "diam\"": "diam-diam", + "mengalahkn": "mengalahkan", + "nyindirin": "menyindir", + "nee": "ini", + "klakuan": "kelakuan", + "mksdx": "maksudnya", + "shabat": "sahabat", + "jk": "jika", + "brbuat": "berbuat", + "mmberi": "memberi", + "nm": "nama", + "sbhabat": "sahabat", + "pnjerumus": "penjerumus", + "tman": "teman", + "shbat": "sahabat", + "melakukn": "melakukan", + "seorg": "seorang", + "berkluarga": "berkeluarga", + "siapa2": "siapa-siapa", + "ketipu": "tertipu", + "laki\"": "laki-laki", + "mengait2kan": "mengait-ngaitkan", + "nyir2": "nyinyir", + "gosipin": "menggosipkan", + "sendri": "sendiri", + "bntar": "bentar", + "duuuhhh": "duh", + "nmbk": "menembak", + "gakkk": "tidak", + "abiiiiisssss": "habis", + "tikung2": "tikung-tikung", + "cwok": "cowok", + "bangedd": "banget", + "wuastagah": "astaga", + "bedaaa": "beda", + "jaoooh": "jauh", + "mateeeeeeeeeeee": "mati", + "parsis": "persis", + "sumpeh": "sumpah", + "iyh": "iya", + "kmana": "kemana", + "cantiqs": "cantik", + "kecik": "kecil", + "brasa": "berasa", + "trlihat": "terlihat", + "mmberikan": "memberikan", + "khidupan": "kehidupan", + "srpti": "seperti", + "dinamakn": "dinamakan", + "sgtu": "sebegitu", + "paati": "pasti", + "kutuu": "kutu", + "setelur2nya": "setelur-telurnya", + "khadiran": "kehadiran", + "kel": "keluarga", + "srta": "serta", + "kcil": "kecil", + "kberkahn": "keberkahan", + "kbahagiaan": "kebahagiaan", + "tntram": "tentram", + "brsama": "bersama", + "hngga": "hingga", + "brpndapat": "berpendapat", + "trutama": "terutama", + "hiraukn": "hiraukan", + "diamkn": "diamkan", + "hrsny": "harusny", + "brsyukur": "bersyukur", + "mmberikn": "memberikan", + "rasakn": "rasakan", + "cari2": "cari-cari", + "kesalahanny": "kesalahannya", + "kekuranganny": "kekurangannya", + "seandainy": "seandainya", + "perasaanny": "perasaannya", + "diindonesis": "diindonesia", + "liyatnya": "lihatnya", + "kayahhh": "kaya", + "ala2": "ala-ala", + "yook": "yuk", + "yoookkkk": "yuk", + "siihh": "sih", + "elus2": "elus-elus", + "asliiiii": "asli", + "aduuhhhh": "aduh", + "belom": "belum", + "padahl": "padahal", + "btul": "betul", + "kejahatn": "kejahatan", + "slam": "salam", + "ngelewati": "melewati", + "kalu": "kalau", + "ngerendahin": "merendahkan", + "apaalahh": "apalah", + "tnp": "tanpa", + "menyebut2": "menyebut-nyebut", + "mnt": "minta", + "kiya": "kita", + "miriiiiip": "mirip", + "dirimuuu": "dirimu", + "yoiiu": "yoi", + "semuanyaaaa": "semuanya", + "suka.a": "sukanya", + "jdii": "jadi", + "blnja": "belanja", + "brangnya": "barangnya", + "ada2": "ada-ada", + "nimpuk": "menimpuk", + "abiss": "habis", + "prlu": "perlu", + "coyyy": "coy", + "suaranyaa": "suaranya", + "jnagn": "jangan", + "kcwa": "kecewa", + "jaman2": "jaman-jaman", + "sok2an": "sok-sokan", + "dket2in": "dekat-dekati", + "cobak": "coba", + "britanya": "beritanya", + "tau2": "tau-tau", + "mahh": "mah", + "jamam": "jaman", + "paraa": "para", + "seharusny": "seharusnya", + "dasaar": "dasar", + "bergunaa": "berguna", + "mulutnyaaa": "mulutnya", + "blkng": "belakang", + "kcewa": "kecewa", + "mnusuk": "menusuk", + "blkang": "belakang", + "nembak": "menembak", + "trmanis": "termanis", + "krng": "kurang", + "kaciiaann": "kasihan", + "nyela": "mencela", + "ngejilat": "menjilat", + "hallooo": "halo", + "deketin": "dekati", + "tayik": "tahi", + "halooo": "halo", + "ustzah": "ustazah", + "halow": "halo", + "poooolllll": "pol", + "tahannn": "tahan", + "aduuh": "aduh", + "nyimpen": "menyimpan", + "ngarsipin": "mengarsipkan", + "cman": "cuman", + "gituuuuu": "begitu", + "kyy": "kaya", + "kngn": "kangen", + "kq": "kok", + "nyesek": "menyesak", + "rinduuuuuuu": "rindu", + "b2": "berdua", + "kangeeeennn": "kangen", + "mg": "moga", + "aamiiiiinnn": "amin", + "gemezzzz": "gemas", + "kangennnnnn": "kangen", + "prh": "parah", + "bakattt": "bakat", + "napsuu": "napsu", + "gmau": "enggak mau", + "luccu": "lucu", + "prasaan": "perasaan", + "lahirr": "lahir", + "setujuuuu": "setuju", + "sering2": "sering-sering", + "ceemen": "cemen", + "sembunyi\"\"": "sembunyi-sembunyi", + "skrnh": "sekarang", + "cerdiik": "cerdik", + "jeniuss": "jenius", + "kurangin": "kurangi", + "kreen": "keren", + "s'lalu": "selalu", + "mauuuuuu": "mau", + "mintk": "minta", + "nyolong": "mencolong", + "pnjng": "panjang", + "slaluuuuu": "selalu", + "kanggggg": "kang", + "rezeqinya": "rezekinya", + "pintr": "pintar", + "dekk": "dek", + "maw": "mau", + "kura2": "kura-kura", + "edaaann": "edan", + "nempel": "menempel", + "hatiiii": "hati", + "sukkaa": "suka", + "luuuucuuu": "lucu", + "caantiiik": "cantik", + "luucuuu": "lucu", + "putihh": "putih", + "terus\"an": "terus-terusan", + "siiih": "sih", + "ternyataa": "ternyata", + "pollll": "pol", + "sekaliiii": "sekali", + "peduliin": "mempedulikan", + "komntr": "komentar", + "smngat": "semangat", + "dosax": "dosanya", + "pahalax": "pahalanya", + "akhirt": "akhirat", + "tinggalkn": "tinggalkan", + "rumh": "rumah", + "mk": "maka", + "trhtung": "terhitung", + "naaaa": "nya", + "ademmm": "adem", + "cerdass": "cerdas", + "tambahh": "tambah", + "polll": "pol", + "didengerin": "didengarkan", + "mantapppppp": "mantap", + "ngerespon": "merespons", + "dulurrr": "dulur", + "brusaha": "berusaha", + "lebh": "lebih", + "amin2": "amin-amin", + "positippp": "positif", + "jauuhhh": "jauh", + "brhjb": "berhijab", + "mdh2n": "mudah-mudahan", + "slmnya": "salamnya", + "jlbb": "jilbab", + "dempga": "semoga", + "selmnya": "selamanya", + "tth": "teteh", + "otot2": "otot-otot", + "lengkeeett": "lengket", + "caaannnttiikkk": "cantik", + "trusss": "terus", + "ademm": "adem", + "apalagiii": "apalagi", + "passs": "pas", + "beginii": "begini", + "satuu": "satu", + "cuantek": "cantik", + "akuuuur": "akur", + "iniehh": "ini", + "sayangx": "sayangnya", + "jodohx": "jodohnya", + "kaliaan": "kalian", + "haslnya": "hasilnya", + "kencng": "kencang", + "pmbantu": "pembantu", + "kmbl": "kembali", + "cuchook": "cocok", + "kloop": "klop", + "sempurnaa": "sempurna", + "klihatan": "kelihatan", + "cintax": "cintanya", + "pnuh": "penuh", + "ngrti": "mengerti", + "mudah\"an": "mudah-mudahan", + "segala\"ny": "segala-galanya", + "mantanny": "mantannya", + "jodohny": "jodohnya", + "cantk": "cantik", + "ituuu": "itu", + "kaasian": "kasihan", + "padhl": "padahal", + "ank\"": "anak-anak", + "pkrin": "pikirkan", + "anakx": "anaknya", + "sia2kan": "sia-siakan", + "naaahhh": "nah", + "beluumm": "belum", + "sayangggq": "sayangku", + "kreean": "keren", + "ciiieee": "cie", + "cieee": "cie", + "cieeee": "cie", + "berenanh": "berenang", + "pacarr": "pacar", + "bingitss": "banget", + "besokkk": "besok", + "annnnjjjjjjrrrr": "anjir", + "ngeliatin": "melihati", + "ngumbar2": "mengumbar-umbar", + "romantiss": "romantis", + "cocook": "cocok", + "lucuuuuuuuu": "lucu", + "kaaaaaann": "kan", + "nyaaaaaaa": "nya", + "lgs": "langsung", + "yaang": "yang", + "bangedt": "banget", + "gde": "gede", + "lutunaaa": "lucunya", + "bikinnn": "bikin", + "keee": "ke", + "bungkuuussss": "bungkus", + "lucux": "lucunya", + "bntr": "bentar", + "buanyaaak": "banyak", + "aaamiiinnn": "amin", + "aaamiiin": "amin", + "ntap": "mantap", + "abgnyaah": "abangnya", + "mungkiin": "mungkin", + "akuhh": "aku", + "manteb": "mantap", + "hiduupppp": "hidup", + "tebel": "tebal", + "nganggepin": "menanggapi", + "mentang\"": "mentang-mentang", + "najiss": "najis", + "perjabatt": "pejabat", + "makany": "makanya", + "benneeeeeerr": "benar", + "krjaan": "pekerjaan", + "kdng": "kadang", + "kdang": "kadang", + "knan": "kanan", + "anak'y": "anaknya", + "trknal": "terkenal", + "gtu\"": "begitu-gitu", + "klok": "kalo", + "aamiiiinnnnn": "amin", + "pengennnn": "pengin", + "guw": "gue", + "siiiiiiippppppppp": "sip", + "selaloe": "selalu", + "donggg": "dong", + "beruntungnyaaaa": "beruntungnya", + "soal'y": "soalnya", + "prhatian": "perhatian", + "hebaat": "hebat", + "ampunn": "ampun", + "gueeeee": "gue", + "liatt": "lihat", + "gehh": "gih", + "ankknyaaa": "anaknya", + "centilnyaaaaaa": "centilnya", + "gemesshzzzzhzzhzhzhsjssjs": "gemas", + "ngacaaa": "mengaca", + "diaaa": "dia", + "jirr": "anjir", + "kayaa": "kaya", + "cempreeng": "cempreng", + "centillll": "centil", + "pcran": "pacaran", + "anedh": "banget", + "ampyunnnn": "ampun", + "boro2": "boro-boro", + "nyta": "nyata", + "kampungn": "kampungan", + "j": "saja", + "mnghina": "menghina", + "ngolokin": "mengolok", + "blum": "belum", + "syirik\"": "syirik-syirik", + "than": "tahan", + "dsr": "dasar", + "siriik": "sirik", + "mampuuu": "mampu", + "khan": "kan", + "aduuhh": "aduh", + "cemburuu": "cemburu", + "bingitzzzz": "banget", + "tlog": "tolong", + "apasihhh": "apasih", + "sirikk": "sirik", + "dbnding": "dibanding", + "nrka": "neraka", + "s3ndiri": "sendiri", + "bhgiain": "membahagiakan", + "kdua": "kedua", + "prcya": "percaya", + "bhagaia": "bahagia", + "cuco": "cocok", + "genittt": "genit", + "kx": "kak", + "mlikk": "milik", + "mentah\"": "mentah-mentah", + "canci": "cantik", + "aneed": "banget", + "gimanaa": "bagaimana", + "lainnn": "lain", + "pentingg": "penting", + "bagaimanaaa": "bagaimana", + "krudung": "kerudung", + "enakkkkk": "enak", + "poool": "pol", + "jirrrr": "anjir", + "oprasiya": "operasinya", + "uwee": "gue", + "pilihannny": "pilihannya", + "itumahh": "itumah", + "muuuu": "mu", + "uuhhh": "uh", + "berbakiti": "berbakti", + "alhamd": "alhamdulillah", + "slmttt": "selamat", + "ucapin": "mengucapkan", + "tetp": "tetap", + "ditengah2": "ditengah-tengah", + "semuanyaa": "semuanya", + "semuannyaaa": "semuanya", + "aminn2": "amin-amin", + "abs": "habis", + "kebnyak'an": "kebanyakan", + "melahirin": "melahirkan", + "aduuuh": "aduh", + "cakeep": "cakep", + "lhooo": "loh", + "kamuuuu": "kamu", + "bagi2": "bagi-bagi", + "gimn": "bagaimana", + "smogaaa": "semoga", + "biza": "bisa", + "aamien": "amien", + "lngsing": "langsing", + "wowoooww": "wow", + "smulla": "semula", + "pdhall": "padahal", + "blln": "bulan", + "senang2": "senang-senang", + "jh": "saja", + "kereeennnn": "keren", + "gilakkk": "gila", + "gtu2": "begitu-gitu", + "cpet": "cepat", + "gataunya": "enggak taunya", + "gtau": "enggak tau", + "fans2": "fan-fan", + "gaharus": "enggak harus", + "sleran": "slearn", + "mobileny": "mobile", + "eror": "error", + "erorr": "error", + "sekala": "skala", + "perkulian": "perkuliahan", + "membuma": "membuka", + "kegitan": "kegiatan", + "tamopilan": "tampilan", + "menrutu": "menurut", + "menggangu": "mengganggu", + "aada": "ada", + "firur": "fitur", + "memudahakan": "memudahkan", + "infomatif": "informatif", + "melaui": "melalui", + "nilainnya": "nilainya", + "sederhan": "sederhana", + "adahal": "padahal", + "selaluuu": "selalu", + "tolongg": "tolong", + "perwakilaln": "perwakilan", + "mengumpukan": "mengumpulkan", + "yangg": "yang", + "lemott": "lambat", + "aaaaa": "", + "ditambahkam": "ditambahkan", + "kedepanya": "ke depannya", + "dann": "dan", + "digunkan": "digunakan", + "perkuliahann": "perkuliahan", + "yabg": "yang", + "lodingnya": "loadingnya", + "download": "unduh", + "didownload": "diunduh", + "mendownload": "mengunduh", + "upload": "unggah", + "diupload": "diunggah", + "mengupload": "mengunggah", + "scan": "pindai", + "discan": "dipindai", + "userfriendly": "ramah pengguna", + "apps": "aplikasi", + "simple": "sederhana", + "simpel": "sederhana", + "matkul": "mata kuliah", + "spek": "spesifikasi", + "aktifitas": "aktivitas", + "ngebaca": "membaca", + "dibenerin": "diperbaiki", + "ngalami": "mengalami", + "bikin": "membuat", + "dibikin": "dibuat", + "ngumpulin": "mengumpulkan", + "kumpulin": "kumpulkan", + "kumpulinnya": "mengumpulkan", + "dikumpulin": "dikumpulkan", + "kekumpul": "terkumpul", + "ngerjain": "mengerjakan", + "kerjain": "mengerjakan", + "kayak": "seperti", + "ribet": "rumit", + "lemot": "lambat", + "nemu": "menemukan", + "nyarinya": "mencari", + "nambahin": "menambahkan", + "mulu": "terus", + "doang": "saja", + "capek": "lelah", + "kelar": "selesai", + "telat": "terlambat", + "kesel": "kesal", + "bukanya": "bukanya", + "kepotong": "terpotong", + "kehambat": "terhambat", + "mepet": "dekat", + "ngekompres": "mengompres", + "apaan": "apa", + "diapus": "dihapus", + "numpang": "menumpang", + "walalupun": "walaupun", + "yaudah": "ya sudah", + "nugas": "mengerjakan tugas", + "ngelag": "lag", + "ngebug": "error", + "kekirim": "terkirim", + "lampirin": "melampirkan", + "muter": "memutar", + "kerasa": "terasa", + "dipelajarin": "dipelajari", + "plis": "tolong", + "khawatirin": "khawatirkan", + "ngebingungin": "membingungkan", + "kebangetan": "keterlaluan", + "ngumpul": "mengumpulkan", + "ngalamin": "mengalami", + "ditambahin": "ditambahkan", + "mampus": "sekali", + "pas": "saat", + "kasih": "beri", + "slear": "slearn", + "absen": "presensi", + "absensi": "presensi", + "scroll": "gulir", + "discroll": "digulir", + "mengscroll": "menggulir", + "rekomend": "direkomendasikan", + "dikalau": "kalau", + "temenku": "temanku", + "kedepannya": "ke depannya", + "raport": "rapor", + "web": "website", + "downloaad": "unduh", + "dikasih": "diberikan", + "mhs": "mahasiswa", + "design": "desain", + "lumanyan": "lumayan", + "fiturpadaa": "fitur pada", + "hutuh": "butuh", + "samain": "disamakan", + "meteri": "materi", + "ser": "geser", + "uxnya": "ux nya", + "tuh": "itu", + "kudu": "harus", + "gapapa": "tidak apa apa", + "dikit": "sedikit", + "dibeberapa": "di beberapa", + "mahasiwa": "mahasiswa", + "update": "pembaruan", + "diupdate": "pembaruan", + "slrean": "slearn", + "diperbaikan": "diperbaiki", + "cuman": "hanya", + "trouble": "masalah", + "webstie": "website", + "notify": "notifikasi", + "tapi": "tetapi", + "tau": "tahu", + "bawa": "bawa", + "kalo": "kalau", + "dikasi": "diberikan", + "pengelompokkan": "pengelompokan", + "dsb": "dan sebagainya", + "client": "klien", + "device": "perangkat", + "barcode": "kode batang", + "wesite": "website", + "notif": "notifikasi", + "mengumpulan": "mengumpulkan", + "mengumpulakn": "mengumpulkan", + "diatas": "di atas", + "transparasi": "transparansi", + "reminder": "pengingat", + "deadline": "tenggat", + "deadlinenya": "tenggatnya", + "megunduh": "mengunduh", + "gampang": "mudah", + "harusnya": "seharusnya", + "dipahamin": "dipahami", + "keseringan": "sering", + "buat": "buat", + "ganggu": "mengganggu", + "tiap": "tiap", + "jadinya": "sehingga", + "dibanding": "dibandingkan", + "aku": "saya", + "nih": "ini", + "resiko": "risiko", + "hp": "ponsel", + "down": "bermasalah", + "diantara": "di antara", + "ngembangin": "mengembangkan", + "keupload": "terunggah", + "menururt": "menurut", + "balikin": "kembalikan", + "dipaksain": "dipaksakan", + "hostingan": "hosting", + "jelek": "buruk", + "bareng": "bersama", + "soalnya": "karena", + "dijalanin": "dijalankan", + "berasa": "terasa", + "jadul": "kuno", + "gampangin": "mempermudah", + "kosongan": "kosong", + "mengasih": "memberi", + "apk": "aplikasi", + "app": "aplikasi", + "enak": "nyaman", + "dikerjain": "dikerjakan", + "kecatat": "tercatat", + "menurutku": "menurut saya", + "makin": "semakin", + "terimakasih": "terima kasih", + "dang": "dan", + "dimasukan": "dimasukkan", + "please": "tolong", + "pliss": "tolong", + "susah": "sulit", + "timingnya": "waktunya", + "worth": "layak", + "seringggggg": "sering", + "susaahhhh": "sulit", + "ngeganggu": "mengganggu", + "keburu": "keburu", + "benerin": "perbaiki", + "ngeleg": "lag", + "mau": "mau", + "thanks": "terima kasih", + "quiz": "kuis", + "rame": "ramai", + "nyelesaikan": "menyelesaikan", + "jatohnya": "jatuhnya", + "kepake": "terpakai", + "ngandelin": "mengandalkan", + "ngulang": "mengulang", + "lamaa": "lama", + "pribadiku": "pribadi saya", + "nangkep": "menangkap", + "ketemu": "temu", + "terkadang": "kadang", + "memuaskan": "puas", + "ditambahkan": "tambah", + "setiap": "tiap", + "nungguinnya": "tunggu", + "lamanya": "lama", + "mereset": "reset", + "ngenalin": "kenal", + "negnalin": "kenal", + "dipublish": "publikasi", + "pelajarinnya": "ajar", + "mengecek": "periksa", + "its": "its", + "okey": "okey" + }, + "frasa_awal": { + "\\bapaan dah\\b": "apa sih", + "\\bterima kasih\\b": "terimakasih", + "\\bngasih tahu\\b": "memberitahukan", + "\\bngasih tau\\b": "memberitahukan", + "\\bdi samain\\b": "disamakan", + "\\bpengeluaran akun\\b": "logout akun", + "\\bs learn\\b": "slearn", + "\\babsen nya\\b": "presensinya", + "\\bdi scroll\\b": "digulir", + "\\bdi kerjakan\\b": "dikerjakan", + "\\bdi berikan\\b": "diberikan", + "\\bdi atas namakan\\b": "diatasnamakan", + "\\bdi perbarui\\b": "diperbarui", + "\\be learning\\b": "elearning", + "\\bweb nya\\b": "websitenya", + "\\bfitur nya\\b": "fiturnya", + "\\bter update\\b": "terbarui", + "\\bdi update\\b": "pembaruan", + "\\bupdate nya\\b": "pembaruannya", + "\\bwebsite nya\\b": "websitenya", + "\\bdeadline nya\\b": "tenggatnya", + "\\bproses nya\\b": "prosesnya", + "\\btampilan nya\\b": "tampilannya", + "\\bsebelum nya\\b": "sebelumnya", + "\\bburuk nya\\b": "buruknya", + "\\bdi perbaiki\\b": "diperbaiki", + "\\blambat nya\\b": "lambatnya", + "\\bdi pindai\\b": "dipindai", + "\\brumit nya\\b": "rumitnya", + "\\bdari pada\\b": "daripada", + "\\bkesan nya\\b": "kesannya", + "\\blemot nya\\b": "lambatnya", + "\\bdi buka\\b": "dibuka", + "\\bdi ganti\\b": "diganti", + "\\bmahasiswa nya\\b": "mahasiswanya", + "\\bmakan waktu\\b": "menghabiskan waktu", + "\\bmau ngumpulin\\b": "mau mengumpulkan", + "\\bke logout\\b": "logout", + "\\bmuter balik\\b": "kembali", + "\\bke submit\\b": "disubmit", + "\\bsuka sama\\b": "suka dengan", + "\\bgara gara\\b": "karena", + "\\bke mahasiswa\\b": "bagi mahasiswa", + "\\bmutet muter\\b": "berputar putar", + "\\bbener bener\\b": "benar benar", + "\\bpengalaman ku\\b": "pengalamanku", + "\\bpribadi ku\\b": "pribadi saya", + "\\bkampus ku\\b": "kampusku", + "\\bjelek nya\\b": "jeleknya", + "\\bkurang nya\\b": "kurangnya", + "\\bwarna nya\\b": "warnanya", + "\\bsistem nya\\b": "sistemnya", + "\\bloading nya\\b": "loadingnya", + "\\bdashboard nya\\b": "dashboardnya", + "\\btugas nya\\b": "tugasnya", + "\\bkode nya\\b": "kodenya", + "\\bnegatif nya\\b": "negatifnya", + "\\baplikasi nya\\b": "aplikasinya", + "\\bbarcode nya\\b": "kode batangnya", + "\\bdi pahami\\b": "dipahami", + "\\bkelar sudah\\b": "sudah berakhir", + "\\bnge\\s+lag\\b": "lag", + "\\bits\\s+okey\\b": "its okey" + }, + "frasa_akhir": { + "\\bdi gunakan\\b": "digunakan", + "\\bdi pakai\\b": "dipakai", + "\\bdi akses\\b": "diakses", + "\\bdi unduh\\b": "diunduh", + "\\bdi download\\b": "diunduh", + "\\bdi upload\\b": "diunggah", + "\\bdi unggah\\b": "diunggah", + "\\bgak bisa\\b": "tidak bisa", + "\\bga bisa\\b": "tidak bisa", + "\\bnggak bisa\\b": "tidak bisa", + "\\buser friendly\\b": "ramah pengguna", + "\\bmemudah kan\\b": "memudahkan", + "\\bdidalam\\b": "di dalam", + "\\bdiluar\\b": "di luar", + "\\bdiatas\\b": "di atas", + "\\bdibawah\\b": "di bawah", + "\\bdisini\\b": "di sini", + "\\bits\\s+okey\\b": "its okey" + }, + "koreksi_validator": { + "ketemu": "temu", + "terkadang": "kadang", + "memuaskan": "puas", + "ditambahkan": "tambah", + "setiap": "tiap", + "nungguinnya": "tunggu", + "lamanya": "lama", + "mereset": "reset", + "ngenalin": "kenal", + "negnalin": "kenal", + "dipublish": "publikasi", + "pelajarinnya": "ajar", + "nanya": "tanya", + "mengecek": "periksa" + }, + "list_stopwords": [ + "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", + "bakal", + "bakalan", + "balik", + "bang", + "bapak", + "baru", + "bawah", + "beberapa", + "begini", + "beginian", + "beginikah", + "beginilah", + "begitu", + "begitukah", + "begitulah", + "begitupun", + "bekerja", + "belakang", + "belakangan", + "belumlah", + "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", + "bisakah", + "boleh", + "bolehkah", + "bolehlah", + "buat", + "bukankah", + "bukanlah", + "bulan", + "bung", + "cara", + "caranya", + "cukupkah", + "cukuplah", + "cuma", + "dahulu", + "dalam", + "dari", + "daripada", + "datang", + "deh", + "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", + "dikira", + "dilakukan", + "dilalui", + "dilihat", + "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", + "ditunjuknya", + "dituturkan", + "dituturkannya", + "diucapkan", + "diucapkannya", + "diungkapkan", + "dong", + "donk", + "dua", + "empat", + "enggak", + "enggaknya", + "entah", + "entahlah", + "gunakan", + "hal", + "hampir", + "hanya", + "hanyalah", + "hari", + "harus", + "haruslah", + "harusnya", + "hendak", + "hendaklah", + "hendaknya", + "hingga", + "ia", + "ialah", + "ibarat", + "ibaratkan", + "ibaratnya", + "ibu", + "ikut", + "ingat", + "ingat-ingat", + "ingin", + "inginkah", + "inginkan", + "ini", + "inikah", + "inilah", + "itu", + "itukah", + "itulah", + "jadi", + "jadilah", + "jadinya", + "jangankan", + "janganlah", + "jauh", + "jawab", + "jawaban", + "jawabnya", + "jelaskan", + "jelaslah", + "jelasnya", + "jika", + "jikalau", + "juga", + "jumlah", + "jumlahnya", + "justru", + "kah", + "kala", + "kalau", + "kalaulah", + "kalaupun", + "kalian", + "kami", + "kamilah", + "kamu", + "kamulah", + "kan", + "kapan", + "kapankah", + "kapanpun", + "karena", + "karenanya", + "kasus", + "kata", + "katakan", + "katakanlah", + "katanya", + "ke", + "keadaan", + "kebetulan", + "kedua", + "keduanya", + "keinginan", + "kelamaan", + "kelihatan", + "kelihatannya", + "kelima", + "kembali", + "kemudian", + "kemungkinan", + "kemungkinannya", + "kenapa", + "kepada", + "kepadanya", + "kesampaian", + "keseluruhan", + "keseluruhannya", + "keterlaluan", + "ketika", + "khususnya", + "kini", + "kinilah", + "kira", + "kira-kira", + "kiranya", + "kita", + "kitalah", + "kok", + "lagi", + "lagian", + "lah", + "lain", + "lainnya", + "lalu", + "lamanya", + "lanjut", + "lanjutnya", + "lewat", + "lima", + "luar", + "macam", + "maka", + "makanya", + "makin", + "malahan", + "mampu", + "mampukah", + "mana", + "manakala", + "manalagi", + "masa", + "masalahnya", + "masihkah", + "masing", + "masing-masing", + "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", + "menaiki", + "menambahkan", + "menandaskan", + "menanti", + "menanti-nanti", + "menantikan", + "menanya", + "menanyai", + "menanyakan", + "mendapat", + "mendapatkan", + "mendatang", + "mendatangi", + "mendatangkan", + "menegaskan", + "mengakhiri", + "mengapa", + "mengatakan", + "mengatakannya", + "mengenai", + "mengetahui", + "menggunakan", + "menghendaki", + "mengibaratkan", + "mengibaratkannya", + "mengingat", + "mengingatkan", + "menginginkan", + "mengira", + "mengucapkan", + "mengucapkannya", + "mengungkapkan", + "menjadi", + "menjawab", + "menjelaskan", + "menuju", + "menunjuk", + "menunjuki", + "menunjukkan", + "menunjuknya", + "menurut", + "menuturkan", + "menyampaikan", + "menyangkut", + "menyatakan", + "menyebutkan", + "menyeluruh", + "menyiapkan", + "merasa", + "mereka", + "merekalah", + "merupakan", + "meski", + "meskipun", + "meyakini", + "meyakinkan", + "min", + "minta", + "mirip", + "misal", + "misalkan", + "misalnya", + "mula", + "mulai", + "mulailah", + "mulanya", + "mungkin", + "mungkinkah", + "nah", + "naik", + "namun", + "nanti", + "nantinya", + "nyaris", + "nyatanya", + "oleh", + "olehnya", + "pada", + "padanya", + "pak", + "paling", + "panjang", + "pantas", + "para", + "pasti", + "pastilah", + "penting", + "pentingnya", + "per", + "percuma", + "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", + "sangatlah", + "satu", + "saya", + "sayalah", + "se", + "sebab", + "sebabnya", + "sebagai", + "sebagaimana", + "sebagainya", + "sebagian", + "sebaik", + "sebaik-baiknya", + "sebaiknya", + "sebaliknya", + "sebanyak", + "sebegini", + "sebegitu", + "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-kali", + "sekalian", + "sekaligus", + "sekalipun", + "sekarang", + "sekecil", + "seketika", + "sekiranya", + "sekitar", + "sekitarnya", + "sekurang-kurangnya", + "sekurangnya", + "sela", + "selain", + "selaku", + "selama", + "selama-lamanya", + "selamanya", + "selanjutnya", + "seluruh", + "seluruhnya", + "semacam", + "semakin", + "semampu", + "semampunya", + "semasa", + "semasih", + "semata", + "semata-mata", + "semaunya", + "sementara", + "semisal", + "semisalnya", + "sempat", + "semua", + "semuanya", + "semula", + "sendirian", + "sendirinya", + "seolah", + "seolah-olah", + "seorang", + "sepanjang", + "sepantasnya", + "sepantasnyalah", + "seperlunya", + "seperti", + "sepertinya", + "sepihak", + "seringnya", + "serta", + "serupa", + "sesaat", + "sesama", + "sesampai", + "sesegera", + "sesekali", + "seseorang", + "sesuatu", + "sesuatunya", + "sesudah", + "sesudahnya", + "setelah", + "setempat", + "setengah", + "seterusnya", + "setiap", + "setiba", + "setibanya", + "setidak-tidaknya", + "setidaknya", + "setinggi", + "seusai", + "sewaktu", + "si", + "siapa", + "siapakah", + "siapapun", + "sih", + "sini", + "sinilah", + "soal", + "soalnya", + "suatu", + "sudahkah", + "sudahlah", + "supaya", + "tadi", + "tadinya", + "tahu", + "tahun", + "tak", + "tambahnya", + "tampak", + "tampaknya", + "tandas", + "tandasnya", + "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", + "terlebih", + "termasuk", + "ternyata", + "tersampaikan", + "tersebut", + "tersebutlah", + "tertentu", + "tertuju", + "terus", + "terutama", + "tetap", + "tetapi", + "tiba-tiba", + "tidakkah", + "tiga", + "tinggi", + "toh", + "tunjuk", + "turut", + "tutur", + "tuturnya", + "ucap", + "ucapnya", + "ujar", + "ujarnya", + "umum", + "umumnya", + "ungkap", + "ungkapnya", + "untuk", + "usah", + "usai", + "waduh", + "wah", + "wahai", + "waktunya", + "walau", + "walaupun", + "wong", + "ya", + "yaitu", + "yakin", + "yakni", + "yang" + ], + "kata_dilindungi_stemming": [ + "ada", + "ajar", + "banyak", + "belum", + "berantakan", + "dan", + "dekat", + "dulu", + "kadang", + "kecil", + "kenal", + "keterangan", + "lag", + "lama", + "puas", + "publikasi", + "reset", + "sebelumnya", + "semoga", + "tambah", + "temu", + "terlihat", + "tiap", + "tunggu", + "waktu" + ] +} \ No newline at end of file diff --git a/api_slearn/artifacts_slearn/preprocessing_slearn.py b/api_slearn/artifacts_slearn/preprocessing_slearn.py new file mode 100644 index 0000000..ef42ca5 --- /dev/null +++ b/api_slearn/artifacts_slearn/preprocessing_slearn.py @@ -0,0 +1,124 @@ +import re, json +from pathlib import Path +from nltk.tokenize import word_tokenize +from Sastrawi.Stemmer.StemmerFactory import StemmerFactory + +config = json.loads( + (Path(__file__).parent / 'preprocessing_config.json') + .read_text(encoding='utf-8') +) + +kamus_normalisasi = config['kamus_normalisasi'] +frasa_awal = config['frasa_awal'] +frasa_akhir = config['frasa_akhir'] +koreksi_validator = config['koreksi_validator'] +list_stopwords = set(config['list_stopwords']) +kata_dilindungi_stemming = set( + config['kata_dilindungi_stemming'] +) + +stemmer = StemmerFactory().create_stemmer() + +def cleaning(text): + text = str(text) + + # 1. Menghapus URL/Link (tidak peka huruf besar/kecil) + text = re.sub(r"https?://\S+|www\.\S+", " ", text, flags=re.IGNORECASE) + + # 2. Menghapus mention (@), pertahankan isi hashtag (#) + text = re.sub(r"@\w+", " ", text) + text = re.sub(r"#(\w+)", r"\1", text) + + # 3. Menghapus angka + text = re.sub(r"\d+", " ", text) + + # 4. Menghapus karakter selain huruf + text = re.sub(r"[^a-zA-Z\s]", " ", text) + + # 5. Merapikan spasi berlebih + text = re.sub(r"\s+", " ", text).strip() + + return text + + +def case_folding(text): + # Mengubah seluruh teks menjadi huruf kecil + return str(text).lower() + + +def ganti_frasa(text, kamus_frasa): + for pola, ganti in kamus_frasa.items(): + text = re.sub(pola, ganti, text) + return text + + +def normalisasi(text): + text = ganti_frasa(str(text), frasa_awal) + text = re.sub(r"\b\w+\b", lambda m: kamus_normalisasi.get(m.group(), m.group()), text) + return ganti_frasa(text, frasa_akhir) + + +def koreksi_normalisasi_validator(text): + text = str(text) + text = re.sub(r"\bnge\s+lag\b", "lag", text) # koreksi frasa hasil validasi + text = re.sub(r"\bits\s+okey\b", "its okey", text) # "its okey" dipertahankan + return re.sub(r"\b\w+\b", lambda x: koreksi_validator.get(x.group(), x.group()), text) + + +def tokenisasi(text): + return word_tokenize(str(text)) + + +def hapus_stopword(tokens): + return " ".join( + kata for kata in tokens + if kata not in list_stopwords and len(kata) > 1 + ) + + +def stemming(text): + hasil = [] + + for kata in str(text).split(): + if kata in kata_dilindungi_stemming: + hasil.append(kata) + else: + hasil.append(stemmer.stem(kata)) + + return " ".join(hasil) + + +def preprocessing_kalimat_baru(text): + + # 1. Cleaning + text = cleaning(text) + + # 2. Case Folding + text = case_folding(text) + + # 3. Normalisasi + text = normalisasi(text) + text = koreksi_normalisasi_validator(text) + + # 4. Tokenisasi + tokens = tokenisasi(text) + + # 5. Stopword Removal + text = hapus_stopword(tokens) + + # 6. Stemming + text = stemming(text) + + return text + + +def prediksi_sentimen(teks, model, vectorizer): + if not isinstance(teks, str) or not teks.strip(): + raise ValueError("Masukkan komentar terlebih dahulu.") + hasil = preprocessing_kalimat_baru(teks) + if not hasil.strip(): + raise ValueError("Komentar tidak memiliki kata yang dapat diproses.") + fitur = vectorizer.transform([hasil]) + if fitur.nnz == 0: + raise ValueError("Kata dalam komentar belum dikenali model.") + return int(model.predict(fitur)[0]) diff --git a/api_slearn/artifacts_slearn/requirements.txt b/api_slearn/artifacts_slearn/requirements.txt new file mode 100644 index 0000000..acac512 --- /dev/null +++ b/api_slearn/artifacts_slearn/requirements.txt @@ -0,0 +1,7 @@ +scikit-learn==1.6.1 +numpy==2.1.3 +scipy==1.16.3 +pandas==2.2.3 +nltk==3.9.1 +Sastrawi==1.0.1 +joblib==1.6.0 diff --git a/api_slearn/artifacts_slearn/stopwords_final.joblib b/api_slearn/artifacts_slearn/stopwords_final.joblib new file mode 100644 index 0000000000000000000000000000000000000000..f632b99ac3da934847f98486285d1cc90d2e139b GIT binary patch literal 7623 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 literal 0 HcmV?d00001 diff --git a/api_slearn/artifacts_slearn/tfidf_vectorizer.joblib b/api_slearn/artifacts_slearn/tfidf_vectorizer.joblib new file mode 100644 index 0000000000000000000000000000000000000000..7149e8a3a0c205df3a4bfc650d3420af73f94ae0 GIT binary patch literal 13944 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 0).sum(axis=0) +).ravel().astype(int) + +idf_per_fitur = np.asarray( + tfidf.idf_ +) + +if len(df_per_fitur) != len(kata_unik): + raise ValueError( + "Jumlah DF tidak sama dengan jumlah fitur." + ) + +if len(idf_per_fitur) != len(kata_unik): + raise ValueError( + "Jumlah IDF tidak sama dengan jumlah fitur." + ) + +print("✓ DF dan IDF berhasil disiapkan.") + + +# ============================================================ +# 8. MEMBUAT POSISI TRAINING DAN TESTING +# ============================================================ + +posisi_training = { + int(index): urutan + for urutan, index + in enumerate( + train_index, + start=1 + ) +} + +posisi_testing = { + int(index): urutan + for urutan, index + in enumerate( + test_index, + start=1 + ) +} + + +# ============================================================ +# 9. MEMBENTUK DETAIL TF-IDF PER DATA +# ============================================================ + +def buat_data_tfidf( + index_array, + matrix, + jenis_data +): + hasil = [] + + for posisi_matrix, index_asli in enumerate( + index_array + ): + index_asli = int(index_asli) + + baris_matrix = matrix.getrow( + posisi_matrix + ) + + stemmed_text = str( + df.at[ + index_asli, + "stemmed_text" + ] + ).strip() + + hitung_kata = Counter( + stemmed_text.split() + ) + + bobot_tfidf = [] + + for kolom, bobot in zip( + baris_matrix.indices, + baris_matrix.data + ): + kata = str( + kata_unik[kolom] + ) + + tf = int( + hitung_kata.get( + kata, + 0 + ) + ) + + df_kata = int( + df_per_fitur[kolom] + ) + + idf = float( + idf_per_fitur[kolom] + ) + + bobot = float( + bobot + ) + + bobot_tfidf.append({ + "kata": kata, + "tf": tf, + "df": df_kata, + "idf": idf, + "bobot": bobot + }) + + # Bobot terbesar ditampilkan lebih dulu. + bobot_tfidf.sort( + key=lambda item: ( + -item["bobot"], + item["kata"] + ) + ) + + if jenis_data == "Training": + urutan_split = posisi_training[ + index_asli + ] + else: + urutan_split = posisi_testing[ + index_asli + ] + + hasil.append({ + "no_data": index_asli + 1, + "ulasan_asli": str( + df.at[ + index_asli, + "komentar" + ] + ), + "stemmed_text": stemmed_text, + "label": int( + df.at[ + index_asli, + "label" + ] + ), + "jenis_data": jenis_data, + "urutan_split": int( + urutan_split + ), + "jumlah_fitur_nonzero": int( + baris_matrix.nnz + ), + "bobot_tfidf": bobot_tfidf + }) + + return hasil + + +print( + "\nMenyusun detail TF-IDF " + "seluruh 571 data..." +) + +data_training = buat_data_tfidf( + train_index, + X_train, + "Training" +) + +data_testing = buat_data_tfidf( + test_index, + X_test, + "Testing" +) + +data_semua = ( + data_training + + data_testing +) + +# Agar website menampilkan +# Data ke-1 sampai Data ke-571. +data_semua.sort( + key=lambda item: item["no_data"] +) + +if len(data_semua) != 571: + raise ValueError( + "Jumlah hasil TF-IDF bukan 571 data." + ) + +print( + "✓ Detail TF-IDF 571 data berhasil dibuat." +) + + +# ============================================================ +# 10. MEMBENTUK JSON FINAL +# ============================================================ + +hasil_json = { + "summary": { + "jumlah_data": 571, + "jumlah_fitur": int( + len(kata_unik) + ), + "data_training": int( + len(train_index) + ), + "data_testing": int( + len(test_index) + ), + "ukuran_matriks_training": [ + int(X_train.shape[0]), + int(X_train.shape[1]) + ], + "ukuran_matriks_testing": [ + int(X_test.shape[0]), + int(X_test.shape[1]) + ], + "test_size": 0.2, + "random_state": 42, + "stratify": "label", + "norm": None, + "smooth_idf": True, + "use_idf": True, + "sublinear_tf": False, + "keterangan": ( + "TF-IDF dihitung tanpa normalisasi L2 " + "(norm=None). Bobot akhir merupakan " + "hasil TF × IDF." + ) + }, + "data": data_semua +} + + +# ============================================================ +# 11. BACKUP JSON LAMA +# ============================================================ + +if OUTPUT_PATH.exists(): + timestamp = datetime.now().strftime( + "%Y%m%d_%H%M%S" + ) + + backup_path = ( + PUBLIC_DATA_DIR + / f"tfidf_research_backup_{timestamp}.json" + ) + + shutil.copy2( + OUTPUT_PATH, + backup_path + ) + + print( + "\n✓ JSON lama dibackup:" + ) + + print( + f" {backup_path.name}" + ) + + +# ============================================================ +# 12. MENYIMPAN JSON BARU +# ============================================================ + +with open( + OUTPUT_PATH, + "w", + encoding="utf-8" +) as file: + json.dump( + hasil_json, + file, + ensure_ascii=False, + indent=2 + ) + +print( + "\n✓ JSON TF-IDF baru berhasil disimpan:" +) + +print( + f" {OUTPUT_PATH}" +) + + +# ============================================================ +# 13. VERIFIKASI FILE HASIL +# ============================================================ + +with open( + OUTPUT_PATH, + "r", + encoding="utf-8" +) as file: + verifikasi = json.load( + file + ) + +if len( + verifikasi.get( + "data", + [] + ) +) != 571: + raise ValueError( + "Verifikasi gagal: jumlah data JSON bukan 571." + ) + +summary_verifikasi = verifikasi.get( + "summary", + {} +) + +if summary_verifikasi.get( + "jumlah_fitur" +) != 675: + raise ValueError( + "Verifikasi gagal: jumlah fitur bukan 675." + ) + +if summary_verifikasi.get( + "norm", + "SALAH" +) is not None: + raise ValueError( + "Verifikasi gagal: norm bukan None." + ) + + +# ============================================================ +# 14. MENCARI BOBOT TF-IDF TERBESAR +# ============================================================ + +bobot_terbesar = 0.0 +kata_bobot_terbesar = "-" +data_bobot_terbesar = None + +for item in data_semua: + for fitur in item["bobot_tfidf"]: + if fitur["bobot"] > bobot_terbesar: + bobot_terbesar = fitur["bobot"] + kata_bobot_terbesar = fitur["kata"] + data_bobot_terbesar = item["no_data"] + + +# ============================================================ +# 15. OUTPUT FINAL +# ============================================================ + +print("\n" + "=" * 80) +print("EXPORT TF-IDF PENELITIAN BERHASIL") +print("=" * 80) + +print( + f"Jumlah data : " + f"{len(data_semua)}" +) + +print( + f"Data training : " + f"{len(train_index)}" +) + +print( + f"Data testing : " + f"{len(test_index)}" +) + +print( + f"Jumlah fitur : " + f"{len(kata_unik)}" +) + +print( + f"Ukuran matriks training : " + f"{X_train.shape}" +) + +print( + f"Ukuran matriks testing : " + f"{X_test.shape}" +) + +print( + f"Normalisasi TF-IDF : " + f"{norm_tfidf}" +) + +print( + "Bobot akhir : " + "TF × IDF" +) + +print( + f"Bobot terbesar : " + f"{bobot_terbesar:.6f}" +) + +print( + f"Kata bobot terbesar : " + f"{kata_bobot_terbesar}" +) + +print( + f"Berada pada Data ke : " + f"{data_bobot_terbesar}" +) + +print( + f"\nFile output:\n{OUTPUT_PATH}" +) + +print( + "\n✓ TF-IDF website sekarang menggunakan " + "hasil penelitian TANPA normalisasi L2." +) \ No newline at end of file diff --git a/api_slearn/main.py b/api_slearn/main.py new file mode 100644 index 0000000..d2dbb2d --- /dev/null +++ b/api_slearn/main.py @@ -0,0 +1,487 @@ +from uuid import uuid4 + +from fastapi import FastAPI, HTTPException, BackgroundTasks +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel, Field +from typing import Literal +from training_input import validate_column +from model_registry import bundle, history, activate, queue, train_job + +from model_service import ( + analisis_teks, + preprocessing_detail, + get_model_info +) + +from database import ( + test_db_connection, + get_database_summary, + save_analysis_batch, + get_analysis_batch, + get_analysis_history, + save_single_prediction, + get_single_prediction_history, + get_single_prediction +) + + +# ============================================================ +# FASTAPI +# ============================================================ + +app = FastAPI( + title="S-Learn Sentiment Analysis API", + description=( + "Backend API analisis sentimen S-Learn " + "menggunakan TF-IDF dan Logistic Regression." + ), + version="1.0.0" +) + + +# ============================================================ +# CORS +# ============================================================ + +app.add_middleware( + CORSMiddleware, + allow_origin_regex=r"http://(localhost|127\.0\.0\.1):\d+", + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"] +) + + +# ============================================================ +# REQUEST MODEL +# ============================================================ + +class TextRequest(BaseModel): + text: str + + +class CSVReviewItem(BaseModel): + source_row_number: int + text: str + + +class CSVAnalysisRequest(BaseModel): + file_name: str + selected_column: str + reviews: list[CSVReviewItem] + + +class TrainingRequest(BaseModel): + selected_column: str = Field(min_length=1, max_length=200) + confirmed_review_column: Literal[True] + file_name: str = Field(min_length=1, max_length=200) + reviews: list[str] = Field(min_length=1, max_length=5000) + + +@app.get('/models') +def model_history(): + return {'status': 'success', 'data': history()} + + +@app.get('/model-info/original') +def original_model_info(): + return {'status': 'success', 'data': bundle('v1')[2]} + + +@app.post('/models/{version}/activate') +def activate_model(version: str): + try: + return {'status': 'success', 'data': activate(version)} + except ValueError as error: + raise HTTPException(status_code=400, detail=str(error)) from error + + +@app.post('/training', status_code=202) +def training(request: TrainingRequest, background: BackgroundTasks): + try: + validate_column(request.selected_column) + except ValueError as error: + raise HTTPException(status_code=400, detail=str(error)) from error + if any(len(text) > 10000 for text in request.reviews): + raise HTTPException(status_code=400, detail='Panjang ulasan maksimal 10.000 karakter.') + if not any(text.strip() for text in request.reviews): + raise HTTPException(status_code=400, detail='CSV tidak berisi ulasan.') + try: + job_id = queue(request.file_name) + except ValueError as error: + raise HTTPException(status_code=409, detail=str(error)) from error + background.add_task(train_job, job_id, request.reviews, request.selected_column) + return {'status': 'success', 'job_id': job_id} + + +# ============================================================ +# ROOT +# ============================================================ + +@app.get("/") +def root(): + return { + "message": "S-Learn Sentiment Analysis API", + "status": "running" + } + + +# ============================================================ +# HEALTH CHECK BACKEND +# ============================================================ + +@app.get("/health") +def health_check(): + return { + "status": "ok", + "service": "api_slearn", + "model": "loaded" + } + + +# ============================================================ +# HEALTH CHECK DATABASE +# ============================================================ + +@app.get("/database/health") +def database_health(): + try: + koneksi = test_db_connection() + summary = get_database_summary() + + return { + "status": "success", + "message": "Koneksi MySQL berhasil.", + "database": koneksi["database"], + "tables": koneksi["tables"], + "summary": summary + } + + except ConnectionError as error: + raise HTTPException( + status_code=500, + detail=str(error) + ) + + +# ============================================================ +# INFORMASI MODEL +# ============================================================ + +@app.get("/model-info") +def model_info(): + return { + "status": "success", + "data": get_model_info() + } + + +# ============================================================ +# PREPROCESSING SATU ULASAN +# ============================================================ + +@app.post("/preprocess") +def preprocess(request: TextRequest): + try: + hasil = preprocessing_detail( + request.text + ) + + return { + "status": "success", + "data": hasil + } + + except ValueError as error: + raise HTTPException( + status_code=400, + detail=str(error) + ) + + +# ============================================================ +# PREDIKSI SATU ULASAN +# ============================================================ + +@app.post("/predict") +def predict(request: TextRequest): + try: + # ---------------------------------------------------- + # 1. ANALISIS TEKS + # ---------------------------------------------------- + + hasil = analisis_teks( + request.text + ) + + # ---------------------------------------------------- + # 2. SIMPAN HASIL KE MYSQL + # ---------------------------------------------------- + + try: + prediction_id = save_single_prediction( + hasil + ) + + except Exception as error: + raise HTTPException( + status_code=500, + detail=( + "Prediksi berhasil dilakukan, " + "tetapi hasil gagal disimpan " + "ke database. " + f"{str(error)}" + ) + ) + + # ---------------------------------------------------- + # 3. KEMBALIKAN HASIL + # ---------------------------------------------------- + + return { + "status": "success", + "message": ( + "Prediksi berhasil dilakukan " + "dan disimpan ke riwayat." + ), + "data": { + **hasil, + "prediction_id": prediction_id + } + } + + except HTTPException: + raise + + except ValueError as error: + raise HTTPException( + status_code=400, + detail=str(error) + ) + + +# ============================================================ +# RIWAYAT PREDIKSI SATU ULASAN +# ============================================================ + +@app.get("/predictions") +def prediction_history(): + try: + history = get_single_prediction_history() + + return { + "status": "success", + "data": history, + "total": len(history) + } + + except Exception as error: + raise HTTPException( + status_code=500, + detail=( + "Gagal mengambil riwayat " + "Prediksi Satu Ulasan. " + f"{str(error)}" + ) + ) + + +# ============================================================ +# DETAIL SATU PREDIKSI +# ============================================================ + +@app.get("/predictions/{prediction_id}") +def prediction_detail( + prediction_id: int +): + if prediction_id < 1: + raise HTTPException( + status_code=400, + detail="ID prediksi tidak valid." + ) + + try: + hasil = get_single_prediction( + prediction_id + ) + + if hasil is None: + raise HTTPException( + status_code=404, + detail="Riwayat prediksi tidak ditemukan." + ) + + return { + "status": "success", + "data": hasil + } + + except HTTPException: + raise + + except Exception as error: + raise HTTPException( + status_code=500, + detail=( + "Gagal mengambil detail prediksi. " + f"{str(error)}" + ) + ) + + +# ============================================================ +# ANALISIS CSV +# ============================================================ + +@app.post("/analysis/csv") +def analysis_csv( + request: CSVAnalysisRequest +): + if not request.file_name.strip(): + raise HTTPException( + status_code=400, + detail="Nama file CSV tidak boleh kosong." + ) + + if not request.selected_column.strip(): + raise HTTPException( + status_code=400, + detail="Kolom ulasan belum dipilih." + ) + + if not request.reviews: + raise HTTPException( + status_code=400, + detail="Tidak ada ulasan yang dapat dianalisis." + ) + + batch_id = ( + "BATCH-" + + uuid4().hex[:12].upper() + ) + + hasil_analisis = [] + snapshot = bundle() + + for review in request.reviews: + + if review.source_row_number < 1: + raise HTTPException( + status_code=400, + detail="Nomor baris CSV tidak valid." + ) + + try: + hasil = analisis_teks( + review.text, snapshot=snapshot + ) + + except ValueError as error: + raise HTTPException( + status_code=400, + detail=( + f"Baris {review.source_row_number}: " + f"{str(error)}" + ) + ) + + hasil[ + "source_row_number" + ] = review.source_row_number + + hasil_analisis.append( + hasil + ) + + try: + batch = save_analysis_batch( + batch_id=batch_id, + file_name=request.file_name.strip(), + selected_column=request.selected_column.strip(), + results=hasil_analisis + ) + + except Exception as error: + raise HTTPException( + status_code=500, + detail=( + "Analisis berhasil dilakukan, " + "tetapi hasil gagal disimpan " + "ke database. " + f"{str(error)}" + ) + ) + + return { + "status": "success", + "message": ( + "CSV berhasil dianalisis dan " + "disimpan ke database." + ), + "data": { + **batch, + "results": hasil_analisis + } + } + + +# ============================================================ +# RIWAYAT SEMUA BATCH CSV +# ============================================================ + +@app.get("/analysis") +def analysis_history(): + try: + history = get_analysis_history() + + return { + "status": "success", + "data": history, + "total": len(history) + } + + except Exception as error: + raise HTTPException( + status_code=500, + detail=( + "Gagal mengambil riwayat analisis. " + f"{str(error)}" + ) + ) + + +# ============================================================ +# DETAIL SATU BATCH CSV +# ============================================================ + +@app.get("/analysis/{batch_id}") +def analysis_detail( + batch_id: str +): + try: + hasil = get_analysis_batch( + batch_id.strip() + ) + + if hasil is None: + raise HTTPException( + status_code=404, + detail="Batch analisis tidak ditemukan." + ) + + return { + "status": "success", + "data": hasil + } + + except HTTPException: + raise + + except Exception as error: + raise HTTPException( + status_code=500, + detail=( + "Gagal mengambil hasil analisis. " + f"{str(error)}" + ) + ) diff --git a/api_slearn/migrate_model_versions.py b/api_slearn/migrate_model_versions.py new file mode 100644 index 0000000..c71825d --- /dev/null +++ b/api_slearn/migrate_model_versions.py @@ -0,0 +1,24 @@ +"""Add nullable version columns without modifying historical data. Safe to rerun.""" +from database import get_db_connection + +def migrate(connection): + cursor = connection.cursor() + try: + for table in ('analysis_batches', 'single_prediction_history'): + cursor.execute('SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=%s AND COLUMN_NAME=%s', (table, 'model_version')) + if not cursor.fetchone()[0]: + cursor.execute(f'ALTER TABLE `{table}` ADD COLUMN `model_version` VARCHAR(64) NULL DEFAULT NULL') + print(f'{table}: model_version ditambahkan.') + else: + print(f'{table}: sudah diperbarui.') + connection.commit() + finally: + cursor.close() + +if __name__ == '__main__': + connection = get_db_connection() + try: + migrate(connection) + print('Selesai. Riwayat lama tetap tersimpan dengan versi tidak tercatat.') + finally: + connection.close() diff --git a/api_slearn/model_registry.py b/api_slearn/model_registry.py new file mode 100644 index 0000000..0d7b71a --- /dev/null +++ b/api_slearn/model_registry.py @@ -0,0 +1,259 @@ +"""Versioned local model store. Run one Uvicorn worker (see PANDUAN_REVISI.md).""" +import hashlib +import json +import logging +import os +import sqlite3 +import threading +import unicodedata +from datetime import datetime, timezone +from contextlib import contextmanager +from pathlib import Path +from uuid import uuid4 + +import joblib +import pandas as pd +from sklearn.base import clone +from sklearn.metrics import accuracy_score, classification_report, confusion_matrix +from sklearn.model_selection import train_test_split + +BASE = Path(__file__).resolve().parent +ARTIFACTS = BASE / 'artifacts_slearn' +STORE = Path(os.getenv('SLEARN_MODEL_STORE', str(BASE / 'model_store'))) +STORE.mkdir(parents=True, exist_ok=True) +DB = STORE / 'registry.sqlite3' +_cache = {} +_cache_lock = threading.RLock() +_seed_cache = None + + +def now(): + return datetime.now(timezone.utc).isoformat() + + +@contextmanager +def connect(): + db = sqlite3.connect(DB, timeout=30) + db.row_factory = sqlite3.Row + try: + with db: + yield db + finally: + db.close() + + +def key(text): + return ' '.join(unicodedata.normalize('NFKC', str(text)).casefold().split()) + + +def initialize(): + with connect() as db: + db.executescript(''' + CREATE TABLE IF NOT EXISTS versions ( + id TEXT PRIMARY KEY, created_at TEXT NOT NULL, metadata TEXT NOT NULL); + CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT NOT NULL); + CREATE TABLE IF NOT EXISTS additions ( + text_key TEXT PRIMARY KEY, text TEXT NOT NULL, stem TEXT NOT NULL, + label INTEGER NOT NULL, confidence REAL NOT NULL, + teacher TEXT NOT NULL, version TEXT NOT NULL); + CREATE TABLE IF NOT EXISTS jobs ( + id TEXT PRIMARY KEY, created_at TEXT NOT NULL, status TEXT NOT NULL, + message TEXT NOT NULL, version TEXT, file_name TEXT NOT NULL); + CREATE UNIQUE INDEX IF NOT EXISTS one_training ON jobs ((1)) + WHERE status IN ('queued', 'running'); + ''') + columns = {r['name'] for r in db.execute('PRAGMA table_info(jobs)')} + if 'audit' not in columns: + db.execute('ALTER TABLE jobs ADD COLUMN audit TEXT') + meta = json.loads((ARTIFACTS / 'metadata_model.json').read_text(encoding='utf-8')) + meta.update(version='v1', label_source='Dataset penelitian berlabel', added_rows=0) + db.execute('INSERT OR IGNORE INTO versions VALUES (?, ?, ?)', + ('v1', meta.get('dibuat_pada_utc', now()), json.dumps(meta))) + db.execute("INSERT OR IGNORE INTO settings VALUES ('active', 'v1')") + # A stopped process cannot finish its queued job. No partial data are committed. + db.execute("UPDATE jobs SET status='failed', message=? WHERE status IN ('queued','running')", + ('Training terhenti saat server dimatikan. Silakan unggah kembali.',)) + + +initialize() + + +def bundle(version=None): + with connect() as db: + version = version or db.execute("SELECT value FROM settings WHERE key='active'").fetchone()[0] + row = db.execute('SELECT metadata FROM versions WHERE id=?', (version,)).fetchone() + if row is None: + raise ValueError('Versi model tidak ditemukan.') + if json.loads(row[0]).get('invalidated'): + raise ValueError('Model ini tidak valid karena input training keliru dan tidak boleh digunakan.') + with _cache_lock: + if version not in _cache: + folder = ARTIFACTS if version == 'v1' else STORE / version + model = joblib.load(folder / 'model_logistic_regression.joblib') + vectorizer = joblib.load(folder / 'tfidf_vectorizer.joblib') + meta = json.loads(row[0]) + if model.n_features_in_ != len(vectorizer.get_feature_names_out()) or list(model.classes_) != [0, 1]: + raise ValueError('Pasangan model dan TF-IDF tidak valid.') + _cache[version] = (model, vectorizer, meta) + return _cache[version] + + +def seed(): + global _seed_cache + if _seed_cache is not None: + return _seed_cache + frame = pd.read_csv(ARTIFACTS / 'training_seed.csv', keep_default_na=False) + model, vec, meta = bundle('v1') + if len(frame) != meta['total_data'] or not set(frame.label).issubset({0, 1}): + raise ValueError('Dataset awal tidak sesuai metadata penelitian.') + train, test = train_test_split(frame, test_size=meta['split']['test_size'], + random_state=meta['split']['random_state'], stratify=frame.label) + predictions = model.predict(vec.transform(test.stemmed_text)) + if confusion_matrix(test.label, predictions, labels=[0, 1]).tolist() != meta['confusion_matrix']: + raise ValueError('Evaluasi model awal tidak cocok. Training dibatalkan agar data uji tidak berubah.') + _seed_cache = (frame, train, test) + return _seed_cache + + +def history(): + with connect() as db: + active = db.execute("SELECT value FROM settings WHERE key='active'").fetchone()[0] + versions = [dict(json.loads(r['metadata']), created_at=r['created_at'], active=r['id'] == active) + for r in db.execute('SELECT * FROM versions ORDER BY rowid DESC')] + jobs = [dict(r) for r in db.execute('SELECT * FROM jobs ORDER BY rowid DESC LIMIT 20')] + for job in jobs: + job['audit'] = json.loads(job['audit']) if job.get('audit') else None + return dict(active_version=active, versions=versions, jobs=jobs) + + +def activate(version): + bundle(version) # Load and validate before changing the active pointer. + with connect() as db: + db.execute("UPDATE settings SET value=? WHERE key='active'", (version,)) + return history() + + +def queue(file_name): + job_id = uuid4().hex + try: + with connect() as db: + db.execute('INSERT INTO jobs (id, created_at, status, message, version, file_name) VALUES (?, ?, ?, ?, ?, ?)', + (job_id, now(), 'queued', 'Menunggu proses training.', None, file_name[:200])) + except sqlite3.IntegrityError: + raise ValueError('Training lain masih berjalan. Tunggu hingga selesai.') from None + return job_id + + +def train_job(job_id, reviews, selected_column=None): + try: + with connect() as db: + db.execute("UPDATE jobs SET status='running', message=? WHERE id=?", + ('Memeriksa data dan membuat label otomatis...', job_id)) + frame, initial_train, test = seed() + teacher_model, teacher_vec, teacher_meta = bundle() + from model_service import preprocessing_detail + with connect() as db: + old = [dict(r) for r in db.execute('SELECT * FROM additions ORDER BY rowid')] + count = db.execute('SELECT COUNT(*) FROM versions').fetchone()[0] + test_text = {key(x) for x in test.komentar} + test_stem = {key(x) for x in test.stemmed_text} + initial_text = {key(x) for x in initial_train.komentar} + old_text = {r['text_key'] for r in old} + seen_stem = {key(x) for x in frame.stemmed_text} | {key(r['stem']) for r in old} + uploaded_text = set() + additions, skipped = [], dict(duplicate=0, empty=0, unknown=0) + skipped_rows = [] + def skip(row_number, text, category, reason): + skipped[category] += 1 + skipped_rows.append(dict(source_row_number=row_number, text=text, reason=reason)) + for index, raw in enumerate(reviews): + text = raw.strip() + row_number = index + 2 # CSV record number, including the header. + if not text: + skip(row_number, text, 'empty', 'Ulasan kosong') + continue + text_key = key(text) + if text_key in test_text: + skip(row_number, text, 'duplicate', 'Cocok dengan data uji yang dilindungi') + continue + if text_key in initial_text: + skip(row_number, text, 'duplicate', 'Sudah ada dalam data latih awal') + continue + if text_key in old_text: + skip(row_number, text, 'duplicate', 'Sudah ada dalam data tambahan sebelumnya') + continue + if text_key in uploaded_text: + skip(row_number, text, 'duplicate', 'Duplikat ulasan dalam file unggahan') + continue + uploaded_text.add(text_key) + stem = preprocessing_detail(text)['stemming'] + if not stem.strip(): + skip(row_number, text, 'empty', 'Tidak ada kata setelah preprocessing') + continue + if key(stem) in test_stem: + skip(row_number, text, 'duplicate', 'Hasil preprocessing cocok dengan data uji yang dilindungi') + continue + if key(stem) in seen_stem: + skip(row_number, text, 'duplicate', 'Hasil preprocessing sama dengan data yang sudah ada') + continue + features = teacher_vec.transform([stem]) + if not features.nnz: + skip(row_number, text, 'unknown', 'Tidak ada fitur yang dikenali model pemberi label') + continue + label = int(teacher_model.predict(features)[0]) + confidence = float(teacher_model.predict_proba(features)[0].max()) + additions.append(dict(text_key=text_key, text=text, stem=stem, label=label, + confidence=confidence, teacher=teacher_meta['version'])) + seen_stem.add(key(stem)) + audit = dict(uploaded_rows=len(reviews), accepted_rows=len(additions), + skipped_rows=skipped_rows, training_before=len(initial_train)+len(old), selected_column=selected_column) + with connect() as db: + db.execute('UPDATE jobs SET audit=? WHERE id=?', (json.dumps(audit), job_id)) + if not additions: + with connect() as db: + db.execute("UPDATE jobs SET status='skipped', message=? WHERE id=?", + ('Tidak ada data baru yang dapat dilatih. Data latih tetap.', job_id)) + return + texts = initial_train.stemmed_text.tolist() + [r['stem'] for r in old + additions] + labels = initial_train.label.tolist() + [r['label'] for r in old + additions] + with connect() as db: + db.execute('UPDATE jobs SET message=? WHERE id=?', + ('Melatih TF-IDF dan Logistic Regression, lalu mengevaluasi model...', job_id)) + base_model, base_vec, base_meta = bundle('v1') + vec, model = clone(base_vec), clone(base_model) + model.fit(vec.fit_transform(texts), labels) + predicted = model.predict(vec.transform(test.stemmed_text)) + report = classification_report(test.label, predicted, labels=[0, 1], output_dict=True, zero_division=0) + version = f'v{count + 1}' + meta = {k: base_meta[k] for k in ('nama_model', 'label_mapping', 'split', 'preprocessing_order')} + meta.update(version=version, total_data=len(labels) + len(test), + jumlah_data_training=len(labels), jumlah_data_testing=len(test), + jumlah_sentimen_negatif=int(labels.count(0) + (test.label == 0).sum()), + jumlah_sentimen_positif=int(labels.count(1) + (test.label == 1).sum()), + jumlah_fitur_tfidf=len(vec.get_feature_names_out()), + akurasi=float(accuracy_score(test.label, predicted)), + confusion_matrix=confusion_matrix(test.label, predicted, labels=[0, 1]).tolist(), + classification_report=report, teacher_version=teacher_meta['version'], + label_source='Pseudo-labeling (label otomatis dari model aktif)', + added_rows=len(additions), cumulative_added_rows=len(old) + len(additions), + audit=audit, skipped=skipped, test_seed_sha256=hashlib.sha256( + (ARTIFACTS / 'training_seed.csv').read_bytes()).hexdigest(), + training_parameters=model.get_params()) + # Write artifacts first; publish the version and corpus together in one transaction. + folder = STORE / version + folder.mkdir(exist_ok=True) + joblib.dump(model, folder / 'model_logistic_regression.joblib') + joblib.dump(vec, folder / 'tfidf_vectorizer.joblib') + (folder / 'metadata_model.json').write_text(json.dumps(meta, indent=2), encoding='utf-8') + with connect() as db: + db.execute('INSERT INTO versions VALUES (?, ?, ?)', (version, now(), json.dumps(meta))) + db.executemany('INSERT INTO additions VALUES (?, ?, ?, ?, ?, ?, ?)', + [(r['text_key'], r['text'], r['stem'], r['label'], r['confidence'], + r['teacher'], version) for r in additions]) + db.execute("UPDATE jobs SET status='completed', message=?, version=? WHERE id=?", + (f'{len(additions)} ulasan ditambahkan. Model siap ditinjau; model aktif tetap.', version, job_id)) + except Exception as error: + logging.exception('Training failed: %s', job_id) + message = str(error) if isinstance(error, ValueError) else 'Training gagal. Periksa log backend lalu coba kembali.' + with connect() as db: + db.execute("UPDATE jobs SET status='failed', message=? WHERE id=?", (message, job_id)) diff --git a/api_slearn/model_service.py b/api_slearn/model_service.py new file mode 100644 index 0000000..52dfff2 --- /dev/null +++ b/api_slearn/model_service.py @@ -0,0 +1,274 @@ +import json +import importlib.util +from pathlib import Path + +import joblib + +# ============================================================ +# PATH +# ============================================================ + +BASE_DIR = Path(__file__).resolve().parent +ARTIFACT_DIR = BASE_DIR / "artifacts_slearn" + +MODEL_PATH = ARTIFACT_DIR / "model_logistic_regression.joblib" +VECTORIZER_PATH = ARTIFACT_DIR / "tfidf_vectorizer.joblib" +METADATA_PATH = ARTIFACT_DIR / "metadata_model.json" +PREPROCESSING_PATH = ARTIFACT_DIR / "preprocessing_slearn.py" + +# ============================================================ +# CEK FILE ARTEFAK +# ============================================================ + +required_files = [ + MODEL_PATH, + VECTORIZER_PATH, + METADATA_PATH, + PREPROCESSING_PATH +] + +for file_path in required_files: + if not file_path.exists(): + raise FileNotFoundError( + f"File artefak tidak ditemukan: {file_path}" + ) + +# ============================================================ +# LOAD PREPROCESSING ASLI PENELITIAN +# ============================================================ + +def load_preprocessing_module(): + spec = importlib.util.spec_from_file_location( + "preprocessing_slearn_artifact", + PREPROCESSING_PATH + ) + + if spec is None or spec.loader is None: + raise ImportError( + "preprocessing_slearn.py tidak dapat dimuat." + ) + + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + return module + + +preprocessing = load_preprocessing_module() + +# ============================================================ +# LOAD MODEL, TF-IDF, METADATA +# ============================================================ + +model = joblib.load(MODEL_PATH) +vectorizer = joblib.load(VECTORIZER_PATH) + +metadata = json.loads( + METADATA_PATH.read_text(encoding="utf-8") +) + +feature_names = vectorizer.get_feature_names_out() + +# ============================================================ +# VERIFIKASI ARTEFAK +# ============================================================ + +jumlah_fitur_metadata = int( + metadata["jumlah_fitur_tfidf"] +) + +if len(feature_names) != jumlah_fitur_metadata: + raise RuntimeError( + "Jumlah fitur TF-IDF tidak sesuai dengan metadata." + ) + +if model.n_features_in_ != len(feature_names): + raise RuntimeError( + "Jumlah fitur model tidak sesuai dengan TF-IDF." + ) + +if model.classes_.tolist() != [0, 1]: + raise RuntimeError( + "Urutan kelas model harus [0, 1]." + ) + +# ============================================================ +# DETAIL PREPROCESSING +# Menggunakan fungsi yang SAMA dari artefak penelitian +# ============================================================ + +def preprocessing_detail(teks): + if not isinstance(teks, str) or not teks.strip(): + raise ValueError( + "Masukkan ulasan terlebih dahulu." + ) + + original_text = teks.strip() + + # 1. Cleaning + cleaned_text = preprocessing.cleaning( + original_text + ) + + # 2. Case Folding + case_folded_text = preprocessing.case_folding( + cleaned_text + ) + + # 3. Normalisasi + normalized_text = preprocessing.normalisasi( + case_folded_text + ) + + normalized_text = ( + preprocessing.koreksi_normalisasi_validator( + normalized_text + ) + ) + + # 4. Tokenisasi + tokenized_text = preprocessing.tokenisasi( + normalized_text + ) + + # 5. Stopword Removal + stopword_text = preprocessing.hapus_stopword( + tokenized_text + ) + + # 6. Stemming + stemmed_text = preprocessing.stemming( + stopword_text + ) + + return { + "original_text": original_text, + "cleaning": cleaned_text, + "case_folding": case_folded_text, + "normalization": normalized_text, + "tokenizing": tokenized_text, + "stopword_removal": stopword_text, + "stemming": stemmed_text + } + +# ============================================================ +# TF-IDF TERATAS +# ============================================================ + +def get_top_tfidf(fitur, top_n=10, names=None): + names = feature_names if names is None else names + if fitur.nnz == 0: + return [] + + row = fitur.getrow(0) + + data = [] + + for index, bobot in zip( + row.indices, + row.data + ): + data.append({ + "kata": str(names[index]), + "bobot": float(bobot) + }) + + data.sort( + key=lambda item: item["bobot"], + reverse=True + ) + + return data[:top_n] + +# ============================================================ +# ANALISIS SATU TEKS +# ============================================================ + +def analisis_teks(teks, snapshot=None): + from model_registry import bundle + model, vectorizer, metadata = snapshot or bundle() + proses = preprocessing_detail(teks) + + stemmed_text = proses["stemming"] + + if not stemmed_text.strip(): + raise ValueError( + "Ulasan tidak memiliki kata yang dapat diproses." + ) + + # PENTING: + # hanya transform(), TIDAK fit ulang + fitur = vectorizer.transform( + [stemmed_text] + ) + + # Jika tidak ada fitur penelitian yang dikenali, + # Logistic Regression TIDAK dijalankan + if fitur.nnz == 0: + raise ValueError( + "Kata dalam ulasan belum dikenali model." + ) + + label = int( + model.predict(fitur)[0] + ) + + sentiment = metadata[ + "label_mapping" + ][str(label)].capitalize() + + tfidf_top = get_top_tfidf( + fitur, + top_n=10, + names=vectorizer.get_feature_names_out() + ) + + return { + "model_version": metadata['version'], + "teks": proses["original_text"], + "label": label, + "sentimen": sentiment, + "preprocessing": { + "cleaning": proses["cleaning"], + "case_folding": proses["case_folding"], + "normalization": proses["normalization"], + "tokenizing": proses["tokenizing"], + "stopword_removal": proses["stopword_removal"], + "stemming": proses["stemming"] + }, + "jumlah_fitur_dikenali": int( + fitur.nnz + ), + "tfidf_top": tfidf_top + } + +# ============================================================ +# INFORMASI MODEL +# ============================================================ + +def get_model_info(): + from model_registry import bundle + _, _, metadata = bundle() + return { + "version": metadata['version'], + "nama_model": metadata["nama_model"], + "total_data": metadata["total_data"], + "jumlah_sentimen_negatif": + metadata["jumlah_sentimen_negatif"], + "jumlah_sentimen_positif": + metadata["jumlah_sentimen_positif"], + "jumlah_data_training": + metadata["jumlah_data_training"], + "jumlah_data_testing": + metadata["jumlah_data_testing"], + "jumlah_fitur_tfidf": + metadata["jumlah_fitur_tfidf"], + "akurasi": + metadata["akurasi"], + "label_mapping": + metadata["label_mapping"], + "confusion_matrix": + metadata["confusion_matrix"], + "classification_report": + metadata["classification_report"] + } diff --git a/api_slearn/model_store/v2/metadata_model.json b/api_slearn/model_store/v2/metadata_model.json new file mode 100644 index 0000000..e01b2c3 --- /dev/null +++ b/api_slearn/model_store/v2/metadata_model.json @@ -0,0 +1,93 @@ +{ + "nama_model": "Logistic Regression", + "label_mapping": { + "0": "NEGATIF", + "1": "POSITIF" + }, + "split": { + "test_size": 0.2, + "random_state": 42, + "stratify": "label" + }, + "preprocessing_order": [ + "cleaning", + "case_folding", + "normalisasi", + "koreksi_normalisasi_validator", + "tokenisasi", + "hapus_stopword", + "stemming" + ], + "version": "v2", + "total_data": 620, + "jumlah_data_training": 505, + "jumlah_data_testing": 115, + "jumlah_sentimen_negatif": 402, + "jumlah_sentimen_positif": 218, + "jumlah_fitur_tfidf": 675, + "akurasi": 0.9304347826086956, + "confusion_matrix": [ + [ + 71, + 5 + ], + [ + 3, + 36 + ] + ], + "classification_report": { + "0": { + "precision": 0.9594594594594594, + "recall": 0.9342105263157895, + "f1-score": 0.9466666666666667, + "support": 76.0 + }, + "1": { + "precision": 0.8780487804878049, + "recall": 0.9230769230769231, + "f1-score": 0.9, + "support": 39.0 + }, + "accuracy": 0.9304347826086956, + "macro avg": { + "precision": 0.9187541199736322, + "recall": 0.9286437246963564, + "f1-score": 0.9233333333333333, + "support": 115.0 + }, + "weighted avg": { + "precision": 0.9318506205038548, + "recall": 0.9304347826086956, + "f1-score": 0.930840579710145, + "support": 115.0 + } + }, + "teacher_version": "v1", + "label_source": "Pseudo-labeling (label otomatis dari model aktif)", + "added_rows": 49, + "cumulative_added_rows": 49, + "skipped": { + "duplicate": 11, + "empty": 0, + "unknown": 0 + }, + "test_seed_sha256": "79ba16d91783a5652de77c6299ac17313188afe5761645c4895516aa8ae9f105", + "training_parameters": { + "C": 1.0, + "class_weight": null, + "dual": false, + "fit_intercept": true, + "intercept_scaling": 1, + "l1_ratio": null, + "max_iter": 1000, + "multi_class": "deprecated", + "n_jobs": null, + "penalty": "l2", + "random_state": null, + "solver": "lbfgs", + "tol": 0.0001, + "verbose": 0, + "warm_start": false + } +} \ No newline at end of file diff --git a/api_slearn/model_store/v2/model_logistic_regression.joblib b/api_slearn/model_store/v2/model_logistic_regression.joblib new file mode 100644 index 0000000000000000000000000000000000000000..086759fef28bcef1c53400212521a93cf627c56d GIT binary patch literal 6271 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 literal 0 HcmV?d00001 diff --git a/api_slearn/model_store/v2/tfidf_vectorizer.joblib b/api_slearn/model_store/v2/tfidf_vectorizer.joblib new file mode 100644 index 0000000000000000000000000000000000000000..4c3d756bd46c82ef26808adfe2569fd7494a15c9 GIT binary patch literal 13947 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 GbN?T=^?;%P literal 0 HcmV?d00001 diff --git a/api_slearn/model_store/v3/metadata_model.json b/api_slearn/model_store/v3/metadata_model.json new file mode 100644 index 0000000..a84bea1 --- /dev/null +++ b/api_slearn/model_store/v3/metadata_model.json @@ -0,0 +1,100 @@ +{ + "nama_model": "Logistic Regression", + "label_mapping": { + "0": "NEGATIF", + "1": "POSITIF" + }, + "split": { + "test_size": 0.2, + "random_state": 42, + "stratify": "label" + }, + "preprocessing_order": [ + "cleaning", + "case_folding", + "normalisasi", + "koreksi_normalisasi_validator", + "tokenisasi", + "hapus_stopword", + "stemming" + ], + "version": "v3", + "total_data": 670, + "jumlah_data_training": 555, + "jumlah_data_testing": 115, + "jumlah_sentimen_negatif": 427, + "jumlah_sentimen_positif": 243, + "jumlah_fitur_tfidf": 754, + "akurasi": 0.9478260869565217, + "confusion_matrix": [ + [ + 73, + 3 + ], + [ + 3, + 36 + ] + ], + "classification_report": { + "0": { + "precision": 0.9605263157894737, + "recall": 0.9605263157894737, + "f1-score": 0.9605263157894737, + "support": 76.0 + }, + "1": { + "precision": 0.9230769230769231, + "recall": 0.9230769230769231, + "f1-score": 0.9230769230769231, + "support": 39.0 + }, + "accuracy": 0.9478260869565217, + "macro avg": { + "precision": 0.9418016194331984, + "recall": 0.9418016194331984, + "f1-score": 0.9418016194331984, + "support": 115.0 + }, + "weighted avg": { + "precision": 0.9478260869565217, + "recall": 0.9478260869565217, + "f1-score": 0.9478260869565217, + "support": 115.0 + } + }, + "teacher_version": "v2", + "label_source": "Pseudo-labeling (label otomatis dari model aktif)", + "added_rows": 50, + "cumulative_added_rows": 99, + "audit": { + "uploaded_rows": 50, + "accepted_rows": 50, + "skipped_rows": [], + "training_before": 505, + "selected_column": "ulasan" + }, + "skipped": { + "duplicate": 0, + "empty": 0, + "unknown": 0 + }, + "test_seed_sha256": "79ba16d91783a5652de77c6299ac17313188afe5761645c4895516aa8ae9f105", + "training_parameters": { + "C": 1.0, + "class_weight": null, + "dual": false, + "fit_intercept": true, + "intercept_scaling": 1, + "l1_ratio": null, + "max_iter": 1000, + "multi_class": "deprecated", + "n_jobs": null, + "penalty": "l2", + "random_state": null, + "solver": "lbfgs", + "tol": 0.0001, + "verbose": 0, + "warm_start": false + } +} \ No newline at end of file diff --git a/api_slearn/model_store/v3/model_logistic_regression.joblib b/api_slearn/model_store/v3/model_logistic_regression.joblib new file mode 100644 index 0000000000000000000000000000000000000000..6f9697bf12f2666dff55f91955fdfc43c3ed6ec3 GIT binary patch literal 6895 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 literal 0 HcmV?d00001 diff --git a/api_slearn/model_store/v3/tfidf_vectorizer.joblib b/api_slearn/model_store/v3/tfidf_vectorizer.joblib new file mode 100644 index 0000000000000000000000000000000000000000..747a3f964e969b3e7225108b62cd3cac5efc129b GIT binary patch literal 15522 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 K%iAZp+5ZDb6$Qiq literal 0 HcmV?d00001 diff --git a/api_slearn/model_store/v4/metadata_model.json b/api_slearn/model_store/v4/metadata_model.json new file mode 100644 index 0000000..a490b32 --- /dev/null +++ b/api_slearn/model_store/v4/metadata_model.json @@ -0,0 +1,100 @@ +{ + "nama_model": "Logistic Regression", + "label_mapping": { + "0": "NEGATIF", + "1": "POSITIF" + }, + "split": { + "test_size": 0.2, + "random_state": 42, + "stratify": "label" + }, + "preprocessing_order": [ + "cleaning", + "case_folding", + "normalisasi", + "koreksi_normalisasi_validator", + "tokenisasi", + "hapus_stopword", + "stemming" + ], + "version": "v4", + "total_data": 720, + "jumlah_data_training": 605, + "jumlah_data_testing": 115, + "jumlah_sentimen_negatif": 457, + "jumlah_sentimen_positif": 263, + "jumlah_fitur_tfidf": 796, + "akurasi": 0.9304347826086956, + "confusion_matrix": [ + [ + 72, + 4 + ], + [ + 4, + 35 + ] + ], + "classification_report": { + "0": { + "precision": 0.9473684210526315, + "recall": 0.9473684210526315, + "f1-score": 0.9473684210526315, + "support": 76.0 + }, + "1": { + "precision": 0.8974358974358975, + "recall": 0.8974358974358975, + "f1-score": 0.8974358974358975, + "support": 39.0 + }, + "accuracy": 0.9304347826086956, + "macro avg": { + "precision": 0.9224021592442645, + "recall": 0.9224021592442645, + "f1-score": 0.9224021592442645, + "support": 115.0 + }, + "weighted avg": { + "precision": 0.9304347826086956, + "recall": 0.9304347826086956, + "f1-score": 0.9304347826086956, + "support": 115.0 + } + }, + "teacher_version": "v2", + "label_source": "Pseudo-labeling (label otomatis dari model aktif)", + "added_rows": 50, + "cumulative_added_rows": 149, + "audit": { + "uploaded_rows": 50, + "accepted_rows": 50, + "skipped_rows": [], + "training_before": 555, + "selected_column": "ulasan" + }, + "skipped": { + "duplicate": 0, + "empty": 0, + "unknown": 0 + }, + "test_seed_sha256": "79ba16d91783a5652de77c6299ac17313188afe5761645c4895516aa8ae9f105", + "training_parameters": { + "C": 1.0, + "class_weight": null, + "dual": false, + "fit_intercept": true, + "intercept_scaling": 1, + "l1_ratio": null, + "max_iter": 1000, + "multi_class": "deprecated", + "n_jobs": null, + "penalty": "l2", + "random_state": null, + "solver": "lbfgs", + "tol": 0.0001, + "verbose": 0, + "warm_start": false + } +} \ No newline at end of file diff --git a/api_slearn/model_store/v4/model_logistic_regression.joblib b/api_slearn/model_store/v4/model_logistic_regression.joblib new file mode 100644 index 0000000000000000000000000000000000000000..8371ed114e67e79556c161cbcbea57ec0df4a352 GIT binary patch literal 7231 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 H6juK~#Cv{J literal 0 HcmV?d00001 diff --git a/api_slearn/model_store/v5/metadata_model.json b/api_slearn/model_store/v5/metadata_model.json new file mode 100644 index 0000000..9ae575f --- /dev/null +++ b/api_slearn/model_store/v5/metadata_model.json @@ -0,0 +1,100 @@ +{ + "nama_model": "Logistic Regression", + "label_mapping": { + "0": "NEGATIF", + "1": "POSITIF" + }, + "split": { + "test_size": 0.2, + "random_state": 42, + "stratify": "label" + }, + "preprocessing_order": [ + "cleaning", + "case_folding", + "normalisasi", + "koreksi_normalisasi_validator", + "tokenisasi", + "hapus_stopword", + "stemming" + ], + "version": "v5", + "total_data": 770, + "jumlah_data_training": 655, + "jumlah_data_testing": 115, + "jumlah_sentimen_negatif": 482, + "jumlah_sentimen_positif": 288, + "jumlah_fitur_tfidf": 796, + "akurasi": 0.9478260869565217, + "confusion_matrix": [ + [ + 73, + 3 + ], + [ + 3, + 36 + ] + ], + "classification_report": { + "0": { + "precision": 0.9605263157894737, + "recall": 0.9605263157894737, + "f1-score": 0.9605263157894737, + "support": 76.0 + }, + "1": { + "precision": 0.9230769230769231, + "recall": 0.9230769230769231, + "f1-score": 0.9230769230769231, + "support": 39.0 + }, + "accuracy": 0.9478260869565217, + "macro avg": { + "precision": 0.9418016194331984, + "recall": 0.9418016194331984, + "f1-score": 0.9418016194331984, + "support": 115.0 + }, + "weighted avg": { + "precision": 0.9478260869565217, + "recall": 0.9478260869565217, + "f1-score": 0.9478260869565217, + "support": 115.0 + } + }, + "teacher_version": "v4", + "label_source": "Pseudo-labeling (label otomatis dari model aktif)", + "added_rows": 50, + "cumulative_added_rows": 199, + "audit": { + "uploaded_rows": 50, + "accepted_rows": 50, + "skipped_rows": [], + "training_before": 605, + "selected_column": "ulasan" + }, + "skipped": { + "duplicate": 0, + "empty": 0, + "unknown": 0 + }, + "test_seed_sha256": "79ba16d91783a5652de77c6299ac17313188afe5761645c4895516aa8ae9f105", + "training_parameters": { + "C": 1.0, + "class_weight": null, + "dual": false, + "fit_intercept": true, + "intercept_scaling": 1, + "l1_ratio": null, + "max_iter": 1000, + "multi_class": "deprecated", + "n_jobs": null, + "penalty": "l2", + "random_state": null, + "solver": "lbfgs", + "tol": 0.0001, + "verbose": 0, + "warm_start": false + } +} \ No newline at end of file diff --git a/api_slearn/model_store/v5/model_logistic_regression.joblib b/api_slearn/model_store/v5/model_logistic_regression.joblib new file mode 100644 index 0000000000000000000000000000000000000000..2029aa924ba8863f80f9df4097f21a46aa3dbfe2 GIT binary patch literal 7231 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 G75^Vx#wK3? literal 0 HcmV?d00001 diff --git a/api_slearn/model_store/v5/tfidf_vectorizer.joblib b/api_slearn/model_store/v5/tfidf_vectorizer.joblib new file mode 100644 index 0000000000000000000000000000000000000000..68c52482b764359ce29e54fecb162b8e1484ffe7 GIT binary patch literal 16385 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 hf!b=79ZjVA6S0!&NSF-l)@s 505 data latih.') + if not apply: + print('Pemeriksaan saja. Belum ada data yang diubah. Jalankan dengan --apply untuk memulihkan.') + return + # Make an SQLite-consistent recovery copy before changing any rows. + backup = store.parent / ('registry_sebelum_pemulihan_' + datetime.now().strftime('%Y%m%d_%H%M%S_%f') + '.sqlite3') + source = sqlite3.connect(path.as_uri() + '?mode=ro', uri=True) + dest = sqlite3.connect(backup) + try: + source.backup(dest) + finally: + source.close(); dest.close() + db.execute('CREATE TABLE IF NOT EXISTS quarantined_additions (text_key TEXT PRIMARY KEY, record_json TEXT NOT NULL, reason TEXT NOT NULL, quarantined_at TEXT NOT NULL)') + stamp = datetime.now(timezone.utc).isoformat() + for row in rows: + db.execute('INSERT INTO quarantined_additions VALUES (?,?,?,?)', (row['text_key'], json.dumps(row), REASON, stamp)) + db.execute('DELETE FROM additions WHERE text_key=? AND version=?', (row['text_key'], 'v3')) + versions['v3'].update(invalidated=True, invalid_reason=REASON, invalidated_at=stamp) + db.execute("UPDATE versions SET metadata=? WHERE id='v3'", (json.dumps(versions['v3']),)) + db.execute("UPDATE settings SET value='v2' WHERE key='active'") + db.execute("UPDATE jobs SET message=? WHERE version='v3'", ('Model v3 tidak valid: input prodi. Empat data telah dikarantina; kumpulan training dipulihkan.',)) + db.commit() + print('Selesai: 4 data dikarantina, kumpulan data latih 505, v2 aktif, v3 ditandai tidak valid.') + print('Berkas model dan riwayat analisis tidak dihapus. Cadangan tambahan:', backup) + finally: + db.close() + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--store', type=Path, default=Path(__file__).resolve().parent / 'model_store') + parser.add_argument('--apply', action='store_true') + args = parser.parse_args() + try: + repair(args.store.resolve(), args.apply) + except (ValueError, sqlite3.Error) as error: + raise SystemExit(str(error)) diff --git a/api_slearn/requirements.txt b/api_slearn/requirements.txt new file mode 100644 index 0000000..ed64fd3 --- /dev/null +++ b/api_slearn/requirements.txt @@ -0,0 +1,11 @@ +fastapi +uvicorn[standard] +python-multipart +mysql-connector-python +scikit-learn==1.6.1 +numpy==2.1.3 +scipy==1.16.3 +pandas==2.2.3 +nltk==3.9.1 +Sastrawi==1.0.1 +joblib==1.6.0 \ No newline at end of file diff --git a/api_slearn/tests/test_retraining.py b/api_slearn/tests/test_retraining.py new file mode 100644 index 0000000..18084dc --- /dev/null +++ b/api_slearn/tests/test_retraining.py @@ -0,0 +1,87 @@ +"""Run: python -B -m unittest discover -s tests -v (uses a temporary model store).""" +import hashlib +import os +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch + +_temporary = tempfile.TemporaryDirectory() +os.environ['SLEARN_MODEL_STORE'] = _temporary.name + +import model_registry as registry +from model_service import analisis_teks +from main import TrainingRequest, training +from fastapi import BackgroundTasks, HTTPException + + +class RetrainingTests(unittest.TestCase): + def test_full_lifecycle(self): + baseline = registry.bundle('v1') + frame, train, test = registry.seed() + self.assertEqual((len(train), len(test)), (456, 115)) + self.assertAlmostEqual(baseline[2]['akurasi'], 0.9304347826086956) + original_hash = hashlib.sha256((registry.ARTIFACTS / 'model_logistic_regression.joblib').read_bytes()).hexdigest() + text = 'Aplikasi bagus sangat membantu belajar dengan materi menarik terbaru sekali' + before = analisis_teks(text) + queued = registry.queue('contoh.csv') + with self.assertRaises(ValueError): + registry.queue('duplikat-proses.csv') + registry.train_job(queued, [text, text, test.iloc[0].komentar, '', '123 !!!', 'zzqqxxvv']) + state = registry.history() + self.assertEqual(state['jobs'][0]['status'], 'completed', state['jobs'][0]['message']) + self.assertEqual(state['active_version'], 'v1') + self.assertEqual(state['versions'][0]['jumlah_data_training'], 457) + self.assertEqual(state['versions'][0]['skipped'], {'duplicate': 2, 'empty': 2, 'unknown': 1}) + self.assertEqual(state['versions'][0]['teacher_version'], 'v1') + self.assertEqual(state['versions'][0]['jumlah_data_testing'], 115) + registry.activate('v2') + self.assertEqual(analisis_teks(text)['model_version'], 'v2') + # Old snapshots remain internally consistent during activation. + self.assertEqual(analisis_teks(text, snapshot=baseline), before) + registry._cache.clear() + self.assertEqual(registry.bundle()[2]['version'], 'v2') + registry.activate('v1') + self.assertEqual(analisis_teks(text), before) + self.assertEqual(original_hash, hashlib.sha256((registry.ARTIFACTS / 'model_logistic_regression.joblib').read_bytes()).hexdigest()) + # Training after rollback still includes all previously committed additions. + job = registry.queue('tambahan.csv') + registry.train_job(job, ['Website lambat sering error saat mengumpulkan tugas malam terbaru']) + self.assertEqual(registry.history()['jobs'][0]['status'], 'completed') + self.assertEqual(registry.history()['versions'][0]['jumlah_data_training'], 458) + with self.assertRaises(ValueError): + registry.activate('../../invalid') + self.assertEqual(registry.history()['active_version'], 'v1') + failed = registry.queue('hanya-data-lama.csv') + registry.train_job(failed, [text, test.iloc[1].komentar]) + state = registry.history() + self.assertEqual(state['jobs'][0]['status'], 'skipped') + self.assertEqual(len(state['versions']), 3) + with registry.connect() as db: + self.assertEqual(db.execute('SELECT COUNT(*) FROM additions').fetchone()[0], 2) + failed_fit = registry.queue('gagal-fit.csv') + with patch('sklearn.linear_model.LogisticRegression.fit', side_effect=RuntimeError('simulated failure')): + registry.train_job(failed_fit, ['Materi pembelajaran bagus membantu mahasiswa mengerjakan latihan unik']) + self.assertEqual(registry.history()['jobs'][0]['status'], 'failed') + self.assertEqual(len(registry.history()['versions']), 3) + with registry.connect() as db: + self.assertEqual(db.execute('SELECT COUNT(*) FROM additions').fetchone()[0], 2) + registry.queue('terputus.csv') + registry.initialize() + self.assertEqual(registry.history()['jobs'][0]['status'], 'failed') + + def test_request_validation(self): + for reviews in [[' '], ['x' * 10001]]: + with self.assertRaises(HTTPException): + training(TrainingRequest(file_name='invalid.csv', reviews=reviews, selected_column='ulasan', confirmed_review_column=True), BackgroundTasks()) + + def test_predict_route_uses_active_model(self): + from main import predict, TextRequest + with patch('main.save_single_prediction', return_value=123): + response = predict(TextRequest(text='Aplikasi bagus membantu belajar')) + self.assertEqual(response['data']['prediction_id'], 123) + self.assertEqual(response['data']['model_version'], registry.history()['active_version']) + + +if __name__ == '__main__': + unittest.main() diff --git a/api_slearn/training_input.py b/api_slearn/training_input.py new file mode 100644 index 0000000..94c43d5 --- /dev/null +++ b/api_slearn/training_input.py @@ -0,0 +1,14 @@ +"""Column metadata checks; content must still be reviewed by the administrator.""" +import re + +IDENTITY = {'no', 'nomor', 'id', 'nama', 'name', 'nim', 'nis', 'nip', 'nik', + 'prodi', 'jurusan', 'fakultas', 'gender', 'kelamin', 'email', + 'telepon', 'phone', 'alamat', 'address', 'angkatan', 'semester', + 'label', 'sentimen', 'sentiment'} + +def validate_column(column): + words = set(re.sub(r'[^a-z0-9]+', ' ', column.casefold()).split()) + compact = ''.join(sorted(words)) + if not words or words & IDENTITY or {'program', 'studi'} <= words or compact in {'jeniskelamin', 'programstudi'}: + raise ValueError('Pilih kolom berisi ulasan, bukan kolom identitas atau label.') + return column.strip() diff --git a/web_slearn/.gitignore b/web_slearn/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/web_slearn/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/web_slearn/README.md b/web_slearn/README.md new file mode 100644 index 0000000..a36934d --- /dev/null +++ b/web_slearn/README.md @@ -0,0 +1,16 @@ +# React + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. diff --git a/web_slearn/backup-pembanding-20261028-175022-231/src/pages/PenelitianModel.jsx b/web_slearn/backup-pembanding-20261028-175022-231/src/pages/PenelitianModel.jsx new file mode 100644 index 0000000..820b279 --- /dev/null +++ b/web_slearn/backup-pembanding-20261028-175022-231/src/pages/PenelitianModel.jsx @@ -0,0 +1,1821 @@ +import { useEffect, useMemo, useRef, useState } from "react"; +import Papa from "papaparse"; +import { ambilModelPenelitian } from "../services/api"; +import "../styles/penelitian-model.css"; + +const tabs = [ + "Dataset & Pembagian Data", + "Preprocessing", + "Pembobotan TF-IDF", + "Model & Evaluasi" +]; + +const ITEMS_PER_PAGE = 10; + + +function PenelitianModel() { + const [activeTab, setActiveTab] = useState("Dataset & Pembagian Data"); + + return ( +
+
+
+

Penelitian & Model Awal

+ +

+ Dokumentasi dataset, preprocessing, TF-IDF, dan evaluasi model awal (v1). + Model aktif dan hasil training baru tersedia di menu Training & Riwayat Model.{" "} + +

+
+
+ +
+ {tabs.map((tab) => ( + + ))} +
+ + {activeTab === "Dataset & Pembagian Data" && } + {activeTab === "Preprocessing" && } + {activeTab === "Pembobotan TF-IDF" && } + {activeTab === "Model & Evaluasi" && } +
+ ); +} + +/* ============================================================ + DATASET + ============================================================ */ + +function DatasetTab() { + const [classDistribution, setClassDistribution] = useState({ + total: 571, + negative: 379, + positive: 192 + }); + + const [distributionLoading, setDistributionLoading] = useState(true); + const [distributionError, setDistributionError] = useState(""); + + useEffect(() => { + let aktif = true; + + Papa.parse("/data/hasil_preprocessing_slearn.csv", { + download: true, + header: true, + skipEmptyLines: true, + + transformHeader: (header) => + String(header).replace(/^\uFEFF/, "").trim(), + + complete: (results) => { + if (!aktif) return; + + const rows = (results.data || []).filter( + (row) => String(row.komentar || "").trim() !== "" + ); + + const negative = rows.filter( + (row) => String(row.label ?? "").trim() === "0" + ).length; + + const positive = rows.filter( + (row) => String(row.label ?? "").trim() === "1" + ).length; + + setClassDistribution({ + total: rows.length, + negative, + positive + }); + + setDistributionLoading(false); + }, + + error: (err) => { + if (!aktif) return; + + setDistributionError( + err?.message || "Distribusi kelas tidak dapat dimuat." + ); + + setDistributionLoading(false); + } + }); + + return () => { + aktif = false; + }; + }, []); + + const negativePercentage = + classDistribution.total > 0 + ? (classDistribution.negative / classDistribution.total) * 100 + : 0; + + const positivePercentage = + classDistribution.total > 0 + ? (classDistribution.positive / classDistribution.total) * 100 + : 0; + + const formatPercentage = (value) => + Number(value).toFixed(1).replace(".", ","); + + return ( +
+
+ + + + + +
+ +
+
+
+

Distribusi Kelas Dataset

+

Distribusi label pada 571 ulasan penelitian.

+
+ + {distributionLoading ? ( +
+ Memuat distribusi kelas... +
+ ) : ( + <> +
+
+
+
+ + +
+ Negatif + Label 0 +
+
+ +
+ {classDistribution.negative} + {formatPercentage(negativePercentage)}% +
+
+ +
+ +
+
+ +
+
+
+ + +
+ Positif + Label 1 +
+
+ +
+ {classDistribution.positive} + {formatPercentage(positivePercentage)}% +
+
+ +
+ +
+
+
+ +
+ 0 = Negatif + 1 = Positif +
+ + {distributionError && ( +

+ {distributionError} +

+ )} + + )} +
+ +
+
+

Pembagian Training dan Testing

+ +

+ Dataset dibagi menjadi 80% data training dan 20% data testing. +

+
+ +
+
+ 80% + 20% +
+ +

+ Data training digunakan untuk membentuk fitur TF-IDF dan melatih + model, sedangkan data testing digunakan untuk evaluasi model. +

+
+
+
+ +
+
+

Alur Pengolahan Data Penelitian

+
+ +
+ + + + + + + + + + + + + + + + + + + + + +
+
+
+ ); +} + +/* ============================================================ + PREPROCESSING + ============================================================ */ + +function formatTokenizedText(value) { + const text = String(value || "").trim(); + + if (!text) return "-"; + + try { + const parsed = JSON.parse(text.replace(/'/g, '"')); + + if (Array.isArray(parsed)) { + return parsed.join(" | "); + } + } catch { + // Format fallback. + } + + return text + .replace(/^\[/, "") + .replace(/\]$/, "") + .split(",") + .map((token) => + token + .trim() + .replace(/^["']|["']$/g, "") + ) + .filter(Boolean) + .join(" | "); +} + +function PreprocessingTab() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + const [search, setSearch] = useState(""); + const [filterLabel, setFilterLabel] = useState("Semua"); + const [currentPage, setCurrentPage] = useState(1); + const [detail, setDetail] = useState(null); + + const preprocessingResultRef = useRef(null); + + useEffect(() => { + let aktif = true; + + Papa.parse("/data/hasil_preprocessing_slearn.csv", { + download: true, + header: true, + skipEmptyLines: true, + + transformHeader: (header) => + String(header).replace(/^\uFEFF/, "").trim(), + + complete: (results) => { + if (!aktif) return; + + const rows = (results.data || []) + .filter( + (row) => String(row.komentar || "").trim() !== "" + ) + .map((row, index) => ({ + id: index + 1, + komentar: String(row.komentar || ""), + label: String(row.label ?? "").trim(), + clean_text: String(row.clean_text || ""), + case_folded_text: String(row.case_folded_text || ""), + normalized_text: String(row.normalized_text || ""), + tokenized_text: String(row.tokenized_text || ""), + stopword_text: String(row.stopword_text || ""), + stemmed_text: String(row.stemmed_text || "") + })); + + setData(rows); + setLoading(false); + }, + + error: (err) => { + if (!aktif) return; + + setError( + err?.message || "Hasil preprocessing tidak dapat dibaca." + ); + + setLoading(false); + } + }); + + return () => { + aktif = false; + }; + }, []); + + useEffect(() => { + setCurrentPage(1); + }, [search, filterLabel]); + + useEscapeModal(detail, () => setDetail(null)); + + const filteredData = useMemo(() => { + const keyword = search.trim().toLowerCase(); + + return data.filter((item) => { + const cocokSearch = + !keyword || + item.komentar.toLowerCase().includes(keyword) || + item.clean_text.toLowerCase().includes(keyword) || + item.case_folded_text.toLowerCase().includes(keyword) || + item.normalized_text.toLowerCase().includes(keyword) || + item.tokenized_text.toLowerCase().includes(keyword) || + item.stopword_text.toLowerCase().includes(keyword) || + item.stemmed_text.toLowerCase().includes(keyword) || + String(item.id).includes(keyword); + + const cocokLabel = + filterLabel === "Semua" || + (filterLabel === "Positif" && item.label === "1") || + (filterLabel === "Negatif" && item.label === "0"); + + return cocokSearch && cocokLabel; + }); + }, [data, search, filterLabel]); + + const totalPages = Math.max( + 1, + Math.ceil(filteredData.length / ITEMS_PER_PAGE) + ); + + const safePage = Math.min(currentPage, totalPages); + const startIndex = (safePage - 1) * ITEMS_PER_PAGE; + + const paginatedData = filteredData.slice( + startIndex, + startIndex + ITEMS_PER_PAGE + ); + + const scrollKeAtasPreprocessing = () => { + window.requestAnimationFrame(() => { + if (!preprocessingResultRef.current) return; + + const posisi = + preprocessingResultRef.current.getBoundingClientRect().top + + window.scrollY - + 18; + + window.scrollTo({ + top: posisi, + behavior: "smooth" + }); + }); + }; + + const halamanSebelumnyaPreprocessing = () => { + setCurrentPage((page) => Math.max(1, page - 1)); + scrollKeAtasPreprocessing(); + }; + + const halamanBerikutnyaPreprocessing = () => { + setCurrentPage((page) => Math.min(totalPages, page + 1)); + scrollKeAtasPreprocessing(); + }; + + const kataPenting = [ + "tidak", + "kurang", + "belum", + "bukan", + "bukannya", + "jangan", + "tanpa", + "tidaklah", + "malah", + "bisa", + "sangat", + "sering", + "sudah", + "masih", + "terlalu", + "baik", + "lebih", + "lama", + "jelas", + "masalah", + "selalu", + "siap", + "dapat", + "guna", + "benar", + "cukup", + "sekali", + "padahal", + "perlu", + "banyak", + "dan", + "terlihat", + "waktu", + "tambah", + "sebelum", + "sebelumnya", + "dekat", + "tiap", + "ada", + "kecil", + "dulu" + ]; + + return ( +
+
+
+
+

Contoh Kamus Normalisasi

+ +

+ Contoh perubahan kata yang digunakan pada proses normalisasi + data penelitian. +

+
+ +
+ + + +
+
+ +
+
+

Kata Penting yang Dipertahankan

+ +

+ Stopword final dibentuk dari stopword Bahasa Indonesia NLTK dan + partikel tambahan. Kata-kata berikut dikeluarkan dari daftar + stopword sehingga tetap dipertahankan selama preprocessing. +

+
+ +
+ {kataPenting.map((kata) => ( + {kata} + ))} +
+
+
+ +
+
+

Preprocessing 571 Ulasan

+ +

+ Hasil preprocessing ditampilkan sesuai enam tahapan penelitian: + Cleaning, Case Folding, Normalisasi, Tokenizing, Stopword Removal, + dan Stemming. +

+
+ +
+ setSearch(event.target.value)} + /> + + +
+ + {loading && ( +
+ Memuat hasil preprocessing penelitian... +
+ )} + + {!loading && error && ( +
+ {error} +
+ )} + + {!loading && !error && ( + <> +
+ {paginatedData.map((item) => ( +
+
+
+ Data ke-{item.id} + +
+ +

{item.komentar}

+ +
+
+ Hasil Akhir Stemming + {item.stemmed_text} +
+ + +
+
+
+ ))} + + {paginatedData.length === 0 && ( +
+ Tidak ada data yang sesuai. +
+ )} +
+ + + + )} +
+ + {detail && ( +
+ event.target === event.currentTarget && + setDetail(null) + } + > +
+
+
+
+

Data ke-{detail.id}

+
+ + +
+ +
+
+ Ulasan Asli + +
+ +

{detail.komentar}

+
+ +
+ + + + + + + + + + + +
+ +
+ Sentimen Penelitian + + + {detail.label === "1" ? "Positif" : "Negatif"} + +
+
+
+
+ )} +
+ ); +} + +/* ============================================================ + TF-IDF + ============================================================ */ + +function TFIDFTab() { + const [summary, setSummary] = useState(null); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + const [search, setSearch] = useState(""); + const [filterJenis, setFilterJenis] = useState("Semua"); + const [filterLabel, setFilterLabel] = useState("Semua"); + const [currentPage, setCurrentPage] = useState(1); + const [detail, setDetail] = useState(null); + + const tfidfResultRef = useRef(null); + + useEffect(() => { + const controller = new AbortController(); + + async function loadTFIDF() { + try { + setLoading(true); + setError(""); + + const response = await fetch("/data/tfidf_research.json", { + signal: controller.signal + }); + + if (!response.ok) { + throw new Error( + "Data pembobotan TF-IDF tidak dapat dibaca." + ); + } + + const json = await response.json(); + + if (!json || !Array.isArray(json.data)) { + throw new Error("Format data TF-IDF tidak sesuai."); + } + + setSummary(json.summary || null); + setData(json.data); + } catch (err) { + if (err.name !== "AbortError") { + setError( + err.message || "Data TF-IDF tidak dapat dimuat." + ); + } + } finally { + if (!controller.signal.aborted) { + setLoading(false); + } + } + } + + loadTFIDF(); + + return () => controller.abort(); + }, []); + + useEffect(() => { + setCurrentPage(1); + }, [search, filterJenis, filterLabel]); + + useEscapeModal(detail, () => setDetail(null)); + + const filteredData = useMemo(() => { + const keyword = search.trim().toLowerCase(); + + return data.filter((item) => { + const bobotKata = (item.bobot_tfidf || []) + .map((x) => x.kata) + .join(" ") + .toLowerCase(); + + const cocokSearch = + !keyword || + String(item.no_data).includes(keyword) || + String(item.ulasan_asli || "") + .toLowerCase() + .includes(keyword) || + String(item.stemmed_text || "") + .toLowerCase() + .includes(keyword) || + bobotKata.includes(keyword); + + const cocokJenis = + filterJenis === "Semua" || + item.jenis_data === filterJenis; + + const cocokLabel = + filterLabel === "Semua" || + (filterLabel === "Positif" && + Number(item.label) === 1) || + (filterLabel === "Negatif" && + Number(item.label) === 0); + + return cocokSearch && cocokJenis && cocokLabel; + }); + }, [data, search, filterJenis, filterLabel]); + + const totalPages = Math.max( + 1, + Math.ceil(filteredData.length / ITEMS_PER_PAGE) + ); + + const safePage = Math.min(currentPage, totalPages); + const startIndex = (safePage - 1) * ITEMS_PER_PAGE; + + const paginatedData = filteredData.slice( + startIndex, + startIndex + ITEMS_PER_PAGE + ); + + const scrollKeAtasHasilTFIDF = () => { + window.requestAnimationFrame(() => { + if (!tfidfResultRef.current) return; + + const posisi = + tfidfResultRef.current.getBoundingClientRect().top + + window.scrollY - + 18; + + window.scrollTo({ + top: posisi, + behavior: "smooth" + }); + }); + }; + + const halamanSebelumnyaTFIDF = () => { + setCurrentPage((page) => Math.max(1, page - 1)); + scrollKeAtasHasilTFIDF(); + }; + + const halamanBerikutnyaTFIDF = () => { + setCurrentPage((page) => Math.min(totalPages, page + 1)); + scrollKeAtasHasilTFIDF(); + }; + + return ( +
+
+ + + + + +
+ +
+
+

Hasil Pembobotan TF-IDF 571 Ulasan

+ +

+ Bobot TF-IDF dihitung menggunakan fitur yang dibentuk dari + data training. Data testing ditransformasikan menggunakan + vectorizer yang sama tanpa melakukan fit ulang. +

+
+ +
+ setSearch(event.target.value)} + /> + + + + +
+ + {loading && ( +
+ Memuat hasil pembobotan TF-IDF penelitian... +
+ )} + + {!loading && error && ( +
+ {error} +
+ )} + + {!loading && !error && ( + <> +
+ {paginatedData.map((item) => ( + setDetail(item)} + /> + ))} + + {paginatedData.length === 0 && ( +
+ Tidak ada data TF-IDF yang sesuai dengan pencarian atau + filter. +
+ )} +
+ + + + )} +
+ +
+
+

Cara Pembentukan Fitur TF-IDF

+ +

+ TF-IDF dibentuk menggunakan 456 data training dan menghasilkan + 675 fitur. Bobot akhir merupakan hasil TF × IDF, sedangkan data + testing ditransformasikan menggunakan vectorizer dari data training. +

+
+ +
+
+ Data Training + 456 Ulasan +
+ +
→
+ +
+ Pembobotan + TF × IDF +
+ +
→
+ +
+ Hasil + 675 Fitur +
+
+
+ + {detail && ( + setDetail(null)} + /> + )} +
+ ); +} + +function TFIDFResearchCard({ item, onDetail }) { + const topWeights = (item.bobot_tfidf || []).slice(0, 5); + + const maxWeight = + topWeights.reduce( + (maksimum, weight) => + Math.max( + maksimum, + Number(weight.bobot) || 0 + ), + 0 + ) || 1; + + return ( +
+
+ + Data ke-{item.no_data} + + +
+ + {item.jenis_data} + + + +
+
+ +
+ Hasil Stemming + {item.stemmed_text} +
+ +
+
+ Bobot TF-IDF Tertinggi + + + {item.jumlah_fitur_nonzero} fitur bernilai > 0 + +
+ + {topWeights.length > 0 ? ( + topWeights.map((weight) => { + const nilaiBobot = Number(weight.bobot) || 0; + + const width = Math.min( + 100, + Math.max( + 0, + (nilaiBobot / maxWeight) * 100 + ) + ); + + return ( +
+ {weight.kata} + +
+ +
+ + + {formatDecimal(weight.bobot)} + +
+ ); + }) + ) : ( +
+ Tidak ada fitur yang dikenali vectorizer. +
+ )} +
+ +
+ +
+
+ ); +} + +function TFIDFDetailModal({ item, onClose }) { + return ( +
+ event.target === event.currentTarget && + onClose() + } + > +
+
+
+
+

Data ke-{item.no_data}

+
+ + +
+ +
+
+ Ulasan Asli +
+ +

{item.ulasan_asli}

+
+ +
+
+ Jenis Data + {item.jenis_data} +
+ +
+ Posisi pada Split + + {item.jenis_data} ke-{item.urutan_split} + +
+ +
+ Fitur Bobot > 0 + {item.jumlah_fitur_nonzero} +
+ +
+ Sentimen + + + {Number(item.label) === 1 + ? "Positif" + : "Negatif"} + +
+
+ +
+ Hasil Stemming + {item.stemmed_text} +
+ +
+

+ Seluruh Fitur dengan Bobot TF-IDF > 0 +

+
+ +
+ + + + + + + + + + + + + + {(item.bobot_tfidf || []).map( + (row, index) => ( + + + + + + + + + + + + + ) + )} + + {(item.bobot_tfidf || []).length === 0 && ( + + + + )} + +
No.KataTFDFIDFBobot TF-IDF
{index + 1} + {row.kata} + {row.tf}{row.df} + {formatDecimal(row.idf)} + + {formatDecimal(row.bobot)} +
+ Tidak ada fitur dengan bobot TF-IDF > 0. +
+
+ +
+ i + +
+ Cara Membaca Bobot + +

+ TF menunjukkan frekuensi term pada satu dokumen, DF menunjukkan + jumlah dokumen training yang mengandung term, sedangkan IDF + menunjukkan bobot berdasarkan kemunculan term pada dokumen + training. Bobot TF-IDF pada tabel merupakan hasil TF × IDF. +

+
+
+
+
+
+ ); +} + +/* ============================================================ + MODEL & EVALUASI + ============================================================ */ + +function ModelTab() { + const [modelInfo, setModelInfo] = useState(null); + const [error, setError] = useState(""); + + useEffect(() => { + let active = true; + async function loadModel() { + try { + const response = await ambilModelPenelitian(); + const data = response.data; + const report = data?.classification_report; + const cm = data?.confusion_matrix; + const validRate = value => typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1; + if (!data || !validRate(data.akurasi) || + !Number.isInteger(data.jumlah_data_testing) || data.jumlah_data_testing <= 0 || + !Array.isArray(cm) || cm.length !== 2 || + !cm.every(row => Array.isArray(row) && row.length === 2 && row.every(value => Number.isInteger(value) && value >= 0)) || + cm.flat().reduce((sum, value) => sum + value, 0) !== data.jumlah_data_testing || + !["0", "1", "weighted avg"].every(key => + report?.[key] && ["precision", "recall", "f1-score"].every(metric => validRate(report[key][metric])) && + Number.isFinite(report[key].support))) { + throw new Error("Metadata evaluasi dari backend tidak lengkap atau tidak valid."); + } + if (active) setModelInfo(data); + } catch (err) { + if (active) setError(err.message || "Gagal memuat evaluasi model."); + } + } + loadModel(); + return () => { active = false; }; + }, []); + + if (error) return ( +
+
+

Evaluasi model 80:20 belum dapat ditampilkan

+

{error}

+

Pastikan backend berjalan. Buka tab lain lalu kembali ke Model & Evaluasi untuk mencoba lagi.

+
+ +
+ ); + if (!modelInfo) return
Memuat evaluasi model dari backend...
; + + const report = modelInfo.classification_report; + const weighted = report["weighted avg"]; + const [[tn, fp], [fn, tp]] = modelInfo.confusion_matrix; + const percent = (value, digits = 2) => (value * 100).toLocaleString("id-ID", { + minimumFractionDigits: digits, maximumFractionDigits: digits + }) + "%"; + const evaluationMetrics = [ + { name: "Accuracy", value: percent(modelInfo.akurasi), description: "Ketepatan prediksi pada " + modelInfo.jumlah_data_testing + " data testing" }, + { name: "Precision", value: percent(weighted.precision), description: "Weighted average seluruh kelas" }, + { name: "Recall", value: percent(weighted.recall), description: "Weighted average seluruh kelas" }, + { name: "F1-Score", value: percent(weighted["f1-score"]), description: "Weighted average seluruh kelas" } + ]; + return ( +
+
+
+

Evaluasi Model Awal — v1

+ +

+ Evaluasi dilakukan terhadap {modelInfo.jumlah_data_testing} data testing. Nilai Precision, + Recall, dan F1-Score berikut menggunakan weighted average. +

+
+ +
+ {evaluationMetrics.map((metric) => ( + + ))} +
+
+ +
+
+
+

Confusion Matrix

+ +

+ Perbandingan antara label aktual dengan hasil prediksi model + pada data testing. +

+
+ +
+
+ Prediksi Model +
+ +
+
+ +
+ Negatif +
+ +
+ Positif +
+ +
+ Negatif Aktual +
+ + + + + +
+ Positif Aktual +
+ + + + +
+ +
+ + + Prediksi benar + + + + + Prediksi salah + +
+
+
+ +
+
+

Evaluasi per Kelas

+ +

+ Precision, Recall, dan F1-Score masing-masing kelas pada + {modelInfo.jumlah_data_testing} data testing. +

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KelasPrecisionRecallF1-ScoreJumlah
+ + Negatif + + {percent(report["0"].precision, 1)}{percent(report["0"].recall, 1)}{percent(report["0"]["f1-score"], 1)}{report["0"].support}
+ + Positif + + {percent(report["1"].precision, 1)}{percent(report["1"].recall, 1)}{percent(report["1"]["f1-score"], 1)}{report["1"].support}
+
+ +
+ i + +

+ Recall kelas positif sebesar {percent(report["1"].recall, 1)} menunjukkan bahwa {tp} dari{" "} + {report["1"].support} ulasan positif pada data testing berhasil diprediksi sebagai + positif, sedangkan {fn} lainnya diprediksi sebagai negatif. +

+
+
+
+ +
+ ); +} + +/* ============================================================ + PERBANDINGAN TIGA RASIO PENELITIAN + Sumber: public/data/perbandingan_rasio_slearn.json + ============================================================ */ + +function PerbandinganRasio() { + const [skenario, setSkenario] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + + useEffect(() => { + const controller = new AbortController(); + async function loadPerbandingan() { + try { + const response = await fetch("/data/perbandingan_rasio_slearn.json", { signal: controller.signal }); + if (!response.ok) throw new Error("File perbandingan_rasio_slearn.json tidak ditemukan."); + const json = await response.json(); + const daftar = json?.skenario; + const rasioWajib = ["70:30", "80:20", "90:10"]; + const valid = Array.isArray(daftar) && daftar.length === 3 && + rasioWajib.every(rasio => daftar.some(item => item.rasio === rasio)) && + daftar.every(item => + Number.isInteger(item.data_latih) && Number.isInteger(item.data_uji) && + Number.isInteger(item.jumlah_fitur) && item.data_latih + item.data_uji === 571 && + item.jumlah_fitur > 0 && + ["accuracy", "precision", "recall", "f1_score"].every(k => + typeof item[k] === "number" && Number.isFinite(item[k]) && item[k] >= 0 && item[k] <= 100) && + Array.isArray(item.confusion_matrix) && item.confusion_matrix.length === 2 && + item.confusion_matrix.every(row => Array.isArray(row) && row.length === 2 && + row.every(n => Number.isInteger(n) && n >= 0)) && + item.confusion_matrix.flat().reduce((total, n) => total + n, 0) === item.data_uji + ); + if (!valid) throw new Error("Struktur atau nilai data perbandingan tiga rasio tidak sesuai."); + if (!controller.signal.aborted) { + setSkenario(rasioWajib.map(rasio => daftar.find(item => item.rasio === rasio))); + setError(""); + } + } catch (err) { + if (err.name !== "AbortError") setError(err.message || "Gagal membaca perbandingan rasio."); + } finally { + if (!controller.signal.aborted) setLoading(false); + } + } + loadPerbandingan(); + return () => controller.abort(); + }, []); + + const persen = angka => Number(angka).toLocaleString("id-ID", { + minimumFractionDigits: 2, maximumFractionDigits: 2 + }) + "%"; + + return ( +
+
+

Perbandingan Tiga Rasio Data Training dan Testing

+

Hasil eksperimen 70:30, 80:20, dan 90:10 menggunakan Logistic Regression dan TF-IDF.

+
+ + {loading &&

Memuat hasil perbandingan rasio...

} + {!loading && error &&

{error}

} + + {!loading && !error && ( + <> +
+ + + + + + + + + {skenario.map(item => ( + + + + + + + ))} + +
RasioData LatihData UjiFitur TF-IDFAccuracyPrecisionRecallF1-Score
{item.rasio}{item.rasio === "80:20" &&
Model utama
}
{item.data_latih}{item.data_uji}{item.jumlah_fitur}{persen(item.accuracy)}{persen(item.precision)}{persen(item.recall)}{persen(item.f1_score)}
+
+ +
+

Grafik Perbandingan Akurasi

+
+
+
+ {[100, 75, 50, 25, 0].map(nilai => {nilai}%)} +
+
+
+ {[0, 25, 50, 75, 100].map(nilai => ( +
+ ))} +
+ {skenario.map(item => ( +
+ {persen(item.accuracy)} +
+ ))} +
+
+
+ {skenario.map(item => ( + {item.rasio} + ))} +
+
+
+
+
+ +

Confusion Matrix per Rasio

+
+ {skenario.map(item => { + const [[tn, fp], [fn, tp]] = item.confusion_matrix; + return ( +
+ Rasio {item.rasio} + + + + + + +
Aktual / PrediksiNegatifPositif
Negatif{tn} (TN){fp} (FP)
Positif{fn} (FN){tp} (TP)
+
+ ); + })} +
+ +
+ i +
+ Catatan Evaluasi +

Precision, Recall, dan F1-Score menggunakan weighted average. Setiap rasio memiliki jumlah data uji berbeda sehingga hasilnya tidak langsung menunjukkan satu rasio selalu lebih baik. Model utama untuk prediksi website tetap rasio 80:20 dengan akurasi 93,04%; eksperimen rasio lain hanya ditampilkan sebagai pembanding.

+
+
+ + )} +
+ ); +} + +/* ============================================================ + COMPONENT + ============================================================ */ + +function useEscapeModal(value, close) { + useEffect(() => { + if (!value) return; + + const handleEscape = (event) => { + if (event.key === "Escape") { + close(); + } + }; + + document.addEventListener("keydown", handleEscape); + + return () => + document.removeEventListener("keydown", handleEscape); + }, [value, close]); +} + +function formatDecimal(value) { + const number = Number(value); + + if (!Number.isFinite(number)) { + return "-"; + } + + return number + .toFixed(6) + .replace(".", ","); +} + +function StatCard({ title, value, info }) { + return ( +
+ {title} + {value} + {info} +
+ ); +} + +function FlowBox({ title, subtitle }) { + return ( +
+ {title} + {subtitle} +
+ ); +} + +function FlowArrow() { + return ( +
+ → +
+ ); +} + +function MetricCard({ + name, + value, + description +}) { + return ( +
+ {name} + {value} + {description} +
+ ); +} + +function SentimentBadge({ label }) { + const positive = Number(label) === 1; + + return ( + + {positive ? "Positif" : "Negatif"} + + ); +} + +function NormalizationRow({ from, to }) { + return ( +
+ {from} + + + → + + + {to} +
+ ); +} + +function ResearchProcessStep({ + number, + title, + value +}) { + return ( +
+
+ {number} +
+ +
+ {title} +

{value || "-"}

+
+
+ ); +} + +function ResearchPagination({ + total, + startIndex, + currentPage, + totalPages, + onPrev, + onNext +}) { + return ( +
+ + Menampilkan{" "} + {total === 0 ? 0 : startIndex + 1} + {" - "} + {Math.min( + startIndex + ITEMS_PER_PAGE, + total + )}{" "} + dari {total} data + + +
+ + + + {currentPage} / {totalPages} + + + +
+
+ ); +} + +function ConfusionValue({ + value, + label, + tone +}) { + return ( +
+ {value} + {label} +
+ ); +} + +export default PenelitianModel; diff --git a/web_slearn/eslint.config.js b/web_slearn/eslint.config.js new file mode 100644 index 0000000..ea36dd3 --- /dev/null +++ b/web_slearn/eslint.config.js @@ -0,0 +1,21 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{js,jsx}'], + extends: [ + js.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + globals: globals.browser, + parserOptions: { ecmaFeatures: { jsx: true } }, + }, + }, +]) diff --git a/web_slearn/index.html b/web_slearn/index.html new file mode 100644 index 0000000..a485092 --- /dev/null +++ b/web_slearn/index.html @@ -0,0 +1,13 @@ + + + + + + + web_slearn + + +
+ + + diff --git a/web_slearn/package-lock.json b/web_slearn/package-lock.json new file mode 100644 index 0000000..6427fb7 --- /dev/null +++ b/web_slearn/package-lock.json @@ -0,0 +1,2512 @@ +{ + "name": "web_slearn", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "web_slearn", + "version": "0.0.0", + "dependencies": { + "papaparse": "^5.7.0", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "react-router-dom": "^7.18.4" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.7", + "@vitejs/plugin-react": "^6.1.1", + "eslint": "^10.10.0", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.6", + "globals": "^17.12.0", + "vite": "^8.3.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cacheable/memory": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz", + "integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/utils": "^2.5.0", + "@keyv/bigmap": "^1.3.1", + "hookified": "^1.15.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz", + "integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.5.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.3.tgz", + "integrity": "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@keyv/bigmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.6.0" + } + }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@oxc-project/types": { + "version": "0.151.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.151.0.tgz", + "integrity": "sha512-J1yXrIlNDZVzE3ada310xeAw7nH8yCAyLPuUIsjKatFPmfn5bS1oW+cM+QsGOtVWd5nhSpbwZWx/rue+r5Z+PA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.11.tgz", + "integrity": "sha512-A5kXfGKvKWWZE0TtPrfsvT+q4Y5d1QG8gGUzpYjGydM+fARM9MuX90PrXYXe0XbsDVgyxxNzHo6giCj90bsFNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.11.tgz", + "integrity": "sha512-z6cTycz+iJ4PVkuL4HHW4DfTfoeU/2nqYYuSOrTmH7yHK5Y0LCOnA03V4ZNxavyVaU1oOqUgIg2klN/s+USGOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.11.tgz", + "integrity": "sha512-jShvqNtP6vDC6/A5JOAzbVV+DkgHqhl/ScVCJEbt+TUY6QYz7YnXcrg3sLtFBniro0f/Ld50ZwCWA6f7KYD1nQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.11.tgz", + "integrity": "sha512-f2i2xiNWq1Z1l2++q2fuhZRdLAT3aqxD6vRNm1RAxpUoBcdqNB3C0s1Bt+K+PbEx2F5F4gQp6hqKkphCY/xF9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.11.tgz", + "integrity": "sha512-4Ir5FSOKIAMr4r0kExpt1s3bMgzJU3rA45AYOHtQpls0oNeqcYBKrWMlckrYH4KCfGLfkfn1tN1dmZPMVsdXow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.11.tgz", + "integrity": "sha512-/gnRDM+39BROzAN/k1OZjDPnDMcZxB/0EUxKjONO5yVkNEvlsoMDrxGNKgZi/ttFriS2gwlDNzB65pvNbFOXIQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.11.tgz", + "integrity": "sha512-PFaK8HwvAHbaKbBcDNQihjMKYvFnA5hiENx/l5tphTDz1E0WFp32l0A7aq7lyUwGsRw/xSrNIy/gIK4thrSCrw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.11.tgz", + "integrity": "sha512-AskzJUIKRLPxkruR1wLKewGbOw+EYfU/9lOrBFj4AFrEA8hPpKFnODWNu2WLaNs0QNkEb9QIJufmVZZIL/bJlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.11.tgz", + "integrity": "sha512-qlUGAheh2yh8afH7QBgx0PrRHN85hKnNd78x8MeMhXivuevgd8vgf6/CstOzmNKY/lLTHvNTrPy98cLnAugzJw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.11.tgz", + "integrity": "sha512-secpEad+0vCbSfn8upFySkDskv+bGPk3THSDS9Y89yc4rb4kzqHp8Dmyd9BkQW4SnhNXBZCl/6CrO//hZahNJQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.11.tgz", + "integrity": "sha512-mOVBT3dPpkWm8XBWPmU4bf+U6dYDLeMo/9ojUmis4N0L5uu10qra5vOyngZ7/PSdoE4G9KvRt4bloRxNjLas7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.11.tgz", + "integrity": "sha512-Is78i9A8Ui4SqcxUwFJ9uMmjDn58IbVTjFWYdQestFEgeuEmHMLGNriXnVJKkwG2YiZjw8cP0zCTyDMdDGtOOg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.11.tgz", + "integrity": "sha512-dUCXneZ87INUMyQ0D+C0HrEBNUPNXHaPmU5GTjyKTJEiussw9Kaj5Ln8UztPe4epV/ffvgNBEadksdYhmW6xJA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.11.tgz", + "integrity": "sha512-jByxb6qfd+bH1xUd0qnfFnb17i9sWBPY2tOavJ0l3tdr3OTu+Kvtm8cd/JV5nFt657b1VqGltxg9olOEfofXWw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.11.tgz", + "integrity": "sha512-/PzKqzAJ03i19oy2ItPvyvaVjOjBCNnfaJs8yvUdGBKmiESgnrJSQ2awd81QzFbbnAmu7YO9ZnJrDCb9VSJPRA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.3.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.1.tgz", + "integrity": "sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "oxc-transform-react": "^0.145.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "oxc-transform-react": { + "optional": true + } + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.26", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.26.tgz", + "integrity": "sha512-GLQdD3y6UF8iVuMJl5fHgE4jdn/ua7n+toKfLgNlg3BqQtOZjpy68T8Tup8/wGWZCDlm7KMg7tPb4MPn7oN0TQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.29.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.3.tgz", + "integrity": "sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.26", + "caniuse-lite": "^1.0.30001813", + "electron-to-chromium": "^1.5.439", + "node-releases": "^2.0.57", + "update-browserslist-db": "^1.3.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/cacheable": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz", + "integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/memory": "^2.2.0", + "@cacheable/utils": "^2.5.0", + "hookified": "^1.15.0", + "keyv": "^5.6.0", + "qified": "^0.10.1" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001814", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001814.tgz", + "integrity": "sha512-/Uaf1lAzr59XcMpW0o96WoEfr+VXK2OX4U9AgFoiSHsVJ4HppnIFUjtYzsyDH2+tgANaQb2/oxYGwCPapN1FpA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.442", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.442.tgz", + "integrity": "sha512-najZYZ3+ZpjN1z3VOsrBiv5ej18vQgfV2lvEmxWzfmKrk4bdm3Owseyud4yGU6DfFn9pyunoAHDDyM4KmP78Ew==", + "dev": true, + "license": "ISC" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.11.0.tgz", + "integrity": "sha512-P7a6UEEqb9G95MYAtqkmsTbVXIYyzIfl6NGOIJk162PaahFxFyeGcrlXYFSiagECg4sEm8IseJdZBKR3rx6MsQ==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.3", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "11.1.5 || >11.1.6 <12", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.7.tgz", + "integrity": "sha512-XhJSzLljuYD4UjNuFGJu2v7aD3sqNVK12w5/DCUfrNfHWegZo2+TsavNx8MuxMwEquuFAzNbNfXKU0WoZ+YUIg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz", + "integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^6.1.23" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "6.1.23", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz", + "integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cacheable": "^2.5.0", + "flatted": "^3.4.2", + "hookified": "^1.15.0" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.12.0.tgz", + "integrity": "sha512-cezEd/DTyyht9cvSSURyygXPfy04GtWO/5e6ZPvH7fCtjKz9PYOmuawphw1Ctd1f6C+5JypXfGD7ahNMXvevBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hashery": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", + "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^1.15.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/hookified": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", + "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.57", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.57.tgz", + "integrity": "sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/papaparse": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.7.0.tgz", + "integrity": "sha512-qBGxg/7Q3Kl9Wfhrz2Z74UnvnHTXLNG6jmKJFeBvP2+y4lV7So+7SR62+Zd47JvdrCkX+nDcnr0ObPzek/+6RA==", + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qified": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", + "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^2.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/qified/node_modules/hookified": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", + "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", + "dev": true, + "license": "MIT" + }, + "node_modules/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", + "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.28.0" + }, + "peerDependencies": { + "react": "^19.3.0" + } + }, + "node_modules/react-router": { + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.4.tgz", + "integrity": "sha512-PUPQcMhMGRAslLcvtlPz/kmzBEWPhLdgLFrL7pLNepBL6dX0lWj4WD2cUYVgYCuT3jxvghYFg81cDTj44DhetQ==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.4.tgz", + "integrity": "sha512-yrfmJHIpDG7taCpqKjT1G5B6q3O2K+RN8/fgNf0lTjCwiPbQ0ei6vXX9ZjQR+7ld8Tr7Z5xmyMnZ8YJrphWQUw==", + "license": "MIT", + "dependencies": { + "react-router": "7.18.4" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/rolldown": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.11.tgz", + "integrity": "sha512-qpSwIyz0jHQq5qXBTNxFmE6664rJ7O+4TvPFOiOaBSrz8IOHc1koKKSqTM2H6u1UG1+TveuC6vaDHKXFOvb1Kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.151.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.11", + "@rolldown/binding-android-arm64": "1.2.11", + "@rolldown/binding-darwin-arm64": "1.2.11", + "@rolldown/binding-darwin-x64": "1.2.11", + "@rolldown/binding-freebsd-x64": "1.2.11", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.11", + "@rolldown/binding-linux-arm64-gnu": "1.2.11", + "@rolldown/binding-linux-arm64-musl": "1.2.11", + "@rolldown/binding-linux-ppc64-gnu": "1.2.11", + "@rolldown/binding-linux-s390x-gnu": "1.2.11", + "@rolldown/binding-linux-x64-gnu": "1.2.11", + "@rolldown/binding-linux-x64-musl": "1.2.11", + "@rolldown/binding-openharmony-arm64": "1.2.11", + "@rolldown/binding-win32-arm64-msvc": "1.2.11", + "@rolldown/binding-win32-x64-msvc": "1.2.11" + } + }, + "node_modules/scheduler": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", + "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.1.tgz", + "integrity": "sha512-/bvH9E9tmCXRGp2uXY3WbOldqpTwFkbha/8ANaEQ6VkxhH60KyqLwgZq6lG2y+4uT55x9+9eUHMpQ7uGnOCKjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.28", + "rolldown": "~1.2.9", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.6.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz", + "integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/web_slearn/package.json b/web_slearn/package.json new file mode 100644 index 0000000..41077d8 --- /dev/null +++ b/web_slearn/package.json @@ -0,0 +1,29 @@ +{ + "name": "web_slearn", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "papaparse": "^5.7.0", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "react-router-dom": "^7.18.4" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.7", + "@vitejs/plugin-react": "^6.1.1", + "eslint": "^10.10.0", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.6", + "globals": "^17.12.0", + "vite": "^8.3.0" + } +} diff --git a/web_slearn/public/favicon.svg b/web_slearn/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/web_slearn/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web_slearn/public/icons.svg b/web_slearn/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/web_slearn/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web_slearn/src/App.css b/web_slearn/src/App.css new file mode 100644 index 0000000..f90339d --- /dev/null +++ b/web_slearn/src/App.css @@ -0,0 +1,184 @@ +.counter { + font-size: 16px; + padding: 5px 10px; + border-radius: 5px; + color: var(--accent); + background: var(--accent-bg); + border: 2px solid transparent; + transition: border-color 0.3s; + margin-bottom: 24px; + + &:hover { + border-color: var(--accent-border); + } + &:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + } +} + +.hero { + position: relative; + + .base, + .framework, + .vite { + inset-inline: 0; + margin: 0 auto; + } + + .base { + width: 170px; + position: relative; + z-index: 0; + } + + .framework, + .vite { + position: absolute; + } + + .framework { + z-index: 1; + top: 34px; + height: 28px; + transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg) + scale(1.4); + } + + .vite { + z-index: 0; + top: 107px; + height: 26px; + width: auto; + transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg) + scale(0.8); + } +} + +#center { + display: flex; + flex-direction: column; + gap: 25px; + place-content: center; + place-items: center; + flex-grow: 1; + + @media (max-width: 1024px) { + padding: 32px 20px 24px; + gap: 18px; + } +} + +#next-steps { + display: flex; + border-top: 1px solid var(--border); + text-align: left; + + & > div { + flex: 1 1 0; + padding: 32px; + @media (max-width: 1024px) { + padding: 24px 20px; + } + } + + .icon { + margin-bottom: 16px; + width: 22px; + height: 22px; + } + + @media (max-width: 1024px) { + flex-direction: column; + text-align: center; + } +} + +#docs { + border-right: 1px solid var(--border); + + @media (max-width: 1024px) { + border-right: none; + border-bottom: 1px solid var(--border); + } +} + +#next-steps ul { + list-style: none; + padding: 0; + display: flex; + gap: 8px; + margin: 32px 0 0; + + .logo { + height: 18px; + } + + a { + color: var(--text-h); + font-size: 16px; + border-radius: 6px; + background: var(--social-bg); + display: flex; + padding: 6px 12px; + align-items: center; + gap: 8px; + text-decoration: none; + transition: box-shadow 0.3s; + + &:hover { + box-shadow: var(--shadow); + } + .button-icon { + height: 18px; + width: 18px; + } + } + + @media (max-width: 1024px) { + margin-top: 20px; + flex-wrap: wrap; + justify-content: center; + + li { + flex: 1 1 calc(50% - 8px); + } + + a { + width: 100%; + justify-content: center; + box-sizing: border-box; + } + } +} + +#spacer { + height: 88px; + border-top: 1px solid var(--border); + @media (max-width: 1024px) { + height: 48px; + } +} + +.ticks { + position: relative; + width: 100%; + + &::before, + &::after { + content: ''; + position: absolute; + top: -4.5px; + border: 5px solid transparent; + } + + &::before { + left: 0; + border-left-color: var(--border); + } + &::after { + right: 0; + border-right-color: var(--border); + } +} diff --git a/web_slearn/src/App.jsx b/web_slearn/src/App.jsx new file mode 100644 index 0000000..8480d29 --- /dev/null +++ b/web_slearn/src/App.jsx @@ -0,0 +1,27 @@ +import { Routes, Route } from "react-router-dom"; +import MainLayout from "./layouts/MainLayout"; +import Dashboard from "./pages/Dashboard"; +import AnalisisCSV from "./pages/AnalisisCSV"; +import HasilAnalisis from "./pages/HasilAnalisis"; +import PrediksiUlasan from "./pages/PrediksiUlasan"; +import RiwayatAnalisis from "./pages/RiwayatAnalisis"; +import PenelitianModel from "./pages/PenelitianModel"; +import RiwayatModel from "./pages/RiwayatModel"; + +function App() { + return ( + + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + ); +} + +export default App; diff --git a/web_slearn/src/assets/hero.png b/web_slearn/src/assets/hero.png new file mode 100644 index 0000000000000000000000000000000000000000..02251f4b956c55af2d76fd0788124d7eee2b45eb GIT binary patch literal 13057 zcmV+cGycqpP)V|)f$;Qooc7=_G zlYe)HToTQIc!$)^+J1M1y0*T%w!p~7%ux`!eRhO?c80XDxKQ*R^lUUMnA>6NT^?feoZ8xxvP32D&s-9ow zqjcM}eesrC)NeDmsf)*P7wJ|K!&xP%Zy4iI8lF)Tv2!reW)tCzg_1=PmOwd1SQfxa z8;58t!=z~Ba7CYlNWVG>he8aRPY|+-JmozNhn!#9i#77Aa_Edt$ijyCWL#=~I>~2X zZNrQ8I0=D+NWD4pq=7~(i zhfThMNw|G>g^y9pGzxX7ZSApl@tIxFcs{p#MX{Ax&XZT+cR#U+OWc@S)pkIuI}dzu zH?^Q=<(y&Vq-oxSLfc0Zmq81bjZWf}RnssBaD6}2g-XJHLcN_|*IOu>m|x$nbm(?E zyNy!Zp=RroS;?Vg*kmoJYBi!n5{_^@rA!)=t#a^;N$8GL!*DsQb}`yvEuX!G@||An znOfUZAevPrkV_qjl|<~3QRZzG&h@C9Y5z zqpNH4xqbF_InIPh)kX}Vn^5kyed|mOuq+2>M;v~KO37a#yrEn3XDqtOl=rc6_KZ!; zreo)DFVB4|>1Zd(bvMI%8uM;3!)YMYu&cG?(PE!B~y@3yKBMt|R zAf=I16tFwPsl)!jDqvYkLHaAQ+f@W1m6F5aZvwhm4JL z{_l)@b;)mDSzle2gyFP5-r1x-5X{G}ot%VyWP@vEW80!Q=f%RTfpg>B*TA^pyWYUQ z<=xPtz}WcZ!;rFl4m1D&FFHv?K~#9!?A%+fn=lXt;9!Fc#kQ;zk~gZFsH z8e5iu@c_pzX&qb8&Dum*oXwB+fm6l6gFfC|o*wgEiy6tw~&co z9Vd_4)P%wP-KwQW7|lN-znGK#?N+j24U=$982myIBM+vsiKsc*@4-rwJxuAaHKna6 zT3wi!C~a4ZKH03qU}_1bKyx0&$CaK7_%Z+Kl$)fF5^op zZApQF2TvDav!s|krTjw-8US6ep z%!VmX4luub+fseQz_D9ATJQ?iQQwD}TZz{-yo#l12a%+7bT@E(X-hyaVS-5vuXc#^ zx^w;L21;NphGVoj*{s3f4dme0y2LC=G1-7THd`#z?;tuC{^9k(dM{Rf2GOxg7Jzho z7nSZHl7?M9kdalX`)YgoKEfiae5+;$(OGeN1eqxrv!ZCVKyH>xiyNqfe8xzY8*7)H zQls8KMp)F4D>ED;idMOU^^WhVF@q>ZSmeB0y~qC~|DB648hr%Sh|*T(4q|w2l?m2+ zvBVw3@7+Mz?^Yc#+se6KM;a<=(W-I>k)$-qL2V*t}VaW`;?P4)WqI%maIDq8!oUcSYAD`}wWjkSyAVsnF65#2zQ zZ>(K*TlS(E#4y$4Zq+e^_&}d)q20hCe3!LfLYP%nQpLJ~gM6a1hJlz3)aS<9C9me| zAcmJ#>tOwBy{HoP0Sm1&_(E+S@6 zgBIFUoei8zJmdpiq8q5=OY7t@`)JWxn_&GvKVr=Zdb_pEL_j|=?f;WK^U9Q0efd#K z9q7SfJTl4pmA$jsZ5oK8@O9#!I3Cv-kL)<8SalSsp#dcpvJ}Nz#G6FC0%9|7Fi#8; zGDJXtj!&GljT3*HE@0EE>G8Se&d)*nkqe}-?`3vPl&UqK?xG z!3XJ4M-x`EuQjhBbu?ik-)rmIt=DF_N?TVMP)8Gjn)TZ2V%H|zENbeix}kOxd@0}Q z>)HuH6Ean!uS#~4g2Ne2WsMGel|h%j9*W_quQheG^JqmKhc*RYzp0wKlGjBq2VzY_ zgOv8WC1+%W=W)k)Yp_`8kfE=uiiwOZTXi8Uj9YGr$f@yJcJ;#&-Nq~sJ7anE(@;QN z=~br%7%7`isKStX|7!1?L(apl^QvPKlrHV4S+6tNVQ*R1iGdC~WMNE1$a+=rpQmcB z>wxiLIBvOnm;u*;9Y!kJdy(T4lk|8>JAm(&wEsFIF1$_*{>2ZNd$V6DS=SfrGxAv0 zzKe377JI`&o9Ljr+VnS*EwehA{f&{cKZF(6*MG5!p5MvrFA3ll{fmRG*L@6^cb;o^ z3Wm8c?Sc6$`>~VEWw(c$Y?nRO;2Q$=ulpqPtM^=1IZx;@xK0PgO7rKQ^WHVLwtgUT z%|JF{^f(VH)wLKQ%dYiu2RmchBdxL0-M?wxxul_z*{h6ZZ`>-k(vizs((vW8Lt6Z6 zY;Dt?@JWyN`O`f;&d1Mb?e%9oyRK1ql?EE5XB2(W)|D1~Rx35$H6@6)$F?)7V|zEO zI}fu0-0}8W5=6sg$fPnZ~7=tTudl?Ecb@pxbo)vni%gP-?hL|%*?62C;x6?@E`VRnJv z?fTb;k4x;TS7Cu-z%J}uy}e-pwpLQ17Q@4DC+FCdAmNKklG$`I_pyw7E{fYmw~{Fj zi?6KcVy=Wrel)EB_DWO|0CKmI|13!gBV?X`Ozp7x>?6jr`>Qz=^4ea35!$*f}) zS$i+x_k+@P2q1RFUH^ZTTk7=n?cjfR>hTq3l3SY~#w+I8SSutXGyhw;Ws~=zMQ%Vc z>$On~47Ut?P*_!TOQ&PFmLAyJieB2X4_Fd_!WxI-AY`q1Lc-oK?+qcOTzlQ?@~x@OT}*9jTVNfl@3rGvZpWI=eKg>T zZb@6YWz)J=IhP7CF|c?G62vMEG%#U}?#86$0jR4sG~i(jRd#jmn`7b(O#?N;3a;1t zhXLssmUwGhp79luw#(*V8WL0|8+E z6=YZ_O@er~$LrD_PYGc(kJgB=;yw#+Z3X6LDUZ(NcwN=B-hjdiHm!JFar%m{(5bEW z@@_VEtG$5;`EJZ|OkJ@l&G9n((w@uNFwmU%bG|s#TbcJJos!{e+bjCjrCq_}LcN!UFgKtgg7siV*7# z!}1whTRRi*-avJPu->C}Z8EiuK$#886+H_#_!btv+rsiBbv2jAJvJ+O0{#}y(%L3H zfjU-kq_-L@2XrL*ae{{qYJkD{@dw%*bkh2P&YS-0!Xt!PRz7KHV0+~j(t9W8lAVWR zt@B*DgURgEz4>WuN>o?_iKcw$?k{||Pg7{Q2o4|VmJ)mg?{VQJA<}zEr^YAAS zgGm5RT4T3p)U;yz-tfBO^kw8?IoG!IVmc+Z3m#}AOQ?5MRa>)OcU!$N^_+yK6ayn? zK>~WK0!#ysuj^oNLakm)Zvu+J)OSubX^kv!c*xgdIvs;kln!rgG4*uZ;w0mQQO4XD zO9P{GNdv!=cQ(CAL{S(%KtuV^zC&Q{%g)PoXnp^gn^>c*`E>$hLYg2HjnbVGtWLa{7zHdG1jT@B{|Dm16 z7K2(jsfG+m*Zxof)iXxu+!H5Mo-0$pkyV3VV4B@Qms46M zuBxGRV@HxU7Wwx-6CB zaU*HO<_qn$5GH>&@?nRy1{z zkik!sLfWQ)r#75)vVwCBU*r_)Q6mp?!j85{#Xqse)ApRdE$V0%I0*~e(_{)5H)`Mk z#rExC>yjhZxuL@|+#v4#<Axw$+VpV zuT;!2Vww$je$DpAW`$FX_Ab|Ip%$;&T$-lW8jS~B$>G}rd>eQG+$h9lQx4Mx0w={m zx9?T6VU`>sR}XClkAhHEShOUe8awiq zmizhL+}5UKs3}6~It7vBTig9dfQ2Q8coo+Miiaw7n~>4ybv2Ptt0^^=VqX(t*Yya9 zr`FxxFX8(v*H=+uJ#JJWIB2A(==HDYx~^zZ2nu?2`}|Wsa*f3h3ixc+U|FDtAG$Y! z*lc_7se5Oso-Cgqe0){{!8H4g$3<8!R<6JOurD;((({c$1(pwb>(#TT!sge@4>r2@ zVL7>U`0`nsWAYErezk4(Z!gMI2?UTo{J3Ajo(u4)KYIRd>BRcG4BoS3G0EXyEp@tw z%P7__?A^a>Q&AKL@ayDO9D*Qkc!NHnO9l}kpp_6hXbMppYL(X1L?njdFT|-h2<_$; zAtDZ!1Rf%|yb!qbWKd}%0b`LzBeyNy43|QO(&h2mxQLUL)|0%agVOW)6TV!&Ip^Ls z`PG2cygM8)IecQx=Fc+nqYRo4hS^^-nM_&-y8?EJXUczP=DIw(GkTJdpEdh<_STs{ z|A)4n1GKdE=Wu!!nYoZHcUQ4S&R;oDOKX2lrkdF(mK>hz<$Pp>igjOcvoRIjlN=W8 zu8Gx5(roqn8$>gEE5vy{GiGeW8Tq{vnf3hS-V=$tZkQuftUVuU8o6k&dn=Yg3)6MOIH>nlK^-2+C6BZITr~1@So?NvG#TwL)|~=1YXGMTLpS<)ziK_CSOabe z=cB#5)yz|@0i9dSo?*CX)}UP=s6)B+F@~Em(u@Q(I9J9i_V{LmMu8BfXYMh~*oPP+ z!3~xTv|(>|=n6ZOtT~C@V!z!w%18*8T2t6}U2S##rC)mekBql&VsBX;$~ByGE$oA9 z`0Wzq8p?R{4)$l*on;!cLa}Dh^Xe?owiQZt9nH1fxxh$pN9K%CtOw?u3>85L7rr!d zXs)l{TZ{xXP&U8exz?9cv~dNNibOmt*K4I$?RxqIBZ0(?Mg-9FS{*9Bc49Qc1`=sIF-rye`aNT1G@4NwXcnyc@+bw_mTsR>5< zF<2;X0QesG_pw|TonqVBhRtfqI>ty(SIu&VOXd0CrLlfp+;WH7HYjhqnu^oAY!9cB z=B6#R?Rfz9BP`dJ=@v_?70s3HxQPk+{6Y+lM85f2NF^00*^OcM0~?JOZfR9ZPYF+# zYSs}(_BUYV8{n@2a1hD^SV41bwmi2uztR;PeBgF1F-`9>`zoNss-@3LaF2sjl~>OaaVmp7PNp+UT`6@}gR%uzqHDVeEZ14{Yt?n%JeQm+t(1_u zSc}oj^{b;+rlS|ME%+LjzSI&xu0Bblxo$MJ-J$kJ?Qu_XUXh}*@*-x@ny|}wVM%Lg z3tNB`yvr*}N?ClGL;H2cglcvErIccU3(eP7>@~4nOIcI~-`P8tSQnx=jI&{9)!1}l z;gQ%_h>ZlPSV@o@Azq1R$C6ja5!^ZGh;YRhhxs58qJWo9@Bceac&yy(pET1hnn`~7@}2L0&dfPKYs$ih7m2}R!25!(hxqA(!UIw; zK4+~Jowy3=RNC6nE=ncU{LH5?*9@W24lacJlvCZXB$CYtE@>c+~H zkV=(5I&gb{xn2!~f&fs2NQgAL6`p|kyt6kpWk}iVlqIp(H;ig`{_U9yxs1jzu^ETM z7~)Rg8C-NueqTYP&U8l{DY=Y47cR zOR@U%$KQV{mkRF|4)z9Y^t3K`@p>duY&QLUFeh6VoV`a`$U@)(z!-N*5Cj<11$EZW&hJLX83TO{lJYP74rlDZQPkm@t<=U^I)x@|UnHHkdQlh?!ltZwl92rE;;^ zZuIappj4dhld1}kttYYV-j|KF1Kus zWBnzttD^00%LFK(wrwNragFub6xiV8QE2rm<`&fcR4SLFcdtLxVuN!Aal-g6dE4%k zARZ}|xeo;K{0yf7@9aua%2j5o)CPcIOc6uLHFJOcgtB5owlcNAwyAHc0QB0Dts?c@ zUemG~j_E&W7R%+x-IO4FJl8e&*2Blmp1S#RA|)geVrxvP)NHdYuxi~g&Etn?QdNK8ZDKZ?QFLU?zh30G|t9G>a_X4zk}Ygw<^$7K!GIn(Io$>(d4ODJQ2XSd%jpK zm7>ptl$a3GyB}5-%p4>Q*p#VL^B{yQMuFCM^#l#+N!Ne z5_PrJWB=@Iy+t)H`g1lX`{bm($KE5I?0c(JEYm#t{F}j!xtsbob0{xu@0TB_*>G7w0ICn zr#VoBktqHZ~XxhiKD*lcG|b;H*|Ny3P^8ceV`sfBRfrhwZ!T+MFZ!F1Bt{q$8d9i6o?~ zODj^POr}&ivSa^R^YFIq7o0giLBKCycH_aU`F6)O6JX%nPTwh~Q`eq6*0iE#Srj2^ z*_hN3%*b83zfafy60@Cp3{J({RlSaEn&E?mrxRNC9GQ7#+f=s! z0KBf-9Ny_v2VbE%aB|Di)5kNJ^t&C`4D(>t7zYUWUFtbxt+Oq=!@O7BU)}>d*R72o zFF)3jQD_lLe4is&xzyJYC1-c{8TX$RU>&>P$%)ufpez0XSAukmh!xcekg`s$c<>-q zI#zn^JU0zzF}V60)o$_gY}PQH>b2M9&8fRZa#OauglPb zeQ@pMm&=!vNgos4CluQjLMV!pfkmxK+35bi^k&=k>9h02?l+u+m0agG;(h2|Jslc-llvtEwn~*w3bx7qnvZACG<8}AGeaDVvcHbKd2>3G^ zSFPULUn-?Pmo^-_`mLZr??uNH`2=I&yajlrF{DtUxMy#Nu}z=3y7qbUA;5`)hibMR zhXL@@uKyV0-2&A@t@!xyrBnMJl&^o@Gx$&5_q6?D=ji5grd-~=?dlg;ur(_V0wjh! zA=JV^C1m+DDkOsgr<%O9ZQFg!0}pD(#PSz4Dr_EyS5$`)VIAv);4n-SFP~YtC7sH= z7&*MfpH;gd*FHbkmD#)hVxb6xjc9~`t?_{=JS+@ip_cTicXxG<=7m9& zPX+Z8IC*GSAXuGCrZDHgR$r%jyk-fctis2Kx4HvZ|B~8uC@o)m^>Hy-O!&TKA?$&n zkP2Xc54w~!=z2?^NafyL*L0V9cbYrugHBBUj`xVyZmGFR&kvk#>1J*Z~i zNTz}?IAdJ$gkqd2!Gw(%LzE!O5s4C7q4%T~e_P{+z=DNDKrG**p=U`d5yg^vp`;Zn zsU=8gd0a9s4s0FPJePWR9eH5=+O^Kks&kC-iblNqTh2&Pw*^(4384f+D8N|fewZu_ zg2ejQ)ov;ztz;NQl7yj;A`(!H!XQu_$sqY9h_IrH*}_%1{L&_YLDvO?%R5Z-t+ClW z_qERbL?HKUZ!nt+!E9S`uoh^5A|DaIHe*_gf1`E_Vq+}{&T@t$EGhMnRjJ4z2w_W8 zp+qjs7as22^&S3wY1?+}^j-I=RcCE>#|39)g(lU7v_8;?=qK(9D8-*pPdiy)P3lIblG`+?%ea| zYoD3dopYt!tKgFicfNmNi(EWE=E4hC6(r|PYtanqJlmt57YOVrr2^tfrG(eG9C##X zu&1t@%L$RIvpj!wUA z8i>Pqot#_+Cnp6L2XPcZy1ar|9MnY+7eNvK1E)@Tr#2KsXq1*>)uUCozT7L##ok?o zhA6ofP4E|b*9tAfG?uf$#}>TIR&1A!yslP8}i7w-EzW(x#9VEvx18k%Tn=-$VV zkOtUr0b2!w3t>h?#8AZl^Az*(6KCGlD;4j~yx};`#2gN1_gv=%7KVzecIRakN{f*4 zeaI>yH;-o4OGhvGTU)(quWI)-q?V*(sVesSMv|wMUQ3hLEt=lBB$KZ9TyHr>)f7o%) zPYeU<3P)*P10*7vE)nA5#{c=6-E-_>r_u4e3i!I2+UksELwDqwMeBZ9FSP$;^Ajro z_@M#_Ss$?ejoB@!wN|kbGKs(0zLo%0QpQXW#t;oC$B0MZYZ&Ej?8~fNhcCVvPo3vo zFn0WWZaPliF^8_}yzb`*f@yg0uWv6HgNI)xa=pO%Ck(C<=-60l#uD3(wXP~c7!NoX z0&^6=N`zcc90F#qt@=Rn@r!3(*1v(Tl{B!m?Mc7yIA+nEHpY{YWr$=)F7rhR1P}(v zt{YhY#;jsW6G>#xhP*B`OCk|Pf+NN;ju1rxa*HAgoGq*rvqw&xe~;t1JA31$s?GBb z*g7&@cbKo4n<`>)!UlIAgR6q&))B0KYU8r66GbFj?8Guw4E%&}Qi_lT003LtoIZei zwD~=XZmeo+yZ2Pq3KYCF-R&11^p= z@H%s+=G`}wrbJ{()Mh71#2SP3Zy3m>l1n?0N-N1Q;z6?oSxr-G(H5m4EO>~&;}VKi zfY}3w+9z>vp#d)hVuu`)vG_aaH%3b=WKMnSu&c31;<3O;bz2iD=w+o4#oBb36 z5ZCF*Gu?zjZIR0S>_%pHY2$k8D^n7Sz_K8tCDeXM+dO<#LSg%h6`~dnVG1N@T7v&e z%wEd1!k{^zfz_1BTW{!$!B%g)J^2b87!9Y>>100X1SgT7s0z$o>^lAA=Gp_cC1(h=*5Tmf8z&LGJJ>$|K^~s`z9*OWz5MFUr?>Bi?_PGBB)#psD5?>n+q{o_ zz7~ez&;t#h8l$jwGPCC&xq2YetXYQT+0F3j(`xmNGf8dj#an|p#I*pvI*kwW4iuB> z+q3_7xB8y;pLzHG-S%+UHQA zvqp;$kmGJY>lLsN4C~&TcvAS1SErTcwcw0r@wngk zShAUA1M9b#g}^pL-zH7Q#z^&j#r9F8BTVfkR&qF<=e35goTu7c|GN)0mokj4m0%~0 zXJ8j4Hc_l;HJ&uU*Iw`8d_EscJ``s0tk9mkKo^&#TYXm-EoAzTQObxa@^u~g2t#T) zJz|rE!I_?i4dCJC=B8(_pZ{YR>|V?0iCcnU;E@$239^x?SYCfNaMHN;CtHIS_zHN9 zTkQc1v@O35okiFtq5_u+5FkY55ap@pi)O?}x0D1c*qB0KpYR}>Ul+B0Vmr}Z@+%mJ|As}sis_=ROPbov@*2thpE&?!V#Qgu$snYvCZ zrkhmkMU+fSf-s8(L37fPr&M*jRs{{THb!aXQu|P9l_-vJhHvLzMGH zE?1U0H_+PmNABp9`|KzkGfrrZ%XvdGo6*<{d5m9~L7 z_^`M;X6xDo=m6LY6RfvJEvsTK1!u8d2HPx|$S}p;sRy!I zWL55Yxu~_B`OP@~(q6&W3#)~I&+MGL%GWR$#udC151^wsswhqlii;rP9jJpiI7o&Z zAb})=HY7?4HA|re3ns`%$)FuvKCFWjhb~?IE)F6dF2K5}poj-NK6Gf;hw$t3=1txY zoxQxZWrQU6K!%|~!m?~Bnw-6Rr!F3BZ{u5!LqnZTDON}Coj9^@&le)V!NYrVwS~B% zEL+>Sr@}qGwGvu|HrOo|gSt__ezN^&%~{*)a=rf7y1HujUcr`zZB<4#l@T#eN)si} z)lZA<{=tKx8E%c9>A(##6}_p+~EZpKsl5a4pj`E*;_-6`ysiv zffA!7=MT1vCz}-m4~tjVey1b2KSR4OEtLd-(_DdUqYZ74LaDkhH?KFh?%WAOP2WbX zp@zT+Dx|5_f%JQiAGvVw!oh+g3e50u!aPfMxdC=E)XB{F5IcEZhePIM- zph6Y`$Oy?JBL<8Ex(SqEhLeQ@XcrdA>a?rx+_~HLA;l14)WmmpH}_w?Pg#HBZs0eS zwypwAW?M-x+3AU-(GGWSJ=ngxUEcEZ5OsX(Qlt!MQ zn^(`S{GHkAv(8@D`EAfSYig%Cxv?z!{=w^F#y)5_d7FuKZH7qlR-#5B0bt806%D0I zT7VdVP_?q*%Rq8UR;JkD4i^RXowt+E%#V2U>TfDqzZSDZ+dR!a#T3I>-z_$q9@k|m zy5~A*m~&JWP@E7a=pc}4kVHTc4h&R;Li7d@f`|hKMLkbb^uhOakNr3&FLjlm~i5NBM< zFaYI{;cpiHCNRdE0dg*>qIm(_t?#$h=(SCw?h3rJV2*ER8{O4^3#=dO)KwklZkoqU zS8i5c%YL*y*4;FY#D=XmkQnYj%LH)?02~gSJH`Qp1XY64g>%c_K$xseI&|e)7vRoL zAqRba$G@%fSGA7X7hQk%_3NVOYVS+$leU_!&6*5uN)8#5ZBz_6ASCA;azYS-Rt@ki zg2NWz(=;t}SC(~Ibl63$5C8FPmhXqb^)5#jaJ~I{Ex3xZ!+2h8$}}h_g@Be>HZ;72 z6#y#>AY3^skuVKF#0WxFBQ()5d5_nWb?c6c>EeMM|Mh+*&wEpPyxHCq{R-Gdr-`hN zF=1sxl&mBoK+#qRLl9#CEN|Fg8>nbmsTg3a1;#M9enQ$RgWk}kp#-5wh=EF&1tl%mJln2V^8o%Qv(*=zEuO7y z=m*8?xpUn-*@h5Cl_3BK3joiGkyaScK+>|MWdMRWm@RT!Q1piAlv5hL@B6>3&GI8) zP!xBc6}ZNIpJLL%2a8Y!+(<=f%WX>_uWVxlga9!D*oYt$l0cxRDMvqfU;Kq_mLK5k z)dvqYcgLa_Lz?3HyeF)@$%$&6lI?r4I>6W#M*<)vq{?&Oqrx``d`mhpVPr> z#q078F6gw_X<=?KR>8%^t%@wbITvNMu!hKiTSkCTJkw>1!e*Y{%31#_yMf=LW7{RJ zYoC^w$6%3cBtVG5)x#{Hg6IVTh9XEcM{gQwXk!R^y95^f-hZ`d{aVa+xW1EO4wDV4 zB?JgD7*?qkvc|$nIykTvNl2x0j3Q!MXoLL^)~}d7jcYf(H8D~c+?$pKL(px>Z3`eb z04RzS6_AgFT6Pn#iZAg$Sl_j8#;6ShF%&(Fag#E2asU@@LaN;=b=Wf7sgPKhfzhBM zC@eFL8^MrnA*9&Khe*Ab@CC9*uyJGXyi(;y2>lQLJZt;ShtJi?3Yf_t`F+$hY!+Q2Ndsx=U+bjTiAy7djLji>7k%k`$9&--f<*BNA3Hy&ZrHH|4 zG5H&9cB?O#zI1_OOf0Ce%mDfQxdtp3vU%(iY6yji3iISS61XLv#z|!zI_sZqza@B+ zyu9st5-h+`H7QUKx9}3w@oU@EO}&cEzG?fu!!bLO->%zkcg;i9^j`S~=WKMnDi1f= P00000NkvXXu0mjft=yBf literal 0 HcmV?d00001 diff --git a/web_slearn/src/assets/react.svg b/web_slearn/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/web_slearn/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web_slearn/src/assets/vite.svg b/web_slearn/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/web_slearn/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/web_slearn/src/components/EvaluasiPembanding.jsx b/web_slearn/src/components/EvaluasiPembanding.jsx new file mode 100644 index 0000000..7575fcb --- /dev/null +++ b/web_slearn/src/components/EvaluasiPembanding.jsx @@ -0,0 +1,89 @@ +import { useRef, useState } from 'react'; +import data from '../data/pengujian-pembanding.json'; +import '../styles/evaluasi-pembanding.css'; + +const pct = (v) => new Intl.NumberFormat('id-ID', {style:'percent', minimumFractionDigits:2, maximumFractionDigits:2}).format(v); +function evaluate(rows) { + const cm = [[0,0],[0,0]]; + rows.forEach(r => { cm[r.label_acuan][r.label_prediksi]++; }); + const n = rows.length; + const classes = [0,1].map(k => { + const tp = cm[k][k], support = cm[k][0]+cm[k][1], predicted = cm[0][k]+cm[1][k]; + const precision = predicted ? tp/predicted : 0, recall = support ? tp/support : 0; + return {precision, recall, f1: precision+recall ? 2*precision*recall/(precision+recall) : 0, support}; + }); + return {cm, classes, n, correct:cm[0][0]+cm[1][1], accuracy:(cm[0][0]+cm[1][1])/n}; +} +const result = evaluate(data.rows); +const initial = {accuracy:(data.initialMatrix[0][0]+data.initialMatrix[1][1])/data.initialMatrix.flat().reduce((a,b)=>a+b,0)}; +const cells = [ + {x:260,y:170,value:result.cm[0][0],label:'TN',correct:true}, + {x:500,y:170,value:result.cm[0][1],label:'FP',correct:false}, + {x:260,y:350,value:result.cm[1][0],label:'FN',correct:false}, + {x:500,y:350,value:result.cm[1][1],label:'TP',correct:true}, +]; +function download(blob, name) { + const url = URL.createObjectURL(blob), a = document.createElement('a'); + a.href=url; a.download=name; a.click(); setTimeout(()=>URL.revokeObjectURL(url), 1000); +} + +export default function EvaluasiPembanding() { + const svgRef = useRef(null); + const [busy,setBusy] = useState(false), [error,setError] = useState(''); + async function savePNG() { + setBusy(true); setError(''); let url; + try { + const source = new XMLSerializer().serializeToString(svgRef.current); + url = URL.createObjectURL(new Blob([source],{type:'image/svg+xml;charset=utf-8'})); + const img = new Image(); + await new Promise((resolve,reject)=>{img.onload=resolve;img.onerror=()=>reject(new Error('Gambar gagal dibuat. Silakan coba lagi.'));img.src=url;}); + const canvas = document.createElement('canvas'); canvas.width=2700; canvas.height=1680; + const ctx=canvas.getContext('2d'); if(!ctx) throw new Error('Browser tidak mendukung ekspor gambar.'); + ctx.drawImage(img,0,0,2700,1680); + const blob=await new Promise(resolve=>canvas.toBlob(resolve,'image/png')); + if(!blob) throw new Error('Unduhan gambar gagal dibuat.'); + download(blob,'confusion_matrix_pembanding_v1.png'); + } catch(e) {setError(e.message);} finally {if(url) URL.revokeObjectURL(url);setBusy(false);} + } + function saveCSV() { + const headers=['id','ulasan','label_acuan','label_prediksi','model_version','prediction_id','kondisi']; + const quote=v=>'"'+String(v??'').replaceAll('"','""')+'"'; + const csv=[headers.join(','),...data.rows.map(r=>headers.map(h=>quote(r[h])).join(','))].join('\r\n'); + download(new Blob(['\uFEFF'+csv],{type:'text/csv;charset=utf-8'}),'hasil_115_pengujian_pembanding.csv'); + } + return
+
+
PENGUJIAN TAMBAHAN · MODEL v1

Pengujian Pembanding

115 ulasan baru
+

Hasil tersimpan dari fitur Prediksi Satu Ulasan. Model awal tetap dilatih dengan 456 ulasan; pengujian ini tidak melakukan training ulang.

+

Data pembanding berupa ulasan sintetis dengan label acuan usulan AI. Hasil ini terpisah dari evaluasi penelitian awal dan belum menggunakan label hasil validasi independen manusia.

+
+
Akurasi awal{pct(initial.accuracy)}115 data uji penelitian
+
Akurasi pembanding{pct(result.accuracy)}115 ulasan sintetis baru
+
Sesuai label acuan{result.correct} / {result.n}{result.n-result.correct} prediksi berbeda
+
+
+
+

Confusion Matrix

Baris menunjukkan label acuan; kolom menunjukkan hasil prediksi.

+ {error &&

{error}

} + + Confusion Matrix Pengujian Pembanding + Model v1. TN 73, FP 3, FN 10, TP 29. Akurasi 88,70 persen pada 115 ulasan sintetis. + + + Confusion Matrix Pengujian Pembanding + Model v1 · 115 ulasan baru · Akurasi {pct(result.accuracy)} + Hasil prediksi + Negatif (0) + Positif (1) + Label acuan + Negatif (0) + Positif (1) + {cells.map(c=>{c.value}{c.label})} + + +
+

Metrik per kelas

{result.classes.map((c,k)=>)}
KelasPrecisionRecallF1-scoreJumlah
{k===0?'Negatif':'Positif'}{pct(c.precision)}{pct(c.recall)}{pct(c.f1)}{c.support}

Akurasi = (TN + TP) / seluruh ulasan = (73 + 29) / 115 = {pct(result.accuracy)}.

+

Perbandingan dengan pengujian awal

KomponenAwal penelitianPengujian pembanding
Modelv1v1 yang sama
Jumlah data uji115115 ulasan baru
TN / FP / FN / TP70 / 6 / 2 / 3773 / 3 / 10 / 29
Akurasi{pct(initial.accuracy)}{pct(result.accuracy)}

Selisih {new Intl.NumberFormat('id-ID',{maximumFractionDigits:2}).format((initial.accuracy-result.accuracy)*100)} poin persentase dihitung sebelum pembulatan. Kedua pengujian memakai kumpulan data berbeda; hasil pembanding tidak menggantikan hasil awal.

+

Hasil 115 ulasan

Arsip pengujian model v1. Prediksi baru di web tidak otomatis mengubah evaluasi ini.

Lihat 13 prediksi yang berbeda
{data.rows.filter(r=>r.label_acuan!==r.label_prediksi).map(r=>)}
IDUlasanAcuanPrediksi
{r.id}{r.ulasan}{r.label_acuan===0?'Negatif':'Positif'}{r.label_prediksi===0?'Negatif':'Positif'}
+
; +} diff --git a/web_slearn/src/components/EvaluasiPembanding.jsx.backup-20261028-180124-636 b/web_slearn/src/components/EvaluasiPembanding.jsx.backup-20261028-180124-636 new file mode 100644 index 0000000..ace1f3f --- /dev/null +++ b/web_slearn/src/components/EvaluasiPembanding.jsx.backup-20261028-180124-636 @@ -0,0 +1,91 @@ +import { useRef, useState } from 'react'; +import data from '../data/pengujian-pembanding.json'; +import '../styles/evaluasi-pembanding.css'; + +const pct = (v) => new Intl.NumberFormat('id-ID', {style:'percent', minimumFractionDigits:2, maximumFractionDigits:2}).format(v); +function evaluate(rows) { + const cm = [[0,0],[0,0]]; + rows.forEach(r => { cm[r.label_acuan][r.label_prediksi]++; }); + const n = rows.length; + const classes = [0,1].map(k => { + const tp = cm[k][k], support = cm[k][0]+cm[k][1], predicted = cm[0][k]+cm[1][k]; + const precision = predicted ? tp/predicted : 0, recall = support ? tp/support : 0; + return {precision, recall, f1: precision+recall ? 2*precision*recall/(precision+recall) : 0, support}; + }); + return {cm, classes, n, correct:cm[0][0]+cm[1][1], accuracy:(cm[0][0]+cm[1][1])/n}; +} +const result = evaluate(data.rows); +const initial = {accuracy:(data.initialMatrix[0][0]+data.initialMatrix[1][1])/data.initialMatrix.flat().reduce((a,b)=>a+b,0)}; +const cells = [ + {x:260,y:170,value:result.cm[0][0],label:'TN',correct:true}, + {x:500,y:170,value:result.cm[0][1],label:'FP',correct:false}, + {x:260,y:350,value:result.cm[1][0],label:'FN',correct:false}, + {x:500,y:350,value:result.cm[1][1],label:'TP',correct:true}, +]; +function download(blob, name) { + const url = URL.createObjectURL(blob), a = document.createElement('a'); + a.href=url; a.download=name; a.click(); setTimeout(()=>URL.revokeObjectURL(url), 1000); +} + +export default function EvaluasiPembanding() { + const svgRef = useRef(null); + const [busy,setBusy] = useState(false), [error,setError] = useState(''); + async function savePNG() { + setBusy(true); setError(''); let url; + try { + const source = new XMLSerializer().serializeToString(svgRef.current); + url = URL.createObjectURL(new Blob([source],{type:'image/svg+xml;charset=utf-8'})); + const img = new Image(); + await new Promise((resolve,reject)=>{img.onload=resolve;img.onerror=()=>reject(new Error('Gambar gagal dibuat. Silakan coba lagi.'));img.src=url;}); + const canvas = document.createElement('canvas'); canvas.width=2700; canvas.height=1920; + const ctx=canvas.getContext('2d'); if(!ctx) throw new Error('Browser tidak mendukung ekspor gambar.'); + ctx.drawImage(img,0,0,2700,1920); + const blob=await new Promise(resolve=>canvas.toBlob(resolve,'image/png')); + if(!blob) throw new Error('Unduhan gambar gagal dibuat.'); + download(blob,'confusion_matrix_pembanding_v1.png'); + } catch(e) {setError(e.message);} finally {if(url) URL.revokeObjectURL(url);setBusy(false);} + } + function saveCSV() { + const headers=['id','ulasan','label_acuan','label_prediksi','model_version','prediction_id','kondisi']; + const quote=v=>'"'+String(v??'').replaceAll('"','""')+'"'; + const csv=[headers.join(','),...data.rows.map(r=>headers.map(h=>quote(r[h])).join(','))].join('\r\n'); + download(new Blob(['\uFEFF'+csv],{type:'text/csv;charset=utf-8'}),'hasil_115_pengujian_pembanding.csv'); + } + return
+
+
PENGUJIAN TAMBAHAN · MODEL v1

Pengujian Pembanding

115 ulasan baru
+

Hasil tersimpan dari fitur Prediksi Satu Ulasan. Model awal tetap dilatih dengan 456 ulasan; pengujian ini tidak melakukan training ulang.

+

Data pembanding berupa ulasan sintetis dengan label acuan usulan AI. Hasil ini terpisah dari evaluasi penelitian awal dan belum menggunakan label hasil validasi independen manusia.

+
+
Akurasi awal{pct(initial.accuracy)}115 data uji penelitian
+
Akurasi pembanding{pct(result.accuracy)}115 ulasan sintetis baru
+
Sesuai label acuan{result.correct} / {result.n}{result.n-result.correct} prediksi berbeda
+
+
+
+

Confusion Matrix

Baris menunjukkan label acuan; kolom menunjukkan hasil prediksi.

+ {error &&

{error}

} + + Confusion Matrix Pengujian Pembanding + Model v1. TN 73, FP 3, FN 10, TP 29. Akurasi 88,70 persen pada 115 ulasan sintetis. + + + Confusion Matrix Pengujian Pembanding + Model v1 · 115 ulasan baru · Akurasi {pct(result.accuracy)} + Hasil prediksi + Negatif (0) + Positif (1) + Label acuan + Negatif (0) + Positif (1) + {cells.map(c=>{c.value}{c.label})} + 102 prediksi sesuai label acuan · 13 prediksi berbeda + Data sintetis · Label acuan usulan AI · Evaluasi tambahan + + +
+

Metrik per kelas

{result.classes.map((c,k)=>)}
KelasPrecisionRecallF1-scoreJumlah
{k===0?'Negatif':'Positif'}{pct(c.precision)}{pct(c.recall)}{pct(c.f1)}{c.support}

Akurasi = (TN + TP) / seluruh ulasan = (73 + 29) / 115 = {pct(result.accuracy)}.

+

Perbandingan dengan pengujian awal

KomponenAwal penelitianPengujian pembanding
Modelv1v1 yang sama
Jumlah data uji115115 ulasan baru
TN / FP / FN / TP70 / 6 / 2 / 3773 / 3 / 10 / 29
Akurasi{pct(initial.accuracy)}{pct(result.accuracy)}

Selisih {new Intl.NumberFormat('id-ID',{maximumFractionDigits:2}).format((initial.accuracy-result.accuracy)*100)} poin persentase dihitung sebelum pembulatan. Kedua pengujian memakai kumpulan data berbeda; hasil pembanding tidak menggantikan hasil awal.

+

Hasil 115 ulasan

Arsip pengujian model v1. Prediksi baru di web tidak otomatis mengubah evaluasi ini.

Lihat 13 prediksi yang berbeda
{data.rows.filter(r=>r.label_acuan!==r.label_prediksi).map(r=>)}
IDUlasanAcuanPrediksi
{r.id}{r.ulasan}{r.label_acuan===0?'Negatif':'Positif'}{r.label_prediksi===0?'Negatif':'Positif'}
+
; +} diff --git a/web_slearn/src/components/ModelContext.jsx b/web_slearn/src/components/ModelContext.jsx new file mode 100644 index 0000000..a1f8e70 --- /dev/null +++ b/web_slearn/src/components/ModelContext.jsx @@ -0,0 +1,6 @@ +export function ModelVersion({ value }) { + return Model saat analisis: {value || "Tidak tercatat"}; +} +export function TfidfNote() { + return

TF-IDF menunjukkan bobot kata dalam ulasan, bukan ukuran langsung pengaruh kata terhadap sentimen positif atau negatif.

; +} diff --git a/web_slearn/src/components/Sidebar.jsx b/web_slearn/src/components/Sidebar.jsx new file mode 100644 index 0000000..f8c25e8 --- /dev/null +++ b/web_slearn/src/components/Sidebar.jsx @@ -0,0 +1,141 @@ +import { + NavLink, + useLocation +} from "react-router-dom"; + + +const menus = [ + {name: "Dashboard", path: "/"}, + {name: "Prediksi Satu Ulasan", path: "/prediksi"}, + {name: "Analisis CSV", path: "/analisis-csv"}, + {name: "Riwayat Analisis", path: "/riwayat"}, + {name: "Training & Riwayat Model", path: "/riwayat-model"}, + {name: "Penelitian & Model Awal", path: "/penelitian-model"} +]; + + +function Sidebar() { + const location = + useLocation(); + + + /* ========================================================== + MENENTUKAN MENU AKTIF + ========================================================== */ + + const isMenuActive = + (menuPath) => { + const pathname = + location.pathname; + + + if (menuPath === "/") { + return pathname === "/"; + } + + + /* + Halaman detail hasil analisis merupakan + bagian dari Riwayat Analisis. + */ + + if ( + menuPath === "/riwayat" + ) { + return ( + pathname === + "/riwayat" || + pathname.startsWith( + "/hasil-analisis/" + ) + ); + } + + + return ( + pathname === menuPath + ); + }; + + + /* ========================================================== + TAMPILAN + ========================================================== */ + + return ( + + ); +} + + +export default Sidebar; diff --git a/web_slearn/src/components/TrainingDetails.jsx b/web_slearn/src/components/TrainingDetails.jsx new file mode 100644 index 0000000..6efb675 --- /dev/null +++ b/web_slearn/src/components/TrainingDetails.jsx @@ -0,0 +1,137 @@ + +const rate = (v) => `${(v * 100).toFixed(2).replace(".", ",")}%`; + +export function SkippedRows({ audit, legacy = false, status = "completed" }) { + if (!audit) { + return legacy + ?

Rincian baris yang dilewati belum dicatat untuk versi lama ini.

+ : null; + } + + return ( +
+

+ + {audit.uploaded_rows} diunggah → {audit.accepted_rows} diterima →{" "} + {audit.skipped_rows.length} dilewati. + +

+ + {audit.selected_column && ( +

+ Kolom yang digunakan: {audit.selected_column} +

+ )} + +

+ {status === "completed" && audit.accepted_rows > 0 + ? `Data latih: ${audit.training_before} sebelum penambahan → ${audit.training_before + audit.accepted_rows} setelah training berhasil.` + : `Data latih tetap ${audit.training_before}.`} +

+ + {!!audit.skipped_rows.length && ( +
+ + Lihat Data yang Dilewati ({audit.skipped_rows.length}) + + +

+ Nomor baris adalah urutan record CSV, termasuk judul kolom + sebagai baris 1. Ulasan dengan baris baru di dalam tanda + kutip dihitung satu record. +

+ +
+ + + + + + + + + + + {audit.skipped_rows.map((r) => ( + + + + + + + + ))} + +
Baris CSVUlasanAlasan dilewati
{r.source_row_number} + {r.text || "(kosong)"} + + + {r.reason} + +
+
+
+ )} +
+ ); +} + +export function VersionComparison({ selected, original }) { + if (!original || selected.version === "v1") { + return ( +

+ Model penelitian awal: {selected.jumlah_data_training} data latih + dan {selected.jumlah_data_testing} data uji. +

+ ); + } + + const metrics = [ + ["Akurasi", m => m.akurasi], + ["Precision", m => m.classification_report["weighted avg"].precision], + ["Recall", m => m.classification_report["weighted avg"].recall], + ["F1-Score", m => m.classification_report["weighted avg"]["f1-score"]] + ]; + + return ( +
+

Perbandingan dengan model awal v1

+ +

+ {selected.jumlah_data_training} data latih. Evaluasi pada{" "} + {selected.jumlah_data_testing} data uji tetap yang sama. +

+ +
+ + + + + + + + + + + + {metrics.map(([name, get]) => ( + + + + + + + ))} + +
Metrikv1{selected.version}Selisih (poin persentase)
{name}{rate(get(original))}{rate(get(selected))} + {((get(selected) - get(original)) * 100).toFixed(2)} +
+
+ +

+ Precision, Recall, dan F1-Score menggunakan weighted average, + yaitu rata-rata yang memperhitungkan jumlah data pada setiap kelas. +

+
+ ); +} diff --git a/web_slearn/src/index.css b/web_slearn/src/index.css new file mode 100644 index 0000000..97ff932 --- /dev/null +++ b/web_slearn/src/index.css @@ -0,0 +1,18 @@ +html { + font-family: Inter, "Segoe UI", Arial, sans-serif; + background: #f5f7fb; +} + +html, body, #root { + width: 100%; + min-height: 100%; + margin: 0; +} + +body { + min-width: 320px; +} + +button, input, select, textarea { + font: inherit; +} \ No newline at end of file diff --git a/web_slearn/src/layouts/MainLayout.jsx b/web_slearn/src/layouts/MainLayout.jsx new file mode 100644 index 0000000..a9270fb --- /dev/null +++ b/web_slearn/src/layouts/MainLayout.jsx @@ -0,0 +1,18 @@ +import { Outlet } from "react-router-dom"; +import Sidebar from "../components/Sidebar"; +import "../styles/layout.css"; + +function MainLayout() { + return ( +
+ +
+
+ +
+
+
+ ); +} + +export default MainLayout; \ No newline at end of file diff --git a/web_slearn/src/main.jsx b/web_slearn/src/main.jsx new file mode 100644 index 0000000..466b579 --- /dev/null +++ b/web_slearn/src/main.jsx @@ -0,0 +1,13 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { BrowserRouter } from "react-router-dom"; +import App from "./App"; +import "./index.css"; + +createRoot(document.getElementById("root")).render( + + + + + +); \ No newline at end of file diff --git a/web_slearn/src/pages/AnalisisCSV.jsx b/web_slearn/src/pages/AnalisisCSV.jsx new file mode 100644 index 0000000..5c6c15b --- /dev/null +++ b/web_slearn/src/pages/AnalisisCSV.jsx @@ -0,0 +1,917 @@ + +import { useEffect, useMemo, useRef, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import Papa from "papaparse"; + +import { analisisCSV, ambilRiwayatModel } from "../services/api"; +import "../styles/analisis-csv.css"; + +const kandidatKolomUlasan = [ + "komentar/ulasan mahasiswa terhadap aplikasi s-learn", + "komentar", + "ulasan", + "review", + "comment", + "teks", + "text", + "feedback", + "kritik", + "saran", + "pendapat" +]; + +const kandidatKolomNonUlasan = [ + "no", + "nomor", + "id", + "nim", + "nama", + "nama mahasiswa", + "jenis kelamin", + "gender", + "prodi", + "program studi", + "semester", + "angkatan", + "golongan", + "tanggal", + "timestamp", + "email", + "telepon", + "no hp", + "nomor hp" +]; + +function normalisasiNamaKolom(nama) { + return String(nama || "") + .trim() + .toLowerCase() + .replace(/[_-]+/g, " ") + .replace(/\s+/g, " "); +} + +function deteksiKolomUlasan(columns) { + const normalized = columns.map((column) => ({ + original: column, + normalized: normalisasiNamaKolom(column) + })); + + for (const kandidat of kandidatKolomUlasan) { + const target = normalisasiNamaKolom(kandidat); + + const exact = normalized.find( + (item) => item.normalized === target + ); + + if (exact) return exact.original; + } + + const sebagian = normalized.find((item) => + [ + "komentar", + "ulasan", + "review", + "comment", + "feedback", + "kritik", + "saran", + "pendapat" + ].some((kata) => item.normalized.includes(kata)) + ); + + return sebagian?.original || ""; +} + +function formatUkuranFile(size) { + const bytes = Number(size || 0); + + if (bytes < 1024) return `${bytes} B`; + + const kb = bytes / 1024; + + if (kb < 1024) { + return `${kb.toFixed(1)} KB`; + } + + const mb = kb / 1024; + + if (mb < 1024) { + return `${mb.toFixed(2)} MB`; + } + + return `${(mb / 1024).toFixed(2)} GB`; +} + +function tampakSebagaiAngka(value) { + const text = String(value || "").trim(); + + if (!text) return false; + + const punyaAngka = /\d/.test(text); + const punyaHuruf = /[a-zA-Z]/.test(text); + + return punyaAngka && !punyaHuruf; +} + +function evaluasiKesesuaianKolom(column, rows) { + if (!column) { + return { + cocok: false, + jenis: "empty", + pesan: "Pilih kolom yang berisi teks ulasan." + }; + } + + const namaKolom = normalisasiNamaKolom(column); + + const values = rows + .map((row) => String(row[column] ?? "").trim()) + .filter(Boolean); + + if (values.length === 0) { + return { + cocok: false, + jenis: "warning", + pesan: "Kolom ini tidak memiliki teks yang dapat dianalisis." + }; + } + + const namaTampakUlasan = kandidatKolomUlasan.some((kandidat) => { + const target = normalisasiNamaKolom(kandidat); + + return ( + namaKolom === target || + namaKolom.includes(target) + ); + }); + + if (namaTampakUlasan) { + return { + cocok: true, + jenis: "success", + pesan: "" + }; + } + + const namaTampakMetadata = kandidatKolomNonUlasan.some( + (kandidat) => namaKolom === normalisasiNamaKolom(kandidat) + ); + + if (namaTampakMetadata) { + return { + cocok: false, + jenis: "warning", + pesan: `Kolom "${column}" tampaknya bukan kolom teks ulasan. Pilih kolom yang berisi komentar atau ulasan.` + }; + } + + const jumlahNumerik = values.filter(tampakSebagaiAngka).length; + + const rasioNumerik = + values.length > 0 + ? jumlahNumerik / values.length + : 0; + + const rataPanjang = + values.reduce( + (total, value) => total + value.length, + 0 + ) / values.length; + + const rataKata = + values.reduce((total, value) => { + const jumlahKata = value + .split(/\s+/) + .filter(Boolean).length; + + return total + jumlahKata; + }, 0) / values.length; + + if (rasioNumerik >= 0.5) { + return { + cocok: false, + jenis: "warning", + pesan: + "Isi kolom ini didominasi angka sehingga tampaknya bukan teks ulasan." + }; + } + + if (rataPanjang < 8 || (rataKata < 2 && rataPanjang < 18)) { + return { + cocok: false, + jenis: "warning", + pesan: + "Isi kolom ini tampak terlalu pendek untuk digunakan sebagai teks ulasan." + }; + } + + return { + cocok: true, + jenis: "success", + pesan: "" + }; +} + +function kolomPerluDisamarkan(column) { + const nama = normalisasiNamaKolom(column); + + return [ + "nim", + "nama", + "nama mahasiswa", + "email", + "telepon", + "no hp", + "nomor hp" + ].includes(nama); +} + +function samarkanNama(value) { + return String(value || "") + .trim() + .split(/\s+/) + .map((kata) => { + if (!kata) return ""; + if (kata.length === 1) return `${kata}•`; + + return `${kata.charAt(0)}${"•".repeat( + Math.min(kata.length - 1, 5) + )}`; + }) + .join(" "); +} + +function formatPreviewValue(column, value) { + const text = String(value ?? ""); + const nama = normalisasiNamaKolom(column); + + if (!text.trim()) return "-"; + + if (nama === "nama" || nama === "nama mahasiswa") { + return samarkanNama(text); + } + + if (nama === "nim") { + const clean = text.trim(); + + if (clean.length <= 4) { + return "••••"; + } + + return `${clean.slice(0, 2)}••••${clean.slice(-2)}`; + } + + if ( + nama === "email" || + nama === "telepon" || + nama === "no hp" || + nama === "nomor hp" + ) { + return "••••••••"; + } + + return text; +} + +function AnalisisCSV() { + const navigate = useNavigate(); + const fileInputRef = useRef(null); + const submitLockRef = useRef(false); + + const [step, setStep] = useState(1); + const [file, setFile] = useState(null); + const [columns, setColumns] = useState([]); + const [rows, setRows] = useState([]); + const [selectedColumn, setSelectedColumn] = useState(""); + const [detectedColumn, setDetectedColumn] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + + // =================================================== + // TAMBAHAN: INFORMASI VERSI MODEL YANG SEDANG AKTIF + // =================================================== + const [modelAktif, setModelAktif] = useState(null); + const [loadingModel, setLoadingModel] = useState(true); + + useEffect(() => { + let mounted = true; + + async function muatModelAktif() { + try { + const response = await ambilRiwayatModel(); + const versions = response?.data?.versions || []; + const active = versions.find((model) => model.active); + + if (mounted) { + setModelAktif(active?.version || null); + } + } catch (err) { + console.error("Gagal mengambil model aktif:", err); + + if (mounted) { + setModelAktif(null); + } + } finally { + if (mounted) { + setLoadingModel(false); + } + } + } + + muatModelAktif(); + + return () => { + mounted = false; + }; + }, []); + + // =================================================== + // PREVIEW DAN VALIDASI DATA CSV + // =================================================== + const previewRows = rows.slice(0, 5); + + const jumlahUlasanValid = selectedColumn + ? rows.filter((row) => { + const value = row[selectedColumn]; + + return ( + value !== undefined && + value !== null && + String(value).trim() !== "" + ); + }).length + : 0; + + const jumlahBarisKosong = selectedColumn + ? Math.max(0, rows.length - jumlahUlasanValid) + : 0; + + const evaluasiKolom = useMemo( + () => evaluasiKesesuaianKolom(selectedColumn, rows), + [selectedColumn, rows] + ); + + const adaKolomDisamarkan = useMemo( + () => columns.some(kolomPerluDisamarkan), + [columns] + ); + + const pilihFile = () => { + fileInputRef.current?.click(); + }; + + // =================================================== + // MEMBACA FILE CSV + // =================================================== + const parseCSV = (selectedFile) => { + setError(""); + + Papa.parse(selectedFile, { + header: true, + skipEmptyLines: "greedy", + transformHeader: (header) => header.trim(), + + complete: (result) => { + const fields = result.meta?.fields || []; + + if (fields.length === 0) { + setError( + "CSV tidak memiliki header kolom yang dapat dibaca." + ); + return; + } + + const headerKosong = fields.some( + (field) => !String(field).trim() + ); + + if (headerKosong) { + setError( + "CSV memiliki nama kolom yang kosong. Periksa kembali header file." + ); + return; + } + + if (result.data.length === 0) { + setError( + "CSV tidak memiliki data untuk dianalisis." + ); + return; + } + + const detected = deteksiKolomUlasan(fields); + + setFile(selectedFile); + setColumns(fields); + setRows(result.data); + setDetectedColumn(detected); + setSelectedColumn(detected); + setStep(2); + }, + + error: () => { + setError( + "File CSV tidak dapat dibaca. Periksa kembali format file." + ); + } + }); + }; + + const handleFile = (selectedFile) => { + if (!selectedFile) return; + + setError(""); + + if (!selectedFile.name.toLowerCase().endsWith(".csv")) { + setError("File harus menggunakan format .csv."); + return; + } + + parseCSV(selectedFile); + }; + + const handleInputFile = (event) => { + handleFile(event.target.files?.[0]); + }; + + const handleDrop = (event) => { + event.preventDefault(); + + handleFile(event.dataTransfer.files?.[0]); + }; + + // =================================================== + // PROSES ANALISIS SENTIMEN CSV + // =================================================== + const mulaiAnalisis = async () => { + if (submitLockRef.current || loading) { + return; + } + + if (!file) { + setError( + "File CSV belum tersedia. Silakan unggah file terlebih dahulu." + ); + return; + } + + if (!selectedColumn) { + setError( + "Pilih kolom yang berisi ulasan sebelum memulai analisis." + ); + return; + } + + if (!evaluasiKolom.cocok) { + setError( + evaluasiKolom.pesan || + "Kolom yang dipilih tampaknya bukan teks ulasan." + ); + return; + } + + const reviews = rows + .map((row, index) => ({ + source_row_number: index + 1, + text: String(row[selectedColumn] ?? "").trim() + })) + .filter((item) => item.text !== ""); + + if (reviews.length === 0) { + setError( + "Kolom yang dipilih tidak memiliki ulasan yang dapat dianalisis." + ); + return; + } + + submitLockRef.current = true; + setError(""); + setLoading(true); + + try { + const hasil = await analisisCSV( + file.name, + selectedColumn, + reviews + ); + + if ( + hasil.status !== "success" || + !hasil.data?.batch_id + ) { + throw new Error( + "Hasil analisis dari server tidak valid." + ); + } + + setStep(3); + + navigate( + `/hasil-analisis/${hasil.data.batch_id}`, + { + state: { + hasilAnalisis: hasil.data, + sumberHalaman: "analisis-csv", + analisisBaruSelesai: true + } + } + ); + } catch (err) { + setError( + err.message || + "Terjadi kesalahan saat menganalisis file CSV." + ); + } finally { + submitLockRef.current = false; + setLoading(false); + } + }; + + // =================================================== + // RESET ANALISIS CSV + // =================================================== + const resetAnalisis = () => { + if (loading) return; + + submitLockRef.current = false; + + setStep(1); + setFile(null); + setColumns([]); + setRows([]); + setSelectedColumn(""); + setDetectedColumn(""); + setLoading(false); + setError(""); + + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + }; + + // =================================================== + // TAMPILAN HALAMAN ANALISIS CSV + // =================================================== + return ( +
+ + {/* HEADING DAN INFORMASI MODEL AKTIF */} +
+

Analisis CSV

+ +

+ Unggah file CSV berisi ulasan S-Learn, lalu pilih + kolom yang akan dianalisis. +

+ + {/* INFORMASI VERSI MODEL AKTIF */} +
+ + + + Model yang sedang digunakan: + + + + {loadingModel + ? "Memuat..." + : modelAktif || "Tidak tersedia"} + +
+
+ + {/* STEPPER — DIPERTAHANKAN */} +
+
= 1 ? "active" : "" + }`} + > + 1 +

Unggah File

+
+ +
= 2 ? "active" : "" + }`} + /> + +
= 2 ? "active" : "" + }`} + > + 2 +

Periksa Data

+
+ +
= 3 ? "active" : "" + }`} + /> + +
= 3 ? "active" : "" + }`} + > + 3 +

Hasil

+
+
+ + {error && ( +
+ {error} +
+ )} + + {/* STEP 1 — UNGGAH FILE CSV */} + {step === 1 && ( +
+
event.preventDefault()} + onDrop={handleDrop} + > +
+ ↑ +
+ +

Unggah File CSV

+ +

+ Pilih file CSV atau tarik file ke area ini + untuk memulai analisis. +

+ + + + + + + Format .csv · Header wajib tersedia + +
+
+ )} + + {/* STEP 2 — PREVIEW DAN PEMILIHAN KOLOM */} + {step === 2 && ( + <> +
+
+
+

{file?.name}

+ +

+ {formatUkuranFile(file?.size)} + · + {rows.length} baris + · + {columns.length} kolom +

+
+ + +
+ +
+

5 Baris Pertama

+ + {adaKolomDisamarkan && ( +

+ +

+ )} +
+ +
+ + + + + + {columns.map((column) => ( + + ))} + + + + + {previewRows.map((row, rowIndex) => ( + + + + {columns.map((column) => ( + + ))} + + ))} + +
+ # + + {column} +
+ {rowIndex + 1} + + {formatPreviewValue( + column, + row[column] + )} +
+
+
+ + {/* PILIH KOLOM ULASAN */} +
+
+
+
+ + + {detectedColumn && + selectedColumn === detectedColumn && ( + + Terdeteksi otomatis + + )} +
+
+ + +
+ + {/* STATUS KOLOM VALID */} + {selectedColumn && evaluasiKolom.cocok && ( +
+ + ✓ + + +
+ + {jumlahBarisKosong === 0 + ? `${jumlahUlasanValid} ulasan siap dianalisis.` + : `${jumlahUlasanValid} dari ${rows.length} baris berisi teks.`} + + + {jumlahBarisKosong > 0 && ( +

+ {jumlahBarisKosong} baris kosong akan + dilewati saat analisis. +

+ )} +
+
+ )} + + {/* STATUS KOLOM TIDAK SESUAI */} + {selectedColumn && !evaluasiKolom.cocok && ( +
+ + ! + + +
+ + Kolom belum sesuai + + +

+ {evaluasiKolom.pesan} +

+
+
+ )} + + {/* INFORMASI PENYIMPANAN */} +
+ i + +

+ Hanya isi kolom yang dipilih yang diproses dan + disimpan sebagai data analisis. +

+
+ + {/* TOMBOL MULAI ANALISIS */} +
+ +
+
+ + )} +
+ ); +} + +export default AnalisisCSV; diff --git a/web_slearn/src/pages/Dashboard.jsx b/web_slearn/src/pages/Dashboard.jsx new file mode 100644 index 0000000..f5cbc36 --- /dev/null +++ b/web_slearn/src/pages/Dashboard.jsx @@ -0,0 +1,50 @@ +import { useEffect, useState } from "react"; +import { Link } from "react-router-dom"; +import { ambilRiwayatAnalisis, ambilRiwayatPrediksi, ambilRiwayatModel } from "../services/api"; +import { ModelVersion } from "../components/ModelContext"; +import "../styles/dashboard.css"; +import "../styles/riwayat-model.css"; +const percent = (v) => `${(v * 100).toFixed(2).replace('.', ',')}%`; +const date = (v) => v ? new Date(v).toLocaleString('id-ID') : 'Tidak tercatat'; +export default function Dashboard() { + const [state,setState]=useState({}); + useEffect(()=>{ + let alive=true; + async function load() { + const results=await Promise.allSettled([ambilRiwayatModel(),ambilRiwayatAnalisis(),ambilRiwayatPrediksi()]); + if(alive) setState(Object.fromEntries(['models','csv','single'].map((k,i)=>[k,results[i].status==='fulfilled'?{data:results[i].value.data}:{error:results[i].reason.message}]))); + } + load(); + window.addEventListener('focus',load); + return ()=>{alive=false;window.removeEventListener('focus',load);}; + },[]); + const active=state.models?.data?.versions.find(v=>v.active); + const latest=state.csv?.data?.[0]; + const total=latest?.total_data || 0; + const positive=total ? latest.total_positive/total*100 : 0; + const card=(label,value,note)=>
{label}{value}{note}
; + return
+

Dashboard

Model yang digunakan, aktivitas prediksi, dan hasil analisis terakhir.

Prediksi Ulasan Baru
+

Model yang sedang digunakan

Model ini digunakan untuk prediksi satu ulasan dan analisis CSV berikutnya.

+ {state.models?.error ?

Informasi model tidak dapat dimuat: {state.models.error}

: !active ?

Memuat model aktif...

: <> +
Versi aktif{active.version}
Akurasi pada data uji{percent(active.akurasi)}
Data latih model{active.jumlah_data_training}
Data uji tetap{active.jumlah_data_testing}
+

Selesai dilatih: {date(active.created_at)}. Akurasi adalah hasil evaluasi pada data uji, bukan jaminan setiap prediksi benar.

Kelola model dan lihat evaluasi →} +
+

Aktivitas analisis

Ulasan yang dianalisis tidak otomatis ditambahkan ke data latih. Analisis berulang tetap dihitung sebagai aktivitas terpisah.

+
+ {card('Prediksi satu ulasan',state.single?.data?.length ?? '—','Hasil tersimpan dari menu Prediksi Satu Ulasan')} + {card('Analisis CSV selesai',state.csv?.data?.length ?? '—','Jumlah batch tersimpan')} + {card('Ulasan dalam seluruh batch CSV',state.csv?.data?.reduce((n,r)=>n+r.total_data,0) ?? '—','Jumlah baris yang diprediksi, bukan data latih')} +
+ {[state.csv?.error,state.single?.error].filter(Boolean).map((e,i)=>

Data aktivitas tidak dapat dimuat: {e}

)} +
+

Hasil analisis CSV terakhir

Distribusi hasil prediksi pada file terakhir, bukan distribusi dataset training.

+ {state.csv?.error ?

Hasil terakhir tidak tersedia karena riwayat gagal dimuat.

: !state.csv ?

Memuat hasil...

: !latest ?

Belum ada analisis CSV. Analisis CSV baru

: <> +

{latest.file_name} · {date(latest.created_at)}

+
{total}Ulasan
+

Positif: {latest.total_positive} ({positive.toFixed(1)}%)

Negatif: {latest.total_negative} ({(total?latest.total_negative/total*100:0).toFixed(1)}%)

Lihat Hasil
+ } +
+

Penelitian awal: 571 ulasan (456 data latih dan 115 data uji). Lihat Penelitian & Model Awal →

+
; +} diff --git a/web_slearn/src/pages/HasilAnalisis.jsx b/web_slearn/src/pages/HasilAnalisis.jsx new file mode 100644 index 0000000..a785b13 --- /dev/null +++ b/web_slearn/src/pages/HasilAnalisis.jsx @@ -0,0 +1,1204 @@ +import { ModelVersion, TfidfNote } from "../components/ModelContext"; +import { + useEffect, + useMemo, + useRef, + useState +} from "react"; + +import { + useNavigate, + useParams +} from "react-router-dom"; + +import { + ambilHasilAnalisis, + ambilRiwayatAnalisis +} from "../services/api"; + +import "../styles/hasil-analisis.css"; + +const ITEMS_PER_PAGE = 10; + +const NAMA_BULAN = [ + "Januari", + "Februari", + "Maret", + "April", + "Mei", + "Juni", + "Juli", + "Agustus", + "September", + "Oktober", + "November", + "Desember" +]; + +/* ============================================================ + FORMAT WAKTU + ============================================================ */ + +function formatWaktu(value) { + if (!value) { + return "-"; + } + + const [tanggalRaw, waktuRaw = ""] = + String(value).split("T"); + + const bagianTanggal = + tanggalRaw.split("-"); + + if (bagianTanggal.length !== 3) { + return value; + } + + const [year, month, day] = + bagianTanggal.map(Number); + + if ( + !year || + !month || + !day || + !NAMA_BULAN[month - 1] + ) { + return value; + } + + const jam = waktuRaw + ? waktuRaw + .slice(0, 5) + .replace(":", ".") + : "-"; + + return `${day} ${NAMA_BULAN[month - 1]} ${year}, ${jam}`; +} + +/* ============================================================ + CSV + ============================================================ */ + +function escapeCSV(value) { + const text = String(value ?? ""); + + return `"${text.replaceAll( + '"', + '""' + )}"`; +} + +/* ============================================================ + HALAMAN + ============================================================ */ + +function HasilAnalisis() { + const navigate = useNavigate(); + + const { + batchId + } = useParams(); + + const [ + dataAnalisis, + setDataAnalisis + ] = useState(null); + + const [ + batchNumber, + setBatchNumber + ] = useState(null); + + const [ + loading, + setLoading + ] = useState(true); + + const [ + error, + setError + ] = useState(""); + + const [ + search, + setSearch + ] = useState(""); + + const [ + filter, + setFilter + ] = useState("Semua"); + + const [ + detail, + setDetail + ] = useState(null); + + const [ + currentPage, + setCurrentPage + ] = useState(1); + + /* + Ref untuk posisi awal + Detail Hasil Analisis. + */ + const hasilDetailRef = useRef(null); + + /* ========================================================== + AMBIL DATA + ========================================================== */ + + useEffect(() => { + let aktif = true; + + async function loadData() { + if (!batchId) { + setError( + "ID batch analisis tidak tersedia." + ); + + setLoading(false); + + return; + } + + try { + setLoading(true); + setError(""); + + const response = + await ambilHasilAnalisis( + batchId + ); + + if ( + aktif && + response.status === "success" + ) { + setDataAnalisis( + response.data + ); + + try { + const historyResponse = + await ambilRiwayatAnalisis(); + + if ( + aktif && + historyResponse.status === + "success" + ) { + const urutanBatch = [ + ...(historyResponse.data || []) + ].sort( + (a, b) => { + const waktuA = + String( + a.created_at || "" + ); + + const waktuB = + String( + b.created_at || "" + ); + + const bandingWaktu = + waktuA.localeCompare( + waktuB + ); + + if ( + bandingWaktu !== 0 + ) { + return bandingWaktu; + } + + return String( + a.batch_id || "" + ).localeCompare( + String( + b.batch_id || "" + ) + ); + } + ); + + const posisiBatch = + urutanBatch.findIndex( + (item) => + item.batch_id === + batchId + ); + + setBatchNumber( + posisiBatch >= 0 + ? posisiBatch + 1 + : null + ); + } + } catch { + if (aktif) { + setBatchNumber(null); + } + } + } + } catch (err) { + if (aktif) { + setError( + err.message || + "Hasil analisis tidak dapat dimuat." + ); + } + } finally { + if (aktif) { + setLoading(false); + } + } + } + + loadData(); + + return () => { + aktif = false; + }; + }, [batchId]); + + /* ========================================================== + RESET HALAMAN SAAT SEARCH / FILTER BERUBAH + ========================================================== */ + + function changeSearch(value) { setSearch(value); setCurrentPage(1); } + function changeFilter(value) { setFilter(value); setCurrentPage(1); } + + /* ========================================================== + DATA + ========================================================== */ + + const hasil = useMemo(() => dataAnalisis?.results || [], [dataAnalisis]); + + const total = + Number( + dataAnalisis?.total_data || + 0 + ); + + const positif = + Number( + dataAnalisis + ?.total_positive || + 0 + ); + + const negatif = + Number( + dataAnalisis + ?.total_negative || + 0 + ); + + const persentasePositif = + total > 0 + ? ( + positif / + total + ) * 100 + : 0; + + /* ========================================================== + SEARCH + FILTER + ========================================================== */ + + const filteredData = + useMemo(() => { + const keyword = + search + .trim() + .toLowerCase(); + + return hasil.filter( + (item) => { + const cocokSearch = + !keyword || + String( + item.teks || "" + ) + .toLowerCase() + .includes( + keyword + ); + + const cocokFilter = + filter === "Semua" || + item.sentimen === + filter; + + return ( + cocokSearch && + cocokFilter + ); + } + ); + }, [ + hasil, + search, + filter + ]); + + /* ========================================================== + PAGINATION + ========================================================== */ + + const totalPages = + Math.max( + 1, + Math.ceil( + filteredData.length / + ITEMS_PER_PAGE + ) + ); + + const safeCurrentPage = + Math.min( + currentPage, + totalPages + ); + + const startIndex = + ( + safeCurrentPage - 1 + ) * ITEMS_PER_PAGE; + + const paginatedData = + filteredData.slice( + startIndex, + startIndex + + ITEMS_PER_PAGE + ); + + /* + Setelah halaman berubah, + otomatis naik ke awal tabel. + */ + const scrollKeAtasDetail = () => { + window.requestAnimationFrame( + () => { + if ( + !hasilDetailRef.current + ) { + return; + } + + const posisi = + hasilDetailRef.current + .getBoundingClientRect() + .top + + window.scrollY - + 18; + + window.scrollTo({ + top: posisi, + behavior: "smooth" + }); + } + ); + }; + + const halamanSebelumnya = + () => { + setCurrentPage( + (page) => + Math.max( + 1, + page - 1 + ) + ); + + scrollKeAtasDetail(); + }; + + const halamanBerikutnya = + () => { + setCurrentPage( + (page) => + Math.min( + totalPages, + page + 1 + ) + ); + + scrollKeAtasDetail(); + }; + + /* ========================================================== + DOWNLOAD + ========================================================== */ + + const unduhHasil = + () => { + if (!dataAnalisis) { + return; + } + + const header = [ + "ID Batch", "File Sumber", "Waktu Analisis", "Versi Model", + "No", + "Ulasan", + "Cleaning", + "Case Folding", + "Normalisasi", + "Tokenizing", + "Stopword Removal", + "Stemming", + "Label", + "Sentimen" + ]; + + const rowsCSV = + hasil.map( + (item) => [ + dataAnalisis.batch_id, dataAnalisis.file_name, dataAnalisis.created_at, dataAnalisis.model_version || "Tidak tercatat", + item.source_row_number, + item.teks, + item.preprocessing + ?.cleaning, + item.preprocessing + ?.case_folding, + item.preprocessing + ?.normalization, + Array.isArray( + item.preprocessing + ?.tokenizing + ) + ? JSON.stringify(item.preprocessing.tokenizing) + : "", + item.preprocessing + ?.stopword_removal, + item.preprocessing + ?.stemming, + item.label, + item.sentimen + ] + ); + + const csvContent = [ + header + .map( + escapeCSV + ) + .join(","), + + ...rowsCSV.map( + (row) => + row + .map( + escapeCSV + ) + .join(",") + ) + ].join("\n"); + + const blob = + new Blob( + [ + "\uFEFF" + + csvContent + ], + { + type: + "text/csv;charset=utf-8;" + } + ); + + const url = + URL.createObjectURL( + blob + ); + + const link = + document.createElement( + "a" + ); + + link.href = + url; + + link.download = + `hasil_${dataAnalisis.batch_id}.csv`; + + document.body.appendChild( + link + ); + + link.click(); + + link.remove(); + + URL.revokeObjectURL( + url + ); + }; + + /* ========================================================== + LOADING + ========================================================== */ + + if (loading) { + return ( +
+
+

+ Mengambil Hasil Analisis +

+ +

+ Data analisis sedang dimuat. +

+
+
+ ); + } + + /* ========================================================== + ERROR + ========================================================== */ + + if ( + error || + !dataAnalisis + ) { + return ( +
+
+

+ Batch tidak ditemukan +

+ +

+ {error || + "Batch analisis tidak ditemukan."} +

+ +
+ + + +
+
+
+ ); + } + + return ( +
+ {/* ====================================================== + BACK + ====================================================== */} + + + + {/* ====================================================== + HEADING + ====================================================== */} + +
+
+

+ Hasil Analisis +

+ +

+ {dataAnalisis.file_name} + {" · "} + {formatWaktu( + dataAnalisis.created_at + )} +

+
+ +
+ + + +
+
+ + {/* ====================================================== + STATISTIK + ====================================================== */} + +
+
+ + Ulasan Dianalisis + + + + {total} + +
+ +
+ + Hasil prediksi positif + + + + {positif} + +
+ +
+ + Hasil prediksi negatif + + + + {negatif} + +
+
+ + {/* ====================================================== + DISTRIBUSI + ====================================================== */} + +
+
+

+ Distribusi Sentimen Hasil Prediksi +

+ +

+ Distribusi sentimen pada file CSV yang dianalisis. +

+
+ +
+
+ + Kolom Ulasan + + + + {dataAnalisis.selected_column} + +
+ +
+ + Batch + + + + {batchNumber + ? `Batch ${batchNumber}` + : "Batch -"} + +
+
+ +
+
0 + ? ( + `conic-gradient(` + + `#1f9d68 0% ${persentasePositif}%, ` + + `#db4e58 ${persentasePositif}% 100%)` + ) + : "#e9edf3" + }} + > +
+ + {total} + + + + Ulasan + +
+
+ +
+
+ + + + Positif: {dataAnalisis.total_positive} ({persentasePositif.toFixed(1)}%) + +
+ +
+ + + + Negatif: {dataAnalisis.total_negative} ({(100 - persentasePositif).toFixed(1)}%) + +
+
+
+
+ + {/* ====================================================== + DETAIL HASIL + ====================================================== */} + +
+
+
+

+ Detail Hasil Analisis +

+ +

+ Daftar ulasan dan hasil prediksi sentimen. +

+
+ +
+ + changeSearch( + event.target.value + ) + } + /> + + +
+
+ +
+ + + + + + + + + + + + {paginatedData.map( + (item) => ( + + + + + + + + + + ) + )} + + {paginatedData.length === 0 && ( + + + + )} + +
NoUlasanSentimenAksi
+ { + item.source_row_number + } + + {item.teks} + + + {item.sentimen} + + + +
+ Tidak ada ulasan yang sesuai. +
+
+ +
+ + Menampilkan{" "} + + {filteredData.length === 0 + ? 0 + : startIndex + 1} + + {" - "} + + {Math.min( + startIndex + + ITEMS_PER_PAGE, + filteredData.length + )} + + {" dari "} + + {filteredData.length} + + {" ulasan"} + + +
+ + + + {safeCurrentPage} + {" / "} + {totalPages} + + + +
+
+
+ + {/* ====================================================== + MODAL + ====================================================== */} + + {detail && ( +
+ setDetail(null) + } + > +
+ event.stopPropagation() + } + > +
+
+ + DETAIL ANALISIS + + +

+ Proses Analisis Ulasan +

+
+ + +
+ +
+ + Ulasan Asli + + +

+ {detail.teks} +

+
+ +
+ {[ + [ + "Cleaning", + detail.preprocessing + ?.cleaning + ], + [ + "Case Folding", + detail.preprocessing + ?.case_folding + ], + [ + "Normalisasi", + detail.preprocessing + ?.normalization + ], + [ + "Tokenizing", + Array.isArray( + detail.preprocessing + ?.tokenizing + ) + ? JSON.stringify( + detail + .preprocessing + .tokenizing + ) + : detail.preprocessing + ?.tokenizing + ], + [ + "Stopword Removal", + detail.preprocessing + ?.stopword_removal + ], + [ + "Stemming", + detail.preprocessing + ?.stemming + ] + ].map( + ( + [nama, proses], + index + ) => ( +
+ + {index + 1} + + +
+ + {nama} + + +

+ {proses || "-"} +

+
+
+ ) + )} +
+ +
+
+

+ Bobot TF-IDF Teratas +

+
+ + {detail.tfidf_top?.length > 0 ? ( + detail.tfidf_top.map( + (item, index) => { + const maxBobot = + Number( + detail + .tfidf_top[0] + ?.bobot || 1 + ); + + const width = + Math.min( + Number( + item.bobot + ) / + maxBobot, + 1 + ) * 100; + + return ( +
+ + {item.kata} + + +
+ +
+ + + {Number( + item.bobot + ) + .toFixed(6) + .replace( + ".", + "," + )} + +
+ ); + } + ) + ) : ( +

+ Tidak ada bobot TF-IDF yang tersedia. +

+ )} +
+ +
+ + Prediksi Sentimen + + + + {detail.sentimen} + +
+
+
+ )} +
+ ); +} + +export default HasilAnalisis; \ No newline at end of file diff --git a/web_slearn/src/pages/PenelitianModel.jsx b/web_slearn/src/pages/PenelitianModel.jsx new file mode 100644 index 0000000..2fa3134 --- /dev/null +++ b/web_slearn/src/pages/PenelitianModel.jsx @@ -0,0 +1,1824 @@ +import EvaluasiPembanding from "../components/EvaluasiPembanding"; +import { useEffect, useMemo, useRef, useState } from "react"; +import Papa from "papaparse"; +import { ambilModelPenelitian } from "../services/api"; +import "../styles/penelitian-model.css"; + +const tabs = [ + "Dataset & Pembagian Data", + "Preprocessing", + "Pembobotan TF-IDF", + "Model & Evaluasi", + "Pengujian Pembanding" +]; + +const ITEMS_PER_PAGE = 10; + + +function PenelitianModel() { + const [activeTab, setActiveTab] = useState("Dataset & Pembagian Data"); + + return ( +
+
+
+

Penelitian & Model Awal

+ +

+ Dokumentasi dataset, preprocessing, TF-IDF, dan evaluasi model awal (v1). + Model aktif dan hasil training baru tersedia di menu Training & Riwayat Model.{" "} + +

+
+
+ +
+ {tabs.map((tab) => ( + + ))} +
+ + {activeTab === "Dataset & Pembagian Data" && } + {activeTab === "Preprocessing" && } + {activeTab === "Pembobotan TF-IDF" && } + {activeTab === "Model & Evaluasi" && } + {activeTab === "Pengujian Pembanding" && } +
+ ); +} + +/* ============================================================ + DATASET + ============================================================ */ + +function DatasetTab() { + const [classDistribution, setClassDistribution] = useState({ + total: 571, + negative: 379, + positive: 192 + }); + + const [distributionLoading, setDistributionLoading] = useState(true); + const [distributionError, setDistributionError] = useState(""); + + useEffect(() => { + let aktif = true; + + Papa.parse("/data/hasil_preprocessing_slearn.csv", { + download: true, + header: true, + skipEmptyLines: true, + + transformHeader: (header) => + String(header).replace(/^\uFEFF/, "").trim(), + + complete: (results) => { + if (!aktif) return; + + const rows = (results.data || []).filter( + (row) => String(row.komentar || "").trim() !== "" + ); + + const negative = rows.filter( + (row) => String(row.label ?? "").trim() === "0" + ).length; + + const positive = rows.filter( + (row) => String(row.label ?? "").trim() === "1" + ).length; + + setClassDistribution({ + total: rows.length, + negative, + positive + }); + + setDistributionLoading(false); + }, + + error: (err) => { + if (!aktif) return; + + setDistributionError( + err?.message || "Distribusi kelas tidak dapat dimuat." + ); + + setDistributionLoading(false); + } + }); + + return () => { + aktif = false; + }; + }, []); + + const negativePercentage = + classDistribution.total > 0 + ? (classDistribution.negative / classDistribution.total) * 100 + : 0; + + const positivePercentage = + classDistribution.total > 0 + ? (classDistribution.positive / classDistribution.total) * 100 + : 0; + + const formatPercentage = (value) => + Number(value).toFixed(1).replace(".", ","); + + return ( +
+
+ + + + + +
+ +
+
+
+

Distribusi Kelas Dataset

+

Distribusi label pada 571 ulasan penelitian.

+
+ + {distributionLoading ? ( +
+ Memuat distribusi kelas... +
+ ) : ( + <> +
+
+
+
+ + +
+ Negatif + Label 0 +
+
+ +
+ {classDistribution.negative} + {formatPercentage(negativePercentage)}% +
+
+ +
+ +
+
+ +
+
+
+ + +
+ Positif + Label 1 +
+
+ +
+ {classDistribution.positive} + {formatPercentage(positivePercentage)}% +
+
+ +
+ +
+
+
+ +
+ 0 = Negatif + 1 = Positif +
+ + {distributionError && ( +

+ {distributionError} +

+ )} + + )} +
+ +
+
+

Pembagian Training dan Testing

+ +

+ Dataset dibagi menjadi 80% data training dan 20% data testing. +

+
+ +
+
+ 80% + 20% +
+ +

+ Data training digunakan untuk membentuk fitur TF-IDF dan melatih + model, sedangkan data testing digunakan untuk evaluasi model. +

+
+
+
+ +
+
+

Alur Pengolahan Data Penelitian

+
+ +
+ + + + + + + + + + + + + + + + + + + + + +
+
+
+ ); +} + +/* ============================================================ + PREPROCESSING + ============================================================ */ + +function formatTokenizedText(value) { + const text = String(value || "").trim(); + + if (!text) return "-"; + + try { + const parsed = JSON.parse(text.replace(/'/g, '"')); + + if (Array.isArray(parsed)) { + return parsed.join(" | "); + } + } catch { + // Format fallback. + } + + return text + .replace(/^\[/, "") + .replace(/\]$/, "") + .split(",") + .map((token) => + token + .trim() + .replace(/^["']|["']$/g, "") + ) + .filter(Boolean) + .join(" | "); +} + +function PreprocessingTab() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + const [search, setSearch] = useState(""); + const [filterLabel, setFilterLabel] = useState("Semua"); + const [currentPage, setCurrentPage] = useState(1); + const [detail, setDetail] = useState(null); + + const preprocessingResultRef = useRef(null); + + useEffect(() => { + let aktif = true; + + Papa.parse("/data/hasil_preprocessing_slearn.csv", { + download: true, + header: true, + skipEmptyLines: true, + + transformHeader: (header) => + String(header).replace(/^\uFEFF/, "").trim(), + + complete: (results) => { + if (!aktif) return; + + const rows = (results.data || []) + .filter( + (row) => String(row.komentar || "").trim() !== "" + ) + .map((row, index) => ({ + id: index + 1, + komentar: String(row.komentar || ""), + label: String(row.label ?? "").trim(), + clean_text: String(row.clean_text || ""), + case_folded_text: String(row.case_folded_text || ""), + normalized_text: String(row.normalized_text || ""), + tokenized_text: String(row.tokenized_text || ""), + stopword_text: String(row.stopword_text || ""), + stemmed_text: String(row.stemmed_text || "") + })); + + setData(rows); + setLoading(false); + }, + + error: (err) => { + if (!aktif) return; + + setError( + err?.message || "Hasil preprocessing tidak dapat dibaca." + ); + + setLoading(false); + } + }); + + return () => { + aktif = false; + }; + }, []); + + useEffect(() => { + setCurrentPage(1); + }, [search, filterLabel]); + + useEscapeModal(detail, () => setDetail(null)); + + const filteredData = useMemo(() => { + const keyword = search.trim().toLowerCase(); + + return data.filter((item) => { + const cocokSearch = + !keyword || + item.komentar.toLowerCase().includes(keyword) || + item.clean_text.toLowerCase().includes(keyword) || + item.case_folded_text.toLowerCase().includes(keyword) || + item.normalized_text.toLowerCase().includes(keyword) || + item.tokenized_text.toLowerCase().includes(keyword) || + item.stopword_text.toLowerCase().includes(keyword) || + item.stemmed_text.toLowerCase().includes(keyword) || + String(item.id).includes(keyword); + + const cocokLabel = + filterLabel === "Semua" || + (filterLabel === "Positif" && item.label === "1") || + (filterLabel === "Negatif" && item.label === "0"); + + return cocokSearch && cocokLabel; + }); + }, [data, search, filterLabel]); + + const totalPages = Math.max( + 1, + Math.ceil(filteredData.length / ITEMS_PER_PAGE) + ); + + const safePage = Math.min(currentPage, totalPages); + const startIndex = (safePage - 1) * ITEMS_PER_PAGE; + + const paginatedData = filteredData.slice( + startIndex, + startIndex + ITEMS_PER_PAGE + ); + + const scrollKeAtasPreprocessing = () => { + window.requestAnimationFrame(() => { + if (!preprocessingResultRef.current) return; + + const posisi = + preprocessingResultRef.current.getBoundingClientRect().top + + window.scrollY - + 18; + + window.scrollTo({ + top: posisi, + behavior: "smooth" + }); + }); + }; + + const halamanSebelumnyaPreprocessing = () => { + setCurrentPage((page) => Math.max(1, page - 1)); + scrollKeAtasPreprocessing(); + }; + + const halamanBerikutnyaPreprocessing = () => { + setCurrentPage((page) => Math.min(totalPages, page + 1)); + scrollKeAtasPreprocessing(); + }; + + const kataPenting = [ + "tidak", + "kurang", + "belum", + "bukan", + "bukannya", + "jangan", + "tanpa", + "tidaklah", + "malah", + "bisa", + "sangat", + "sering", + "sudah", + "masih", + "terlalu", + "baik", + "lebih", + "lama", + "jelas", + "masalah", + "selalu", + "siap", + "dapat", + "guna", + "benar", + "cukup", + "sekali", + "padahal", + "perlu", + "banyak", + "dan", + "terlihat", + "waktu", + "tambah", + "sebelum", + "sebelumnya", + "dekat", + "tiap", + "ada", + "kecil", + "dulu" + ]; + + return ( +
+
+
+
+

Contoh Kamus Normalisasi

+ +

+ Contoh perubahan kata yang digunakan pada proses normalisasi + data penelitian. +

+
+ +
+ + + +
+
+ +
+
+

Kata Penting yang Dipertahankan

+ +

+ Stopword final dibentuk dari stopword Bahasa Indonesia NLTK dan + partikel tambahan. Kata-kata berikut dikeluarkan dari daftar + stopword sehingga tetap dipertahankan selama preprocessing. +

+
+ +
+ {kataPenting.map((kata) => ( + {kata} + ))} +
+
+
+ +
+
+

Preprocessing 571 Ulasan

+ +

+ Hasil preprocessing ditampilkan sesuai enam tahapan penelitian: + Cleaning, Case Folding, Normalisasi, Tokenizing, Stopword Removal, + dan Stemming. +

+
+ +
+ setSearch(event.target.value)} + /> + + +
+ + {loading && ( +
+ Memuat hasil preprocessing penelitian... +
+ )} + + {!loading && error && ( +
+ {error} +
+ )} + + {!loading && !error && ( + <> +
+ {paginatedData.map((item) => ( +
+
+
+ Data ke-{item.id} + +
+ +

{item.komentar}

+ +
+
+ Hasil Akhir Stemming + {item.stemmed_text} +
+ + +
+
+
+ ))} + + {paginatedData.length === 0 && ( +
+ Tidak ada data yang sesuai. +
+ )} +
+ + + + )} +
+ + {detail && ( +
+ event.target === event.currentTarget && + setDetail(null) + } + > +
+
+
+
+

Data ke-{detail.id}

+
+ + +
+ +
+
+ Ulasan Asli + +
+ +

{detail.komentar}

+
+ +
+ + + + + + + + + + + +
+ +
+ Sentimen Penelitian + + + {detail.label === "1" ? "Positif" : "Negatif"} + +
+
+
+
+ )} +
+ ); +} + +/* ============================================================ + TF-IDF + ============================================================ */ + +function TFIDFTab() { + const [summary, setSummary] = useState(null); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + const [search, setSearch] = useState(""); + const [filterJenis, setFilterJenis] = useState("Semua"); + const [filterLabel, setFilterLabel] = useState("Semua"); + const [currentPage, setCurrentPage] = useState(1); + const [detail, setDetail] = useState(null); + + const tfidfResultRef = useRef(null); + + useEffect(() => { + const controller = new AbortController(); + + async function loadTFIDF() { + try { + setLoading(true); + setError(""); + + const response = await fetch("/data/tfidf_research.json", { + signal: controller.signal + }); + + if (!response.ok) { + throw new Error( + "Data pembobotan TF-IDF tidak dapat dibaca." + ); + } + + const json = await response.json(); + + if (!json || !Array.isArray(json.data)) { + throw new Error("Format data TF-IDF tidak sesuai."); + } + + setSummary(json.summary || null); + setData(json.data); + } catch (err) { + if (err.name !== "AbortError") { + setError( + err.message || "Data TF-IDF tidak dapat dimuat." + ); + } + } finally { + if (!controller.signal.aborted) { + setLoading(false); + } + } + } + + loadTFIDF(); + + return () => controller.abort(); + }, []); + + useEffect(() => { + setCurrentPage(1); + }, [search, filterJenis, filterLabel]); + + useEscapeModal(detail, () => setDetail(null)); + + const filteredData = useMemo(() => { + const keyword = search.trim().toLowerCase(); + + return data.filter((item) => { + const bobotKata = (item.bobot_tfidf || []) + .map((x) => x.kata) + .join(" ") + .toLowerCase(); + + const cocokSearch = + !keyword || + String(item.no_data).includes(keyword) || + String(item.ulasan_asli || "") + .toLowerCase() + .includes(keyword) || + String(item.stemmed_text || "") + .toLowerCase() + .includes(keyword) || + bobotKata.includes(keyword); + + const cocokJenis = + filterJenis === "Semua" || + item.jenis_data === filterJenis; + + const cocokLabel = + filterLabel === "Semua" || + (filterLabel === "Positif" && + Number(item.label) === 1) || + (filterLabel === "Negatif" && + Number(item.label) === 0); + + return cocokSearch && cocokJenis && cocokLabel; + }); + }, [data, search, filterJenis, filterLabel]); + + const totalPages = Math.max( + 1, + Math.ceil(filteredData.length / ITEMS_PER_PAGE) + ); + + const safePage = Math.min(currentPage, totalPages); + const startIndex = (safePage - 1) * ITEMS_PER_PAGE; + + const paginatedData = filteredData.slice( + startIndex, + startIndex + ITEMS_PER_PAGE + ); + + const scrollKeAtasHasilTFIDF = () => { + window.requestAnimationFrame(() => { + if (!tfidfResultRef.current) return; + + const posisi = + tfidfResultRef.current.getBoundingClientRect().top + + window.scrollY - + 18; + + window.scrollTo({ + top: posisi, + behavior: "smooth" + }); + }); + }; + + const halamanSebelumnyaTFIDF = () => { + setCurrentPage((page) => Math.max(1, page - 1)); + scrollKeAtasHasilTFIDF(); + }; + + const halamanBerikutnyaTFIDF = () => { + setCurrentPage((page) => Math.min(totalPages, page + 1)); + scrollKeAtasHasilTFIDF(); + }; + + return ( +
+
+ + + + + +
+ +
+
+

Hasil Pembobotan TF-IDF 571 Ulasan

+ +

+ Bobot TF-IDF dihitung menggunakan fitur yang dibentuk dari + data training. Data testing ditransformasikan menggunakan + vectorizer yang sama tanpa melakukan fit ulang. +

+
+ +
+ setSearch(event.target.value)} + /> + + + + +
+ + {loading && ( +
+ Memuat hasil pembobotan TF-IDF penelitian... +
+ )} + + {!loading && error && ( +
+ {error} +
+ )} + + {!loading && !error && ( + <> +
+ {paginatedData.map((item) => ( + setDetail(item)} + /> + ))} + + {paginatedData.length === 0 && ( +
+ Tidak ada data TF-IDF yang sesuai dengan pencarian atau + filter. +
+ )} +
+ + + + )} +
+ +
+
+

Cara Pembentukan Fitur TF-IDF

+ +

+ TF-IDF dibentuk menggunakan 456 data training dan menghasilkan + 675 fitur. Bobot akhir merupakan hasil TF × IDF, sedangkan data + testing ditransformasikan menggunakan vectorizer dari data training. +

+
+ +
+
+ Data Training + 456 Ulasan +
+ +
→
+ +
+ Pembobotan + TF × IDF +
+ +
→
+ +
+ Hasil + 675 Fitur +
+
+
+ + {detail && ( + setDetail(null)} + /> + )} +
+ ); +} + +function TFIDFResearchCard({ item, onDetail }) { + const topWeights = (item.bobot_tfidf || []).slice(0, 5); + + const maxWeight = + topWeights.reduce( + (maksimum, weight) => + Math.max( + maksimum, + Number(weight.bobot) || 0 + ), + 0 + ) || 1; + + return ( +
+
+ + Data ke-{item.no_data} + + +
+ + {item.jenis_data} + + + +
+
+ +
+ Hasil Stemming + {item.stemmed_text} +
+ +
+
+ Bobot TF-IDF Tertinggi + + + {item.jumlah_fitur_nonzero} fitur bernilai > 0 + +
+ + {topWeights.length > 0 ? ( + topWeights.map((weight) => { + const nilaiBobot = Number(weight.bobot) || 0; + + const width = Math.min( + 100, + Math.max( + 0, + (nilaiBobot / maxWeight) * 100 + ) + ); + + return ( +
+ {weight.kata} + +
+ +
+ + + {formatDecimal(weight.bobot)} + +
+ ); + }) + ) : ( +
+ Tidak ada fitur yang dikenali vectorizer. +
+ )} +
+ +
+ +
+
+ ); +} + +function TFIDFDetailModal({ item, onClose }) { + return ( +
+ event.target === event.currentTarget && + onClose() + } + > +
+
+
+
+

Data ke-{item.no_data}

+
+ + +
+ +
+
+ Ulasan Asli +
+ +

{item.ulasan_asli}

+
+ +
+
+ Jenis Data + {item.jenis_data} +
+ +
+ Posisi pada Split + + {item.jenis_data} ke-{item.urutan_split} + +
+ +
+ Fitur Bobot > 0 + {item.jumlah_fitur_nonzero} +
+ +
+ Sentimen + + + {Number(item.label) === 1 + ? "Positif" + : "Negatif"} + +
+
+ +
+ Hasil Stemming + {item.stemmed_text} +
+ +
+

+ Seluruh Fitur dengan Bobot TF-IDF > 0 +

+
+ +
+ + + + + + + + + + + + + + {(item.bobot_tfidf || []).map( + (row, index) => ( + + + + + + + + + + + + + ) + )} + + {(item.bobot_tfidf || []).length === 0 && ( + + + + )} + +
No.KataTFDFIDFBobot TF-IDF
{index + 1} + {row.kata} + {row.tf}{row.df} + {formatDecimal(row.idf)} + + {formatDecimal(row.bobot)} +
+ Tidak ada fitur dengan bobot TF-IDF > 0. +
+
+ +
+ i + +
+ Cara Membaca Bobot + +

+ TF menunjukkan frekuensi term pada satu dokumen, DF menunjukkan + jumlah dokumen training yang mengandung term, sedangkan IDF + menunjukkan bobot berdasarkan kemunculan term pada dokumen + training. Bobot TF-IDF pada tabel merupakan hasil TF × IDF. +

+
+
+
+
+
+ ); +} + +/* ============================================================ + MODEL & EVALUASI + ============================================================ */ + +function ModelTab() { + const [modelInfo, setModelInfo] = useState(null); + const [error, setError] = useState(""); + + useEffect(() => { + let active = true; + async function loadModel() { + try { + const response = await ambilModelPenelitian(); + const data = response.data; + const report = data?.classification_report; + const cm = data?.confusion_matrix; + const validRate = value => typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1; + if (!data || !validRate(data.akurasi) || + !Number.isInteger(data.jumlah_data_testing) || data.jumlah_data_testing <= 0 || + !Array.isArray(cm) || cm.length !== 2 || + !cm.every(row => Array.isArray(row) && row.length === 2 && row.every(value => Number.isInteger(value) && value >= 0)) || + cm.flat().reduce((sum, value) => sum + value, 0) !== data.jumlah_data_testing || + !["0", "1", "weighted avg"].every(key => + report?.[key] && ["precision", "recall", "f1-score"].every(metric => validRate(report[key][metric])) && + Number.isFinite(report[key].support))) { + throw new Error("Metadata evaluasi dari backend tidak lengkap atau tidak valid."); + } + if (active) setModelInfo(data); + } catch (err) { + if (active) setError(err.message || "Gagal memuat evaluasi model."); + } + } + loadModel(); + return () => { active = false; }; + }, []); + + if (error) return ( +
+
+

Evaluasi model 80:20 belum dapat ditampilkan

+

{error}

+

Pastikan backend berjalan. Buka tab lain lalu kembali ke Model & Evaluasi untuk mencoba lagi.

+
+ +
+ ); + if (!modelInfo) return
Memuat evaluasi model dari backend...
; + + const report = modelInfo.classification_report; + const weighted = report["weighted avg"]; + const [[tn, fp], [fn, tp]] = modelInfo.confusion_matrix; + const percent = (value, digits = 2) => (value * 100).toLocaleString("id-ID", { + minimumFractionDigits: digits, maximumFractionDigits: digits + }) + "%"; + const evaluationMetrics = [ + { name: "Accuracy", value: percent(modelInfo.akurasi), description: "Ketepatan prediksi pada " + modelInfo.jumlah_data_testing + " data testing" }, + { name: "Precision", value: percent(weighted.precision), description: "Weighted average seluruh kelas" }, + { name: "Recall", value: percent(weighted.recall), description: "Weighted average seluruh kelas" }, + { name: "F1-Score", value: percent(weighted["f1-score"]), description: "Weighted average seluruh kelas" } + ]; + return ( +
+
+
+

Evaluasi Model Awal — v1

+ +

+ Evaluasi dilakukan terhadap {modelInfo.jumlah_data_testing} data testing. Nilai Precision, + Recall, dan F1-Score berikut menggunakan weighted average. +

+
+ +
+ {evaluationMetrics.map((metric) => ( + + ))} +
+
+ +
+
+
+

Confusion Matrix

+ +

+ Perbandingan antara label aktual dengan hasil prediksi model + pada data testing. +

+
+ +
+
+ Prediksi Model +
+ +
+
+ +
+ Negatif +
+ +
+ Positif +
+ +
+ Negatif Aktual +
+ + + + + +
+ Positif Aktual +
+ + + + +
+ +
+ + + Prediksi benar + + + + + Prediksi salah + +
+
+
+ +
+
+

Evaluasi per Kelas

+ +

+ Precision, Recall, dan F1-Score masing-masing kelas pada + {modelInfo.jumlah_data_testing} data testing. +

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KelasPrecisionRecallF1-ScoreJumlah
+ + Negatif + + {percent(report["0"].precision, 1)}{percent(report["0"].recall, 1)}{percent(report["0"]["f1-score"], 1)}{report["0"].support}
+ + Positif + + {percent(report["1"].precision, 1)}{percent(report["1"].recall, 1)}{percent(report["1"]["f1-score"], 1)}{report["1"].support}
+
+ +
+ i + +

+ Recall kelas positif sebesar {percent(report["1"].recall, 1)} menunjukkan bahwa {tp} dari{" "} + {report["1"].support} ulasan positif pada data testing berhasil diprediksi sebagai + positif, sedangkan {fn} lainnya diprediksi sebagai negatif. +

+
+
+
+ +
+ ); +} + +/* ============================================================ + PERBANDINGAN TIGA RASIO PENELITIAN + Sumber: public/data/perbandingan_rasio_slearn.json + ============================================================ */ + +function PerbandinganRasio() { + const [skenario, setSkenario] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + + useEffect(() => { + const controller = new AbortController(); + async function loadPerbandingan() { + try { + const response = await fetch("/data/perbandingan_rasio_slearn.json", { signal: controller.signal }); + if (!response.ok) throw new Error("File perbandingan_rasio_slearn.json tidak ditemukan."); + const json = await response.json(); + const daftar = json?.skenario; + const rasioWajib = ["70:30", "80:20", "90:10"]; + const valid = Array.isArray(daftar) && daftar.length === 3 && + rasioWajib.every(rasio => daftar.some(item => item.rasio === rasio)) && + daftar.every(item => + Number.isInteger(item.data_latih) && Number.isInteger(item.data_uji) && + Number.isInteger(item.jumlah_fitur) && item.data_latih + item.data_uji === 571 && + item.jumlah_fitur > 0 && + ["accuracy", "precision", "recall", "f1_score"].every(k => + typeof item[k] === "number" && Number.isFinite(item[k]) && item[k] >= 0 && item[k] <= 100) && + Array.isArray(item.confusion_matrix) && item.confusion_matrix.length === 2 && + item.confusion_matrix.every(row => Array.isArray(row) && row.length === 2 && + row.every(n => Number.isInteger(n) && n >= 0)) && + item.confusion_matrix.flat().reduce((total, n) => total + n, 0) === item.data_uji + ); + if (!valid) throw new Error("Struktur atau nilai data perbandingan tiga rasio tidak sesuai."); + if (!controller.signal.aborted) { + setSkenario(rasioWajib.map(rasio => daftar.find(item => item.rasio === rasio))); + setError(""); + } + } catch (err) { + if (err.name !== "AbortError") setError(err.message || "Gagal membaca perbandingan rasio."); + } finally { + if (!controller.signal.aborted) setLoading(false); + } + } + loadPerbandingan(); + return () => controller.abort(); + }, []); + + const persen = angka => Number(angka).toLocaleString("id-ID", { + minimumFractionDigits: 2, maximumFractionDigits: 2 + }) + "%"; + + return ( +
+
+

Perbandingan Tiga Rasio Data Training dan Testing

+

Hasil eksperimen 70:30, 80:20, dan 90:10 menggunakan Logistic Regression dan TF-IDF.

+
+ + {loading &&

Memuat hasil perbandingan rasio...

} + {!loading && error &&

{error}

} + + {!loading && !error && ( + <> +
+ + + + + + + + + {skenario.map(item => ( + + + + + + + ))} + +
RasioData LatihData UjiFitur TF-IDFAccuracyPrecisionRecallF1-Score
{item.rasio}{item.rasio === "80:20" &&
Model utama
}
{item.data_latih}{item.data_uji}{item.jumlah_fitur}{persen(item.accuracy)}{persen(item.precision)}{persen(item.recall)}{persen(item.f1_score)}
+
+ +
+

Grafik Perbandingan Akurasi

+
+
+
+ {[100, 75, 50, 25, 0].map(nilai => {nilai}%)} +
+
+
+ {[0, 25, 50, 75, 100].map(nilai => ( +
+ ))} +
+ {skenario.map(item => ( +
+ {persen(item.accuracy)} +
+ ))} +
+
+
+ {skenario.map(item => ( + {item.rasio} + ))} +
+
+
+
+
+ +

Confusion Matrix per Rasio

+
+ {skenario.map(item => { + const [[tn, fp], [fn, tp]] = item.confusion_matrix; + return ( +
+ Rasio {item.rasio} + + + + + + +
Aktual / PrediksiNegatifPositif
Negatif{tn} (TN){fp} (FP)
Positif{fn} (FN){tp} (TP)
+
+ ); + })} +
+ +
+ i +
+ Catatan Evaluasi +

Precision, Recall, dan F1-Score menggunakan weighted average. Setiap rasio memiliki jumlah data uji berbeda sehingga hasilnya tidak langsung menunjukkan satu rasio selalu lebih baik. Model utama untuk prediksi website tetap rasio 80:20 dengan akurasi 93,04%; eksperimen rasio lain hanya ditampilkan sebagai pembanding.

+
+
+ + )} +
+ ); +} + +/* ============================================================ + COMPONENT + ============================================================ */ + +function useEscapeModal(value, close) { + useEffect(() => { + if (!value) return; + + const handleEscape = (event) => { + if (event.key === "Escape") { + close(); + } + }; + + document.addEventListener("keydown", handleEscape); + + return () => + document.removeEventListener("keydown", handleEscape); + }, [value, close]); +} + +function formatDecimal(value) { + const number = Number(value); + + if (!Number.isFinite(number)) { + return "-"; + } + + return number + .toFixed(6) + .replace(".", ","); +} + +function StatCard({ title, value, info }) { + return ( +
+ {title} + {value} + {info} +
+ ); +} + +function FlowBox({ title, subtitle }) { + return ( +
+ {title} + {subtitle} +
+ ); +} + +function FlowArrow() { + return ( +
+ → +
+ ); +} + +function MetricCard({ + name, + value, + description +}) { + return ( +
+ {name} + {value} + {description} +
+ ); +} + +function SentimentBadge({ label }) { + const positive = Number(label) === 1; + + return ( + + {positive ? "Positif" : "Negatif"} + + ); +} + +function NormalizationRow({ from, to }) { + return ( +
+ {from} + + + → + + + {to} +
+ ); +} + +function ResearchProcessStep({ + number, + title, + value +}) { + return ( +
+
+ {number} +
+ +
+ {title} +

{value || "-"}

+
+
+ ); +} + +function ResearchPagination({ + total, + startIndex, + currentPage, + totalPages, + onPrev, + onNext +}) { + return ( +
+ + Menampilkan{" "} + {total === 0 ? 0 : startIndex + 1} + {" - "} + {Math.min( + startIndex + ITEMS_PER_PAGE, + total + )}{" "} + dari {total} data + + +
+ + + + {currentPage} / {totalPages} + + + +
+
+ ); +} + +function ConfusionValue({ + value, + label, + tone +}) { + return ( +
+ {value} + {label} +
+ ); +} + +export default PenelitianModel; diff --git a/web_slearn/src/pages/PrediksiUlasan.jsx b/web_slearn/src/pages/PrediksiUlasan.jsx new file mode 100644 index 0000000..f7c83a2 --- /dev/null +++ b/web_slearn/src/pages/PrediksiUlasan.jsx @@ -0,0 +1,567 @@ +import { ModelVersion, TfidfNote } from "../components/ModelContext"; +import { useState } from "react"; +import { prediksiUlasan } from "../services/api"; +import "../styles/prediksi-ulasan.css"; + + +function PrediksiUlasan() { + const [teks, setTeks] = useState(""); + const [hasil, setHasil] = useState(null); + const [loading, setLoading] = useState(false); + const [detailTerbuka, setDetailTerbuka] = useState(false); + const [error, setError] = useState(""); + + + /* ============================================================ + INPUT ULASAN + ============================================================ */ + + const handleChange = (event) => { + setTeks(event.target.value); + setError(""); + + if (hasil) { + setHasil(null); + setDetailTerbuka(false); + } + }; + + + /* ============================================================ + MULAI ANALISIS + ============================================================ */ + + const mulaiAnalisis = async () => { + const input = teks.trim(); + + if (!input) { + setError( + "Masukkan ulasan terlebih dahulu." + ); + + setHasil(null); + + return; + } + + setLoading(true); + setError(""); + setHasil(null); + + try { + const response = + await prediksiUlasan(input); + + setHasil( + response.data + ); + } catch (err) { + setError( + err.message || + "Ulasan tidak dapat dianalisis." + ); + } finally { + setLoading(false); + } + }; + + + /* ============================================================ + RESET + ============================================================ */ + + const resetPrediksi = () => { + setTeks(""); + setHasil(null); + setError(""); + setDetailTerbuka(false); + }; + + + /* ============================================================ + DETAIL PREPROCESSING + ============================================================ */ + + const prosesPreprocessing = + hasil + ? [ + [ + "Cleaning", + hasil.preprocessing.cleaning + ], + [ + "Case Folding", + hasil.preprocessing.case_folding + ], + [ + "Normalisasi", + hasil.preprocessing.normalization + ], + [ + "Tokenizing", + Array.isArray( + hasil.preprocessing.tokenizing + ) + ? JSON.stringify( + hasil.preprocessing.tokenizing + ) + : hasil.preprocessing.tokenizing + ], + [ + "Stopword Removal", + hasil.preprocessing.stopword_removal + ], + [ + "Stemming", + hasil.preprocessing.stemming + ] + ] + : []; + + + const hasilPositif = + hasil?.sentimen === "Positif"; + + + /* ============================================================ + TAMPILAN + ============================================================ */ + + return ( +
+ + {/* ======================================================== + HEADING + ======================================================== */} + +
+ +

+ Prediksi Satu Ulasan +

+ +

+ Masukkan satu ulasan S-Learn untuk memperoleh hasil prediksi sentimen. +

+ +
+ + + {/* ======================================================== + INPUT & HASIL + ======================================================== */} + +
+ + {/* ====================================================== + INPUT + ====================================================== */} + +
+ +
+

+ Masukkan Ulasan +

+
+ + +