summaryrefslogtreecommitdiff
path: root/makima/frontend/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'makima/frontend/src/components')
-rw-r--r--makima/frontend/src/components/listen/TranscriptAnalysisPanel.tsx339
1 files changed, 339 insertions, 0 deletions
diff --git a/makima/frontend/src/components/listen/TranscriptAnalysisPanel.tsx b/makima/frontend/src/components/listen/TranscriptAnalysisPanel.tsx
new file mode 100644
index 0000000..89d56a8
--- /dev/null
+++ b/makima/frontend/src/components/listen/TranscriptAnalysisPanel.tsx
@@ -0,0 +1,339 @@
+import { useState, useCallback } from "react";
+import {
+ analyzeTranscript,
+ createContractFromTranscript,
+ updateContractFromTranscript,
+ type TranscriptAnalysisResult,
+ type CreateContractResponse,
+} from "../../lib/listenApi";
+
+interface TranscriptAnalysisPanelProps {
+ fileId: string;
+ contractId: string;
+ selectedContractId: string | null;
+ onContractCreated?: (response: CreateContractResponse) => void;
+ onContractUpdated?: (response: CreateContractResponse) => void;
+ onClose?: () => void;
+}
+
+type AnalysisState = "idle" | "analyzing" | "analyzed" | "creating" | "updating";
+
+export function TranscriptAnalysisPanel({
+ fileId,
+ contractId,
+ selectedContractId,
+ onContractCreated,
+ onContractUpdated,
+ onClose,
+}: TranscriptAnalysisPanelProps) {
+ const [state, setState] = useState<AnalysisState>("idle");
+ const [analysis, setAnalysis] = useState<TranscriptAnalysisResult | null>(null);
+ const [error, setError] = useState<string | null>(null);
+ const [successMessage, setSuccessMessage] = useState<string | null>(null);
+
+ const handleAnalyze = useCallback(async () => {
+ setState("analyzing");
+ setError(null);
+ setSuccessMessage(null);
+
+ try {
+ const response = await analyzeTranscript(fileId);
+ setAnalysis(response.analysis);
+ setState("analyzed");
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Failed to analyze transcript");
+ setState("idle");
+ }
+ }, [fileId]);
+
+ const handleCreateContract = useCallback(async () => {
+ if (!analysis) return;
+
+ setState("creating");
+ setError(null);
+
+ try {
+ const response = await createContractFromTranscript(fileId, {
+ name: analysis.suggestedContractName,
+ description: analysis.suggestedDescription,
+ includeRequirements: true,
+ includeDecisions: true,
+ includeActionItems: true,
+ });
+ setSuccessMessage(`Created contract "${response.contractName}"`);
+ onContractCreated?.(response);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Failed to create contract");
+ setState("analyzed");
+ }
+ }, [fileId, analysis, onContractCreated]);
+
+ const handleUpdateContract = useCallback(async () => {
+ if (!analysis || !selectedContractId) return;
+
+ setState("updating");
+ setError(null);
+
+ try {
+ const response = await updateContractFromTranscript(fileId, selectedContractId, {
+ description: analysis.suggestedDescription,
+ includeRequirements: true,
+ includeDecisions: true,
+ includeActionItems: true,
+ });
+ setSuccessMessage(`Updated contract "${response.contractName}"`);
+ onContractUpdated?.(response);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Failed to update contract");
+ setState("analyzed");
+ }
+ }, [fileId, analysis, selectedContractId, onContractUpdated]);
+
+ // Group requirements by category
+ const groupedRequirements = analysis?.requirements.reduce((acc, req) => {
+ const category = req.category || "General";
+ if (!acc[category]) {
+ acc[category] = [];
+ }
+ acc[category].push(req);
+ return acc;
+ }, {} as Record<string, typeof analysis.requirements>) || {};
+
+ return (
+ <div className="panel p-4 flex flex-col gap-4">
+ {/* Header */}
+ <div className="flex justify-between items-center border-b border-dashed border-[rgba(117,170,252,0.35)] pb-2">
+ <div className="font-mono text-xs text-[#9bc3ff] tracking-wide uppercase">
+ TRANSCRIPT ANALYSIS//
+ </div>
+ {onClose && (
+ <button
+ onClick={onClose}
+ className="font-mono text-xs text-[#9bc3ff] hover:text-[#dbe7ff] transition-colors"
+ >
+ [X]
+ </button>
+ )}
+ </div>
+
+ {/* Error display */}
+ {error && (
+ <div className="font-mono text-xs text-red-400 px-2 py-2 border border-red-400/50 bg-red-400/10">
+ {error}
+ </div>
+ )}
+
+ {/* Success message */}
+ {successMessage && (
+ <div className="font-mono text-xs text-green-400 px-2 py-2 border border-green-400/50 bg-green-400/10">
+ {successMessage}
+ </div>
+ )}
+
+ {/* Initial state - Show analyze button */}
+ {state === "idle" && (
+ <div className="flex flex-col items-center gap-3 py-4">
+ <p className="font-mono text-sm text-[#9bc3ff] text-center">
+ Transcript saved. Analyze to extract requirements, decisions, and action items.
+ </p>
+ <button
+ onClick={handleAnalyze}
+ className="px-4 py-2 font-mono text-xs text-[#dbe7ff] bg-[#0d1b2d] border border-[#3f6fb3] hover:bg-[#153667] transition-colors uppercase tracking-wide"
+ >
+ Analyze Transcript
+ </button>
+ </div>
+ )}
+
+ {/* Analyzing state */}
+ {state === "analyzing" && (
+ <div className="flex flex-col items-center gap-3 py-8">
+ <div className="w-6 h-6 border-2 border-[#3f6fb3] border-t-[#75aafc] rounded-full animate-spin" />
+ <p className="font-mono text-sm text-[#9bc3ff]">Analyzing transcript...</p>
+ </div>
+ )}
+
+ {/* Analysis results */}
+ {(state === "analyzed" || state === "creating" || state === "updating") && analysis && (
+ <div className="flex flex-col gap-4 overflow-y-auto max-h-[60vh]">
+ {/* Suggested Contract Info */}
+ {(analysis.suggestedContractName || analysis.suggestedDescription) && (
+ <div className="border border-[rgba(117,170,252,0.25)] p-3">
+ <div className="font-mono text-xs text-[#75aafc] uppercase tracking-wide mb-2">
+ Suggested Contract
+ </div>
+ {analysis.suggestedContractName && (
+ <div className="font-mono text-sm text-[#dbe7ff] mb-1">
+ {analysis.suggestedContractName}
+ </div>
+ )}
+ {analysis.suggestedDescription && (
+ <div className="font-mono text-xs text-[#9bc3ff]">
+ {analysis.suggestedDescription}
+ </div>
+ )}
+ </div>
+ )}
+
+ {/* Requirements */}
+ {Object.keys(groupedRequirements).length > 0 && (
+ <div className="border border-[rgba(117,170,252,0.25)] p-3">
+ <div className="font-mono text-xs text-[#75aafc] uppercase tracking-wide mb-2">
+ Requirements ({analysis.requirements.length})
+ </div>
+ {Object.entries(groupedRequirements).map(([category, reqs]) => (
+ <div key={category} className="mb-3 last:mb-0">
+ <div className="font-mono text-[10px] text-[#3f6fb3] uppercase tracking-wider mb-1">
+ {category}
+ </div>
+ <ul className="space-y-1">
+ {reqs.map((req, idx) => (
+ <li key={idx} className="font-mono text-xs text-[#dbe7ff] flex gap-2">
+ <span className="text-[#3f6fb3]">-</span>
+ <span>{req.text}</span>
+ {req.speaker && (
+ <span className="text-[#9bc3ff] text-[10px]">({req.speaker})</span>
+ )}
+ </li>
+ ))}
+ </ul>
+ </div>
+ ))}
+ </div>
+ )}
+
+ {/* Decisions */}
+ {analysis.decisions.length > 0 && (
+ <div className="border border-[rgba(117,170,252,0.25)] p-3">
+ <div className="font-mono text-xs text-[#75aafc] uppercase tracking-wide mb-2">
+ Decisions ({analysis.decisions.length})
+ </div>
+ <ul className="space-y-2">
+ {analysis.decisions.map((decision, idx) => (
+ <li key={idx} className="font-mono text-xs text-[#dbe7ff]">
+ <div className="flex gap-2">
+ <span className="text-[#3f6fb3]">-</span>
+ <span>{decision.text}</span>
+ </div>
+ {decision.context && (
+ <div className="ml-4 text-[10px] text-[#9bc3ff] mt-1">
+ Context: {decision.context}
+ </div>
+ )}
+ </li>
+ ))}
+ </ul>
+ </div>
+ )}
+
+ {/* Action Items */}
+ {analysis.actionItems.length > 0 && (
+ <div className="border border-[rgba(117,170,252,0.25)] p-3">
+ <div className="font-mono text-xs text-[#75aafc] uppercase tracking-wide mb-2">
+ Action Items ({analysis.actionItems.length})
+ </div>
+ <ul className="space-y-2">
+ {analysis.actionItems.map((item, idx) => (
+ <li key={idx} className="font-mono text-xs text-[#dbe7ff]">
+ <div className="flex gap-2 items-start">
+ <span className="text-[#3f6fb3]">-</span>
+ <div className="flex-1">
+ <span>{item.text}</span>
+ <div className="flex gap-2 mt-1">
+ {item.assignee && (
+ <span className="text-[10px] px-1.5 py-0.5 bg-[#0f1c2f] border border-[#3f6fb3] text-[#9bc3ff]">
+ @{item.assignee}
+ </span>
+ )}
+ {item.priority && (
+ <span
+ className={`text-[10px] px-1.5 py-0.5 border ${
+ item.priority === "high"
+ ? "border-red-400/50 text-red-400 bg-red-400/10"
+ : item.priority === "medium"
+ ? "border-yellow-400/50 text-yellow-400 bg-yellow-400/10"
+ : "border-[#3f6fb3] text-[#9bc3ff] bg-[#0f1c2f]"
+ }`}
+ >
+ {item.priority}
+ </span>
+ )}
+ </div>
+ </div>
+ </div>
+ </li>
+ ))}
+ </ul>
+ </div>
+ )}
+
+ {/* Key Topics */}
+ {analysis.keyTopics.length > 0 && (
+ <div className="border border-[rgba(117,170,252,0.25)] p-3">
+ <div className="font-mono text-xs text-[#75aafc] uppercase tracking-wide mb-2">
+ Key Topics
+ </div>
+ <div className="flex flex-wrap gap-2">
+ {analysis.keyTopics.map((topic, idx) => (
+ <span
+ key={idx}
+ className="font-mono text-[10px] px-2 py-1 bg-[#0f1c2f] border border-[#3f6fb3] text-[#9bc3ff]"
+ >
+ {topic}
+ </span>
+ ))}
+ </div>
+ </div>
+ )}
+
+ {/* Speaker Statistics */}
+ {analysis.speakerSummary.length > 0 && (
+ <div className="border border-[rgba(117,170,252,0.25)] p-3">
+ <div className="font-mono text-xs text-[#75aafc] uppercase tracking-wide mb-2">
+ Speaker Statistics
+ </div>
+ <div className="space-y-2">
+ {analysis.speakerSummary.map((speaker, idx) => (
+ <div key={idx} className="flex items-center gap-3">
+ <span className="font-mono text-xs text-[#dbe7ff] min-w-[100px]">
+ {speaker.speaker}
+ </span>
+ <div className="flex-1 h-2 bg-[#0f1c2f] overflow-hidden">
+ <div
+ className="h-full bg-[#3f6fb3]"
+ style={{ width: `${speaker.contributionPercentage}%` }}
+ />
+ </div>
+ <span className="font-mono text-[10px] text-[#9bc3ff] min-w-[40px] text-right">
+ {speaker.contributionPercentage.toFixed(0)}%
+ </span>
+ </div>
+ ))}
+ </div>
+ </div>
+ )}
+
+ {/* Action Buttons */}
+ <div className="flex gap-2 pt-2 border-t border-dashed border-[rgba(117,170,252,0.35)]">
+ <button
+ onClick={handleCreateContract}
+ disabled={state === "creating" || state === "updating"}
+ className="flex-1 px-3 py-2 font-mono text-xs text-[#dbe7ff] bg-[#0d1b2d] border border-[#3f6fb3] hover:bg-[#153667] transition-colors uppercase tracking-wide disabled:opacity-50 disabled:cursor-not-allowed"
+ >
+ {state === "creating" ? "Creating..." : "Create New Contract"}
+ </button>
+ {selectedContractId && selectedContractId !== contractId && (
+ <button
+ onClick={handleUpdateContract}
+ disabled={state === "creating" || state === "updating"}
+ className="flex-1 px-3 py-2 font-mono text-xs text-[#dbe7ff] bg-[#0d1b2d] border border-[#0f3c78] hover:border-[#3f6fb3] transition-colors uppercase tracking-wide disabled:opacity-50 disabled:cursor-not-allowed"
+ >
+ {state === "updating" ? "Updating..." : "Add to Current Contract"}
+ </button>
+ )}
+ </div>
+ </div>
+ )}
+ </div>
+ );
+}