fix(transcription): Behebt Start- und API-Fehler in der App [2f488f42]
This commit is contained in:
@@ -84,6 +84,10 @@ export default function App() {
|
||||
const [insightResult, setInsightResult] = useState('')
|
||||
const [insightTitle, setInsightTitle] = useState('')
|
||||
|
||||
// Translation State
|
||||
const [translationResult, setTranslationResult] = useState<AnalysisResult | null>(null)
|
||||
const [activeTab, setActiveTab] = useState<'transcript' | 'translation'>('transcript');
|
||||
|
||||
const fetchMeetings = async () => {
|
||||
try {
|
||||
const res = await axios.get(`${API_BASE}/meetings`)
|
||||
@@ -170,6 +174,43 @@ export default function App() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleTranslate = async () => {
|
||||
if (!detailMeeting) return;
|
||||
setInsightLoading(true);
|
||||
|
||||
try {
|
||||
// Use a consistent key for the translation type
|
||||
const translationType = 'translation_english';
|
||||
|
||||
// Check if result already exists in the main meeting object
|
||||
const existing = detailMeeting.analysis_results?.find(r => r.prompt_key === translationType);
|
||||
if (existing) {
|
||||
setTranslationResult(existing);
|
||||
// Here you might want to switch to a "Translation" tab or show a modal
|
||||
alert("Translation already exists and has been loaded.");
|
||||
return;
|
||||
}
|
||||
|
||||
// If not, generate it
|
||||
const res = await axios.post(`${API_BASE}/meetings/${detailMeeting.id}/translate`, {
|
||||
target_language: 'English'
|
||||
});
|
||||
|
||||
// The API returns an AnalysisResult object, let's store it
|
||||
setTranslationResult(res.data);
|
||||
|
||||
// Also refresh the main meeting detail to include this new result for persistence
|
||||
fetchDetail(detailMeeting.id);
|
||||
setActiveTab('translation');
|
||||
alert("Translation successful!");
|
||||
|
||||
} catch(e) {
|
||||
console.error("Failed to generate translation", e);
|
||||
alert(`Error: Could not translate.`);
|
||||
} finally {
|
||||
setInsightLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
// --- GLOBAL SPEAKER MANAGEMENT ---
|
||||
|
||||
@@ -378,17 +419,17 @@ 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>
|
||||
<h3 className="font-bold text-purple-800 dark:text-purple-200">AI Tools</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">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 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>
|
||||
<button disabled={insightLoading} onClick={() => handleTranslate()} 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">Translate (EN)</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -402,116 +443,143 @@ export default function App() {
|
||||
)}
|
||||
|
||||
<div className="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 shadow-sm overflow-hidden">
|
||||
{flatMessages.length > 0 ? (
|
||||
<div className="divide-y divide-slate-100 dark:divide-slate-800">
|
||||
{flatMessages.map((msg, uniqueIdx) => {
|
||||
const isEditingSpeaker = editingRow?.chunkId === msg._chunkId && editingRow?.idx === msg._idx && editingRow?.field === 'speaker';
|
||||
const isEditingText = editingRow?.chunkId === msg._chunkId && editingRow?.idx === msg._idx && editingRow?.field === 'text';
|
||||
{/* TAB Navigation */}
|
||||
<div className="flex border-b border-slate-200 dark:border-slate-800">
|
||||
<button
|
||||
onClick={() => setActiveTab('transcript')}
|
||||
className={clsx("px-4 py-3 font-medium text-sm", activeTab === 'transcript' ? "text-blue-600 border-b-2 border-blue-600" : "text-slate-500 hover:bg-slate-50 dark:hover:bg-slate-800/50")}
|
||||
>
|
||||
Original Transcript
|
||||
</button>
|
||||
{translationResult && (
|
||||
<button
|
||||
onClick={() => setActiveTab('translation')}
|
||||
className={clsx("px-4 py-3 font-medium text-sm", activeTab === 'translation' ? "text-blue-600 border-b-2 border-blue-600" : "text-slate-500 hover:bg-slate-50 dark:hover:bg-slate-800/50")}
|
||||
>
|
||||
English Translation
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
return (
|
||||
<div key={uniqueIdx} className="p-4 hover:bg-slate-50 dark:hover:bg-slate-800/50 transition-colors flex gap-4 group relative">
|
||||
{/* Time */}
|
||||
<div className="w-16 pt-1 flex flex-col items-end gap-1 flex-shrink-0">
|
||||
<span className="text-xs text-slate-400 font-mono">{msg.display_time || "00:00"}</span>
|
||||
</div>
|
||||
{/* Conditional Content */}
|
||||
{activeTab === 'transcript' && (
|
||||
flatMessages.length > 0 ? (
|
||||
<div className="divide-y divide-slate-100 dark:divide-slate-800">
|
||||
{flatMessages.map((msg, uniqueIdx) => {
|
||||
const isEditingSpeaker = editingRow?.chunkId === msg._chunkId && editingRow?.idx === msg._idx && editingRow?.field === 'speaker';
|
||||
const isEditingText = editingRow?.chunkId === msg._chunkId && editingRow?.idx === msg._idx && editingRow?.field === 'text';
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
{/* Speaker */}
|
||||
<div className="mb-1 flex items-center gap-2">
|
||||
{isEditingSpeaker ? (
|
||||
<input
|
||||
autoFocus
|
||||
className="text-sm font-bold text-blue-600 dark:text-blue-400 bg-slate-100 dark:bg-slate-800 border rounded px-2 py-0.5 outline-none w-48"
|
||||
value={editValue}
|
||||
onChange={e => setEditValue(e.target.value)}
|
||||
onBlur={() => handleUpdateRow(msg._chunkId!, msg._idx!, 'speaker', editValue)}
|
||||
onKeyDown={e => e.key === 'Enter' && handleUpdateRow(msg._chunkId!, msg._idx!, 'speaker', editValue)}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className="font-bold text-sm text-blue-600 dark:text-blue-400 cursor-pointer hover:underline flex items-center gap-1 w-fit"
|
||||
onClick={() => { setEditingRow({chunkId: msg._chunkId!, idx: msg._idx!, field: 'speaker'}); setEditValue(msg.speaker); }}
|
||||
>
|
||||
{msg.speaker}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
return (
|
||||
<div key={uniqueIdx} className="p-4 hover:bg-slate-50 dark:hover:bg-slate-800/50 transition-colors flex gap-4 group relative">
|
||||
{/* Time */}
|
||||
<div className="w-16 pt-1 flex flex-col items-end gap-1 flex-shrink-0">
|
||||
<span className="text-xs text-slate-400 font-mono">{msg.display_time || "00:00"}</span>
|
||||
</div>
|
||||
|
||||
{/* Text */}
|
||||
<div>
|
||||
{isEditingText ? (
|
||||
<textarea
|
||||
autoFocus
|
||||
className="w-full text-slate-800 dark:text-slate-200 bg-slate-100 dark:bg-slate-800 border rounded px-2 py-1 outline-none min-h-[60px]"
|
||||
value={editValue}
|
||||
onChange={e => setEditValue(e.target.value)}
|
||||
onBlur={() => handleUpdateRow(msg._chunkId!, msg._idx!, 'text', editValue)}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className="text-slate-800 dark:text-slate-200 leading-relaxed whitespace-pre-wrap cursor-text hover:bg-slate-100/50 dark:hover:bg-slate-800/30 rounded p-1 -ml-1 transition-colors"
|
||||
onClick={() => { setEditingRow({chunkId: msg._chunkId!, idx: msg._idx!, field: 'text'}); setEditValue(msg.text); }}
|
||||
>
|
||||
{msg.text}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
{/* Speaker */}
|
||||
<div className="mb-1 flex items-center gap-2">
|
||||
{isEditingSpeaker ? (
|
||||
<input
|
||||
autoFocus
|
||||
className="text-sm font-bold text-blue-600 dark:text-blue-400 bg-slate-100 dark:bg-slate-800 border rounded px-2 py-0.5 outline-none w-48"
|
||||
value={editValue}
|
||||
onChange={e => setEditValue(e.target.value)}
|
||||
onBlur={() => handleUpdateRow(msg._chunkId!, msg._idx!, 'speaker', editValue)}
|
||||
onKeyDown={e => e.key === 'Enter' && handleUpdateRow(msg._chunkId!, msg._idx!, 'speaker', editValue)}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className="font-bold text-sm text-blue-600 dark:text-blue-400 cursor-pointer hover:underline flex items-center gap-1 w-fit"
|
||||
onClick={() => { setEditingRow({chunkId: msg._chunkId!, idx: msg._idx!, field: 'speaker'}); setEditValue(msg.speaker); }}
|
||||
>
|
||||
{msg.speaker}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Hover Actions (Trim / Delete) */}
|
||||
<div className="absolute right-2 top-2 opacity-0 group-hover:opacity-100 flex gap-1 bg-white dark:bg-slate-900 border dark:border-slate-700 shadow-sm rounded-lg p-1 transition-all">
|
||||
<button
|
||||
onClick={() => handleTrim(msg, 'start')}
|
||||
className="p-1.5 text-slate-400 hover:text-red-500 hover:bg-red-50 rounded"
|
||||
title="Trim Start: Delete everything BEFORE this line"
|
||||
>
|
||||
<Scissors className="h-3.5 w-3.5 rotate-180" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleTrim(msg, 'end')}
|
||||
className="p-1.5 text-slate-400 hover:text-red-500 hover:bg-red-50 rounded"
|
||||
title="Trim End: Delete everything AFTER this line"
|
||||
>
|
||||
<Scissors className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<div className="w-px bg-slate-200 dark:bg-slate-700 mx-1"></div>
|
||||
<button
|
||||
onClick={() => {
|
||||
// Single delete logic is redundant if we have trims, but nice to keep
|
||||
// Reuse update logic but filter out index
|
||||
// Simplified: Just use handleTrim but that's overkill
|
||||
// Let's implement quick single delete
|
||||
const cId = msg._chunkId!;
|
||||
const idx = msg._idx!;
|
||||
const newChunks = detailMeeting?.chunks?.map(c => {
|
||||
if (c.id === cId && c.json_content) {
|
||||
return { ...c, json_content: c.json_content.filter((_, i) => i !== idx) }
|
||||
}
|
||||
return c
|
||||
}) || [];
|
||||
setDetailMeeting(prev => prev ? ({ ...prev, chunks: newChunks }) : null);
|
||||
const updatedC = newChunks.find(c => c.id === cId);
|
||||
if(updatedC?.json_content) saveChunkUpdate(cId, updatedC.json_content);
|
||||
}}
|
||||
className="p-1.5 text-slate-400 hover:text-red-500 hover:bg-red-50 rounded"
|
||||
title="Delete this line"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
{/* Text */}
|
||||
<div>
|
||||
{isEditingText ? (
|
||||
<textarea
|
||||
autoFocus
|
||||
className="w-full text-slate-800 dark:text-slate-200 bg-slate-100 dark:bg-slate-800 border rounded px-2 py-1 outline-none min-h-[60px]"
|
||||
value={editValue}
|
||||
onChange={e => setEditValue(e.target.value)}
|
||||
onBlur={() => handleUpdateRow(msg._chunkId!, msg._idx!, 'text', editValue)}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className="text-slate-800 dark:text-slate-200 leading-relaxed whitespace-pre-wrap cursor-text hover:bg-slate-100/50 dark:hover:bg-slate-800/30 rounded p-1 -ml-1 transition-colors"
|
||||
onClick={() => { setEditingRow({chunkId: msg._chunkId!, idx: msg._idx!, field: 'text'}); setEditValue(msg.text); }}
|
||||
>
|
||||
{msg.text}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hover Actions (Trim / Delete) */}
|
||||
<div className="absolute right-2 top-2 opacity-0 group-hover:opacity-100 flex gap-1 bg-white dark:bg-slate-900 border dark:border-slate-700 shadow-sm rounded-lg p-1 transition-all">
|
||||
<button
|
||||
onClick={() => handleTrim(msg, 'start')}
|
||||
className="p-1.5 text-slate-400 hover:text-red-500 hover:bg-red-50 rounded"
|
||||
title="Trim Start: Delete everything BEFORE this line"
|
||||
>
|
||||
<Scissors className="h-3.5 w-3.5 rotate-180" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleTrim(msg, 'end')}
|
||||
className="p-1.5 text-slate-400 hover:text-red-500 hover:bg-red-50 rounded"
|
||||
title="Trim End: Delete everything AFTER this line"
|
||||
>
|
||||
<Scissors className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<div className="w-px bg-slate-200 dark:bg-slate-700 mx-1"></div>
|
||||
<button
|
||||
onClick={() => {
|
||||
// Single delete logic is redundant if we have trims, but nice to keep
|
||||
// Reuse update logic but filter out index
|
||||
// Simplified: Just use handleTrim but that's overkill
|
||||
// Let's implement quick single delete
|
||||
const cId = msg._chunkId!;
|
||||
const idx = msg._idx!;
|
||||
const newChunks = detailMeeting?.chunks?.map(c => {
|
||||
if (c.id === cId && c.json_content) {
|
||||
return { ...c, json_content: c.json_content.filter((_, i) => i !== idx) }
|
||||
}
|
||||
return c
|
||||
}) || [];
|
||||
setDetailMeeting(prev => prev ? ({ ...prev, chunks: newChunks }) : null);
|
||||
const updatedC = newChunks.find(c => c.id === cId);
|
||||
if(updatedC?.json_content) saveChunkUpdate(cId, updatedC.json_content);
|
||||
}}
|
||||
className="p-1.5 text-slate-400 hover:text-red-500 hover:bg-red-50 rounded"
|
||||
title="Delete this line"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)})}
|
||||
</div>
|
||||
)})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-12">
|
||||
{detailMeeting.chunks && detailMeeting.chunks.length > 0 && detailMeeting.chunks[0].raw_text ? (
|
||||
<div className="p-8">
|
||||
<p className="text-yellow-600 mb-2 font-medium">Legacy Format</p>
|
||||
<p className="text-slate-500 text-sm mb-4">Re-upload file to enable editing.</p>
|
||||
<div className="text-left font-mono text-xs overflow-auto max-h-96">{detailMeeting.chunks[0].raw_text}</div>
|
||||
</div>
|
||||
) : (<p className="text-slate-500">Processing...</p>)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-12">
|
||||
{detailMeeting.chunks && detailMeeting.chunks.length > 0 && detailMeeting.chunks[0].raw_text ? (
|
||||
<div className="p-8">
|
||||
<p className="text-yellow-600 mb-2 font-medium">Legacy Format</p>
|
||||
<p className="text-slate-500 text-sm mb-4">Re-upload file to enable editing.</p>
|
||||
<div className="text-left font-mono text-xs overflow-auto max-h-96">{detailMeeting.chunks[0].raw_text}</div>
|
||||
</div>
|
||||
) : (<p className="text-slate-500">Processing...</p>)}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
{activeTab === 'translation' && (
|
||||
<div className="p-6">
|
||||
<pre className="whitespace-pre-wrap font-sans text-sm leading-relaxed">{translationResult?.result_text || "Loading translation..."}</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user