anggars commited on
Commit
11962ff
·
verified ·
1 Parent(s): 5976aec

Sync from GitHub Actions: 1466e2b8a37e8b0eee3c2880205c785f39c6fbe1

Browse files
api/core/chatbot.py CHANGED
@@ -4,12 +4,12 @@ import google.generativeai as genai
4
 
5
  class MBTIChatbot:
6
  def __init__(self):
7
- print("🚀 Initializing MBTI Chatbot (Lite Version)...")
8
 
9
  # 1. Setup Google Gemini
10
  api_key = os.getenv("GEMINI_API_KEY")
11
  if not api_key:
12
- print("⚠️ WARNING: GEMINI_API_KEY not found in .env.")
13
  else:
14
  genai.configure(api_key=api_key)
15
 
@@ -17,7 +17,7 @@ class MBTIChatbot:
17
  # Pake Gemini 2.0 Flash (Standard)
18
  self.model = genai.GenerativeModel('gemini-2.0-flash')
19
  except Exception:
20
- print("⚠️ 2.0 Flash failed, fallback to Lite")
21
  self.model = genai.GenerativeModel('gemini-2.0-flash-lite')
22
 
23
  def generate_response(self, user_query, lang="en"):
@@ -36,6 +36,7 @@ INSTRUCTIONS:
36
  - Answer directly based on your extensive knowledge about MBTI and Psychology.
37
  - Be empathetic, insightful, and use formatting (bullet points) if helpful.
38
  - Keep answers concise (under 200 words) unless asked for details.
 
39
  """
40
  try:
41
  response = self.model.generate_content(system_prompt)
 
4
 
5
  class MBTIChatbot:
6
  def __init__(self):
7
+ print("[INIT] Initializing MBTI Chatbot (Lite Version)...")
8
 
9
  # 1. Setup Google Gemini
10
  api_key = os.getenv("GEMINI_API_KEY")
11
  if not api_key:
12
+ print("[WARN] GEMINI_API_KEY not found in .env.")
13
  else:
14
  genai.configure(api_key=api_key)
15
 
 
17
  # Pake Gemini 2.0 Flash (Standard)
18
  self.model = genai.GenerativeModel('gemini-2.0-flash')
19
  except Exception:
20
+ print("[WARN] 2.0 Flash failed, fallback to Lite")
21
  self.model = genai.GenerativeModel('gemini-2.0-flash-lite')
22
 
23
  def generate_response(self, user_query, lang="en"):
 
36
  - Answer directly based on your extensive knowledge about MBTI and Psychology.
37
  - Be empathetic, insightful, and use formatting (bullet points) if helpful.
38
  - Keep answers concise (under 200 words) unless asked for details.
39
+ - DO NOT use emojis in your response. Keep it clean and text-only.
40
  """
41
  try:
42
  response = self.model.generate_content(system_prompt)
api/core/nlp_handler.py CHANGED
@@ -7,6 +7,7 @@ import html
7
  from deep_translator import GoogleTranslator
8
  from youtube_transcript_api import YouTubeTranscriptApi
9
  import google.generativeai as genai
 
10
 
11
  # --- CONFIG PATH ---
12
  BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -14,7 +15,9 @@ MBTI_PATH = os.path.join(BASE_DIR, 'data', 'model_mbti.pkl')
14
  EMOTION_PATH = os.path.join(BASE_DIR, 'data', 'model_emotion.pkl')
15
 
16
  _model_mbti = None
17
- _model_emotion = None
 
 
18
 
19
  EMOTION_TRANSLATIONS = {
20
  'admiration': 'Kagum', 'amusement': 'Terhibur', 'anger': 'Marah',
@@ -72,18 +75,35 @@ class NLPHandler:
72
 
73
  @staticmethod
74
  def load_models():
75
- global _model_mbti, _model_emotion
76
- print(f"📂 Loading models from: {BASE_DIR}")
77
 
78
  if _model_mbti is None and os.path.exists(MBTI_PATH):
79
  try:
 
80
  _model_mbti = joblib.load(MBTI_PATH)
81
- except Exception as e: print(f"MBTI Load Error: {e}")
82
-
83
- if _model_emotion is None and os.path.exists(EMOTION_PATH):
84
- try:
85
- _model_emotion = joblib.load(EMOTION_PATH)
86
- except Exception as e: print(f"❌ Emotion Load Error: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
 
88
  # --- GEMINI VALIDATOR SETUP ---
89
  _gemini_model = None
@@ -97,9 +117,9 @@ class NLPHandler:
97
  try:
98
  genai.configure(api_key=api_key)
99
  NLPHandler._gemini_model = genai.GenerativeModel('gemini-2.0-flash-lite')
100
- print("Gemini Validator Ready")
101
  except Exception as e:
102
- print(f"⚠️ Gemini Init Failed: {e}")
103
  return NLPHandler._gemini_model is not None
104
 
105
  @staticmethod
@@ -176,13 +196,13 @@ REASON: Explicit mentions of networking, leading teams, and structured planning
176
 
177
  # Validate MBTI format (must be 4 chars)
178
  if len(validated_mbti) != 4 or not all(c in 'IENTFSJP' for c in validated_mbti):
179
- print(f"⚠️ Invalid Gemini MBTI: {validated_mbti}, using ML: {ml_prediction}")
180
  return ml_prediction, 0.6, "Invalid Gemini response - using ML"
181
 
182
  return validated_mbti, confidence, reason
183
 
184
  except Exception as e:
185
- print(f"⚠️ Gemini Validation Error: {e}")
186
  return ml_prediction, 0.6, f"Gemini error - using ML"
187
 
188
  @staticmethod
@@ -219,68 +239,127 @@ REASON: Explicit mentions of networking, leading teams, and structured planning
219
  mbti_confidence = 0.0
220
  mbti_reasoning = ""
221
 
222
- if _model_mbti:
223
  try:
224
- # Step 1: ML Model Prediction
225
- ml_prediction = _model_mbti.predict([processed_text])[0]
226
- print(f"📊 ML Prediction: {ml_prediction}")
227
 
228
- # Step 2: Gemini Validation (Always run)
229
- # if len(raw_text.split()) >= 50:
230
- validated_mbti, confidence, reason = NLPHandler._validate_with_gemini(
231
- processed_text, ml_prediction
232
- )
233
- mbti_result = validated_mbti
234
- mbti_confidence = confidence
235
- mbti_reasoning = reason
236
 
237
- if validated_mbti != ml_prediction:
238
- print(f"🔄 Gemini Override: {ml_prediction} {validated_mbti} (Confidence: {confidence:.2f})")
 
 
 
 
239
  else:
240
- print(f"✅ Gemini Confirmed: {validated_mbti} (Confidence: {confidence:.2f})")
 
 
 
 
 
 
 
 
 
 
 
 
 
241
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
  except Exception as e:
243
- print(f" MBTI Prediction Error: {e}")
244
- mbti_result = "INTJ" # Fallback
245
- mbti_confidence = 0.3
246
- mbti_reasoning = "Error - fallback prediction"
247
-
248
- # --- EMOTION PREDICTION & CONFIDENCE ---
249
- emotion_data = {"id": "Kompleks", "en": "Complex", "raw": "unknown"}
 
 
 
 
250
  confidence_score = 0.0
251
 
252
- if _model_emotion:
253
- try:
254
- pred_label = "neutral"
255
- if hasattr(_model_emotion, "predict_proba"):
256
- probs = _model_emotion.predict_proba([processed_text])[0]
257
- classes = _model_emotion.classes_
258
-
259
- # Neutral dampening logic
260
- neutral_indices = [i for i, c in enumerate(classes) if c.lower() == 'neutral']
261
- if neutral_indices:
262
- idx = neutral_indices[0]
263
- if probs[idx] < 0.65: probs[idx] = 0.0
264
-
265
- # RE-NORMALIZE PROBABILITIES
266
- # Agar sisa probabilitas naik proporsional.
267
- # Misal: [0.1, 0.1, 0.1, 0.0] -> [0.33, 0.33, 0.33, 0.0]
268
- total_prob = np.sum(probs)
269
- if total_prob > 0:
270
- probs = probs / total_prob
271
-
272
- if np.sum(probs) > 0:
273
- best_idx = np.argmax(probs)
274
- pred_label = classes[best_idx]
275
- confidence_score = float(probs[best_idx])
276
- else:
277
- pred_label = _model_emotion.predict([processed_text])[0]
278
- else:
279
- pred_label = _model_emotion.predict([processed_text])[0]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
280
 
281
- indo_label = EMOTION_TRANSLATIONS.get(pred_label, pred_label.capitalize())
282
- emotion_data = {"id": indo_label, "en": pred_label.capitalize(), "raw": pred_label}
283
- except: pass
284
 
285
  # --- REASONING GENERATION ---
286
  mbti_desc = MBTI_EXPLANATIONS.get(mbti_result, {
@@ -292,19 +371,21 @@ REASON: Explicit mentions of networking, leading teams, and structured planning
292
  if mbti_reasoning:
293
  mbti_desc['validation'] = mbti_reasoning
294
  mbti_desc['confidence'] = mbti_confidence
295
-
296
  # Emotion Reasoning
297
  conf_percent = int(confidence_score * 100)
 
 
 
 
 
 
 
298
  emotion_reasoning = {
299
- 'en': f"Based on the text pattern, the AI is {conf_percent}% confident this matches '{emotion_data['en']}'.",
300
- 'id': f"Dari gaya tulisan lo, AI {conf_percent}% yakin mood lo lagi '{emotion_data['id']}'."
301
  }
302
- if conf_percent < 50 and confidence_score > 0:
303
- emotion_reasoning = {
304
- 'en': f"The sentiment is mixed, but slightly leans towards '{emotion_data['en']}' ({conf_percent}%).",
305
- 'id': f"Mood lo campur aduk, tapi agak condong ke '{emotion_data['id']}' dikit ({conf_percent}%)."
306
- }
307
-
308
  # Keywords Reasoning
309
  keywords_reasoning = {
310
  'en': "These words appeared most frequently and define the main topic.",
@@ -325,7 +406,7 @@ REASON: Explicit mentions of networking, leading teams, and structured planning
325
  # --- JALUR RESMI: YOUTUBE DATA API ---
326
  @staticmethod
327
  def _fetch_official_api(video_id, api_key):
328
- print(f"🔑 Using Official API Key for {video_id}...")
329
 
330
  result = {
331
  "video": None,
@@ -398,7 +479,7 @@ REASON: Explicit mentions of networking, leading teams, and structured planning
398
  return result
399
 
400
  except Exception as e:
401
- print(f"Official API Error: {e}")
402
  return None
403
 
404
  @staticmethod
@@ -412,7 +493,7 @@ REASON: Explicit mentions of networking, leading teams, and structured planning
412
  return official_data
413
 
414
  # 2. PRIORITAS KEDUA: Fallback Scraping
415
- print(f"🎬 Fetching transcript (fallback) for: {video_id}")
416
  try:
417
  transcript_list = YouTubeTranscriptApi.get_transcript(video_id, languages=['id', 'en', 'en-US'])
418
  full_text = " ".join([item['text'] for item in transcript_list])
 
7
  from deep_translator import GoogleTranslator
8
  from youtube_transcript_api import YouTubeTranscriptApi
9
  import google.generativeai as genai
10
+ import time
11
 
12
  # --- CONFIG PATH ---
13
  BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 
15
  EMOTION_PATH = os.path.join(BASE_DIR, 'data', 'model_emotion.pkl')
16
 
17
  _model_mbti = None
18
+ _classifier_mbti_transformer = None
19
+ _classifier_roberta = None
20
+ _classifier_distilbert = None
21
 
22
  EMOTION_TRANSLATIONS = {
23
  'admiration': 'Kagum', 'amusement': 'Terhibur', 'anger': 'Marah',
 
75
 
76
  @staticmethod
77
  def load_models():
78
+ global _model_mbti, _classifier_mbti_transformer, _classifier_roberta, _classifier_distilbert
79
+ print(f"Loading models from: {BASE_DIR}")
80
 
81
  if _model_mbti is None and os.path.exists(MBTI_PATH):
82
  try:
83
+ print(f"Loading MBTI Model (SVM) from: {MBTI_PATH}")
84
  _model_mbti = joblib.load(MBTI_PATH)
85
+ except Exception as e: print(f"MBTI Load Error: {e}")
86
+
87
+ if _classifier_mbti_transformer is None:
88
+ try:
89
+ print(f"Loading MBTI Model (Transformer): parka735/mbti-classifier")
90
+ from transformers import pipeline
91
+ _classifier_mbti_transformer = pipeline("text-classification", model="parka735/mbti-classifier", top_k=1)
92
+ except Exception as e: print(f"MBTI Transformer Load Error: {e}")
93
+
94
+ if _classifier_roberta is None:
95
+ try:
96
+ print("Loading Emotion Model 1: SamLowe/roberta-base-go_emotions")
97
+ from transformers import pipeline
98
+ _classifier_roberta = pipeline("text-classification", model="SamLowe/roberta-base-go_emotions", top_k=None)
99
+ except Exception as e: print(f"Emotion 1 Load Error: {e}")
100
+
101
+ if _classifier_distilbert is None:
102
+ try:
103
+ print("Loading Emotion Model 2: joeddav/distilbert-base-uncased-go-emotions-student")
104
+ from transformers import pipeline
105
+ _classifier_distilbert = pipeline("text-classification", model="joeddav/distilbert-base-uncased-go-emotions-student", top_k=None)
106
+ except Exception as e: print(f"Emotion 2 Load Error: {e}")
107
 
108
  # --- GEMINI VALIDATOR SETUP ---
109
  _gemini_model = None
 
117
  try:
118
  genai.configure(api_key=api_key)
119
  NLPHandler._gemini_model = genai.GenerativeModel('gemini-2.0-flash-lite')
120
+ print("Gemini Validator Ready")
121
  except Exception as e:
122
+ print(f"Gemini Init Failed: {e}")
123
  return NLPHandler._gemini_model is not None
124
 
125
  @staticmethod
 
196
 
197
  # Validate MBTI format (must be 4 chars)
198
  if len(validated_mbti) != 4 or not all(c in 'IENTFSJP' for c in validated_mbti):
199
+ print(f"Invalid Gemini MBTI: {validated_mbti}, using ML: {ml_prediction}")
200
  return ml_prediction, 0.6, "Invalid Gemini response - using ML"
201
 
202
  return validated_mbti, confidence, reason
203
 
204
  except Exception as e:
205
+ print(f"Gemini Validation Error: {e}")
206
  return ml_prediction, 0.6, f"Gemini error - using ML"
207
 
208
  @staticmethod
 
239
  mbti_confidence = 0.0
240
  mbti_reasoning = ""
241
 
242
+ if _model_mbti and _classifier_mbti_transformer:
243
  try:
244
+ # 1. SVM Prediction (Keyword/Structure)
245
+ svm_pred = _model_mbti.predict([processed_text])[0]
 
246
 
247
+ # 2. Transformer Prediction
248
+ trans_input = processed_text[:2000]
249
+ trans_output = _classifier_mbti_transformer(trans_input)
 
 
 
 
 
250
 
251
+ # Handle nested list output (common in batched pipelines)
252
+ # Output can be [{'label': 'A'}] OR [[{'label': 'A'}]]
253
+ if isinstance(trans_output, list) and isinstance(trans_output[0], list):
254
+ trans_res = trans_output[0][0]
255
+ elif isinstance(trans_output, list):
256
+ trans_res = trans_output[0]
257
  else:
258
+ trans_res = trans_output
259
+
260
+ trans_pred = trans_res['label'].upper()
261
+ trans_conf = trans_res['score']
262
+
263
+ print(f"[Voting] SVM='{svm_pred}' vs Transformer='{trans_pred}' ({trans_conf:.2%})")
264
+
265
+ # 3. Consensus Logic
266
+ if svm_pred == trans_pred:
267
+ # Both agree! High confidence.
268
+ print("[Check] Models AGREE! Auto-approving.")
269
+ mbti_result = svm_pred
270
+ mbti_confidence = 0.95
271
+ mbti_reasoning = f"Both AI models agreed strictly on {mbti_result}."
272
 
273
+ # Optional: Lightweight Gemini check just for reasoning text, IF enabled.
274
+ # validation is skipped for speed since we have consensus.
275
+ else:
276
+ # Disagreement! Gemini is the Tie-Breaker.
277
+ print("[Warning] Models DISAGREE! Summoning Gemini Judge...")
278
+
279
+ # Prepare context for Gemini
280
+ validation_context = f"Model A (Keyword) detected {svm_pred}. Model B (Context) detected {trans_pred}."
281
+
282
+ validated_mbti, confidence, reason = NLPHandler._validate_with_gemini(
283
+ processed_text, validation_context
284
+ )
285
+
286
+ mbti_result = validated_mbti
287
+ mbti_confidence = confidence
288
+ mbti_reasoning = reason
289
+ print(f"[Gemini] Verdict: {mbti_result} (Confidence: {confidence})")
290
+
291
  except Exception as e:
292
+ print(f"[Error] Hybrid MBTI Error: {e}")
293
+ # Fallback to SVM if everything explodes
294
+ try:
295
+ mbti_result = _model_mbti.predict([processed_text])[0]
296
+ mbti_confidence = 0.4
297
+ except:
298
+ mbti_result = "INTJ"
299
+ mbti_reasoning = "System fallback due to hybrid error."
300
+
301
+ # --- EMOTION PREDICTION (HYBRID TRANSFORMER) ---
302
+ emotion_data = {"id": "Netral", "en": "Neutral", "raw": "neutral", "list": []}
303
  confidence_score = 0.0
304
 
305
+ try:
306
+ # Load pipelines (Ensured in load_models)
307
+ global _classifier_roberta, _classifier_distilbert
308
+
309
+ # Truncate for safety
310
+ emo_input = processed_text[:1500]
311
+
312
+ combined_scores = {}
313
+
314
+ def add_scores(results):
315
+ if isinstance(results, list) and isinstance(results[0], list):
316
+ results = results[0]
317
+ for item in results:
318
+ label = item['label']
319
+ score = item['score']
320
+ combined_scores[label] = combined_scores.get(label, 0) + score
321
+
322
+ if _classifier_roberta:
323
+ add_scores(_classifier_roberta(emo_input))
324
+ if _classifier_distilbert:
325
+ add_scores(_classifier_distilbert(emo_input))
326
+
327
+ # Normalize and filter
328
+ if 'neutral' in combined_scores:
329
+ del combined_scores['neutral'] # Remove neutral preference
330
+
331
+ sorted_emotions = sorted(combined_scores.items(), key=lambda x: x[1], reverse=True)
332
+
333
+ top_3_list = []
334
+ if sorted_emotions:
335
+ # Top 1 for legacy compatibility
336
+ best_label, total_score = sorted_emotions[0]
337
+ confidence_score = (total_score / 2.0)
338
+
339
+ indo_label = EMOTION_TRANSLATIONS.get(best_label, best_label.capitalize())
340
+ emotion_data = {
341
+ "id": indo_label,
342
+ "en": best_label.capitalize(),
343
+ "raw": best_label,
344
+ "list": [] # Will populate below
345
+ }
346
+
347
+ # Populate Top 3 List
348
+ for label, score in sorted_emotions[:3]:
349
+ norm_score = score / 2.0
350
+ top_3_list.append({
351
+ "en": label.capitalize(),
352
+ "id": EMOTION_TRANSLATIONS.get(label, label.capitalize()),
353
+ "score": norm_score
354
+ })
355
+
356
+ emotion_data["list"] = top_3_list
357
+ print(f"Emotion Hybrid Top 1: {emotion_data['en']} ({confidence_score:.2%})")
358
+ else:
359
+ print("Emotion Hybrid: No clear emotion found (Neutral)")
360
 
361
+ except Exception as e:
362
+ print(f"Emotion Prediction Error: {e}")
 
363
 
364
  # --- REASONING GENERATION ---
365
  mbti_desc = MBTI_EXPLANATIONS.get(mbti_result, {
 
371
  if mbti_reasoning:
372
  mbti_desc['validation'] = mbti_reasoning
373
  mbti_desc['confidence'] = mbti_confidence
374
+
375
  # Emotion Reasoning
376
  conf_percent = int(confidence_score * 100)
377
+
378
+ # Generate dynamic reasoning for Top 3
379
+ em_list_str = ""
380
+ if 'list' in emotion_data and emotion_data['list']:
381
+ labels = [f"{item['en']} ({int(item['score']*100)}%)" for item in emotion_data['list']]
382
+ em_list_str = ", ".join(labels)
383
+
384
  emotion_reasoning = {
385
+ 'en': f"Dominant emotion is '{emotion_data['en']}'. Mix: {em_list_str}.",
386
+ 'id': f"Emosi dominan '{emotion_data['id']}'. Campuran: {em_list_str}."
387
  }
388
+
 
 
 
 
 
389
  # Keywords Reasoning
390
  keywords_reasoning = {
391
  'en': "These words appeared most frequently and define the main topic.",
 
406
  # --- JALUR RESMI: YOUTUBE DATA API ---
407
  @staticmethod
408
  def _fetch_official_api(video_id, api_key):
409
+ print(f"Using Official API Key for {video_id}...")
410
 
411
  result = {
412
  "video": None,
 
479
  return result
480
 
481
  except Exception as e:
482
+ print(f"Official API Error: {e}")
483
  return None
484
 
485
  @staticmethod
 
493
  return official_data
494
 
495
  # 2. PRIORITAS KEDUA: Fallback Scraping
496
+ print(f"Fetching transcript (fallback) for: {video_id}")
497
  try:
498
  transcript_list = YouTubeTranscriptApi.get_transcript(video_id, languages=['id', 'en', 'en-US'])
499
  full_text = " ".join([item['text'] for item in transcript_list])
api/index.py CHANGED
@@ -40,11 +40,19 @@ async def startup_event():
40
  api_key = os.getenv("YOUTUBE_API_KEY")
41
  print("\n" + "="*40)
42
  if api_key:
43
- print(f" API KEY DITEMUKAN: {api_key[:5]}...******")
44
- print("🚀 Mode: OFFICIAL API (Anti-Blokir)")
45
  else:
46
- print(" API KEY TIDAK DITEMUKAN!")
47
- print("⚠️ Mode: FALLBACK SCRAPING (Rawan Error)")
 
 
 
 
 
 
 
 
48
  print("="*40 + "\n")
49
 
50
  app.add_api_route("/api/predict", predict_endpoint, methods=["POST"])
 
40
  api_key = os.getenv("YOUTUBE_API_KEY")
41
  print("\n" + "="*40)
42
  if api_key:
43
+ print(f"[OK] API KEY DITEMUKAN: {api_key[:5]}...******")
44
+ print("[MODE] Mode: OFFICIAL API (Anti-Blokir)")
45
  else:
46
+ print("[ERR] API KEY TIDAK DITEMUKAN!")
47
+ print("[WARN] Mode: FALLBACK SCRAPING (Rawan Error)")
48
+
49
+ print("\n[WAIT] PRE-LOADING MODELS (Transformer + Emotions)...")
50
+ try:
51
+ NLPHandler.load_models()
52
+ print("[OK] Models Loaded Successfully!")
53
+ except Exception as e:
54
+ print(f"[ERR] Model Preload Failed: {e}")
55
+
56
  print("="*40 + "\n")
57
 
58
  app.add_api_route("/api/predict", predict_endpoint, methods=["POST"])
api/requirements.txt CHANGED
@@ -8,4 +8,6 @@ joblib
8
  deep-translator
9
  requests
10
  youtube-transcript-api
11
- google-generativeai
 
 
 
8
  deep-translator
9
  requests
10
  youtube-transcript-api
11
+ google-generativeai
12
+ transformers
13
+ torch
src/app/analyzer/page.tsx CHANGED
@@ -378,51 +378,74 @@ export default function AnalysisPage() {
378
  className="w-full max-w-3xl mx-auto mt-6 space-y-4 text-left"
379
  >
380
  <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
 
381
  {/* MBTI */}
382
  <div className="liquid-glass p-5 border-t-4 border-orange-500 bg-white/60 dark:bg-black/40 backdrop-blur-md rounded-xl flex flex-col h-full group hover:bg-white/80 dark:hover:bg-white/5 transition-all">
383
- <h3 className="text-[10px] font-bold uppercase tracking-widest opacity-60 flex justify-center gap-2 items-center text-gray-800 dark:text-gray-200 mb-2">
384
  <BrainCircuit size={12} /> {content.resMBTI}
385
  </h3>
386
- <div className="flex-1 flex flex-col items-center justify-center gap-2">
387
  <div className="text-4xl font-black text-orange-600 tracking-tight">
388
  {result.mbti_type}
389
  </div>
390
- {/* Reasoning MBTI */}
391
- {result.reasoning && (
392
- <p className="text-xs text-center text-gray-600 dark:text-gray-300 leading-relaxed px-2 font-medium">
 
 
 
393
  {result.reasoning.mbti?.[lang] ||
394
  "Analisis kepribadian mendalam."}
395
  </p>
396
- )}
397
- </div>
398
  </div>
399
 
400
  {/* EMOTION */}
401
  <div className="liquid-glass p-5 border-t-4 border-green-500 bg-white/60 dark:bg-black/40 backdrop-blur-md rounded-xl flex flex-col h-full group hover:bg-white/80 dark:hover:bg-white/5 transition-all">
402
- <h3 className="text-[10px] font-bold uppercase tracking-widest opacity-60 flex justify-center gap-2 items-center text-gray-800 dark:text-gray-200 mb-2">
403
  <Smile size={12} /> {content.resSentiment}
404
  </h3>
405
- <div className="flex-1 flex flex-col items-center justify-center gap-2">
406
- <div className="text-2xl font-bold capitalize text-green-600 dark:text-green-400 truncate px-2 text-center">
407
- {result.emotion
408
- ? result.emotion[lang] ||
409
- result.emotion.id ||
410
- result.emotion
411
- : result.sentiment}
412
- </div>
413
- {/* Reasoning Emotion */}
414
- {result.reasoning && (
415
- <p className="text-xs text-center text-gray-600 dark:text-gray-300 leading-relaxed px-2 font-medium">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
416
  {result.reasoning.emotion?.[lang] ||
417
  "Analisis sentimen teks."}
418
  </p>
419
- )}
420
- </div>
421
  </div>
422
 
423
  {/* KEYWORDS */}
424
  <div className="liquid-glass p-5 border-t-4 border-blue-500 bg-white/60 dark:bg-black/40 backdrop-blur-md rounded-xl h-full flex flex-col group hover:bg-white/80 dark:hover:bg-white/5 transition-all">
425
- <h3 className="text-[10px] font-bold uppercase tracking-widest opacity-60 flex justify-center gap-2 items-center text-gray-800 dark:text-gray-200 mb-3">
426
  <Tag size={12} /> {content.resKeywords}
427
  </h3>
428
  <div className="flex-1 flex flex-col items-center justify-center gap-3">
@@ -438,14 +461,17 @@ export default function AnalysisPage() {
438
  </span>
439
  ))}
440
  </div>
441
- {/* Reasoning Keywords */}
442
- {result.reasoning && (
443
- <p className="text-[10px] text-center text-gray-500 dark:text-gray-400 mt-1 italic">
 
 
 
444
  {result.reasoning.keywords?.[lang] ||
445
  "Kata kunci dominan."}
446
  </p>
447
- )}
448
- </div>
449
  </div>
450
  </div>
451
 
 
378
  className="w-full max-w-3xl mx-auto mt-6 space-y-4 text-left"
379
  >
380
  <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
381
+ {/* MBTI */}
382
  {/* MBTI */}
383
  <div className="liquid-glass p-5 border-t-4 border-orange-500 bg-white/60 dark:bg-black/40 backdrop-blur-md rounded-xl flex flex-col h-full group hover:bg-white/80 dark:hover:bg-white/5 transition-all">
384
+ <h3 className="text-[10px] font-bold uppercase tracking-widest opacity-60 flex justify-center gap-2 items-center text-gray-800 dark:text-gray-200 mb-0 border-b border-gray-100 dark:border-white/5 pb-3">
385
  <BrainCircuit size={12} /> {content.resMBTI}
386
  </h3>
387
+ <div className="flex-1 flex flex-col items-center justify-center gap-3">
388
  <div className="text-4xl font-black text-orange-600 tracking-tight">
389
  {result.mbti_type}
390
  </div>
391
+ </div>
392
+
393
+ {/* Footer Reasoning */}
394
+ {result.reasoning && (
395
+ <div className="mt-auto pt-3 border-t border-orange-100 dark:border-white/5 w-full">
396
+ <p className="text-[10px] text-center text-gray-500 dark:text-gray-400 leading-relaxed px-2 font-medium italic">
397
  {result.reasoning.mbti?.[lang] ||
398
  "Analisis kepribadian mendalam."}
399
  </p>
400
+ </div>
401
+ )}
402
  </div>
403
 
404
  {/* EMOTION */}
405
  <div className="liquid-glass p-5 border-t-4 border-green-500 bg-white/60 dark:bg-black/40 backdrop-blur-md rounded-xl flex flex-col h-full group hover:bg-white/80 dark:hover:bg-white/5 transition-all">
406
+ <h3 className="text-[10px] font-bold uppercase tracking-widest opacity-60 flex justify-center gap-2 items-center text-gray-800 dark:text-gray-200 mb-0 border-b border-gray-100 dark:border-white/5 pb-3">
407
  <Smile size={12} /> {content.resSentiment}
408
  </h3>
409
+
410
+ <div className="flex-1 flex flex-col items-center justify-center gap-3 w-full">
411
+ {/* Unified Top 3 List (Badge Style like Keywords) */}
412
+ {result.emotion?.list ? (
413
+ <div className="flex flex-wrap gap-2 justify-center items-center w-full">
414
+ {result.emotion.list.map((item: any, idx: number) => (
415
+ <span
416
+ key={idx}
417
+ className="bg-green-100 dark:bg-green-900/30 px-3 py-1.5 rounded-full text-xs font-bold text-green-700 dark:text-green-300 border border-green-200 dark:border-green-800/50 capitalize shadow-sm"
418
+ >
419
+ {lang === "id" ? item.id : item.en}{" "}
420
+ {Math.round(item.score * 100)}%
421
+ </span>
422
+ ))}
423
+ </div>
424
+ ) : (
425
+ <div className="text-2xl font-bold capitalize text-green-600 dark:text-green-400 truncate px-2 text-center">
426
+ {result.emotion
427
+ ? result.emotion[lang] ||
428
+ result.emotion.id ||
429
+ result.emotion
430
+ : result.sentiment}
431
+ </div>
432
+ )}
433
+ </div>
434
+
435
+ {/* Footer Reasoning */}
436
+ {result.reasoning && (
437
+ <div className="mt-auto pt-3 border-t border-green-100 dark:border-green-800/20 w-full">
438
+ <p className="text-[10px] text-center text-gray-500 dark:text-gray-400 italic leading-relaxed px-2 font-medium">
439
  {result.reasoning.emotion?.[lang] ||
440
  "Analisis sentimen teks."}
441
  </p>
442
+ </div>
443
+ )}
444
  </div>
445
 
446
  {/* KEYWORDS */}
447
  <div className="liquid-glass p-5 border-t-4 border-blue-500 bg-white/60 dark:bg-black/40 backdrop-blur-md rounded-xl h-full flex flex-col group hover:bg-white/80 dark:hover:bg-white/5 transition-all">
448
+ <h3 className="text-[10px] font-bold uppercase tracking-widest opacity-60 flex justify-center gap-2 items-center text-gray-800 dark:text-gray-200 mb-0 border-b border-gray-100 dark:border-white/5 pb-3">
449
  <Tag size={12} /> {content.resKeywords}
450
  </h3>
451
  <div className="flex-1 flex flex-col items-center justify-center gap-3">
 
461
  </span>
462
  ))}
463
  </div>
464
+ </div>
465
+
466
+ {/* Footer Reasoning */}
467
+ {result.reasoning && (
468
+ <div className="mt-auto pt-3 border-t border-blue-100 dark:border-white/5 w-full">
469
+ <p className="text-[10px] text-center text-gray-500 dark:text-gray-400 italic font-medium">
470
  {result.reasoning.keywords?.[lang] ||
471
  "Kata kunci dominan."}
472
  </p>
473
+ </div>
474
+ )}
475
  </div>
476
  </div>
477