Spaces:
Running
Running
Commit
·
0c3803c
1
Parent(s):
2d575f7
update
Browse files- DEPLOYMENT_GUIDE.md +0 -87
- DEPLOYMENT_GUIDE_HF.md +62 -0
- Dockerfile +32 -0
- FUTURE_WORK.md +28 -0
- README.md +9 -0
- app.py +46 -15
- 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 (
|
| 80 |
-
sentiment_order_bin = ['negative', '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 |
else:
|
| 87 |
fig_sent = go.Figure()
|
| 88 |
-
fig_sent.update_layout(title='Overall Sentiment Distribution
|
| 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': '
|
| 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 |
-
|
| 126 |
-
|
| 127 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 166 |
-
elif sentiment_label == 'neutral': return "Moderate likelihood
|
| 167 |
-
else: return "Low likelihood
|
| 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 >
|
| 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 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 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 #
|
| 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: #
|
| 40 |
-
border-color: #
|
| 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('
|
|
|
|
| 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')">
|
| 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">
|
| 70 |
-
<p class="text-
|
|
|
|
| 71 |
</div>
|
| 72 |
-
<div class="card p-4">
|
| 73 |
-
<
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
<
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 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">
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
<div class="row justify-content-center mt-4">
|
| 145 |
-
<div class="col-md-6
|
| 146 |
-
<
|
| 147 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
</div>
|
| 149 |
</div>
|
| 150 |
</div>
|
| 151 |
<div id="dashResults" style="display: none;">
|
| 152 |
<div id="dashAnalyticsArea"></div>
|
| 153 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
<
|
| 177 |
-
|
| 178 |
-
<div class="
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 255 |
-
document.getElementById('
|
| 256 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 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
|
| 312 |
-
<td>${r.tpb_label}</td>
|
| 313 |
-
<td>${r.label}</td>
|
| 314 |
-
<td>${r.score.toFixed(
|
| 315 |
</tr>`).join('');
|
| 316 |
|
|
|
|
|
|
|
|
|
|
| 317 |
document.getElementById('dashResults').style.display = 'block';
|
| 318 |
-
document.getElementById('dashPlaceholder')
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 346 |
const img = target.querySelector('.'+cls);
|
| 347 |
-
if(
|
| 348 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 349 |
};
|
| 350 |
-
|
| 351 |
-
|
| 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 |
-
|
| 358 |
-
|
| 359 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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="
|
| 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="
|
| 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 |
}
|