HakimiMasstar commited on
Commit
0c3803c
·
1 Parent(s): 2d575f7
Files changed (7) hide show
  1. DEPLOYMENT_GUIDE.md +0 -87
  2. DEPLOYMENT_GUIDE_HF.md +62 -0
  3. Dockerfile +32 -0
  4. FUTURE_WORK.md +28 -0
  5. README.md +9 -0
  6. app.py +46 -15
  7. templates/index.html +643 -76
DEPLOYMENT_GUIDE.md DELETED
@@ -1,87 +0,0 @@
1
- # Deploying Flask App to Hugging Face Spaces
2
-
3
- Since you moved from Gradio to Flask, you must use the **Docker SDK** on Hugging Face. This guide explains how to configure your repository for successful deployment.
4
-
5
- ---
6
-
7
- ## 1. Project Configuration Changes
8
-
9
- ### **A. Update the Port**
10
- Hugging Face Spaces strictly monitors port **7860**. You must update your `web_app/app.py` to listen on this port instead of the default 5000.
11
-
12
- **File:** `web_app/app.py`
13
- ```python
14
- if __name__ == '__main__':
15
- # Change port from 5000 to 7860
16
- app.run(host='0.0.0.0', port=7860)
17
- ```
18
-
19
- ### **B. Create a `Dockerfile`**
20
- Create a file named `Dockerfile` (no extension) in the **root** folder of your project. This file tells Hugging Face how to build your environment.
21
-
22
- **File:** `Dockerfile`
23
- ```dockerfile
24
- # Use official Python image
25
- FROM python:3.10-slim
26
-
27
- # Set working directory
28
- WORKDIR /app
29
-
30
- # Install system dependencies (needed for Malaya/Matplotlib)
31
- RUN apt-get update && apt-get install -y \
32
- build-essential \
33
- && rm -rf /var/lib/apt/lists/*
34
-
35
- # Copy requirements and install
36
- COPY requirements.txt .
37
- RUN pip install --no-cache-dir -r requirements.txt
38
- RUN pip install flask # Ensure Flask is installed
39
-
40
- # Copy the entire project
41
- COPY . .
42
-
43
- # Expose the HF default port
44
- EXPOSE 7860
45
-
46
- # Run the Flask app
47
- CMD ["python", "web_app/app.py"]
48
- ```
49
-
50
- ---
51
-
52
- ## 2. Hugging Face Space Metadata
53
-
54
- You must update the header of your `README.md` file so Hugging Face knows to use Docker.
55
-
56
- **File:** `README.md`
57
- ```yaml
58
- ---
59
- title: HalalNLP
60
- emoji: 🌖
61
- colorFrom: green
62
- colorTo: purple
63
- sdk: docker
64
- pinned: false
65
- ---
66
- ```
67
-
68
- ---
69
-
70
- ## 3. Deployment Steps
71
-
72
- 1. **Commit your changes:** Ensure all new files (`web_app/`, `Dockerfile`) are added to your Git repo.
73
- 2. **Set Secrets:** In your Hugging Face Space settings, add your `HUGGINGFACE_HUB_TOKEN` under **Variables and Secrets**.
74
- 3. **Push to Hugging Face:**
75
- ```bash
76
- git add .
77
- git commit -m "Migrate to Flask with Docker"
78
- git push origin main
79
- ```
80
-
81
- ## 4. Local Testing with Docker (Optional)
82
- If you have Docker installed locally, you can test the build before pushing:
83
- ```bash
84
- docker build -t halalnlp-flask .
85
- docker run -p 7860:7860 halalnlp-flask
86
- ```
87
- Then visit `http://localhost:7860`.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
DEPLOYMENT_GUIDE_HF.md ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Deploying to Hugging Face Spaces
2
+
3
+ This guide will help you deploy the **HalalNLP Dashboard** to a Hugging Face Space.
4
+
5
+ ## Prerequisites
6
+ * A Hugging Face account.
7
+ * The project files prepared (Dockerfile is already created).
8
+
9
+ ## Step 1: Create a New Space
10
+ 1. Go to [huggingface.co/spaces](https://huggingface.co/spaces).
11
+ 2. Click **"Create new Space"**.
12
+ 3. **Space name:** Enter a name (e.g., `halal-food-insight`).
13
+ 4. **License:** Choose a license (e.g., Apache 2.0).
14
+ 5. **SDK:** Select **Docker**.
15
+ 6. **Space Hardware:** "CPU Basic (Free)" is usually sufficient, but upgrade if you run out of memory.
16
+ 7. Click **"Create Space"**.
17
+
18
+ ## Step 2: Upload Files
19
+ You can upload files via the Web UI or Git.
20
+
21
+ ### Option A: Using the Web UI (Easiest)
22
+ 1. In your new Space, go to the **Files** tab.
23
+ 2. Click **"Add file"** > **"Upload files"**.
24
+ 3. Drag and drop **ALL** files and folders from your local project directory:
25
+ * `app.py`
26
+ * `Dockerfile`
27
+ * `requirements.txt`
28
+ * `halalnlp/` (folder)
29
+ * `static/` (folder)
30
+ * `templates/` (folder)
31
+ * (Do NOT upload `venv`, `.git`, or `__pycache__`)
32
+ 4. Commit the changes.
33
+
34
+ ### Option B: Using Git (Recommended)
35
+ 1. Clone your Space locally:
36
+ ```bash
37
+ git clone https://huggingface.co/spaces/YOUR_USERNAME/YOUR_SPACE_NAME
38
+ ```
39
+ 2. Copy your project files into that folder.
40
+ 3. Add, commit, and push:
41
+ ```bash
42
+ git add .
43
+ git commit -m "Initial commit"
44
+ git push
45
+ ```
46
+
47
+ ## Step 3: Configure Secrets (Optional)
48
+ If your app uses a private Hugging Face token (e.g., to access gated models):
49
+ 1. Go to **Settings** in your Space.
50
+ 2. Scroll to **"Variables and secrets"**.
51
+ 3. Add a New Secret:
52
+ * Name: `HUGGINGFACE_HUB_TOKEN`
53
+ * Value: Your Access Token (from your HF Settings).
54
+
55
+ ## Step 4: Watch it Build
56
+ 1. Go to the **App** tab.
57
+ 2. You will see "Building...". This may take a few minutes as it installs PyTorch and other libraries.
58
+ 3. Once "Running", your dashboard is live!
59
+
60
+ ## Troubleshooting
61
+ * **Build Error:** Check the "Logs" tab. Common issues are missing requirements or memory limits.
62
+ * **Timeout:** If the app takes too long to load the models, try increasing the Gunicorn timeout in `Dockerfile` (currently set to 120s).
Dockerfile ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use the official Python 3.9 image
2
+ FROM python:3.9
3
+
4
+ # Set up a new user named "user" with user ID 1000
5
+ RUN useradd -m -u 1000 user
6
+
7
+ # Switch to the "user" user
8
+ USER user
9
+
10
+ # Set home to the user's home directory
11
+ ENV HOME=/home/user \
12
+ PATH=/home/user/.local/bin:$PATH
13
+
14
+ # Set the working directory to the user's home directory
15
+ WORKDIR $HOME/app
16
+
17
+ # Copy the current directory contents into the container at $HOME/app setting the owner to the user
18
+ COPY --chown=user . $HOME/app
19
+
20
+ # Install any needed packages specified in requirements.txt
21
+ RUN pip install --no-cache-dir --upgrade pip
22
+ RUN pip install --no-cache-dir -r requirements.txt
23
+ RUN pip install gunicorn
24
+
25
+ # Expose port 7860 for Hugging Face Spaces
26
+ EXPOSE 7860
27
+
28
+ # Define environment variable
29
+ ENV FLASK_APP=app.py
30
+
31
+ # Run app.py using Gunicorn
32
+ CMD ["gunicorn", "-b", "0.0.0.0:7860", "app:app", "--timeout", "120"]
FUTURE_WORK.md ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Future Work: Dynamic Popular Keywords
2
+
3
+ ## Objective
4
+ Replace the hardcoded popular search buttons in the "Keyword Analyzer Dashboard" with a dynamic list derived from the actual dataset or real-time trends.
5
+
6
+ ## Proposed Implementation: Dataset-Driven Approach (Recommended)
7
+ Instead of relying on external APIs (Google Trends) which may return keywords not present in our local dataset, we should extract the most frequent meaningful terms directly from our loaded data.
8
+
9
+ ### Steps:
10
+ 1. **Backend (`app.py`):**
11
+ * Create a helper function to process `global_df['text']`.
12
+ * Tokenize the text and remove standard stopwords (English + Malay).
13
+ * Calculate frequency distribution (`Counter`) of the remaining words.
14
+ * Extract the top 10 most common nouns/adjectives.
15
+ * Expose a new API endpoint (e.g., `/api/popular_keywords`) that returns this list.
16
+
17
+ 2. **Frontend (`index.html`):**
18
+ * On page load (or when the Dashboard tab is clicked), call `/api/popular_keywords`.
19
+ * Dynamically render the 10 buttons using the received list instead of the hardcoded HTML.
20
+
21
+ ### Benefits:
22
+ * **Guaranteed Hits:** Every keyword suggested is guaranteed to exist in the dataset, avoiding "No results found" errors.
23
+ * **Contextual Relevance:** Reflects what people are *actually* discussing in the provided data.
24
+ * **Performance:** Faster than external API calls; can be cached on server startup.
25
+
26
+ ## Alternative: Google Trends API
27
+ * Use `pytrends` to fetch real-time search data for "Halal Food Malaysia".
28
+ * *Risk:* High chance of rate limiting and keywords having zero overlap with the local static dataset.
README.md ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: flask-halalnlp
3
+ emoji: 🕌
4
+ colorFrom: green
5
+ colorTo: orange
6
+ sdk: docker
7
+ pinned: false
8
+ short_description: Sentiment Analysis & TPB Factor Assessment for Halal Food
9
+ ---
app.py CHANGED
@@ -76,16 +76,22 @@ def generate_analytics_package(filtered_df):
76
  return None
77
 
78
  # 1. Charts
79
- # Overall Sentiment (Exclude Neutral for specific chart as requested)
80
- sentiment_order_bin = ['negative', 'positive']
81
- sent_df_bin = filtered_df[filtered_df['label'] != 'neutral']
82
  if not sent_df_bin.empty:
83
  sent_counts = sent_df_bin['label'].value_counts(normalize=True) * 100
84
  sent_counts = sent_counts.reindex(sentiment_order_bin).fillna(0)
85
- fig_sent = go.Figure(data=[go.Bar(x=sent_counts.index.tolist(), y=sent_counts.values.tolist(), marker_color=['red', 'blue'], text=[f'{v:.1f}%' for v in sent_counts.values], textposition='auto')])
 
 
 
 
 
 
86
  else:
87
  fig_sent = go.Figure()
88
- fig_sent.update_layout(title='Overall Sentiment Distribution (Pos vs Neg)', xaxis_title='Sentiment', yaxis_title='Percentage', margin=dict(t=40), autosize=True)
89
 
90
  # 2. TPB Distribution (Keep original)
91
  tpb_counts = filtered_df['tpb_label'].value_counts(normalize=True) * 100
@@ -106,7 +112,7 @@ def generate_analytics_package(filtered_df):
106
  tpb_sent_pct = tpb_sent_df.div(tpb_sent_df.sum(axis=1), axis=0) * 100
107
 
108
  fig_stack = go.Figure()
109
- color_map = {'negative': 'red', 'neutral': 'gray', 'positive': 'blue'}
110
  for col in sentiment_order:
111
  fig_stack.add_trace(go.Bar(
112
  name=col, x=tpb_sent_pct.index.tolist(), y=tpb_sent_pct[col].tolist(),
@@ -116,17 +122,42 @@ def generate_analytics_package(filtered_df):
116
  fig_stack.update_layout(barmode='stack', title='Sentiment Distribution within TPB Factors', xaxis_title='TPB Factor', yaxis_title='Percentage', margin=dict(t=40), autosize=True)
117
 
118
  # 2. Word Clouds & Bigrams
119
- wcs = {}
 
 
 
120
  bg_data = {}
 
 
 
 
 
 
 
 
121
  factors = ['attitude', 'religious knowledge', 'subjective norms', 'perceived behavioural control']
122
  for factor in factors:
123
  subset = filtered_df[filtered_df['tpb_label'] == factor]
124
  if not subset.empty:
125
- text = ' '.join(subset['text']).lower().replace('quote','').replace('sijil halal','').replace('halal','')
126
- wcs[factor] = generate_wordcloud_base64(text)
127
- bg_data[factor] = generate_bigrams(text)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  else:
129
- wcs[factor] = None
130
  bg_data[factor] = []
131
 
132
  # 3. Report
@@ -162,16 +193,16 @@ def generate_bigrams(text):
162
  return [[ " ".join(bg), count] for bg, count in Counter(bi_grams).most_common(10)]
163
 
164
  def predict_decision(sentiment_label):
165
- if sentiment_label == 'positive': return "High likelihood of purchase"
166
- elif sentiment_label == 'neutral': return "Moderate likelihood of purchase"
167
- else: return "Low likelihood of purchase"
168
 
169
  def generate_report_content(tpb_sentiment_df):
170
  report = []
171
  for _, row in tpb_sentiment_df.iterrows():
172
  tpb_label = row['tpb_label']
173
  negative_percentage = row.get('negative', 0)
174
- if negative_percentage > 70:
175
  item = {"factor": tpb_label.capitalize(), "negative_pct": negative_percentage, "content": ""}
176
  if tpb_label == "attitude":
177
  item["content"] = """
 
76
  return None
77
 
78
  # 1. Charts
79
+ # Overall Sentiment (Now includes Neutral)
80
+ sentiment_order_bin = ['negative', 'neutral', 'positive']
81
+ sent_df_bin = filtered_df
82
  if not sent_df_bin.empty:
83
  sent_counts = sent_df_bin['label'].value_counts(normalize=True) * 100
84
  sent_counts = sent_counts.reindex(sentiment_order_bin).fillna(0)
85
+ fig_sent = go.Figure(data=[go.Bar(
86
+ x=[s.capitalize() for s in sent_counts.index.tolist()],
87
+ y=sent_counts.values.tolist(),
88
+ marker_color=['red', 'gray', 'green'],
89
+ text=[f'{v:.1f}%' for v in sent_counts.values],
90
+ textposition='auto'
91
+ )])
92
  else:
93
  fig_sent = go.Figure()
94
+ fig_sent.update_layout(title='Overall Sentiment Distribution', xaxis_title='Sentiment', yaxis_title='Percentage', margin=dict(t=40), autosize=True)
95
 
96
  # 2. TPB Distribution (Keep original)
97
  tpb_counts = filtered_df['tpb_label'].value_counts(normalize=True) * 100
 
112
  tpb_sent_pct = tpb_sent_df.div(tpb_sent_df.sum(axis=1), axis=0) * 100
113
 
114
  fig_stack = go.Figure()
115
+ color_map = {'negative': 'red', 'neutral': 'gray', 'positive': 'green'}
116
  for col in sentiment_order:
117
  fig_stack.add_trace(go.Bar(
118
  name=col, x=tpb_sent_pct.index.tolist(), y=tpb_sent_pct[col].tolist(),
 
122
  fig_stack.update_layout(barmode='stack', title='Sentiment Distribution within TPB Factors', xaxis_title='TPB Factor', yaxis_title='Percentage', margin=dict(t=40), autosize=True)
123
 
124
  # 2. Word Clouds & Bigrams
125
+ wcs = {
126
+ "global": {},
127
+ "tpb": {}
128
+ }
129
  bg_data = {}
130
+
131
+ # Global Clouds (Positive vs Negative)
132
+ for sentiment in ['positive', 'negative']:
133
+ subset = filtered_df[filtered_df['label'] == sentiment]
134
+ text = ' '.join(subset['text']).lower().replace('quote','').replace('sijil halal','').replace('halal','') if not subset.empty else ''
135
+ wcs["global"][sentiment] = generate_wordcloud_base64(text)
136
+
137
+ # TPB Factor Clouds (All, Pos, Neg)
138
  factors = ['attitude', 'religious knowledge', 'subjective norms', 'perceived behavioural control']
139
  for factor in factors:
140
  subset = filtered_df[filtered_df['tpb_label'] == factor]
141
  if not subset.empty:
142
+ # All
143
+ text_all = ' '.join(subset['text']).lower().replace('quote','').replace('sijil halal','').replace('halal','')
144
+
145
+ # Positive
146
+ sub_pos = subset[subset['label'] == 'positive']
147
+ text_pos = ' '.join(sub_pos['text']).lower().replace('quote','').replace('sijil halal','').replace('halal','') if not sub_pos.empty else ''
148
+
149
+ # Negative
150
+ sub_neg = subset[subset['label'] == 'negative']
151
+ text_neg = ' '.join(sub_neg['text']).lower().replace('quote','').replace('sijil halal','').replace('halal','') if not sub_neg.empty else ''
152
+
153
+ wcs["tpb"][factor] = {
154
+ "all": generate_wordcloud_base64(text_all),
155
+ "positive": generate_wordcloud_base64(text_pos),
156
+ "negative": generate_wordcloud_base64(text_neg)
157
+ }
158
+ bg_data[factor] = generate_bigrams(text_all)
159
  else:
160
+ wcs["tpb"][factor] = {"all": None, "positive": None, "negative": None}
161
  bg_data[factor] = []
162
 
163
  # 3. Report
 
193
  return [[ " ".join(bg), count] for bg, count in Counter(bi_grams).most_common(10)]
194
 
195
  def predict_decision(sentiment_label):
196
+ if sentiment_label == 'positive': return "High likelihood to purchase"
197
+ elif sentiment_label == 'neutral': return "Moderate likelihood to purchase"
198
+ else: return "Low likelihood to purchase"
199
 
200
  def generate_report_content(tpb_sentiment_df):
201
  report = []
202
  for _, row in tpb_sentiment_df.iterrows():
203
  tpb_label = row['tpb_label']
204
  negative_percentage = row.get('negative', 0)
205
+ if negative_percentage > 75:
206
  item = {"factor": tpb_label.capitalize(), "negative_pct": negative_percentage, "content": ""}
207
  if tpb_label == "attitude":
208
  item["content"] = """
templates/index.html CHANGED
@@ -15,20 +15,28 @@
15
  body { font-family: 'Inter', sans-serif; background-color: #F9FAFB; }
16
  .navbar { background-color: #ffffff; box-shadow: 0 1px 3px rgba(0,0,0,0.05); }
17
  .card { border: 1px solid #E5E7EB; border-radius: 12px; box-shadow: 0 2px 4px rgba(0,0,0,0.02); background: white; margin-bottom: 25px; }
18
- .btn-primary { background-color: #10B981; border: none; font-weight: 600; padding: 10px 20px; }
19
- .btn-primary:hover { background-color: #059669; }
 
 
 
20
  .score-card { text-align: center; padding: 24px; border-radius: 12px; color: white; }
21
  .bg-tpb { background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%); }
22
  .bg-sent { background: linear-gradient(135deg, #F59E0B 0%, #D97706 100%); }
23
  .bg-dec { background: linear-gradient(135deg, #10B981 0%, #059669 100%); }
24
  .spinner-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(255,255,255,0.8); z-index: 9999; justify-content: center; align-items: center; }
25
- .nav-pills .nav-link.active { background-color: #10B981; }
 
 
26
  .nav-pills .nav-link { color: #374151; font-weight: 600; }
 
27
  .chart-container { height: 400px; width: 100%; }
28
  .clickable-row { cursor: pointer; }
29
  .clickable-row:hover { background-color: #f3f4f6 !important; }
 
 
30
  .drop-zone {
31
- border: 2px dashed #10B981;
32
  border-radius: 12px;
33
  padding: 40px;
34
  background-color: #fff;
@@ -36,10 +44,24 @@
36
  cursor: pointer;
37
  }
38
  .drop-zone.drag-over {
39
- background-color: #ecfdf5;
40
- border-color: #059669;
41
  transform: scale(1.02);
42
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  </style>
44
  </head>
45
  <body>
@@ -52,39 +74,219 @@
52
  <div class="container">
53
  <a class="navbar-brand fw-bold text-success" href="#">🕌 HalalNLP</a>
54
  <ul class="nav nav-pills ms-auto">
55
- <li class="nav-item"><button class="nav-link active" onclick="showTab('analyzer')">Single Analyzer</button></li>
 
56
  <li class="nav-item"><button class="nav-link" onclick="showTab('bulk')">Bulk Analyzer</button></li>
57
- <li class="nav-item"><button class="nav-link" onclick="showTab('dashboard')">Company Dashboard</button></li>
58
  </ul>
59
  </div>
60
  </nav>
61
 
62
  <div class="container" style="margin-top: 100px; padding-bottom: 80px;">
63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  <!-- View 1: Single Analyzer -->
65
- <div id="analyzer-view">
66
  <div class="row justify-content-center">
67
  <div class="col-lg-8">
68
  <div class="text-center mb-5">
69
- <h1 class="fw-bold mb-2 h2">Text Classification and Sentiment Analysis</h1>
70
- <p class="text-muted">Instant AI-driven behavioral and sentiment feedback.</p>
 
71
  </div>
72
- <div class="card p-4">
73
- <textarea id="inputText" class="form-control mb-3" rows="4" placeholder="Model can make mistakes, we are striving to improve the model."></textarea>
74
- <button onclick="analyzeText()" class="btn btn-primary w-100">Analyze</button>
75
- <div class="mt-3">
76
- <small class="text-muted d-block mb-2">Examples (click to use):</small>
 
 
 
 
77
  <div class="d-flex flex-column gap-2">
78
  <button class="btn btn-sm btn-outline-secondary text-start overflow-hidden text-truncate" onclick="useExample(0)">Alhamdulillah, hari ni dapat makan dekat restoran halal baru. Rasa puas hati dan tenang bila tau makanan yang kita makan dijamin halal.</button>
79
  <button class="btn btn-sm btn-outline-secondary text-start overflow-hidden text-truncate" onclick="useExample(1)">Semua orang cakap kena check logo halal sebelum beli makanan. Dah jadi macam second nature dah sekarang. Korang pun sama kan?</button>
80
  </div>
81
  </div>
 
 
 
 
82
  </div>
83
  <div id="resultsArea" class="mt-4" style="display: none;">
84
- <div class="row g-3">
85
- <div class="col-md-4"><div class="score-card bg-tpb"><small>TPB Label</small><h4 id="resTpb" class="mt-2">--</h4></div></div>
86
- <div class="col-md-4"><div class="score-card bg-sent"><small>Sentiment</small><h4 id="resSent" class="mt-2">--</h4><small id="resProb">0%</small></div></div>
87
- <div class="col-md-4"><div class="score-card bg-dec"><small>Decision</small><h4 id="resDec" class="mt-2">--</h4></div></div>
88
  </div>
89
  </div>
90
  </div>
@@ -122,37 +324,92 @@
122
  <div id="bulkResults" style="display: none;">
123
  <div id="bulkAnalyticsArea"></div> <!-- Dynamic Charts Container -->
124
 
125
- <h4 class="fw-bold mt-5 mb-3">All Analyzed Inputs</h4>
126
- <p class="text-muted small">Click any row to view full details.</p>
127
- <div class="card overflow-hidden">
128
- <div class="table-responsive" style="max-height: 500px;">
129
- <table class="table table-hover mb-0">
130
- <thead class="table-light sticky-top">
131
- <tr><th>Input Text</th><th>Sentiment</th><th>TPB Label</th></tr>
132
- </thead>
133
- <tbody id="bulkTableBody"></tbody>
134
- </table>
135
  </div>
136
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  </div>
138
  </div>
139
 
140
  <!-- View 3: Company Dashboard -->
141
  <div id="dashboard-view" style="display: none;">
142
  <div class="text-center mb-5">
143
- <h1 class="fw-bold h2">Sentiment Analysis and Purchase Decision Factor for Halal Food Acquisition</h1>
 
 
 
 
144
  <div class="row justify-content-center mt-4">
145
- <div class="col-md-6 d-flex gap-2">
146
- <input type="text" id="dashKeyword" class="form-control" placeholder="Search Brand/Keyword...">
147
- <button onclick="updateDashboard()" class="btn btn-primary">Search</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  </div>
149
  </div>
150
  </div>
151
  <div id="dashResults" style="display: none;">
152
  <div id="dashAnalyticsArea"></div>
153
- <div class="card mt-4"><div class="card-header bg-light fw-bold">Data Preview</div><div class="table-responsive"><table class="table table-sm table-striped mb-0"><thead><tr><th>Text</th><th>TPB</th><th>Sent</th><th>Score</th></tr></thead><tbody id="dashPreviewBody"></tbody></table></div></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  </div>
155
- <div id="dashPlaceholder" class="text-center py-5 text-muted"><p>Enter a keyword to generate insights.</p></div>
156
  </div>
157
  </div>
158
 
@@ -173,12 +430,110 @@
173
  <div class="col-md-6"><div class="card p-2"><div class="chart-tpb chart-container"></div></div></div>
174
  <div class="col-12"><div class="card p-2"><div class="chart-stack chart-container" style="height: 450px;"></div></div></div>
175
  </div>
176
- <h5 class="fw-bold mb-3">Contextual Word Clouds</h5>
177
- <div class="row g-3 mb-4">
178
- <div class="col-lg-3 col-6 wc-card"><div class="card h-100"><div class="card-header bg-white small text-center fw-bold">Attitude</div><div class="card-body p-1 text-center"><img class="img-fluid wc-img-attitude"></div></div></div>
179
- <div class="col-lg-3 col-6 wc-card"><div class="card h-100"><div class="card-header bg-white small text-center fw-bold">Knowledge</div><div class="card-body p-1 text-center"><img class="img-fluid wc-img-knowledge"></div></div></div>
180
- <div class="col-lg-3 col-6 wc-card"><div class="card h-100"><div class="card-header bg-white small text-center fw-bold">Norms</div><div class="card-body p-1 text-center"><img class="img-fluid wc-img-norms"></div></div></div>
181
- <div class="col-lg-3 col-6 wc-card"><div class="card h-100"><div class="card-header bg-white small text-center fw-bold">Control</div><div class="card-body p-1 text-center"><img class="img-fluid wc-img-control"></div></div></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  </div>
183
  <div class="card"><div class="card-header bg-danger text-white fw-bold">Strategic Recommendation Report</div><div class="card-body report-area"></div></div>
184
  <div class="card"><div class="card-header bg-dark text-white fw-bold">Top Bigrams by Factor</div><div class="card-body table-responsive"><table class="table table-bordered text-center"><thead><tr><th>Rank</th><th>Attitude</th><th>Knowledge</th><th>Norms</th><th>Control</th></tr></thead><tbody class="bigrams-body"></tbody></table></div></div>
@@ -188,6 +543,95 @@
188
  <script>
189
  const modal = new bootstrap.Modal(document.getElementById('detailModal'));
190
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
  function initDropZone() {
192
  const dz = document.getElementById('dropZone');
193
  ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(evt => {
@@ -230,7 +674,7 @@
230
  }
231
 
232
  function showTab(tab) {
233
- ['analyzer-view', 'bulk-view', 'dashboard-view'].forEach(v => document.getElementById(v).style.display = 'none');
234
  document.getElementById(tab + '-view').style.display = 'block';
235
  document.querySelectorAll('.nav-link').forEach(btn => btn.classList.toggle('active', btn.onclick.toString().includes(tab)));
236
  }
@@ -250,10 +694,46 @@
250
  body: JSON.stringify({text})
251
  });
252
  const data = await res.json();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
  document.getElementById('resTpb').innerText = data.tpb_label;
254
- document.getElementById('resSent').innerText = data.sentiment_label;
255
- document.getElementById('resProb').innerText = (data.sentiment_score * 100).toFixed(2) + "%";
256
- document.getElementById('resDec').innerText = data.decision;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
257
  document.getElementById('resultsArea').style.display = 'block';
258
  } catch(e) { console.error(e); }
259
  showLoader(false);
@@ -270,23 +750,21 @@
270
  const data = await res.json();
271
  renderAnalytics(data, 'bulkAnalyticsArea');
272
 
273
- const tbody = document.getElementById('bulkTableBody');
274
- tbody.innerHTML = "";
275
- data.full_results.forEach((row, idx) => {
276
- const tr = document.createElement('tr');
277
- tr.className = "clickable-row";
278
- tr.innerHTML = `
279
- <td class="text-truncate" style="max-width:400px">${row.text}</td>
280
- <td><span class="badge ${row.label === 'positive' ? 'bg-success' : row.label === 'negative' ? 'bg-danger' : 'bg-secondary'}">${row.label}</span></td>
281
- <td>${row.tpb_label}</td>`;
282
- tr.onclick = () => showRowDetail(row);
283
- tbody.appendChild(tr);
284
- });
285
  document.getElementById('bulkResults').style.display = 'block';
286
  } catch(e) { console.error(e); alert("Error processing bulk data"); }
287
  showLoader(false);
288
  }
289
 
 
 
 
 
 
290
  async function updateDashboard() {
291
  const keyword = document.getElementById('dashKeyword').value;
292
  if(!keyword) return;
@@ -305,17 +783,22 @@
305
  }
306
 
307
  renderAnalytics(data, 'dashAnalyticsArea');
 
308
  const tbody = document.getElementById('dashPreviewBody');
309
  tbody.innerHTML = data.preview.map(r => `
310
- <tr>
311
- <td class="text-truncate" style="max-width:300px">${r.text}</td>
312
- <td>${r.tpb_label}</td>
313
- <td>${r.label}</td>
314
- <td>${r.score.toFixed(4)}</td>
315
  </tr>`).join('');
316
 
 
 
 
317
  document.getElementById('dashResults').style.display = 'block';
318
- document.getElementById('dashPlaceholder').style.display = 'none';
 
319
  } catch(e) { console.error(e); alert("Error loading dashboard"); }
320
  showLoader(false);
321
  }
@@ -342,21 +825,96 @@
342
  window.dispatchEvent(new Event('resize'));
343
  }, 100);
344
 
345
- const setWc = (cls, b64) => {
 
 
 
 
346
  const img = target.querySelector('.'+cls);
347
- if(b64) img.src = "data:image/png;base64," + b64;
348
- else img.closest('.wc-card').style.display = 'none';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
349
  };
350
- setWc('wc-img-attitude', data.wordclouds.attitude);
351
- setWc('wc-img-knowledge', data.wordclouds['religious knowledge']);
352
- setWc('wc-img-norms', data.wordclouds['subjective norms']);
353
- setWc('wc-img-control', data.wordclouds['perceived behavioural control']);
354
 
355
  const repArea = target.querySelector('.report-area');
356
  if(data.report.length > 0) {
357
- repArea.innerHTML = "<h2>TPB Factor Analysis and Recommendations Report</h2>" +
358
- data.report.map(item => `<h3>${item.factor} (${item.negative_pct.toFixed(1)}% Negative)</h3>` + marked.parse(item.content)).join('');
359
- } else { repArea.innerHTML = "<p class='text-muted'>No issues detected.</p>"; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
360
 
361
  const bgBody = target.querySelector('.bigrams-body');
362
  bgBody.innerHTML = Array.from({length: 10}).map((_, i) =>
@@ -369,13 +927,22 @@
369
 
370
  function showRowDetail(row) {
371
  const content = document.getElementById('modalContent');
 
 
 
 
 
 
 
 
 
372
  content.innerHTML = `
373
  <p class="mb-3"><strong>Original Input:</strong><br><span class="text-muted">${row.text}</span></p>
374
  <div class="list-group">
375
- <div class="list-group-item d-flex justify-content-between"><span>Sentiment</span><span class="fw-bold">${row.label}</span></div>
376
  <div class="list-group-item d-flex justify-content-between"><span>Model Confidence</span><span class="fw-bold">${(row.score * 100).toFixed(2)}%</span></div>
377
  <div class="list-group-item d-flex justify-content-between"><span>TPB Category</span><span class="fw-bold">${row.tpb_label}</span></div>
378
- <div class="list-group-item d-flex justify-content-between"><span>Recommendation</span><span class="fw-bold text-success">${row.decision}</span></div>
379
  </div>`;
380
  modal.show();
381
  }
 
15
  body { font-family: 'Inter', sans-serif; background-color: #F9FAFB; }
16
  .navbar { background-color: #ffffff; box-shadow: 0 1px 3px rgba(0,0,0,0.05); }
17
  .card { border: 1px solid #E5E7EB; border-radius: 12px; box-shadow: 0 2px 4px rgba(0,0,0,0.02); background: white; margin-bottom: 25px; }
18
+
19
+ /* New Button Color: Bright Orange (Matches Sentiment Card) */
20
+ .btn-primary { background-color: #F97316; border-color: #F97316; font-weight: 600; padding: 10px 20px; }
21
+ .btn-primary:hover { background-color: #ea580c; border-color: #ea580c; }
22
+
23
  .score-card { text-align: center; padding: 24px; border-radius: 12px; color: white; }
24
  .bg-tpb { background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%); }
25
  .bg-sent { background: linear-gradient(135deg, #F59E0B 0%, #D97706 100%); }
26
  .bg-dec { background: linear-gradient(135deg, #10B981 0%, #059669 100%); }
27
  .spinner-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(255,255,255,0.8); z-index: 9999; justify-content: center; align-items: center; }
28
+
29
+ /* New Nav Tab Color: HalalNLP Brand Green */
30
+ .nav-pills .nav-link.active { background-color: #198754; color: white; }
31
  .nav-pills .nav-link { color: #374151; font-weight: 600; }
32
+
33
  .chart-container { height: 400px; width: 100%; }
34
  .clickable-row { cursor: pointer; }
35
  .clickable-row:hover { background-color: #f3f4f6 !important; }
36
+
37
+ /* Updated Drop Zone to match Brand Green */
38
  .drop-zone {
39
+ border: 2px dashed #198754;
40
  border-radius: 12px;
41
  padding: 40px;
42
  background-color: #fff;
 
44
  cursor: pointer;
45
  }
46
  .drop-zone.drag-over {
47
+ background-color: #f0fdf4;
48
+ border-color: #166534;
49
  transform: scale(1.02);
50
  }
51
+
52
+ /* Modern Table Styling */
53
+ .table-modern { border-collapse: separate; border-spacing: 0 8px; }
54
+ .table-modern thead th { border: none; color: #6B7280; font-weight: 700; text-transform: uppercase; font-size: 0.75rem; letter-spacing: 0.05em; padding: 12px 20px; }
55
+ .table-modern tbody tr { background: white; box-shadow: 0 1px 3px rgba(0,0,0,0.05); transition: all 0.2s ease; border-radius: 8px; }
56
+ .table-modern tbody tr:hover { transform: translateY(-2px); box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
57
+ .table-modern tbody td { border: none; padding: 20px; background: white; }
58
+ .table-modern tbody td:first-child { border-top-left-radius: 8px; border-bottom-left-radius: 8px; border-left: 4px solid #E5E7EB; }
59
+ .table-modern tbody td:last-child { border-top-right-radius: 8px; border-bottom-right-radius: 8px; }
60
+
61
+ /* Row Sentiment Accents */
62
+ .row-sent-positive td:first-child { border-left-color: #10B981 !important; }
63
+ .row-sent-negative td:first-child { border-left-color: #EF4444 !important; }
64
+ .row-sent-neutral td:first-child { border-left-color: #9CA3AF !important; }
65
  </style>
66
  </head>
67
  <body>
 
74
  <div class="container">
75
  <a class="navbar-brand fw-bold text-success" href="#">🕌 HalalNLP</a>
76
  <ul class="nav nav-pills ms-auto">
77
+ <li class="nav-item"><button class="nav-link active" onclick="showTab('overview')">Overview</button></li>
78
+ <li class="nav-item"><button class="nav-link" onclick="showTab('analyzer')">Single Analyzer</button></li>
79
  <li class="nav-item"><button class="nav-link" onclick="showTab('bulk')">Bulk Analyzer</button></li>
80
+ <li class="nav-item"><button class="nav-link" onclick="showTab('dashboard')">Keyword Analyzer Dashboard</button></li>
81
  </ul>
82
  </div>
83
  </nav>
84
 
85
  <div class="container" style="margin-top: 100px; padding-bottom: 80px;">
86
 
87
+ <!-- View 0: Overview -->
88
+ <div id="overview-view">
89
+ <div class="text-center mb-5">
90
+ <h1 class="fw-bold display-5 mb-2">Halal Food Insight Dashboard</h1>
91
+ <h4 class="text-success mb-3">Sentiment Analysis & Theory of Planned Behavior (TPB) Factor Assessment</h4>
92
+ <div class="px-5">
93
+ <p class="text-muted small border-top border-bottom py-2 d-inline-block">
94
+ RESEARCH PROJECT: “HALAL FOOD PURCHASE DECISION PREDICTION FRAMEWORK FOR SOCIAL MEDIA POSTING USING MACHINE LEARNING”
95
+ </p>
96
+ </div>
97
+ </div>
98
+
99
+ <div class="row g-4">
100
+ <!-- What this dashboard does -->
101
+ <div class="col-12">
102
+ <div class="card border-0 shadow-sm">
103
+ <div class="card-body p-4">
104
+ <h3 class="fw-bold mb-3 text-success">What this dashboard does</h3>
105
+ <p class="lead-sm text-secondary">
106
+ The Halal Food Insight Dashboard uses AI (Natural Language Processing) to analyze public discussions about halal food across social media. It reveals overall sentiment and the TPB factors—attitudes, social influence, perceived control, and religious knowledge—that shape purchasing decisions. These insights help businesses, policymakers, and certification bodies respond strategically, address concerns, and strengthen trust in the halal ecosystem. You can explore trends, track sentiment shifts, and generate culturally and ethically aware recommendations.
107
+ </p>
108
+ <div class="alert alert-warning d-flex align-items-center mt-3 mb-0" role="alert">
109
+ <div>
110
+ <h6 class="alert-heading fw-bold mb-1">⚠️ Note on model status</h6>
111
+ <p class="mb-0 small">The model is still being trained. Outputs are indicative and may contain errors. Please review results before drawing conclusions.</p>
112
+ </div>
113
+ </div>
114
+ </div>
115
+ </div>
116
+ </div>
117
+
118
+ <!-- How it works -->
119
+ <div class="col-lg-8">
120
+ <div class="card border-0 shadow-sm h-100">
121
+ <div class="card-body p-4">
122
+ <h4 class="fw-bold mb-4">How it works (at a glance)</h4>
123
+
124
+ <div class="mb-4">
125
+ <h6 class="fw-bold text-success">1) Text Classification (Aspect-Based Extraction)</h6>
126
+ <p class="small text-muted mb-2">The system first categorizes each post to enable deeper analysis. It detects:</p>
127
+ <ul class="small text-muted">
128
+ <li>Opinions or experiences related to halal food</li>
129
+ <li>Religious views or knowledge</li>
130
+ <li>Questions, comparisons, or general feedback</li>
131
+ <li>Salient keywords</li>
132
+ </ul>
133
+ </div>
134
+
135
+ <div class="mb-4">
136
+ <h6 class="fw-bold text-success">2) Sentiment Analysis</h6>
137
+ <p class="small text-muted mb-2">Each post is scored across multiple dimensions (trust, price, brand, compliance):</p>
138
+ <div class="row g-2 mb-2">
139
+ <div class="col-md-4"><div class="p-2 border rounded small bg-light text-success"><strong>Positive:</strong> Confidence, trusted brands, fair pricing.</div></div>
140
+ <div class="col-md-4"><div class="p-2 border rounded small bg-light text-danger"><strong>Negative:</strong> Doubts, high prices, distrust.</div></div>
141
+ <div class="col-md-4"><div class="p-2 border rounded small bg-light text-secondary"><strong>Neutral:</strong> Info seeking, general queries.</div></div>
142
+ </div>
143
+ </div>
144
+
145
+ <div class="mb-4">
146
+ <h6 class="fw-bold text-success">3) TPB Factor Mapping</h6>
147
+ <p class="small text-muted mb-2">Each post is mapped to relevant TPB constructs using advanced NLP:</p>
148
+ <div class="row g-3">
149
+ <div class="col-md-6">
150
+ <div class="p-3 border rounded h-100">
151
+ <strong class="d-block mb-1">Attitudes (Sikap)</strong>
152
+ <p class="extra-small text-muted mb-0">Evaluative judgments (taste, cleanliness, quality).<br><em>Keywords: sedap, enak, bersih, kualiti, healthy.</em></p>
153
+ </div>
154
+ </div>
155
+ <div class="col-md-6">
156
+ <div class="p-3 border rounded h-100">
157
+ <strong class="d-block mb-1">Subjective Norms</strong>
158
+ <p class="extra-small text-muted mb-0">Social influences (friends, family, viral trends).<br><em>Keywords: keluarga, netizen, viral, trend, "orang kata".</em></p>
159
+ </div>
160
+ </div>
161
+ <div class="col-md-6">
162
+ <div class="p-3 border rounded h-100">
163
+ <strong class="d-block mb-1">Perceived Behavioural Control</strong>
164
+ <p class="extra-small text-muted mb-0">Perceived ease/difficulty and verification actions.<br><em>Keywords: susah, mudah, logo, scan, semak, verify.</em></p>
165
+ </div>
166
+ </div>
167
+ <div class="col-md-6">
168
+ <div class="p-3 border rounded h-100">
169
+ <strong class="d-block mb-1">Religious Knowledge</strong>
170
+ <p class="extra-small text-muted mb-0">Knowledge of halal–haram principles and authorities.<br><em>Keywords: syariah, fatwa, JAKIM, sijil, gelatin.</em></p>
171
+ </div>
172
+ </div>
173
+ </div>
174
+ </div>
175
+
176
+ <div class="mb-0">
177
+ <h6 class="fw-bold text-success">4) Purchase Likelihood Prediction</h6>
178
+ <p class="small text-muted mb-0">Combining sentiment and TPB signals, the system estimates likelihood as: <strong>High, Medium, or Low.</strong></p>
179
+ </div>
180
+ </div>
181
+ </div>
182
+ </div>
183
+
184
+ <!-- Requirements & Details -->
185
+ <div class="col-lg-4">
186
+ <div class="card border-0 shadow-sm h-100 bg-light">
187
+ <div class="card-body p-4">
188
+ <h4 class="fw-bold mb-4">Dashboard Details</h4>
189
+
190
+ <h6 class="fw-bold small mb-2">TARGET USERS</h6>
191
+ <ul class="extra-small text-muted mb-4">
192
+ <li class="mb-2"><strong>Businesses/Brands:</strong> Optimize messaging/claims; track campaign impact.</li>
193
+ <li class="mb-2"><strong>Policymakers/Agencies:</strong> Identify gaps; target outreach; evaluate responses.</li>
194
+ <li><strong>Certification Bodies:</strong> Monitor perceptions; spot recurring pain points.</li>
195
+ </ul>
196
+
197
+ <h6 class="fw-bold small mb-2">TECHNICAL SPECS</h6>
198
+ <ul class="extra-small text-muted mb-0">
199
+ <li class="mb-1"><strong>Data Sources:</strong> Social media posts.</li>
200
+ <li class="mb-1"><strong>Languages:</strong> English, Malay, mixed-language (slang/colloquialism).</li>
201
+ <li><strong>Outputs:</strong> Sentiment scores, TPB factors, Purchase Prediction.</li>
202
+ </ul>
203
+ </div>
204
+ </div>
205
+ </div>
206
+
207
+ <!-- TPB Detailed Framework -->
208
+ <div class="col-12">
209
+ <div class="card border-0 shadow-sm">
210
+ <div class="card-header bg-dark text-white fw-bold py-3">Theory of Planned Behavior (TPB) Framework Depth</div>
211
+ <div class="card-body p-0">
212
+ <div class="list-group list-group-flush">
213
+ <div class="list-group-item p-4">
214
+ <h5 class="fw-bold text-success mb-2">Attitudes</h5>
215
+ <p class="small text-muted mb-0">Reflect a positive perception of Halal food based on personal feelings and past experiences. Tied to religious values, health benefits, and food quality. These views shape how people express support or concern online.</p>
216
+ </div>
217
+ <div class="list-group-item p-4">
218
+ <h5 class="fw-bold text-success mb-2">Subjective Norms</h5>
219
+ <p class="small text-muted mb-0">Highlight the role of social influence (community, workplace, peer expectations). Reveals tension when social norms clash with personal beliefs. Reflects <strong>"what people think others expect of them"</strong> and collective concerns influence behavior.</p>
220
+ </div>
221
+ <div class="list-group-item p-4">
222
+ <h5 class="fw-bold text-success mb-2">Perceived Behavioral Control</h5>
223
+ <p class="small text-muted mb-0">Refers to how easy or hard individuals feel it is to follow a Halal diet. Includes personal confidence, motivation to comply despite practical barriers, and how digital tools (apps/reviews) support Halal decision-making.</p>
224
+ </div>
225
+ <div class="list-group-item p-4">
226
+ <h5 class="fw-bold text-success mb-2">Religious Knowledge</h5>
227
+ <p class="small text-muted mb-0">Indicates a deeper understanding of Halal in Islam, including ethical and environmental values. Encourages conscious food choices based on faith and drives more cautious sentiment on social platforms.</p>
228
+ </div>
229
+ </div>
230
+ </div>
231
+ </div>
232
+ </div>
233
+
234
+ <!-- Final Call to Action -->
235
+ <div class="col-12">
236
+ <div class="card text-white bg-success border-0 shadow-sm overflow-hidden">
237
+ <div class="card-body p-5 text-center position-relative">
238
+ <div style="position:relative; z-index:2">
239
+ <h2 class="fw-bold mb-4">🎯 Why Use This Dashboard?</h2>
240
+ <div class="row justify-content-center">
241
+ <div class="col-md-10">
242
+ <div class="row g-3 text-start small">
243
+ <div class="col-md-6">✓ Understand real-time public sentiment on halal food</div>
244
+ <div class="col-md-6">✓ Identify key concerns and influencers</div>
245
+ <div class="col-md-6">✓ Support certification bodies, businesses, and policymakers</div>
246
+ <div class="col-md-6">✓ Promote ethical AI use in culturally sensitive topics</div>
247
+ </div>
248
+ </div>
249
+ </div>
250
+ </div>
251
+ </div>
252
+ </div>
253
+ </div>
254
+ </div>
255
+ </div>
256
+
257
  <!-- View 1: Single Analyzer -->
258
+ <div id="analyzer-view" style="display: none;">
259
  <div class="row justify-content-center">
260
  <div class="col-lg-8">
261
  <div class="text-center mb-5">
262
+ <h1 class="fw-bold mb-2 h2">Post Like You’re Online!</h1>
263
+ <p class="text-secondary lead mb-1">Share your thoughts about halal food acquisition or issues like you would on a social media post!</p>
264
+ <p class="text-muted small"><em>Kongsikan pendapat anda tentang pencarian atau isu makanan halal seperti unggahan di media sosial!</em></p>
265
  </div>
266
+ <div class="card p-4 border-0 shadow-sm">
267
+ <div class="mb-3">
268
+ <label for="inputText" class="form-label fw-bold small text-uppercase text-secondary">Draft your social post</label>
269
+ <textarea id="inputText" class="form-control" rows="4" placeholder="English, Malay, mixed language, and local slang are all supported!"></textarea>
270
+ </div>
271
+ <button onclick="analyzeText()" class="btn btn-primary w-100 py-2 shadow-sm">Analyze Post Insights</button>
272
+
273
+ <div class="mt-4 pt-3 border-top">
274
+ <small class="text-muted d-block mb-2 fw-bold">Select an example to try:</small>
275
  <div class="d-flex flex-column gap-2">
276
  <button class="btn btn-sm btn-outline-secondary text-start overflow-hidden text-truncate" onclick="useExample(0)">Alhamdulillah, hari ni dapat makan dekat restoran halal baru. Rasa puas hati dan tenang bila tau makanan yang kita makan dijamin halal.</button>
277
  <button class="btn btn-sm btn-outline-secondary text-start overflow-hidden text-truncate" onclick="useExample(1)">Semua orang cakap kena check logo halal sebelum beli makanan. Dah jadi macam second nature dah sekarang. Korang pun sama kan?</button>
278
  </div>
279
  </div>
280
+
281
+ <div class="mt-3 text-center">
282
+ <small class="text-muted" style="font-size: 0.75rem;">Model can make mistakes; we are striving to improve the model for better accuracy.</small>
283
+ </div>
284
  </div>
285
  <div id="resultsArea" class="mt-4" style="display: none;">
286
+ <div class="row g-3 align-items-stretch">
287
+ <div class="col-md-4"><div class="score-card bg-tpb h-100 d-flex flex-column justify-content-center"><small>TPB Label</small><h4 id="resTpb" class="mt-2 mb-0">--</h4></div></div>
288
+ <div class="col-md-4"><div class="score-card bg-sent h-100 d-flex flex-column justify-content-center"><small>Sentiment</small><h4 id="resSent" class="mt-2">--</h4><small id="resProb" class="mb-0">0%</small></div></div>
289
+ <div class="col-md-4"><div class="score-card bg-dec h-100 d-flex flex-column justify-content-center"><small>Decision</small><h4 id="resDec" class="mt-2 mb-0">--</h4></div></div>
290
  </div>
291
  </div>
292
  </div>
 
324
  <div id="bulkResults" style="display: none;">
325
  <div id="bulkAnalyticsArea"></div> <!-- Dynamic Charts Container -->
326
 
327
+ <div class="mt-5 mb-4 d-flex justify-content-between align-items-end">
328
+ <div>
329
+ <h4 class="fw-bold mb-1 text-dark">All Analyzed Inputs</h4>
330
+ <p class="text-muted small mb-0">Detailed breakdown of your uploaded CSV content. Click any row to view full details.</p>
 
 
 
 
 
 
331
  </div>
332
  </div>
333
+
334
+ <div class="table-responsive">
335
+ <table class="table table-modern align-middle">
336
+ <thead>
337
+ <tr>
338
+ <th style="width: 55%;">Input Text</th>
339
+ <th style="width: 20%;">TPB Factor</th>
340
+ <th class="text-center">Sentiment</th>
341
+ <th class="text-center">Confidence</th>
342
+ </tr>
343
+ </thead>
344
+ <tbody id="bulkTableBody"></tbody>
345
+ </table>
346
+ </div>
347
+
348
+ <!-- Pagination Controls -->
349
+ <div id="bulkPagination" class="d-flex justify-content-between align-items-center mt-4">
350
+ <button class="btn btn-outline-secondary btn-sm" onclick="changePage(-1)" id="btnPrev">Previous</button>
351
+ <span class="text-muted small fw-bold" id="pageInfo">Page 1 of 1</span>
352
+ <button class="btn btn-outline-secondary btn-sm" onclick="changePage(1)" id="btnNext">Next</button>
353
+ </div>
354
  </div>
355
  </div>
356
 
357
  <!-- View 3: Company Dashboard -->
358
  <div id="dashboard-view" style="display: none;">
359
  <div class="text-center mb-5">
360
+ <h1 class="fw-bold mb-2 h2">Search Pre-trained Analysis</h1>
361
+ <p class="text-secondary lead mb-1">Search our trained dataset of pre-analyzed data using keywords to view sentiment and TPB insights</p>
362
+ <div class="mt-3">
363
+ <p class="text-muted small">This analysis utilizes a comprehensive trained dataset spanning from 2015 to September 2024.</p>
364
+ </div>
365
  <div class="row justify-content-center mt-4">
366
+ <div class="col-md-6">
367
+ <div class="d-flex gap-2">
368
+ <input type="text" id="dashKeyword" class="form-control" placeholder="Search Brand/Keyword...">
369
+ <button onclick="updateDashboard()" class="btn btn-primary text-nowrap">Search Dataset</button>
370
+ </div>
371
+ <div class="mt-3 text-start">
372
+ <small class="text-muted fw-bold d-block mb-2">Popular searches:</small>
373
+ <div class="d-flex flex-wrap gap-2 justify-content-start">
374
+ <button class="btn btn-sm btn-outline-secondary px-3 rounded-pill" onclick="usePopularSearch('Ayam')">Ayam</button>
375
+ <button class="btn btn-sm btn-outline-secondary px-3 rounded-pill" onclick="usePopularSearch('Jakim')">Jakim</button>
376
+ <button class="btn btn-sm btn-outline-secondary px-3 rounded-pill" onclick="usePopularSearch('Price')">Price</button>
377
+ <button class="btn btn-sm btn-outline-secondary px-3 rounded-pill" onclick="usePopularSearch('Sijil')">Sijil</button>
378
+ <button class="btn btn-sm btn-outline-secondary px-3 rounded-pill" onclick="usePopularSearch('Daging')">Daging</button>
379
+ <button class="btn btn-sm btn-outline-secondary px-3 rounded-pill" onclick="usePopularSearch('Food')">Food</button>
380
+ <button class="btn btn-sm btn-outline-secondary px-3 rounded-pill" onclick="usePopularSearch('Clean')">Clean</button>
381
+ <button class="btn btn-sm btn-outline-secondary px-3 rounded-pill" onclick="usePopularSearch('Logo')">Logo</button>
382
+ <button class="btn btn-sm btn-outline-secondary px-3 rounded-pill" onclick="usePopularSearch('Haram')">Haram</button>
383
+ <button class="btn btn-sm btn-outline-secondary px-3 rounded-pill" onclick="usePopularSearch('Trust')">Trust</button>
384
+ </div>
385
+ </div>
386
  </div>
387
  </div>
388
  </div>
389
  <div id="dashResults" style="display: none;">
390
  <div id="dashAnalyticsArea"></div>
391
+
392
+ <div class="mt-5 mb-4 d-flex justify-content-between align-items-end">
393
+ <div>
394
+ <h4 class="fw-bold mb-1 text-dark">Keyword Analysis Result</h4>
395
+ <p class="text-muted small mb-0">Detailed breakdown of posts matching your search criteria.</p>
396
+ </div>
397
+ </div>
398
+
399
+ <div class="table-responsive">
400
+ <table class="table table-modern align-middle">
401
+ <thead>
402
+ <tr>
403
+ <th style="width: 55%;">Review Text</th>
404
+ <th style="width: 20%;">TPB Factor</th>
405
+ <th class="text-center">Sentiment</th>
406
+ <th class="text-center">Confidence</th>
407
+ </tr>
408
+ </thead>
409
+ <tbody id="dashPreviewBody"></tbody>
410
+ </table>
411
+ </div>
412
  </div>
 
413
  </div>
414
  </div>
415
 
 
430
  <div class="col-md-6"><div class="card p-2"><div class="chart-tpb chart-container"></div></div></div>
431
  <div class="col-12"><div class="card p-2"><div class="chart-stack chart-container" style="height: 450px;"></div></div></div>
432
  </div>
433
+ <div class="d-flex justify-content-between align-items-center mb-3">
434
+ <h5 class="fw-bold mb-0">Contextual Word Clouds</h5>
435
+ <div class="btn-group" role="group">
436
+ <input type="radio" class="btn-check" name="wcView" id="wcViewTpb" autocomplete="off" checked onchange="toggleWcView('tpb')">
437
+ <label class="btn btn-outline-primary btn-sm" for="wcViewTpb">By TPB Factors</label>
438
+
439
+ <input type="radio" class="btn-check" name="wcView" id="wcViewSent" autocomplete="off" onchange="toggleWcView('sent')">
440
+ <label class="btn btn-outline-primary btn-sm" for="wcViewSent">By Sentiment</label>
441
+ </div>
442
+ </div>
443
+
444
+ <!-- TPB View (Default) -->
445
+ <div id="wc-tpb-container" class="row g-3 mb-4">
446
+ <div class="col-lg-3 col-6 wc-card"><div class="card h-100">
447
+ <div class="card-header bg-white small text-center fw-bold d-flex justify-content-between align-items-center px-2">
448
+ <span>Attitude</span>
449
+ <div class="dropdown">
450
+ <button class="btn btn-link btn-sm p-0 text-muted" data-bs-toggle="dropdown">⋮</button>
451
+ <ul class="dropdown-menu dropdown-menu-end small">
452
+ <li><button class="dropdown-item active" onclick="setWcFilter(this, 'attitude', 'all')">All Words</button></li>
453
+ <li><button class="dropdown-item text-success" onclick="setWcFilter(this, 'attitude', 'positive')">Positive Only</button></li>
454
+ <li><button class="dropdown-item text-danger" onclick="setWcFilter(this, 'attitude', 'negative')">Negative Only</button></li>
455
+ </ul>
456
+ </div>
457
+ </div>
458
+ <div class="card-body p-1 text-center position-relative">
459
+ <img class="img-fluid wc-img-attitude" data-factor="attitude">
460
+ <div class="position-absolute top-0 end-0 m-1"><span class="badge bg-secondary opacity-50 wc-badge-attitude">All</span></div>
461
+ </div>
462
+ </div></div>
463
+
464
+ <div class="col-lg-3 col-6 wc-card"><div class="card h-100">
465
+ <div class="card-header bg-white small text-center fw-bold d-flex justify-content-between align-items-center px-2">
466
+ <span>Knowledge</span>
467
+ <div class="dropdown">
468
+ <button class="btn btn-link btn-sm p-0 text-muted" data-bs-toggle="dropdown">⋮</button>
469
+ <ul class="dropdown-menu dropdown-menu-end small">
470
+ <li><button class="dropdown-item active" onclick="setWcFilter(this, 'religious knowledge', 'all')">All Words</button></li>
471
+ <li><button class="dropdown-item text-success" onclick="setWcFilter(this, 'religious knowledge', 'positive')">Positive Only</button></li>
472
+ <li><button class="dropdown-item text-danger" onclick="setWcFilter(this, 'religious knowledge', 'negative')">Negative Only</button></li>
473
+ </ul>
474
+ </div>
475
+ </div>
476
+ <div class="card-body p-1 text-center position-relative">
477
+ <img class="img-fluid wc-img-knowledge" data-factor="religious knowledge">
478
+ <div class="position-absolute top-0 end-0 m-1"><span class="badge bg-secondary opacity-50 wc-badge-knowledge">All</span></div>
479
+ </div>
480
+ </div></div>
481
+
482
+ <div class="col-lg-3 col-6 wc-card"><div class="card h-100">
483
+ <div class="card-header bg-white small text-center fw-bold d-flex justify-content-between align-items-center px-2">
484
+ <span>Norms</span>
485
+ <div class="dropdown">
486
+ <button class="btn btn-link btn-sm p-0 text-muted" data-bs-toggle="dropdown">⋮</button>
487
+ <ul class="dropdown-menu dropdown-menu-end small">
488
+ <li><button class="dropdown-item active" onclick="setWcFilter(this, 'subjective norms', 'all')">All Words</button></li>
489
+ <li><button class="dropdown-item text-success" onclick="setWcFilter(this, 'subjective norms', 'positive')">Positive Only</button></li>
490
+ <li><button class="dropdown-item text-danger" onclick="setWcFilter(this, 'subjective norms', 'negative')">Negative Only</button></li>
491
+ </ul>
492
+ </div>
493
+ </div>
494
+ <div class="card-body p-1 text-center position-relative">
495
+ <img class="img-fluid wc-img-norms" data-factor="subjective norms">
496
+ <div class="position-absolute top-0 end-0 m-1"><span class="badge bg-secondary opacity-50 wc-badge-norms">All</span></div>
497
+ </div>
498
+ </div></div>
499
+
500
+ <div class="col-lg-3 col-6 wc-card"><div class="card h-100">
501
+ <div class="card-header bg-white small text-center fw-bold d-flex justify-content-between align-items-center px-2">
502
+ <span>Control</span>
503
+ <div class="dropdown">
504
+ <button class="btn btn-link btn-sm p-0 text-muted" data-bs-toggle="dropdown">⋮</button>
505
+ <ul class="dropdown-menu dropdown-menu-end small">
506
+ <li><button class="dropdown-item active" onclick="setWcFilter(this, 'perceived behavioural control', 'all')">All Words</button></li>
507
+ <li><button class="dropdown-item text-success" onclick="setWcFilter(this, 'perceived behavioural control', 'positive')">Positive Only</button></li>
508
+ <li><button class="dropdown-item text-danger" onclick="setWcFilter(this, 'perceived behavioural control', 'negative')">Negative Only</button></li>
509
+ </ul>
510
+ </div>
511
+ </div>
512
+ <div class="card-body p-1 text-center position-relative">
513
+ <img class="img-fluid wc-img-control" data-factor="perceived behavioural control">
514
+ <div class="position-absolute top-0 end-0 m-1"><span class="badge bg-secondary opacity-50 wc-badge-control">All</span></div>
515
+ </div>
516
+ </div></div>
517
+ </div>
518
+
519
+ <!-- Global Sentiment View (Hidden) -->
520
+ <div id="wc-sent-container" class="row g-3 mb-4" style="display: none;">
521
+ <div class="col-md-6">
522
+ <div class="card h-100 border-success">
523
+ <div class="card-header bg-success text-white fw-bold text-center">Overall Positive</div>
524
+ <div class="card-body p-2 text-center">
525
+ <img class="img-fluid wc-img-global-pos">
526
+ </div>
527
+ </div>
528
+ </div>
529
+ <div class="col-md-6">
530
+ <div class="card h-100 border-danger">
531
+ <div class="card-header bg-danger text-white fw-bold text-center">Overall Negative</div>
532
+ <div class="card-body p-2 text-center">
533
+ <img class="img-fluid wc-img-global-neg">
534
+ </div>
535
+ </div>
536
+ </div>
537
  </div>
538
  <div class="card"><div class="card-header bg-danger text-white fw-bold">Strategic Recommendation Report</div><div class="card-body report-area"></div></div>
539
  <div class="card"><div class="card-header bg-dark text-white fw-bold">Top Bigrams by Factor</div><div class="card-body table-responsive"><table class="table table-bordered text-center"><thead><tr><th>Rank</th><th>Attitude</th><th>Knowledge</th><th>Norms</th><th>Control</th></tr></thead><tbody class="bigrams-body"></tbody></table></div></div>
 
543
  <script>
544
  const modal = new bootstrap.Modal(document.getElementById('detailModal'));
545
 
546
+ // Pagination State
547
+ let currentBulkData = [];
548
+ let currentPage = 1;
549
+ const itemsPerPage = 10;
550
+
551
+ // Word Cloud State
552
+ let currentWcData = null;
553
+
554
+ // Global badge helper for sentiment
555
+ function getBadge(s) {
556
+ if(s === 'positive') return '<span class="badge rounded-pill bg-success px-3">Positive</span>';
557
+ if(s === 'negative') return '<span class="badge rounded-pill bg-danger px-3">Negative</span>';
558
+ return '<span class="badge rounded-pill bg-secondary px-3">Neutral</span>';
559
+ }
560
+
561
+ function toggleWcView(view) {
562
+ const tpbContainer = document.getElementById('wc-tpb-container');
563
+ const sentContainer = document.getElementById('wc-sent-container');
564
+ if (view === 'tpb') {
565
+ tpbContainer.style.display = 'flex';
566
+ sentContainer.style.display = 'none';
567
+ } else {
568
+ tpbContainer.style.display = 'none';
569
+ sentContainer.style.display = 'flex';
570
+ }
571
+ }
572
+
573
+ function setWcFilter(btn, factor, type) {
574
+ // Update Active State in Dropdown
575
+ const list = btn.closest('ul');
576
+ list.querySelectorAll('.dropdown-item').forEach(b => b.classList.remove('active'));
577
+ btn.classList.add('active');
578
+
579
+ // Update Image
580
+ const cardBody = btn.closest('.card').querySelector('.card-body');
581
+ const img = cardBody.querySelector('img');
582
+ const badge = cardBody.querySelector('.badge');
583
+
584
+ if (currentWcData && currentWcData.tpb[factor] && currentWcData.tpb[factor][type]) {
585
+ img.src = "data:image/png;base64," + currentWcData.tpb[factor][type];
586
+ img.style.display = 'inline-block';
587
+ } else {
588
+ img.style.display = 'none'; // No data for this filter
589
+ }
590
+
591
+ // Update Badge Text/Color
592
+ badge.innerText = type.charAt(0).toUpperCase() + type.slice(1);
593
+ badge.className = 'badge opacity-75 position-absolute top-0 end-0 m-1'; // Reset classes
594
+ if(type === 'positive') badge.classList.add('bg-success');
595
+ else if(type === 'negative') badge.classList.add('bg-danger');
596
+ else badge.classList.add('bg-secondary');
597
+ }
598
+
599
+ function renderBulkTable() {
600
+ const tbody = document.getElementById('bulkTableBody');
601
+ tbody.innerHTML = "";
602
+
603
+ const start = (currentPage - 1) * itemsPerPage;
604
+ const end = start + itemsPerPage;
605
+ const pageData = currentBulkData.slice(start, end);
606
+ const totalPages = Math.ceil(currentBulkData.length / itemsPerPage);
607
+
608
+ pageData.forEach((row) => {
609
+ const tr = document.createElement('tr');
610
+ tr.className = `clickable-row row-sent-${row.label}`;
611
+ tr.innerHTML = `
612
+ <td class="fw-medium text-truncate" style="max-width: 450px;">${row.text}</td>
613
+ <td class="text-muted small fw-bold text-uppercase">${row.tpb_label}</td>
614
+ <td class="text-center">${getBadge(row.label)}</td>
615
+ <td class="text-center font-monospace small">${(row.score * 100).toFixed(2)}%</td>`;
616
+ tr.onclick = () => showRowDetail(row);
617
+ tbody.appendChild(tr);
618
+ });
619
+
620
+ // Update Controls
621
+ document.getElementById('pageInfo').innerText = `Page ${currentPage} of ${totalPages || 1}`;
622
+ document.getElementById('btnPrev').disabled = currentPage === 1;
623
+ document.getElementById('btnNext').disabled = currentPage === totalPages || totalPages === 0;
624
+ }
625
+
626
+ function changePage(delta) {
627
+ const totalPages = Math.ceil(currentBulkData.length / itemsPerPage);
628
+ const newPage = currentPage + delta;
629
+ if (newPage >= 1 && newPage <= totalPages) {
630
+ currentPage = newPage;
631
+ renderBulkTable();
632
+ }
633
+ }
634
+
635
  function initDropZone() {
636
  const dz = document.getElementById('dropZone');
637
  ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(evt => {
 
674
  }
675
 
676
  function showTab(tab) {
677
+ ['overview-view', 'analyzer-view', 'bulk-view', 'dashboard-view'].forEach(v => document.getElementById(v).style.display = 'none');
678
  document.getElementById(tab + '-view').style.display = 'block';
679
  document.querySelectorAll('.nav-link').forEach(btn => btn.classList.toggle('active', btn.onclick.toString().includes(tab)));
680
  }
 
694
  body: JSON.stringify({text})
695
  });
696
  const data = await res.json();
697
+
698
+ // Map Sentiment to Emoji and Color
699
+ let sentEmoji = '😐';
700
+ let sentColor = '#6c757d'; // Gray
701
+ if(data.sentiment_label === 'positive') { sentEmoji = '😃'; sentColor = '#198754'; } // Green
702
+ if(data.sentiment_label === 'negative') { sentEmoji = '😡'; sentColor = '#dc3545'; } // Red
703
+
704
+ // Map Decision to Icon and Color
705
+ let decIcon = '⚠️';
706
+ let decColor = '#3b82f6'; // Default Blue (Moderate)
707
+
708
+ if(data.decision.includes("High")) {
709
+ decIcon = '✅';
710
+ decColor = '#14b8a6'; // Teal
711
+ } else if(data.decision.includes("Low")) {
712
+ decIcon = '❌';
713
+ decColor = '#1e3a8a'; // Navy
714
+ }
715
+
716
  document.getElementById('resTpb').innerText = data.tpb_label;
717
+
718
+ const resSent = document.getElementById('resSent');
719
+ const sentCard = resSent.closest('.score-card');
720
+ sentCard.style.backgroundColor = sentColor;
721
+ sentCard.classList.remove('bg-sent');
722
+
723
+ resSent.innerHTML = `${sentEmoji} <span class="text-capitalize">${data.sentiment_label}</span>`;
724
+
725
+ document.getElementById('resProb').innerText = (data.sentiment_score * 100).toFixed(2) + "% confidence";
726
+
727
+ const resDec = document.getElementById('resDec');
728
+ const decCard = resDec.closest('.score-card');
729
+
730
+ // Set the background color of the card
731
+ decCard.style.backgroundColor = decColor;
732
+ decCard.classList.remove('bg-dec');
733
+
734
+ resDec.innerHTML = `${decIcon} <br>${data.decision}`;
735
+ resDec.style.color = 'white';
736
+
737
  document.getElementById('resultsArea').style.display = 'block';
738
  } catch(e) { console.error(e); }
739
  showLoader(false);
 
750
  const data = await res.json();
751
  renderAnalytics(data, 'bulkAnalyticsArea');
752
 
753
+ // Initialize Pagination
754
+ currentBulkData = data.full_results;
755
+ currentPage = 1;
756
+ renderBulkTable();
757
+
 
 
 
 
 
 
 
758
  document.getElementById('bulkResults').style.display = 'block';
759
  } catch(e) { console.error(e); alert("Error processing bulk data"); }
760
  showLoader(false);
761
  }
762
 
763
+ function usePopularSearch(kw) {
764
+ document.getElementById('dashKeyword').value = kw;
765
+ updateDashboard();
766
+ }
767
+
768
  async function updateDashboard() {
769
  const keyword = document.getElementById('dashKeyword').value;
770
  if(!keyword) return;
 
783
  }
784
 
785
  renderAnalytics(data, 'dashAnalyticsArea');
786
+
787
  const tbody = document.getElementById('dashPreviewBody');
788
  tbody.innerHTML = data.preview.map(r => `
789
+ <tr class="row-sent-${r.label}">
790
+ <td style="white-space: pre-wrap;" class="fw-medium">${r.text}</td>
791
+ <td class="text-muted small fw-bold text-uppercase">${r.tpb_label}</td>
792
+ <td class="text-center">${getBadge(r.label)}</td>
793
+ <td class="text-center font-monospace small">${(r.score * 100).toFixed(2)}%</td>
794
  </tr>`).join('');
795
 
796
+ // Update title area if needed, though strictly we're just updating the table content here.
797
+ // To change the "Data Preview" header, we need to target the HTML structure in the main view, not just this JS.
798
+
799
  document.getElementById('dashResults').style.display = 'block';
800
+ const ph = document.getElementById('dashPlaceholder');
801
+ if(ph) ph.style.display = 'none';
802
  } catch(e) { console.error(e); alert("Error loading dashboard"); }
803
  showLoader(false);
804
  }
 
825
  window.dispatchEvent(new Event('resize'));
826
  }, 100);
827
 
828
+ // Save Word Cloud Data
829
+ currentWcData = data.wordclouds;
830
+
831
+ // Initialize TPB View (Default to 'all')
832
+ const setWc = (cls, factor, type='all') => {
833
  const img = target.querySelector('.'+cls);
834
+ if(currentWcData.tpb[factor] && currentWcData.tpb[factor][type]) {
835
+ img.src = "data:image/png;base64," + currentWcData.tpb[factor][type];
836
+ img.closest('.wc-card').style.display = 'block';
837
+ } else {
838
+ img.closest('.wc-card').style.display = 'none';
839
+ }
840
+ };
841
+ setWc('wc-img-attitude', 'attitude');
842
+ setWc('wc-img-knowledge', 'religious knowledge');
843
+ setWc('wc-img-norms', 'subjective norms');
844
+ setWc('wc-img-control', 'perceived behavioural control');
845
+
846
+ // Initialize Global Sentiment View
847
+ const setGlobalWc = (cls, type) => {
848
+ const img = target.querySelector('.'+cls);
849
+ if(currentWcData.global[type]) {
850
+ img.src = "data:image/png;base64," + currentWcData.global[type];
851
+ }
852
  };
853
+ setGlobalWc('wc-img-global-pos', 'positive');
854
+ setGlobalWc('wc-img-global-neg', 'negative');
 
 
855
 
856
  const repArea = target.querySelector('.report-area');
857
  if(data.report.length > 0) {
858
+ // Generate a unique ID suffix based on targetId to avoid conflicts
859
+ const accId = "accordion-" + targetId;
860
+
861
+ repArea.innerHTML = `
862
+ <div class="accordion" id="${accId}">
863
+ ${data.report.map((item, index) => {
864
+ const itemId = accId + "-item-" + index;
865
+ const isFirst = index === 0;
866
+ return `
867
+ <div class="accordion-item">
868
+ <h2 class="accordion-header">
869
+ <button class="accordion-button ${!isFirst ? 'collapsed' : ''}" type="button" data-bs-toggle="collapse" data-bs-target="#${itemId}">
870
+ <div class="d-flex w-100 justify-content-between align-items-center me-3">
871
+ <strong>${item.factor}</strong>
872
+ <span class="badge bg-danger rounded-pill">${item.negative_pct.toFixed(1)}% Negative</span>
873
+ </div>
874
+ </button>
875
+ </h2>
876
+ <div id="${itemId}" class="accordion-collapse collapse ${isFirst ? 'show' : ''}" data-bs-parent="#${accId}">
877
+ <div class="accordion-body p-4">
878
+ ${(() => {
879
+ // Client-side parsing to separate sections
880
+ const parts = item.content.split('**Recommended Actions:**');
881
+ const issuesContent = parts[0].replace('**Current Issues:**', '').trim();
882
+ const actionsContent = parts.length > 1 ? parts[1].trim() : '';
883
+
884
+ return `
885
+ <!-- Issues Section -->
886
+ <div class="mb-4">
887
+ <h6 class="fw-bold text-danger d-flex align-items-center mb-3">
888
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-exclamation-circle-fill me-2" viewBox="0 0 16 16"><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM8 4a.905.905 0 0 0-.9.995l.35 3.507a.552.552 0 0 0 1.1 0l.35-3.507A.905.905 0 0 0 8 4zm.002 6a1 1 0 1 0 0 2 1 1 0 0 0 0-2z"/></svg>
889
+ Current Issues
890
+ </h6>
891
+ <div class="p-3 bg-danger bg-opacity-10 border border-danger border-opacity-25 rounded text-danger small">
892
+ ${marked.parse(issuesContent)}
893
+ </div>
894
+ </div>
895
+
896
+ <!-- Actions Section -->
897
+ <div>
898
+ <h6 class="fw-bold text-success d-flex align-items-center mb-3">
899
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check-circle-fill me-2" viewBox="0 0 16 16"><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/></svg>
900
+ Recommended Actions
901
+ </h6>
902
+ <div class="p-3 bg-success bg-opacity-10 border border-success border-opacity-25 rounded text-dark small">
903
+ ${marked.parse(actionsContent)}
904
+ </div>
905
+ </div>
906
+ `;
907
+ })()}
908
+ </div>
909
+ </div>
910
+ </div>
911
+ `;
912
+ }).join('')}
913
+ </div>
914
+ `;
915
+ } else {
916
+ repArea.innerHTML = "<p class='text-muted text-center my-3'>No critical issues detected (Threshold: >75% negative sentiment).</p>";
917
+ }
918
 
919
  const bgBody = target.querySelector('.bigrams-body');
920
  bgBody.innerHTML = Array.from({length: 10}).map((_, i) =>
 
927
 
928
  function showRowDetail(row) {
929
  const content = document.getElementById('modalContent');
930
+
931
+ let decColor = '#3b82f6'; // Blue
932
+ if(row.decision.includes("High")) decColor = '#14b8a6'; // Teal
933
+ if(row.decision.includes("Low")) decColor = '#1e3a8a'; // Navy
934
+
935
+ let sentBadgeClass = 'bg-secondary';
936
+ if(row.label === 'positive') sentBadgeClass = 'bg-success';
937
+ if(row.label === 'negative') sentBadgeClass = 'bg-danger';
938
+
939
  content.innerHTML = `
940
  <p class="mb-3"><strong>Original Input:</strong><br><span class="text-muted">${row.text}</span></p>
941
  <div class="list-group">
942
+ <div class="list-group-item d-flex justify-content-between"><span>Sentiment</span><span class="badge ${sentBadgeClass} rounded-pill">${row.label}</span></div>
943
  <div class="list-group-item d-flex justify-content-between"><span>Model Confidence</span><span class="fw-bold">${(row.score * 100).toFixed(2)}%</span></div>
944
  <div class="list-group-item d-flex justify-content-between"><span>TPB Category</span><span class="fw-bold">${row.tpb_label}</span></div>
945
+ <div class="list-group-item d-flex justify-content-between"><span>Recommendation</span><span class="badge" style="background-color: ${decColor}; color: white; line-height: 2;">${row.decision}</span></div>
946
  </div>`;
947
  modal.show();
948
  }