diff --git a/endpoints/postmark (localhost).bru b/endpoints/postmark (localhost).bru index 8f7f284..5c6ad96 100644 --- a/endpoints/postmark (localhost).bru +++ b/endpoints/postmark (localhost).bru @@ -14,12 +14,12 @@ body:json { { "TextBody": "Dear customer, you owe $1,250.50 to CreditPlus for your January utility bills. Please pay by 2025-06-15 to avoid penalties.", "FromFull": { - "Email": "billing@creditplus.com", + "Email": "contact@francisco-pessano.com", "Name": "CreditPlus Billing Department" }, "ToFull": [ { - "Email": "franpessano1@gmail.com", + "Email": "contacto@francisco-pessano.com", "Name": "", "MailboxHash": "ahoy" } diff --git a/src/components/ConversationTimeline.tsx b/src/components/ConversationTimeline.tsx new file mode 100644 index 0000000..da2341c --- /dev/null +++ b/src/components/ConversationTimeline.tsx @@ -0,0 +1,586 @@ +import React, { useEffect, useState } from "react"; +import { + CheckCircle, + Clock, + AlertCircle, + XCircle, + StopCircle, + Mail, + MailOpen, + MessageSquare, + TrendingUp, + TrendingDown, + Calendar, + User, + Building2, +} from "lucide-react"; +import { supabase, type Debt } from "../lib/supabase"; +import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"; +import { Badge } from "./ui/badge"; +import { Separator } from "./ui/separator"; +import { toast } from "sonner"; +import { formatCurrency } from "../lib/utils"; + +interface ConversationMessage { + id: string; + debt_id: string; + message_type: string; + direction: "inbound" | "outbound"; + subject?: string; + body: string; + from_email?: string; + to_email?: string; + message_id?: string; + ai_analysis?: any; + created_at: string; + updated_at: string; +} + +interface ConversationTimelineProps { + debt: Debt; + onDebtUpdate?: (updatedDebt: Debt) => void; +} + +const messageTypeLabels = { + initial_debt: "Initial Debt Notice", + negotiation_sent: "Negotiation Sent", + response_received: "Response Received", + counter_offer: "Counter Offer", + acceptance: "Offer Accepted", + rejection: "Offer Rejected", +}; + +const statusColors = { + received: "text-blue-600 dark:text-blue-400", + negotiating: "text-yellow-600 dark:text-yellow-400", + approved: "text-purple-600 dark:text-purple-400", + sent: "text-orange-600 dark:text-orange-400", + awaiting_response: "text-blue-600 dark:text-blue-400", + counter_negotiating: "text-yellow-600 dark:text-yellow-400", + accepted: "text-green-600 dark:text-green-400", + rejected: "text-red-600 dark:text-red-400", + settled: "text-green-600 dark:text-green-400", + failed: "text-red-600 dark:text-red-400", + opted_out: "text-gray-600 dark:text-gray-400", +}; + +const statusLabels = { + received: "Received", + negotiating: "Negotiating", + approved: "Approved", + sent: "Sent", + awaiting_response: "Awaiting Response", + counter_negotiating: "Counter Negotiating", + accepted: "Accepted", + rejected: "Rejected", + settled: "Settled", + failed: "Failed", + opted_out: "Opted Out", +}; + +export function ConversationTimeline({ + debt, + onDebtUpdate, +}: ConversationTimelineProps) { + const [messages, setMessages] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + fetchConversationMessages(); + + // Set up real-time subscription for conversation messages + const channel = supabase + .channel(`conversation_messages:debt_id=eq.${debt.id}`) + .on( + "postgres_changes", + { + event: "*", + schema: "public", + table: "conversation_messages", + filter: `debt_id=eq.${debt.id}`, + }, + (payload) => { + console.log("Real-time conversation message update:", payload); + + if (payload.eventType === "INSERT") { + // Add new message to the list + const newMessage = payload.new as ConversationMessage; + setMessages((prev) => [...prev, newMessage]); + + // Show toast notification for new messages + if (newMessage.direction === "inbound") { + const isAcceptance = newMessage.message_type === "acceptance"; + toast.success( + isAcceptance ? "🎉 Offer Accepted!" : "New Response Received", + { + description: isAcceptance + ? "Creditor accepted your negotiation offer!" + : `${ + messageTypeLabels[newMessage.message_type] || + "New message" + } from creditor`, + } + ); + } + } else if (payload.eventType === "UPDATE") { + // Update existing message + setMessages((prev) => + prev.map((msg) => + msg.id === payload.new.id + ? (payload.new as ConversationMessage) + : msg + ) + ); + } else if (payload.eventType === "DELETE") { + // Remove deleted message + setMessages((prev) => + prev.filter((msg) => msg.id !== payload.old.id) + ); + } + } + ) + .subscribe(); + + // Set up real-time subscription for debt status changes + const debtChannel = supabase + .channel(`debt_status:id=eq.${debt.id}`) + .on( + "postgres_changes", + { + event: "UPDATE", + schema: "public", + table: "debts", + filter: `id=eq.${debt.id}`, + }, + (payload) => { + console.log("Real-time debt status update:", payload); + // Notify parent component about debt update + if (onDebtUpdate) { + onDebtUpdate(payload.new as Debt); + } + } + ) + .subscribe(); + + // Cleanup subscriptions on unmount + return () => { + supabase.removeChannel(channel); + supabase.removeChannel(debtChannel); + }; + }, [debt.id]); + + const fetchConversationMessages = async () => { + try { + const { data, error } = await supabase + .from("conversation_messages") + .select("*") + .eq("debt_id", debt.id) + .order("created_at", { ascending: true }); + + if (error) { + console.error("Error fetching conversation messages:", error); + return; + } + + setMessages(data || []); + } catch (error) { + console.error("Error fetching conversation messages:", error); + } finally { + setLoading(false); + } + }; + + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleDateString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }); + }; + + const getMessageIcon = (message: ConversationMessage) => { + if (message.direction === "outbound") { + return ; + } else { + switch (message.message_type) { + case "acceptance": + return ; + case "rejection": + return ; + case "counter_offer": + return ; + default: + return ; + } + } + }; + + const getMessageColor = (message: ConversationMessage) => { + if (message.direction === "outbound") { + return "text-blue-600 dark:text-blue-400"; + } else { + switch (message.message_type) { + case "acceptance": + return "text-green-600 dark:text-green-400"; + case "rejection": + return "text-red-600 dark:text-red-400"; + case "counter_offer": + return "text-yellow-600 dark:text-yellow-400"; + default: + return "text-gray-600 dark:text-gray-400"; + } + } + }; + + const getSentimentIcon = (sentiment?: string) => { + switch (sentiment) { + case "positive": + return ; + case "negative": + return ; + default: + return null; + } + }; + + if (loading) { + return ( + + + + + Loading Conversation... + + + + ); + } + + return ( + + + +
+ + Conversation Timeline +
+
+ + {statusLabels[debt.status]} + + + Round {debt.negotiation_round || 1} + +
+
+
+ + {messages.length === 0 ? ( +
+ +

No conversation messages yet

+

+ Messages will appear here as the negotiation progresses +

+
+ ) : ( +
+ {messages.map((message, index) => ( +
+ {/* Timeline line */} + {index < messages.length - 1 && ( +
+ )} + +
+ {/* Icon */} +
+ {getMessageIcon(message)} +
+ + {/* Content */} +
+
+
+

+ {messageTypeLabels[message.message_type] || + message.message_type} +

+ {message.ai_analysis?.sentiment && + getSentimentIcon(message.ai_analysis.sentiment)} +
+
+ + {formatDate(message.created_at)} +
+
+ +
+ {message.direction === "outbound" ? ( + <> + + You → {message.to_email} + + ) : ( + <> + + {message.from_email} → You + + )} +
+ + {message.subject && ( +

+ Subject: {message.subject} +

+ )} + +
+

+ {message.body.length > 200 + ? `${message.body.substring(0, 200)}...` + : message.body} +

+
+ + {/* AI Analysis */} + {message.ai_analysis && ( +
+ {message.ai_analysis.intent && ( +
+ + Intent: {message.ai_analysis.intent} + + {message.ai_analysis.confidence && ( + + {Math.round( + message.ai_analysis.confidence * 100 + )} + % confidence + + )} +
+ )} + + {!!message.ai_analysis.extractedTerms + ?.proposedAmount && ( +
+ Proposed Amount: $ + {message.ai_analysis.extractedTerms.proposedAmount} +
+ )} + + {/* Show financial outcome for accepted offers */} + {message.ai_analysis.intent === "acceptance" && + debt.metadata?.financialOutcome && ( +
+ {debt.metadata.financialOutcome.financialBenefit + ?.type === "principal_reduction" ? ( + <> +
+ + + Principal Reduction Achieved + +
+
+
+ + Original Debt: + +
+ $ + { + debt.metadata.financialOutcome + .originalAmount + } +
+
+
+ + Settlement Amount: + +
+ $ + { + debt.metadata.financialOutcome + .acceptedAmount + } +
+
+
+ + Total Savings: + +
+ $ + { + debt.metadata.financialOutcome + .actualSavings + } +
+
+
+ + Reduction: + +
+ { + debt.metadata.financialOutcome + .financialBenefit.percentage + } + % +
+
+
+ + ) : debt.metadata.financialOutcome + .financialBenefit?.type === + "payment_restructuring" ? ( + <> +
+ + + Payment Plan Restructured + +
+
+
+
+ { + debt.metadata.financialOutcome + .financialBenefit.description + } +
+ {debt.metadata.financialOutcome + .financialBenefit.cashFlowBenefit && ( +
+ 💰{" "} + { + debt.metadata.financialOutcome + .financialBenefit.cashFlowBenefit + } +
+ )} +
+ {debt.metadata.financialOutcome + .paymentStructure && ( +
+
+ + Monthly Payment: + +
+ $ + { + debt.metadata.financialOutcome + .paymentStructure.monthlyAmount + } +
+
+
+ + Term Length: + +
+ { + debt.metadata.financialOutcome + .paymentStructure + .numberOfPayments + }{" "} + months +
+
+
+ + Total Amount: + +
+ $ + {formatCurrency( + debt.metadata.financialOutcome + .paymentStructure.totalAmount + )} +
+
+
+ + Frequency: + +
+ { + debt.metadata.financialOutcome + .paymentStructure.frequency + } +
+
+
+ )} +
+ + ) : ( + <> +
+ + + Offer Accepted + +
+
+ Settlement terms have been agreed upon. +
+ + )} +
+ )} +
+ )} +
+
+
+ ))} +
+ )} + + + + {/* Summary */} +
+
+
+ + Total Messages: {messages.length} + + + Negotiation Round: {debt.negotiation_round || 1} + +
+ {debt.last_message_at && ( + + Last Activity: {formatDate(debt.last_message_at)} + + )} +
+
+ + + ); +} diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx index 1c753b8..93210de 100644 --- a/src/components/Dashboard.tsx +++ b/src/components/Dashboard.tsx @@ -1,16 +1,10 @@ -import React, { useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { supabase, type Debt, type UserProfile } from "../lib/supabase"; import { Button } from "./ui/button"; import { DebtCard } from "./DebtCard"; -import { DebtTimeline } from "./DebtTimeline"; +import { ConversationTimeline } from "./ConversationTimeline"; import { OnboardingDialog } from "./OnboardingDialog"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "./ui/card"; +import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs"; import { Badge } from "./ui/badge"; import { Separator } from "./ui/separator"; @@ -21,10 +15,9 @@ import { CheckCircle, AlertTriangle, RefreshCw, - BarChart3, - LogOut, Settings, } from "lucide-react"; +import { formatCurrency } from "../lib/utils"; export function Dashboard() { const [debts, setDebts] = useState([]); @@ -130,10 +123,13 @@ export function Dashboard() { const calculateStats = () => { const totalDebts = debts.length; const totalAmount = debts.reduce((sum, debt) => sum + debt.amount, 0); - const projectedSavings = debts.reduce( - (sum, debt) => sum + debt.projected_savings, - 0 - ); + const projectedSavings = debts.reduce((sum, debt) => { + // Use actual savings for accepted debts, projected for others + if (debt.status === "accepted" && debt.metadata?.actualSavings?.amount) { + return sum + debt.metadata.actualSavings.amount; + } + return sum + debt.projected_savings; + }, 0); const settledCount = debts.filter( (debt) => debt.status === "settled" ).length; @@ -157,23 +153,22 @@ export function Dashboard() { window.location.href = "/"; }; - const formatCurrency = (amount: number) => { - return new Intl.NumberFormat("en-US", { - style: "currency", - currency: "USD", - }).format(amount); - }; - const groupedDebts = { all: debts, active: debts.filter((debt) => - ["received", "negotiating"].includes(debt.status) + [ + "received", + "negotiating", + "approved", + "awaiting_response", + "counter_negotiating", + ].includes(debt.status) ), settled: debts.filter((debt) => - ["settled", "approved", "sent"].includes(debt.status) + ["settled", "accepted", "sent"].includes(debt.status) ), failed: debts.filter((debt) => - ["failed", "opted_out"].includes(debt.status) + ["failed", "rejected", "opted_out"].includes(debt.status) ), }; @@ -318,11 +313,14 @@ export function Dashboard() { {debtList.map((debt) => (
- - - - - + { + setDebts( + debts.map((d) => (d.id === debt.id ? debt : d)) + ); + }} + />
))}
diff --git a/src/components/DebtCard.tsx b/src/components/DebtCard.tsx index 2b97c43..7a8f7bc 100644 --- a/src/components/DebtCard.tsx +++ b/src/components/DebtCard.tsx @@ -13,6 +13,7 @@ import { Textarea } from "./ui/textarea"; import { Label } from "./ui/label"; import { Dialog, + DialogClose, DialogContent, DialogDescription, DialogHeader, @@ -45,6 +46,7 @@ import { } from "lucide-react"; import { supabase, type Debt, type DebtVariable } from "../lib/supabase"; import { toast } from "sonner"; +import { formatCurrency } from "../lib/utils"; interface DebtCardProps { debt: Debt; @@ -59,6 +61,14 @@ const statusColors = { approved: "bg-teal-100 text-teal-800 border-teal-200 dark:bg-teal-900/20 dark:text-teal-300 dark:border-teal-800", sent: "bg-purple-100 text-purple-800 border-purple-200 dark:bg-purple-900/20 dark:text-purple-300 dark:border-purple-800", + awaiting_response: + "bg-blue-100 text-blue-800 border-blue-200 dark:bg-blue-900/20 dark:text-blue-300 dark:border-blue-800", + counter_negotiating: + "bg-yellow-100 text-yellow-800 border-yellow-200 dark:bg-yellow-900/20 dark:text-yellow-300 dark:border-yellow-800", + accepted: + "bg-green-100 text-green-800 border-green-200 dark:bg-green-900/20 dark:text-green-300 dark:border-green-800", + rejected: + "bg-red-100 text-red-800 border-red-200 dark:bg-red-900/20 dark:text-red-300 dark:border-red-800", settled: "bg-green-100 text-green-800 border-green-200 dark:bg-green-900/20 dark:text-green-300 dark:border-green-800", failed: @@ -72,6 +82,10 @@ const statusLabels = { negotiating: "Negotiating", approved: "Approved", sent: "Sent", + awaiting_response: "Awaiting Response", + counter_negotiating: "Counter Negotiating", + accepted: "Accepted", + rejected: "Rejected", settled: "Settled", failed: "Failed", opted_out: "Opted Out", @@ -81,21 +95,20 @@ export function DebtCard({ debt, onUpdate }: DebtCardProps) { const [isApproving, setIsApproving] = useState(false); const [isRejecting, setIsRejecting] = useState(false); const [userProfile, setUserProfile] = useState(null); - const [hasServerToken, setHasServerToken] = useState(false); + const [hasServerToken, setHasServerToken] = useState( + undefined + ); const isReadOnly = debt.status === "approved" || debt.status === "sent" || + debt.status === "awaiting_response" || + debt.status === "accepted" || + debt.status === "rejected" || + debt.status === "settled" || debt.status === "failed" || debt.status === "opted_out"; - const formatCurrency = (amount: number) => { - return new Intl.NumberFormat("en-US", { - style: "currency", - currency: "USD", - }).format(amount); - }; - const formatDate = (dateString: string) => { return new Date(dateString).toLocaleDateString("en-US", { year: "numeric", @@ -466,9 +479,11 @@ export function DebtCard({ debt, onUpdate }: DebtCardProps) { {/* Action Buttons */} {!isReadOnly && (
- + + + @@ -663,8 +678,8 @@ export function DebtCard({ debt, onUpdate }: DebtCardProps) {
-
- +
+ {formatCurrency(debt.amount)} @@ -710,7 +725,7 @@ export function DebtCard({ debt, onUpdate }: DebtCardProps) { {/* Approve/Reject Buttons */} {showApproveRejectButtons() && (
- {!hasServerToken && ( + {hasServerToken === false && (
@@ -721,7 +736,7 @@ export function DebtCard({ debt, onUpdate }: DebtCardProps) { variant="outline" size="sm" onClick={() => (window.location.href = "/configuration")} - className="text-amber-700 border-amber-300 hover:bg-amber-100" + className="text-amber-700 dark:text-amber-500 border-amber-300" > Settings @@ -757,7 +772,7 @@ export function DebtCard({ debt, onUpdate }: DebtCardProps) { Cancel handleApprove()} - className="bg-green-600 hover:bg-green-700" + className="bg-green-600 hover:bg-green-700 text-white" > Send Email diff --git a/src/components/DebtTimeline.tsx b/src/components/DebtTimeline.tsx index 384398c..852646a 100644 --- a/src/components/DebtTimeline.tsx +++ b/src/components/DebtTimeline.tsx @@ -1,26 +1,83 @@ -import React from "react"; +import React, { useState, useEffect } from "react"; import { CheckCircle, Clock, AlertCircle, XCircle, StopCircle, + Send, + MessageSquare, + ThumbsUp, + ThumbsDown, + RefreshCw, } from "lucide-react"; import type { Debt } from "../lib/supabase"; +import { createClient } from "@supabase/supabase-js"; interface DebtTimelineProps { debt: Debt; } -const timelineSteps = [ - { key: "received", label: "Email Received", icon: CheckCircle }, - { key: "negotiating", label: "Negotiating", icon: Clock }, - { key: "settled", label: "Settled", icon: CheckCircle }, -]; +interface ConversationMessage { + id: string; + message_type: string; + direction: "inbound" | "outbound"; + subject: string; + body: string; + from_email: string; + to_email: string; + ai_analysis?: any; + created_at: string; +} + +const getTimelineSteps = (debt: Debt) => { + const baseSteps = [ + { key: "received", label: "Debt Email Received", icon: MessageSquare }, + { key: "negotiating", label: "AI Response Generated", icon: Clock }, + { key: "approved", label: "Response Approved", icon: CheckCircle }, + { key: "sent", label: "Negotiation Email Sent", icon: Send }, + ]; + + // Add dynamic steps based on conversation + if (debt.status === "counter_negotiating" || debt.negotiation_round > 1) { + baseSteps.push({ + key: "counter_negotiating", + label: "Counter-Negotiating", + icon: RefreshCw, + }); + } + + if (debt.status === "accepted" || debt.status === "settled") { + baseSteps.push({ + key: "accepted", + label: "Offer Accepted", + icon: ThumbsUp, + }); + baseSteps.push({ + key: "settled", + label: "Debt Settled", + icon: CheckCircle, + }); + } else if (debt.status === "rejected") { + baseSteps.push({ + key: "rejected", + label: "Offer Rejected", + icon: ThumbsDown, + }); + } + + return baseSteps; +}; const statusIcons = { - received: CheckCircle, + received: MessageSquare, negotiating: Clock, + approved: CheckCircle, + sent: Send, + awaiting_response: Clock, + counter_negotiating: RefreshCw, + accepted: ThumbsUp, + rejected: ThumbsDown, settled: CheckCircle, failed: XCircle, opted_out: StopCircle, @@ -29,20 +86,139 @@ const statusIcons = { const statusColors = { received: "text-blue-600 dark:text-blue-400", negotiating: "text-yellow-600 dark:text-yellow-400", + approved: "text-green-600 dark:text-green-400", + sent: "text-purple-600 dark:text-purple-400", + awaiting_response: "text-orange-600 dark:text-orange-400", + counter_negotiating: "text-indigo-600 dark:text-indigo-400", + accepted: "text-green-600 dark:text-green-400", + rejected: "text-red-600 dark:text-red-400", settled: "text-green-600 dark:text-green-400", failed: "text-red-600 dark:text-red-400", opted_out: "text-gray-600 dark:text-gray-400", }; export function DebtTimeline({ debt }: DebtTimelineProps) { + const [conversationMessages, setConversationMessages] = useState< + ConversationMessage[] + >([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchConversationMessages = async () => { + try { + const supabase = createClient( + import.meta.env.PUBLIC_SUPABASE_URL, + import.meta.env.PUBLIC_SUPABASE_ANON_KEY + ); + + const { data, error } = await supabase + .from("conversation_messages") + .select("*") + .eq("debt_id", debt.id) + .order("created_at", { ascending: true }); + + if (error) { + console.error("Error fetching conversation messages:", error); + } else { + setConversationMessages(data || []); + } + } catch (error) { + console.error("Error:", error); + } finally { + setLoading(false); + } + }; + + fetchConversationMessages(); + }, [debt.id]); + + const timelineSteps = getTimelineSteps(debt); const currentStepIndex = timelineSteps.findIndex( (step) => step.key === debt.status ); - return ( -
-

Progress Timeline

+ const formatDateTime = (dateString: string) => { + const date = new Date(dateString); + return { + date: date.toLocaleDateString(), + time: date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }), + }; + }; + const getStatusDescription = (status: string, round?: number) => { + switch (status) { + case "received": + return "Debt collection email received and parsed"; + case "negotiating": + return "AI analyzing debt and generating negotiation strategy"; + case "approved": + return "Negotiation response approved and ready to send"; + case "sent": + return "Negotiation email sent to creditor"; + case "awaiting_response": + return "Waiting for creditor's response"; + case "counter_negotiating": + return `Round ${round || 1} - Analyzing creditor's counter-offer`; + case "accepted": + return "Creditor accepted the negotiation terms"; + case "rejected": + return "Creditor rejected the offer - manual review needed"; + case "settled": + return "Debt successfully settled with agreed terms"; + case "failed": + return "Negotiation failed - escalated for manual handling"; + default: + return "Processing..."; + } + }; + + return ( +
+
+

Negotiation Timeline

+
+ Round {debt.negotiation_round || 1} • {debt.conversation_count || 0}{" "} + messages +
+
+ + {/* Current Status Overview */} +
+
+
+ {React.createElement( + statusIcons[debt.status as keyof typeof statusIcons] || Clock, + { + className: `h-5 w-5 ${ + statusColors[debt.status as keyof typeof statusColors] + }`, + } + )} +
+
+
+ {debt.status.replace("_", " ")} +
+
+ {getStatusDescription(debt.status, debt.negotiation_round)} +
+ {debt.last_message_at && ( +
+ Last updated: {formatDateTime(debt.last_message_at).date} at{" "} + {formatDateTime(debt.last_message_at).time} +
+ )} +
+
+
+ + {/* Timeline Steps */}
{timelineSteps.map((step, index) => { const isCompleted = index <= currentStepIndex; @@ -50,90 +226,138 @@ export function DebtTimeline({ debt }: DebtTimelineProps) { const Icon = step.icon; return ( -
+
-
+
{step.label}
{isActive && (
- {debt.status === "received" && - "Processing email and generating response..."} - {debt.status === "negotiating" && - "Response generated, waiting for creditor reply"} - {debt.status === "settled" && "Payment plan agreed upon"} + {getStatusDescription(debt.status, debt.negotiation_round)}
)}
- {isActive && ( -
- {new Date(debt.updated_at).toLocaleDateString()} + {isCompleted && debt.updated_at && ( +
+
{formatDateTime(debt.updated_at).date}
+
{formatDateTime(debt.updated_at).time}
)}
); })} - - {/* Special cases for failed and opted_out */} - {(debt.status === "failed" || debt.status === "opted_out") && ( -
-
- {React.createElement(statusIcons[debt.status], { - className: "h-4 w-4", - })} -
- -
-
- {debt.status === "failed" ? "Negotiation Failed" : "Opted Out"} -
-
- {debt.status === "failed" - ? "Creditor declined the negotiation proposal" - : "User requested to stop communication"} -
-
- -
- {new Date(debt.updated_at).toLocaleDateString()} -
-
- )}
+ + {/* Conversation Messages */} + {!loading && conversationMessages.length > 0 && ( +
+

+ Conversation History +

+
+ {conversationMessages.map((message) => { + const isOutbound = message.direction === "outbound"; + const datetime = formatDateTime(message.created_at); + + return ( +
+
+
+ {isOutbound ? "You" : "Creditor"} • {datetime.date}{" "} + {datetime.time} +
+
+ {message.subject} +
+
+ {message.body.length > 100 + ? `${message.body.substring(0, 100)}...` + : message.body} +
+ {message.ai_analysis && ( +
+ AI: {message.ai_analysis.intent || "Analyzed"} + {message.ai_analysis.confidence && + ` (${Math.round( + message.ai_analysis.confidence * 100 + )}% confidence)`} +
+ )} +
+
+ ); + })} +
+
+ )} + + {/* Savings Summary */} + {(debt.projected_savings > 0 || + (debt.actual_savings && debt.actual_savings > 0)) && ( +
+

+ Savings Summary +

+
+ {debt.projected_savings > 0 && ( +
+
+ Projected Savings +
+
+ ${debt.projected_savings.toFixed(2)} +
+
+ )} + {debt.actual_savings && debt.actual_savings > 0 && ( +
+
+ Actual Savings +
+
+ ${debt.actual_savings.toFixed(2)} +
+
+ )} +
+
+ )}
); } diff --git a/src/components/ExtractionTester.tsx b/src/components/ExtractionTester.tsx new file mode 100644 index 0000000..9158797 --- /dev/null +++ b/src/components/ExtractionTester.tsx @@ -0,0 +1,212 @@ +import { useState } from "react"; +import { Button } from "./ui/button"; +import { Textarea } from "./ui/textarea"; +import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"; +import { Badge } from "./ui/badge"; +import { toast } from "sonner"; +import { TestTube, Loader2 } from "lucide-react"; + +export function ExtractionTester() { + const [testEmail, setTestEmail] = useState(`Thank you for your payment arrangement request. We are pleased to offer you a structured payment plan. We can accept $250 per month for 18 months, totaling $4,500. This arrangement will allow you to resolve this matter with manageable monthly payments.`); + const [result, setResult] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + const testExtraction = async () => { + setIsLoading(true); + try { + const response = await fetch('/api/supabase/functions/test-extraction', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ testEmail }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + setResult(data); + + if (data.success) { + toast.success("Extraction Test Complete", { + description: `Found ${Object.keys(data.extractedTerms || {}).length} extracted terms`, + }); + } else { + toast.error("Extraction Test Failed", { + description: data.error || "Unknown error", + }); + } + } catch (error) { + console.error("Error testing extraction:", error); + toast.error("Test Failed", { + description: "Failed to test extraction", + }); + } finally { + setIsLoading(false); + } + }; + + const presetEmails = [ + { + name: "Payment Plan", + content: "Thank you for your payment arrangement request. We are pleased to offer you a structured payment plan. We can accept $250 per month for 18 months, totaling $4,500. This arrangement will allow you to resolve this matter with manageable monthly payments." + }, + { + name: "Settlement Offer", + content: "We are pleased to accept your proposed settlement offer of $3,200. We will process the settlement as discussed and consider this matter resolved upon receipt of payment." + }, + { + name: "Counter Offer", + content: "Thank you for your proposal. However, we can only accept a settlement of $4,000 or a payment plan of $300 per month for 15 months. Please let us know which option works better for you." + }, + { + name: "Rejection", + content: "Thank you for your payment proposal. Unfortunately, we cannot accept the terms you have proposed. We require the full amount of $5,000 to be paid within 30 days." + } + ]; + + return ( + + + + + AI Extraction Tester + + + + {/* Preset Emails */} +
+ +
+ {presetEmails.map((preset) => ( + + ))} +
+
+ + {/* Email Input */} +
+ +