+
+
Sentiment Analysis
+
+
+
+ {/* Text Input Section */}
+
+
Analyze Custom Text
+
+ Paste text from social media comments, reviews, or feedback to analyze sentiment
+
+
+
+ Or click "Refresh" above to analyze feedback from your campaign metrics
+
+
+
+
+
+
Overall Sentiment
+
+ {data.overall_sentiment}
+
+
Score: {data.sentiment_score.toFixed(2)}
+
+
+
Sentiment Score
+
+
0 ? "bg-green-500" : "bg-red-500"
+ }`}
+ style={{ width: `${Math.abs(data.sentiment_score) * 100}%` }}
+ />
+
+
+
+
+
+
+
Positive Aspects
+
+ {data.positive_aspects.map((aspect, idx) => (
+ -
+ •
+ {aspect}
+
+ ))}
+
+
+
+
Negative Aspects
+
+ {data.negative_aspects.map((aspect, idx) => (
+ -
+ •
+ {aspect}
+
+ ))}
+
+
+
+
+
+
Recommendations
+
+ {data.recommendations.map((rec, idx) => (
+ -
+ •
+ {rec}
+
+ ))}
+
+
+
+ );
+}
+
+function AnomaliesTab({
+ data,
+ loading,
+ error,
+ onRefresh,
+}: {
+ data: AnomalyDetectionResponse | null;
+ loading: boolean;
+ error: string | null;
+ onRefresh: () => void;
+}) {
+ if (loading)
+ return (
+
+
+
+ );
+ if (error)
+ return (
+
+ );
+ if (!data) return null;
+
+ return (
+
+
+
Anomaly Detection
+
+
+
+
+
Summary
+
{data.summary}
+
+
+ {data.anomalies.length > 0 ? (
+
+ {data.anomalies.map((anomaly, idx) => (
+
+
+
{anomaly.metric}
+
+ {anomaly.severity}
+
+
+
Date: {anomaly.date}
+
{anomaly.description}
+
+
+
Value
+
{anomaly.value.toFixed(2)}
+
+
+
Expected
+
{anomaly.expected_value.toFixed(2)}
+
+
+
Deviation
+
{anomaly.deviation.toFixed(2)}
+
+
+
+ ))}
+
+ ) : (
+
+
No anomalies detected. Your metrics are within normal ranges.
+
+ )}
+
+ );
+}
+
+function AttributionTab({
+ data,
+ loading,
+ error,
+ onRefresh,
+}: {
+ data: AttributionModelingResponse | null;
+ loading: boolean;
+ error: string | null;
+ onRefresh: () => void;
+}) {
+ if (loading)
+ return (
+
+
+
+ );
+ if (error)
+ return (
+
+ );
+ if (!data) return null;
+
+ return (
+
+
+
Attribution Modeling
+
+
+
+
+
Attribution Breakdown
+
+ {Object.entries(data.attribution).map(([channel, percent]) => (
+
+
+ {channel}
+ {percent.toFixed(1)}%
+
+
+
+ ))}
+
+
+
+
+
Top Contributors
+
+ {data.top_contributors.map((contributor, idx) => (
+
+
+
{contributor.name}
+
+ {contributor.contribution_percent.toFixed(1)}%
+
+
+
+ Total Value: {contributor.total_value.toFixed(2)}
+
+
{contributor.insight}
+
+ ))}
+
+
+
+
+
Insights
+
+ {data.insights.map((insight, idx) => (
+ -
+ •
+ {insight}
+
+ ))}
+
+
+
+ );
+}
+
+function BenchmarkingTab({
+ data,
+ loading,
+ error,
+ onRefresh,
+}: {
+ data: BenchmarkingResponse | null;
+ loading: boolean;
+ error: string | null;
+ onRefresh: () => void;
+}) {
+ if (loading)
+ return (
+
+
+
+ );
+ if (error)
+ return (
+
+ );
+ if (!data) return null;
+
+ return (
+
+
+
Industry Benchmarking
+
+
+
+
+ {Object.entries(data.comparison).map(([metric, comp]) => (
+
+
{metric}
+
+
+ Your Value
+ {comp.your_value.toFixed(2)}
+
+
+ Industry Average
+ {comp.industry_avg.toFixed(2)}
+
+
+ Percentile
+ {comp.percentile}th
+
+
+
+ {comp.status} average
+
+
+
+
+ ))}
+
+
+
+
Recommendations
+
+ {data.recommendations.map((rec, idx) => (
+ -
+ •
+ {rec}
+
+ ))}
+
+
+
+ );
+}
+
+function ChurnTab({
+ data,
+ loading,
+ error,
+ onRefresh,
+}: {
+ data: ChurnPredictionResponse | null;
+ loading: boolean;
+ error: string | null;
+ onRefresh: () => void;
+}) {
+ if (loading)
+ return (
+
+
+
+ );
+ if (error)
+ return (
+
+ );
+ if (!data) return null;
+
+ return (
+
+
+
Churn Prediction
+
+
+
+ {data.at_risk_segments.length > 0 ? (
+
+ {data.at_risk_segments.map((segment, idx) => (
+
+
+
{segment.segment}
+
+ {(segment.risk_score * 100).toFixed(0)}% Risk
+
+
+
+
+
Indicators
+
+ {segment.indicators.map((indicator, indIdx) => (
+ -
+ •
+ {indicator}
+
+ ))}
+
+
+
+
Recommendations
+
+ {segment.recommendations.map((rec, recIdx) => (
+ -
+ •
+ {rec}
+
+ ))}
+
+
+
+
+ ))}
+
+ ) : (
+
+
No high-risk segments detected. Your audience engagement is stable.
+
+ )}
+
+
+
General Recommendations
+
+ {data.recommendations.map((rec, idx) => (
+ -
+ •
+ {rec}
+
+ ))}
+
+
+
+ );
+}
+
+function KPITab({
+ data,
+ loading,
+ error,
+ onRefresh,
+}: {
+ data: KPIOptimizationResponse | null;
+ loading: boolean;
+ error: string | null;
+ onRefresh: () => void;
+}) {
+ if (loading)
+ return (
+
+
+
+ );
+ if (error)
+ return (
+
+ );
+ if (!data) return null;
+
+ return (
+
+
+
KPI Optimization
+
+
+
+
+
Current KPIs
+
+ {Object.entries(data.current_kpis).map(([kpi, value]) => (
+
+
{kpi}
+
{value.toFixed(2)}
+
+ ))}
+
+
+
+
+ {data.optimization_suggestions.map((suggestion, idx) => (
+
+
+
{suggestion.kpi}
+
+ {suggestion.expected_impact} impact
+
+
+
+
+
Current Value
+
+ {suggestion.current_value.toFixed(2)}
+
+
+
+
Target Value
+
+ {suggestion.target_value.toFixed(2)}
+
+
+
+
+
Suggestions
+
+ {suggestion.suggestions.map((sug, sugIdx) => (
+ -
+ •
+ {sug}
+
+ ))}
+
+
+
+ ))}
+
+
+
+
Priority Actions
+
+ {data.priority_actions.map((action, idx) => (
+ -
+ {idx + 1}.
+ {action}
+
+ ))}
+
+
+
+ );
+}
+
diff --git a/frontend/components/contracts/ContractsWorkspace.tsx b/frontend/components/contracts/ContractsWorkspace.tsx
index e0b1140..08edfa5 100644
--- a/frontend/components/contracts/ContractsWorkspace.tsx
+++ b/frontend/components/contracts/ContractsWorkspace.tsx
@@ -3,22 +3,32 @@
import {
approveContractVersion,
approveDeliverablesList,
+ askContractQuestion,
createContractVersion,
createOrUpdateDeliverablesList,
+ explainContractClause,
fetchContractChatMessages,
fetchContractDeliverables,
fetchContractDetail,
fetchContractVersions,
fetchContracts,
+ generateContractTemplate,
postContractChatMessage,
requestContractStatusChange,
respondToStatusChangeRequest,
reviewDeliverable,
submitDeliverable,
+ summarizeContract,
trackSignedContractDownload,
trackUnsignedContractDownload,
+ translateContract,
updateSignedContractLink,
updateUnsignedContractLink,
+ type ClauseExplanation,
+ type ContractQuestionAnswer,
+ type ContractSummary,
+ type ContractTemplate,
+ type ContractTranslation,
} from "@/lib/api/proposals";
import {
Contract,
@@ -33,14 +43,19 @@ import {
CheckCircle,
Download,
FileText,
+ HelpCircle,
History,
+ Languages,
Loader2,
MessageCircle,
Plus,
Send,
+ Sparkles,
Upload,
X,
XCircle,
+ FileCode,
+ BookOpen,
} from "lucide-react";
import { useEffect, useMemo, useRef, useState } from "react";
@@ -144,6 +159,30 @@ export function ContractsWorkspace({ role }: ContractsWorkspaceProps) {
const [creatingAmendment, setCreatingAmendment] = useState(false);
const [approvingVersion, setApprovingVersion] = useState
(null);
+ // AI Features State
+ const [questionText, setQuestionText] = useState("");
+ const [questionAnswer, setQuestionAnswer] = useState(null);
+ const [loadingQuestion, setLoadingQuestion] = useState(false);
+ const [contractSummary, setContractSummary] = useState(null);
+ const [loadingSummary, setLoadingSummary] = useState(false);
+ const [clauseText, setClauseText] = useState("");
+ const [clauseContext, setClauseContext] = useState("");
+ const [clauseExplanation, setClauseExplanation] = useState(null);
+ const [loadingClause, setLoadingClause] = useState(false);
+ const [translationLanguage, setTranslationLanguage] = useState("es");
+ const [contractTranslation, setContractTranslation] = useState(null);
+ const [loadingTranslation, setLoadingTranslation] = useState(false);
+ const [showTemplateModal, setShowTemplateModal] = useState(false);
+ const [templateData, setTemplateData] = useState({
+ deal_type: "",
+ deliverables: "",
+ payment_amount: "",
+ duration: "",
+ additional_requirements: "",
+ });
+ const [generatedTemplate, setGeneratedTemplate] = useState(null);
+ const [loadingTemplate, setLoadingTemplate] = useState(false);
+
const selectedContract = useMemo(() => {
if (!selectedContractId) return null;
if (contractDetail && contractDetail.id === selectedContractId) {
@@ -169,6 +208,14 @@ export function ContractsWorkspace({ role }: ContractsWorkspaceProps) {
setContractVersions([]);
setCurrentVersion(null);
}
+ // Reset AI state when switching contracts
+ setQuestionAnswer(null);
+ setContractSummary(null);
+ setClauseExplanation(null);
+ setContractTranslation(null);
+ setQuestionText("");
+ setClauseText("");
+ setClauseContext("");
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedContractId]);
@@ -456,6 +503,103 @@ export function ContractsWorkspace({ role }: ContractsWorkspaceProps) {
}
}
+ // AI Feature Handlers
+ async function handleAskQuestion() {
+ if (!selectedContract || !questionText.trim()) {
+ setGlobalError("Please enter a question");
+ return;
+ }
+ setLoadingQuestion(true);
+ setQuestionAnswer(null);
+ try {
+ const result = await askContractQuestion(selectedContract.id, {
+ question: questionText.trim(),
+ });
+ setQuestionAnswer(result);
+ } catch (error: any) {
+ setGlobalError(error?.message || "Failed to get answer");
+ } finally {
+ setLoadingQuestion(false);
+ }
+ }
+
+ async function handleSummarizeContract() {
+ if (!selectedContract) return;
+ setLoadingSummary(true);
+ setContractSummary(null);
+ try {
+ const result = await summarizeContract(selectedContract.id);
+ setContractSummary(result);
+ } catch (error: any) {
+ setGlobalError(error?.message || "Failed to summarize contract");
+ } finally {
+ setLoadingSummary(false);
+ }
+ }
+
+ async function handleExplainClause() {
+ if (!selectedContract || !clauseText.trim()) {
+ setGlobalError("Please enter the clause text to explain");
+ return;
+ }
+ setLoadingClause(true);
+ setClauseExplanation(null);
+ try {
+ const result = await explainContractClause(selectedContract.id, {
+ clause_text: clauseText.trim(),
+ clause_context: clauseContext.trim() || undefined,
+ });
+ setClauseExplanation(result);
+ } catch (error: any) {
+ setGlobalError(error?.message || "Failed to explain clause");
+ } finally {
+ setLoadingClause(false);
+ }
+ }
+
+ async function handleTranslateContract() {
+ if (!selectedContract) return;
+ setLoadingTranslation(true);
+ setContractTranslation(null);
+ try {
+ const result = await translateContract(selectedContract.id, {
+ target_language: translationLanguage,
+ });
+ setContractTranslation(result);
+ } catch (error: any) {
+ setGlobalError(error?.message || "Failed to translate contract");
+ } finally {
+ setLoadingTranslation(false);
+ }
+ }
+
+ async function handleGenerateTemplate() {
+ if (!templateData.deal_type.trim()) {
+ setGlobalError("Please specify the deal type");
+ return;
+ }
+ setLoadingTemplate(true);
+ setGeneratedTemplate(null);
+ try {
+ const result = await generateContractTemplate({
+ deal_type: templateData.deal_type,
+ deliverables: templateData.deliverables
+ ? templateData.deliverables.split(",").map((d) => d.trim()).filter(Boolean)
+ : undefined,
+ payment_amount: templateData.payment_amount
+ ? parseFloat(templateData.payment_amount)
+ : undefined,
+ duration: templateData.duration || undefined,
+ additional_requirements: templateData.additional_requirements || undefined,
+ });
+ setGeneratedTemplate(result);
+ } catch (error: any) {
+ setGlobalError(error?.message || "Failed to generate template");
+ } finally {
+ setLoadingTemplate(false);
+ }
+ }
+
async function handleUploadUnsignedLink(contractId: string) {
if (!unsignedLinkInput.trim()) {
setGlobalError("Please enter a valid link");
@@ -674,10 +818,222 @@ export function ContractsWorkspace({ role }: ContractsWorkspaceProps) {