jayasrees commited on
Commit
155dd44
·
1 Parent(s): 1220a21

Updated frontend UI and backend improvements

Browse files
README.md CHANGED
@@ -24,18 +24,21 @@ Legal document analysis web app with authentication, upload, line-level issue de
24
  ## Active User Flow
25
 
26
  1. `index.html` -> Login / Sign up
27
- 2. `upload.html` -> Upload file + run analysis
28
  3. `issues.html` -> Line-level issue analysis (duplication, inconsistency, contradiction)
29
  4. `summary.html` ->
30
  - Detailed document summary (Page 1, Page 2, ... style)
31
  - Page-wise summary cards
32
  - Top findings
33
- - Line Error Dashboard (exact page/line)
 
 
34
 
35
  ## Features
36
 
37
  - Auth endpoints (`register`, `login`) with SQLite
38
  - Upload support: `PDF`, `DOCX`, `TXT`
 
39
  - Detection categories:
40
  - Duplication
41
  - Inconsistency
 
24
  ## Active User Flow
25
 
26
  1. `index.html` -> Login / Sign up
27
+ 2. `upload.html` -> Upload up to 2 reference files + final file, then run cross-verification analysis
28
  3. `issues.html` -> Line-level issue analysis (duplication, inconsistency, contradiction)
29
  4. `summary.html` ->
30
  - Detailed document summary (Page 1, Page 2, ... style)
31
  - Page-wise summary cards
32
  - Top findings
33
+ 5. `dashboard.html` ->
34
+ - Line error table (exact page/line)
35
+ - Reference vs Final mismatch explanation + rectify action
36
 
37
  ## Features
38
 
39
  - Auth endpoints (`register`, `login`) with SQLite
40
  - Upload support: `PDF`, `DOCX`, `TXT`
41
+ - Cross verification: optional 1-2 reference documents + required final document
42
  - Detection categories:
43
  - Duplication
44
  - Inconsistency
backend/app.py CHANGED
@@ -87,11 +87,11 @@ def _extract_text_data(file_bytes: bytes, file_ext: str):
87
  raise ValueError("Unsupported file type. Use PDF, DOCX, or TXT.")
88
 
89
 
90
- def _extract_clauses(text_data):
91
  import re
92
 
93
  clauses = []
94
- clause_id = 0
95
 
96
  for chunk in text_data:
97
  raw_text = chunk.get("text", "")
@@ -111,6 +111,7 @@ def _extract_clauses(text_data):
111
  "text": cleaned,
112
  "page": page_num,
113
  "line": line_no,
 
114
  }
115
  )
116
  clause_id += 1
@@ -207,6 +208,19 @@ def _threshold_for_mode(scan_mode: str) -> float:
207
  return 0.60
208
 
209
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
  def _normalized_clause_text(text: str) -> str:
211
  import re
212
 
@@ -262,7 +276,7 @@ def _rule_based_category(text_a: str, text_b: str, similarity: float):
262
  return (None, None, 0.0, "")
263
 
264
 
265
- def _analyze_clauses(clauses, threshold: float):
266
  if str(PROJECT_ROOT) not in sys.path:
267
  sys.path.append(str(PROJECT_ROOT))
268
 
@@ -302,6 +316,14 @@ def _analyze_clauses(clauses, threshold: float):
302
 
303
  clause_a = clauses[i]
304
  clause_b = clauses[j]
 
 
 
 
 
 
 
 
305
  similarity = _similarity(clause_a["text"], clause_b["text"])
306
 
307
  category, label, confidence, reason = _rule_based_category(
@@ -344,6 +366,10 @@ def _analyze_clauses(clauses, threshold: float):
344
  "clause2": clause_b["text"],
345
  "location1": f"Pg {clause_a['page']}, Ln {clause_a['line']}",
346
  "location2": f"Pg {clause_b['page']}, Ln {clause_b['line']}",
 
 
 
 
347
  "page1": clause_a["page"],
348
  "line1": clause_a["line"],
349
  "page2": clause_b["page"],
@@ -352,7 +378,8 @@ def _analyze_clauses(clauses, threshold: float):
352
  )
353
  counts[category] += 1
354
  for clause in (clause_a, clause_b):
355
- line_key = (category, clause["page"], clause["line"], label)
 
356
  if line_key in seen_line_issues:
357
  continue
358
  seen_line_issues.add(line_key)
@@ -363,7 +390,9 @@ def _analyze_clauses(clauses, threshold: float):
363
  "confidence": round(float(confidence), 4),
364
  "page": clause["page"],
365
  "line": clause["line"],
366
- "location": f"Pg {clause['page']}, Ln {clause['line']}",
 
 
367
  "reason": reason,
368
  }
369
  )
@@ -638,35 +667,56 @@ def login():
638
  @app.post("/api/analyze")
639
  def analyze():
640
  uploaded = request.files.get("file")
 
641
  scan_mode = request.form.get("scanMode", "Standard Scan (Recommended)")
642
  threshold = _threshold_for_mode(scan_mode)
643
 
644
  if uploaded is None or uploaded.filename is None or uploaded.filename.strip() == "":
645
- return jsonify({"error": "Please upload a file."}), 400
646
 
647
  file_ext = uploaded.filename.rsplit(".", 1)[-1].lower() if "." in uploaded.filename else ""
648
- if file_ext not in {"pdf", "docx", "txt"}:
649
  return jsonify({"error": "Unsupported file type. Use PDF, DOCX, or TXT."}), 400
 
 
650
 
651
  try:
652
- file_bytes = uploaded.read()
653
- text_data = _extract_text_data(file_bytes=file_bytes, file_ext=file_ext)
654
- if not text_data:
655
- return jsonify({"error": "Could not extract text from file."}), 400
656
-
657
- clauses = _extract_clauses(text_data)
658
- if len(clauses) < 2:
659
- return jsonify({"error": "Not enough clauses found for analysis."}), 400
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
660
 
661
- parties = _extract_document_parties(text_data)
662
  findings, line_issues, counts, compared_pairs = _analyze_clauses(
663
- clauses=clauses, threshold=threshold
664
  )
 
665
  page_summaries = _build_page_summaries(
666
- clauses=clauses, line_issues=line_issues, text_data=text_data
667
  )
668
  detailed_summary = _build_detailed_summary(
669
- clauses=clauses,
670
  page_summaries=page_summaries,
671
  findings=findings,
672
  )
@@ -682,7 +732,8 @@ def analyze():
682
  "threshold": threshold,
683
  "vendor": parties["vendor"],
684
  "vendee": parties["vendee"],
685
- "clauses": len(clauses),
 
686
  "pairsCompared": compared_pairs,
687
  "issuesFound": len(findings),
688
  "duplicationCount": counts["duplication"],
@@ -693,6 +744,7 @@ def analyze():
693
  "detailedSummary": detailed_summary,
694
  "findings": findings[:50],
695
  "lineIssues": line_issues[:200],
 
696
  }
697
  ),
698
  200,
 
87
  raise ValueError("Unsupported file type. Use PDF, DOCX, or TXT.")
88
 
89
 
90
+ def _extract_clauses(text_data, source: str = "final", id_start: int = 0):
91
  import re
92
 
93
  clauses = []
94
+ clause_id = id_start
95
 
96
  for chunk in text_data:
97
  raw_text = chunk.get("text", "")
 
111
  "text": cleaned,
112
  "page": page_num,
113
  "line": line_no,
114
+ "source": source,
115
  }
116
  )
117
  clause_id += 1
 
208
  return 0.60
209
 
210
 
211
+ def _is_supported_ext(file_ext: str) -> bool:
212
+ return file_ext in {"pdf", "docx", "txt"}
213
+
214
+
215
+ def _source_label(source: str) -> str:
216
+ if source == "final":
217
+ return "Final"
218
+ if source.startswith("reference_"):
219
+ idx = source.split("_")[-1]
220
+ return f"Reference {idx}"
221
+ return source.title()
222
+
223
+
224
  def _normalized_clause_text(text: str) -> str:
225
  import re
226
 
 
276
  return (None, None, 0.0, "")
277
 
278
 
279
+ def _analyze_clauses(clauses, threshold: float, focus_source: str = "final"):
280
  if str(PROJECT_ROOT) not in sys.path:
281
  sys.path.append(str(PROJECT_ROOT))
282
 
 
316
 
317
  clause_a = clauses[i]
318
  clause_b = clauses[j]
319
+ source_a = str(clause_a.get("source", "final"))
320
+ source_b = str(clause_b.get("source", "final"))
321
+ # Compare only pairs involving final/focus doc:
322
+ # - final vs final
323
+ # - final vs reference
324
+ if source_a != focus_source and source_b != focus_source:
325
+ continue
326
+
327
  similarity = _similarity(clause_a["text"], clause_b["text"])
328
 
329
  category, label, confidence, reason = _rule_based_category(
 
366
  "clause2": clause_b["text"],
367
  "location1": f"Pg {clause_a['page']}, Ln {clause_a['line']}",
368
  "location2": f"Pg {clause_b['page']}, Ln {clause_b['line']}",
369
+ "source1": source_a,
370
+ "source2": source_b,
371
+ "sourceLabel1": _source_label(source_a),
372
+ "sourceLabel2": _source_label(source_b),
373
  "page1": clause_a["page"],
374
  "line1": clause_a["line"],
375
  "page2": clause_b["page"],
 
378
  )
379
  counts[category] += 1
380
  for clause in (clause_a, clause_b):
381
+ source = str(clause.get("source", "final"))
382
+ line_key = (category, source, clause["page"], clause["line"], label)
383
  if line_key in seen_line_issues:
384
  continue
385
  seen_line_issues.add(line_key)
 
390
  "confidence": round(float(confidence), 4),
391
  "page": clause["page"],
392
  "line": clause["line"],
393
+ "source": source,
394
+ "sourceLabel": _source_label(source),
395
+ "location": f"{_source_label(source)} - Pg {clause['page']}, Ln {clause['line']}",
396
  "reason": reason,
397
  }
398
  )
 
667
  @app.post("/api/analyze")
668
  def analyze():
669
  uploaded = request.files.get("file")
670
+ reference_uploads = request.files.getlist("referenceFiles")
671
  scan_mode = request.form.get("scanMode", "Standard Scan (Recommended)")
672
  threshold = _threshold_for_mode(scan_mode)
673
 
674
  if uploaded is None or uploaded.filename is None or uploaded.filename.strip() == "":
675
+ return jsonify({"error": "Please upload the final document file."}), 400
676
 
677
  file_ext = uploaded.filename.rsplit(".", 1)[-1].lower() if "." in uploaded.filename else ""
678
+ if not _is_supported_ext(file_ext):
679
  return jsonify({"error": "Unsupported file type. Use PDF, DOCX, or TXT."}), 400
680
+ if len(reference_uploads) > 2:
681
+ return jsonify({"error": "You can upload up to 2 reference documents."}), 400
682
 
683
  try:
684
+ final_file_bytes = uploaded.read()
685
+ final_text_data = _extract_text_data(file_bytes=final_file_bytes, file_ext=file_ext)
686
+ if not final_text_data:
687
+ return jsonify({"error": "Could not extract text from final document."}), 400
688
+
689
+ final_clauses = _extract_clauses(final_text_data, source="final", id_start=0)
690
+ if len(final_clauses) < 2:
691
+ return jsonify({"error": "Not enough clauses found in final document for analysis."}), 400
692
+
693
+ all_clauses = list(final_clauses)
694
+ for idx, ref in enumerate(reference_uploads, start=1):
695
+ if ref is None or ref.filename is None or ref.filename.strip() == "":
696
+ continue
697
+ ref_ext = ref.filename.rsplit(".", 1)[-1].lower() if "." in ref.filename else ""
698
+ if not _is_supported_ext(ref_ext):
699
+ return jsonify({"error": f"Unsupported reference file type for {ref.filename}."}), 400
700
+ ref_text_data = _extract_text_data(file_bytes=ref.read(), file_ext=ref_ext)
701
+ if not ref_text_data:
702
+ continue
703
+ ref_clauses = _extract_clauses(
704
+ ref_text_data,
705
+ source=f"reference_{idx}",
706
+ id_start=len(all_clauses),
707
+ )
708
+ all_clauses.extend(ref_clauses)
709
 
710
+ parties = _extract_document_parties(final_text_data)
711
  findings, line_issues, counts, compared_pairs = _analyze_clauses(
712
+ clauses=all_clauses, threshold=threshold, focus_source="final"
713
  )
714
+ final_line_issues = [item for item in line_issues if item.get("source") == "final"]
715
  page_summaries = _build_page_summaries(
716
+ clauses=final_clauses, line_issues=final_line_issues, text_data=final_text_data
717
  )
718
  detailed_summary = _build_detailed_summary(
719
+ clauses=final_clauses,
720
  page_summaries=page_summaries,
721
  findings=findings,
722
  )
 
732
  "threshold": threshold,
733
  "vendor": parties["vendor"],
734
  "vendee": parties["vendee"],
735
+ "clauses": len(final_clauses),
736
+ "referenceDocs": len([r for r in reference_uploads if r and r.filename]),
737
  "pairsCompared": compared_pairs,
738
  "issuesFound": len(findings),
739
  "duplicationCount": counts["duplication"],
 
744
  "detailedSummary": detailed_summary,
745
  "findings": findings[:50],
746
  "lineIssues": line_issues[:200],
747
+ "finalLineIssues": final_line_issues[:200],
748
  }
749
  ),
750
  200,
frontend/README.md ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Frontend (Multi-Page Flow)
2
+
3
+ This frontend now uses a strict page flow:
4
+
5
+ 1. `index.html` -> Login/Signup
6
+ 2. `upload.html` -> Upload 1-2 reference documents (optional) + final document (required), then run analysis
7
+ 3. `issues.html` -> Line-level issue page (duplication, inconsistency, contradiction)
8
+ 4. `summary.html` -> Final full-document summary
9
+ 5. `dashboard.html` -> Final error dashboard (Reference vs Final comparison + line-level table)
10
+
11
+ ## Run
12
+
13
+ Serve this folder using any static server from `frontend/`:
14
+
15
+ ```bash
16
+ python -m http.server 8080
17
+ ```
18
+
19
+ Open:
20
+
21
+ - `http://127.0.0.1:8080/index.html`
22
+
23
+ ## Backend dependency
24
+
25
+ Frontend expects Flask backend endpoints:
26
+
27
+ - `POST /api/register`
28
+ - `POST /api/login`
29
+ - `POST /api/analyze` (multipart: `file` final doc, optional `referenceFiles[]`, `scanMode`)
30
+
31
+ Fallback aliases are also supported in client code (`/register`, `/login`, `/analyze`) across ports `5000` and `5001`.
32
+
33
+ ## Notes
34
+
35
+ - Login state and analysis payload are stored in `sessionStorage`.
36
+ - If user session is missing, `upload.html`, `issues.html`, and `summary.html` redirect to `index.html`.
37
+ - If analysis payload is missing, `issues.html` and `summary.html` redirect to `upload.html`.
frontend/app.js ADDED
@@ -0,0 +1,667 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const currentHost = window.location.hostname || "127.0.0.1";
2
+
3
+ const API_BASES = [
4
+ `http://${currentHost}:5000/api`,
5
+ `http://${currentHost}:5001/api`,
6
+ "http://127.0.0.1:5000/api",
7
+ "http://localhost:5000/api",
8
+ "http://127.0.0.1:5001/api",
9
+ "http://localhost:5001/api",
10
+ ];
11
+
12
+ const ANALYZE_URLS = [
13
+ `http://${currentHost}:5000/api/analyze`,
14
+ `http://${currentHost}:5000/analyze`,
15
+ `http://${currentHost}:5001/api/analyze`,
16
+ `http://${currentHost}:5001/analyze`,
17
+ "http://127.0.0.1:5000/api/analyze",
18
+ "http://127.0.0.1:5000/analyze",
19
+ "http://localhost:5000/api/analyze",
20
+ "http://localhost:5000/analyze",
21
+ "http://127.0.0.1:5001/api/analyze",
22
+ "http://127.0.0.1:5001/analyze",
23
+ "http://localhost:5001/api/analyze",
24
+ "http://localhost:5001/analyze",
25
+ ];
26
+
27
+ const page = (window.location.pathname.split("/").pop() || "index.html").toLowerCase();
28
+
29
+ function escapeHtml(value) {
30
+ return String(value)
31
+ .replaceAll("&", "&amp;")
32
+ .replaceAll("<", "&lt;")
33
+ .replaceAll(">", "&gt;")
34
+ .replaceAll('"', "&quot;")
35
+ .replaceAll("'", "&#039;");
36
+ }
37
+
38
+ function setText(el, text, type = null) {
39
+ if (!el) return;
40
+ el.textContent = text;
41
+ el.classList.remove("success", "error");
42
+ if (type) el.classList.add(type);
43
+ }
44
+
45
+ function getUser() {
46
+ const userRaw = sessionStorage.getItem("lsi_user");
47
+ if (!userRaw) return null;
48
+ try {
49
+ return JSON.parse(userRaw);
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+
55
+ function setUser(user) {
56
+ sessionStorage.setItem("lsi_user", JSON.stringify(user));
57
+ }
58
+
59
+ function clearSession() {
60
+ sessionStorage.removeItem("lsi_user");
61
+ sessionStorage.removeItem("lsi_analysis_payload");
62
+ }
63
+
64
+ function getAnalysisPayload() {
65
+ const raw = sessionStorage.getItem("lsi_analysis_payload");
66
+ if (!raw) return null;
67
+ try {
68
+ return JSON.parse(raw);
69
+ } catch {
70
+ return null;
71
+ }
72
+ }
73
+
74
+ function setAnalysisPayload(payload) {
75
+ sessionStorage.setItem("lsi_analysis_payload", JSON.stringify(payload));
76
+ }
77
+
78
+ function normalizeSpaces(text) {
79
+ return String(text || "").replace(/\s+/g, " ").trim();
80
+ }
81
+
82
+ function ensureAuth() {
83
+ const user = getUser();
84
+ if (!user) {
85
+ window.location.href = "index.html#home";
86
+ return null;
87
+ }
88
+
89
+ const badge = document.getElementById("userBadge");
90
+ if (badge) {
91
+ badge.textContent = `${user.fullName || user.email || "User"}`;
92
+ }
93
+
94
+ const logoutBtn = document.getElementById("logoutBtn");
95
+ if (logoutBtn) {
96
+ logoutBtn.addEventListener("click", () => {
97
+ clearSession();
98
+ window.location.href = "index.html#home";
99
+ });
100
+ }
101
+
102
+ return user;
103
+ }
104
+
105
+ async function postAuth(endpoint, payload) {
106
+ let response = null;
107
+ let data = null;
108
+ let lastNetworkError = null;
109
+
110
+ for (const base of API_BASES) {
111
+ try {
112
+ response = await fetch(`${base}${endpoint}`, {
113
+ method: "POST",
114
+ headers: { "Content-Type": "application/json" },
115
+ body: JSON.stringify(payload),
116
+ });
117
+ data = await response.json().catch(() => null);
118
+ lastNetworkError = null;
119
+ break;
120
+ } catch (error) {
121
+ lastNetworkError = error;
122
+ }
123
+ }
124
+
125
+ if (lastNetworkError) {
126
+ throw new Error(`Cannot reach backend at ${API_BASES.join(", ")}.`);
127
+ }
128
+
129
+ return { response, data };
130
+ }
131
+
132
+ async function runDocumentAnalysis(formData) {
133
+ let response = null;
134
+ let data = null;
135
+ let lastNetworkError = null;
136
+ let status = null;
137
+
138
+ for (const url of ANALYZE_URLS) {
139
+ try {
140
+ response = await fetch(url, { method: "POST", body: formData });
141
+ data = await response.json().catch(() => null);
142
+ status = response.status;
143
+ lastNetworkError = null;
144
+ if (response.status !== 404) break;
145
+ } catch (error) {
146
+ lastNetworkError = error;
147
+ }
148
+ }
149
+
150
+ if (lastNetworkError) {
151
+ throw new Error("Cannot connect to backend for analysis.");
152
+ }
153
+
154
+ if (!response.ok) {
155
+ throw new Error(data?.error || `Analysis request failed with HTTP ${status || response.status}.`);
156
+ }
157
+
158
+ return data;
159
+ }
160
+
161
+ function buildIssueRows(lineIssues, category) {
162
+ const rows = lineIssues
163
+ .filter((item) => item.category === category)
164
+ .slice(0, 80)
165
+ .map(
166
+ (item) => `
167
+ <tr>
168
+ <td>${escapeHtml(item.location || `Pg ${item.page}, Ln ${item.line}`)}</td>
169
+ <td>${escapeHtml(item.issueType || "-")}</td>
170
+ <td>${escapeHtml(item.confidence ?? "-")}</td>
171
+ </tr>
172
+ `
173
+ )
174
+ .join("");
175
+
176
+ if (!rows) {
177
+ return `<p class="result-muted">No ${category} lines detected.</p>`;
178
+ }
179
+
180
+ return `
181
+ <div class="table-wrap">
182
+ <table class="result-table">
183
+ <thead>
184
+ <tr>
185
+ <th>Page/Line</th>
186
+ <th>Issue Type</th>
187
+ <th>Confidence</th>
188
+ </tr>
189
+ </thead>
190
+ <tbody>${rows}</tbody>
191
+ </table>
192
+ </div>
193
+ `;
194
+ }
195
+
196
+ function initIndexPage() {
197
+ const loginTab = document.getElementById("loginTab");
198
+ const signupTab = document.getElementById("signupTab");
199
+ const authForm = document.getElementById("authForm");
200
+ const nameField = document.getElementById("nameField");
201
+ const fullNameInput = document.getElementById("fullName");
202
+ const emailInput = document.getElementById("email");
203
+ const passwordInput = document.getElementById("password");
204
+ const submitBtn = document.getElementById("submitBtn");
205
+ const formSubtitle = document.getElementById("formSubtitle");
206
+ const message = document.getElementById("message");
207
+
208
+ let mode = "login";
209
+
210
+ function setMode(nextMode) {
211
+ mode = nextMode;
212
+ const isSignup = mode === "signup";
213
+ signupTab.classList.toggle("active", isSignup);
214
+ loginTab.classList.toggle("active", !isSignup);
215
+ nameField.classList.toggle("hidden", !isSignup);
216
+ submitBtn.textContent = isSignup ? "Create Account" : "Login";
217
+ formSubtitle.textContent = isSignup
218
+ ? "Create your account to start securely."
219
+ : "Enter your credentials to access your account.";
220
+ fullNameInput.required = isSignup;
221
+ setText(message, "", null);
222
+ }
223
+
224
+ async function handleAuthSubmit(event) {
225
+ event.preventDefault();
226
+ setText(message, "", null);
227
+
228
+ const email = emailInput.value.trim();
229
+ const password = passwordInput.value;
230
+ const fullName = fullNameInput.value.trim();
231
+
232
+ if (!email || !password || (mode === "signup" && !fullName)) {
233
+ setText(message, "Please fill all required fields.", "error");
234
+ return;
235
+ }
236
+
237
+ submitBtn.disabled = true;
238
+
239
+ try {
240
+ const endpoint = mode === "signup" ? "/register" : "/login";
241
+ const payload = mode === "signup" ? { fullName, email, password } : { email, password };
242
+ const { response, data } = await postAuth(endpoint, payload);
243
+
244
+ if (!response.ok) {
245
+ throw new Error(data?.error || `Request failed with HTTP ${response.status}.`);
246
+ }
247
+
248
+ if (mode === "signup") {
249
+ setText(message, "Account created. Please login now.", "success");
250
+ authForm.reset();
251
+ setMode("login");
252
+ return;
253
+ }
254
+
255
+ const user = data?.user || { fullName: fullName || email, email };
256
+ setUser(user);
257
+ window.location.href = "upload.html";
258
+ } catch (error) {
259
+ setText(message, error.message || "Something went wrong.", "error");
260
+ } finally {
261
+ submitBtn.disabled = false;
262
+ }
263
+ }
264
+
265
+ loginTab.addEventListener("click", () => setMode("login"));
266
+ signupTab.addEventListener("click", () => setMode("signup"));
267
+ authForm.addEventListener("submit", handleAuthSubmit);
268
+ setMode("login");
269
+
270
+ if (getUser()) {
271
+ window.location.href = "upload.html";
272
+ }
273
+ }
274
+
275
+ function initUploadPage() {
276
+ if (!ensureAuth()) return;
277
+
278
+ const uploadForm = document.getElementById("uploadForm");
279
+ const legalFile = document.getElementById("legalFile");
280
+ const referenceFiles = document.getElementById("referenceFiles");
281
+ const scanMode = document.getElementById("scanMode");
282
+ const uploadMessage = document.getElementById("uploadMessage");
283
+ const loadingState = document.getElementById("loadingState");
284
+ const analysisInputSummary = document.getElementById("analysisInputSummary");
285
+
286
+ function renderUploadSummary() {
287
+ if (!legalFile.files || !legalFile.files[0]) return;
288
+ const selectedFile = legalFile.files[0];
289
+ const refs = Array.from((referenceFiles && referenceFiles.files) ? referenceFiles.files : []).slice(0, 2);
290
+ const refNames = refs.length ? refs.map((f) => escapeHtml(f.name)).join(", ") : "None";
291
+
292
+ analysisInputSummary.classList.remove("hidden");
293
+ analysisInputSummary.innerHTML = `
294
+ <p><strong>Final File:</strong> ${escapeHtml(selectedFile.name)}</p>
295
+ <p><strong>Final Type:</strong> ${escapeHtml(selectedFile.type || "unknown")}</p>
296
+ <p><strong>Final Size:</strong> ${escapeHtml((selectedFile.size / 1024).toFixed(2))} KB</p>
297
+ <p><strong>Reference Docs:</strong> ${refs.length}</p>
298
+ <p><strong>Reference Names:</strong> ${refNames}</p>
299
+ <p><strong>Scan Mode:</strong> ${escapeHtml(scanMode.value)}</p>
300
+ `;
301
+ setText(uploadMessage, `Final document selected: ${selectedFile.name}`, "success");
302
+ }
303
+
304
+ legalFile.addEventListener("change", () => {
305
+ renderUploadSummary();
306
+ });
307
+
308
+ if (referenceFiles) {
309
+ referenceFiles.addEventListener("change", () => {
310
+ const refs = Array.from(referenceFiles.files || []);
311
+ if (refs.length > 2) {
312
+ setText(uploadMessage, "Please select at most 2 reference documents.", "error");
313
+ referenceFiles.value = "";
314
+ return;
315
+ }
316
+ renderUploadSummary();
317
+ });
318
+ }
319
+
320
+ scanMode.addEventListener("change", () => {
321
+ renderUploadSummary();
322
+ });
323
+
324
+ uploadForm.addEventListener("submit", async (event) => {
325
+ event.preventDefault();
326
+ setText(uploadMessage, "", null);
327
+
328
+ if (!legalFile.files || legalFile.files.length === 0) {
329
+ setText(uploadMessage, "Please choose a file to continue.", "error");
330
+ return;
331
+ }
332
+
333
+ const selectedFile = legalFile.files[0];
334
+ const selectedScanMode = scanMode.value;
335
+ const refs = Array.from((referenceFiles && referenceFiles.files) ? referenceFiles.files : []);
336
+ if (refs.length > 2) {
337
+ setText(uploadMessage, "You can upload up to 2 reference documents.", "error");
338
+ return;
339
+ }
340
+
341
+ const formData = new FormData();
342
+ formData.append("file", selectedFile);
343
+ formData.append("scanMode", selectedScanMode);
344
+ refs.forEach((file) => formData.append("referenceFiles", file));
345
+
346
+ uploadForm.classList.add("hidden");
347
+ loadingState.classList.remove("hidden");
348
+
349
+ try {
350
+ const payload = await runDocumentAnalysis(formData);
351
+ payload._meta = {
352
+ fileName: selectedFile.name,
353
+ fileType: selectedFile.type || "unknown",
354
+ fileSizeKb: Number((selectedFile.size / 1024).toFixed(2)),
355
+ referenceFiles: refs.map((f) => f.name),
356
+ };
357
+ setAnalysisPayload(payload);
358
+ window.location.href = "issues.html";
359
+ } catch (error) {
360
+ loadingState.classList.add("hidden");
361
+ uploadForm.classList.remove("hidden");
362
+ setText(uploadMessage, error.message || "Analysis failed.", "error");
363
+ }
364
+ });
365
+ }
366
+
367
+ function initIssuesPage() {
368
+ if (!ensureAuth()) return;
369
+
370
+ const payload = getAnalysisPayload();
371
+ if (!payload) {
372
+ window.location.href = "upload.html";
373
+ return;
374
+ }
375
+
376
+ const summary = payload.summary || {};
377
+ const lineIssues = Array.isArray(payload.finalLineIssues)
378
+ ? payload.finalLineIssues
379
+ : Array.isArray(payload.lineIssues)
380
+ ? payload.lineIssues
381
+ : [];
382
+
383
+ const issueStats = document.getElementById("issueStats");
384
+ issueStats.innerHTML = `
385
+ <article class="stat-card stat-dup">
386
+ <h3>Duplication</h3>
387
+ <p>${escapeHtml(summary.duplicationCount ?? 0)}</p>
388
+ </article>
389
+ <article class="stat-card stat-inc">
390
+ <h3>Inconsistency</h3>
391
+ <p>${escapeHtml(summary.inconsistencyCount ?? 0)}</p>
392
+ </article>
393
+ <article class="stat-card stat-con">
394
+ <h3>Contradiction</h3>
395
+ <p>${escapeHtml(summary.contradictionCount ?? 0)}</p>
396
+ </article>
397
+ `;
398
+
399
+ const lineIssueTables = document.getElementById("lineIssueTables");
400
+ lineIssueTables.innerHTML = `
401
+ <section class="result-card">
402
+ <h4>Duplication Lines</h4>
403
+ ${buildIssueRows(lineIssues, "duplication")}
404
+ </section>
405
+ <section class="result-card">
406
+ <h4>Inconsistency Lines</h4>
407
+ ${buildIssueRows(lineIssues, "inconsistency")}
408
+ </section>
409
+ <section class="result-card">
410
+ <h4>Contradiction Lines</h4>
411
+ ${buildIssueRows(lineIssues, "contradiction")}
412
+ </section>
413
+ `;
414
+ }
415
+
416
+ function initSummaryPage() {
417
+ if (!ensureAuth()) return;
418
+
419
+ const payload = getAnalysisPayload();
420
+ if (!payload) {
421
+ window.location.href = "upload.html";
422
+ return;
423
+ }
424
+
425
+ const summary = payload.summary || {};
426
+ const findings = Array.isArray(payload.findings) ? payload.findings : [];
427
+ const pageSummaries = Array.isArray(payload.pageSummaries) ? payload.pageSummaries : [];
428
+ const lineIssues = Array.isArray(payload.finalLineIssues)
429
+ ? payload.finalLineIssues
430
+ : Array.isArray(payload.lineIssues)
431
+ ? payload.lineIssues
432
+ : [];
433
+ const detailedSummary = String(payload.detailedSummary || "").trim();
434
+ const meta = payload._meta || {};
435
+
436
+ const summaryDetails = document.getElementById("summaryDetails");
437
+ summaryDetails.innerHTML = `
438
+ <article class="summary-item"><span>File</span><strong>${escapeHtml(meta.fileName || "-")}</strong></article>
439
+ <article class="summary-item"><span>Scan Mode</span><strong>${escapeHtml(summary.scanMode || "-")}</strong></article>
440
+ <article class="summary-item"><span>Threshold</span><strong>${escapeHtml(summary.threshold ?? "-")}</strong></article>
441
+ <article class="summary-item"><span>Vendor</span><strong>${escapeHtml(summary.vendor || "Not found")}</strong></article>
442
+ <article class="summary-item"><span>Vendee</span><strong>${escapeHtml(summary.vendee || "Not found")}</strong></article>
443
+ <article class="summary-item"><span>Clauses</span><strong>${escapeHtml(summary.clauses ?? 0)}</strong></article>
444
+ <article class="summary-item"><span>Pairs Compared</span><strong>${escapeHtml(summary.pairsCompared ?? 0)}</strong></article>
445
+ <article class="summary-item"><span>Total Issues</span><strong>${escapeHtml(summary.issuesFound ?? 0)}</strong></article>
446
+ <article class="summary-item"><span>Reference Docs</span><strong>${escapeHtml(summary.referenceDocs ?? 0)}</strong></article>
447
+ `;
448
+
449
+ const findingsBoard = document.getElementById("findingsBoard");
450
+ const pageSummaryBoard = document.getElementById("pageSummaryBoard");
451
+ const detailedSummaryText = document.getElementById("detailedSummaryText");
452
+
453
+ if (detailedSummaryText) {
454
+ detailedSummaryText.textContent = detailedSummary || "Detailed summary is not available for this document.";
455
+ }
456
+
457
+ if (pageSummaryBoard) {
458
+ if (pageSummaries.length === 0) {
459
+ pageSummaryBoard.innerHTML =
460
+ `<article class="result-card"><p class="result-muted">No page-wise summary available for this document.</p></article>`;
461
+ } else {
462
+ pageSummaryBoard.innerHTML = pageSummaries
463
+ .map((item) => {
464
+ const keyLines = Array.isArray(item.keyLines) ? item.keyLines : [];
465
+ const keyLineHtml = keyLines.length
466
+ ? keyLines.map((k) => `<li>${escapeHtml(k)}</li>`).join("")
467
+ : "<li>No flagged lines on this page.</li>";
468
+ return `
469
+ <article class="result-card">
470
+ <h4>Page ${escapeHtml(item.page)}</h4>
471
+ <p><strong>Clauses:</strong> ${escapeHtml(item.clauseCount ?? 0)}</p>
472
+ <p><strong>Issues:</strong> ${escapeHtml(item.issueCount ?? 0)} (Duplication: ${escapeHtml(item.duplicationCount ?? 0)}, Inconsistency: ${escapeHtml(item.inconsistencyCount ?? 0)}, Contradiction: ${escapeHtml(item.contradictionCount ?? 0)})</p>
473
+ <p><strong>Page Snippet:</strong> ${escapeHtml(item.pageSnippet || "-")}</p>
474
+ <p><strong>Summary:</strong> ${escapeHtml(item.summaryText || "-")}</p>
475
+ <p><strong>Key Lines:</strong></p>
476
+ <ul>${keyLineHtml}</ul>
477
+ </article>
478
+ `;
479
+ })
480
+ .join("");
481
+ }
482
+ }
483
+
484
+ if (findings.length === 0) {
485
+ findingsBoard.innerHTML = `<article class="result-card"><p class="result-muted">No major findings detected for this document.</p></article>`;
486
+ return;
487
+ }
488
+
489
+ const topFindings = findings.slice(0, 20);
490
+ findingsBoard.innerHTML = topFindings
491
+ .map(
492
+ (item) => `
493
+ <article class="result-card">
494
+ <h4>${escapeHtml(item.category || "issue")} - ${escapeHtml(item.issueType || "-")}</h4>
495
+ <p><strong>Confidence:</strong> ${escapeHtml(item.confidence ?? "-")}</p>
496
+ <p><strong>Location A:</strong> ${escapeHtml(item.location1 || "-")}</p>
497
+ <p><strong>Location B:</strong> ${escapeHtml(item.location2 || "-")}</p>
498
+ <p><strong>Reason:</strong> ${escapeHtml(item.reason || "-")}</p>
499
+ </article>
500
+ `
501
+ )
502
+ .join("");
503
+
504
+ }
505
+
506
+ function initDashboardPage() {
507
+ if (!ensureAuth()) return;
508
+
509
+ const payload = getAnalysisPayload();
510
+ if (!payload) {
511
+ window.location.href = "upload.html";
512
+ return;
513
+ }
514
+
515
+ const findings = Array.isArray(payload.findings) ? payload.findings : [];
516
+ const lineIssues = Array.isArray(payload.finalLineIssues)
517
+ ? payload.finalLineIssues
518
+ : Array.isArray(payload.lineIssues)
519
+ ? payload.lineIssues
520
+ : [];
521
+
522
+ const lineErrorDashboard = document.getElementById("lineErrorDashboard");
523
+ const comparisonBoard = document.getElementById("comparisonBoard");
524
+
525
+ if (lineErrorDashboard) {
526
+ if (lineIssues.length === 0) {
527
+ lineErrorDashboard.innerHTML = `<p class="result-muted">No line-level errors detected.</p>`;
528
+ } else {
529
+ const rows = lineIssues
530
+ .slice(0, 200)
531
+ .map(
532
+ (item) => `
533
+ <tr>
534
+ <td>${escapeHtml(item.location || `Pg ${item.page}, Ln ${item.line}`)}</td>
535
+ <td>${escapeHtml(item.category || "-")}</td>
536
+ <td>${escapeHtml(item.issueType || "-")}</td>
537
+ <td>${escapeHtml(item.confidence ?? "-")}</td>
538
+ <td>${escapeHtml(item.reason || "-")}</td>
539
+ </tr>
540
+ `
541
+ )
542
+ .join("");
543
+
544
+ lineErrorDashboard.innerHTML = `
545
+ <div class="table-wrap">
546
+ <table class="result-table">
547
+ <thead>
548
+ <tr>
549
+ <th>Page/Line</th>
550
+ <th>Category</th>
551
+ <th>Issue Type</th>
552
+ <th>Confidence</th>
553
+ <th>Reason</th>
554
+ </tr>
555
+ </thead>
556
+ <tbody>${rows}</tbody>
557
+ </table>
558
+ </div>
559
+ `;
560
+ }
561
+ }
562
+
563
+ if (comparisonBoard) {
564
+ const crossFindings = findings
565
+ .filter((f) => String(f.source1 || "").startsWith("reference_") || String(f.source2 || "").startsWith("reference_"))
566
+ .slice(0, 80);
567
+
568
+ if (!crossFindings.length) {
569
+ comparisonBoard.innerHTML = `<article class="result-card"><p class="result-muted">Reference vs Final cross-verification mismatches not found.</p></article>`;
570
+ return;
571
+ }
572
+
573
+ function suggestionFor(category, issueType, refText, finalText) {
574
+ const c = String(category || "").toLowerCase();
575
+ const i = String(issueType || "").toLowerCase();
576
+ if (c === "duplication" || i.includes("duplication")) {
577
+ return "இந்த clause repeated/near-duplicate. ஒரே legal meaning உள்ள line-ஐ மட்டும் வைத்துக்கொண்டு மற்றதை remove செய்யவும்.";
578
+ }
579
+ if (c === "inconsistency" || i.includes("inconsistency") || i.includes("numeric")) {
580
+ return "Number/term mismatch இருக்கு. Reference document value-ஐ verify பண்ணி final document-ல் same value update செய்யவும்.";
581
+ }
582
+ if (c === "contradiction" || i.includes("conflict") || i.includes("contradiction")) {
583
+ return "இரண்டு lines opposite meaning கொடுக்குது. Reference document intent எது சரி என்று confirm செய்து final document line-ஐ அதற்கு align செய்யவும்.";
584
+ }
585
+ if (String(refText || "").trim() && String(finalText || "").trim()) {
586
+ return "Reference line மற்றும் final line legal intent same ஆக இருக்கிறதா verify செய்து, ambiguous words remove செய்து rewrite செய்யவும்.";
587
+ }
588
+ return "Clause wording-ஐ reference document-ஓடு compare செய்து consistent version-ஆ மாற்றவும்.";
589
+ }
590
+
591
+ comparisonBoard.innerHTML = crossFindings
592
+ .map((item) => {
593
+ const source1 = String(item.source1 || "");
594
+ const source2 = String(item.source2 || "");
595
+ const firstIsFinal = source1 === "final";
596
+ const finalText = firstIsFinal ? item.clause1 : item.clause2;
597
+ const refText = firstIsFinal ? item.clause2 : item.clause1;
598
+ const finalLoc = firstIsFinal ? item.location1 : item.location2;
599
+ const refLoc = firstIsFinal ? item.location2 : item.location1;
600
+ const refLabel = firstIsFinal ? item.sourceLabel2 : item.sourceLabel1;
601
+ const fixSuggestion = suggestionFor(item.category, item.issueType, refText, finalText);
602
+ return `
603
+ <article class="result-card comparison-card">
604
+ <h4>Error at ${escapeHtml(finalLoc || "-")}</h4>
605
+ <p><strong>Type:</strong> ${escapeHtml(item.category || "issue")} - ${escapeHtml(item.issueType || "-")}</p>
606
+ <p><strong>What is wrong:</strong> ${escapeHtml(item.reason || "-")}</p>
607
+ <p><strong>Original (${escapeHtml(refLabel || "Reference")} - ${escapeHtml(refLoc || "-")}):</strong></p>
608
+ <p class="compare-text">${escapeHtml(refText || "-")}</p>
609
+ <p><strong>Your Final Document (${escapeHtml(finalLoc || "-")}):</strong></p>
610
+ <p class="compare-text">${escapeHtml(finalText || "-")}</p>
611
+ <p><strong>How to rectify:</strong> ${escapeHtml(fixSuggestion)}</p>
612
+ <div class="workflow-actions">
613
+ <button
614
+ type="button"
615
+ class="secondary-btn rectify-btn"
616
+ data-ref-text="${escapeHtml(normalizeSpaces(refText || ""))}"
617
+ data-final-text="${escapeHtml(normalizeSpaces(finalText || ""))}"
618
+ >
619
+ Rectify this line
620
+ </button>
621
+ <span class="rectify-hint">Suggested corrected line will be copied.</span>
622
+ </div>
623
+ </article>
624
+ `;
625
+ })
626
+ .join("");
627
+
628
+ const rectifyButtons = comparisonBoard.querySelectorAll(".rectify-btn");
629
+ rectifyButtons.forEach((btn) => {
630
+ btn.addEventListener("click", async () => {
631
+ const refLine = normalizeSpaces(btn.getAttribute("data-ref-text") || "");
632
+ const finalLine = normalizeSpaces(btn.getAttribute("data-final-text") || "");
633
+ const suggestion = refLine || finalLine || "Review this clause with reference document and update wording for consistency.";
634
+
635
+ try {
636
+ if (navigator.clipboard && navigator.clipboard.writeText) {
637
+ await navigator.clipboard.writeText(suggestion);
638
+ }
639
+ const hint = btn.parentElement && btn.parentElement.querySelector(".rectify-hint");
640
+ if (hint) {
641
+ hint.textContent = "Corrected line copied. Paste into your final document.";
642
+ }
643
+ btn.textContent = "Copied";
644
+ } catch {
645
+ const hint = btn.parentElement && btn.parentElement.querySelector(".rectify-hint");
646
+ if (hint) {
647
+ hint.textContent = `Suggested line: ${suggestion}`;
648
+ }
649
+ }
650
+ });
651
+ });
652
+ }
653
+ }
654
+
655
+ if (page === "index.html" || page === "") {
656
+ initIndexPage();
657
+ } else if (page === "upload.html") {
658
+ initUploadPage();
659
+ } else if (page === "issues.html") {
660
+ initIssuesPage();
661
+ } else if (page === "summary.html") {
662
+ initSummaryPage();
663
+ } else if (page === "dashboard.html") {
664
+ initDashboardPage();
665
+ } else if (page === "workflow.html") {
666
+ window.location.href = "upload.html";
667
+ }
frontend/dashboard.html ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Dashboard | LegalSI</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link
10
+ href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;700;800&family=Space+Grotesk:wght@500;700&display=swap"
11
+ rel="stylesheet"
12
+ />
13
+ <link rel="stylesheet" href="styles.css" />
14
+ </head>
15
+ <body>
16
+ <header class="topbar">
17
+ <div class="container topbar-inner">
18
+ <a class="brand" href="index.html#home">LegalSI</a>
19
+ <div class="page-links">
20
+ <a class="page-link" href="upload.html">Upload</a>
21
+ <a class="page-link" href="issues.html">Analysis</a>
22
+ <a class="page-link" href="summary.html">Summary</a>
23
+ <a class="page-link active" href="dashboard.html">Dashboard</a>
24
+ <button id="logoutBtn" class="logout-btn" type="button">Logout</button>
25
+ </div>
26
+ </div>
27
+ </header>
28
+
29
+ <main class="flow-main">
30
+ <section class="container flow-card">
31
+ <div class="upload-header">
32
+ <h1>Final Error Dashboard</h1>
33
+ <span id="userBadge" class="user-badge"></span>
34
+ </div>
35
+ <p class="upload-subtitle">Each error is shown in simple format: what is wrong, original line, your final line, and how to rectify.</p>
36
+
37
+ <h3 class="section-subtitle">Line Error Table</h3>
38
+ <article class="result-card">
39
+ <div id="lineErrorDashboard"></div>
40
+ </article>
41
+
42
+ <h3 class="section-subtitle">Reference vs Final Comparison</h3>
43
+ <div id="comparisonBoard" class="result-list"></div>
44
+
45
+ <div class="workflow-actions">
46
+ <a class="secondary-btn as-link" href="summary.html">Back to Summary</a>
47
+ <a class="submit-btn as-link submit-link" href="upload.html">Analyze New Document</a>
48
+ </div>
49
+ </section>
50
+ </main>
51
+
52
+ <script src="app.js"></script>
53
+ </body>
54
+ </html>
frontend/issues.html ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Analysis | LegalSI</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link
10
+ href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;700;800&family=Space+Grotesk:wght@500;700&display=swap"
11
+ rel="stylesheet"
12
+ />
13
+ <link rel="stylesheet" href="styles.css" />
14
+ </head>
15
+ <body>
16
+ <header class="topbar">
17
+ <div class="container topbar-inner">
18
+ <a class="brand" href="index.html#home">LegalSI</a>
19
+ <div class="page-links">
20
+ <a class="page-link" href="upload.html">Upload</a>
21
+ <a class="page-link active" href="issues.html">Analysis</a>
22
+ <a class="page-link" href="summary.html">Summary</a>
23
+ <a class="page-link" href="dashboard.html">Dashboard</a>
24
+ <button id="logoutBtn" class="logout-btn" type="button">Logout</button>
25
+ </div>
26
+ </div>
27
+ </header>
28
+
29
+ <main class="flow-main">
30
+ <section class="container flow-card">
31
+ <div class="upload-header">
32
+ <h1>Line-Level Analysis</h1>
33
+ <span id="userBadge" class="user-badge"></span>
34
+ </div>
35
+ <p class="upload-subtitle">Inconsistencies, contradictions, and duplications with page and line references.</p>
36
+
37
+ <div id="issueStats" class="stats-grid"></div>
38
+ <div id="lineIssueTables"></div>
39
+
40
+ <div class="workflow-actions">
41
+ <a class="secondary-btn as-link" href="upload.html">Back to Upload</a>
42
+ <a class="submit-btn as-link submit-link" href="summary.html">Next: Summary</a>
43
+ </div>
44
+ </section>
45
+ </main>
46
+
47
+ <script src="app.js"></script>
48
+ </body>
49
+ </html>
frontend/styles.css ADDED
@@ -0,0 +1,981 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --bg: #f3f5f8;
3
+ --surface: #ffffff;
4
+ --surface-soft: #f8fafc;
5
+ --ink: #0e2238;
6
+ --muted: #5b6f85;
7
+ --border: #d3dee9;
8
+ --navy: #12385f;
9
+ --navy-2: #1f4d79;
10
+ --gold: #b78a28;
11
+ --primary: #1f5fa6;
12
+ --primary-2: #2e79c8;
13
+ --teal: #1f8a75;
14
+ --danger: #b93f4f;
15
+ --ok: #166a47;
16
+ }
17
+
18
+ * {
19
+ box-sizing: border-box;
20
+ }
21
+
22
+ html {
23
+ scroll-behavior: smooth;
24
+ }
25
+
26
+ body {
27
+ margin: 0;
28
+ font-family: "Manrope", sans-serif;
29
+ color: var(--ink);
30
+ background:
31
+ radial-gradient(1000px 450px at -10% -8%, #dfe9f5 0%, rgba(223, 233, 245, 0) 60%),
32
+ radial-gradient(900px 420px at 110% -10%, #ece5d5 0%, rgba(236, 229, 213, 0) 58%),
33
+ linear-gradient(180deg, #eff3f7 0%, #f8fafd 42%, #ffffff 100%);
34
+ line-height: 1.45;
35
+ }
36
+
37
+ .container {
38
+ width: min(1180px, 92%);
39
+ margin: 0 auto;
40
+ }
41
+
42
+ .topbar {
43
+ position: sticky;
44
+ top: 0;
45
+ z-index: 20;
46
+ background: rgba(249, 251, 253, 0.94);
47
+ backdrop-filter: blur(6px);
48
+ border-bottom: 1px solid #cfd9e4;
49
+ box-shadow: 0 4px 18px rgba(14, 34, 56, 0.06);
50
+ }
51
+
52
+ .topbar-inner {
53
+ display: flex;
54
+ align-items: center;
55
+ justify-content: space-between;
56
+ min-height: 68px;
57
+ }
58
+
59
+ .brand {
60
+ font-family: "Space Grotesk", sans-serif;
61
+ font-size: 24px;
62
+ font-weight: 700;
63
+ color: var(--navy);
64
+ text-decoration: none;
65
+ }
66
+
67
+ .nav-links {
68
+ display: flex;
69
+ gap: 20px;
70
+ }
71
+
72
+ .nav-links a {
73
+ color: #264868;
74
+ text-decoration: none;
75
+ font-weight: 700;
76
+ font-size: 14px;
77
+ padding: 6px 8px;
78
+ border-radius: 8px;
79
+ }
80
+
81
+ .nav-links a:hover {
82
+ background: #e9f0f8;
83
+ color: var(--navy);
84
+ }
85
+
86
+ .hero {
87
+ position: relative;
88
+ padding: 48px 0 42px;
89
+ overflow: hidden;
90
+ }
91
+
92
+ .hero-bg {
93
+ position: absolute;
94
+ inset: 0;
95
+ background:
96
+ linear-gradient(120deg, rgba(18, 56, 95, 0.1), rgba(183, 138, 40, 0.09)),
97
+ url("assets/legal-tech-bg.svg") right center / cover no-repeat;
98
+ opacity: 0.95;
99
+ pointer-events: none;
100
+ }
101
+
102
+ .hero-grid {
103
+ position: relative;
104
+ display: grid;
105
+ grid-template-columns: 1.1fr 0.95fr;
106
+ gap: 24px;
107
+ align-items: start;
108
+ }
109
+
110
+ .hero-copy {
111
+ background: rgba(255, 255, 255, 0.82);
112
+ border: 1px solid var(--border);
113
+ border-radius: 20px;
114
+ padding: 24px;
115
+ box-shadow: 0 14px 34px rgba(15, 38, 66, 0.11);
116
+ animation: fadeInUp 0.45s ease-out;
117
+ }
118
+
119
+ .eyebrow {
120
+ margin: 0 0 10px;
121
+ font-size: 13px;
122
+ letter-spacing: 0.05em;
123
+ text-transform: uppercase;
124
+ color: var(--navy-2);
125
+ font-weight: 800;
126
+ }
127
+
128
+ .hero-copy h1 {
129
+ margin: 0;
130
+ font-size: clamp(30px, 4.6vw, 50px);
131
+ line-height: 1.08;
132
+ font-family: "Space Grotesk", sans-serif;
133
+ }
134
+
135
+ .hero-text {
136
+ margin: 14px 0 18px;
137
+ color: var(--muted);
138
+ line-height: 1.6;
139
+ max-width: 66ch;
140
+ }
141
+
142
+ .hero-cta-row {
143
+ display: flex;
144
+ gap: 10px;
145
+ flex-wrap: wrap;
146
+ margin: 8px 0 14px;
147
+ }
148
+
149
+ .hero-cta-primary,
150
+ .hero-cta-secondary {
151
+ text-decoration: none;
152
+ border-radius: 11px;
153
+ font-size: 14px;
154
+ font-weight: 800;
155
+ padding: 10px 14px;
156
+ }
157
+
158
+ .hero-cta-primary {
159
+ color: #ffffff;
160
+ background: linear-gradient(92deg, var(--navy), var(--primary-2) 58%, var(--teal));
161
+ box-shadow: 0 10px 18px rgba(17, 62, 110, 0.22);
162
+ }
163
+
164
+ .hero-cta-secondary {
165
+ color: #1c446b;
166
+ background: #ecf4ff;
167
+ border: 1px solid #bfd6f2;
168
+ }
169
+
170
+ .trust-strip {
171
+ display: flex;
172
+ flex-wrap: wrap;
173
+ gap: 8px;
174
+ margin: 0 0 14px;
175
+ }
176
+
177
+ .trust-strip span {
178
+ border: 1px solid #d0dded;
179
+ border-radius: 999px;
180
+ padding: 5px 10px;
181
+ font-size: 12px;
182
+ font-weight: 700;
183
+ color: #315579;
184
+ background: #f5f9ff;
185
+ }
186
+
187
+ .hero-metrics {
188
+ display: grid;
189
+ grid-template-columns: repeat(3, 1fr);
190
+ gap: 10px;
191
+ }
192
+
193
+ .hero-metrics > div {
194
+ border: 1px solid #d5e2f0;
195
+ background: #ffffff;
196
+ border-radius: 12px;
197
+ padding: 12px;
198
+ transition: transform 0.18s ease, box-shadow 0.18s ease;
199
+ }
200
+
201
+ .hero-metrics > div:hover {
202
+ transform: translateY(-2px);
203
+ box-shadow: 0 10px 18px rgba(16, 43, 74, 0.09);
204
+ }
205
+
206
+ .hero-metrics h3 {
207
+ margin: 0;
208
+ font-size: 14px;
209
+ }
210
+
211
+ .hero-metrics p {
212
+ margin: 6px 0 0;
213
+ color: var(--muted);
214
+ font-size: 12px;
215
+ }
216
+
217
+ .preview-card {
218
+ margin-top: 12px;
219
+ border: 1px solid #ccdaea;
220
+ border-radius: 14px;
221
+ padding: 12px;
222
+ background: linear-gradient(160deg, #f7fbff 0%, #edf5ff 100%);
223
+ }
224
+
225
+ .preview-card h3 {
226
+ margin: 0 0 10px;
227
+ font-size: 14px;
228
+ color: #163a60;
229
+ }
230
+
231
+ .preview-grid {
232
+ display: grid;
233
+ grid-template-columns: repeat(4, 1fr);
234
+ gap: 8px;
235
+ }
236
+
237
+ .preview-grid div {
238
+ border: 1px solid #c8d9ec;
239
+ border-radius: 10px;
240
+ background: #ffffff;
241
+ padding: 8px;
242
+ display: grid;
243
+ gap: 3px;
244
+ }
245
+
246
+ .preview-grid span {
247
+ font-size: 11px;
248
+ color: #5a7090;
249
+ }
250
+
251
+ .preview-grid strong {
252
+ font-size: 19px;
253
+ color: #15395e;
254
+ }
255
+
256
+ .panel {
257
+ background: var(--surface);
258
+ border: 1px solid var(--border);
259
+ border-radius: 18px;
260
+ box-shadow: 0 14px 30px rgba(12, 31, 53, 0.12);
261
+ animation: fadeInUp 0.5s ease-out;
262
+ }
263
+
264
+ .auth-panel {
265
+ padding: 22px;
266
+ }
267
+
268
+ .form-header {
269
+ margin-bottom: 18px;
270
+ }
271
+
272
+ .switcher {
273
+ display: grid;
274
+ grid-template-columns: 1fr 1fr;
275
+ background: #e9eff7;
276
+ border-radius: 12px;
277
+ padding: 4px;
278
+ margin-bottom: 12px;
279
+ }
280
+
281
+ .switcher button {
282
+ border: 0;
283
+ background: transparent;
284
+ border-radius: 9px;
285
+ padding: 10px;
286
+ font-weight: 800;
287
+ cursor: pointer;
288
+ color: #315579;
289
+ transition: background 0.2s ease, color 0.2s ease, transform 0.12s ease;
290
+ }
291
+
292
+ .switcher button.active {
293
+ color: #112a48;
294
+ background: #ffffff;
295
+ box-shadow: 0 6px 14px rgba(8, 26, 49, 0.08);
296
+ }
297
+
298
+ .switcher button:active {
299
+ transform: scale(0.98);
300
+ }
301
+
302
+ #formSubtitle {
303
+ margin: 0;
304
+ color: var(--muted);
305
+ font-size: 14px;
306
+ }
307
+
308
+ .auth-form {
309
+ display: grid;
310
+ gap: 14px;
311
+ }
312
+
313
+ .field {
314
+ display: grid;
315
+ gap: 7px;
316
+ }
317
+
318
+ .field label {
319
+ font-size: 14px;
320
+ font-weight: 700;
321
+ }
322
+
323
+ .field input,
324
+ .control {
325
+ border: 1px solid var(--border);
326
+ border-radius: 12px;
327
+ padding: 12px 13px;
328
+ font: inherit;
329
+ background: #ffffff;
330
+ outline: none;
331
+ width: 100%;
332
+ }
333
+
334
+ .field input:focus,
335
+ .control:focus {
336
+ border-color: var(--primary);
337
+ box-shadow: 0 0 0 4px rgba(31, 95, 166, 0.16);
338
+ }
339
+
340
+ .hidden {
341
+ display: none;
342
+ }
343
+
344
+ .submit-btn {
345
+ margin-top: 8px;
346
+ border: 0;
347
+ border-radius: 12px;
348
+ padding: 12px;
349
+ background: linear-gradient(92deg, var(--navy), var(--primary-2) 58%, var(--teal));
350
+ color: #ffffff;
351
+ font-weight: 800;
352
+ font-size: 15px;
353
+ cursor: pointer;
354
+ transition: transform 0.16s ease, box-shadow 0.16s ease, filter 0.16s ease;
355
+ }
356
+
357
+ .submit-btn:hover {
358
+ filter: brightness(1.03);
359
+ transform: translateY(-1px);
360
+ box-shadow: 0 10px 18px rgba(17, 62, 110, 0.22);
361
+ }
362
+
363
+ .message {
364
+ min-height: 22px;
365
+ margin: 14px 0 0;
366
+ font-size: 14px;
367
+ font-weight: 700;
368
+ }
369
+
370
+ .message.success {
371
+ color: var(--ok);
372
+ }
373
+
374
+ .message.error {
375
+ color: var(--danger);
376
+ }
377
+
378
+ .upload-header {
379
+ display: flex;
380
+ align-items: center;
381
+ justify-content: space-between;
382
+ }
383
+
384
+ .upload-header h2 {
385
+ margin: 0;
386
+ font-family: "Space Grotesk", sans-serif;
387
+ }
388
+
389
+ .upload-subtitle {
390
+ margin: 10px 0 18px;
391
+ color: var(--muted);
392
+ }
393
+
394
+ .stepper {
395
+ display: grid;
396
+ grid-template-columns: repeat(3, 1fr);
397
+ gap: 8px;
398
+ margin: 10px 0 16px;
399
+ }
400
+
401
+ .step-chip {
402
+ text-align: center;
403
+ border: 1px solid var(--border);
404
+ border-radius: 10px;
405
+ padding: 8px 10px;
406
+ font-size: 13px;
407
+ font-weight: 800;
408
+ color: #5d7190;
409
+ background: #f3f6fb;
410
+ transition: all 0.2s ease;
411
+ }
412
+
413
+ .step-chip.active {
414
+ color: #0f2d4e;
415
+ border-color: #b7cde7;
416
+ background: #e8f1fc;
417
+ box-shadow: inset 0 0 0 1px rgba(38, 97, 166, 0.15);
418
+ }
419
+
420
+ .workflow-step {
421
+ margin-top: 6px;
422
+ }
423
+
424
+ .summary-box {
425
+ border: 1px solid var(--border);
426
+ border-radius: 12px;
427
+ background: var(--surface-soft);
428
+ padding: 12px;
429
+ color: #25496f;
430
+ box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.6);
431
+ }
432
+
433
+ .summary-box p {
434
+ margin: 5px 0;
435
+ font-size: 14px;
436
+ }
437
+
438
+ .workflow-actions {
439
+ display: flex;
440
+ gap: 10px;
441
+ margin-top: 12px;
442
+ flex-wrap: wrap;
443
+ }
444
+
445
+ .upload-zone-wrap {
446
+ margin-top: 2px;
447
+ }
448
+
449
+ .upload-zone {
450
+ border: 1.5px dashed #b8cbe0;
451
+ border-radius: 14px;
452
+ background: linear-gradient(180deg, #f8fbff 0%, #f3f8ff 100%);
453
+ min-height: 132px;
454
+ display: grid;
455
+ place-content: center;
456
+ text-align: center;
457
+ gap: 6px;
458
+ cursor: pointer;
459
+ padding: 14px;
460
+ transition: border-color 0.2s ease, background 0.2s ease, transform 0.18s ease;
461
+ }
462
+
463
+ .upload-zone:hover {
464
+ border-color: #7ca4cf;
465
+ background: linear-gradient(180deg, #fafdff 0%, #eef5ff 100%);
466
+ transform: translateY(-1px);
467
+ }
468
+
469
+ .upload-icon {
470
+ width: 34px;
471
+ height: 34px;
472
+ border-radius: 999px;
473
+ margin: 0 auto;
474
+ display: grid;
475
+ place-content: center;
476
+ font-size: 22px;
477
+ font-weight: 700;
478
+ color: #21507f;
479
+ background: #e5eef9;
480
+ }
481
+
482
+ .upload-title {
483
+ font-size: 14px;
484
+ font-weight: 800;
485
+ color: #1f4469;
486
+ }
487
+
488
+ .upload-hint {
489
+ font-size: 12px;
490
+ color: #5f7691;
491
+ }
492
+
493
+ .file-input-hidden {
494
+ position: absolute;
495
+ left: -10000px;
496
+ width: 1px;
497
+ height: 1px;
498
+ opacity: 0;
499
+ }
500
+
501
+ .chat-panel {
502
+ border: 1px solid var(--border);
503
+ border-radius: 12px;
504
+ background: #f7fbff;
505
+ padding: 12px;
506
+ margin-top: 10px;
507
+ display: grid;
508
+ gap: 10px;
509
+ max-height: 220px;
510
+ overflow-y: auto;
511
+ }
512
+
513
+ .chat-bubble {
514
+ padding: 10px 12px;
515
+ border-radius: 12px;
516
+ font-size: 13px;
517
+ line-height: 1.5;
518
+ }
519
+
520
+ .chat-bubble.user {
521
+ justify-self: end;
522
+ max-width: 92%;
523
+ background: #e8f1ff;
524
+ border: 1px solid #bfd6f4;
525
+ color: #1f4268;
526
+ }
527
+
528
+ .chat-bubble.bot {
529
+ justify-self: start;
530
+ max-width: 96%;
531
+ background: #ffffff;
532
+ border: 1px solid #d4e0ee;
533
+ color: #274968;
534
+ }
535
+
536
+ .logout-btn {
537
+ border: 1px solid var(--border);
538
+ background: #ffffff;
539
+ border-radius: 10px;
540
+ padding: 8px 12px;
541
+ font-weight: 700;
542
+ cursor: pointer;
543
+ }
544
+
545
+ .secondary-btn {
546
+ border: 1px solid #b8cbe0;
547
+ background: #f1f6fc;
548
+ color: #1f4469;
549
+ border-radius: 12px;
550
+ padding: 12px 14px;
551
+ font-weight: 800;
552
+ font-size: 14px;
553
+ cursor: pointer;
554
+ transition: background 0.18s ease, transform 0.14s ease;
555
+ }
556
+
557
+ .secondary-btn:hover {
558
+ background: #e7f0fa;
559
+ }
560
+
561
+ .secondary-btn:active {
562
+ transform: scale(0.98);
563
+ }
564
+
565
+ .section {
566
+ padding: 20px 0 26px;
567
+ }
568
+
569
+ .section-card {
570
+ background: var(--surface);
571
+ border: 1px solid var(--border);
572
+ border-radius: 18px;
573
+ padding: 24px;
574
+ box-shadow: 0 10px 24px rgba(12, 34, 58, 0.09);
575
+ transition: box-shadow 0.2s ease, transform 0.2s ease;
576
+ }
577
+
578
+ .section-card:hover {
579
+ box-shadow: 0 14px 26px rgba(12, 34, 58, 0.13);
580
+ transform: translateY(-1px);
581
+ }
582
+
583
+ .section-card h2 {
584
+ margin: 0 0 10px;
585
+ font-family: "Space Grotesk", sans-serif;
586
+ }
587
+
588
+ .section-card p {
589
+ margin: 0;
590
+ color: var(--muted);
591
+ line-height: 1.7;
592
+ }
593
+
594
+ .service-grid {
595
+ margin-top: 14px;
596
+ display: grid;
597
+ grid-template-columns: repeat(3, 1fr);
598
+ gap: 12px;
599
+ }
600
+
601
+ .service-grid article {
602
+ border: 1px solid var(--border);
603
+ border-radius: 12px;
604
+ padding: 14px;
605
+ background: var(--surface-soft);
606
+ }
607
+
608
+ .service-grid h3 {
609
+ margin: 0 0 8px;
610
+ font-size: 16px;
611
+ }
612
+
613
+ .contact-grid {
614
+ margin-top: 14px;
615
+ display: grid;
616
+ gap: 8px;
617
+ color: #193b61;
618
+ }
619
+
620
+ .analysis-result {
621
+ margin-top: 16px;
622
+ border-top: 1px solid var(--border);
623
+ padding-top: 14px;
624
+ }
625
+
626
+ .result-summary h3 {
627
+ margin: 0 0 8px;
628
+ font-family: "Space Grotesk", sans-serif;
629
+ }
630
+
631
+ .result-summary p {
632
+ margin: 4px 0;
633
+ color: #1d3352;
634
+ }
635
+
636
+ .result-visual {
637
+ margin-top: 12px;
638
+ border: 1px solid var(--border);
639
+ border-radius: 12px;
640
+ padding: 12px;
641
+ background: linear-gradient(180deg, #f8fbff 0%, #f4f8fd 100%);
642
+ }
643
+
644
+ .result-visual h3 {
645
+ margin: 0 0 10px;
646
+ }
647
+
648
+ .bar-row {
649
+ display: grid;
650
+ grid-template-columns: 170px 1fr 52px;
651
+ align-items: center;
652
+ gap: 8px;
653
+ margin-bottom: 8px;
654
+ }
655
+
656
+ .bar-label,
657
+ .bar-value {
658
+ font-size: 13px;
659
+ font-weight: 700;
660
+ }
661
+
662
+ .bar-track {
663
+ width: 100%;
664
+ height: 12px;
665
+ border-radius: 999px;
666
+ background: #dde5f1;
667
+ overflow: hidden;
668
+ }
669
+
670
+ .bar-fill {
671
+ height: 100%;
672
+ border-radius: 999px;
673
+ }
674
+
675
+ .bar-fill.dup {
676
+ background: #2d6ec8;
677
+ }
678
+
679
+ .bar-fill.inc {
680
+ background: #d08f28;
681
+ }
682
+
683
+ .bar-fill.con {
684
+ background: #bd4b58;
685
+ }
686
+
687
+ .result-list {
688
+ margin-top: 12px;
689
+ display: grid;
690
+ gap: 10px;
691
+ }
692
+
693
+ .result-card {
694
+ border: 1px solid var(--border);
695
+ border-radius: 12px;
696
+ padding: 10px 12px;
697
+ background: #f9fbfe;
698
+ transition: box-shadow 0.16s ease;
699
+ }
700
+
701
+ .result-card:hover {
702
+ box-shadow: 0 10px 20px rgba(12, 34, 58, 0.09);
703
+ }
704
+
705
+ .result-card h4 {
706
+ margin: 0 0 6px;
707
+ }
708
+
709
+ .result-muted {
710
+ color: var(--muted);
711
+ }
712
+
713
+ .table-wrap {
714
+ width: 100%;
715
+ overflow-x: auto;
716
+ }
717
+
718
+ .result-table {
719
+ width: 100%;
720
+ border-collapse: collapse;
721
+ margin-top: 8px;
722
+ }
723
+
724
+ .result-table th,
725
+ .result-table td {
726
+ border: 1px solid var(--border);
727
+ padding: 8px;
728
+ text-align: left;
729
+ font-size: 13px;
730
+ vertical-align: top;
731
+ }
732
+
733
+ .result-table th {
734
+ background: #eef4ff;
735
+ }
736
+
737
+ @keyframes fadeInUp {
738
+ from {
739
+ opacity: 0;
740
+ transform: translateY(8px);
741
+ }
742
+ to {
743
+ opacity: 1;
744
+ transform: translateY(0);
745
+ }
746
+ }
747
+
748
+ @media (max-width: 980px) {
749
+ .hero-grid {
750
+ grid-template-columns: 1fr;
751
+ }
752
+
753
+ .hero-metrics {
754
+ grid-template-columns: 1fr;
755
+ }
756
+
757
+ .preview-grid {
758
+ grid-template-columns: repeat(2, 1fr);
759
+ }
760
+
761
+ .service-grid {
762
+ grid-template-columns: 1fr;
763
+ }
764
+
765
+ .bar-row {
766
+ grid-template-columns: 1fr;
767
+ gap: 6px;
768
+ }
769
+
770
+ .nav-links {
771
+ gap: 12px;
772
+ flex-wrap: wrap;
773
+ justify-content: flex-end;
774
+ }
775
+
776
+ .topbar-inner {
777
+ padding-block: 8px;
778
+ }
779
+ }
780
+
781
+ .page-links {
782
+ display: flex;
783
+ align-items: center;
784
+ gap: 10px;
785
+ }
786
+
787
+ .page-link {
788
+ border: 1px solid #bfd0e3;
789
+ border-radius: 10px;
790
+ padding: 6px 10px;
791
+ font-size: 13px;
792
+ font-weight: 700;
793
+ color: #23496f;
794
+ text-decoration: none;
795
+ background: #f4f8fd;
796
+ }
797
+
798
+ .page-link.active {
799
+ background: #e7f1ff;
800
+ border-color: #98b9dc;
801
+ color: #14395f;
802
+ }
803
+
804
+ .flow-main {
805
+ padding: 28px 0 36px;
806
+ }
807
+
808
+ .flow-card {
809
+ background: var(--surface);
810
+ border: 1px solid var(--border);
811
+ border-radius: 18px;
812
+ box-shadow: 0 14px 30px rgba(12, 31, 53, 0.12);
813
+ padding: 22px;
814
+ }
815
+
816
+ .flow-card h1 {
817
+ margin: 0;
818
+ font-family: "Space Grotesk", sans-serif;
819
+ font-size: clamp(28px, 4vw, 40px);
820
+ }
821
+
822
+ .user-badge {
823
+ border: 1px solid #c6d9ee;
824
+ border-radius: 999px;
825
+ padding: 8px 12px;
826
+ background: #f2f8ff;
827
+ color: #24486d;
828
+ font-weight: 700;
829
+ font-size: 13px;
830
+ }
831
+
832
+ .loading-panel {
833
+ margin-top: 16px;
834
+ border: 1px solid var(--border);
835
+ border-radius: 12px;
836
+ padding: 18px;
837
+ background: #f5f9ff;
838
+ display: grid;
839
+ justify-items: center;
840
+ gap: 10px;
841
+ }
842
+
843
+ .spinner {
844
+ width: 30px;
845
+ height: 30px;
846
+ border: 3px solid #c8d8eb;
847
+ border-top-color: #1f5fa6;
848
+ border-radius: 50%;
849
+ animation: spin 0.8s linear infinite;
850
+ }
851
+
852
+ .stats-grid {
853
+ display: grid;
854
+ grid-template-columns: repeat(3, minmax(0, 1fr));
855
+ gap: 10px;
856
+ margin: 12px 0 14px;
857
+ }
858
+
859
+ .stat-card {
860
+ border: 1px solid var(--border);
861
+ border-radius: 12px;
862
+ padding: 12px;
863
+ background: #f9fbfe;
864
+ }
865
+
866
+ .stat-card h3 {
867
+ margin: 0;
868
+ font-size: 14px;
869
+ }
870
+
871
+ .stat-card p {
872
+ margin: 6px 0 0;
873
+ font-size: 28px;
874
+ font-weight: 800;
875
+ }
876
+
877
+ .stat-dup p {
878
+ color: #2d6ec8;
879
+ }
880
+
881
+ .stat-inc p {
882
+ color: #d08f28;
883
+ }
884
+
885
+ .stat-con p {
886
+ color: #bd4b58;
887
+ }
888
+
889
+ .summary-grid {
890
+ display: grid;
891
+ grid-template-columns: repeat(2, minmax(0, 1fr));
892
+ gap: 10px;
893
+ margin-bottom: 14px;
894
+ }
895
+
896
+ .summary-item {
897
+ border: 1px solid var(--border);
898
+ border-radius: 12px;
899
+ padding: 10px;
900
+ background: #f9fbfe;
901
+ display: grid;
902
+ gap: 4px;
903
+ }
904
+
905
+ .summary-item span {
906
+ color: var(--muted);
907
+ font-size: 13px;
908
+ }
909
+
910
+ .summary-item strong {
911
+ color: #1b3d63;
912
+ font-size: 14px;
913
+ }
914
+
915
+ .section-subtitle {
916
+ margin: 6px 0 10px;
917
+ font-family: "Space Grotesk", sans-serif;
918
+ font-size: 20px;
919
+ color: #183d62;
920
+ }
921
+
922
+ .detailed-summary-text {
923
+ white-space: pre-wrap;
924
+ line-height: 1.65;
925
+ color: #1d3552;
926
+ font-size: 14px;
927
+ }
928
+
929
+ .comparison-card {
930
+ border-left: 4px solid #2d6ec8;
931
+ }
932
+
933
+ .compare-text {
934
+ border: 1px solid #d2e2f2;
935
+ border-radius: 10px;
936
+ padding: 8px;
937
+ background: #ffffff;
938
+ white-space: pre-wrap;
939
+ line-height: 1.55;
940
+ color: #1a3d61;
941
+ }
942
+
943
+ .rectify-btn {
944
+ min-width: 170px;
945
+ }
946
+
947
+ .rectify-hint {
948
+ font-size: 12px;
949
+ color: #496787;
950
+ align-self: center;
951
+ }
952
+
953
+ .as-link {
954
+ text-decoration: none;
955
+ display: inline-flex;
956
+ align-items: center;
957
+ justify-content: center;
958
+ }
959
+
960
+ .submit-link {
961
+ min-width: 220px;
962
+ }
963
+
964
+ @keyframes spin {
965
+ to {
966
+ transform: rotate(360deg);
967
+ }
968
+ }
969
+
970
+ @media (max-width: 980px) {
971
+ .page-links {
972
+ gap: 6px;
973
+ flex-wrap: wrap;
974
+ justify-content: flex-end;
975
+ }
976
+
977
+ .stats-grid,
978
+ .summary-grid {
979
+ grid-template-columns: 1fr;
980
+ }
981
+ }
frontend/summary.html ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Summary | LegalSI</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link
10
+ href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;700;800&family=Space+Grotesk:wght@500;700&display=swap"
11
+ rel="stylesheet"
12
+ />
13
+ <link rel="stylesheet" href="styles.css" />
14
+ </head>
15
+ <body>
16
+ <header class="topbar">
17
+ <div class="container topbar-inner">
18
+ <a class="brand" href="index.html#home">LegalSI</a>
19
+ <div class="page-links">
20
+ <a class="page-link" href="upload.html">Upload</a>
21
+ <a class="page-link" href="issues.html">Analysis</a>
22
+ <a class="page-link active" href="summary.html">Summary</a>
23
+ <a class="page-link" href="dashboard.html">Dashboard</a>
24
+ <button id="logoutBtn" class="logout-btn" type="button">Logout</button>
25
+ </div>
26
+ </div>
27
+ </header>
28
+
29
+ <main class="flow-main">
30
+ <section class="container flow-card">
31
+ <div class="upload-header">
32
+ <h1>Document Summary</h1>
33
+ <span id="userBadge" class="user-badge"></span>
34
+ </div>
35
+ <p class="upload-subtitle">Overall analysis result for the entire uploaded legal document.</p>
36
+
37
+ <div id="summaryDetails" class="summary-grid"></div>
38
+ <h3 class="section-subtitle">Detailed Document Summary</h3>
39
+ <article class="result-card">
40
+ <div id="detailedSummaryText" class="detailed-summary-text"></div>
41
+ </article>
42
+ <h3 class="section-subtitle">Page-wise Summary</h3>
43
+ <div id="pageSummaryBoard" class="result-list"></div>
44
+ <h3 class="section-subtitle">Top Findings</h3>
45
+ <div id="findingsBoard" class="result-list"></div>
46
+
47
+ <div class="workflow-actions">
48
+ <a class="secondary-btn as-link" href="issues.html">Back to Analysis</a>
49
+ <a class="submit-btn as-link submit-link" href="dashboard.html">Next: Dashboard</a>
50
+ </div>
51
+ </section>
52
+ </main>
53
+
54
+ <script src="app.js"></script>
55
+ </body>
56
+ </html>
frontend/upload.html ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Upload Document | LegalSI</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link
10
+ href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;700;800&family=Space+Grotesk:wght@500;700&display=swap"
11
+ rel="stylesheet"
12
+ />
13
+ <link rel="stylesheet" href="styles.css" />
14
+ </head>
15
+ <body>
16
+ <header class="topbar">
17
+ <div class="container topbar-inner">
18
+ <a class="brand" href="index.html#home">LegalSI</a>
19
+ <div class="page-links">
20
+ <a class="page-link active" href="upload.html">Upload</a>
21
+ <a class="page-link" href="issues.html">Analysis</a>
22
+ <a class="page-link" href="summary.html">Summary</a>
23
+ <a class="page-link" href="dashboard.html">Dashboard</a>
24
+ <button id="logoutBtn" class="logout-btn" type="button">Logout</button>
25
+ </div>
26
+ </div>
27
+ </header>
28
+
29
+ <main class="flow-main">
30
+ <section class="container flow-card">
31
+ <div class="upload-header">
32
+ <h1>Upload Document</h1>
33
+ <span id="userBadge" class="user-badge"></span>
34
+ </div>
35
+ <p class="upload-subtitle">Upload 1-2 reference documents for cross verification, then upload the final document for analysis.</p>
36
+
37
+ <form id="uploadForm" class="auth-form" novalidate>
38
+ <div class="field">
39
+ <label for="scanMode">Scan Mode</label>
40
+ <select id="scanMode" class="control">
41
+ <option>Standard Scan (Recommended)</option>
42
+ <option>Deep Search (Fuzzy)</option>
43
+ <option>Strict (Duplicates Only)</option>
44
+ </select>
45
+ </div>
46
+
47
+ <div class="field upload-zone-wrap">
48
+ <label for="referenceFiles">Reference Documents</label>
49
+ <label class="upload-zone" for="referenceFiles">
50
+ <span class="upload-icon">+</span>
51
+ <span class="upload-title">Upload 1 or 2 reference documents for cross verification</span>
52
+ <span class="upload-hint">Supported: PDF, DOCX, TXT</span>
53
+ </label>
54
+ <input id="referenceFiles" class="control file-input-hidden" type="file" accept=".pdf,.docx,.txt" multiple />
55
+ </div>
56
+
57
+ <div class="field upload-zone-wrap">
58
+ <label for="legalFile">Final Document (Required)</label>
59
+ <label class="upload-zone" for="legalFile">
60
+ <span class="upload-icon">+</span>
61
+ <span class="upload-title">Drop the final document or click to browse</span>
62
+ <span class="upload-hint">Supported: PDF, DOCX, TXT</span>
63
+ </label>
64
+ <input id="legalFile" class="control file-input-hidden" type="file" accept=".pdf,.docx,.txt" required />
65
+ </div>
66
+
67
+ <div id="analysisInputSummary" class="summary-box hidden"></div>
68
+
69
+ <div class="workflow-actions">
70
+ <a class="secondary-btn as-link" href="index.html#home">Back to Home</a>
71
+ <button id="runUploadBtn" class="submit-btn" type="submit">Upload and Analyze</button>
72
+ </div>
73
+ </form>
74
+
75
+ <div id="loadingState" class="loading-panel hidden" aria-live="polite">
76
+ <div class="spinner"></div>
77
+ <p>Analyzing document. Please wait...</p>
78
+ </div>
79
+
80
+ <p id="uploadMessage" class="message" aria-live="polite"></p>
81
+ </section>
82
+ </main>
83
+
84
+ <script src="app.js"></script>
85
+ </body>
86
+ </html>