This commit is contained in:
윤찬 2026-04-13 14:53:17 +09:00
parent 9f7ea1d729
commit feb82abbf7
14 changed files with 117 additions and 41 deletions

View file

@ -0,0 +1 @@
{"sessionId":"2ac1c12e-84cc-457a-867a-5483881cd12c","pid":79317,"acquiredAt":1775918535843}

View file

@ -1,8 +1,8 @@
// src/renderer/components/meeting/MeetingChatPanel.tsx // src/renderer/components/meeting/MeetingChatPanel.tsx
// Phase 15: 회의 상세 페이지 하단 AI 채팅 패널 // Phase 15: 회의 상세 페이지 하단 AI 채팅 패널
import { useState, useCallback, useEffect, useRef } from 'react' import { useState, useCallback, useEffect, useRef, useMemo } from 'react'
import { Box, TextField, IconButton, LinearProgress, Tooltip } from '@mui/material' import { Box, TextField, IconButton, Tooltip } from '@mui/material'
import SendIcon from '@mui/icons-material/Send' import SendIcon from '@mui/icons-material/Send'
import DeleteSweepIcon from '@mui/icons-material/DeleteSweep' import DeleteSweepIcon from '@mui/icons-material/DeleteSweep'
import ExpandLessIcon from '@mui/icons-material/ExpandLess' import ExpandLessIcon from '@mui/icons-material/ExpandLess'
@ -21,16 +21,25 @@ interface MeetingChatPanelProps {
const COLLAPSED_HEIGHT = 40 const COLLAPSED_HEIGHT = 40
const EXPANDED_HEIGHT = 240 const EXPANDED_HEIGHT = 240
/** LLM <think>…</think> 리즈닝 블록 제거 (스트리밍 중 열린 태그 포함) */
function stripThinkTags(text: string): string {
// 완성된 <think>…</think> 블록 제거
let result = text.replace(/<think>[\s\S]*?<\/think>/gi, '')
// 스트리밍 중 아직 닫히지 않은 <think>… 제거
result = result.replace(/<think>[\s\S]*$/gi, '')
return result.trim()
}
// 타이핑 인디케이터 점 3개 애니메이션 // 타이핑 인디케이터 점 3개 애니메이션
function TypingIndicator(): React.ReactElement { function TypingIndicator(): React.ReactElement {
return ( return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: '3px', px: 1, py: 0.5 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: '4px', px: 0.5, py: 0.25 }}>
{[0, 1, 2].map((i) => ( {[0, 1, 2].map((i) => (
<Box <Box
key={i} key={i}
sx={{ sx={{
width: 5, width: 4,
height: 5, height: 4,
borderRadius: '50%', borderRadius: '50%',
bgcolor: d3roPalette.accent.amber, bgcolor: d3roPalette.accent.amber,
animation: 'typing-dot 1.2s infinite', animation: 'typing-dot 1.2s infinite',
@ -131,6 +140,14 @@ export function MeetingChatPanel({ sessionId }: MeetingChatPanelProps): React.Re
setCollapsed((prev) => !prev) setCollapsed((prev) => !prev)
}, []) }, [])
// 스트리밍 콘텐츠에서 think 태그 제거한 표시용 텍스트
const visibleStreamingContent = useMemo(
() => stripThinkTags(streamingContent),
[streamingContent],
)
// think 블록 안에 있는 동안(표시할 텍스트 없음) → 리즈닝 중 표시
const isReasoning = streaming && streamingContent.length > 0 && visibleStreamingContent.length === 0
return ( return (
<Box <Box
sx={{ sx={{
@ -201,7 +218,12 @@ export function MeetingChatPanel({ sessionId }: MeetingChatPanelProps): React.Re
minHeight: 0, minHeight: 0,
}} }}
> >
{messages.map((msg) => ( {messages.map((msg) => {
const displayContent = msg.role === 'assistant'
? stripThinkTags(msg.content)
: msg.content
if (!displayContent) return null
return (
<Box <Box
key={`${msg.role}-${msg.timestamp}`} key={`${msg.role}-${msg.timestamp}`}
sx={{ sx={{
@ -230,12 +252,14 @@ export function MeetingChatPanel({ sessionId }: MeetingChatPanelProps): React.Re
}`, }`,
wordBreak: 'break-word', wordBreak: 'break-word',
whiteSpace: 'pre-wrap', whiteSpace: 'pre-wrap',
overflow: 'hidden',
}} }}
> >
{msg.content} {displayContent}
</Box> </Box>
</Box> </Box>
))} )
})}
{/* 에러 메시지 */} {/* 에러 메시지 */}
{error && ( {error && (
@ -280,24 +304,63 @@ export function MeetingChatPanel({ sessionId }: MeetingChatPanelProps): React.Re
border: `1px solid ${d3roPalette.border.subtle}`, border: `1px solid ${d3roPalette.border.subtle}`,
wordBreak: 'break-word', wordBreak: 'break-word',
whiteSpace: 'pre-wrap', whiteSpace: 'pre-wrap',
overflow: 'hidden',
}} }}
> >
{streamingContent || <TypingIndicator />} {visibleStreamingContent || (
<Box sx={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<TypingIndicator />
{isReasoning && (
<PhosphorText
variant="dim"
sx={{
fontSize: d3roTypo.micro.size,
opacity: 0.6,
animation: 'reasoning-pulse 2s ease-in-out infinite',
'@keyframes reasoning-pulse': {
'0%, 100%': { opacity: 0.4 },
'50%': { opacity: 0.8 },
},
}}
>
{t('meeting.chatReasoning')}
</PhosphorText>
)}
</Box>
)}
</Box> </Box>
</Box> </Box>
)} )}
</Box> </Box>
{/* 스트리밍 진행 표시 */} {/* 스트리밍 진행 표시 — 얇은 accent 라인 */}
{streaming && ( {streaming && (
<LinearProgress <Box
sx={{ sx={{
height: 1, height: 2,
flexShrink: 0, flexShrink: 0,
bgcolor: d3roPalette.bg.inset, bgcolor: d3roPalette.bg.inset,
'& .MuiLinearProgress-bar': { bgcolor: d3roPalette.accent.amber }, overflow: 'hidden',
position: 'relative',
}}
>
<Box
sx={{
position: 'absolute',
top: 0,
left: 0,
height: '100%',
width: '40%',
bgcolor: d3roPalette.accent.amber,
borderRadius: 1,
animation: 'chat-progress 1.5s ease-in-out infinite',
'@keyframes chat-progress': {
'0%': { left: '-40%' },
'100%': { left: '100%' },
},
}} }}
/> />
</Box>
)} )}
{/* 입력 영역 */} {/* 입력 영역 */}

View file

@ -285,6 +285,7 @@
"meeting.copied": "Kopiert", "meeting.copied": "Kopiert",
"meeting.chatCollapse": "Chat einklappen", "meeting.chatCollapse": "Chat einklappen",
"meeting.chatExpand": "Chat ausklappen", "meeting.chatExpand": "Chat ausklappen",
"meeting.chatReasoning": "Denkt nach…",
"meeting.exportMd": "Markdown (.md)", "meeting.exportMd": "Markdown (.md)",
"meeting.exportPdfFmt": "PDF (.pdf)", "meeting.exportPdfFmt": "PDF (.pdf)",
"meeting.polishFailed": "KI-Polierung fehlgeschlagen", "meeting.polishFailed": "KI-Polierung fehlgeschlagen",

View file

@ -510,6 +510,7 @@
"meeting.copied": "Copied", "meeting.copied": "Copied",
"meeting.chatCollapse": "Collapse Chat", "meeting.chatCollapse": "Collapse Chat",
"meeting.chatExpand": "Expand Chat", "meeting.chatExpand": "Expand Chat",
"meeting.chatReasoning": "Reasoning…",
"meeting.exportMd": "Markdown (.md)", "meeting.exportMd": "Markdown (.md)",
"meeting.exportPdfFmt": "PDF (.pdf)", "meeting.exportPdfFmt": "PDF (.pdf)",
"meeting.polishFailed": "AI polishing failed", "meeting.polishFailed": "AI polishing failed",

View file

@ -285,6 +285,7 @@
"meeting.copied": "Copiado", "meeting.copied": "Copiado",
"meeting.chatCollapse": "Contraer chat", "meeting.chatCollapse": "Contraer chat",
"meeting.chatExpand": "Expandir chat", "meeting.chatExpand": "Expandir chat",
"meeting.chatReasoning": "Razonando…",
"meeting.exportMd": "Markdown (.md)", "meeting.exportMd": "Markdown (.md)",
"meeting.exportPdfFmt": "PDF (.pdf)", "meeting.exportPdfFmt": "PDF (.pdf)",
"meeting.polishFailed": "Error al pulir con IA", "meeting.polishFailed": "Error al pulir con IA",

View file

@ -285,6 +285,7 @@
"meeting.copied": "Copié", "meeting.copied": "Copié",
"meeting.chatCollapse": "Réduire le chat", "meeting.chatCollapse": "Réduire le chat",
"meeting.chatExpand": "Agrandir le chat", "meeting.chatExpand": "Agrandir le chat",
"meeting.chatReasoning": "Raisonnement…",
"meeting.exportMd": "Markdown (.md)", "meeting.exportMd": "Markdown (.md)",
"meeting.exportPdfFmt": "PDF (.pdf)", "meeting.exportPdfFmt": "PDF (.pdf)",
"meeting.polishFailed": "Echec du polish IA", "meeting.polishFailed": "Echec du polish IA",

View file

@ -285,6 +285,7 @@
"meeting.copied": "コピーしました", "meeting.copied": "コピーしました",
"meeting.chatCollapse": "チャットを折り畳む", "meeting.chatCollapse": "チャットを折り畳む",
"meeting.chatExpand": "チャットを展開", "meeting.chatExpand": "チャットを展開",
"meeting.chatReasoning": "推論中…",
"meeting.exportMd": "Markdown (.md)", "meeting.exportMd": "Markdown (.md)",
"meeting.exportPdfFmt": "PDF (.pdf)", "meeting.exportPdfFmt": "PDF (.pdf)",
"meeting.polishFailed": "AI磨き上げに失敗しました", "meeting.polishFailed": "AI磨き上げに失敗しました",

View file

@ -510,6 +510,7 @@
"meeting.copied": "복사됨", "meeting.copied": "복사됨",
"meeting.chatCollapse": "채팅 접기", "meeting.chatCollapse": "채팅 접기",
"meeting.chatExpand": "채팅 펼치기", "meeting.chatExpand": "채팅 펼치기",
"meeting.chatReasoning": "추론 중…",
"meeting.exportMd": "Markdown (.md)", "meeting.exportMd": "Markdown (.md)",
"meeting.exportPdfFmt": "PDF (.pdf)", "meeting.exportPdfFmt": "PDF (.pdf)",
"meeting.polishFailed": "AI 다듬기에 실패했습니다", "meeting.polishFailed": "AI 다듬기에 실패했습니다",

View file

@ -285,6 +285,7 @@
"meeting.copied": "Copiado", "meeting.copied": "Copiado",
"meeting.chatCollapse": "Recolher chat", "meeting.chatCollapse": "Recolher chat",
"meeting.chatExpand": "Expandir chat", "meeting.chatExpand": "Expandir chat",
"meeting.chatReasoning": "Raciocinando…",
"meeting.exportMd": "Markdown (.md)", "meeting.exportMd": "Markdown (.md)",
"meeting.exportPdfFmt": "PDF (.pdf)", "meeting.exportPdfFmt": "PDF (.pdf)",
"meeting.polishFailed": "Falha no polimento de IA", "meeting.polishFailed": "Falha no polimento de IA",

View file

@ -285,6 +285,7 @@
"meeting.copied": "Скопировано", "meeting.copied": "Скопировано",
"meeting.chatCollapse": "Свернуть чат", "meeting.chatCollapse": "Свернуть чат",
"meeting.chatExpand": "Развернуть чат", "meeting.chatExpand": "Развернуть чат",
"meeting.chatReasoning": "Размышляет…",
"meeting.exportMd": "Markdown (.md)", "meeting.exportMd": "Markdown (.md)",
"meeting.exportPdfFmt": "PDF (.pdf)", "meeting.exportPdfFmt": "PDF (.pdf)",
"meeting.polishFailed": "Ошибка ИИ-полировки", "meeting.polishFailed": "Ошибка ИИ-полировки",

View file

@ -285,6 +285,7 @@
"meeting.copied": "คัดลอกแล้ว", "meeting.copied": "คัดลอกแล้ว",
"meeting.chatCollapse": "ยุบแชท", "meeting.chatCollapse": "ยุบแชท",
"meeting.chatExpand": "ขยายแชท", "meeting.chatExpand": "ขยายแชท",
"meeting.chatReasoning": "กำลังคิด…",
"meeting.exportMd": "Markdown (.md)", "meeting.exportMd": "Markdown (.md)",
"meeting.exportPdfFmt": "PDF (.pdf)", "meeting.exportPdfFmt": "PDF (.pdf)",
"meeting.polishFailed": "AI ปรับแต่งล้มเหลว", "meeting.polishFailed": "AI ปรับแต่งล้มเหลว",

View file

@ -285,6 +285,7 @@
"meeting.copied": "Đã sao chép", "meeting.copied": "Đã sao chép",
"meeting.chatCollapse": "Thu gọn chat", "meeting.chatCollapse": "Thu gọn chat",
"meeting.chatExpand": "Mở rộng chat", "meeting.chatExpand": "Mở rộng chat",
"meeting.chatReasoning": "Đang suy luận…",
"meeting.exportMd": "Markdown (.md)", "meeting.exportMd": "Markdown (.md)",
"meeting.exportPdfFmt": "PDF (.pdf)", "meeting.exportPdfFmt": "PDF (.pdf)",
"meeting.polishFailed": "AI đánh bóng thất bại", "meeting.polishFailed": "AI đánh bóng thất bại",

View file

@ -285,6 +285,7 @@
"meeting.copied": "已複製", "meeting.copied": "已複製",
"meeting.chatCollapse": "收起聊天", "meeting.chatCollapse": "收起聊天",
"meeting.chatExpand": "展開聊天", "meeting.chatExpand": "展開聊天",
"meeting.chatReasoning": "推理中…",
"meeting.exportMd": "Markdown (.md)", "meeting.exportMd": "Markdown (.md)",
"meeting.exportPdfFmt": "PDF (.pdf)", "meeting.exportPdfFmt": "PDF (.pdf)",
"meeting.polishFailed": "AI潤色失敗", "meeting.polishFailed": "AI潤色失敗",

View file

@ -285,6 +285,7 @@
"meeting.copied": "已复制", "meeting.copied": "已复制",
"meeting.chatCollapse": "收起聊天", "meeting.chatCollapse": "收起聊天",
"meeting.chatExpand": "展开聊天", "meeting.chatExpand": "展开聊天",
"meeting.chatReasoning": "推理中…",
"meeting.exportMd": "Markdown (.md)", "meeting.exportMd": "Markdown (.md)",
"meeting.exportPdfFmt": "PDF (.pdf)", "meeting.exportPdfFmt": "PDF (.pdf)",
"meeting.polishFailed": "AI润色失败", "meeting.polishFailed": "AI润色失败",