feat(transcription): [2f388f42] integrate prompt database and AI insights

Implements the core functionality for the AI-powered analysis of meeting transcripts in the Transcription Tool.

This commit introduces a new 'AI Insights' feature that allows users to generate various summaries and analyses from a transcript on demand.

- Creates a  to manage and version different AI prompts for tasks like generating meeting minutes, extracting action items, and creating sales summaries.
- Adds a new  responsible for orchestrating the analysis process: fetching the transcript, calling the Gemini API with the appropriate prompt, and caching the results in the database.
- Extends the FastAPI backend with a new endpoint  to trigger the insight generation.
- Updates the React frontend () with a new 'AI Insights' panel, including buttons to trigger the analyses and a modal to display the results.
- Updates the documentation () to reflect the new features, API endpoints, and version.
This commit is contained in:
2026-01-26 07:43:24 +00:00
parent f96235c607
commit e427ec19f2
5 changed files with 348 additions and 10 deletions

View File

@@ -1,10 +1,19 @@
import React, { useState, useEffect } from 'react'
import axios from 'axios'
import { Upload, FileText, Clock, CheckCircle2, Loader2, AlertCircle, Trash2, ArrowLeft, Copy, Edit3, X, Scissors, Users } from 'lucide-react'
import { Upload, FileText, Clock, CheckCircle2, Loader2, AlertCircle, Trash2, ArrowLeft, Copy, Edit3, X, Scissors, Users, Wand2 } from 'lucide-react'
import clsx from 'clsx'
const API_BASE = '/tr/api'
// --- INTERFACES ---
interface AnalysisResult {
id: number;
prompt_key: string;
result_text: string;
created_at: string;
}
interface TranscriptMessage {
time: string
display_time: string
@@ -30,8 +39,29 @@ interface Meeting {
duration_seconds?: number
created_at: string
chunks?: Chunk[]
analysis_results?: AnalysisResult[]
}
// --- MODAL COMPONENT ---
const InsightModal = ({ title, content, onClose, onCopy }: { title: string, content: string, onClose: () => void, onCopy: (text: string) => void }) => (
<div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4 backdrop-blur-sm animate-fade-in">
<div className="bg-white dark:bg-slate-900 rounded-2xl shadow-2xl w-full max-w-3xl max-h-[90vh] flex flex-col border border-slate-200 dark:border-slate-800">
<header className="p-4 border-b border-slate-200 dark:border-slate-800 flex justify-between items-center flex-shrink-0">
<h2 className="text-lg font-bold flex items-center gap-2"><Wand2 className="h-5 w-5 text-blue-500" /> {title}</h2>
<div className='flex items-center gap-2'>
<button onClick={() => onCopy(content)} className="flex items-center gap-2 text-sm px-3 py-1.5 bg-slate-100 dark:bg-slate-800 hover:bg-slate-200 dark:hover:bg-slate-700 rounded-md font-medium transition-colors"><Copy className='h-4 w-4'/> Copy</button>
<button onClick={onClose} className="p-1.5 text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-full"><X className="h-5 w-5" /></button>
</div>
</header>
<main className="p-6 overflow-auto">
<pre className="whitespace-pre-wrap font-sans text-sm leading-relaxed">{content}</pre>
</main>
</div>
</div>
)
export default function App() {
const [view, setView] = useState<'list' | 'detail'>('list')
const [selectedId, setSelectedId] = useState<number | null>(null)
@@ -48,6 +78,12 @@ export default function App() {
const [editValue, setEditValue] = useState("")
const [isProcessing, setIsProcessing] = useState(false)
// Insight State
const [showInsightModal, setShowInsightModal] = useState(false)
const [insightLoading, setInsightLoading] = useState(false)
const [insightResult, setInsightResult] = useState('')
const [insightTitle, setInsightTitle] = useState('')
const fetchMeetings = async () => {
try {
const res = await axios.get(`${API_BASE}/meetings`)
@@ -102,6 +138,39 @@ export default function App() {
} catch (e) { alert("Delete failed") }
}
// --- AI INSIGHTS ---
const handleGenerateInsight = async (insightType: string, title: string) => {
if (!detailMeeting) return;
setInsightLoading(true);
setInsightTitle(title);
try {
// First, check if result already exists
const existing = detailMeeting.analysis_results?.find(r => r.prompt_key === insightType);
if (existing) {
setInsightResult(existing.result_text);
setShowInsightModal(true);
return;
}
// If not, generate it
const res = await axios.post(`${API_BASE}/meetings/${detailMeeting.id}/insights`, {
insight_type: insightType
});
setInsightResult(res.data.result_text);
setShowInsightModal(true);
// Refresh detail view to get the new analysis result in the meeting object
fetchDetail(detailMeeting.id);
} catch(e) {
console.error("Failed to generate insight", e);
alert(`Error: Could not generate ${title}.`);
} finally {
setInsightLoading(false);
}
}
// --- GLOBAL SPEAKER MANAGEMENT ---
const getUniqueSpeakers = () => {
@@ -235,6 +304,7 @@ export default function App() {
return (
<div className="min-h-screen bg-slate-50 dark:bg-slate-950 text-slate-900 dark:text-slate-200 font-sans">
{showInsightModal && <InsightModal title={insightTitle} content={insightResult} onClose={() => setShowInsightModal(false)} onCopy={text => navigator.clipboard.writeText(text)} />}
<div className="max-w-4xl mx-auto px-4 py-8">
<button
onClick={() => { setView('list'); setSelectedId(null); setDetailMeeting(null); }}
@@ -287,6 +357,20 @@ export default function App() {
</div>
)}
{/* AI INSIGHTS PANEL */}
<div className="mb-6 bg-purple-50 dark:bg-purple-900/20 p-4 rounded-xl border border-purple-100 dark:border-purple-800">
<div className="flex items-center gap-3 mb-3">
<Wand2 className="h-5 w-5 text-purple-600 dark:text-purple-300" />
<h3 className="font-bold text-purple-800 dark:text-purple-200">AI Insights</h3>
{insightLoading && <Loader2 className="h-4 w-4 animate-spin text-purple-500" />}
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<button disabled={insightLoading} onClick={() => handleGenerateInsight('meeting_minutes', 'Meeting Protocol')} className="text-sm font-medium px-4 py-2 bg-white dark:bg-slate-800 border dark:border-slate-700 rounded-lg shadow-sm hover:bg-slate-50 dark:hover:bg-slate-700/50 disabled:opacity-50 transition-colors">Generate Protocol</button>
<button disabled={insightLoading} onClick={() => handleGenerateInsight('action_items', 'Action Items')} className="text-sm font-medium px-4 py-2 bg-white dark:bg-slate-800 border dark:border-slate-700 rounded-lg shadow-sm hover:bg-slate-50 dark:hover:bg-slate-700/50 disabled:opacity-50 transition-colors">Extract Action Items</button>
<button disabled={insightLoading} onClick={() => handleGenerateInsight('sales_summary', 'Sales Summary')} className="text-sm font-medium px-4 py-2 bg-white dark:bg-slate-800 border dark:border-slate-700 rounded-lg shadow-sm hover:bg-slate-50 dark:hover:bg-slate-700/50 disabled:opacity-50 transition-colors">Sales Summary</button>
</div>
</div>
{isProcessing && (
<div className="fixed inset-0 bg-white/50 dark:bg-black/50 z-50 flex items-center justify-center backdrop-blur-sm">
<div className="bg-white dark:bg-slate-800 p-6 rounded-xl shadow-2xl flex items-center gap-4">