Dyraa18 commited on
Commit
2853e42
·
verified ·
1 Parent(s): fb2123c

Upload 12 files

Browse files
static/admin.css ADDED
@@ -0,0 +1,256 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* =========
2
+ Admin UI — RAG App
3
+ File: static/admin.css
4
+ ========= */
5
+
6
+ /* --- Design tokens --- */
7
+ :root{
8
+ --admin-bg: #f7f8fa;
9
+ --admin-card: #ffffff;
10
+ --admin-text: #1d2433;
11
+ --admin-muted: #6b7280;
12
+ --admin-border: #e5e7eb;
13
+ --admin-primary: #3b82f6; /* biru */
14
+ --admin-accent: #22c55e; /* hijau */
15
+ --admin-danger: #ef4444; /* merah */
16
+ --admin-warning: #f59e0b; /* oranye */
17
+ --radius: 16px;
18
+ --shadow-100: 0 1px 3px rgba(0,0,0,.06), 0 1px 2px rgba(0,0,0,.04);
19
+ --shadow-200: 0 4px 10px rgba(0,0,0,.08);
20
+ --shadow-300: 0 12px 24px rgba(0,0,0,.12);
21
+ }
22
+
23
+ /* Dark mode otomatis */
24
+ @media (prefers-color-scheme: dark){
25
+ :root{
26
+ --admin-bg: #0b1220;
27
+ --admin-card: #0f172a;
28
+ --admin-text: #e5e7eb;
29
+ --admin-muted: #9ca3af;
30
+ --admin-border: #1f2937;
31
+ --admin-primary: #60a5fa;
32
+ --admin-accent: #34d399;
33
+ --admin-danger: #f87171;
34
+ --admin-warning: #fbbf24;
35
+ }
36
+ }
37
+
38
+ /* --- Global --- */
39
+ body{
40
+ background: var(--admin-bg);
41
+ color: var(--admin-text);
42
+ }
43
+
44
+ /* Navbar */
45
+ .navbar{
46
+ box-shadow: var(--shadow-100);
47
+ border-bottom: 1px solid var(--admin-border);
48
+ background: var(--admin-card) !important;
49
+ }
50
+ .navbar .navbar-brand{
51
+ font-weight: 700;
52
+ letter-spacing: .2px;
53
+ }
54
+ .navbar .nav-link{
55
+ color: var(--admin-text);
56
+ opacity: .85;
57
+ }
58
+ .navbar .nav-link:hover{
59
+ opacity: 1;
60
+ }
61
+ .navbar-text{
62
+ color: var(--admin-muted);
63
+ }
64
+
65
+ /* Container spacing */
66
+ .container{
67
+ padding-bottom: 48px;
68
+ }
69
+
70
+ /* Alerts */
71
+ .alert{
72
+ border-radius: 12px;
73
+ border: 1px solid var(--admin-border);
74
+ box-shadow: var(--shadow-100);
75
+ }
76
+
77
+ /* Headings */
78
+ h4, .h4{
79
+ font-weight: 700;
80
+ letter-spacing: .2px;
81
+ }
82
+
83
+ /* --- Cards / dashboard --- */
84
+ .card{
85
+ border: 1px solid var(--admin-border);
86
+ background: var(--admin-card);
87
+ border-radius: var(--radius);
88
+ box-shadow: var(--shadow-100);
89
+ transition: transform .08s ease, box-shadow .2s ease;
90
+ }
91
+ .card:hover{
92
+ transform: translateY(-1px);
93
+ box-shadow: var(--shadow-200);
94
+ }
95
+ .card .fs-1{
96
+ font-weight: 800;
97
+ line-height: 1;
98
+ }
99
+
100
+ /* --- Forms / filters --- */
101
+ form .form-control, form .form-select{
102
+ border-radius: 12px;
103
+ border-color: var(--admin-border);
104
+ background: var(--admin-card);
105
+ color: var(--admin-text);
106
+ }
107
+ form .form-control::placeholder{
108
+ color: var(--admin-muted);
109
+ }
110
+ .btn{
111
+ border-radius: 12px;
112
+ box-shadow: var(--shadow-100);
113
+ }
114
+ .btn-primary{
115
+ background: var(--admin-primary);
116
+ border-color: var(--admin-primary);
117
+ }
118
+ .btn-outline-dark{
119
+ color: var(--admin-text);
120
+ border-color: var(--admin-border);
121
+ }
122
+ .btn-outline-dark:hover{
123
+ background: var(--admin-border);
124
+ }
125
+
126
+ /* --- Tables --- */
127
+ .table{
128
+ margin-bottom: 12px;
129
+ color: var(--admin-text);
130
+ }
131
+ .table thead th{
132
+ font-size: .84rem;
133
+ text-transform: uppercase;
134
+ letter-spacing: .06em;
135
+ color: var(--admin-muted);
136
+ border-bottom: 1px solid var(--admin-border) !important;
137
+ background: var(--admin-bg);
138
+ position: sticky;
139
+ top: 0; /* sticky header saat scroll */
140
+ z-index: 1;
141
+ }
142
+ .table td, .table th{
143
+ border-color: var(--admin-border);
144
+ vertical-align: middle;
145
+ }
146
+ .table-hover tbody tr:hover{
147
+ background: color-mix(in oklab, var(--admin-primary) 8%, transparent);
148
+ }
149
+ .table .text-truncate{
150
+ max-width: 600px;
151
+ }
152
+
153
+ /* Truncate multi-baris */
154
+ .truncate-2{
155
+ display: -webkit-box;
156
+ -webkit-line-clamp: 2;
157
+ -webkit-box-orient: vertical;
158
+ overflow: hidden;
159
+ }
160
+ .truncate-3{
161
+ display: -webkit-box;
162
+ -webkit-line-clamp: 3;
163
+ -webkit-box-orient: vertical;
164
+ overflow: hidden;
165
+ }
166
+
167
+ /* Badges */
168
+ .badge{
169
+ border-radius: 999px;
170
+ padding: .4em .7em;
171
+ font-weight: 600;
172
+ }
173
+ .text-bg-dark{
174
+ background: #111827 !important;
175
+ color: #f9fafb !important;
176
+ }
177
+ .text-bg-info{
178
+ background: #0ea5e9 !important;
179
+ }
180
+ .text-bg-secondary{
181
+ background: #64748b !important;
182
+ }
183
+
184
+ /* Status pills khusus */
185
+ .badge-yes{
186
+ background: var(--admin-accent);
187
+ }
188
+ .badge-no{
189
+ background: var(--admin-muted);
190
+ }
191
+
192
+ /* Pagination */
193
+ .pagination .page-link{
194
+ border-radius: 10px !important;
195
+ margin: 0 4px;
196
+ border-color: var(--admin-border);
197
+ color: var(--admin-text);
198
+ background: var(--admin-card);
199
+ }
200
+ .pagination .page-item.disabled .page-link{
201
+ opacity: .6;
202
+ }
203
+ .pagination .page-link:hover{
204
+ border-color: var(--admin-primary);
205
+ }
206
+
207
+ /* Table responsive wrapper */
208
+ .table-responsive{
209
+ border: 1px solid var(--admin-border);
210
+ border-radius: var(--radius);
211
+ box-shadow: var(--shadow-100);
212
+ background: var(--admin-card);
213
+ padding: 6px;
214
+ }
215
+
216
+ /* Small helper for “chip” subjects */
217
+ .subject-chip{
218
+ display: inline-flex;
219
+ align-items: center;
220
+ gap: .4rem;
221
+ padding: .3rem .6rem;
222
+ border-radius: 999px;
223
+ border: 1px solid var(--admin-border);
224
+ background: var(--admin-bg);
225
+ font-size: .8rem;
226
+ color: var(--admin-text);
227
+ }
228
+
229
+ /* Links */
230
+ a{
231
+ text-decoration: none;
232
+ }
233
+ a:hover{
234
+ text-decoration: underline;
235
+ }
236
+
237
+ /* Footer (kalau dibutuhkan) */
238
+ .admin-footer{
239
+ margin-top: 24px;
240
+ color: var(--admin-muted);
241
+ font-size: .9rem;
242
+ }
243
+
244
+ /* --- Utilities --- */
245
+ .shadow-100{ box-shadow: var(--shadow-100); }
246
+ .shadow-200{ box-shadow: var(--shadow-200); }
247
+ .shadow-300{ box-shadow: var(--shadow-300); }
248
+ .rounded-xl{ border-radius: var(--radius); }
249
+ .muted{ color: var(--admin-muted); }
250
+
251
+ /* --- Mobile tweaks --- */
252
+ @media (max-width: 768px){
253
+ .table .text-truncate{ max-width: 260px; }
254
+ .navbar .navbar-text{ display: none; }
255
+ .card .fs-1{ font-size: 2rem; }
256
+ }
static/style.css ADDED
@@ -0,0 +1,289 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* static/style.css – tambahan ringkas biar mirip SS */
2
+ body { font-family: system-ui, Segoe UI, Roboto, Arial, sans-serif; background:#f5f7fb; margin:0; }
3
+ .container { max-width: 980px; margin: 0 auto; padding: 24px; }
4
+
5
+ .hero { background:#4a6ea9; color:#fff; padding:28px 24px; border-radius:12px; }
6
+ .hero h1 { margin:0 0 8px; letter-spacing:1px; }
7
+ .nav a { color:#e8eefc; margin-right:16px; text-decoration:none; font-weight:600; }
8
+ .nav a[aria-disabled="true"] { opacity:.6; pointer-events:none; }
9
+
10
+ .welcome { text-align:center; padding:28px 0; }
11
+ .welcome h2 { margin:0 0 8px; color:#2b3852; }
12
+
13
+ .subjects h3 { text-align:center; color:#2b3852; margin-bottom:16px; }
14
+ .grid { display:grid; grid-template-columns: repeat(3, 1fr); gap:18px; }
15
+ .card { display:block; background:linear-gradient(180deg,#6b86bd,#5575b1); color:#fff; padding:22px; border-radius:12px; text-decoration:none; box-shadow:0 6px 16px rgba(0,0,0,.08); }
16
+ .card h4 { margin:0 0 8px; letter-spacing:.5px; }
17
+ .card p { margin:0; opacity:.95; }
18
+
19
+ .header { display:flex; justify-content:space-between; align-items:center; margin-bottom:14px; color:#fff;
20
+ background:#6b86bd; padding:12px 16px; border-radius:10px; }
21
+ .header .link { color:#fff; text-decoration:none; }
22
+
23
+ .main { background:#fff; border-radius:12px; padding:16px; box-shadow:0 6px 16px rgba(0,0,0,.06); }
24
+ .chat-box { height:420px; overflow:auto; padding:8px; background:#f6f8fc; border-radius:10px; }
25
+ .msg { max-width:70%; margin:8px 0; padding:10px 12px; border-radius:14px; line-height:1.3; }
26
+ .msg.user { margin-left:auto; background:#4a6ea9; color:#fff; }
27
+ .msg.bot { background:#e9eef8; color:#2b3852; }
28
+ .input-row { display:flex; gap:8px; margin-top:12px; }
29
+ .input-row input { flex:1; padding:10px 12px; border-radius:10px; border:1px solid #cbd5e1; }
30
+ .btn { background:#4a6ea9; color:#fff; border:none; padding:10px 14px; border-radius:10px; cursor:pointer; }
31
+ .btn:hover { filter:brightness(.95); }
32
+
33
+ .about { line-height:1.6; color:#2b3852; }
34
+ .about h3 { color:#4a6ea9; margin-bottom:8px; }
35
+ .about h4 { margin-top:24px; color:#344767; }
36
+ .bio { list-style:none; padding:0; margin:0; }
37
+ .bio li { margin-bottom:4px; }
38
+
39
+ /* ===========================================
40
+ AUTH UI — Login & Register
41
+ Fokus: tampilan saja (tanpa ubah HTML)
42
+ =========================================== */
43
+
44
+ /* ---------- Design tokens ---------- */
45
+ :root {
46
+ --auth-bg1: #6c8bd6;
47
+ --auth-bg2: #405eae;
48
+ --auth-card: #ffffff;
49
+ --auth-text: #1f2937;
50
+ --auth-muted:#6b7280;
51
+ --auth-primary: #4f6fd0;
52
+ --auth-primary-2: #3e5cc2;
53
+ --auth-success-bg:#e8f7ee;
54
+ --auth-success-fg:#11623c;
55
+ --auth-error-bg: #fdeaea;
56
+ --auth-error-fg: #8f1d1d;
57
+ --auth-border: #e5e7eb;
58
+ --shadow-1: 0 6px 20px rgba(0,0,0,.12);
59
+ --shadow-2: 0 16px 48px rgba(0,0,0,.18);
60
+ --radius-lg: 16px;
61
+ --radius-md: 12px;
62
+ }
63
+
64
+ /* Dark mode otomatis */
65
+ @media (prefers-color-scheme: dark) {
66
+ :root {
67
+ --auth-bg1: #0f1b3a;
68
+ --auth-bg2: #0b1330;
69
+ --auth-card: #0f172a;
70
+ --auth-text: #e5e7eb;
71
+ --auth-muted:#a3a9b5;
72
+ --auth-primary: #6d8cff;
73
+ --auth-primary-2: #5876f3;
74
+ --auth-success-bg:#0f2b20;
75
+ --auth-success-fg:#a6f3c7;
76
+ --auth-error-bg: #2a1212;
77
+ --auth-error-fg: #ffb3b3;
78
+ --auth-border: #1f2937;
79
+ --shadow-1: 0 8px 26px rgba(0,0,0,.35);
80
+ --shadow-2: 0 24px 60px rgba(0,0,0,.45);
81
+ }
82
+ }
83
+
84
+ /* ---------- Background full ---------- */
85
+ .auth-bg {
86
+ min-height: 100vh;
87
+ padding: 32px 18px;
88
+ display: grid;
89
+ place-items: center;
90
+ background: radial-gradient(1200px 800px at 10% 10%, rgba(255,255,255,.18), transparent 40%),
91
+ radial-gradient(1000px 700px at 90% 20%, rgba(255,255,255,.12), transparent 45%),
92
+ linear-gradient(160deg, var(--auth-bg1) 0%, var(--auth-bg2) 100%);
93
+ position: relative;
94
+ overflow: hidden;
95
+ }
96
+
97
+ /* Decorative blobs */
98
+ .auth-bg::before, .auth-bg::after {
99
+ content: "";
100
+ position: absolute;
101
+ filter: blur(60px);
102
+ opacity: .35;
103
+ animation: floaty 14s ease-in-out infinite alternate;
104
+ }
105
+ .auth-bg::before {
106
+ width: 480px; height: 480px;
107
+ left: -120px; bottom: -120px;
108
+ background: radial-gradient(circle at 30% 30%, #ffffff66, transparent 60%);
109
+ }
110
+ .auth-bg::after {
111
+ width: 520px; height: 520px;
112
+ right: -140px; top: -140px;
113
+ background: radial-gradient(circle at 70% 70%, #ffffff55, transparent 60%);
114
+ animation-duration: 18s;
115
+ }
116
+
117
+ @keyframes floaty {
118
+ from { transform: translateY(-6px) translateX(0); }
119
+ to { transform: translateY(6px) translateX(4px); }
120
+ }
121
+
122
+ /* ---------- Card ---------- */
123
+ .auth-card {
124
+ width: 100%;
125
+ max-width: 440px;
126
+ background: color-mix(in oklab, var(--auth-card) 92%, transparent);
127
+ -webkit-backdrop-filter: blur(10px);
128
+ backdrop-filter: blur(10px);
129
+ border: 1px solid color-mix(in oklab, var(--auth-border) 70%, transparent);
130
+ border-radius: var(--radius-lg);
131
+ box-shadow: var(--shadow-1);
132
+ padding: 26px 22px;
133
+ position: relative;
134
+ overflow: hidden;
135
+ transform: translateZ(0);
136
+ }
137
+ .auth-card::after{
138
+ /* top accent line */
139
+ content:"";
140
+ position:absolute;
141
+ inset: 0 0 auto 0;
142
+ height: 3px;
143
+ background: linear-gradient(90deg, var(--auth-primary), var(--auth-primary-2));
144
+ opacity: .9;
145
+ }
146
+
147
+ /* micro-hover lift */
148
+ .auth-card:hover{
149
+ box-shadow: var(--shadow-2);
150
+ transform: translateY(-1px);
151
+ }
152
+
153
+ /* ---------- Titles ---------- */
154
+ .auth-title{
155
+ margin: 2px 0 6px;
156
+ font-size: 24px;
157
+ font-weight: 800;
158
+ letter-spacing: .2px;
159
+ color: var(--auth-text);
160
+ }
161
+ .auth-subtitle{
162
+ margin: 0 0 16px;
163
+ color: var(--auth-muted);
164
+ font-size: 14px;
165
+ }
166
+
167
+ /* ---------- Form ---------- */
168
+ .auth-card form{
169
+ display: grid;
170
+ gap: 12px;
171
+ margin-top: 10px;
172
+ }
173
+
174
+ /* Input wrapper (adds subtle focus ring) */
175
+ .auth-input{
176
+ position: relative;
177
+ }
178
+ .auth-card input{
179
+ width: 100%;
180
+ padding: 12px 14px;
181
+ border-radius: var(--radius-md);
182
+ border: 1px solid var(--auth-border);
183
+ background: color-mix(in oklab, var(--auth-card) 98%, transparent);
184
+ color: var(--auth-text);
185
+ font-size: 14px;
186
+ transition: border-color .15s ease, box-shadow .2s ease, background .2s ease;
187
+ }
188
+ .auth-card input::placeholder{
189
+ color: color-mix(in oklab, var(--auth-muted) 80%, transparent);
190
+ }
191
+ .auth-card input:focus{
192
+ outline: none;
193
+ border-color: color-mix(in oklab, var(--auth-primary) 70%, #000 0%);
194
+ box-shadow: 0 0 0 4px color-mix(in oklab, var(--auth-primary) 18%, transparent);
195
+ background: color-mix(in oklab, var(--auth-card) 100%, transparent);
196
+ }
197
+
198
+ /* ---------- Button ---------- */
199
+ .auth-card .btn{
200
+ width: 100%;
201
+ padding: 12px 16px;
202
+ border-radius: var(--radius-md);
203
+ font-weight: 700;
204
+ letter-spacing: .2px;
205
+ border: none;
206
+ cursor: pointer;
207
+ background: linear-gradient(180deg, var(--auth-primary), var(--auth-primary-2));
208
+ color: #fff;
209
+ box-shadow: 0 8px 24px color-mix(in oklab, var(--auth-primary) 35%, transparent);
210
+ transition: transform .06s ease, filter .2s ease, box-shadow .2s ease;
211
+ }
212
+ .auth-card .btn:hover{
213
+ filter: brightness(.98);
214
+ box-shadow: 0 10px 30px color-mix(in oklab, var(--auth-primary) 45%, transparent);
215
+ }
216
+ .auth-card .btn:active{
217
+ transform: translateY(1px);
218
+ }
219
+
220
+ /* ---------- Helper text & links ---------- */
221
+ .muted{
222
+ margin-top: 14px;
223
+ text-align: center;
224
+ color: var(--auth-muted);
225
+ font-size: 13px;
226
+ }
227
+ .muted a{
228
+ color: var(--auth-primary);
229
+ text-decoration: none;
230
+ font-weight: 600;
231
+ }
232
+ .muted a:hover{ text-decoration: underline; }
233
+
234
+ /* ---------- Flash messages ---------- */
235
+ .flash{
236
+ margin: 8px 0 4px;
237
+ padding: 10px 12px;
238
+ border-radius: var(--radius-md);
239
+ font-size: 13px;
240
+ border: 1px solid transparent;
241
+ }
242
+ .flash.success{
243
+ background: var(--auth-success-bg);
244
+ color: var(--auth-success-fg);
245
+ border-color: color-mix(in oklab, var(--auth-success-fg) 24%, transparent);
246
+ }
247
+ .flash.error{
248
+ background: var(--auth-error-bg);
249
+ color: var(--auth-error-fg);
250
+ border-color: color-mix(in oklab, var(--auth-error-fg) 24%, transparent);
251
+ }
252
+
253
+ /* ---------- Small divider (opsional, jika nanti butuh) ---------- */
254
+ .auth-divider{
255
+ display: grid;
256
+ grid-template-columns: 1fr auto 1fr;
257
+ align-items: center;
258
+ gap: 10px;
259
+ color: var(--auth-muted);
260
+ font-size: 12px;
261
+ }
262
+ .auth-divider::before,
263
+ .auth-divider::after{
264
+ content:"";
265
+ height: 1px;
266
+ background: color-mix(in oklab, var(--auth-border) 80%, transparent);
267
+ }
268
+
269
+ /* ---------- Responsive ---------- */
270
+ @media (max-width: 560px){
271
+ .auth-card{ padding: 22px 18px; border-radius: 14px; }
272
+ .auth-title{ font-size: 22px; }
273
+ }
274
+
275
+ /* ---------- Reduced motion ---------- */
276
+ @media (prefers-reduced-motion: reduce){
277
+ .auth-bg::before, .auth-bg::after{ animation: none; }
278
+ .auth-card, .auth-card .btn{ transition: none; }
279
+ }
280
+
281
+ /* inset kanan–kiri untuk semua field di login & register */
282
+ .auth-card form{
283
+ padding-inline: 10px; /* kiri–kanan 10px */
284
+ }
285
+ .auth-card input,
286
+ .auth-card .btn{
287
+ width: 100%;
288
+ box-sizing: border-box; /* pastikan hitung padding/border */
289
+ }
templates/about.html ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+ {% block content %}
3
+ <header class="header">
4
+ <h2>About • Virtual Asisten</h2>
5
+ <a class="link" href="{{ url_for('subjects') }}">← Kembali ke Home</a>
6
+ </header>
7
+
8
+ <main class="main about">
9
+ <h3>Virtual Asisten Pembelajaran Berbasis RAG</h3>
10
+ <p>
11
+ Virtual Asisten ini merupakan sistem pembelajaran berbasis Retrieval-Augmented Generation (RAG)
12
+ yang dirancang untuk membantu siswa memahami materi IPAS, PJOK, dan Pancasila lewat tanya-jawab
13
+ interaktif dengan LLM yang terhubung ke basis pengetahuan.
14
+ </p>
15
+
16
+ <h4>Identitas Peneliti</h4>
17
+ <ul class="bio">
18
+ <li><strong>Nama:</strong> <em>isi di sini</em></li>
19
+ <li><strong>NIM:</strong> <em>isi di sini</em></li>
20
+ <li><strong>Program Studi:</strong> <em>isi di sini</em></li>
21
+ <li><strong>Fakultas:</strong> <em>isi di sini</em></li>
22
+ <li><strong>Universitas:</strong> <em>isi di sini</em></li>
23
+ <li><strong>Judul Skripsi:</strong> <em>isi di sini</em></li>
24
+ <li><strong>Dosen Pembimbing Utama:</strong> <em>isi di sini</em></li>
25
+ <li><strong>Dosen Pembimbing Pendamping:</strong> <em>isi di sini</em></li>
26
+ </ul>
27
+
28
+ <h4>Tujuan</h4>
29
+ <p>
30
+ jadi dpr
31
+ </p>
32
+
33
+ <h4>Cara Penggunaan</h4>
34
+ <ol>
35
+ <li>Pilih mata pelajaran (IPAS, PJOK, Pancasila).</li>
36
+ <li>Ketik pertanyaan di kotak chat.</li>
37
+ <li>Sistem menjawab berdasarkan database mapel terpilih.</li>
38
+ </ol>
39
+ </main>
40
+ {% endblock %}
templates/admin_base.html ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="id">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Admin • RAG App</title>
6
+ <meta name="viewport" content="width=device-width,initial-scale=1">
7
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
8
+ <link rel="stylesheet" href="{{ url_for('static', filename='admin.css') }}">
9
+ </head>
10
+ <body>
11
+ <nav class="navbar navbar-expand-lg bg-body-tertiary mb-3">
12
+ <div class="container-fluid">
13
+ <a class="navbar-brand" href="{{ url_for('admin_dashboard') }}">Admin Panel</a>
14
+ <div class="collapse navbar-collapse">
15
+ <ul class="navbar-nav me-auto">
16
+ <li class="nav-item"><a class="nav-link" href="{{ url_for('admin_users') }}">Users</a></li>
17
+ <li class="nav-item"><a class="nav-link" href="{{ url_for('admin_history') }}">History</a></li>
18
+ <li class="nav-item"><a class="nav-link" href="{{ url_for('subjects') }}">Kembali ke App</a></li>
19
+ </ul>
20
+ <span class="navbar-text">Login: {{ session.get('username') }}</span>
21
+ </div>
22
+ </div>
23
+ </nav>
24
+
25
+ <div class="container">
26
+ {% with msgs = get_flashed_messages(with_categories=true) %}
27
+ {% if msgs %}
28
+ {% for cat, msg in msgs %}
29
+ <div class="alert alert-{{ 'danger' if cat=='error' else cat }} alert-dismissible fade show" role="alert">
30
+ {{ msg }}
31
+ <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
32
+ </div>
33
+ {% endfor %}
34
+ {% endif %}
35
+ {% endwith %}
36
+
37
+ {% block content %}{% endblock %}
38
+ </div>
39
+
40
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
41
+ </body>
42
+ </html>
templates/admin_dashboard.html ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "admin_base.html" %}
2
+ {% block content %}
3
+ <h4 class="mb-3">Dashboard</h4>
4
+ <div class="row g-3">
5
+ <div class="col-md-3">
6
+ <div class="card"><div class="card-body">
7
+ <div class="fs-1">{{ total_users }}</div>
8
+ <div class="text-muted">Total Users</div>
9
+ </div></div>
10
+ </div>
11
+ <div class="col-md-3">
12
+ <div class="card"><div class="card-body">
13
+ <div class="fs-1">{{ total_active }}</div>
14
+ <div class="text-muted">Active Users</div>
15
+ </div></div>
16
+ </div>
17
+ <div class="col-md-3">
18
+ <div class="card"><div class="card-body">
19
+ <div class="fs-1">{{ total_admins }}</div>
20
+ <div class="text-muted">Admin</div>
21
+ </div></div>
22
+ </div>
23
+ <div class="col-md-3">
24
+ <div class="card"><div class="card-body">
25
+ <div class="fs-1">{{ total_msgs }}</div>
26
+ <div class="text-muted">Total Messages</div>
27
+ </div></div>
28
+ </div>
29
+ </div>
30
+ {% endblock %}
templates/admin_history.html ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "admin_base.html" %}
2
+ {% block content %}
3
+
4
+ <h4 class="mb-3">Riwayat Chat</h4>
5
+
6
+ <form class="row g-2 mb-3" method="get">
7
+ <div class="col-lg-3 col-md-4 col-sm-6">
8
+ <input type="text" class="form-control" name="q" value="{{ q }}" placeholder="Cari di pesan…">
9
+ </div>
10
+ <div class="col-lg-3 col-md-4 col-sm-6">
11
+ <input type="text" class="form-control" name="username" value="{{ username }}" placeholder="username/email tepat…">
12
+ </div>
13
+ <div class="col-lg-2 col-md-4 col-sm-6">
14
+ <select class="form-select" name="subject">
15
+ <option value="">Semua Mapel</option>
16
+ {% for k, v in subjects.items() %}
17
+ <option value="{{ k }}" {% if subject==k %}selected{% endif %}>{{ v.label }}</option>
18
+ {% endfor %}
19
+ </select>
20
+ </div>
21
+ <div class="col-lg-2 col-md-4 col-sm-6">
22
+ <select class="form-select" name="role">
23
+ <option value="">Semua Role</option>
24
+ <option value="user" {% if role=='user' %}selected{% endif %}>user</option>
25
+ <option value="bot" {% if role=='bot' %}selected{% endif %}>bot</option>
26
+ </select>
27
+ </div>
28
+ <div class="col-lg-1 col-md-3 col-sm-6">
29
+ <input type="number" class="form-control" name="per_page" value="{{ per_page }}" min="5" max="200" title="Jumlah per halaman">
30
+ </div>
31
+ <div class="col-lg-1 col-md-3 col-sm-6">
32
+ <button class="btn btn-primary w-100">Filter</button>
33
+ </div>
34
+ </form>
35
+
36
+ <div class="table-responsive">
37
+ <table class="table table-sm table-hover align-middle">
38
+ <thead>
39
+ <tr>
40
+ <th style="width:72px">ID</th>
41
+ <th style="width:160px">Waktu (WIB)</th>
42
+ <th style="width:220px">User</th>
43
+ <th style="width:120px">Subject</th>
44
+ <th style="width:90px">Role</th>
45
+ <th>Message</th>
46
+ <th style="width:90px">Aksi</th>
47
+ </tr>
48
+ </thead>
49
+ <tbody>
50
+ {% for it in items %}
51
+ <tr>
52
+ <td>{{ it.id }}</td>
53
+ <td>{{ it.timestamp|fmt_ts }}</td>
54
+ <td>
55
+ <div>{{ it.username }}</div>
56
+ <div class="text-muted" style="font-size:.86rem">{{ it.email }}</div>
57
+ </td>
58
+ <td><span class="subject-chip">{{ it.subject }}</span></td>
59
+ <td>
60
+ <span class="badge text-bg-{{ 'secondary' if it.role=='user' else 'info' }}">{{ it.role }}</span>
61
+ </td>
62
+ <td>
63
+ <div class="truncate-3" title="{{ it.message }}">{{ it.message }}</div>
64
+ </td>
65
+ <td class="text-nowrap">
66
+ <form action="{{ url_for('admin_delete_chat', chat_id=it.id) }}"
67
+ method="post" style="display:inline"
68
+ onsubmit="return confirm('Hapus riwayat chat #{{it.id}}?');">
69
+ <button class="btn btn-sm btn-outline-danger">Hapus</button>
70
+ </form>
71
+ </td>
72
+ </tr>
73
+ {% else %}
74
+ <tr>
75
+ <td colspan="7" class="text-center text-muted py-4">Belum ada data riwayat yang cocok.</td>
76
+ </tr>
77
+ {% endfor %}
78
+ </tbody>
79
+ </table>
80
+ </div>
81
+
82
+ {% set last = (total // per_page) + (1 if total % per_page else 0) %}
83
+ <nav aria-label="pagination">
84
+ <ul class="pagination">
85
+ <li class="page-item {% if page <= 1 %}disabled{% endif %}">
86
+ <a class="page-link"
87
+ href="{{ url_for('admin_history', q=q, username=username, subject=subject, role=role, per_page=per_page, page=page-1) }}">Prev</a>
88
+ </li>
89
+ <li class="page-item disabled">
90
+ <span class="page-link">Page {{ page }} / {{ last or 1 }}</span>
91
+ </li>
92
+ <li class="page-item {% if page >= last %}disabled{% endif %}">
93
+ <a class="page-link"
94
+ href="{{ url_for('admin_history', q=q, username=username, subject=subject, role=role, per_page=per_page, page=page+1) }}">Next</a>
95
+ </li>
96
+ </ul>
97
+ </nav>
98
+
99
+ <div class="admin-footer">Total baris: <strong>{{ total }}</strong></div>
100
+
101
+ {% endblock %}
templates/admin_users.html ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "admin_base.html" %}
2
+ {% block content %}
3
+
4
+ <h4 class="mb-3">Daftar Users</h4>
5
+
6
+ <form class="row g-2 mb-3" method="get">
7
+ <div class="col-md-4 col-sm-6">
8
+ <input type="text" class="form-control" name="q" value="{{ q }}" placeholder="Cari username atau email…">
9
+ </div>
10
+ <div class="col-md-2 col-sm-3">
11
+ <input type="number" class="form-control" name="per_page" value="{{ per_page }}" min="5" max="100" title="Jumlah per halaman">
12
+ </div>
13
+ <div class="col-md-2 col-sm-3">
14
+ <button class="btn btn-primary w-100">Filter</button>
15
+ </div>
16
+ </form>
17
+
18
+ <div class="table-responsive">
19
+ <table class="table table-sm table-hover align-middle">
20
+ <thead>
21
+ <tr>
22
+ <th style="width:72px">ID</th>
23
+ <th>Username</th>
24
+ <th>Email</th>
25
+ <th style="width:90px">Active</th>
26
+ <th style="width:90px">Admin</th>
27
+ <th style="width:90px">#Chats</th>
28
+ <th style="width:280px">Aksi</th>
29
+ </tr>
30
+ </thead>
31
+ <tbody>
32
+ {% for u in users %}
33
+ <tr>
34
+ <td>{{ u.id }}</td>
35
+ <td>{{ u.username }}</td>
36
+ <td>{{ u.email }}</td>
37
+ <td>
38
+ <span class="badge {{ 'badge-yes' if u.is_active else 'badge-no' }}">
39
+ {{ 'Yes' if u.is_active else 'No' }}
40
+ </span>
41
+ </td>
42
+ <td>
43
+ <span class="badge {{ 'badge-yes' if u.is_admin else 'badge-no' }}">
44
+ {{ 'Yes' if u.is_admin else 'No' }}
45
+ </span>
46
+ </td>
47
+ <td>{{ counts.get(u.id, 0) }}</td>
48
+ <td class="text-nowrap">
49
+ <a class="btn btn-sm btn-outline-dark"
50
+ href="{{ url_for('admin_history', username=u.username) }}">Lihat History</a>
51
+
52
+ <form action="{{ url_for('admin_clear_user_history', user_id=u.id) }}"
53
+ method="post" style="display:inline"
54
+ onsubmit="return confirm('Hapus semua riwayat user {{u.username}}?');">
55
+ <button class="btn btn-sm btn-outline-warning">Clear History</button>
56
+ </form>
57
+
58
+ <form action="{{ url_for('admin_delete_user', user_id=u.id) }}"
59
+ method="post" style="display:inline"
60
+ onsubmit="return confirm('Hapus user {{u.username}} dan semua datanya? Tindakan ini tidak bisa dibatalkan.');">
61
+ <button class="btn btn-sm btn-outline-danger">Delete User</button>
62
+ </form>
63
+ </td>
64
+ </tr>
65
+ {% else %}
66
+ <tr>
67
+ <td colspan="7" class="text-center text-muted py-4">Belum ada data user.</td>
68
+ </tr>
69
+ {% endfor %}
70
+ </tbody>
71
+ </table>
72
+ </div>
73
+
74
+ {% set last = (total // per_page) + (1 if total % per_page else 0) %}
75
+ <nav aria-label="pagination">
76
+ <ul class="pagination">
77
+ <li class="page-item {% if page <= 1 %}disabled{% endif %}">
78
+ <a class="page-link" href="{{ url_for('admin_users', q=q, per_page=per_page, page=page-1) }}">Prev</a>
79
+ </li>
80
+ <li class="page-item disabled">
81
+ <span class="page-link">Page {{ page }} / {{ last or 1 }}</span>
82
+ </li>
83
+ <li class="page-item {% if page >= last %}disabled{% endif %}">
84
+ <a class="page-link" href="{{ url_for('admin_users', q=q, per_page=per_page, page=page+1) }}">Next</a>
85
+ </li>
86
+ </ul>
87
+ </nav>
88
+
89
+ <div class="admin-footer">Total users: <strong>{{ total }}</strong></div>
90
+
91
+ {% endblock %}
templates/base.html ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="id">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>RAG Chat</title>
7
+ <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
8
+ </head>
9
+ <body>
10
+ <div class="container">
11
+ {% block content %}{% endblock %}
12
+ </div>
13
+ </body>
14
+ </html>
templates/chat.html ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+ {% block content %}
3
+ <header class="header">
4
+ <h2>{{ subject_label }} - {{ "Ilmu Pengetahuan Alam dan Sosial" if subject=='ipas'
5
+ else "Pendidikan Jasmani, Olahraga, dan Kesehatan" if subject=='penjas'
6
+ else "Pendidikan Pancasila dan Kewarganegaraan" }}</h2>
7
+ <a class="link" href="{{ url_for('subjects') }}">← Kembali ke Home</a>
8
+ </header>
9
+
10
+ <main class="main">
11
+ <div id="chat-box" class="chat-box">
12
+ <div class="msg bot">Halo! Selamat datang di chat mata pelajaran {{ subject_label }}. Ada yang bisa saya bantu?</div>
13
+ </div>
14
+ <div class="input-row">
15
+ <input id="message" type="text" placeholder="Ketik pesan Anda...">
16
+ <button id="send" class="btn">SEND</button>
17
+ </div>
18
+ </main>
19
+
20
+ <script>
21
+ const chatBox = document.getElementById('chat-box');
22
+ const msgInput = document.getElementById('message');
23
+ const sendBtn = document.getElementById('send');
24
+
25
+ function append(role, text) {
26
+ const item = document.createElement('div');
27
+ item.className = 'msg ' + role;
28
+ item.textContent = text;
29
+ chatBox.appendChild(item);
30
+ chatBox.scrollTop = chatBox.scrollHeight;
31
+ }
32
+
33
+ async function ask(text){
34
+ const res = await fetch('{{ url_for("ask", subject_key=subject) }}', {
35
+ method: 'POST',
36
+ headers: {'Content-Type': 'application/json'},
37
+ body: JSON.stringify({message: text})
38
+ });
39
+ return await res.json();
40
+ }
41
+
42
+ // ✅ FIX: pastikan history selalu ada array valid
43
+ const history = {{ history|default([])|tojson|safe }};
44
+ for (const item of history) {
45
+ const div = document.createElement('div');
46
+ div.className = 'msg ' + item.role;
47
+ div.textContent = item.message;
48
+ chatBox.appendChild(div);
49
+ }
50
+
51
+ sendBtn.addEventListener('click', async () => {
52
+ const text = msgInput.value.trim();
53
+ if (!text) return;
54
+ append('user', text);
55
+ msgInput.value = '';
56
+ append('bot', '⏳ Sedang memikirkan jawaban...');
57
+ try {
58
+ const data = await ask(text);
59
+ chatBox.lastChild.textContent = data.ok ? data.answer : ('Error: ' + (data.error || 'unknown'));
60
+ } catch (e) {
61
+ chatBox.lastChild.textContent = 'Error jaringan.';
62
+ }
63
+ });
64
+
65
+ msgInput.addEventListener('keydown', (e) => {
66
+ if (e.key === 'Enter') sendBtn.click();
67
+ });
68
+ </script>
69
+ {% endblock %}
templates/home.html ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+ {% block content %}
3
+ <header class="hero">
4
+ <h1>VIRTUAL ASISTEN</h1>
5
+ <nav class="nav">
6
+ <a class="nav-link" href="{{ url_for('subjects') }}">Home</a>
7
+ <a class="nav-link" href="{{ url_for('about') }}">About</a>
8
+
9
+ {% if session.get('logged_in') %}
10
+ <a class="nav-link" href="{{ url_for('auth_logout') }}">Logout</a>
11
+ {% endif %}
12
+
13
+ {% if session.get('is_admin') %}
14
+ <a class="nav-link nav-admin" href="{{ url_for('admin_dashboard') }}">Admin</a>
15
+ {% endif %}
16
+ </nav>
17
+ </header>
18
+
19
+
20
+ <section class="welcome">
21
+ <h2>Selamat Datang di Virtual Asisten</h2>
22
+ <p>Platform pembelajaran online untuk membantu Anda memahami materi pelajaran dengan lebih mudah</p>
23
+ </section>
24
+
25
+ <section class="subjects">
26
+ <h3>Mata Pelajaran</h3>
27
+ <div class="grid">
28
+ {% for key, cfg in subjects.items() %}
29
+ <a class="card" href="{{ url_for('chat_subject', subject_key=key) }}">
30
+ <h4>{{ cfg.label }}</h4>
31
+ <p>{{ cfg.desc }}</p>
32
+ </a>
33
+ {% endfor %}
34
+ </div>
35
+ </section>
36
+ {% endblock %}
templates/login.html ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+ {% block content %}
3
+ <div class="auth-bg">
4
+ <div class="auth-card">
5
+ <h1 class="auth-title">Selamat Datang</h1>
6
+ <p class="auth-subtitle">Silakan masuk ke akun Anda untuk melanjutkan.</p>
7
+
8
+ {# flash message #}
9
+ {% with messages = get_flashed_messages(with_categories=true) %}
10
+ {% if messages %}
11
+ {% for category, msg in messages %}
12
+ <div class="flash {{ category }}">{{ msg }}</div>
13
+ {% endfor %}
14
+ {% endif %}
15
+ {% endwith %}
16
+
17
+ <form method="POST" action="{{ url_for('auth_login') }}">
18
+ <input name="identity" placeholder="Email atau Username" required>
19
+ <input name="password" type="password" placeholder="Password" required>
20
+ <button class="btn" type="submit">Masuk</button>
21
+ </form>
22
+
23
+ <p class="muted">Tidak punya akun? <a href="{{ url_for('auth_register') }}">Daftar sekarang</a></p>
24
+ </div>
25
+ </div>
26
+ {% endblock %}
templates/register.html ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+ {% block content %}
3
+ <div class="auth-bg">
4
+ <div class="auth-card" style="width:420px;">
5
+ <h1 class="auth-title">Daftar Akun</h1>
6
+ <p class="auth-subtitle">Buat akun baru untuk menggunakan RAG Chat • Sekolah.</p>
7
+
8
+ {# flash message #}
9
+ {% with messages = get_flashed_messages(with_categories=true) %}
10
+ {% if messages %}
11
+ {% for category, msg in messages %}
12
+ <div class="flash {{ category }}">{{ msg }}</div>
13
+ {% endfor %}
14
+ {% endif %}
15
+ {% endwith %}
16
+
17
+ <form method="POST" action="{{ url_for('auth_register') }}">
18
+ <input name="username" placeholder="Username" required>
19
+ <input name="email" type="email" placeholder="Email" required>
20
+ <input name="password" type="password" placeholder="Password" minlength="6" required>
21
+ <input name="confirm" type="password" placeholder="Konfirmasi Password" minlength="6" required>
22
+ <button class="btn" type="submit">Daftar</button>
23
+ </form>
24
+
25
+ <p class="muted">Sudah punya akun? <a href="{{ url_for('auth_login') }}">Masuk</a></p>
26
+ </div>
27
+ </div>
28
+ {% endblock %}