Phase 15 구현: Auto Polish + AI 채팅 + 마인드맵 + 공유

- Auto Polish: LLM으로 전사 다듬기 (필러 제거, 문장 교정)
- AI 채팅: 회의 전사 기반 Q&A (LocalLLMService.chatStream 활용, 스트리밍)
- 마인드맵: 빌트인 템플릿 추가 (마크다운 계층 트리)
- 공유: 클립보드 복사 메뉴 추가
- MeetingChatPanel UI (접기/펼치기, 스트리밍 인디케이터)
- TranscriptTab에 AI 다듬기 버튼 추가
- 12개 locale i18n 키 추가
This commit is contained in:
Yun Chan 2026-04-08 10:18:42 +09:00
parent d4928ffa60
commit 2b17bf47b7
26 changed files with 980 additions and 26 deletions

View file

@ -65,6 +65,10 @@ class MeetingModeService extends EventEmitter {
/** CaptionService session-saved 방지 플래그 */
private _meetingModeActive = false
// Phase 15: Meeting AI Chat
private _chatHistory: Array<{ role: 'user' | 'assistant'; content: string }> = []
private _chatAbortController: AbortController | null = null
// ── 공개 접근자 ──
getState(): MeetingModeState {
@ -823,6 +827,99 @@ class MeetingModeService extends EventEmitter {
}
}
// ── Phase 15: Auto Polish ──
async polishTranscript(sessionId: string): Promise<string> {
const db = getDatabase()
const row = db.select().from(meetingSessions).where(eq(meetingSessions.id, sessionId)).get()
if (!row) throw new D3ROError(ErrorCode.MeetingSessionNotFound, `세션 없음: ${sessionId}`)
const transcript = row.rawTranscript
if (!transcript) throw new D3ROError(ErrorCode.MeetingPolishFailed, '전사 텍스트가 없습니다')
const { getLocalLLMService } = await import('./LocalLLMService')
const llm = getLocalLLMService()
const result = await llm.generate(transcript, {
systemPrompt: '다음 음성 전사 텍스트를 다듬어주세요. 필러 단어(음, 어, 그, 아 등)를 제거하고, 문장 구조를 자연스럽게 교정하되, 원래 의미와 내용은 절대 변경하지 마세요. 타임스탬프 형식 [MM:SS]은 그대로 유지하세요.',
temperature: 0.3,
})
db.update(meetingSessions).set({
editedTranscript: result.text,
updatedAt: Date.now(),
}).where(eq(meetingSessions.id, sessionId)).run()
logger.info(`Auto Polish 완료: sessionId=${sessionId}`)
return result.text
}
// ── Phase 15: Meeting AI Chat ──
async chatWithMeeting(sessionId: string, userMessage: string): Promise<void> {
const db = getDatabase()
const row = db.select().from(meetingSessions).where(eq(meetingSessions.id, sessionId)).get()
if (!row) throw new D3ROError(ErrorCode.MeetingSessionNotFound, `세션 없음: ${sessionId}`)
const transcript = row.editedTranscript ?? row.rawTranscript ?? ''
const systemPrompt = `당신은 회의 내용을 분석하는 AI 어시스턴트입니다.
.
"전사 내용에서 확인되지 않습니다" .
##
${transcript}`
this._chatHistory.push({ role: 'user', content: userMessage })
const { getLocalLLMService } = await import('./LocalLLMService')
const llm = getLocalLLMService()
const messages = [
{ role: 'system' as const, content: systemPrompt },
...this._chatHistory,
]
let fullResponse = ''
this._chatAbortController = new AbortController()
const signal = this._chatAbortController.signal
try {
const generator = llm.chatStream(messages, { temperature: 0.5 })
for await (const token of generator) {
if (signal.aborted) break
fullResponse += token
this._sendToRenderer(IPC_CHANNELS.MEETING_CHAT.DELTA, { token })
}
this._chatHistory.push({ role: 'assistant', content: fullResponse })
this._sendToRenderer(IPC_CHANNELS.MEETING_CHAT.MESSAGE, {
role: 'assistant',
content: fullResponse,
timestamp: Date.now(),
})
} catch (err) {
if ((err as Error).name !== 'AbortError') {
this._sendToRenderer(IPC_CHANNELS.MEETING_CHAT.ERROR, {
message: err instanceof Error ? err.message : String(err),
})
}
} finally {
this._chatAbortController = null
}
}
cancelMeetingChat(): void {
if (this._chatAbortController) {
this._chatAbortController.abort()
this._chatAbortController = null
}
}
clearMeetingChatHistory(): void {
this._chatHistory = []
}
private _buildPdfHtml(session: MeetingSessionDetail): string {
const durationMin = session.durationMs ? Math.round(session.durationMs / 60000) : 0
const minutesHtml = session.minutesMarkdown