gaurv007 commited on
Commit
7ba0245
·
verified ·
1 Parent(s): dadbbff

v4.0: Analyze page — add Redlining + Q&A Chat tabs with full UI

Browse files
web/app/dashboard-pages/analyze/page.tsx CHANGED
@@ -9,7 +9,8 @@ import {
9
  AlertTriangle, Tag, BookOpen, ClipboardList, DollarSign,
10
  Calendar, Building, MapPin, Hash, Bot, FileSearch, Percent, Clock,
11
  User, BookMarked, ShieldX, HelpCircle, Cpu, PenTool, Zap,
12
- ShieldOff, CircleSlash, MessageSquareWarning, Construction
 
13
  } from "lucide-react";
14
 
15
  interface Cat { name: string; severity: string; description?: string; confidence?: number; }
@@ -19,6 +20,17 @@ interface Contradiction { type: string; explanation: string; severity: string; c
19
  interface Obligation { type: string; party: string; description: string; deadline: string; priority?: number; }
20
  interface ComplianceCheck { requirement: string; description: string; severity: string; status: string; matched_keywords: string[]; context?: string[]; }
21
  interface ComplianceReg { description: string; compliance_rate: number; checks: ComplianceCheck[]; overall_status: string; negated_count?: number; ambiguous_count?: number; }
 
 
 
 
 
 
 
 
 
 
 
22
  interface AnalysisResult {
23
  risk_score: number;
24
  grade: string;
@@ -29,8 +41,10 @@ interface AnalysisResult {
29
  contradictions: Contradiction[];
30
  obligations: Obligation[];
31
  compliance: Record<string, ComplianceReg>;
 
32
  model: string;
33
  latency_ms: number;
 
34
  }
35
 
36
  const SEV_CONFIG: Record<string, { icon: any; label: string; text: string; bg: string; border: string; ring: string }> = {
@@ -169,6 +183,9 @@ export default function AnalyzePage() {
169
  const [scanLimit, setScanLimit] = useState(10);
170
  const [canUpload, setCanUpload] = useState(false);
171
  const [showUpgrade, setShowUpgrade] = useState(false);
 
 
 
172
  const fileInputRef = useRef<HTMLInputElement>(null);
173
 
174
  // Fetch user profile from DB on mount — no hardcoded emails or plans
@@ -237,6 +254,31 @@ export default function AnalyzePage() {
237
  setCopied(true); setTimeout(() => setCopied(false), 2000);
238
  }
239
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
240
  const flagged = results?.results.filter(r => r.categories.length > 0) || [];
241
  const filtered = filter === "all" ? flagged : flagged.filter(r => r.categories.some(c => c.severity === filter));
242
  const sevCounts = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 };
@@ -260,6 +302,8 @@ export default function AnalyzePage() {
260
  { key: "contradictions", label: "Issues", icon: AlertTriangle, count: results?.contradictions.length || 0 },
261
  { key: "obligations", label: "Obligations", icon: ClipboardList, count: results?.obligations.length || 0 },
262
  { key: "compliance", label: "Compliance", icon: ShieldCheck, count: Object.keys(results?.compliance || {}).length },
 
 
263
  ];
264
 
265
  return (
@@ -668,6 +712,139 @@ export default function AnalyzePage() {
668
  })}
669
  </div>
670
  )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
671
  </div>
672
  </div>
673
  ) : (
 
9
  AlertTriangle, Tag, BookOpen, ClipboardList, DollarSign,
10
  Calendar, Building, MapPin, Hash, Bot, FileSearch, Percent, Clock,
11
  User, BookMarked, ShieldX, HelpCircle, Cpu, PenTool, Zap,
12
+ ShieldOff, CircleSlash, MessageSquareWarning, Construction,
13
+ MessageSquare, Send, Loader2
14
  } from "lucide-react";
15
 
16
  interface Cat { name: string; severity: string; description?: string; confidence?: number; }
 
20
  interface Obligation { type: string; party: string; description: string; deadline: string; priority?: number; }
21
  interface ComplianceCheck { requirement: string; description: string; severity: string; status: string; matched_keywords: string[]; context?: string[]; }
22
  interface ComplianceReg { description: string; compliance_rate: number; checks: ComplianceCheck[]; overall_status: string; negated_count?: number; ambiguous_count?: number; }
23
+ interface Redline {
24
+ original_text: string;
25
+ clause_label: string;
26
+ risk_level: string;
27
+ safe_alternative: string;
28
+ template_alternative?: string;
29
+ legal_basis: string;
30
+ consumer_standard: string;
31
+ tier: string;
32
+ }
33
+ interface ChatMessage { role: "user" | "assistant"; content: string; }
34
  interface AnalysisResult {
35
  risk_score: number;
36
  grade: string;
 
41
  contradictions: Contradiction[];
42
  obligations: Obligation[];
43
  compliance: Record<string, ComplianceReg>;
44
+ redlines: Redline[];
45
  model: string;
46
  latency_ms: number;
47
+ session_id?: string;
48
  }
49
 
50
  const SEV_CONFIG: Record<string, { icon: any; label: string; text: string; bg: string; border: string; ring: string }> = {
 
183
  const [scanLimit, setScanLimit] = useState(10);
184
  const [canUpload, setCanUpload] = useState(false);
185
  const [showUpgrade, setShowUpgrade] = useState(false);
186
+ const [chatMessages, setChatMessages] = useState<ChatMessage[]>([]);
187
+ const [chatInput, setChatInput] = useState("");
188
+ const [chatLoading, setChatLoading] = useState(false);
189
  const fileInputRef = useRef<HTMLInputElement>(null);
190
 
191
  // Fetch user profile from DB on mount — no hardcoded emails or plans
 
254
  setCopied(true); setTimeout(() => setCopied(false), 2000);
255
  }
256
 
257
+ async function handleChat() {
258
+ if (!chatInput.trim() || !results?.session_id) return;
259
+ const userMsg: ChatMessage = { role: "user", content: chatInput.trim() };
260
+ setChatMessages(prev => [...prev, userMsg]);
261
+ setChatInput("");
262
+ setChatLoading(true);
263
+ try {
264
+ const res = await fetch("/api/chat", {
265
+ method: "POST",
266
+ headers: { "Content-Type": "application/json" },
267
+ body: JSON.stringify({
268
+ message: userMsg.content,
269
+ session_id: results.session_id,
270
+ history: chatMessages.slice(-6),
271
+ }),
272
+ });
273
+ if (!res.ok) throw new Error((await res.json()).error || "Chat failed");
274
+ const data = await res.json();
275
+ setChatMessages(prev => [...prev, { role: "assistant", content: data.response }]);
276
+ } catch (e: any) {
277
+ setChatMessages(prev => [...prev, { role: "assistant", content: `⚠️ ${e.message}` }]);
278
+ }
279
+ setChatLoading(false);
280
+ }
281
+
282
  const flagged = results?.results.filter(r => r.categories.length > 0) || [];
283
  const filtered = filter === "all" ? flagged : flagged.filter(r => r.categories.some(c => c.severity === filter));
284
  const sevCounts = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 };
 
302
  { key: "contradictions", label: "Issues", icon: AlertTriangle, count: results?.contradictions.length || 0 },
303
  { key: "obligations", label: "Obligations", icon: ClipboardList, count: results?.obligations.length || 0 },
304
  { key: "compliance", label: "Compliance", icon: ShieldCheck, count: Object.keys(results?.compliance || {}).length },
305
+ { key: "redlining", label: "Redlining", icon: PenTool, count: results?.redlines?.length || 0 },
306
+ { key: "chat", label: "Q&A", icon: MessageSquare, count: chatMessages.length },
307
  ];
308
 
309
  return (
 
712
  })}
713
  </div>
714
  )}
715
+
716
+ {/* Redlining */}
717
+ {activeTab === "redlining" && (
718
+ <div className="space-y-3">
719
+ {(!results.redlines || results.redlines.length === 0) ? (
720
+ <div className="border border-dashed border-zinc-200 rounded-xl p-8 sm:p-10 text-center bg-white">
721
+ <PenTool className="w-8 h-8 text-zinc-300 mx-auto mb-2" />
722
+ <p className="text-sm text-zinc-500">No redlining suggestions for this contract.</p>
723
+ </div>
724
+ ) : (
725
+ <>
726
+ <div className="bg-gradient-to-r from-blue-50 to-emerald-50 rounded-xl p-4 border border-zinc-200 mb-2">
727
+ <div className="flex items-center gap-2 mb-1">
728
+ <PenTool className="w-4 h-4 text-zinc-600" />
729
+ <span className="text-sm font-semibold text-zinc-800">Clause Redlining Suggestions</span>
730
+ </div>
731
+ <p className="text-xs text-zinc-500">
732
+ {results.redlines.length} suggestions · {results.redlines.filter(r => r.tier === "llm_refined").length} LLM-refined
733
+ </p>
734
+ </div>
735
+ {results.redlines.map((rl, i) => {
736
+ const isHigh = rl.risk_level === "CRITICAL" || rl.risk_level === "HIGH";
737
+ const conf = SEV_CONFIG[rl.risk_level] || SEV_CONFIG.MEDIUM;
738
+ return (
739
+ <div key={i} className={`bg-white border rounded-xl overflow-hidden ${conf.border}`}>
740
+ <div className={`px-4 py-3 ${conf.bg} border-b ${conf.border} flex items-center justify-between`}>
741
+ <div className="flex items-center gap-2">
742
+ <conf.icon className={`w-4 h-4 ${conf.text}`} />
743
+ <span className={`text-sm font-semibold ${conf.text}`}>{rl.clause_label}</span>
744
+ <span className={`text-[10px] uppercase font-bold ${conf.text}`}>{rl.risk_level}</span>
745
+ </div>
746
+ <span className={`text-[10px] px-2 py-0.5 rounded border ${
747
+ rl.tier === "llm_refined"
748
+ ? "bg-indigo-50 text-indigo-600 border-indigo-200"
749
+ : "bg-emerald-50 text-emerald-600 border-emerald-200"
750
+ }`}>
751
+ {rl.tier === "llm_refined" ? "🤖 LLM Refined" : "📋 Template"}
752
+ </span>
753
+ </div>
754
+ <div className="p-4 space-y-3">
755
+ <div>
756
+ <p className="text-[10px] font-semibold text-red-600 uppercase mb-1">❌ Original (Risky)</p>
757
+ <div className="bg-red-50 border border-red-100 rounded-lg p-3 text-xs text-red-800 leading-relaxed line-through">
758
+ {rl.original_text.slice(0, 200)}{rl.original_text.length > 200 ? "..." : ""}
759
+ </div>
760
+ </div>
761
+ <div>
762
+ <p className="text-[10px] font-semibold text-emerald-600 uppercase mb-1">✅ Suggested Alternative</p>
763
+ <div className="bg-emerald-50 border border-emerald-100 rounded-lg p-3 text-xs text-emerald-800 leading-relaxed">
764
+ {rl.safe_alternative}
765
+ </div>
766
+ </div>
767
+ <div className="flex gap-3 flex-wrap text-[10px] text-zinc-500">
768
+ <span>📚 {rl.legal_basis}</span>
769
+ <span>🛡️ {rl.consumer_standard}</span>
770
+ </div>
771
+ </div>
772
+ </div>
773
+ );
774
+ })}
775
+ <div className="bg-amber-50 border border-amber-200 rounded-lg p-3 text-[11px] text-amber-800">
776
+ <strong>⚠️ Disclaimer:</strong> These are AI-generated suggestions, NOT legal advice. Consult an attorney before use.
777
+ </div>
778
+ </>
779
+ )}
780
+ </div>
781
+ )}
782
+
783
+ {/* Chat */}
784
+ {activeTab === "chat" && (
785
+ <div className="flex flex-col h-[350px] sm:h-[420px]">
786
+ {!results.session_id ? (
787
+ <div className="flex-1 flex items-center justify-center">
788
+ <div className="text-center">
789
+ <MessageSquare className="w-8 h-8 text-zinc-300 mx-auto mb-2" />
790
+ <p className="text-sm text-zinc-500">Chat unavailable — session not initialized.</p>
791
+ <p className="text-xs text-zinc-400 mt-1">Try analyzing again with the backend running.</p>
792
+ </div>
793
+ </div>
794
+ ) : (
795
+ <>
796
+ <div className="flex-1 overflow-y-auto space-y-3 pr-1 mb-3">
797
+ {chatMessages.length === 0 && (
798
+ <div className="text-center py-8">
799
+ <MessageSquare className="w-8 h-8 text-zinc-200 mx-auto mb-2" />
800
+ <p className="text-sm text-zinc-400">Ask a question about your contract</p>
801
+ <div className="mt-3 flex flex-wrap justify-center gap-2">
802
+ {["What are the main risks?", "Who are the parties?", "Is there an arbitration clause?", "Summarize key terms"].map(q => (
803
+ <button key={q} onClick={() => { setChatInput(q); }}
804
+ className="text-xs px-3 py-1.5 rounded-full border border-zinc-200 text-zinc-500 hover:bg-zinc-50 transition-colors">
805
+ {q}
806
+ </button>
807
+ ))}
808
+ </div>
809
+ </div>
810
+ )}
811
+ {chatMessages.map((msg, i) => (
812
+ <div key={i} className={`flex ${msg.role === "user" ? "justify-end" : "justify-start"}`}>
813
+ <div className={`max-w-[85%] rounded-xl px-3.5 py-2.5 text-sm leading-relaxed ${
814
+ msg.role === "user"
815
+ ? "bg-zinc-900 text-white"
816
+ : "bg-zinc-100 text-zinc-700 border border-zinc-200"
817
+ }`}>
818
+ {msg.content}
819
+ </div>
820
+ </div>
821
+ ))}
822
+ {chatLoading && (
823
+ <div className="flex justify-start">
824
+ <div className="bg-zinc-100 border border-zinc-200 rounded-xl px-4 py-3">
825
+ <Loader2 className="w-4 h-4 text-zinc-400 animate-spin" />
826
+ </div>
827
+ </div>
828
+ )}
829
+ </div>
830
+ <div className="flex gap-2 border-t border-zinc-100 pt-3">
831
+ <input
832
+ value={chatInput}
833
+ onChange={(e) => setChatInput(e.target.value)}
834
+ onKeyDown={(e) => e.key === "Enter" && !e.shiftKey && handleChat()}
835
+ placeholder="Ask about your contract..."
836
+ className="flex-1 px-3 py-2 border border-zinc-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-zinc-900/10"
837
+ disabled={chatLoading}
838
+ />
839
+ <button onClick={handleChat} disabled={chatLoading || !chatInput.trim()}
840
+ className="px-3 py-2 bg-zinc-900 text-white rounded-lg hover:bg-zinc-800 disabled:opacity-40 transition-colors">
841
+ <Send className="w-4 h-4" />
842
+ </button>
843
+ </div>
844
+ </>
845
+ )}
846
+ </div>
847
+ )}
848
  </div>
849
  </div>
850
  ) : (