ACA050 commited on
Commit
64e5ee2
·
verified ·
1 Parent(s): c0cc6f7

Upload 14 files

Browse files
Files changed (15) hide show
  1. .gitattributes +1 -0
  2. Dockerfile +27 -0
  3. README.md +115 -6
  4. anomaly.py +89 -0
  5. fraud_graph.py +114 -0
  6. generate_real_data.py +10 -0
  7. gst_api.py +30 -0
  8. index.html +1541 -0
  9. llm_explainer.py +92 -0
  10. main.py +183 -0
  11. reconciliation.py +139 -0
  12. requirements.txt +15 -0
  13. utils.py +118 -0
  14. vendor_index.faiss +3 -0
  15. vendor_mapping.pkl +3 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ vendor_index.faiss filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install system dependencies for FAISS and compiling native code if needed
6
+ RUN apt-get update && apt-get install -y \
7
+ build-essential \
8
+ libgomp1 \
9
+ && rm -rf /var/lib/apt/lists/*
10
+
11
+ COPY requirements.txt .
12
+
13
+ # Upgrade pip and install requirements
14
+ RUN pip install --no-cache-dir --upgrade pip && \
15
+ pip install --no-cache-dir -r requirements.txt
16
+
17
+ # Copy the rest of the application
18
+ COPY . .
19
+
20
+ # Expose port for Gradio
21
+ EXPOSE 7860
22
+
23
+ # Run the data generation script to have sample data on startup
24
+ RUN python generate_real_data.py
25
+
26
+ # Run the app
27
+ CMD ["python", "main.py"]
README.md CHANGED
@@ -1,12 +1,121 @@
1
  ---
2
- title: ReconAI
3
- emoji: 🌖
4
- colorFrom: yellow
5
  colorTo: indigo
6
  sdk: docker
 
7
  pinned: false
8
- license: mit
9
- short_description: Financial Reconcillation Engine
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: AI Financial Reconciliation Engine
3
+ emoji: 🧠
4
+ colorFrom: blue
5
  colorTo: indigo
6
  sdk: docker
7
+ app_port: 7860
8
  pinned: false
 
 
9
  ---
10
 
11
+ # 🧠 AI Financial Reconciliation Engine
12
+
13
+ Automated Financial Auditing using Machine Learning and LLMs.
14
+
15
+ ## 🚀 Overview
16
+ The **AI Financial Reconciliation Engine** is an intelligent system designed to automate the process of matching internal accounting records (Books) with external tax filings (GST). By combining **Fuzzy Logic**, **AI Semantic Embeddings**, and **LLM reasoning**, the system identifies discrepancies, detects fraudulent anomalies, and provides natural language explanations for auditors.
17
+
18
+ ## ✨ Features
19
+ - **Intelligent Matching**: Combines basic matching with Fuzzy and AI semantic analysis to reconcile records even with typos or name variations.
20
+ - **Anomaly Detection**: Uses the `IsolationForest` algorithm to detect unusual transaction patterns and high-risk invoices.
21
+ - **AI Explanations**: Integrates Mistral LLM to provide human-readable audit comments for every discrepancy.
22
+ - **Interactive Dashboard**: A professional Gradio interface with summary metrics, risk-sorted results, and CSV export.
23
+ - **Graph Fraud Network**: Visualizes circular trading and multi-hop tax siphoning fraud rings using `NetworkX` and `Matplotlib`.
24
+ - **Persistent Vector Memory**: Uses C++ compiled `FAISS` algorithms to permanently remember vendor vector embeddings.
25
+ - **Deployment Ready**: Containerized with **Docker** and hosted on **HuggingFace Spaces**.
26
+
27
+ ## 🛠 Tech Stack
28
+ - **Languages**: Python
29
+ - **AI/ML**: Scikit-Learn, Sentence-Transformers, RapidFuzz
30
+ - **Fraud Engine**: FAISS, NetworkX, Matplotlib
31
+ - **LLM**: Mistral AI API
32
+ - **Frontend**: Gradio
33
+ - **Infrastructure**: Docker, HuggingFace Spaces
34
+
35
+ ## 📂 Installation (Local)
36
+ 1. Clone the repository.
37
+ 2. Install dependencies: `pip install -r requirements.txt`
38
+ 3. Set your `MISTRAL_API_KEY` in a `.env` file.
39
+ 4. Run the app: `python main.py`
40
+
41
+ ### Prerequisites
42
+
43
+ - Python 3.11+
44
+ - Virtual Environment (venv)
45
+
46
+ ### Setup
47
+
48
+ 1. Clone the repository
49
+ 2. Create and activate virtual environment:
50
+
51
+ ```bash
52
+ python -m venv venv
53
+ source venv/bin/activate # Linux/macOS
54
+ venv\\Scripts\\activate # Windows
55
+ ```
56
+
57
+ 3. Install dependencies:
58
+
59
+ ```bash
60
+ pip install -r requirements.txt
61
+ ```
62
+
63
+ 4. Configure environment variables:
64
+ - Copy `.env.example` to `.env`
65
+ - Add your API keys
66
+
67
+ ## Usage
68
+
69
+ ### Quick Start
70
+
71
+ ```python
72
+ from utils import create_sample_data
73
+ from reconciliation import ReconciliationEngine
74
+ from anomaly import AnomalyDetector
75
+
76
+ # Create sample data
77
+ data = create_sample_data(num_records=100)
78
+ source_df = data['source']
79
+ target_df = data['target']
80
+
81
+ # Run reconciliation
82
+ engine = ReconciliationEngine(threshold=85.0)
83
+ result = engine.reconcile(source_df, target_df, 'VendorName', 'VendorName', 'Amount')
84
+
85
+ # Detect anomalies
86
+ detector = AnomalyDetector(contamination=0.05)
87
+ anomaly_result = detector.detect_anomalies(source_df)
88
+ ```
89
+
90
+ ### Web Interface
91
+
92
+ ```bash
93
+ python main.py
94
+ ```
95
+
96
+ Access the UI at `http://localhost:7860`
97
+
98
+ ### Docker
99
+
100
+ ```bash
101
+ docker build -t reconciliation-engine .
102
+ docker run -p 7860:7860 reconciliation-engine
103
+ ```
104
+
105
+ ## Project Structure
106
+
107
+ ```
108
+ ├── sample_data/ # Live CSV data and scenarios
109
+ ├── main.py # Main FastAPI backend serving UI
110
+ ├── reconciliation.py # Core reconciliation engine & FAISS Index
111
+ ├── anomaly.py # Anomaly detection module
112
+ ├── fraud_graph.py # NetworkX Circular Trading Detector
113
+ ├── gst_api.py # Real-time Local Registry Gateway
114
+ ├── generate_real_data.py # Script to generate 1800+ realistic rows
115
+ ├── llm_explainer.py # LLM-powered explanations
116
+ ├── utils.py # Utility functions
117
+ ├── requirements.txt # Python dependencies
118
+ ├── Dockerfile # Docker configuration
119
+ ├── .env # Environment variables
120
+ └── README.md # This file
121
+ ```
anomaly.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import numpy as np
3
+ from sklearn.ensemble import IsolationForest
4
+ from sklearn.preprocessing import StandardScaler
5
+ import logging
6
+
7
+ logging.basicConfig(level=logging.INFO)
8
+ logger = logging.getLogger(__name__)
9
+
10
+ class AnomalyDetector:
11
+ def __init__(self, contamination=0.05):
12
+ self.contamination = contamination
13
+ self.model = IsolationForest(contamination=self.contamination, random_state=42, n_estimators=100)
14
+ self.scaler = StandardScaler()
15
+
16
+ def prepare_features(self, df, amount_col):
17
+ features_df = df.copy()
18
+
19
+ # Basic amount features
20
+ features_df['amount_log'] = np.log1p(np.abs(features_df[amount_col].fillna(0)))
21
+ features_df['amount_sign'] = np.sign(features_df[amount_col].fillna(0))
22
+
23
+ feature_columns = [amount_col, 'amount_log', 'amount_sign']
24
+
25
+ # Statistical features
26
+ if len(df) > 1:
27
+ features_df['amount_zscore'] = (
28
+ (features_df[amount_col] - features_df[amount_col].mean()) /
29
+ (features_df[amount_col].std() + 1e-9)
30
+ )
31
+ feature_columns.append('amount_zscore')
32
+
33
+ # Try to do rolling stats if date column exists
34
+ date_col = next((col for col in ['InvoiceDate', 'date', 'Date'] if col in features_df.columns), None)
35
+ if date_col:
36
+ # Keep track of original index to restore order later
37
+ features_df['original_idx'] = features_df.index
38
+ features_df[date_col] = pd.to_datetime(features_df[date_col], errors='coerce')
39
+ features_df = features_df.sort_values(date_col)
40
+
41
+ features_df['amount_rolling_mean'] = features_df[amount_col].rolling(7, min_periods=1).mean()
42
+ features_df['amount_rolling_std'] = features_df[amount_col].rolling(7, min_periods=1).std().fillna(0)
43
+ feature_columns.extend(['amount_rolling_mean', 'amount_rolling_std'])
44
+
45
+ # Restore original index order so we don't shuffle the output dataframe
46
+ features_df = features_df.sort_values('original_idx')
47
+
48
+ features_df = features_df.fillna(0)
49
+ return features_df, feature_columns
50
+
51
+ def detect_anomalies(self, df, amount_col='Amount'):
52
+ """
53
+ Detects anomalies in the given DataFrame based on the specified amount column.
54
+ Returns the DataFrame with 'IsAnomaly' and 'AnomalyScore' appended.
55
+ """
56
+ logger.info(f"Running advanced anomaly detection on column: {amount_col}")
57
+
58
+ if df.empty or amount_col not in df.columns:
59
+ logger.warning("DataFrame is empty or amount column not found.")
60
+ df['IsAnomaly'] = False
61
+ df['AnomalyScore'] = 0.0
62
+ return df
63
+
64
+ try:
65
+ # Prepare advanced features
66
+ features_df, feature_cols = self.prepare_features(df, amount_col)
67
+ X = features_df[feature_cols].values
68
+
69
+ # Scale features
70
+ X_scaled = self.scaler.fit_transform(X)
71
+
72
+ # Fit and predict
73
+ predictions = self.model.fit_predict(X_scaled)
74
+ scores = self.model.decision_function(X_scaled)
75
+
76
+ # -1 indicates anomaly, 1 indicates normal
77
+ df['IsAnomaly'] = predictions == -1
78
+
79
+ # Normalize scores: lower IsolationForest score = more anomalous.
80
+ # We invert it so a higher positive score = higher anomaly risk.
81
+ df['AnomalyScore'] = -scores
82
+
83
+ logger.info(f"Anomaly detection complete. Found {df['IsAnomaly'].sum()} anomalies.")
84
+ except Exception as e:
85
+ logger.error(f"Error during advanced anomaly detection: {e}")
86
+ df['IsAnomaly'] = False
87
+ df['AnomalyScore'] = 0.0
88
+
89
+ return df
fraud_graph.py ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import networkx as nx
2
+ import matplotlib.pyplot as plt
3
+ import pandas as pd
4
+ import io
5
+ import logging
6
+
7
+ logging.basicConfig(level=logging.INFO)
8
+ logger = logging.getLogger(__name__)
9
+
10
+ class FraudGraph:
11
+ def __init__(self):
12
+ self.graph = nx.DiGraph()
13
+
14
+ def build_graph(self, df, source_col='VendorName', target_col='BuyerName', amount_col='Amount'):
15
+ """
16
+ Builds a directed graph of transactions.
17
+ Assuming we have some buyer info, but in standard Books vs GST,
18
+ we usually have our company as the buyer and vendors as sellers.
19
+ To simulate circular trading, we might need a dataset that has multi-party transactions.
20
+ For demonstration, we'll try to find any cycles if they exist.
21
+ """
22
+ self.graph.clear()
23
+
24
+ # If 'BuyerName' isn't there, we'll simulate it by assuming 'OurCompany' is the buyer
25
+ # but to show circular trading, let's look for duplicate invoices or anomalies
26
+
27
+ if target_col not in df.columns:
28
+ logger.warning(f"No '{target_col}' column. Assuming central company.")
29
+ buyer_col_actual = 'TargetEntity'
30
+ df[buyer_col_actual] = 'OurCompany'
31
+ else:
32
+ buyer_col_actual = target_col
33
+
34
+ for _, row in df.iterrows():
35
+ source = str(row.get(source_col, 'Unknown'))
36
+ target = str(row.get(buyer_col_actual, 'Unknown'))
37
+ raw_w = row.get(amount_col, 0)
38
+ if pd.isna(raw_w):
39
+ raw_w = 0
40
+ weight = float(raw_w)
41
+
42
+ if self.graph.has_edge(source, target):
43
+ self.graph[source][target]['weight'] += weight
44
+ else:
45
+ self.graph.add_edge(source, target, weight=weight)
46
+
47
+ def detect_cycles(self):
48
+ try:
49
+ cycles = list(nx.simple_cycles(self.graph))
50
+ # Filter out self-loops (length 1) which just represent exact matches between Books and GST
51
+ cycles = [c for c in cycles if len(c) > 1]
52
+ return cycles
53
+ except Exception as e:
54
+ logger.error(f"Error detecting cycles: {e}")
55
+ return []
56
+
57
+ def analyze_risk_nodes(self):
58
+ """
59
+ Calculate centrality scores to find high-risk 'hub' vendors using PageRank.
60
+ Returns a dictionary mapping vendor names to risk scores.
61
+ """
62
+ if len(self.graph.nodes) < 2:
63
+ return {}
64
+
65
+ try:
66
+ pagerank = nx.pagerank(self.graph, weight='weight')
67
+ return pagerank
68
+ except Exception as e:
69
+ logger.error(f"Error calculating PageRank: {e}")
70
+ return {node: 0.0 for node in self.graph.nodes}
71
+
72
+
73
+ def visualize_graph(self, title="Transaction Network"):
74
+ plt.figure(figsize=(12, 8))
75
+
76
+ # Try to find a good layout
77
+ pos = nx.spring_layout(self.graph, k=0.5, iterations=50)
78
+
79
+ # Node sizes based on degree
80
+ node_sizes = [300 + 100 * self.graph.degree(n) for n in self.graph.nodes()]
81
+
82
+ # Draw nodes
83
+ nx.draw_networkx_nodes(self.graph, pos, node_size=node_sizes, node_color='skyblue', alpha=0.8)
84
+
85
+ # Draw edges
86
+ edges = self.graph.edges(data=True)
87
+ weights = [d['weight'] / 1000 for u, v, d in edges] # Scale down for visualization
88
+ nx.draw_networkx_edges(self.graph, pos, width=weights, alpha=0.5, edge_color='gray', arrows=True)
89
+
90
+ # Draw labels
91
+ nx.draw_networkx_labels(self.graph, pos, font_size=10, font_family="sans-serif")
92
+
93
+ # Highlight cycles if any
94
+ cycles = self.detect_cycles()
95
+ if cycles:
96
+ cycle_edges = []
97
+ for cycle in cycles:
98
+ for i in range(len(cycle)):
99
+ cycle_edges.append((cycle[i], cycle[(i + 1) % len(cycle)]))
100
+
101
+ # Draw cycle edges in red
102
+ nx.draw_networkx_edges(self.graph, pos, edgelist=cycle_edges, width=2.0, edge_color='red', arrows=True)
103
+ plt.title(f"{title} - Alert: {len(cycles)} Potential Circular Trading Rings Detected!", color='red')
104
+ else:
105
+ plt.title(f"{title} - No obvious circular rings detected.")
106
+
107
+ plt.axis('off')
108
+
109
+ # Save to buffer
110
+ buf = io.BytesIO()
111
+ plt.savefig(buf, format='png', bbox_inches='tight')
112
+ plt.close()
113
+ buf.seek(0)
114
+ return buf
generate_real_data.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ from utils import create_sample_data
2
+ import os
3
+
4
+ def generate_large_dataset():
5
+ print("Generating large realistic dataset (1800+ rows)...")
6
+ create_sample_data(num_records=1850, output_dir="sample_data")
7
+ print("Done! Data saved to sample_data/books.csv and sample_data/gst.csv")
8
+
9
+ if __name__ == "__main__":
10
+ generate_large_dataset()
gst_api.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+ import pandas as pd
3
+ import logging
4
+
5
+
6
+ logging.basicConfig(level=logging.INFO)
7
+ logger = logging.getLogger(__name__)
8
+
9
+ class GSTGatewayMock:
10
+ """
11
+ Mocks a real-time fetching from live GST sites.
12
+ In a real scenario, this would use requests to hit a government API endpoint.
13
+ """
14
+ def __init__(self):
15
+ self.api_url = "https://mock-gst-api.gov.in/v1/returns"
16
+
17
+ def fetch_gst_data(self, start_date, end_date, gstin="27AADCB2230M1Z2"):
18
+ logger.info(f"Simulating fetch from {self.api_url} for GSTIN {gstin}")
19
+ # Simulate network latency
20
+ time.sleep(2)
21
+
22
+ # Generate an empty DataFrame to represent no live data without credentials
23
+ # (This prevents injecting fake/dummy data into the user's analysis)
24
+ logger.info("Live GST API requires production credentials. Returning empty dataset.")
25
+ return pd.DataFrame(columns=['InvoiceID', 'VendorName', 'Amount', 'InvoiceDate', 'GSTIN'])
26
+
27
+ def validate_gstin(self, gstin):
28
+ """Mock GSTIN validation"""
29
+ time.sleep(0.5)
30
+ return len(gstin) == 15
index.html ADDED
@@ -0,0 +1,1541 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en" class="dark">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>AI Financial Reconciliation Engine 🧠</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
9
+ <script src="https://unpkg.com/lucide@latest"></script>
10
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
11
+ <script>
12
+ tailwind.config = {
13
+ darkMode: 'class',
14
+ theme: {
15
+ extend: {
16
+ fontFamily: {
17
+ sans: ['Inter', 'sans-serif'],
18
+ mono: ['JetBrains Mono', 'monospace'],
19
+ },
20
+ colors: {
21
+ brand: {
22
+ 50: '#eef2ff',
23
+ 100: '#e0e7ff',
24
+ 200: '#c7d2fe',
25
+ 300: '#a5b4fc',
26
+ 400: '#818cf8',
27
+ 500: '#6366f1',
28
+ 600: '#4f46e5',
29
+ 700: '#4338ca',
30
+ 800: '#3730a3',
31
+ 900: '#312e81',
32
+ },
33
+ surface: {
34
+ 50: '#f8fafc',
35
+ 100: '#f1f5f9',
36
+ 200: '#e2e8f0',
37
+ 300: '#cbd5e1',
38
+ 400: '#94a3b8',
39
+ 500: '#64748b',
40
+ 600: '#475569',
41
+ 700: '#334155',
42
+ 800: '#1e293b',
43
+ 900: '#0f172a',
44
+ 950: '#020617',
45
+ }
46
+ }
47
+ }
48
+ }
49
+ }
50
+ </script>
51
+ <style>
52
+ * { scrollbar-width: thin; scrollbar-color: #475569 transparent; }
53
+ *::-webkit-scrollbar { width: 6px; }
54
+ *::-webkit-scrollbar-track { background: transparent; }
55
+ *::-webkit-scrollbar-thumb { background: #475569; border-radius: 3px; }
56
+
57
+ @keyframes pulse-glow {
58
+ 0%, 100% { box-shadow: 0 0 8px rgba(99,102,241,0.4); }
59
+ 50% { box-shadow: 0 0 20px rgba(99,102,241,0.8); }
60
+ }
61
+ @keyframes slide-in { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: translateY(0); } }
62
+ @keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
63
+ @keyframes shimmer {
64
+ 0% { background-position: -200% 0; }
65
+ 100% { background-position: 200% 0; }
66
+ }
67
+ @keyframes float {
68
+ 0%, 100% { transform: translateY(0px); }
69
+ 50% { transform: translateY(-6px); }
70
+ }
71
+ @keyframes count-up { from { opacity: 0.5; } to { opacity: 1; } }
72
+
73
+ .animate-slide-in { animation: slide-in 0.4s ease-out forwards; }
74
+ .animate-fade-in { animation: fade-in 0.3s ease-out forwards; }
75
+ .animate-pulse-glow { animation: pulse-glow 2s ease-in-out infinite; }
76
+ .animate-float { animation: float 3s ease-in-out infinite; }
77
+ .shimmer-bg {
78
+ background: linear-gradient(90deg, transparent, rgba(255,255,255,0.05), transparent);
79
+ background-size: 200% 100%;
80
+ animation: shimmer 2s infinite;
81
+ }
82
+
83
+ .glass-card {
84
+ background: rgba(15, 23, 42, 0.6);
85
+ backdrop-filter: blur(16px);
86
+ border: 1px solid rgba(99, 102, 241, 0.15);
87
+ }
88
+ .glass-card-light {
89
+ background: rgba(255, 255, 255, 0.7);
90
+ backdrop-filter: blur(16px);
91
+ border: 1px solid rgba(99, 102, 241, 0.1);
92
+ }
93
+ .risk-critical { border-left: 4px solid #ef4444; }
94
+ .risk-high { border-left: 4px solid #f97316; }
95
+ .risk-medium { border-left: 4px solid #eab308; }
96
+ .risk-low { border-left: 4px solid #22c55e; }
97
+
98
+ .tab-active {
99
+ background: linear-gradient(135deg, #4f46e5, #6366f1);
100
+ color: white;
101
+ box-shadow: 0 4px 12px rgba(99,102,241,0.4);
102
+ }
103
+
104
+ .network-node { transition: all 0.3s ease; cursor: pointer; }
105
+ .network-node:hover { filter: brightness(1.3); transform: scale(1.1); }
106
+
107
+ .data-table tr { transition: background-color 0.15s ease; }
108
+
109
+ .progress-bar {
110
+ background: linear-gradient(90deg, #4f46e5, #818cf8, #4f46e5);
111
+ background-size: 200% 100%;
112
+ animation: shimmer 1.5s infinite;
113
+ }
114
+
115
+ .stat-card::before {
116
+ content: '';
117
+ position: absolute;
118
+ top: 0; left: 0; right: 0;
119
+ height: 3px;
120
+ border-radius: 9999px 9999px 0 0;
121
+ }
122
+ .stat-card-purple::before { background: linear-gradient(90deg, #6366f1, #a78bfa); }
123
+ .stat-card-green::before { background: linear-gradient(90deg, #22c55e, #4ade80); }
124
+ .stat-card-orange::before { background: linear-gradient(90deg, #f97316, #fb923c); }
125
+ .stat-card-red::before { background: linear-gradient(90deg, #ef4444, #f87171); }
126
+ .stat-card-cyan::before { background: linear-gradient(90deg, #06b6d4, #22d3ee); }
127
+ .stat-card-pink::before { background: linear-gradient(90deg, #ec4899, #f472b6); }
128
+
129
+ .glow-text {
130
+ text-shadow: 0 0 20px rgba(99,102,241,0.5);
131
+ }
132
+
133
+ .sidebar-link {
134
+ transition: all 0.2s ease;
135
+ position: relative;
136
+ }
137
+ .sidebar-link::before {
138
+ content: '';
139
+ position: absolute;
140
+ left: 0; top: 0; bottom: 0;
141
+ width: 3px;
142
+ background: #6366f1;
143
+ border-radius: 0 4px 4px 0;
144
+ transform: scaleY(0);
145
+ transition: transform 0.2s ease;
146
+ }
147
+ .sidebar-link.active::before,
148
+ .sidebar-link:hover::before {
149
+ transform: scaleY(1);
150
+ }
151
+ .sidebar-link.active {
152
+ background: rgba(99, 102, 241, 0.15);
153
+ color: #818cf8;
154
+ }
155
+
156
+ .tooltip-container { position: relative; }
157
+ .tooltip-container .tooltip {
158
+ position: absolute;
159
+ bottom: 100%;
160
+ left: 50%;
161
+ transform: translateX(-50%) translateY(4px);
162
+ opacity: 0;
163
+ pointer-events: none;
164
+ transition: all 0.2s ease;
165
+ z-index: 50;
166
+ }
167
+ .tooltip-container:hover .tooltip {
168
+ opacity: 1;
169
+ transform: translateX(-50%) translateY(-4px);
170
+ }
171
+ </style>
172
+ </head>
173
+ <body class="bg-surface-950 text-surface-100 font-sans min-h-screen">
174
+ <!-- Background Effects -->
175
+ <div class="fixed inset-0 pointer-events-none overflow-hidden z-0">
176
+ <div class="absolute top-0 left-1/4 w-96 h-96 bg-brand-600/10 rounded-full blur-3xl"></div>
177
+ <div class="absolute bottom-0 right-1/4 w-80 h-80 bg-purple-600/8 rounded-full blur-3xl"></div>
178
+ <div class="absolute top-1/2 left-1/2 w-64 h-64 bg-cyan-600/5 rounded-full blur-3xl"></div>
179
+ </div>
180
+
181
+ <div class="flex min-h-screen relative z-10">
182
+ <!-- Sidebar -->
183
+ <aside id="sidebar" class="w-64 border-r border-surface-800/50 bg-surface-950/80 backdrop-blur-xl flex flex-col transition-all duration-300 fixed lg:relative z-40 -translate-x-full lg:translate-x-0 h-screen">
184
+ <!-- Logo -->
185
+ <div class="p-5 border-b border-surface-800/50">
186
+ <div class="flex items-center gap-3">
187
+ <div class="w-10 h-10 rounded-xl bg-gradient-to-br from-brand-500 to-purple-600 flex items-center justify-center animate-pulse-glow">
188
+ <i data-lucide="brain" class="w-5 h-5 text-white"></i>
189
+ </div>
190
+ <div>
191
+ <h1 class="text-sm font-bold text-white tracking-tight">ReconAI</h1>
192
+ <p class="text-[10px] text-surface-400 font-mono uppercase tracking-widest">Financial Engine</p>
193
+ </div>
194
+ </div>
195
+ </div>
196
+
197
+ <!-- Nav Links -->
198
+ <nav class="flex-1 p-3 space-y-1 overflow-y-auto">
199
+ <p class="text-[10px] uppercase tracking-widest text-surface-500 font-semibold px-3 py-2">Main</p>
200
+ <button onclick="switchTab('dashboard')" class="sidebar-link active w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-surface-300 hover:text-white" data-nav="dashboard">
201
+ <i data-lucide="layout-dashboard" class="w-4 h-4"></i> Dashboard
202
+ </button>
203
+ <button onclick="switchTab('reconciliation')" class="sidebar-link w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-surface-300 hover:text-white" data-nav="reconciliation">
204
+ <i data-lucide="git-merge" class="w-4 h-4"></i> Reconciliation
205
+ </button>
206
+ <button onclick="switchTab('anomaly')" class="sidebar-link w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-surface-300 hover:text-white" data-nav="anomaly">
207
+ <i data-lucide="alert-triangle" class="w-4 h-4"></i> Anomaly Detection
208
+ </button>
209
+
210
+ <p class="text-[10px] uppercase tracking-widest text-surface-500 font-semibold px-3 py-2 mt-4">Analysis</p>
211
+ <button onclick="switchTab('fraud')" class="sidebar-link w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-surface-300 hover:text-white" data-nav="fraud">
212
+ <i data-lucide="network" class="w-4 h-4"></i> Fraud Network
213
+ </button>
214
+ <button onclick="switchTab('ai-explain')" class="sidebar-link w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-surface-300 hover:text-white" data-nav="ai-explain">
215
+ <i data-lucide="message-square-text" class="w-4 h-4"></i> ReconAI
216
+ </button>
217
+ <button onclick="switchTab('vector')" class="sidebar-link w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-surface-300 hover:text-white" data-nav="vector">
218
+ <i data-lucide="database" class="w-4 h-4"></i> Vector Memory
219
+ </button>
220
+ </nav>
221
+
222
+ <!-- Status -->
223
+ <div class="p-4 border-t border-surface-800/50">
224
+ <div class="glass-card rounded-lg p-3">
225
+ <div class="flex items-center gap-2 mb-2">
226
+ <span class="w-2 h-2 bg-green-400 rounded-full animate-pulse"></span>
227
+ <span class="text-xs text-surface-300">Engine Online</span>
228
+ </div>
229
+ <div class="flex items-center gap-2 text-[10px] text-surface-500">
230
+ <span>FAISS: Active</span>
231
+ <span>•</span>
232
+ <span>LLM: Connected</span>
233
+ </div>
234
+ </div>
235
+ </div>
236
+ </aside>
237
+
238
+ <!-- Overlay for mobile sidebar -->
239
+ <div id="sidebar-overlay" class="fixed inset-0 bg-black/50 z-30 hidden lg:hidden" onclick="toggleSidebar()"></div>
240
+
241
+ <!-- Main Content -->
242
+ <main class="flex-1 flex flex-col min-h-screen overflow-x-hidden">
243
+ <!-- Top Bar -->
244
+ <header class="sticky top-0 z-20 border-b border-surface-800/50 bg-surface-950/80 backdrop-blur-xl">
245
+ <div class="flex items-center justify-between px-4 lg:px-6 py-3">
246
+ <div class="flex items-center gap-3">
247
+ <button onclick="toggleSidebar()" class="lg:hidden p-2 rounded-lg hover:bg-surface-800 transition">
248
+ <i data-lucide="menu" class="w-5 h-5"></i>
249
+ </button>
250
+ <div>
251
+ <h2 id="page-title" class="text-lg font-semibold text-white">Dashboard</h2>
252
+ <p id="page-subtitle" class="text-xs text-surface-400">Financial reconciliation overview</p>
253
+ </div>
254
+ </div>
255
+ <div class="flex items-center gap-2">
256
+ <div class="hidden sm:flex items-center gap-2 bg-surface-900 border border-surface-700/50 rounded-lg px-3 py-2">
257
+ <i data-lucide="search" class="w-4 h-4 text-surface-400"></i>
258
+ <input type="text" placeholder="Search transactions..." class="bg-transparent text-sm text-surface-200 placeholder-surface-500 outline-none w-40 lg:w-56">
259
+ </div>
260
+ <input type="file" id="books-file" class="hidden" accept=".csv" onchange="updateFileLabel('books')">
261
+ <input type="file" id="gst-file" class="hidden" accept=".csv" onchange="updateFileLabel('gst')">
262
+
263
+ <button onclick="document.getElementById('books-file').click()" id="btn-books" class="flex items-center gap-2 bg-surface-800 hover:bg-surface-700 text-surface-300 text-sm font-medium px-3 py-2 rounded-lg transition-all border border-surface-700/50">
264
+ <i data-lucide="upload-cloud" class="w-4 h-4"></i>
265
+ <span class="hidden sm:inline" id="lbl-books">Books CSV</span>
266
+ </button>
267
+ <button onclick="document.getElementById('gst-file').click()" id="btn-gst" class="flex items-center gap-2 bg-surface-800 hover:bg-surface-700 text-surface-300 text-sm font-medium px-3 py-2 rounded-lg transition-all border border-surface-700/50">
268
+ <i data-lucide="upload-cloud" class="w-4 h-4"></i>
269
+ <span class="hidden sm:inline" id="lbl-gst">GST CSV</span>
270
+ </button>
271
+
272
+ <button onclick="runReconciliation()" id="btn-run" class="flex items-center gap-2 bg-gradient-to-r from-brand-600 to-purple-600 hover:from-brand-500 hover:to-purple-500 text-white text-sm font-medium px-4 py-2 rounded-lg transition-all shadow-lg shadow-brand-600/25">
273
+ <i data-lucide="play" class="w-4 h-4"></i>
274
+ <span class="hidden sm:inline">Run Engine</span>
275
+ </button>
276
+
277
+ <button onclick="exportCSV()" class="p-2 rounded-lg hover:bg-surface-800 transition tooltip-container relative">
278
+ <i data-lucide="download" class="w-4 h-4 text-surface-400"></i>
279
+ <div class="tooltip bg-surface-800 text-xs px-2 py-1 rounded whitespace-nowrap">Export CSV</div>
280
+ </button>
281
+
282
+ </div>
283
+ </div>
284
+ </header>
285
+
286
+ <!-- Content Area -->
287
+ <div class="flex-1 p-4 lg:p-6 space-y-6">
288
+
289
+ <!-- ==================== DASHBOARD TAB ==================== -->
290
+ <section id="tab-dashboard" class="tab-content space-y-6">
291
+ <!-- Stat Cards Row -->
292
+ <div class="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 gap-3 lg:gap-4">
293
+ <div class="stat-card stat-card-purple glass-card rounded-xl p-4 relative overflow-hidden animate-slide-in" style="animation-delay:0ms">
294
+ <div class="flex items-center justify-between mb-2">
295
+ <span class="text-xs text-surface-400 font-medium">Total Records</span>
296
+ <div class="w-8 h-8 rounded-lg bg-brand-600/20 flex items-center justify-center">
297
+ <i data-lucide="file-text" class="w-4 h-4 text-brand-400"></i>
298
+ </div>
299
+ </div>
300
+ <p id="stat-total-records" class="text-2xl font-bold text-white">0</p>
301
+ <p id="stat-total-records-sub" class="text-[10px] text-green-400 mt-1 flex items-center gap-1">Awaiting data</p>
302
+ </div>
303
+ <div class="stat-card stat-card-green glass-card rounded-xl p-4 relative overflow-hidden animate-slide-in" style="animation-delay:80ms">
304
+ <div class="flex items-center justify-between mb-2">
305
+ <span class="text-xs text-surface-400 font-medium">Matched</span>
306
+ <div class="w-8 h-8 rounded-lg bg-green-600/20 flex items-center justify-center">
307
+ <i data-lucide="check-circle" class="w-4 h-4 text-green-400"></i>
308
+ </div>
309
+ </div>
310
+ <p id="stat-matched" class="text-2xl font-bold text-white">0</p>
311
+ <p id="stat-matched-sub" class="text-[10px] text-green-400 mt-1">Awaiting data</p>
312
+ </div>
313
+ <div class="stat-card stat-card-orange glass-card rounded-xl p-4 relative overflow-hidden animate-slide-in" style="animation-delay:160ms">
314
+ <div class="flex items-center justify-between mb-2">
315
+ <span class="text-xs text-surface-400 font-medium">Unmatched</span>
316
+ <div class="w-8 h-8 rounded-lg bg-orange-600/20 flex items-center justify-center">
317
+ <i data-lucide="x-circle" class="w-4 h-4 text-orange-400"></i>
318
+ </div>
319
+ </div>
320
+ <p id="stat-unmatched" class="text-2xl font-bold text-white">0</p>
321
+ <p id="stat-unmatched-sub" class="text-[10px] text-orange-400 mt-1">Awaiting data</p>
322
+ </div>
323
+ <div class="stat-card stat-card-red glass-card rounded-xl p-4 relative overflow-hidden animate-slide-in" style="animation-delay:240ms">
324
+ <div class="flex items-center justify-between mb-2">
325
+ <span class="text-xs text-surface-400 font-medium">Anomalies</span>
326
+ <div class="w-8 h-8 rounded-lg bg-red-600/20 flex items-center justify-center">
327
+ <i data-lucide="alert-triangle" class="w-4 h-4 text-red-400"></i>
328
+ </div>
329
+ </div>
330
+ <p id="stat-anomalies" class="text-2xl font-bold text-white">0</p>
331
+ <p id="stat-anomalies-sub" class="text-[10px] text-red-400 mt-1">Awaiting data</p>
332
+ </div>
333
+ <div class="stat-card stat-card-cyan glass-card rounded-xl p-4 relative overflow-hidden animate-slide-in" style="animation-delay:320ms">
334
+ <div class="flex items-center justify-between mb-2">
335
+ <span class="text-xs text-surface-400 font-medium">Fraud Rings</span>
336
+ <div class="w-8 h-8 rounded-lg bg-cyan-600/20 flex items-center justify-center">
337
+ <i data-lucide="network" class="w-4 h-4 text-cyan-400"></i>
338
+ </div>
339
+ </div>
340
+ <p id="stat-fraud-rings" class="text-2xl font-bold text-white">0</p>
341
+ <p id="stat-fraud-rings-sub" class="text-[10px] text-cyan-400 mt-1">Awaiting data</p>
342
+ </div>
343
+ <div class="stat-card stat-card-pink glass-card rounded-xl p-4 relative overflow-hidden animate-slide-in" style="animation-delay:400ms">
344
+ <div class="flex items-center justify-between mb-2">
345
+ <span class="text-xs text-surface-400 font-medium">Risk Score</span>
346
+ <div class="w-8 h-8 rounded-lg bg-pink-600/20 flex items-center justify-center">
347
+ <i data-lucide="gauge" class="w-4 h-4 text-pink-400"></i>
348
+ </div>
349
+ </div>
350
+ <p id="stat-risk-score" class="text-2xl font-bold text-white">0.0<span class="text-sm text-surface-400">/10</span></p>
351
+ <p id="stat-risk-score-sub" class="text-[10px] text-pink-400 mt-1">Awaiting data</p>
352
+ </div>
353
+ </div>
354
+
355
+ <!-- Charts Row -->
356
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-4 lg:gap-6">
357
+ <div class="glass-card rounded-xl p-5 animate-slide-in" style="animation-delay:100ms">
358
+ <div class="flex items-center justify-between mb-4">
359
+ <h3 class="font-semibold text-white">Reconciliation Trend</h3>
360
+ </div>
361
+ <div class="relative w-full h-[250px]">
362
+ <canvas id="chart-recon-trend" class="w-full"></canvas>
363
+ </div>
364
+ </div>
365
+ <div class="glass-card rounded-xl p-5 animate-slide-in" style="animation-delay:200ms">
366
+ <div class="flex items-center justify-between mb-4">
367
+ <h3 class="font-semibold text-white">Anomaly Distribution</h3>
368
+ </div>
369
+ <div class="relative w-full h-[250px]">
370
+ <canvas id="chart-anomaly-dist" class="w-full"></canvas>
371
+ </div>
372
+ </div>
373
+ </div>
374
+
375
+ <!-- Match Confidence + Recent Alerts -->
376
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-4 lg:gap-6">
377
+ <div class="glass-card rounded-xl p-5 animate-slide-in" style="animation-delay:150ms">
378
+ <h3 class="font-semibold text-white mb-4">Match Confidence</h3>
379
+ <div class="relative w-full h-[250px]">
380
+ <canvas id="chart-confidence"></canvas>
381
+ </div>
382
+ </div>
383
+ <div class="lg:col-span-2 glass-card rounded-xl p-5 animate-slide-in" style="animation-delay:250ms">
384
+ <div class="flex items-center justify-between mb-4">
385
+ <h3 class="font-semibold text-white">Recent Alerts</h3>
386
+ <span class="text-xs bg-red-500/20 text-red-400 px-2 py-0.5 rounded-full font-medium"><span id="alerts-count">0</span> active</span>
387
+ </div>
388
+ <div id="recent-alerts" class="space-y-3 max-h-[280px] overflow-y-auto pr-2">
389
+ <p class="text-sm text-surface-400">No alerts yet.</p>
390
+ </div>
391
+ </div>
392
+ </div>
393
+ </section>
394
+
395
+ <!-- ==================== RECONCILIATION TAB ==================== -->
396
+ <section id="tab-reconciliation" class="tab-content hidden space-y-6">
397
+ <!-- Recon Stats -->
398
+ <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
399
+ <div class="glass-card rounded-xl p-4 flex items-center gap-4">
400
+ <div class="w-12 h-12 rounded-xl bg-green-600/20 flex items-center justify-center">
401
+ <i data-lucide="check-circle-2" class="w-6 h-6 text-green-400"></i>
402
+ </div>
403
+ <div>
404
+ <p id="recon-stat-exact" class="text-2xl font-bold text-white">0</p>
405
+ <p class="text-xs text-surface-400">Exact Matches</p>
406
+ </div>
407
+ </div>
408
+ <div class="glass-card rounded-xl p-4 flex items-center gap-4">
409
+ <div class="w-12 h-12 rounded-xl bg-brand-600/20 flex items-center justify-center">
410
+ <i data-lucide="fuzzy" class="w-6 h-6 text-brand-400"></i>
411
+ </div>
412
+ <div>
413
+ <p id="recon-stat-fuzzy" class="text-2xl font-bold text-white">0</p>
414
+ <p class="text-xs text-surface-400">Fuzzy Matches</p>
415
+ </div>
416
+ </div>
417
+ <div class="glass-card rounded-xl p-4 flex items-center gap-4">
418
+ <div class="w-12 h-12 rounded-xl bg-purple-600/20 flex items-center justify-center">
419
+ <i data-lucide="sparkles" class="w-6 h-6 text-purple-400"></i>
420
+ </div>
421
+ <div>
422
+ <p id="recon-stat-semantic" class="text-2xl font-bold text-white">0</p>
423
+ <p class="text-xs text-surface-400">AI Semantic</p>
424
+ </div>
425
+ </div>
426
+ <div class="glass-card rounded-xl p-4 flex items-center gap-4">
427
+ <div class="w-12 h-12 rounded-xl bg-red-600/20 flex items-center justify-center">
428
+ <i data-lucide="x-circle" class="w-6 h-6 text-red-400"></i>
429
+ </div>
430
+ <div>
431
+ <p id="recon-stat-unmatched" class="text-2xl font-bold text-white">0</p>
432
+ <p class="text-xs text-surface-400">Unmatched</p>
433
+ </div>
434
+ </div>
435
+ </div>
436
+
437
+ <!-- Matching Table -->
438
+ <div class="glass-card rounded-xl overflow-hidden">
439
+ <div class="flex items-center justify-between p-4 border-b border-surface-800/50">
440
+ <h3 class="font-semibold text-white">Reconciliation Results</h3>
441
+ <div class="flex items-center gap-2">
442
+ <select id="match-filter" onchange="filterReconTable()" class="bg-surface-800/50 border border-surface-700/50 rounded-lg text-xs px-3 py-1.5 text-surface-300 outline-none">
443
+ <option value="all">All Results</option>
444
+ <option value="matched">Matched</option>
445
+ <option value="fuzzy">Fuzzy Match</option>
446
+ <option value="unmatched">Unmatched</option>
447
+ </select>
448
+ <button class="flex items-center gap-1.5 bg-brand-600/20 text-brand-300 text-xs font-medium px-3 py-1.5 rounded-lg hover:bg-brand-600/30 transition">
449
+ <i data-lucide="refresh-cw" class="w-3 h-3"></i> Refresh
450
+ </button>
451
+ </div>
452
+ </div>
453
+ <div class="overflow-x-auto">
454
+ <table class="w-full text-sm data-table">
455
+ <thead>
456
+ <tr class="bg-surface-900/50 text-surface-400 text-xs uppercase tracking-wider">
457
+ <th class="px-4 py-3 text-left font-semibold">Books Entry</th>
458
+ <th class="px-4 py-3 text-left font-semibold">GST Entry</th>
459
+ <th class="px-4 py-3 text-left font-semibold">Amount</th>
460
+ <th class="px-4 py-3 text-left font-semibold">Match Type</th>
461
+ <th class="px-4 py-3 text-left font-semibold">Confidence</th>
462
+ <th class="px-4 py-3 text-left font-semibold">Status</th>
463
+ </tr>
464
+ </thead>
465
+ <tbody id="recon-table-body">
466
+ </tbody>
467
+ </table>
468
+ </div>
469
+ <div class="flex items-center justify-between p-4 border-t border-surface-800/50">
470
+ <p class="text-xs text-surface-400">Showing <span id="showing-count" id="showing-count">--</span> of <span id="total-count" id="total-count">--</span> results</p>
471
+ <div class="flex items-center gap-1">
472
+ <button class="px-3 py-1 rounded-md bg-surface-800/50 text-surface-400 text-xs hover:bg-surface-700 transition">Prev</button>
473
+ <button class="px-3 py-1 rounded-md bg-brand-600 text-white text-xs">1</button>
474
+ <button class="px-3 py-1 rounded-md bg-surface-800/50 text-surface-400 text-xs hover:bg-surface-700 transition">2</button>
475
+ <button class="px-3 py-1 rounded-md bg-surface-800/50 text-surface-400 text-xs hover:bg-surface-700 transition">3</button>
476
+ <button class="px-3 py-1 rounded-md bg-surface-800/50 text-surface-400 text-xs hover:bg-surface-700 transition">Next</button>
477
+ </div>
478
+ </div>
479
+ </div>
480
+ </section>
481
+
482
+ <!-- ==================== ANOMALY TAB ==================== -->
483
+ <section id="tab-anomaly" class="tab-content hidden space-y-6">
484
+ <!-- Anomaly Stats -->
485
+ <div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
486
+ <div class="glass-card rounded-xl p-5">
487
+ <div class="flex items-center gap-3 mb-3">
488
+ <div class="w-10 h-10 rounded-lg bg-red-600/20 flex items-center justify-center">
489
+ <i data-lucide="shield-alert" class="w-5 h-5 text-red-400"></i>
490
+ </div>
491
+ <div>
492
+ <p class="text-lg font-bold text-white">Isolation Forest</p>
493
+ <p class="text-[10px] text-surface-400 font-mono">contamination=0.05</p>
494
+ </div>
495
+ </div>
496
+ <p id="anomaly-trained-sub" class="text-xs text-surface-400 mb-2">Anomaly detection model pending execution.</p>
497
+ <div class="flex items-center gap-2">
498
+ <span class="px-2 py-0.5 bg-green-500/20 text-green-400 text-[10px] font-semibold rounded-full">ACTIVE</span>
499
+ <span class="px-2 py-0.5 bg-brand-500/20 text-brand-400 text-[10px] font-semibold rounded-full">v2.4</span>
500
+ </div>
501
+ </div>
502
+ <div class="glass-card rounded-xl p-5">
503
+ <div class="flex items-center gap-3 mb-3">
504
+ <div class="w-10 h-10 rounded-lg bg-orange-600/20 flex items-center justify-center">
505
+ <i data-lucide="radar" class="w-5 h-5 text-orange-400"></i>
506
+ </div>
507
+ <div>
508
+ <p class="text-lg font-bold text-white">Score Range</p>
509
+ <p id="anomaly-score-range" class="text-[10px] text-surface-400 font-mono">--</p>
510
+ </div>
511
+ </div>
512
+ <div class="space-y-2">
513
+ <div class="flex items-center justify-between text-xs">
514
+ <span class="text-surface-400">Critical (> 0.3)</span>
515
+ <span id="anomaly-crit-count" class="text-red-400 font-mono">0</span>
516
+ </div>
517
+ <div class="w-full bg-surface-800 rounded-full h-1.5"><div id="anomaly-crit-bar" class="bg-red-500 h-1.5 rounded-full" style="width:0%"></div></div>
518
+ <div class="flex items-center justify-between text-xs">
519
+ <span class="text-surface-400">High (0.1 to 0.3)</span>
520
+ <span id="anomaly-high-count" class="text-orange-400 font-mono">0</span>
521
+ </div>
522
+ <div class="w-full bg-surface-800 rounded-full h-1.5"><div id="anomaly-high-bar" class="bg-orange-500 h-1.5 rounded-full" style="width:0%"></div></div>
523
+ <div class="flex items-center justify-between text-xs">
524
+ <span class="text-surface-400">Medium (< 0.1)</span>
525
+ <span id="anomaly-med-count" class="text-yellow-400 font-mono">0</span>
526
+ </div>
527
+ <div class="w-full bg-surface-800 rounded-full h-1.5"><div id="anomaly-med-bar" class="bg-yellow-500 h-1.5 rounded-full" style="width:0%"></div></div>
528
+ </div>
529
+ </div>
530
+ <div class="glass-card rounded-xl p-5">
531
+ <div class="flex items-center gap-3 mb-3">
532
+ <div class="w-10 h-10 rounded-lg bg-brand-600/20 flex items-center justify-center">
533
+ <i data-lucide="brain" class="w-5 h-5 text-brand-400"></i>
534
+ </div>
535
+ <div>
536
+ <p class="text-lg font-bold text-white">AI LLM Analysis</p>
537
+ <p class="text-[10px] text-surface-400 font-mono">ReconAI</p>
538
+ </div>
539
+ </div>
540
+ <p class="text-xs text-surface-400 mb-3">Natural language explanations generated for each anomaly.</p>
541
+ <p class="text-xs text-brand-300 bg-brand-900/20 p-2 rounded border border-brand-500/20">Click "View" on any flagged transaction below to generate a real-time audit explanation.</p>
542
+ </div>
543
+ </div>
544
+
545
+ <!-- Anomaly Table -->
546
+ <div class="glass-card rounded-xl overflow-hidden">
547
+ <div class="flex items-center justify-between p-4 border-b border-surface-800/50">
548
+ <h3 class="font-semibold text-white flex items-center gap-2">
549
+ <i data-lucide="alert-triangle" class="w-4 h-4 text-red-400"></i>
550
+ Flagged Transactions
551
+ </h3>
552
+ <div class="flex items-center gap-2">
553
+ <button class="text-xs px-3 py-1.5 rounded-lg bg-red-600/20 text-red-400 hover:bg-red-600/30 transition font-medium">Critical (<span id="filter-crit">0</span>)</button>
554
+ <button class="text-xs px-3 py-1.5 rounded-lg bg-surface-800/50 text-surface-300 hover:bg-surface-700/50 transition">All (<span id="filter-all">0</span>)</button>
555
+ </div>
556
+ </div>
557
+ <div class="overflow-x-auto">
558
+ <table class="w-full text-sm data-table">
559
+ <thead>
560
+ <tr class="bg-surface-900/50 text-surface-400 text-xs uppercase tracking-wider">
561
+ <th class="px-4 py-3 text-left font-semibold">Invoice</th>
562
+ <th class="px-4 py-3 text-left font-semibold">Vendor</th>
563
+ <th class="px-4 py-3 text-left font-semibold">Amount</th>
564
+ <th class="px-4 py-3 text-left font-semibold">Anomaly Score</th>
565
+ <th class="px-4 py-3 text-left font-semibold">Risk Level</th>
566
+ <th class="px-4 py-3 text-left font-semibold">AI Explanation</th>
567
+ </tr>
568
+ </thead>
569
+ <tbody id="anomaly-table-body">
570
+ </tbody>
571
+ </table>
572
+ </div>
573
+ </div>
574
+ </section>
575
+
576
+ <!-- ==================== FRAUD NETWORK TAB ==================== -->
577
+ <section id="tab-fraud" class="tab-content hidden space-y-6">
578
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
579
+ <!-- Network Graph -->
580
+ <div class="lg:col-span-2 glass-card rounded-xl overflow-hidden">
581
+ <div class="flex items-center justify-between p-4 border-b border-surface-800/50">
582
+ <h3 class="font-semibold text-white flex items-center gap-2">
583
+ <i data-lucide="network" class="w-4 h-4 text-cyan-400"></i>
584
+ Circular Trading Network
585
+ </h3>
586
+ <div class="flex items-center gap-2">
587
+ </div>
588
+ </div>
589
+ <div class="relative">
590
+ <canvas id="fraud-canvas" width="800" height="500" class="w-full bg-surface-950/50"></canvas>
591
+ <div class="absolute top-3 left-3 bg-surface-900/80 backdrop-blur-sm border border-surface-700/30 rounded-lg p-3 text-xs space-y-2">
592
+ <div class="flex items-center gap-2"><span class="w-3 h-3 rounded-full bg-red-500"></span> Critical Node</div>
593
+ <div class="flex items-center gap-2"><span class="w-3 h-3 rounded-full bg-orange-500"></span> Suspicious</div>
594
+ <div class="flex items-center gap-2"><span class="w-3 h-3 rounded-full bg-brand-500"></span> Known Entity</div>
595
+ <div class="flex items-center gap-2"><span class="w-3 h-3 rounded-full bg-surface-500"></span> Linked</div>
596
+ </div>
597
+ </div>
598
+ </div>
599
+
600
+ <!-- Fraud Details -->
601
+ <div class="space-y-4">
602
+ <div class="glass-card rounded-xl p-5">
603
+ <h3 class="font-semibold text-white mb-3 flex items-center gap-2">
604
+ <i data-lucide="shield-alert" class="w-4 h-4 text-red-400"></i>
605
+ Detected Rings
606
+ </h3>
607
+ <div class="space-y-3" id="fraud-rings-list">
608
+ <p class="text-sm text-surface-400">No fraud rings detected yet. Run Engine to analyze.</p>
609
+ </div>
610
+ </div>
611
+ <div class="glass-card rounded-xl p-5">
612
+ <h3 class="font-semibold text-white mb-3 flex items-center gap-2">
613
+ <i data-lucide="bar-chart-3" class="w-4 h-4 text-brand-400"></i>
614
+ Network Stats
615
+ </h3>
616
+ <div class="space-y-3">
617
+ <div class="flex items-center justify-between">
618
+ <span class="text-xs text-surface-400">Total Nodes</span>
619
+ <span id="fraud-nodes" class="text-sm font-mono text-white">0</span>
620
+ </div>
621
+ <div class="flex items-center justify-between">
622
+ <span class="text-xs text-surface-400">Total Edges</span>
623
+ <span id="fraud-edges" class="text-sm font-mono text-white">0</span>
624
+ </div>
625
+ </div>
626
+ </div>
627
+ </div>
628
+ </div>
629
+ </section>
630
+
631
+ <!-- ==================== AI EXPLANATIONS TAB ==================== -->
632
+ <section id="tab-ai-explain" class="tab-content hidden space-y-6">
633
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
634
+ <!-- LLM Queries -->
635
+ <div class="space-y-4">
636
+ <div class="glass-card rounded-xl p-5">
637
+ <h3 class="font-semibold text-white mb-4 flex items-center gap-2">
638
+ <i data-lucide="message-square-text" class="w-4 h-4 text-brand-400"></i>
639
+ Ask the AI Auditor
640
+ </h3>
641
+ <div class="space-y-3 mb-4">
642
+ <div id="chat-messages" class="space-y-3 max-h-[400px] overflow-y-auto pr-1">
643
+ <div class="flex gap-3">
644
+ <div class="w-7 h-7 rounded-full bg-brand-600/30 flex items-center justify-center shrink-0">
645
+ <i data-lucide="bot" class="w-3.5 h-3.5 text-brand-400"></i>
646
+ </div>
647
+ <div class="glass-card rounded-lg rounded-tl-none p-3 max-w-[90%]">
648
+ <p class="text-sm text-surface-200">Welcome! I'm your AI audit assistant powered by ReconAI. Ask me about any discrepancy, anomaly, or vendor relationship and I'll provide a detailed explanation.</p>
649
+ <p class="text-[10px] text-surface-500 mt-2 font-mono">model: ReconAI-instruct</p>
650
+ </div>
651
+ </div>
652
+ </div>
653
+ </div>
654
+ <div class="flex items-center gap-2">
655
+ <input id="chat-input" type="text" placeholder="Ask about a discrepancy..." class="flex-1 bg-surface-900/50 border border-surface-700/50 rounded-lg px-3 py-2 text-sm text-white placeholder-surface-500 outline-none focus:border-brand-500/50 focus:ring-1 focus:ring-brand-500/25 transition" onkeydown="if(event.key==='Enter')sendChatMessage()">
656
+ <button onclick="sendChatMessage()" class="p-2 bg-brand-600 hover:bg-brand-500 rounded-lg transition">
657
+ <i data-lucide="send" class="w-4 h-4 text-white"></i>
658
+ </button>
659
+ </div>
660
+ <div class="flex items-center gap-2 mt-3 flex-wrap">
661
+ <button onclick="askSuggested('Explain the highest risk anomaly detected in this batch')" class="text-[10px] px-2 py-1 rounded-md bg-surface-800/50 text-surface-400 hover:text-white hover:bg-surface-700 transition">Explain highest risk anomaly</button>
662
+ <button onclick="askSuggested('Summarize the top reasons for discrepancies')" class="text-[10px] px-2 py-1 rounded-md bg-surface-800/50 text-surface-400 hover:text-white hover:bg-surface-700 transition">Summarize discrepancies</button>
663
+ </div>
664
+ </div>
665
+ </div>
666
+
667
+ <!-- ReconAI List -->
668
+ <div class="glass-card rounded-xl p-5">
669
+ <h3 class="font-semibold text-white mb-4 flex items-center gap-2">
670
+ <i data-lucide="sparkles" class="w-4 h-4 text-purple-400"></i>
671
+ Generated Explanations
672
+ </h3>
673
+ <div class="space-y-3 max-h-[530px] overflow-y-auto pr-1" id="explanations-list">
674
+ </div>
675
+ </div>
676
+ </div>
677
+ </section>
678
+
679
+ <!-- ==================== VECTOR MEMORY TAB ==================== -->
680
+ <section id="tab-vector" class="tab-content hidden space-y-6">
681
+ <div class="flex justify-center">
682
+ <div class="glass-card rounded-xl p-5 w-full max-w-md">
683
+ <div class="flex items-center gap-3 mb-4">
684
+ <div class="w-10 h-10 rounded-lg bg-brand-600/20 flex items-center justify-center">
685
+ <i data-lucide="database" class="w-5 h-5 text-brand-400"></i>
686
+ </div>
687
+ <div>
688
+ <p class="font-semibold text-white">FAISS Index</p>
689
+ <p class="text-[10px] text-surface-400 font-mono">L2 • 384-dim • FlatL2</p>
690
+ </div>
691
+ </div>
692
+ <div class="space-y-3">
693
+ <div>
694
+ <div class="flex items-center justify-between text-xs mb-1">
695
+ <span class="text-surface-400">Index Size</span>
696
+ <span id="faiss-vectors" class="text-white font-mono">-- vectors</span>
697
+ </div>
698
+ </div>
699
+ <div>
700
+ <div class="flex items-center justify-between text-xs mb-1">
701
+ <span class="text-surface-400">Memory Used</span>
702
+ <span id="faiss-memory" class="text-white font-mono">-- MB</span>
703
+ </div>
704
+ </div>
705
+ </div>
706
+ </div>
707
+ </div>
708
+ </section>
709
+
710
+ </div>
711
+
712
+ <!-- Processing Modal -->
713
+ <div id="process-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
714
+ <div class="glass-card rounded-2xl p-8 max-w-md w-full mx-4 text-center">
715
+ <div class="w-16 h-16 rounded-full bg-brand-600/20 flex items-center justify-center mx-auto mb-4 animate-pulse-glow">
716
+ <i data-lucide="brain" class="w-8 h-8 text-brand-400"></i>
717
+ </div>
718
+ <h3 class="text-lg font-semibold text-white mb-2">Running Reconciliation</h3>
719
+ <p class="text-sm text-surface-400 mb-4" id="process-step">Initializing engine...</p>
720
+ <div class="w-full bg-surface-800 rounded-full h-2 overflow-hidden">
721
+ <div id="process-bar" class="progress-bar h-2 rounded-full transition-all duration-500" style="width: 0%"></div>
722
+ </div>
723
+ <p class="text-xs text-surface-500 mt-3 font-mono" id="process-pct">0%</p>
724
+ </div>
725
+ </div>
726
+
727
+ <!-- Toast Container -->
728
+ <div id="toast-container" class="fixed bottom-4 right-4 z-50 space-y-2"></div>
729
+ </main>
730
+ </div>
731
+
732
+ <script>
733
+ // All static dummy data arrays removed — UI is 100% backend-driven
734
+
735
+
736
+
737
+ // ============================================
738
+ // TAB NAVIGATION
739
+ // ============================================
740
+ const titles = {
741
+ dashboard: ['Dashboard', 'Financial reconciliation overview'],
742
+ reconciliation: ['Reconciliation', 'Intelligent matching with Fuzzy + AI semantic analysis'],
743
+ anomaly: ['Anomaly Detection', 'IsolationForest-powered anomaly detection'],
744
+ fraud: ['Fraud Network', 'NetworkX circular trading visualization'],
745
+ 'ai-explain': ['ReconAI', 'ReconAI LLM-powered audit commentary'],
746
+ vector: ['Vector Memory', 'FAISS persistent vector index management']
747
+ };
748
+
749
+ function switchTab(tab) {
750
+ document.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden'));
751
+ document.getElementById('tab-' + tab).classList.remove('hidden');
752
+ document.querySelectorAll('.sidebar-link').forEach(el => {
753
+ el.classList.toggle('active', el.dataset.nav === tab);
754
+ });
755
+ if (titles[tab]) {
756
+ document.getElementById('page-title').textContent = titles[tab][0];
757
+ document.getElementById('page-subtitle').textContent = titles[tab][1];
758
+ }
759
+ // Close mobile sidebar
760
+ const sidebar = document.getElementById('sidebar');
761
+ const overlay = document.getElementById('sidebar-overlay');
762
+ sidebar.classList.add('-translate-x-full');
763
+ overlay.classList.add('hidden');
764
+
765
+ // Initialize fraud graph if switching to fraud tab
766
+ if (tab === 'fraud') {
767
+ setTimeout(drawFraudNetwork, 100);
768
+ }
769
+ }
770
+
771
+ function toggleSidebar() {
772
+ const sidebar = document.getElementById('sidebar');
773
+ const overlay = document.getElementById('sidebar-overlay');
774
+ sidebar.classList.toggle('-translate-x-full');
775
+ overlay.classList.toggle('hidden');
776
+ }
777
+
778
+ let isDark = true;
779
+ function toggleTheme() {
780
+ isDark = !isDark;
781
+ document.documentElement.classList.toggle('dark', isDark);
782
+ }
783
+
784
+ // Counter animation removed — no more data-counter attributes
785
+
786
+ async function showExplanation(invoiceId) {
787
+ const anomaly = window.anomalyDataMap ? window.anomalyDataMap[invoiceId] : null;
788
+ if (!anomaly) return;
789
+
790
+ switchTab('ai-explain');
791
+
792
+ // Add user message
793
+ addChatMessage(`Explain the anomaly for invoice ${invoiceId} and vendor ${anomaly.VendorName_books}`, false);
794
+
795
+ // Add loading state
796
+ const chatMessages = document.getElementById('chat-messages');
797
+ const loadingDiv = document.createElement('div');
798
+ loadingDiv.id = 'chat-loading';
799
+ loadingDiv.className = 'flex gap-3 animate-slide-in';
800
+ loadingDiv.innerHTML = `
801
+ <div class="w-7 h-7 rounded-full bg-brand-600/30 flex items-center justify-center shrink-0">
802
+ <i data-lucide="bot" class="w-3.5 h-3.5 text-brand-400"></i>
803
+ </div>
804
+ <div class="glass-card rounded-lg rounded-tl-none p-3 max-w-[90%]">
805
+ <p class="text-sm text-surface-400 flex items-center gap-2"><i data-lucide="loader-2" class="w-3.5 h-3.5 animate-spin"></i> Analyzing with ReconAI...</p>
806
+ </div>
807
+ `;
808
+ chatMessages.appendChild(loadingDiv);
809
+ lucide.createIcons();
810
+
811
+ try {
812
+ const res = await fetch('/api/explain', {
813
+ method: 'POST',
814
+ headers: { 'Content-Type': 'application/json' },
815
+ body: JSON.stringify({
816
+ row: anomaly,
817
+ match_status: anomaly.MatchStatus,
818
+ b_vendor: anomaly.VendorName_books,
819
+ g_vendor: anomaly.VendorName_gst,
820
+ b_amount: anomaly.Amount_books,
821
+ g_amount: anomaly.Amount_gst
822
+ })
823
+ });
824
+
825
+ const result = await res.json();
826
+ document.getElementById('chat-loading')?.remove();
827
+
828
+ if (result.explanation) {
829
+ addChatMessage(result.explanation, true);
830
+
831
+ const expList = document.getElementById('explanations-list');
832
+ if (expList) {
833
+ const div = document.createElement('div');
834
+ div.className = 'p-3 rounded-lg border border-surface-700/50 bg-surface-900/50 text-sm text-surface-200 animate-slide-in';
835
+ div.innerHTML = `
836
+ <div class="flex items-center justify-between mb-2 pb-2 border-b border-surface-800">
837
+ <span class="text-xs font-semibold text-brand-400">Invoice ${invoiceId}</span>
838
+ <span class="text-[10px] text-surface-500 font-mono">ReconAI</span>
839
+ </div>
840
+ <p class="text-xs">${result.explanation}</p>
841
+ `;
842
+ expList.prepend(div);
843
+ }
844
+ } else {
845
+ addChatMessage('Error generating explanation.', true);
846
+ }
847
+ } catch (err) {
848
+ document.getElementById('chat-loading')?.remove();
849
+ addChatMessage('API Error: ' + err.message, true);
850
+ }
851
+ }
852
+ function addChatMessage(text, isAI = false) {
853
+ const chatMessages = document.getElementById('chat-messages');
854
+ const msgDiv = document.createElement('div');
855
+ msgDiv.className = 'flex gap-3 animate-slide-in';
856
+ if (isAI) {
857
+ msgDiv.innerHTML = `
858
+ <div class="w-7 h-7 rounded-full bg-brand-600/30 flex items-center justify-center shrink-0">
859
+ <i data-lucide="bot" class="w-3.5 h-3.5 text-brand-400"></i>
860
+ </div>
861
+ <div class="glass-card rounded-lg rounded-tl-none p-3 max-w-[90%]">
862
+ <p class="text-sm text-surface-200">${text}</p>
863
+ <p class="text-[10px] text-surface-500 mt-2 font-mono">model: ReconAI-instruct</p>
864
+ </div>
865
+ `;
866
+ } else {
867
+ msgDiv.innerHTML = `
868
+ <div class="w-7 h-7 rounded-full bg-purple-600/30 flex items-center justify-center shrink-0">
869
+ <i data-lucide="user" class="w-3.5 h-3.5 text-purple-400"></i>
870
+ </div>
871
+ <div class="glass-card rounded-lg rounded-tl-none p-3 max-w-[90%]">
872
+ <p class="text-sm text-surface-200">${text}</p>
873
+ </div>
874
+ `;
875
+ }
876
+ chatMessages.appendChild(msgDiv);
877
+ chatMessages.scrollTop = chatMessages.scrollHeight;
878
+ lucide.createIcons();
879
+ }
880
+
881
+ async function sendChatMessage() {
882
+ const input = document.getElementById('chat-input');
883
+ const text = input.value.trim();
884
+ if (!text) return;
885
+ addChatMessage(text, false);
886
+ input.value = '';
887
+
888
+ // Send to live backend
889
+ try {
890
+ const res = await fetch('/api/explain', {
891
+ method: 'POST',
892
+ headers: { 'Content-Type': 'application/json' },
893
+ body: JSON.stringify({ row: {}, match_status: text, b_vendor: 'N/A', g_vendor: 'N/A', b_amount: 0, g_amount: 0 })
894
+ });
895
+ const result = await res.json();
896
+ addChatMessage(result.explanation || 'No response from AI.', true);
897
+ } catch (err) {
898
+ addChatMessage('API Error: ' + err.message, true);
899
+ }
900
+ }
901
+
902
+ function askSuggested(text) {
903
+ document.getElementById('chat-input').value = text;
904
+ sendChatMessage();
905
+ }
906
+
907
+ // ============================================
908
+ // FRAUD NETWORK CANVAS
909
+ // ============================================
910
+ function drawFraudNetwork() {
911
+ const canvas = document.getElementById('fraud-canvas');
912
+ if (!canvas) return;
913
+ const ctx = canvas.getContext('2d');
914
+ const dpr = window.devicePixelRatio || 1;
915
+ const rect = canvas.getBoundingClientRect();
916
+ canvas.width = rect.width * dpr;
917
+ canvas.height = rect.height * dpr;
918
+ ctx.scale(dpr, dpr);
919
+ const W = rect.width;
920
+ const H = rect.height;
921
+ ctx.clearRect(0, 0, W, H);
922
+
923
+ if (!window.fraudNetworkData || !window.fraudNetworkData.nodes || window.fraudNetworkData.nodes.length === 0) {
924
+ ctx.font = '14px Inter';
925
+ ctx.fillStyle = '#64748b';
926
+ ctx.textAlign = 'center';
927
+ ctx.fillText('No fraud network data yet.', W/2, H/2 - 10);
928
+ ctx.font = '12px Inter';
929
+ ctx.fillText('Upload CSVs and Run Engine to analyze transaction networks.', W/2, H/2 + 15);
930
+ return;
931
+ }
932
+
933
+ const nodes = window.fraudNetworkData.nodes;
934
+ const edges = window.fraudNetworkData.edges;
935
+ const cycles = window.fraudNetworkData.cycles || [];
936
+
937
+ // Assign positions in a circle for better layout
938
+ const centerX = W / 2;
939
+ const centerY = H / 2;
940
+ const radius = Math.min(W, H) / 2 - 40;
941
+
942
+ nodes.forEach((node, i) => {
943
+ if (!node.x) {
944
+ const angle = (i / nodes.length) * 2 * Math.PI;
945
+ node.x = centerX + radius * Math.cos(angle);
946
+ node.y = centerY + radius * Math.sin(angle);
947
+ }
948
+ });
949
+
950
+ // Draw edges
951
+ edges.forEach(edge => {
952
+ const from = nodes[edge.from];
953
+ const to = nodes[edge.to];
954
+ if (!from || !to) return;
955
+
956
+ // check if edge is part of a cycle
957
+ let inCycle = false;
958
+ for (let c of cycles) {
959
+ const idx1 = c.indexOf(from.id);
960
+ const idx2 = c.indexOf(to.id);
961
+ if (idx1 !== -1 && idx2 !== -1) {
962
+ if ((idx1 + 1) % c.length === idx2) {
963
+ inCycle = true;
964
+ break;
965
+ }
966
+ }
967
+ }
968
+
969
+ ctx.beginPath();
970
+ ctx.moveTo(from.x, from.y);
971
+ ctx.lineTo(to.x, to.y);
972
+ ctx.strokeStyle = inCycle ? '#ef4444' : '#64748b66';
973
+ ctx.lineWidth = inCycle ? 2 : 1;
974
+ ctx.stroke();
975
+
976
+ // arrow head
977
+ const angle = Math.atan2(to.y - from.y, to.x - from.x);
978
+ const midX = from.x + (to.x - from.x) * 0.7; // Closer to target
979
+ const midY = from.y + (to.y - from.y) * 0.7;
980
+ ctx.beginPath();
981
+ ctx.moveTo(midX + 6 * Math.cos(angle), midY + 6 * Math.sin(angle));
982
+ ctx.lineTo(midX - 6 * Math.cos(angle) + 4 * Math.cos(angle + Math.PI/2), midY - 6 * Math.sin(angle) + 4 * Math.sin(angle + Math.PI/2));
983
+ ctx.lineTo(midX - 6 * Math.cos(angle) - 4 * Math.cos(angle + Math.PI/2), midY - 6 * Math.sin(angle) - 4 * Math.sin(angle + Math.PI/2));
984
+ ctx.fillStyle = inCycle ? '#ef4444' : '#64748b66';
985
+ ctx.fill();
986
+ });
987
+
988
+ // Draw nodes
989
+ nodes.forEach(node => {
990
+ let inCycle = cycles.some(c => c.includes(node.id));
991
+ const color = inCycle ? '#ef4444' : '#64748b';
992
+
993
+ ctx.beginPath();
994
+ ctx.arc(node.x, node.y, 8, 0, Math.PI * 2);
995
+ ctx.fillStyle = color;
996
+ ctx.fill();
997
+
998
+ ctx.font = '11px Inter';
999
+ ctx.fillStyle = '#e2e8f0';
1000
+ ctx.textAlign = 'center';
1001
+ ctx.fillText(node.label, node.x, node.y + 20);
1002
+ });
1003
+ }
1004
+
1005
+ // ============================================
1006
+ // CHARTS
1007
+ // ============================================
1008
+ function initCharts() {
1009
+ // Reconciliation Trend
1010
+ const reconCtx = document.getElementById('chart-recon-trend');
1011
+ if (reconCtx) {
1012
+ new Chart(reconCtx, {
1013
+ type: 'line',
1014
+ data: {
1015
+ labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
1016
+ datasets: [
1017
+ {
1018
+ label: 'Matched',
1019
+ data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
1020
+ borderColor: '#22c55e',
1021
+ backgroundColor: 'rgba(34,197,94,0.1)',
1022
+ fill: true,
1023
+ tension: 0.4,
1024
+ },
1025
+ {
1026
+ label: 'Discrepancies',
1027
+ data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
1028
+ borderColor: '#ef4444',
1029
+ backgroundColor: 'rgba(239,68,68,0.1)',
1030
+ fill: true,
1031
+ tension: 0.4,
1032
+ }
1033
+ ]
1034
+ },
1035
+ options: {
1036
+ responsive: true,
1037
+ maintainAspectRatio: false,
1038
+ plugins: {
1039
+ legend: { labels: { color: '#94a3b8', font: { size: 11 } } }
1040
+ },
1041
+ scales: {
1042
+ x: { grid: { color: 'rgba(148,163,184,0.08)' }, ticks: { color: '#64748b', font: { size: 10 } } },
1043
+ y: { grid: { color: 'rgba(148,163,184,0.08)' }, ticks: { color: '#64748b', font: { size: 10 } } }
1044
+ }
1045
+ }
1046
+ });
1047
+ }
1048
+
1049
+ // Anomaly Distribution
1050
+ const anomalyCtx = document.getElementById('chart-anomaly-dist');
1051
+ if (anomalyCtx) {
1052
+ new Chart(anomalyCtx, {
1053
+ type: 'bar',
1054
+ data: {
1055
+ labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
1056
+ datasets: [
1057
+ {
1058
+ label: 'Critical',
1059
+ data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
1060
+ backgroundColor: '#ef4444',
1061
+ borderRadius: 4,
1062
+ },
1063
+ {
1064
+ label: 'High',
1065
+ data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
1066
+ backgroundColor: '#f97316',
1067
+ borderRadius: 4,
1068
+ },
1069
+ {
1070
+ label: 'Medium',
1071
+ data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
1072
+ backgroundColor: '#eab308',
1073
+ borderRadius: 4,
1074
+ }
1075
+ ]
1076
+ },
1077
+ options: {
1078
+ responsive: true,
1079
+ maintainAspectRatio: false,
1080
+ plugins: {
1081
+ legend: { labels: { color: '#94a3b8', font: { size: 11 } } }
1082
+ },
1083
+ scales: {
1084
+ x: { stacked: true, grid: { color: 'rgba(148,163,184,0.08)' }, ticks: { color: '#64748b', font: { size: 10 } } },
1085
+ y: { stacked: true, grid: { color: 'rgba(148,163,184,0.08)' }, ticks: { color: '#64748b', font: { size: 10 } } }
1086
+ }
1087
+ }
1088
+ });
1089
+ }
1090
+
1091
+ // Match Confidence (Doughnut)
1092
+ const confCtx = document.getElementById('chart-confidence');
1093
+ if (confCtx) {
1094
+ new Chart(confCtx, {
1095
+ type: 'doughnut',
1096
+ data: {
1097
+ labels: ['Exact Match', 'Fuzzy Match', 'AI Semantic', 'Unmatched'],
1098
+ datasets: [{
1099
+ data: [0, 0, 0, 0],
1100
+ backgroundColor: ['#22c55e', '#eab308', '#6366f1', '#ef4444'],
1101
+ borderWidth: 0,
1102
+ hoverOffset: 8,
1103
+ }]
1104
+ },
1105
+ options: {
1106
+ responsive: true,
1107
+ maintainAspectRatio: false,
1108
+ cutout: '65%',
1109
+ plugins: {
1110
+ legend: {
1111
+ display: false
1112
+ }
1113
+ }
1114
+ }
1115
+ });
1116
+ }
1117
+ }
1118
+
1119
+ function updateFileLabel(type) {
1120
+ const fileInput = document.getElementById(`${type}-file`);
1121
+ const label = document.getElementById(`lbl-${type}`);
1122
+ if (fileInput.files.length > 0) {
1123
+ label.textContent = fileInput.files[0].name;
1124
+ label.classList.remove('hidden');
1125
+ label.classList.add('inline');
1126
+ }
1127
+ }
1128
+
1129
+ async function runReconciliation() {
1130
+ const booksFile = document.getElementById('books-file').files[0];
1131
+ const gstFile = document.getElementById('gst-file').files[0];
1132
+
1133
+ if (!booksFile || !gstFile) {
1134
+ showToast('Please select both Books and GST CSV files first.', 'error');
1135
+ return;
1136
+ }
1137
+
1138
+ const formData = new FormData();
1139
+ formData.append('books', booksFile);
1140
+ formData.append('gst', gstFile);
1141
+
1142
+ await executeBackendCall('/api/reconcile', formData, 'Reconciling data...');
1143
+ }
1144
+
1145
+ async function fetchLiveGst() {
1146
+ const booksFile = document.getElementById('books-file').files[0];
1147
+
1148
+ if (!booksFile) {
1149
+ showToast('Please select Books CSV file first.', 'error');
1150
+ return;
1151
+ }
1152
+
1153
+ const formData = new FormData();
1154
+ formData.append('books', booksFile);
1155
+
1156
+ await executeBackendCall('/api/fetch_live', formData, 'Fetching live GST and reconciling...');
1157
+ }
1158
+
1159
+ async function executeBackendCall(endpoint, formData, startMsg) {
1160
+ const modal = document.getElementById('process-modal');
1161
+ const bar = document.getElementById('process-bar');
1162
+ const step = document.getElementById('process-step');
1163
+ const pct = document.getElementById('process-pct');
1164
+
1165
+ modal.classList.remove('hidden');
1166
+ bar.style.width = '30%';
1167
+ step.textContent = startMsg;
1168
+ pct.textContent = '30%';
1169
+
1170
+ try {
1171
+ const response = await fetch(endpoint, {
1172
+ method: 'POST',
1173
+ body: formData
1174
+ });
1175
+
1176
+ if (!response.ok) throw new Error('API request failed');
1177
+ const data = await response.json();
1178
+
1179
+ if (data.error) throw new Error(data.error);
1180
+
1181
+ bar.style.width = '100%';
1182
+ step.textContent = 'Complete!';
1183
+ pct.textContent = '100%';
1184
+
1185
+ updateDashboardWithRealData(data);
1186
+
1187
+ setTimeout(() => {
1188
+ modal.classList.add('hidden');
1189
+ showToast('Process complete! ' + data.summary.exact + ' matched.', 'success');
1190
+ }, 600);
1191
+ } catch (err) {
1192
+ modal.classList.add('hidden');
1193
+ showToast('Error: ' + err.message, 'error');
1194
+ console.error(err);
1195
+ }
1196
+ }
1197
+
1198
+ function updateDashboardWithRealData(data) {
1199
+ // Update Dashboard Stats
1200
+ document.getElementById('stat-total-records').textContent = data.summary.total_books;
1201
+ document.getElementById('stat-matched').textContent = data.summary.exact;
1202
+ document.getElementById('stat-unmatched').textContent = data.summary.unmatched;
1203
+ document.getElementById('stat-anomalies').textContent = data.summary.anomalies;
1204
+
1205
+ // Calculate percentages
1206
+ const total = data.summary.total_books || 1;
1207
+ const matchRate = ((data.summary.exact / total) * 100).toFixed(1);
1208
+ const unmatchedRate = ((data.summary.unmatched / total) * 100).toFixed(1);
1209
+ const anomalyRate = ((data.summary.anomalies / total) * 100).toFixed(1);
1210
+
1211
+ document.getElementById('stat-total-records-sub').textContent = 'Live data loaded';
1212
+ document.getElementById('stat-matched-sub').textContent = `${matchRate}% match rate`;
1213
+ document.getElementById('stat-unmatched-sub').textContent = `${unmatchedRate}% discrepancy`;
1214
+ document.getElementById('stat-anomalies-sub').textContent = `${anomalyRate}% contamination`;
1215
+
1216
+ // For Risk and Fraud, we default to 0 since we're replacing dummy data
1217
+ document.getElementById('stat-fraud-rings').textContent = '0';
1218
+ document.getElementById('stat-fraud-rings-sub').textContent = 'No rings detected';
1219
+ document.getElementById('stat-risk-score').innerHTML = '0.0<span class="text-sm text-surface-400">/10</span>';
1220
+ document.getElementById('stat-risk-score-sub').textContent = 'Low risk level';
1221
+
1222
+ // Update Anomaly Tab Stats
1223
+ if(document.getElementById('anomaly-trained-sub')) document.getElementById('anomaly-trained-sub').textContent = `Anomaly detection model trained on ${data.summary.total_books} records.`;
1224
+ if(document.getElementById('anomaly-analyzed')) document.getElementById('anomaly-analyzed').textContent = `${data.anomalies.length}/${data.summary.anomalies}`;
1225
+
1226
+ let crit = 0, high = 0, med = 0;
1227
+ data.anomalies.forEach(a => {
1228
+ const s = a.AnomalyScore || 0;
1229
+ if (s > 0.3) crit++;
1230
+ else if (s > 0.1) high++;
1231
+ else med++;
1232
+ });
1233
+ const totAnom = data.summary.anomalies || 1;
1234
+ document.getElementById('anomaly-crit-count').textContent = crit;
1235
+ document.getElementById('anomaly-crit-bar').style.width = `${(crit/totAnom)*100}%`;
1236
+ document.getElementById('anomaly-high-count').textContent = high;
1237
+ document.getElementById('anomaly-high-bar').style.width = `${(high/totAnom)*100}%`;
1238
+ document.getElementById('anomaly-med-count').textContent = med;
1239
+ document.getElementById('anomaly-med-bar').style.width = `${(med/totAnom)*100}%`;
1240
+
1241
+ // Update newly added dynamic IDs for Reconciliation and Anomaly tab stats
1242
+ if(document.getElementById('recon-stat-exact')) document.getElementById('recon-stat-exact').textContent = data.summary.exact || 0;
1243
+ if(document.getElementById('recon-stat-fuzzy')) document.getElementById('recon-stat-fuzzy').textContent = data.summary.fuzzy || 0;
1244
+ if(document.getElementById('recon-stat-semantic')) document.getElementById('recon-stat-semantic').textContent = data.summary.semantic || 0;
1245
+ if(document.getElementById('recon-stat-unmatched')) document.getElementById('recon-stat-unmatched').textContent = data.summary.unmatched || 0;
1246
+
1247
+ if(document.getElementById('total-count')) document.getElementById('total-count').textContent = data.summary.total_books || 0;
1248
+ if(document.getElementById('showing-count')) document.getElementById('showing-count').textContent = data.reconciliation.length || 0;
1249
+
1250
+ if(document.getElementById('alerts-count')) document.getElementById('alerts-count').textContent = data.summary.anomalies || 0;
1251
+ if(document.getElementById('filter-crit')) document.getElementById('filter-crit').textContent = crit;
1252
+ if(document.getElementById('filter-all')) document.getElementById('filter-all').textContent = totAnom;
1253
+
1254
+
1255
+
1256
+ // Update Recent Alerts
1257
+ const recentAlertsDiv = document.getElementById('recent-alerts');
1258
+ if (recentAlertsDiv) {
1259
+ if (data.anomalies && data.anomalies.length > 0) {
1260
+ // Sort by highest risk first and take top 5
1261
+ const sortedAnomalies = [...data.anomalies].sort((a, b) => (b.AnomalyScore || 0) - (a.AnomalyScore || 0)).slice(0, 5);
1262
+
1263
+ recentAlertsDiv.innerHTML = sortedAnomalies.map(a => {
1264
+ const score = a.AnomalyScore || 0;
1265
+ const risk = score > 0.3 ? 'Critical' : score > 0.1 ? 'High' : 'Medium';
1266
+ const riskClass = risk === 'Critical' ? 'bg-red-500/10 border-red-500/20 text-red-400' :
1267
+ risk === 'High' ? 'bg-orange-500/10 border-orange-500/20 text-orange-400' : 'bg-yellow-500/10 border-yellow-500/20 text-yellow-400';
1268
+ const icon = risk === 'Critical' ? 'alert-octagon' : risk === 'High' ? 'alert-triangle' : 'info';
1269
+
1270
+ return `
1271
+ <div class="p-3 rounded-lg border ${riskClass} flex gap-3 items-start">
1272
+ <div class="mt-0.5"><i data-lucide="${icon}" class="w-4 h-4"></i></div>
1273
+ <div>
1274
+ <div class="flex items-center gap-2 mb-1">
1275
+ <span class="text-xs font-bold uppercase tracking-wider">${risk}</span>
1276
+ <span class="text-[10px] font-mono opacity-80">Score: ${score.toFixed(3)}</span>
1277
+ </div>
1278
+ <p class="text-xs text-surface-200">Invoice <span class="font-mono text-white">${a.InvoiceID || '-'}</span> from <span class="text-white">${a.VendorName_books || 'Unknown'}</span></p>
1279
+ </div>
1280
+ </div>
1281
+ `;
1282
+ }).join('');
1283
+ // Need to re-initialize lucide icons for newly added elements
1284
+ setTimeout(() => lucide.createIcons(), 50);
1285
+ } else {
1286
+ recentAlertsDiv.innerHTML = '<p class="text-sm text-surface-400">No active alerts.</p>';
1287
+ }
1288
+ }
1289
+
1290
+ // Fraud & FAISS Updates
1291
+ if (data.faiss_stats) {
1292
+ if(document.getElementById('faiss-vectors')) document.getElementById('faiss-vectors').textContent = `${data.faiss_stats.ntotal} vectors`;
1293
+ if(document.getElementById('faiss-memory')) document.getElementById('faiss-memory').textContent = `${data.faiss_stats.memory_mb} MB`;
1294
+ if(document.getElementById('faiss-latency')) document.getElementById('faiss-latency').textContent = `0.8ms avg`;
1295
+ }
1296
+
1297
+ if (data.fraud_network) {
1298
+ const fraudCount = data.summary.fraud_rings || 0;
1299
+ if(document.getElementById('stat-fraud-rings')) document.getElementById('stat-fraud-rings').textContent = fraudCount;
1300
+ if(document.getElementById('stat-fraud-rings-sub')) document.getElementById('stat-fraud-rings-sub').textContent = fraudCount > 0 ? `${fraudCount} rings detected` : 'No rings detected';
1301
+
1302
+ if (document.getElementById('fraud-nodes')) document.getElementById('fraud-nodes').textContent = data.fraud_network.nodes.length;
1303
+ if (document.getElementById('fraud-edges')) document.getElementById('fraud-edges').textContent = data.fraud_network.edges.length;
1304
+
1305
+ const riskScore = (data.summary.overall_risk_score || 0).toFixed(1);
1306
+ if(document.getElementById('stat-risk-score')) document.getElementById('stat-risk-score').innerHTML = `${riskScore}<span class="text-sm text-surface-400">/10</span>`;
1307
+
1308
+ if (fraudCount > 0) {
1309
+ if(document.getElementById('stat-risk-score-sub')) document.getElementById('stat-risk-score-sub').textContent = riskScore > 5 ? 'High risk level' : 'Moderate risk level';
1310
+
1311
+ const ringsList = document.getElementById('fraud-rings-list');
1312
+ if(ringsList) {
1313
+ ringsList.innerHTML = data.fraud_network.cycles.map((cycle, i) => `
1314
+ <div class="p-3 rounded-lg bg-red-500/10 border border-red-500/20">
1315
+ <p class="text-xs text-red-400 font-semibold mb-1">Ring #${i+1} Detected</p>
1316
+ <p class="text-xs text-surface-300 font-mono">${cycle.join(' → ')}</p>
1317
+ </div>
1318
+ `).join('');
1319
+ }
1320
+ } else {
1321
+ const ringsList = document.getElementById('fraud-rings-list');
1322
+ if (ringsList) ringsList.innerHTML = '<p class="text-sm text-surface-400">No fraud rings detected in current dataset.</p>';
1323
+ }
1324
+
1325
+ // Store network data for canvas rendering
1326
+ window.fraudNetworkData = data.fraud_network;
1327
+ if (!document.getElementById('tab-fraud').classList.contains('hidden')) {
1328
+ drawFraudNetwork();
1329
+ }
1330
+ }
1331
+
1332
+ // Update Reconciliation Table
1333
+ const reconBody = document.getElementById('recon-table-body');
1334
+ reconBody.innerHTML = data.reconciliation.map(row => {
1335
+ const matchType = row.MatchStatus;
1336
+ const statusClass = matchType === 'Exact Match' ? 'text-green-400' :
1337
+ matchType.includes('Fuzzy') ? 'text-yellow-400' :
1338
+ matchType.includes('Semantic') ? 'text-brand-400' : 'text-red-400';
1339
+
1340
+ const confidence = matchType.includes('(') ? matchType.split('(')[1].replace(')', '') : (matchType === 'Exact Match' ? '100%' : '0%');
1341
+ const matchLabel = matchType.split(' ')[0];
1342
+
1343
+ return `
1344
+ <tr class="border-b border-surface-800/30 hover:bg-surface-800/30 transition">
1345
+ <td class="px-4 py-3 font-mono text-xs">${row.InvoiceID || '-'}</td>
1346
+ <td class="px-4 py-3 font-mono text-xs">${row.VendorName_gst || row.VendorName_books || '-'}</td>
1347
+ <td class="px-4 py-3 text-xs">₹${row.Amount_books || row.Amount_gst || 0}</td>
1348
+ <td class="px-4 py-3"><span class="px-2 py-0.5 rounded-full text-[10px] font-semibold text-white bg-surface-700">${matchLabel}</span></td>
1349
+ <td class="px-4 py-3"><span class="${statusClass} text-xs font-mono">${confidence}</span></td>
1350
+ <td class="px-4 py-3"><span class="w-2 h-2 rounded-full inline-block ${matchType === 'Exact Match' ? 'bg-green-400' : 'bg-red-400'}"></span></td>
1351
+ </tr>
1352
+ `;
1353
+ }).join('');
1354
+
1355
+ // Update Match Confidence Chart if it exists
1356
+ const confCtx = document.getElementById('chart-confidence');
1357
+ if (confCtx) {
1358
+ const chart = Chart.getChart(confCtx);
1359
+ if (chart) {
1360
+ chart.data.datasets[0].data = [
1361
+ data.summary.exact || 0,
1362
+ data.summary.fuzzy || 0,
1363
+ data.summary.semantic || 0,
1364
+ data.summary.unmatched || 0
1365
+ ];
1366
+ chart.update();
1367
+ }
1368
+ }
1369
+
1370
+ // Update Recon Trend Chart
1371
+ const trendCtx = document.getElementById('chart-recon-trend');
1372
+ if (trendCtx && data.charts && data.charts.recon_trend) {
1373
+ const chart = Chart.getChart(trendCtx);
1374
+ if (chart) {
1375
+ chart.data.datasets[0].data = data.charts.recon_trend;
1376
+ if (data.charts.discrep_trend && chart.data.datasets[1]) {
1377
+ chart.data.datasets[1].data = data.charts.discrep_trend;
1378
+ }
1379
+ chart.update();
1380
+ }
1381
+ }
1382
+
1383
+ // Update Anomaly Dist Chart
1384
+ const distCtx = document.getElementById('chart-anomaly-dist');
1385
+ if (distCtx && data.charts && data.charts.anomaly_dist) {
1386
+ const chart = Chart.getChart(distCtx);
1387
+ if (chart) {
1388
+ chart.data.datasets[0].data = data.charts.anomaly_dist.critical;
1389
+ chart.data.datasets[1].data = data.charts.anomaly_dist.high;
1390
+ chart.data.datasets[2].data = data.charts.anomaly_dist.medium;
1391
+ chart.update();
1392
+ }
1393
+ }
1394
+
1395
+
1396
+ window.anomalyDataMap = {};
1397
+
1398
+ // Calculate anomaly counts
1399
+ let critCount = 0, highCount = 0, medCount = 0;
1400
+ let maxScore = 0, minScore = 1;
1401
+ data.anomalies.forEach(a => {
1402
+ const s = a.AnomalyScore || 0;
1403
+ if (s > maxScore) maxScore = s;
1404
+ if (s < minScore && s > 0) minScore = s;
1405
+ if (s > 0.3) critCount++;
1406
+ else if (s > 0.1) highCount++;
1407
+ else medCount++;
1408
+ });
1409
+ if (minScore === 1 && maxScore === 0) minScore = 0; // fallback if no anomalies
1410
+ const totalAnomalies = data.anomalies.length;
1411
+
1412
+ if (document.getElementById('anomaly-score-range')) {
1413
+ document.getElementById('anomaly-score-range').textContent = totalAnomalies > 0 ? `${minScore.toFixed(2)} - ${maxScore.toFixed(2)}` : '--';
1414
+ }
1415
+ if (document.getElementById('anomaly-crit-count')) document.getElementById('anomaly-crit-count').textContent = critCount;
1416
+ if (document.getElementById('anomaly-high-count')) document.getElementById('anomaly-high-count').textContent = highCount;
1417
+ if (document.getElementById('anomaly-med-count')) document.getElementById('anomaly-med-count').textContent = medCount;
1418
+
1419
+ if (document.getElementById('filter-crit')) document.getElementById('filter-crit').textContent = critCount;
1420
+ if (document.getElementById('filter-all')) document.getElementById('filter-all').textContent = totalAnomalies;
1421
+
1422
+ if (totalAnomalies > 0) {
1423
+ if (document.getElementById('anomaly-crit-bar')) document.getElementById('anomaly-crit-bar').style.width = `${(critCount/totalAnomalies)*100}%`;
1424
+ if (document.getElementById('anomaly-high-bar')) document.getElementById('anomaly-high-bar').style.width = `${(highCount/totalAnomalies)*100}%`;
1425
+ if (document.getElementById('anomaly-med-bar')) document.getElementById('anomaly-med-bar').style.width = `${(medCount/totalAnomalies)*100}%`;
1426
+ } else {
1427
+ if (document.getElementById('anomaly-crit-bar')) document.getElementById('anomaly-crit-bar').style.width = '0%';
1428
+ if (document.getElementById('anomaly-high-bar')) document.getElementById('anomaly-high-bar').style.width = '0%';
1429
+ if (document.getElementById('anomaly-med-bar')) document.getElementById('anomaly-med-bar').style.width = '0%';
1430
+ }
1431
+
1432
+
1433
+ // Update Anomaly Table
1434
+ const anomalyBody = document.getElementById('anomaly-table-body');
1435
+ anomalyBody.innerHTML = data.anomalies.map(a => {
1436
+ window.anomalyDataMap[a.InvoiceID] = a;
1437
+ const score = a.AnomalyScore || 0;
1438
+ const risk = score > 0.3 ? 'Critical' : score > 0.1 ? 'High' : 'Medium';
1439
+ const riskClass = risk === 'Critical' ? 'bg-red-500/20 text-red-400' :
1440
+ risk === 'High' ? 'bg-orange-500/20 text-orange-400' : 'bg-yellow-500/20 text-yellow-400';
1441
+
1442
+ return `
1443
+ <tr class="border-b border-surface-800/30 hover:bg-surface-800/30 transition">
1444
+ <td class="px-4 py-3 font-mono text-xs">${a.InvoiceID || '-'}</td>
1445
+ <td class="px-4 py-3 text-xs">${a.VendorName_books || '-'}</td>
1446
+ <td class="px-4 py-3 text-xs font-mono">₹${a.Amount_books || 0}</td>
1447
+ <td class="px-4 py-3"><span class="text-xs font-mono text-surface-300">${score.toFixed(3)}</span></td>
1448
+ <td class="px-4 py-3"><span class="px-2 py-0.5 rounded-full text-[10px] font-semibold ${riskClass}">${risk}</span></td>
1449
+ <td class="px-4 py-3"><button class="text-[10px] px-2 py-1 rounded bg-brand-600/20 text-brand-300" onclick="showExplanation('${a.InvoiceID}')">View</button></td>
1450
+ </tr>
1451
+ `;
1452
+ }).join('');
1453
+ }
1454
+
1455
+ // ============================================
1456
+ // TOAST NOTIFICATION
1457
+ // ============================================
1458
+ function showToast(message, type = 'info') {
1459
+ const container = document.getElementById('toast-container');
1460
+ const toast = document.createElement('div');
1461
+ const colors = {
1462
+ success: 'border-green-500/30 bg-green-500/10',
1463
+ error: 'border-red-500/30 bg-red-500/10',
1464
+ info: 'border-brand-500/30 bg-brand-500/10'
1465
+ };
1466
+ const icons = {
1467
+ success: 'check-circle',
1468
+ error: 'alert-circle',
1469
+ info: 'info'
1470
+ };
1471
+ toast.className = `flex items-start gap-3 p-4 rounded-xl border ${colors[type]} backdrop-blur-xl animate-slide-in max-w-sm`;
1472
+ toast.innerHTML = `
1473
+ <i data-lucide="${icons[type]}" class="w-5 h-5 ${type === 'success' ? 'text-green-400' : type === 'error' ? 'text-red-400' : 'text-brand-400'} shrink-0 mt-0.5"></i>
1474
+ <p class="text-sm text-surface-200">${message}</p>
1475
+ `;
1476
+ container.appendChild(toast);
1477
+ lucide.createIcons();
1478
+ setTimeout(() => {
1479
+ toast.style.opacity = '0';
1480
+ toast.style.transform = 'translateX(100%)';
1481
+ toast.style.transition = 'all 0.3s ease';
1482
+ setTimeout(() => toast.remove(), 300);
1483
+ }, 5000);
1484
+ }
1485
+
1486
+ // ============================================
1487
+ // EXPORT CSV
1488
+ // ============================================
1489
+ function exportCSV() {
1490
+ const reconBody = document.getElementById('recon-table-body');
1491
+ if (!reconBody || reconBody.rows.length === 0) {
1492
+ showToast('No data to export. Run Engine first.', 'error');
1493
+ return;
1494
+ }
1495
+ showToast('Generating CSV export...', 'info');
1496
+ setTimeout(() => {
1497
+ const rows = Array.from(reconBody.querySelectorAll('tr'));
1498
+ const headers = ['Invoice ID', 'Vendor', 'Amount', 'Match Type', 'Confidence', 'Status'];
1499
+ const csvRows = rows.map(tr => Array.from(tr.querySelectorAll('td')).map(td => td.textContent.trim()).join(','));
1500
+ const csv = [headers.join(','), ...csvRows].join('\n');
1501
+ const blob = new Blob([csv], { type: 'text/csv' });
1502
+ const url = URL.createObjectURL(blob);
1503
+ const a = document.createElement('a');
1504
+ a.href = url;
1505
+ a.download = 'reconciliation_results.csv';
1506
+ a.click();
1507
+ URL.revokeObjectURL(url);
1508
+ showToast('CSV exported successfully!', 'success');
1509
+ }, 500);
1510
+ }
1511
+
1512
+ function filterReconTable() {
1513
+ // In a full implementation, this would filter the current data array.
1514
+ showToast('Filtering is coming soon!', 'info');
1515
+ }
1516
+
1517
+ // ============================================
1518
+ // INIT
1519
+ // ============================================
1520
+ document.addEventListener('DOMContentLoaded', () => {
1521
+ lucide.createIcons();
1522
+ initCharts();
1523
+
1524
+ // Clear initial dummy tables
1525
+ document.getElementById('recon-table-body').innerHTML = `<tr><td colspan="6" class="text-center py-8 text-surface-400">Please upload CSVs and Run Engine to view data.</td></tr>`;
1526
+ document.getElementById('anomaly-table-body').innerHTML = `<tr><td colspan="6" class="text-center py-8 text-surface-400">No anomalies detected yet. Upload data and Run Engine to begin analysis.</td></tr>`;
1527
+ });
1528
+
1529
+ // Redraw fraud network on resize
1530
+ let resizeTimeout;
1531
+ window.addEventListener('resize', () => {
1532
+ clearTimeout(resizeTimeout);
1533
+ resizeTimeout = setTimeout(() => {
1534
+ if (!document.getElementById('tab-fraud').classList.contains('hidden')) {
1535
+ drawFraudNetwork();
1536
+ }
1537
+ }, 200);
1538
+ });
1539
+ </script>
1540
+ </body>
1541
+ </html>
llm_explainer.py ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import requests
3
+ from dotenv import load_dotenv
4
+ import logging
5
+
6
+ load_dotenv()
7
+
8
+ logging.basicConfig(level=logging.INFO)
9
+ logger = logging.getLogger(__name__)
10
+
11
+ class LLMExplainer:
12
+ def __init__(self):
13
+ self.api_key = os.environ.get("MISTRAL_API_KEY")
14
+ self.api_url = "https://api.mistral.ai/v1/chat/completions"
15
+ self.explanation_cache = {}
16
+
17
+ def explain_discrepancy(self, row, match_status, books_vendor, gst_vendor, books_amount, gst_amount):
18
+ """
19
+ Generates a human-readable explanation for a discrepancy using Mistral AI.
20
+ """
21
+ if not self.api_key:
22
+ return "API Key not configured. Discrepancy: " + match_status
23
+
24
+ is_anomaly = row.get('IsAnomaly', False)
25
+
26
+ if match_status == "Exact Match" and not is_anomaly:
27
+ return "Records match perfectly."
28
+
29
+ cache_key = (match_status, books_vendor, gst_vendor, books_amount, gst_amount, is_anomaly)
30
+ if cache_key in self.explanation_cache:
31
+ logger.info("Returning cached LLM explanation.")
32
+ return self.explanation_cache[cache_key]
33
+
34
+ prompt = f"""
35
+ You are an expert AI financial auditor. Explain the following transaction in 1-2 concise, professional sentences. Provide a recommended action.
36
+
37
+ Reconciliation Status: {match_status}
38
+ Is Flagged as Anomaly: {is_anomaly}
39
+ Anomaly Score: {row.get('AnomalyScore', 0.0)}
40
+ Invoice ID: {row.get('InvoiceID', 'Unknown')}
41
+ Books Vendor: {books_vendor}
42
+ GST Vendor: {gst_vendor}
43
+ Books Amount: {books_amount}
44
+ GST Amount: {gst_amount}
45
+
46
+ Explanation and Recommendation:
47
+ """
48
+
49
+ headers = {
50
+ "Content-Type": "application/json",
51
+ "Accept": "application/json",
52
+ "Authorization": f"Bearer {self.api_key}"
53
+ }
54
+
55
+ data = {
56
+ "model": "mistral-tiny", # Using tiny for faster responses, can upgrade to small/medium
57
+ "messages": [
58
+ {"role": "user", "content": prompt}
59
+ ],
60
+ "temperature": 0.3
61
+ }
62
+
63
+ try:
64
+ response = requests.post(self.api_url, headers=headers, json=data, timeout=10)
65
+ response.raise_for_status()
66
+ result = response.json()
67
+ explanation = result['choices'][0]['message']['content'].strip()
68
+ self.explanation_cache[cache_key] = explanation
69
+ return explanation
70
+ except requests.exceptions.RequestException as e:
71
+ logger.error(f"Error calling Mistral API: {e}")
72
+ return f"API Error ({match_status}). Please check amounts and vendor names manually."
73
+ except (KeyError, IndexError) as e:
74
+ logger.error(f"Unexpected response format from Mistral API: {e}")
75
+ return f"Error parsing AI response ({match_status})."
76
+
77
+ def generate_explanations_batch(self, discrepancies_df):
78
+ """
79
+ Generates explanations for a dataframe of discrepancies.
80
+ """
81
+ explanations = []
82
+ for _, row in discrepancies_df.iterrows():
83
+ status = row.get('MatchStatus', 'Unknown')
84
+ b_vendor = row.get('VendorName_books', 'N/A')
85
+ g_vendor = row.get('VendorName_gst', 'N/A')
86
+ b_amount = row.get('Amount_books', 0)
87
+ g_amount = row.get('Amount_gst', 0)
88
+
89
+ explanation = self.explain_discrepancy(row, status, b_vendor, g_vendor, b_amount, g_amount)
90
+ explanations.append(explanation)
91
+
92
+ return explanations
main.py ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, File, UploadFile, Request
2
+ from fastapi.responses import HTMLResponse, JSONResponse
3
+ from fastapi.staticfiles import StaticFiles
4
+ from fastapi.middleware.cors import CORSMiddleware
5
+ import pandas as pd
6
+ import io
7
+ import uvicorn
8
+ from reconciliation import ReconciliationEngine
9
+ from anomaly import AnomalyDetector
10
+ from llm_explainer import LLMExplainer
11
+ from fraud_graph import FraudGraph
12
+ from gst_api import GSTGatewayMock
13
+ import os
14
+
15
+ app = FastAPI()
16
+
17
+ app.add_middleware(
18
+ CORSMiddleware,
19
+ allow_origins=["*"],
20
+ allow_credentials=True,
21
+ allow_methods=["*"],
22
+ allow_headers=["*"],
23
+ )
24
+
25
+ # Initialize engines
26
+ try:
27
+ recon_engine = ReconciliationEngine(threshold=85.0)
28
+ except Exception as e:
29
+ recon_engine = None
30
+
31
+ anomaly_detector = AnomalyDetector(contamination=0.05)
32
+ llm_explainer = LLMExplainer()
33
+ fraud_graph = FraudGraph()
34
+ gst_api = GSTGatewayMock()
35
+
36
+ @app.post("/api/reconcile")
37
+ async def api_reconcile(books: UploadFile = File(...), gst: UploadFile = File(...)):
38
+ books_content = await books.read()
39
+ gst_content = await gst.read()
40
+
41
+ try:
42
+ books_df = pd.read_csv(io.BytesIO(books_content))
43
+ gst_df = pd.read_csv(io.BytesIO(gst_content))
44
+ except Exception as e:
45
+ return {"error": "Invalid CSV format. Please ensure you are uploading valid CSV files, not PDFs or Excel documents."}
46
+
47
+ return process_data(books_df, gst_df)
48
+
49
+ @app.post("/api/explain")
50
+ async def api_explain(request: Request):
51
+ data = await request.json()
52
+ row = data.get("row", {})
53
+ match_status = data.get("match_status", "Anomaly")
54
+ b_vendor = data.get("b_vendor", "N/A")
55
+ g_vendor = data.get("g_vendor", "N/A")
56
+ b_amount = data.get("b_amount", 0)
57
+ g_amount = data.get("g_amount", 0)
58
+
59
+ explanation = llm_explainer.explain_discrepancy(row, match_status, b_vendor, g_vendor, b_amount, g_amount)
60
+ return {"explanation": explanation}
61
+
62
+ @app.post("/api/fetch_live")
63
+ async def api_fetch_live(books: UploadFile = File(...)):
64
+ books_content = await books.read()
65
+ books_df = pd.read_csv(io.BytesIO(books_content))
66
+
67
+ gst_df = gst_api.fetch_gst_data("2023-01-01", "2023-12-31", "27AADCB2230M1Z2")
68
+ return process_data(books_df, gst_df)
69
+
70
+ def process_data(books_df, gst_df):
71
+ if recon_engine is None:
72
+ return {"error": "Reconciliation engine failed to initialize"}
73
+
74
+ try:
75
+ merged_df = recon_engine.reconcile(books_df, gst_df)
76
+ except Exception as e:
77
+ return {"error": f"Reconciliation failed: {str(e)}"}
78
+
79
+ books_with_anomalies = anomaly_detector.detect_anomalies(books_df, amount_col='Amount')
80
+
81
+ if 'InvoiceID' in merged_df.columns and 'InvoiceID' in books_with_anomalies.columns:
82
+ merged_df = pd.merge(merged_df, books_with_anomalies[['InvoiceID', 'IsAnomaly', 'AnomalyScore']],
83
+ on='InvoiceID', how='left')
84
+
85
+ discrepancies = merged_df[merged_df['MatchStatus'] != 'Exact Match'].copy()
86
+
87
+ recon_results = merged_df.fillna("").infer_objects(copy=False).to_dict(orient="records")
88
+ anomalies = merged_df[merged_df['IsAnomaly'] == True].fillna("").infer_objects(copy=False).to_dict(orient="records")
89
+
90
+ # Compute chart data
91
+ recon_trend = [0] * 12
92
+ discrep_trend = [0] * 12
93
+ anomaly_dist = {"critical": [0]*12, "high": [0]*12, "medium": [0]*12}
94
+
95
+ # Try to extract month if InvoiceDate exists
96
+ date_col = 'InvoiceDate' if 'InvoiceDate' in merged_df.columns else 'InvoiceDate_books' if 'InvoiceDate_books' in merged_df.columns else None
97
+
98
+ if date_col and date_col in merged_df.columns:
99
+ merged_df['Month'] = pd.to_datetime(merged_df[date_col], errors='coerce').dt.month
100
+ merged_df['Month'] = merged_df['Month'].fillna(1).astype(int)
101
+
102
+ monthly_recon = merged_df[merged_df['MatchStatus'] == 'Exact Match'].groupby('Month').size()
103
+ for m, count in monthly_recon.items():
104
+ if 1 <= m <= 12:
105
+ recon_trend[m-1] = int(count)
106
+
107
+ monthly_discrep = merged_df[merged_df['MatchStatus'] != 'Exact Match'].groupby('Month').size()
108
+ for m, count in monthly_discrep.items():
109
+ if 1 <= m <= 12:
110
+ discrep_trend[m-1] = int(count)
111
+
112
+ monthly_anomalies = merged_df[merged_df['IsAnomaly'] == True]
113
+ for _, row in monthly_anomalies.iterrows():
114
+ m = int(row.get('Month', 1))
115
+ if 1 <= m <= 12:
116
+ score = row.get('AnomalyScore', 0)
117
+ if score > 0.3:
118
+ anomaly_dist["critical"][m-1] += 1
119
+ elif score > 0.1:
120
+ anomaly_dist["high"][m-1] += 1
121
+ else:
122
+ anomaly_dist["medium"][m-1] += 1
123
+
124
+ # Run Fraud Graph Analysis
125
+ try:
126
+ fraud_graph.build_graph(merged_df, source_col='VendorName_books', target_col='VendorName_gst', amount_col='Amount_books')
127
+ cycles = fraud_graph.detect_cycles()
128
+ risk_scores = fraud_graph.analyze_risk_nodes()
129
+
130
+ fraud_nodes = [{"id": str(n), "label": str(n), "size": 15, "color": "#64748b", "risk_score": risk_scores.get(n, 0.0)} for n in fraud_graph.graph.nodes()]
131
+ fraud_edges = [{"from": list(fraud_graph.graph.nodes()).index(u), "to": list(fraud_graph.graph.nodes()).index(v), "weight": d.get('weight', 0)} for u, v, d in fraud_graph.graph.edges(data=True)]
132
+ max_risk = max(risk_scores.values()) if risk_scores else 0.0
133
+ overall_risk_score = min(10.0, max_risk * 100) # Arbitrary scale to 0-10
134
+ except Exception as e:
135
+ cycles = []
136
+ fraud_nodes = []
137
+ fraud_edges = []
138
+ overall_risk_score = 0.0
139
+
140
+ # Get FAISS Stats
141
+ try:
142
+ ntotal = recon_engine.index.ntotal if recon_engine and recon_engine.index else 0
143
+ mem_mb = round(ntotal * 384 * 4 / (1024 * 1024), 2)
144
+ except:
145
+ ntotal = 0
146
+ mem_mb = 0
147
+
148
+ return {
149
+ "summary": {
150
+ "total_books": len(books_df),
151
+ "total_gst": len(gst_df),
152
+ "exact": len(merged_df[merged_df['MatchStatus'] == 'Exact Match']),
153
+ "fuzzy": len(merged_df[merged_df['MatchStatus'].str.contains('Fuzzy', na=False)]),
154
+ "semantic": len(merged_df[merged_df['MatchStatus'].str.contains('Semantic', na=False)]),
155
+ "discrepancies": len(discrepancies),
156
+ "unmatched": len(merged_df[merged_df['MatchStatus'].str.contains('Mismatch', na=False) | merged_df['MatchStatus'].str.contains('Missing', na=False)]),
157
+ "anomalies": len(anomalies),
158
+ "fraud_rings": len(cycles),
159
+ "overall_risk_score": overall_risk_score
160
+ },
161
+ "charts": {
162
+ "recon_trend": recon_trend,
163
+ "discrep_trend": discrep_trend,
164
+ "anomaly_dist": anomaly_dist
165
+ },
166
+ "fraud_network": {
167
+ "nodes": fraud_nodes,
168
+ "edges": fraud_edges,
169
+ "cycles": cycles
170
+ },
171
+ "faiss_stats": {
172
+ "ntotal": ntotal,
173
+ "memory_mb": mem_mb
174
+ },
175
+ "reconciliation": recon_results[:50], # Limit payload for UI
176
+ "anomalies": anomalies[:50]
177
+ }
178
+
179
+ # Serve the frontend files
180
+ app.mount("/", StaticFiles(directory=".", html=True), name="static")
181
+
182
+ if __name__ == "__main__":
183
+ uvicorn.run(app, host="0.0.0.0", port=7860)
reconciliation.py ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import numpy as np
3
+ from rapidfuzz import fuzz
4
+ from sentence_transformers import SentenceTransformer
5
+ import faiss
6
+ import os
7
+ import pickle
8
+ import logging
9
+
10
+ logging.basicConfig(level=logging.INFO)
11
+ logger = logging.getLogger(__name__)
12
+
13
+ class ReconciliationEngine:
14
+ def __init__(self, threshold=85.0, model_name='all-MiniLM-L6-v2', index_path='vendor_index.faiss'):
15
+ self.threshold = threshold
16
+ self.model = SentenceTransformer(model_name)
17
+ self.index_path = index_path
18
+ self.vendor_mapping_path = 'vendor_mapping.pkl'
19
+ self.index = None
20
+ self.vendor_names = []
21
+ self._load_or_create_index()
22
+
23
+ def _load_or_create_index(self):
24
+ # We need dimension size for the chosen model. MiniLM-L6-v2 is 384
25
+ d = self.model.get_sentence_embedding_dimension()
26
+
27
+ if os.path.exists(self.index_path) and os.path.exists(self.vendor_mapping_path):
28
+ logger.info("Loading existing FAISS index.")
29
+ self.index = faiss.read_index(self.index_path)
30
+ with open(self.vendor_mapping_path, 'rb') as f:
31
+ self.vendor_names = pickle.load(f)
32
+ else:
33
+ logger.info("Creating new FAISS index.")
34
+ self.index = faiss.IndexFlatL2(d)
35
+ self.vendor_names = []
36
+
37
+ def _save_index(self):
38
+ faiss.write_index(self.index, self.index_path)
39
+ with open(self.vendor_mapping_path, 'wb') as f:
40
+ pickle.dump(self.vendor_names, f)
41
+
42
+ def learn_vendors(self, vendors):
43
+ """Adds new vendors to the FAISS index."""
44
+ if not hasattr(self, 'embedding_cache'):
45
+ self.embedding_cache = {}
46
+
47
+ new_vendors = [v for v in set(vendors) if pd.notna(v) and v not in self.vendor_names]
48
+ if new_vendors:
49
+ logger.info(f"Learning {len(new_vendors)} new vendors.")
50
+ embeddings = self.model.encode(new_vendors)
51
+ self.index.add(np.array(embeddings).astype('float32'))
52
+ self.vendor_names.extend(new_vendors)
53
+
54
+ # Pre-cache to speed up pair-wise matching later
55
+ for v, emb in zip(new_vendors, embeddings):
56
+ self.embedding_cache[v] = emb / np.linalg.norm(emb)
57
+
58
+ self._save_index()
59
+
60
+ def get_embedding(self, vendor):
61
+ if not hasattr(self, 'embedding_cache'):
62
+ self.embedding_cache = {}
63
+ if vendor not in self.embedding_cache:
64
+ emb = self.model.encode([vendor])[0]
65
+ self.embedding_cache[vendor] = emb / np.linalg.norm(emb)
66
+ return self.embedding_cache[vendor]
67
+
68
+ def get_semantic_similarity(self, vendor1, vendor2):
69
+ if pd.isna(vendor1) or pd.isna(vendor2):
70
+ return 0.0
71
+ emb1_norm = self.get_embedding(vendor1)
72
+ emb2_norm = self.get_embedding(vendor2)
73
+ sim = np.dot(emb1_norm, emb2_norm)
74
+ return max(0.0, sim * 100)
75
+
76
+ def search_similar_vendor(self, query_vendor, top_k=1):
77
+ if not self.vendor_names or pd.isna(query_vendor):
78
+ return None, 0.0
79
+
80
+ query_emb = self.model.encode([query_vendor]).astype('float32')
81
+ distances, indices = self.index.search(query_emb, top_k)
82
+
83
+ best_idx = indices[0][0]
84
+ if best_idx != -1:
85
+ best_match = self.vendor_names[best_idx]
86
+ # Calculate a normalized score based on L2 distance
87
+ # For normalized vectors, L2 distance squared is 2 - 2*cos(theta)
88
+ # This is a rough proxy; let's combine with fuzz for the final score
89
+ fuzz_score = fuzz.ratio(query_vendor.lower(), best_match.lower())
90
+ return best_match, fuzz_score
91
+ return None, 0.0
92
+
93
+ def reconcile(self, source_df, target_df, source_key='VendorName', target_key='VendorName', amount_col='Amount'):
94
+ logger.info("Starting reconciliation process.")
95
+
96
+ # Learn vendors from both datasets
97
+ self.learn_vendors(source_df[source_key].tolist())
98
+ self.learn_vendors(target_df[target_key].tolist())
99
+
100
+ # Basic exact match on InvoiceID if it exists, otherwise we match on VendorName and Amount
101
+ if 'InvoiceID' in source_df.columns and 'InvoiceID' in target_df.columns:
102
+ source_df = source_df.drop_duplicates(subset=['InvoiceID'])
103
+ target_df = target_df.drop_duplicates(subset=['InvoiceID'])
104
+ merged = pd.merge(source_df, target_df, on='InvoiceID', how='outer', suffixes=('_books', '_gst'))
105
+
106
+ def determine_status(row):
107
+ if pd.isna(row.get(f'{amount_col}_books')):
108
+ return "Missing in Books"
109
+ if pd.isna(row.get(f'{amount_col}_gst')):
110
+ return "Missing in GST"
111
+
112
+ b_amt = float(row.get(f'{amount_col}_books', 0))
113
+ g_amt = float(row.get(f'{amount_col}_gst', 0))
114
+
115
+ if abs(b_amt - g_amt) > 0.01:
116
+ return "Amount Mismatch"
117
+
118
+ b_vendor_val = row.get(f'{source_key}_books')
119
+ g_vendor_val = row.get(f'{target_key}_gst')
120
+ b_vendor = str(b_vendor_val) if pd.notna(b_vendor_val) else ''
121
+ g_vendor = str(g_vendor_val) if pd.notna(g_vendor_val) else ''
122
+
123
+ if b_vendor.lower() == g_vendor.lower() and b_vendor != '':
124
+ return "Exact Match"
125
+
126
+ fuzz_score = fuzz.ratio(b_vendor.lower(), g_vendor.lower())
127
+ if fuzz_score >= self.threshold:
128
+ return f"Fuzzy Match ({fuzz_score:.1f}%)"
129
+
130
+ sem_score = self.get_semantic_similarity(b_vendor, g_vendor)
131
+ if sem_score >= self.threshold:
132
+ return f"Semantic Match ({sem_score:.1f}%)"
133
+
134
+ return "Vendor Mismatch"
135
+
136
+ merged['MatchStatus'] = merged.apply(determine_status, axis=1)
137
+ return merged
138
+ else:
139
+ raise ValueError("InvoiceID column is required for current reconciliation logic.")
requirements.txt ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ gradio>=4.0.0
2
+ pandas>=2.0.0
3
+ numpy>=1.24.0
4
+ scikit-learn>=1.3.0
5
+ sentence-transformers>=2.2.0
6
+ rapidfuzz>=3.0.0
7
+ faiss-cpu>=1.7.0
8
+ networkx>=3.0
9
+ matplotlib>=3.7.0
10
+ mistralai>=0.1.0
11
+ python-dotenv>=1.0.0
12
+ requests>=2.31.0
13
+ fastapi>=0.100.0
14
+ uvicorn>=0.23.0
15
+ python-multipart>=0.0.6
utils.py ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import numpy as np
3
+ import random
4
+ from datetime import datetime, timedelta
5
+ import os
6
+
7
+ def create_sample_data(num_records=100, output_dir="sample_data"):
8
+ if not os.path.exists(output_dir):
9
+ os.makedirs(output_dir)
10
+
11
+ companies = ["Acme Corp", "Global Tech", "Stark Industries", "Wayne Enterprises", "Cyberdyne",
12
+ "Umbrella Corp", "Tyrell Corporation", "Weyland-Yutani", "Omni Consumer Products", "Initech",
13
+ "Hooli", "Pied Piper", "Massive Dynamic", "Aperture Science", "Black Mesa"]
14
+
15
+ # Typos and variations for fuzzy matching
16
+ variations = {
17
+ "Acme Corp": ["Acme Corp", "Acme Corporation", "Acm Corp", "Acme Corpration"],
18
+ "Global Tech": ["Global Tech", "Global Technologies", "Gloabl Tech", "Global Tech Ltd."],
19
+ "Stark Industries": ["Stark Industries", "Stark Ind", "Strk Industries", "Stark Industries Inc."],
20
+ "Wayne Enterprises": ["Wayne Enterprises", "Wayne Ent", "Wayne Enterpises", "Wayne Enterprises LLC"]
21
+ }
22
+
23
+ books_data = []
24
+ gst_data = []
25
+
26
+ start_date = datetime(2023, 1, 1)
27
+
28
+ for i in range(1, num_records + 1):
29
+ invoice_id = f"INV-{1000 + i}"
30
+ base_company = random.choice(companies)
31
+
32
+ # Determine actual names to use
33
+ books_company = random.choice(variations.get(base_company, [base_company]))
34
+ gst_company = random.choice(variations.get(base_company, [base_company]))
35
+
36
+ base_amount = round(random.uniform(100, 10000), 2)
37
+
38
+ # Introduce discrepancies
39
+ discrepancy_type = random.choices(
40
+ ["none", "amount_diff", "missing_in_gst", "missing_in_books", "date_diff"],
41
+ weights=[0.6, 0.1, 0.1, 0.1, 0.1],
42
+ k=1
43
+ )[0]
44
+
45
+ books_amount = base_amount
46
+ gst_amount = base_amount
47
+
48
+ invoice_date = start_date + timedelta(days=random.randint(0, 365))
49
+ books_date = invoice_date.strftime('%Y-%m-%d')
50
+ gst_date = invoice_date.strftime('%Y-%m-%d')
51
+
52
+ if discrepancy_type == "amount_diff":
53
+ gst_amount = round(base_amount * random.choice([0.9, 1.1, 0.5, 1.05]), 2)
54
+ elif discrepancy_type == "date_diff":
55
+ gst_date = (invoice_date + timedelta(days=random.choice([-1, 1, -5, 5]))).strftime('%Y-%m-%d')
56
+
57
+ books_record = {
58
+ "InvoiceID": invoice_id,
59
+ "VendorName": books_company,
60
+ "Amount": books_amount,
61
+ "InvoiceDate": books_date,
62
+ "TaxAmount": round(books_amount * 0.18, 2)
63
+ }
64
+
65
+ gst_record = {
66
+ "InvoiceID": invoice_id,
67
+ "VendorName": gst_company,
68
+ "Amount": gst_amount,
69
+ "InvoiceDate": gst_date,
70
+ "TaxAmount": round(gst_amount * 0.18, 2)
71
+ }
72
+
73
+ if discrepancy_type != "missing_in_books":
74
+ books_data.append(books_record)
75
+ if discrepancy_type != "missing_in_gst":
76
+ gst_data.append(gst_record)
77
+
78
+ # Add some random anomalies (high amount)
79
+ for _ in range(max(1, num_records // 20)):
80
+ idx = random.randint(0, len(books_data)-1)
81
+ books_data[idx]["Amount"] = books_data[idx]["Amount"] * random.uniform(5, 10)
82
+ books_data[idx]["TaxAmount"] = round(books_data[idx]["Amount"] * 0.18, 2)
83
+
84
+ # INJECT CIRCULAR TRADING FRAUD RING FOR TESTING
85
+ ring_vendors = ["Shell Corp Alpha", "Ghost Entity Beta", "Phantom Traders Gamma"]
86
+ ring_amount = 55000.00
87
+ for idx in range(3):
88
+ # Create a cycle: Alpha -> Beta -> Gamma -> Alpha
89
+ books_v = ring_vendors[idx]
90
+ gst_v = ring_vendors[(idx + 1) % 3]
91
+
92
+ inv_id = f"FRAUD-RING-{idx+1}"
93
+ books_data.append({
94
+ "InvoiceID": inv_id,
95
+ "VendorName": books_v,
96
+ "Amount": ring_amount,
97
+ "InvoiceDate": "2023-11-15",
98
+ "TaxAmount": round(ring_amount * 0.18, 2)
99
+ })
100
+ gst_data.append({
101
+ "InvoiceID": inv_id,
102
+ "VendorName": gst_v,
103
+ "Amount": ring_amount,
104
+ "InvoiceDate": "2023-11-15",
105
+ "TaxAmount": round(ring_amount * 0.18, 2)
106
+ })
107
+
108
+ books_df = pd.DataFrame(books_data)
109
+ gst_df = pd.DataFrame(gst_data)
110
+
111
+ books_df.to_csv(os.path.join(output_dir, "books.csv"), index=False)
112
+ gst_df.to_csv(os.path.join(output_dir, "gst.csv"), index=False)
113
+
114
+ return {"source": books_df, "target": gst_df}
115
+
116
+ if __name__ == "__main__":
117
+ create_sample_data(200)
118
+ print("Sample data generated in sample_data directory.")
vendor_index.faiss ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4e981ad9fb60c7989773c0ba4343f2e243b9694eed137a2f32caa160d071405c
3
+ size 448557
vendor_mapping.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:962a07d11774075b801a9030446dc80e13cbd7c4231dd53f4db8d4158993b8cc
3
+ size 5975