// src/renderer/components/shared/HistoryEntryCard.tsx // 공유: 히스토리 항목 카드 (Dashboard + HistoryPage에서 재사용) // Phase 10: 태그 표시/추가/삭제 기능 통합 import { useState, useEffect, useCallback } from 'react' import { Box, IconButton, Tooltip, Chip } from '@mui/material' import ContentCopyIcon from '@mui/icons-material/ContentCopy' import DeleteIcon from '@mui/icons-material/Delete' import LocalOfferIcon from '@mui/icons-material/LocalOffer' import CloseIcon from '@mui/icons-material/Close' import SummarizeIcon from '@mui/icons-material/Summarize' import ExpandMoreIcon from '@mui/icons-material/ExpandMore' import { MetalCard, Led } from '@d3ro/ui/components/ds' import { d3roPalette, d3roFontMono, d3roTypo, d3roRadius } from '@d3ro/ui/theme' import { useI18n } from '@d3ro/i18n' import { formatDuration } from '../../utils/formatters' import { isImeComposingEvent } from '../../utils/keyboard' import type { HistoryEntry, MemoTag, MeetingSummaryResult } from '@d3ro/core/types' interface HistoryEntryCardProps { entry: HistoryEntry onCopy?: (text: string) => void onDelete?: (id: string) => void showTags?: boolean onTagClick?: (tag: string) => void } export function HistoryEntryCard({ entry, onCopy, onDelete, showTags = false, onTagClick }: HistoryEntryCardProps): React.ReactElement { const { t, formatTime } = useI18n() const displayText = entry.polishedText || entry.originalText const [tags, setTags] = useState([]) const [tagInput, setTagInput] = useState('') const [showTagInput, setShowTagInput] = useState(false) const [summaryExpanded, setSummaryExpanded] = useState(false) const [summary, setSummary] = useState(null) const [summaryLoading, setSummaryLoading] = useState(false) const hasSummary = !!entry.summaryText const loadTags = useCallback(async () => { if (!showTags) return const result = await window.electronAPI.memo.getTags(entry.id) if (result.success) setTags(result.data) }, [entry.id, showTags]) useEffect(() => { loadTags() }, [loadTags]) const handleAddTag = useCallback(async () => { const trimmed = tagInput.trim() if (!trimmed) return const result = await window.electronAPI.memo.addTag(entry.id, trimmed) if (result.success) { setTags(prev => [...prev, result.data]) setTagInput('') setShowTagInput(false) } }, [entry.id, tagInput]) const handleRemoveTag = useCallback(async (tag: string) => { const result = await window.electronAPI.memo.removeTag(entry.id, tag) if (result.success) { setTags(prev => prev.filter(t => t.tag !== tag)) } }, [entry.id]) const handleTagKeyDown = useCallback((e: React.KeyboardEvent) => { if (isImeComposingEvent(e)) return if (e.key === 'Enter') { e.preventDefault(); handleAddTag() } if (e.key === 'Escape') { setShowTagInput(false); setTagInput('') } }, [handleAddTag]) const handleToggleSummary = useCallback(async () => { if (summaryExpanded) { setSummaryExpanded(false) return } setSummaryExpanded(true) if (!summary) { setSummaryLoading(true) const result = await window.electronAPI.meetingSummary.getSummary({ historyId: entry.id }) if (result.success && result.data) { setSummary(result.data) } setSummaryLoading(false) } }, [summaryExpanded, summary, entry.id]) const handleGenerateSummary = useCallback(async () => { setSummaryLoading(true) const result = await window.electronAPI.meetingSummary.summarize({ historyId: entry.id }) if (result.success) { setSummary(result.data) } setSummaryLoading(false) }, [entry.id]) const handleExportSummary = useCallback(async () => { await window.electronAPI.meetingSummary.exportMarkdown({ historyId: entry.id }) }, [entry.id]) return ( {/* 타이틀 */} {entry.title ?? displayText.slice(0, 60)} {/* 내용 미리보기 */} {displayText} {formatTime(entry.createdAt)} {formatDuration(entry.duration)} {entry.detectedLanguage && {entry.detectedLanguage.toUpperCase()}} {entry.mode.toUpperCase()} {hasSummary && ( } label={t('meetingSummary.title')} size="small" onClick={handleToggleSummary} sx={{ height: 18, fontFamily: d3roFontMono, fontSize: d3roTypo.micro.size, bgcolor: d3roPalette.tag.greenBg, color: d3roPalette.tag.green, borderRadius: d3roRadius.small, cursor: 'pointer', '& .MuiChip-icon': { color: d3roPalette.tag.green }, }} /> )} {/* 태그 영역 */} {showTags && ( {tags.map(tag => ( onTagClick?.(tag.tag)} onDelete={() => handleRemoveTag(tag.tag)} deleteIcon={} sx={{ height: 20, fontFamily: d3roFontMono, fontSize: d3roTypo.micro.size, bgcolor: d3roPalette.tag.purpleBg, color: d3roPalette.tag.purple, borderRadius: d3roRadius.small, '& .MuiChip-deleteIcon': { color: d3roPalette.tag.purple, fontSize: 12 }, '&:hover': { bgcolor: d3roPalette.tag.purple, color: d3roPalette.bg.card }, }} /> ))} {showTagInput ? ( ) => setTagInput(e.target.value)} onKeyDown={handleTagKeyDown} onBlur={() => { if (!tagInput.trim()) setShowTagInput(false) }} autoFocus placeholder={t('memo.tagPlaceholder')} sx={{ border: `1px solid ${d3roPalette.border.subtle}`, bgcolor: d3roPalette.bg.input, color: d3roPalette.text.primary, fontFamily: d3roFontMono, fontSize: d3roTypo.micro.size, px: 1, py: 0.25, borderRadius: d3roRadius.xs, outline: 'none', width: 100, '&:focus': { borderColor: d3roPalette.accent.amber }, }} /> ) : ( setShowTagInput(true)} sx={{ p: 0.25, color: d3roPalette.text.muted, '&:hover': { color: d3roPalette.accent.amber } }} > )} )} {onCopy && ( onCopy(displayText)} sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.accent.amber } }} > )} {onDelete && ( onDelete(entry.id)} sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.tag.red } }} > )} {/* Phase 12.2: 회의록 요약 확장 뷰 */} {summaryExpanded && ( {summaryLoading ? ( {t('meetingSummary.generating')} ) : summary ? ( {summary.summary && ( {t('meetingSummary.summary').toUpperCase()} {summary.summary} )} {summary.decisions.length > 0 && ( {t('meetingSummary.decisions').toUpperCase()} {summary.decisions.map((d, i) => ( • {d} ))} )} {summary.actionItems.length > 0 && ( {t('meetingSummary.actionItems').toUpperCase()} {summary.actionItems.map((a, i) => ( ☐ {a} ))} )} ) : ( {t('meetingSummary.noSummary')} {(entry.mode === 'caption' || entry.mode === 'file-transcription') && ( )} )} )} {/* 요약 토글 버튼 (caption/file-transcription 모드만) */} {(entry.mode === 'caption' || entry.mode === 'file-transcription') && !summaryExpanded && !hasSummary && ( )} ) }