Spaces:
Sleeping
Sleeping
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 |
) : (
|