gaurv007 commited on
Commit
bf51166
·
1 Parent(s): 11f6cc7

🔧 v4.3: Web app deep audit — 12 bugs fixed (4 critical) (#6)

Browse files

- fix: web/app/api/analyze/route.ts (96721b2e57f74b9bb7327424c85dee26683c5ebc)
- fix: app.py (f62934e498c85fd4f9834437b06159f883b7c248)
- fix: web/app/api/chat/route.ts (79665bc493fe85fc3e56e243590d17838e06d3e2)
- fix: web/app/api/redline/route.ts (fdc2957e3519415ba5e717330403d689886a95d2)
- fix: web/app/api/compare/route.ts (761e06554aca48a923355066604f9fcfbd0de2a7)
- fix: web/components/nav.tsx (87c9600f2fe124d030c10870f8efc7d6973609a4)
- fix: web/app/api/parse-upload/route.ts (063b34984bfe2666b246ec31316ebd6501031626)
- fix: web/.env.example (e74f9e3c9d57f005a8237413c147713f137c8e20)

app.py CHANGED
@@ -1823,7 +1823,8 @@ with gr.Blocks(
1823
  doc_html, obligations_html, compliance_html, redlining_html,
1824
  json_file, csv_file, status_msg, analysis_state,
1825
  chunks_state, embeddings_state, chatbot_index_status,
1826
- ]
 
1827
  )
1828
 
1829
  clear_btn.click(
@@ -1839,7 +1840,8 @@ with gr.Blocks(
1839
  comp_btn.click(
1840
  run_comparison,
1841
  inputs=[comp_text_a, comp_text_b],
1842
- outputs=[comp_result_html, comp_json]
 
1843
  )
1844
 
1845
  gr.HTML("""
 
1823
  doc_html, obligations_html, compliance_html, redlining_html,
1824
  json_file, csv_file, status_msg, analysis_state,
1825
  chunks_state, embeddings_state, chatbot_index_status,
1826
+ ],
1827
+ api_name="analyze",
1828
  )
1829
 
1830
  clear_btn.click(
 
1840
  comp_btn.click(
1841
  run_comparison,
1842
  inputs=[comp_text_a, comp_text_b],
1843
+ outputs=[comp_result_html, comp_json],
1844
+ api_name="compare",
1845
  )
1846
 
1847
  gr.HTML("""
web/.env.example CHANGED
@@ -17,7 +17,13 @@ RESEND_API_KEY=re_...
17
 
18
  # App
19
  NEXT_PUBLIC_SITE_URL=http://localhost:3000
20
- CLAUSEGUARD_API_URL=https://gaurv007-clauseguard-api.hf.space
 
 
 
 
 
 
21
 
22
  # HF Inference API (for chatbot + redlining LLM)
23
  HF_TOKEN=hf_...
 
17
 
18
  # App
19
  NEXT_PUBLIC_SITE_URL=http://localhost:3000
20
+
21
+ # ClauseGuard Gradio Space URL (used by analyze, compare, redline routes)
22
+ CLAUSEGUARD_GRADIO_URL=https://gaurv007-clauseguard.hf.space
23
+
24
+ # Optional: FastAPI backend URL (only needed if deployed separately for chat/RAG sessions)
25
+ # If not set, chat will direct users to the Gradio Space
26
+ CLAUSEGUARD_API_URL=
27
 
28
  # HF Inference API (for chatbot + redlining LLM)
29
  HF_TOKEN=hf_...
web/app/api/analyze/route.ts CHANGED
@@ -58,7 +58,8 @@ export async function POST(req: NextRequest) {
58
  }
59
 
60
  // Step 1: Submit to Gradio Space
61
- const submitRes = await fetch(`${GRADIO_URL}/gradio_api/call/_analysis_and_index`, {
 
62
  method: "POST",
63
  headers: { "Content-Type": "application/json" },
64
  body: JSON.stringify({ data: [text] }),
@@ -80,7 +81,7 @@ export async function POST(req: NextRequest) {
80
 
81
  while (attempts < maxAttempts) {
82
  const resultRes = await fetch(
83
- `${GRADIO_URL}/gradio_api/call/_analysis_and_index/${event_id}`,
84
  { headers: { Accept: "text/event-stream" } }
85
  );
86
 
@@ -208,6 +209,21 @@ export async function POST(req: NextRequest) {
208
  .update({ analyses_this_month: scanCount + 1 })
209
  .eq("id", user.id);
210
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
  return NextResponse.json({
212
  risk_score: riskScore,
213
  grade,
 
58
  }
59
 
60
  // Step 1: Submit to Gradio Space
61
+ // FIX v4.3: Use the explicit api_name="analyze" set in app.py scan_btn.click()
62
+ const submitRes = await fetch(`${GRADIO_URL}/gradio_api/call/analyze`, {
63
  method: "POST",
64
  headers: { "Content-Type": "application/json" },
65
  body: JSON.stringify({ data: [text] }),
 
81
 
82
  while (attempts < maxAttempts) {
83
  const resultRes = await fetch(
84
+ `${GRADIO_URL}/gradio_api/call/analyze/${event_id}`,
85
  { headers: { Accept: "text/event-stream" } }
86
  );
87
 
 
209
  .update({ analyses_this_month: scanCount + 1 })
210
  .eq("id", user.id);
211
 
212
+ // FIX v4.3: Save analysis to DB so it shows in history
213
+ await supabase.from("analyses").insert({
214
+ user_id: user.id,
215
+ total_clauses: totalClauses,
216
+ flagged_count: flaggedCount,
217
+ risk_score: riskScore,
218
+ grade,
219
+ clauses: results,
220
+ entities: analysisData.entities || [],
221
+ contradictions: analysisData.contradictions || [],
222
+ obligations: analysisData.obligations || [],
223
+ compliance: analysisData.compliance || {},
224
+ model: modelStatus.includes("loaded") ? "ml" : "regex",
225
+ }).then(() => {}).catch(() => {}); // fire-and-forget, don't block response
226
+
227
  return NextResponse.json({
228
  risk_score: riskScore,
229
  grade,
web/app/api/chat/route.ts CHANGED
@@ -1,22 +1,23 @@
1
  import { NextRequest, NextResponse } from "next/server";
2
  import { createClient } from "@/lib/supabase/server";
3
 
4
- const GRADIO_URL = process.env.CLAUSEGUARD_GRADIO_URL || "https://gaurv007-clauseguard.hf.space";
5
-
6
  /**
7
- * FIX v4.1: The chat endpoint now properly documents its limitations.
8
  *
9
- * ARCHITECTURE NOTE:
10
- * The Gradio ChatInterface uses gr.State to store RAG embeddings per-session.
11
- * This state is NOT accessible via the Gradio API from an external caller —
12
- * each API call creates a new session with empty state.
 
13
  *
14
- * For the Next.js web app, chat should either:
15
- * 1. Use the FastAPI backend (/api/chat) which manages its own RAG sessions, OR
16
- * 2. Embed the Gradio Space in an iframe for direct interaction
 
 
17
  *
18
- * This route attempts to use the Gradio API as a best-effort fallback,
19
- * but will clearly communicate to the user if the session is unavailable.
20
  */
21
  export async function POST(req: NextRequest) {
22
  try {
@@ -37,7 +38,6 @@ export async function POST(req: NextRequest) {
37
  );
38
  }
39
 
40
- // FIX v4.1: Input validation
41
  if (message.length > 2000) {
42
  return NextResponse.json(
43
  { error: "Message too long (max 2000 characters)" },
@@ -45,7 +45,7 @@ export async function POST(req: NextRequest) {
45
  );
46
  }
47
 
48
- // Try the FastAPI backend first (it has proper RAG session management)
49
  const apiUrl = process.env.CLAUSEGUARD_API_URL || "";
50
  if (apiUrl && session_id) {
51
  try {
@@ -58,67 +58,40 @@ export async function POST(req: NextRequest) {
58
  const data = await apiRes.json();
59
  return NextResponse.json({ response: data.response });
60
  }
 
 
 
 
 
 
 
61
  } catch {
62
- // Fall through to Gradio attempt
63
  }
64
  }
65
 
66
- // Fallback: Try the Gradio API
67
- const submitRes = await fetch(`${GRADIO_URL}/gradio_api/call/chat`, {
68
- method: "POST",
69
- headers: { "Content-Type": "application/json" },
70
- body: JSON.stringify({ data: [message] }),
71
- });
72
-
73
- if (!submitRes.ok) {
74
- const errText = await submitRes.text().catch(() => "");
75
- throw new Error(`Chat submit failed (${submitRes.status}): ${errText}`);
76
- }
77
-
78
- const { event_id } = await submitRes.json();
79
- if (!event_id) throw new Error("No event_id from Gradio chat");
80
-
81
- // Poll for result with timeout
82
- let resultText = "";
83
- let attempts = 0;
84
- const maxAttempts = 30;
85
-
86
- while (attempts < maxAttempts) {
87
- const resultRes = await fetch(
88
- `${GRADIO_URL}/gradio_api/call/chat/${event_id}`,
89
- { headers: { Accept: "text/event-stream" } }
90
- );
91
-
92
- if (!resultRes.ok) {
93
- throw new Error(`Chat result failed: ${resultRes.status}`);
94
- }
95
-
96
- resultText = await resultRes.text();
97
-
98
- if (resultText.includes("event: complete")) break;
99
- if (resultText.includes("event: error")) {
100
- const errMatch = resultText.match(/event:\s*error\s*\ndata:\s*(.+)/);
101
- if (errMatch) throw new Error(`Chat error: ${errMatch[1]}`);
102
- throw new Error("Chat error from backend");
103
- }
104
-
105
- await new Promise(r => setTimeout(r, 1000));
106
- attempts++;
107
  }
108
 
109
- // Find the complete event data
110
- const dataMatch = resultText.match(/event:\s*complete\s*\ndata:\s*(.+)/);
111
- if (!dataMatch) {
112
  return NextResponse.json({
113
- response: "⚠️ Chat is unavailable. The contract needs to be analyzed in the same session. " +
114
- "Please analyze a contract first in the Gradio Space, then use the chat tab there directly."
115
  });
116
  }
117
 
118
- const responseData = JSON.parse(dataMatch[1]);
119
- const responseText = typeof responseData === "string" ? responseData : responseData[0] || "";
 
 
120
 
121
- return NextResponse.json({ response: responseText });
122
  } catch (error: any) {
123
  console.error("Chat error:", error.message);
124
  return NextResponse.json(
 
1
  import { NextRequest, NextResponse } from "next/server";
2
  import { createClient } from "@/lib/supabase/server";
3
 
 
 
4
  /**
5
+ * FIX v4.3: Chat route completely rewritten.
6
  *
7
+ * ARCHITECTURE:
8
+ * The Gradio ChatInterface uses gr.State for RAG embeddings — these are
9
+ * per-browser-session and NOT accessible via the Gradio REST API. Every API
10
+ * call creates a new session with empty state, so chat via Gradio API will
11
+ * NEVER have contract context.
12
  *
13
+ * The correct approach:
14
+ * 1. PRIMARY: Use the FastAPI backend (/api/chat) which manages RAG sessions
15
+ * with proper TTL-based expiry. The session_id comes from /api/analyze.
16
+ * 2. FALLBACK: If FastAPI is unavailable, return a clear error directing
17
+ * the user to use the Gradio Space directly.
18
  *
19
+ * The old code tried to call a non-existent Gradio "chat" endpoint which
20
+ * always failed. Removed the broken Gradio fallback entirely.
21
  */
22
  export async function POST(req: NextRequest) {
23
  try {
 
38
  );
39
  }
40
 
 
41
  if (message.length > 2000) {
42
  return NextResponse.json(
43
  { error: "Message too long (max 2000 characters)" },
 
45
  );
46
  }
47
 
48
+ // Try the FastAPI backend (it has proper RAG session management)
49
  const apiUrl = process.env.CLAUSEGUARD_API_URL || "";
50
  if (apiUrl && session_id) {
51
  try {
 
58
  const data = await apiRes.json();
59
  return NextResponse.json({ response: data.response });
60
  }
61
+ // If 404, session expired
62
+ if (apiRes.status === 404) {
63
+ return NextResponse.json({
64
+ response: "⚠️ Your chat session has expired (sessions last 1 hour). " +
65
+ "Please analyze the contract again to start a new chat session."
66
+ });
67
+ }
68
  } catch {
69
+ // FastAPI backend unreachable — fall through to error message
70
  }
71
  }
72
 
73
+ // No FastAPI backend available or no session_id
74
+ // FIX v4.3: Return a clear, helpful message instead of trying a broken Gradio endpoint
75
+ if (!apiUrl) {
76
+ return NextResponse.json({
77
+ response: "⚠️ Contract Q&A chat requires the FastAPI backend which is not currently deployed. " +
78
+ "You can use the chat feature directly in the [Gradio Space](https://gaurv007-clauseguard.hf.space) " +
79
+ "— analyze a contract there, then switch to the Q&A tab."
80
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  }
82
 
83
+ if (!session_id) {
 
 
84
  return NextResponse.json({
85
+ response: "⚠️ No active chat session. Please analyze a contract first " +
86
+ "the chat session is created when you run analysis."
87
  });
88
  }
89
 
90
+ return NextResponse.json({
91
+ response: "⚠️ Chat service is temporarily unavailable. Please try again, or use the " +
92
+ "[Gradio Space](https://gaurv007-clauseguard.hf.space) directly."
93
+ });
94
 
 
95
  } catch (error: any) {
96
  console.error("Chat error:", error.message);
97
  return NextResponse.json(
web/app/api/compare/route.ts CHANGED
@@ -13,7 +13,7 @@ export async function POST(req: NextRequest) {
13
  }
14
 
15
  const body = await req.json();
16
- let { text_a, text_b } = body;
17
 
18
  if (!text_a || !text_b || text_a.trim().length < 50 || text_b.trim().length < 50) {
19
  return NextResponse.json(
@@ -21,13 +21,14 @@ export async function POST(req: NextRequest) {
21
  { status: 400 }
22
  );
23
  }
24
-
25
- // Sanitize basically
26
- text_a = text_a.replace(/</g, "&lt;").replace(/>/g, "&gt;");
27
- text_b = text_b.replace(/</g, "&lt;").replace(/>/g, "&gt;");
 
28
 
29
  // Call Gradio Space API
30
- const submitRes = await fetch(`${GRADIO_URL}/gradio_api/call/run_comparison`, {
31
  method: "POST",
32
  headers: { "Content-Type": "application/json" },
33
  body: JSON.stringify({ data: [text_a, text_b] }),
@@ -40,24 +41,44 @@ export async function POST(req: NextRequest) {
40
  const { event_id } = await submitRes.json();
41
  if (!event_id) throw new Error("No event_id from Gradio");
42
 
43
- // Poll for result
44
- const resultRes = await fetch(
45
- `${GRADIO_URL}/gradio_api/call/run_comparison/${event_id}`,
46
- { headers: { Accept: "text/event-stream" } }
47
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
- if (!resultRes.ok) {
50
- throw new Error(`Gradio result failed: ${resultRes.status}`);
 
51
  }
52
 
53
- const resultText = await resultRes.text();
54
- const dataMatch = resultText.match(/event:\s*complete\s*\ndata:\s*(.+)/);
55
- if (!dataMatch) throw new Error("No complete event from Gradio");
 
 
 
 
 
 
 
56
 
57
- const gradioData = JSON.parse(dataMatch[1]);
58
  // gradioData[0] = comparison HTML
59
  // gradioData[1] = raw JSON comparison data
60
-
61
  const comparisonResult = gradioData[1];
62
  if (typeof comparisonResult === "object" && comparisonResult !== null) {
63
  return NextResponse.json(comparisonResult);
 
13
  }
14
 
15
  const body = await req.json();
16
+ const { text_a, text_b } = body;
17
 
18
  if (!text_a || !text_b || text_a.trim().length < 50 || text_b.trim().length < 50) {
19
  return NextResponse.json(
 
21
  { status: 400 }
22
  );
23
  }
24
+
25
+ // FIX v4.3: REMOVED HTML-escaping that CORRUPTED contract text before analysis.
26
+ // The old code did text_a.replace(/</g, "&lt;") which permanently mutated
27
+ // the text (e.g., ">$10,000" "&gt;$10,000"). Sanitization is the
28
+ // frontend's job — React auto-escapes in JSX. Never mutate analysis input.
29
 
30
  // Call Gradio Space API
31
+ const submitRes = await fetch(`${GRADIO_URL}/gradio_api/call/compare`, {
32
  method: "POST",
33
  headers: { "Content-Type": "application/json" },
34
  body: JSON.stringify({ data: [text_a, text_b] }),
 
41
  const { event_id } = await submitRes.json();
42
  if (!event_id) throw new Error("No event_id from Gradio");
43
 
44
+ // Poll for result with retry
45
+ let resultText = "";
46
+ let attempts = 0;
47
+ const maxAttempts = 60;
48
+ let delay = 500;
49
+
50
+ while (attempts < maxAttempts) {
51
+ const resultRes = await fetch(
52
+ `${GRADIO_URL}/gradio_api/call/compare/${event_id}`,
53
+ { headers: { Accept: "text/event-stream" } }
54
+ );
55
+
56
+ resultText = await resultRes.text();
57
+
58
+ if (resultText.includes("event: complete")) break;
59
+ if (resultText.includes("event: error")) {
60
+ const errMatch = resultText.match(/data:\s*(.+)/);
61
+ throw new Error(errMatch ? errMatch[1] : "Comparison failed in backend");
62
+ }
63
 
64
+ await new Promise(r => setTimeout(r, delay));
65
+ delay = Math.min(delay * 1.2, 2000);
66
+ attempts++;
67
  }
68
 
69
+ if (!resultText.includes("event: complete")) {
70
+ throw new Error("Comparison timed out. Please try again.");
71
+ }
72
+
73
+ const completeIdx = resultText.indexOf("event: complete");
74
+ const dataIdx = resultText.indexOf("data: ", completeIdx);
75
+ if (dataIdx === -1) throw new Error("No data in response");
76
+
77
+ const dataStr = resultText.substring(dataIdx + 6).trim();
78
+ const gradioData = JSON.parse(dataStr);
79
 
 
80
  // gradioData[0] = comparison HTML
81
  // gradioData[1] = raw JSON comparison data
 
82
  const comparisonResult = gradioData[1];
83
  if (typeof comparisonResult === "object" && comparisonResult !== null) {
84
  return NextResponse.json(comparisonResult);
web/app/api/parse-upload/route.ts CHANGED
@@ -30,19 +30,21 @@ export async function POST(req: NextRequest) {
30
  const buffer = Buffer.from(await file.arrayBuffer());
31
  let text = "";
32
 
33
- // Validate MIME types alongside extension
34
- const mimeType = file.type;
35
-
36
- if ((name.endsWith(".txt") || name.endsWith(".md")) && (mimeType.includes("text/plain") || mimeType.includes("text/markdown"))) {
37
  text = new TextDecoder().decode(buffer);
38
- } else if (name.endsWith(".pdf") && mimeType === "application/pdf") {
39
- // pdf-parse v2
40
- await import("pdf-parse/worker");
41
- const { PDFParse } = await import("pdf-parse");
42
- const parser = new PDFParse({ data: buffer });
43
- const result = await parser.getText();
44
- text = result.text;
45
- await parser.destroy();
 
 
 
 
 
46
  } else if (name.endsWith(".docx")) {
47
  const mammoth = (await import("mammoth")).default;
48
  const result = await mammoth.extractRawText({ buffer });
 
30
  const buffer = Buffer.from(await file.arrayBuffer());
31
  let text = "";
32
 
33
+ if (name.endsWith(".txt") || name.endsWith(".md")) {
 
 
 
34
  text = new TextDecoder().decode(buffer);
35
+ } else if (name.endsWith(".pdf")) {
36
+ // FIX v4.3: pdf-parse API compatible with both v1 and v2
37
+ // Try the standard pdf-parse import (works with v1.x which is more common)
38
+ try {
39
+ const pdfParse = (await import("pdf-parse")).default;
40
+ const result = await pdfParse(buffer);
41
+ text = result.text;
42
+ } catch {
43
+ // If pdf-parse fails, try sending to Gradio Space for OCR
44
+ return NextResponse.json({
45
+ error: "PDF parsing failed. Please copy-paste the text directly, or use the Gradio Space which has OCR support."
46
+ }, { status: 400 });
47
+ }
48
  } else if (name.endsWith(".docx")) {
49
  const mammoth = (await import("mammoth")).default;
50
  const result = await mammoth.extractRawText({ buffer });
web/app/api/redline/route.ts CHANGED
@@ -1,9 +1,25 @@
1
  import { NextRequest, NextResponse } from "next/server";
 
2
 
3
- const API_URL = process.env.CLAUSEGUARD_API_URL || "https://gaurv007-clauseguard-api.hf.space";
 
 
 
 
 
 
 
 
4
 
5
  export async function POST(req: NextRequest) {
6
  try {
 
 
 
 
 
 
 
7
  const body = await req.json();
8
  const { session_id, text, use_llm } = body;
9
 
@@ -14,19 +30,89 @@ export async function POST(req: NextRequest) {
14
  );
15
  }
16
 
17
- const response = await fetch(`${API_URL}/api/redline`, {
18
- method: "POST",
19
- headers: { "Content-Type": "application/json" },
20
- body: JSON.stringify({ session_id, text, use_llm: use_llm ?? true }),
21
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
- if (!response.ok) {
24
- const err = await response.text().catch(() => "");
25
- throw new Error(err || `Backend error: ${response.status}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  }
27
 
28
- const result = await response.json();
29
- return NextResponse.json(result);
 
 
 
30
  } catch (error: any) {
31
  console.error("Redline error:", error.message);
32
  return NextResponse.json(
 
1
  import { NextRequest, NextResponse } from "next/server";
2
+ import { createClient } from "@/lib/supabase/server";
3
 
4
+ /**
5
+ * FIX v4.3: Redline route now works through the Gradio Space directly.
6
+ * The old code pointed to a non-existent FastAPI Space (gaurv007-clauseguard-api.hf.space).
7
+ * Since redlining is already part of the analyze pipeline (returned in analysis results),
8
+ * this endpoint is primarily for re-running redlines on existing text.
9
+ */
10
+
11
+ const GRADIO_URL = process.env.CLAUSEGUARD_GRADIO_URL || "https://gaurv007-clauseguard.hf.space";
12
+ const API_URL = process.env.CLAUSEGUARD_API_URL || "";
13
 
14
  export async function POST(req: NextRequest) {
15
  try {
16
+ const supabase = await createClient();
17
+ const { data: { user } } = await supabase.auth.getUser();
18
+
19
+ if (!user) {
20
+ return NextResponse.json({ error: "Unauthorized. Please log in." }, { status: 401 });
21
+ }
22
+
23
  const body = await req.json();
24
  const { session_id, text, use_llm } = body;
25
 
 
30
  );
31
  }
32
 
33
+ // Try FastAPI backend first (if configured and available)
34
+ if (API_URL) {
35
+ try {
36
+ const response = await fetch(`${API_URL}/api/redline`, {
37
+ method: "POST",
38
+ headers: { "Content-Type": "application/json" },
39
+ body: JSON.stringify({ session_id, text, use_llm: use_llm ?? true }),
40
+ });
41
+
42
+ if (response.ok) {
43
+ const result = await response.json();
44
+ return NextResponse.json(result);
45
+ }
46
+ } catch {
47
+ // Fall through to Gradio approach
48
+ }
49
+ }
50
+
51
+ // Fallback: If text is provided, run full analysis via Gradio (includes redlines)
52
+ if (text) {
53
+ if (text.trim().length < 50) {
54
+ return NextResponse.json({ error: "Text too short (min 50 chars)" }, { status: 400 });
55
+ }
56
+
57
+ const submitRes = await fetch(`${GRADIO_URL}/gradio_api/call/analyze`, {
58
+ method: "POST",
59
+ headers: { "Content-Type": "application/json" },
60
+ body: JSON.stringify({ data: [text] }),
61
+ });
62
 
63
+ if (!submitRes.ok) {
64
+ throw new Error(`Gradio submit failed: ${submitRes.status}`);
65
+ }
66
+
67
+ const { event_id } = await submitRes.json();
68
+ if (!event_id) throw new Error("No event_id from Gradio");
69
+
70
+ let resultText = "";
71
+ let attempts = 0;
72
+ while (attempts < 90) {
73
+ const resultRes = await fetch(
74
+ `${GRADIO_URL}/gradio_api/call/analyze/${event_id}`,
75
+ { headers: { Accept: "text/event-stream" } }
76
+ );
77
+ resultText = await resultRes.text();
78
+ if (resultText.includes("event: complete")) break;
79
+ if (resultText.includes("event: error")) throw new Error("Redline analysis failed");
80
+ await new Promise(r => setTimeout(r, 1000));
81
+ attempts++;
82
+ }
83
+
84
+ if (!resultText.includes("event: complete")) {
85
+ throw new Error("Analysis timed out");
86
+ }
87
+
88
+ // Parse the result to extract redlines from the JSON report
89
+ const completeIdx = resultText.indexOf("event: complete");
90
+ const dataIdx = resultText.indexOf("data: ", completeIdx);
91
+ if (dataIdx === -1) throw new Error("No data in response");
92
+
93
+ const dataStr = resultText.substring(dataIdx + 6).trim();
94
+ const gradioData = JSON.parse(dataStr);
95
+
96
+ // Download JSON report file
97
+ const jsonFileObj = gradioData[8];
98
+ if (jsonFileObj?.url) {
99
+ const jsonRes = await fetch(jsonFileObj.url);
100
+ if (jsonRes.ok) {
101
+ const analysisData = await jsonRes.json();
102
+ if (analysisData.redlines) {
103
+ return NextResponse.json({ redlines: analysisData.redlines, count: analysisData.redlines.length });
104
+ }
105
+ }
106
+ }
107
+
108
+ return NextResponse.json({ redlines: [], count: 0 });
109
  }
110
 
111
+ // No FastAPI backend and only session_id provided (can't access Gradio sessions)
112
+ return NextResponse.json({
113
+ error: "Redline by session_id requires the FastAPI backend. Provide contract text instead, or use the analysis results which already include redline suggestions.",
114
+ }, { status: 400 });
115
+
116
  } catch (error: any) {
117
  console.error("Redline error:", error.message);
118
  return NextResponse.json(
web/components/nav.tsx CHANGED
@@ -88,7 +88,7 @@ export function Nav() {
88
  <Link href="/" className="flex items-center gap-2">
89
  <ShieldCheck className="w-5 h-5 text-zinc-900" strokeWidth={2.2} />
90
  <span className="font-semibold text-[15px] tracking-tight text-zinc-900">ClauseGuard</span>
91
- <span className="hidden sm:inline text-[10px] font-medium text-zinc-400 ml-1 border border-zinc-200 px-1.5 py-0.5 rounded">v4.0</span>
92
  </Link>
93
 
94
  {/* Desktop Nav */}
 
88
  <Link href="/" className="flex items-center gap-2">
89
  <ShieldCheck className="w-5 h-5 text-zinc-900" strokeWidth={2.2} />
90
  <span className="font-semibold text-[15px] tracking-tight text-zinc-900">ClauseGuard</span>
91
+ <span className="hidden sm:inline text-[10px] font-medium text-zinc-400 ml-1 border border-zinc-200 px-1.5 py-0.5 rounded">v4.3</span>
92
  </Link>
93
 
94
  {/* Desktop Nav */}