feat: rapportage UI refactor, speech streaming, docs & seed data
Rapportage: - Refactor workspace into modular components (quick-actions, timeline-card, timeline-sidebar) - Add updateReport action for inline editing - Improve report timeline with better UX Speech: - Add Deepgram token API endpoint - Add use-deepgram-streaming hook - Add confidence-text component Docs: - Add architecture documentation - Add performance optimization plan - Add speech specs and seed data docs Scripts & Data: - Add seed-reports script and migration - Update AGENTS.md guidelines 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
194
components/confidence-text.tsx
Normal file
194
components/confidence-text.tsx
Normal file
@@ -0,0 +1,194 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { TranscriptWord } from '@/hooks/use-deepgram-streaming'
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Types
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface ConfidenceTextProps {
|
||||
/** Array van woorden met confidence scores */
|
||||
words: TranscriptWord[]
|
||||
/** Extra CSS classes voor de container */
|
||||
className?: string
|
||||
/** Drempel voor medium confidence (default: 0.9) */
|
||||
mediumThreshold?: number
|
||||
/** Drempel voor low confidence (default: 0.7) */
|
||||
lowThreshold?: number
|
||||
}
|
||||
|
||||
type ConfidenceLevel = 'high' | 'medium' | 'low'
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Confidence Word Component
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
function ConfidenceWord({
|
||||
word,
|
||||
confidence,
|
||||
level,
|
||||
}: {
|
||||
word: string
|
||||
confidence: number
|
||||
level: ConfidenceLevel
|
||||
}) {
|
||||
const [showTooltip, setShowTooltip] = useState(false)
|
||||
|
||||
const confidencePercent = Math.round(confidence * 100)
|
||||
|
||||
// High confidence - geen markering
|
||||
if (level === 'high') {
|
||||
return <span>{word} </span>
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
className="relative inline-block"
|
||||
onMouseEnter={() => setShowTooltip(true)}
|
||||
onMouseLeave={() => setShowTooltip(false)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
'cursor-help border-b-2 border-dotted',
|
||||
level === 'medium' && 'border-amber-400 bg-amber-50/50',
|
||||
level === 'low' && 'border-orange-500 bg-orange-50/50'
|
||||
)}
|
||||
>
|
||||
{word}
|
||||
</span>
|
||||
|
||||
{/* Tooltip */}
|
||||
{showTooltip && (
|
||||
<span
|
||||
className={cn(
|
||||
'absolute bottom-full left-1/2 -translate-x-1/2 mb-1 px-2 py-1 text-xs rounded shadow-lg whitespace-nowrap z-50',
|
||||
level === 'medium' && 'bg-amber-100 text-amber-800 border border-amber-200',
|
||||
level === 'low' && 'bg-orange-100 text-orange-800 border border-orange-200'
|
||||
)}
|
||||
>
|
||||
Zekerheid: {confidencePercent}%
|
||||
{level === 'low' && (
|
||||
<span className="block text-[10px] opacity-75">
|
||||
Mogelijk onjuist herkend
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
{' '}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Main Component
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
export function ConfidenceText({
|
||||
words,
|
||||
className,
|
||||
mediumThreshold = 0.9,
|
||||
lowThreshold = 0.7,
|
||||
}: ConfidenceTextProps) {
|
||||
if (!words || words.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const getConfidenceLevel = (confidence: number): ConfidenceLevel => {
|
||||
if (confidence >= mediumThreshold) return 'high'
|
||||
if (confidence >= lowThreshold) return 'medium'
|
||||
return 'low'
|
||||
}
|
||||
|
||||
// Statistieken voor samenvatting
|
||||
const lowConfidenceCount = words.filter(
|
||||
(w) => w.confidence < lowThreshold
|
||||
).length
|
||||
const mediumConfidenceCount = words.filter(
|
||||
(w) => w.confidence >= lowThreshold && w.confidence < mediumThreshold
|
||||
).length
|
||||
|
||||
const hasIssues = lowConfidenceCount > 0 || mediumConfidenceCount > 0
|
||||
|
||||
return (
|
||||
<div className={cn('space-y-2', className)}>
|
||||
{/* Tekst met confidence markering */}
|
||||
<div className="text-sm text-slate-900 leading-relaxed">
|
||||
{words.map((word, index) => (
|
||||
<ConfidenceWord
|
||||
key={`${word.word}-${word.start}-${index}`}
|
||||
word={word.word}
|
||||
confidence={word.confidence}
|
||||
level={getConfidenceLevel(word.confidence)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Samenvatting indien er issues zijn */}
|
||||
{hasIssues && (
|
||||
<div className="text-xs text-slate-500 flex items-center gap-3 pt-1 border-t border-slate-100">
|
||||
{mediumConfidenceCount > 0 && (
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="w-2 h-2 rounded-full bg-amber-400" />
|
||||
{mediumConfidenceCount} woord{mediumConfidenceCount !== 1 && 'en'}{' '}
|
||||
<90% zeker
|
||||
</span>
|
||||
)}
|
||||
{lowConfidenceCount > 0 && (
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="w-2 h-2 rounded-full bg-orange-500" />
|
||||
{lowConfidenceCount} woord{lowConfidenceCount !== 1 && 'en'}{' '}
|
||||
<70% zeker
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Inline Confidence Preview (voor in de recorder)
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
export function ConfidencePreview({
|
||||
words,
|
||||
className,
|
||||
}: {
|
||||
words: TranscriptWord[]
|
||||
className?: string
|
||||
}) {
|
||||
if (!words || words.length === 0) return null
|
||||
|
||||
const lowCount = words.filter((w) => w.confidence < 0.7).length
|
||||
const mediumCount = words.filter(
|
||||
(w) => w.confidence >= 0.7 && w.confidence < 0.9
|
||||
).length
|
||||
|
||||
if (lowCount === 0 && mediumCount === 0) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'text-xs flex items-center gap-2 px-2 py-1 rounded bg-slate-50',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<span className="text-slate-500">Onzekere woorden:</span>
|
||||
{mediumCount > 0 && (
|
||||
<span className="text-amber-600 font-medium">
|
||||
{mediumCount}
|
||||
<span className="text-amber-400 ml-0.5">●</span>
|
||||
</span>
|
||||
)}
|
||||
{lowCount > 0 && (
|
||||
<span className="text-orange-600 font-medium">
|
||||
{lowCount}
|
||||
<span className="text-orange-500 ml-0.5">●</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user