refactor(desktop): session-scoped voice runtime, STT/LLM ports, caption ownership and meeting export fixes

This commit is contained in:
Yun Chan 2026-09-28 00:53:43 +09:00
parent 0adedf5e7b
commit 83cf9133ac
39 changed files with 4787 additions and 1061 deletions

View file

@ -27,6 +27,17 @@ function stripThinkTags(text: string): string {
return result.trim()
}
/**
* main 이 실어 보낸 sessionId 가 이 패널의 회의인지 확인한다.
* sessionId 가 없는(구버전) 이벤트는 그대로 받아들인다.
*/
function isForSession(payload: object, sessionId: string): boolean {
if ('sessionId' in payload && typeof payload.sessionId === 'string') {
return payload.sessionId === sessionId
}
return true
}
// 타이핑 인디케이터 점 3개 애니메이션
function TypingIndicator(): React.ReactElement {
return (
@ -74,21 +85,38 @@ export function MeetingChatPanel({ sessionId }: MeetingChatPanelProps): React.Re
scrollToBottom()
}, [messages, streamingContent, scrollToBottom])
// 이벤트 구독
// 회의가 바뀌면(같은 패널 인스턴스 재사용 포함) 이전 회의의 대화 표시와 진행 중 답변을 버린다.
// main 은 회의마다 히스토리를 따로 보관하므로 여기서 지우지 않는다.
useEffect(() => {
setMessages([])
setStreaming(false)
setStreamingContent('')
setError(null)
isSendingChatRef.current = false
return () => {
// 떠나는 회의의 답변 스트림을 끊는다 — 다음 회의 패널에 섞이지 않게
void window.electronAPI.meetingChat.cancel()
}
}, [sessionId])
// 이벤트 구독 — 다른 회의로 향한 이벤트는 무시한다
useEffect(() => {
const unsubDelta = window.electronAPI.meetingChat.onDelta((data) => {
if (!isForSession(data, sessionId)) return
setStreaming(true)
setStreamingContent((prev) => prev + data.token)
})
const unsubMessage = window.electronAPI.meetingChat.onMessage((msg) => {
setMessages((prev) => [...prev, msg])
if (!isForSession(msg, sessionId)) return
setMessages((prev) => [...prev, { role: msg.role, content: msg.content, timestamp: msg.timestamp }])
setStreaming(false)
setStreamingContent('')
isSendingChatRef.current = false
})
const unsubError = window.electronAPI.meetingChat.onError((err) => {
if (!isForSession(err, sessionId)) return
setStreaming(false)
setStreamingContent('')
setError(err.message)
@ -139,7 +167,7 @@ export function MeetingChatPanel({ sessionId }: MeetingChatPanelProps): React.Re
setMessages([])
setStreamingContent('')
setStreaming(false)
window.electronAPI.meetingChat.clear({ sessionId })
window.electronAPI.meetingChat.clear()
}, [sessionId])
const handleToggleCollapse = useCallback(() => {