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

@ -32,7 +32,7 @@ npm run typecheck # tsc --noEmit
- 녹음 UI: 9개 웨이브 바, cos 분포, 100ms - 녹음 UI: 9개 웨이브 바, cos 분포, 100ms
## 현재 상태 ## 현재 상태
Phase 1~14 전체 완료 + 랜딩 페이지(site/). Phase 1~14.5 전체 완료 + 랜딩 페이지(site/).
차단 이슈: @nut-tree-fork/nut-js 포크 사용 차단 이슈: @nut-tree-fork/nut-js 포크 사용
## 설계 문서 (구현 시 Read 도구로 참조) ## 설계 문서 (구현 시 Read 도구로 참조)
@ -50,6 +50,7 @@ Phase10+: MemoService, VoiceCommandService, ScreenContextService, ChainService,
Phase12+: FileTranscriptionService, MeetingSummaryService, DictationTemplateService Phase12+: FileTranscriptionService, MeetingSummaryService, DictationTemplateService
Phase13+: VoiceConversationService, RAGService, VoiceActionService Phase13+: VoiceConversationService, RAGService, VoiceActionService
Phase14: MeetingModeService Phase14: MeetingModeService
Phase14.5: MeetingDocTemplateService + 다중 문서 생성/편집/내보내기
## DS 컴포넌트 (src/renderer/components/ds/) ## DS 컴포넌트 (src/renderer/components/ds/)
CrtDisplay, InstrumentPanel, Led, PhysicalButton, MetalCard, CrtDisplay, InstrumentPanel, Led, PhysicalButton, MetalCard,

View file

@ -0,0 +1,205 @@
# Phase 15: 회의 모드 Pro — Auto Polish + AI 채팅 + 마인드맵 + 공유
> Phase 14.5 기반 확장: AI 전사 다듬기, 회의록 기반 AI 채팅, 마인드맵 생성, 공유 링크
---
## 1. 개요
### 1.1 기능 4종
| 기능 | 설명 | 난이도 |
|------|------|--------|
| **Auto Polish** | 전사 텍스트 AI 다듬기 (필러 제거, 가독성 향상) | 소 |
| **마인드맵** | 전사 내용에서 AI 마인드맵 생성 (마크다운 트리) | 소 |
| **AI 채팅** | 회의 전사 기반 Q&A 채팅 | 중 |
| **공유** | 마크다운 → 클립보드 복사 + 파일 공유 | 소 |
**화자 구분**: sidecar에 pyannote-audio 추가 필요 → TODO로 유지
---
## 2. Auto Polish
### 2.1 동작 방식
1. Transcript 탭에서 [AI 다듬기] 버튼 클릭
2. rawTranscript를 LLM에 전달 (refine 프롬프트)
3. 결과를 editedTranscript에 저장
4. "원본/AI 다듬기본" 토글 가능
### 2.2 IPC
```
MEETING_MODE.POLISH_TRANSCRIPT: 'meetingMode:polishTranscript'
```
### 2.3 서비스 메서드
```typescript
async polishTranscript(sessionId: string): Promise<string> {
// rawTranscript → LLM refine → editedTranscript 저장 → 반환
}
```
### 2.4 UI
TranscriptTab에 [AI 다듬기] PhysicalButton 추가. 처리 중 LinearProgress.
---
## 3. 마인드맵
### 3.1 동작 방식
MeetingDocTemplateType에 'mindmap' 추가. 빌트인 템플릿으로 등록.
LLM이 마크다운 트리 구조로 마인드맵 생성. DocumentTab에서 렌더링.
### 3.2 프롬프트
```
전사록을 분석하여 마인드맵을 작성하세요.
마크다운 계층 구조를 사용하세요:
# 중심 주제
## 주요 주제 1
- 세부 항목 A
- 세부 항목 B
## 주요 주제 2
- 세부 항목 C
```
### 3.3 DB 변경
`meetingDocuments.templateType` enum에 'mindmap' 추가.
`meetingSessions.templateType` (schema.ts) enum에 'mindmap' 추가.
---
## 4. AI 채팅
### 4.1 동작 방식
1. 상세 페이지 하단에 채팅 바 (고정, 탭과 별개)
2. 사용자가 질문 입력 → 전사+문서를 컨텍스트로 LLM 질문
3. 스트리밍 응답 표시
4. 대화 히스토리는 메모리에만 유지 (DB 저장 안 함)
### 4.2 IPC
```
MEETING_CHAT: {
SEND: 'meetingChat:send',
CANCEL: 'meetingChat:cancel',
CLEAR: 'meetingChat:clear',
// Main → Renderer
DELTA: 'meetingChat:delta',
MESSAGE: 'meetingChat:message',
ERROR: 'meetingChat:error',
}
```
### 4.3 서비스
MeetingModeService에 채팅 메서드 추가 (별도 서비스 불필요):
```typescript
async chatWithMeeting(sessionId: string, userMessage: string): Promise<void>
cancelMeetingChat(): void
clearMeetingChatHistory(): void
```
LocalLLMService.chatStream() 활용. 시스템 프롬프트에 전사 내용 포함.
### 4.4 UI
MeetingDetailTabs 하단에 고정 채팅 바:
```
┌──────────────────────────────────────┐
│ [탭 콘텐츠...] │
├──────────────────────────────────────┤
│ 🤖 AI: 예산은 5000만원으로 확정... │
│ 👤 다음 회의 일정은? │
│ 🤖 AI: 다음 주 수요일... │
│ ┌──────────────────────────┐ [Send] │
│ │ 질문을 입력하세요... │ │
│ └──────────────────────────┘ │
└──────────────────────────────────────┘
```
### 4.5 타입
```typescript
interface MeetingChatMessage {
role: 'user' | 'assistant'
content: string
timestamp: number
}
interface MeetingChatSendParams {
sessionId: string
message: string
}
interface MeetingChatDelta {
token: string
}
```
---
## 5. 공유
### 5.1 동작 방식
1. 클립보드 복사: 문서/전사 마크다운을 클립보드에 복사
2. 파일 공유: 기존 내보내기(MD/PDF/TXT/DOCX)로 파일 저장
3. 향후: 웹 서비스 연동 (TODO)
### 5.2 UI
ExportMenu에 "클립보드 복사" 항목 추가.
---
## 6. 에러 코드
```typescript
MeetingPolishFailed = 893,
MeetingChatFailed = 894,
```
---
## 7. i18n 키
```json
"meeting.polish": "AI 다듬기",
"meeting.polishing": "AI가 전사를 다듬고 있습니다...",
"meeting.polished": "AI 다듬기 완료",
"meeting.mindmapTemplate": "마인드맵",
"meeting.chat": "AI 채팅",
"meeting.chatPlaceholder": "회의 내용에 대해 질문하세요...",
"meeting.chatSend": "전송",
"meeting.chatClear": "대화 초기화",
"meeting.copyToClipboard": "클립보드 복사",
"meeting.copied": "복사됨"
```
---
## 8. 구현 순서
### Step A: 기반 (타입 + IPC + 에러)
### Step B: Auto Polish (서비스 + IPC + UI)
### Step C: 마인드맵 (템플릿 추가 + DB enum)
### Step D: AI 채팅 (서비스 + IPC + UI)
### Step E: 공유 (클립보드 복사)
### Step F: i18n + 검증
---
## 파일 목록
### 신규
| 파일 | 설명 |
|------|------|
| `src/renderer/components/meeting/MeetingChatPanel.tsx` | AI 채팅 UI |
### 수정
| 파일 | 변경 |
|------|------|
| `src/shared/types.ts` | MeetingChatMessage, MeetingChatSendParams, MeetingChatDelta, MeetingDocTemplateType에 mindmap 추가 |
| `src/shared/ipc-channels.ts` | MEETING_MODE.POLISH_TRANSCRIPT + MEETING_CHAT 그룹 |
| `src/shared/errors.ts` | 893-894 |
| `src/main/db/schema.ts` | meetingDocuments templateType enum에 mindmap |
| `src/main/services/MeetingModeService.ts` | polishTranscript, chatWithMeeting, cancelMeetingChat, clearMeetingChatHistory |
| `src/main/services/MeetingDocTemplateService.ts` | 빌트인 마인드맵 템플릿 |
| `src/main/ipc/meeting-mode-handlers.ts` | 4개 핸들러 추가 |
| `src/preload/index.ts` | meetingChat 그룹 + polishTranscript |
| `src/renderer/components/meeting/TranscriptTab.tsx` | AI 다듬기 버튼 |
| `src/renderer/components/meeting/MeetingDetailTabs.tsx` | 하단 채팅 패널 |
| `src/renderer/components/meeting/ExportMenu.tsx` | 클립보드 복사 |
| `src/renderer/i18n/*.json` | 12개 locale |

View file

@ -210,7 +210,7 @@ export const meetingDocuments = sqliteTable(
id: text('id').primaryKey(), id: text('id').primaryKey(),
sessionId: text('session_id').notNull(), sessionId: text('session_id').notNull(),
templateType: text('template_type', { templateType: text('template_type', {
enum: ['minutes', 'report', 'idea-note', 'custom'], enum: ['minutes', 'report', 'idea-note', 'custom', 'mindmap'],
}).notNull(), }).notNull(),
title: text('title').notNull(), title: text('title').notNull(),
content: text('content').notNull().default(''), content: text('content').notNull().default(''),

View file

@ -14,6 +14,7 @@ import type {
MeetingExportParams, MeetingExportParams,
MeetingGenerateDocParams, MeetingGenerateDocParams,
MeetingExportFormat, MeetingExportFormat,
MeetingChatSendParams,
} from '@shared/types' } from '@shared/types'
const logger = getLogger('meeting-mode-handlers') const logger = getLogger('meeting-mode-handlers')
@ -198,5 +199,51 @@ export function registerMeetingModeHandlers(): void {
} }
}) })
// ── Phase 15 핸들러 ──
ipcMain.handle(ch.POLISH_TRANSCRIPT, async (_event, params: { sessionId: string }) => {
try {
const result = await getMeetingModeService().polishTranscript(params.sessionId)
return ipcSuccess(result)
} catch (err) {
logger.error(`polishTranscript 실패: ${err instanceof Error ? err.message : String(err)}`)
return ipcError(
ErrorCode.MeetingPolishFailed,
err instanceof Error ? err.message : String(err),
)
}
})
ipcMain.handle(IPC_CHANNELS.MEETING_CHAT.SEND, async (_event, params: MeetingChatSendParams) => {
try {
await getMeetingModeService().chatWithMeeting(params.sessionId, params.message)
return ipcSuccess(undefined)
} catch (err) {
logger.error(`meetingChat:send 실패: ${err instanceof Error ? err.message : String(err)}`)
return ipcError(
ErrorCode.MeetingChatFailed,
err instanceof Error ? err.message : String(err),
)
}
})
ipcMain.handle(IPC_CHANNELS.MEETING_CHAT.CANCEL, () => {
try {
getMeetingModeService().cancelMeetingChat()
return ipcSuccess(undefined)
} catch (err) {
return ipcError(ErrorCode.UnknownError, err instanceof Error ? err.message : String(err))
}
})
ipcMain.handle(IPC_CHANNELS.MEETING_CHAT.CLEAR, () => {
try {
getMeetingModeService().clearMeetingChatHistory()
return ipcSuccess(undefined)
} catch (err) {
return ipcError(ErrorCode.UnknownError, err instanceof Error ? err.message : String(err))
}
})
logger.info('Meeting Mode IPC handlers registered') logger.info('Meeting Mode IPC handlers registered')
} }

View file

@ -118,6 +118,32 @@ const BUILTIN_TEMPLATES: MeetingDocTemplate[] = [
createdAt: Date.now(), createdAt: Date.now(),
updatedAt: Date.now(), updatedAt: Date.now(),
}, },
{
id: 'builtin-mindmap',
name: '마인드맵',
description: '전사 내용의 핵심 구조를 시각적 마인드맵으로 정리',
templateType: 'mindmap' as MeetingDocTemplateType,
systemPrompt: `전사록을 분석하여 마인드맵을 마크다운 계층 구조로 작성하세요.
# ( )
## 1
- A
- B
-
## 2
- C
##
-
:
- 4
- ( )
-
- `,
isBuiltin: true,
createdAt: Date.now(),
updatedAt: Date.now(),
},
] ]
interface MeetingDocTemplateStoreSchema { interface MeetingDocTemplateStoreSchema {

View file

@ -65,6 +65,10 @@ class MeetingModeService extends EventEmitter {
/** CaptionService session-saved 방지 플래그 */ /** CaptionService session-saved 방지 플래그 */
private _meetingModeActive = false private _meetingModeActive = false
// Phase 15: Meeting AI Chat
private _chatHistory: Array<{ role: 'user' | 'assistant'; content: string }> = []
private _chatAbortController: AbortController | null = null
// ── 공개 접근자 ── // ── 공개 접근자 ──
getState(): MeetingModeState { 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 { private _buildPdfHtml(session: MeetingSessionDetail): string {
const durationMin = session.durationMs ? Math.round(session.durationMs / 60000) : 0 const durationMin = session.durationMs ? Math.round(session.durationMs / 60000) : 0
const minutesHtml = session.minutesMarkdown const minutesHtml = session.minutesMarkdown

View file

@ -162,6 +162,9 @@ import type {
CreateMeetingDocTemplateParams, CreateMeetingDocTemplateParams,
UpdateMeetingDocTemplateParams, UpdateMeetingDocTemplateParams,
DeleteMeetingDocTemplateParams, DeleteMeetingDocTemplateParams,
MeetingChatMessage,
MeetingChatSendParams,
MeetingChatDelta,
} from '@shared/types' } from '@shared/types'
import { Feature } from '@shared/types' import { Feature } from '@shared/types'
import type { IPCResult } from '@shared/errors' import type { IPCResult } from '@shared/errors'
@ -635,6 +638,25 @@ const electronAPI = {
invoke<void>(IPC_CHANNELS.MEETING_MODE.EDIT_SEGMENT, params), invoke<void>(IPC_CHANNELS.MEETING_MODE.EDIT_SEGMENT, params),
onDocGeneratingProgress: (cb: (e: MeetingDocGeneratingProgress) => void): Unsubscribe => onDocGeneratingProgress: (cb: (e: MeetingDocGeneratingProgress) => void): Unsubscribe =>
on(IPC_CHANNELS.MEETING_MODE.DOC_GENERATING_PROGRESS, cb), on(IPC_CHANNELS.MEETING_MODE.DOC_GENERATING_PROGRESS, cb),
// Phase 15
polishTranscript: (params: { sessionId: string }) =>
invoke<string>(IPC_CHANNELS.MEETING_MODE.POLISH_TRANSCRIPT, params),
},
// ── Meeting Chat (Phase 15) ───────────────────────────
meetingChat: {
send: (params: MeetingChatSendParams) =>
invoke<void>(IPC_CHANNELS.MEETING_CHAT.SEND, params),
cancel: () =>
invoke<void>(IPC_CHANNELS.MEETING_CHAT.CANCEL),
clear: () =>
invoke<void>(IPC_CHANNELS.MEETING_CHAT.CLEAR),
onDelta: (cb: (e: MeetingChatDelta) => void): Unsubscribe =>
on(IPC_CHANNELS.MEETING_CHAT.DELTA, cb),
onMessage: (cb: (e: MeetingChatMessage) => void): Unsubscribe =>
on(IPC_CHANNELS.MEETING_CHAT.MESSAGE, cb),
onError: (cb: (e: { message: string }) => void): Unsubscribe =>
on(IPC_CHANNELS.MEETING_CHAT.ERROR, cb),
}, },
// ── Meeting Doc Templates (Phase 14.5) ─────────────── // ── Meeting Doc Templates (Phase 14.5) ───────────────

View file

@ -11,6 +11,7 @@ import type { MeetingExportFormat } from '@shared/types'
interface ExportMenuProps { interface ExportMenuProps {
onExport: (format: MeetingExportFormat) => void onExport: (format: MeetingExportFormat) => void
onCopyToClipboard?: () => void
} }
interface FormatItem { interface FormatItem {
@ -18,7 +19,7 @@ interface FormatItem {
label: string label: string
} }
export function ExportMenu({ onExport }: ExportMenuProps): React.ReactElement { export function ExportMenu({ onExport, onCopyToClipboard }: ExportMenuProps): React.ReactElement {
const { t } = useI18n() const { t } = useI18n()
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null) const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null)
const open = Boolean(anchorEl) const open = Boolean(anchorEl)
@ -90,6 +91,22 @@ export function ExportMenu({ onExport }: ExportMenuProps): React.ReactElement {
{label} {label}
</MenuItem> </MenuItem>
))} ))}
{onCopyToClipboard && (
<MenuItem
onClick={() => { onCopyToClipboard(); handleClose() }}
sx={{
fontFamily: d3roFontMono,
fontSize: d3roTypo.compact.size,
color: d3roPalette.text.primary,
'&:hover': {
bgcolor: d3roPalette.bg.cardHover,
color: d3roPalette.accent.amber,
},
}}
>
{t('meeting.copyToClipboard')}
</MenuItem>
)}
</Menu> </Menu>
</> </>
) )

View file

@ -0,0 +1,325 @@
// src/renderer/components/meeting/MeetingChatPanel.tsx
// Phase 15: 회의 상세 페이지 하단 AI 채팅 패널
import { useState, useCallback, useEffect, useRef } from 'react'
import { Box, TextField, IconButton, LinearProgress, Tooltip } from '@mui/material'
import SendIcon from '@mui/icons-material/Send'
import DeleteSweepIcon from '@mui/icons-material/DeleteSweep'
import ExpandLessIcon from '@mui/icons-material/ExpandLess'
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
import { PhosphorText } from '../ds/PhosphorText'
import { PhysicalButton } from '../ds/PhysicalButton'
import { d3roPalette, d3roFontMono, d3roTypo, d3roShadow } from '../../theme'
import { useI18n } from '../../i18n'
import type { MeetingChatMessage } from '@shared/types'
interface MeetingChatPanelProps {
sessionId: string
}
const COLLAPSED_HEIGHT = 40
const EXPANDED_HEIGHT = 240
// 타이핑 인디케이터 점 3개 애니메이션
function TypingIndicator(): React.ReactElement {
return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: '3px', px: 1, py: 0.5 }}>
{[0, 1, 2].map((i) => (
<Box
key={i}
sx={{
width: 5,
height: 5,
borderRadius: '50%',
bgcolor: d3roPalette.accent.amber,
animation: 'typing-dot 1.2s infinite',
animationDelay: `${i * 0.2}s`,
'@keyframes typing-dot': {
'0%, 80%, 100%': { opacity: 0.3, transform: 'scale(0.8)' },
'40%': { opacity: 1, transform: 'scale(1)' },
},
}}
/>
))}
</Box>
)
}
export function MeetingChatPanel({ sessionId }: MeetingChatPanelProps): React.ReactElement {
const { t } = useI18n()
const [messages, setMessages] = useState<MeetingChatMessage[]>([])
const [inputValue, setInputValue] = useState('')
const [streaming, setStreaming] = useState(false)
const [streamingContent, setStreamingContent] = useState('')
const [collapsed, setCollapsed] = useState(false)
const scrollRef = useRef<HTMLDivElement>(null)
const inputRef = useRef<HTMLInputElement>(null)
// 스크롤 하단 유지
const scrollToBottom = useCallback(() => {
if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight
}
}, [])
useEffect(() => {
scrollToBottom()
}, [messages, streamingContent, scrollToBottom])
// 이벤트 구독
useEffect(() => {
const unsubDelta = window.electronAPI.meetingChat.onDelta((data) => {
setStreaming(true)
setStreamingContent((prev) => prev + data.token)
})
const unsubMessage = window.electronAPI.meetingChat.onMessage((msg) => {
setMessages((prev) => [...prev, msg])
setStreaming(false)
setStreamingContent('')
})
const unsubError = window.electronAPI.meetingChat.onError((_err) => {
setStreaming(false)
setStreamingContent('')
})
return () => {
unsubDelta()
unsubMessage()
unsubError()
}
}, [])
const handleSend = useCallback(() => {
const text = inputValue.trim()
if (!text || streaming) return
const userMsg: MeetingChatMessage = {
role: 'user',
content: text,
timestamp: Date.now(),
}
setMessages((prev) => [...prev, userMsg])
setInputValue('')
window.electronAPI.meetingChat.send({ sessionId, message: text })
inputRef.current?.focus()
}, [inputValue, streaming, sessionId])
const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSend()
}
},
[handleSend],
)
const handleClear = useCallback(() => {
setMessages([])
setStreamingContent('')
setStreaming(false)
window.electronAPI.meetingChat.clear({ sessionId })
}, [sessionId])
const handleToggleCollapse = useCallback(() => {
setCollapsed((prev) => !prev)
}, [])
return (
<Box
sx={{
flexShrink: 0,
borderTop: `1px solid ${d3roPalette.border.default}`,
bgcolor: d3roPalette.bg.card,
display: 'flex',
flexDirection: 'column',
height: collapsed ? COLLAPSED_HEIGHT : EXPANDED_HEIGHT,
transition: 'height 0.2s ease',
overflow: 'hidden',
boxShadow: d3roShadow.inset,
}}
>
{/* 헤더 */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
px: 1.5,
height: COLLAPSED_HEIGHT,
flexShrink: 0,
borderBottom: collapsed ? 'none' : `1px solid ${d3roPalette.border.subtle}`,
gap: 1,
}}
>
<PhosphorText
variant="dim"
sx={{ fontSize: d3roTypo.meta.size, letterSpacing: d3roTypo.meta.spacing, flex: 1 }}
>
{t('meeting.chat')}
</PhosphorText>
<Tooltip title={t('meeting.chatClear')}>
<IconButton
size="small"
onClick={handleClear}
disabled={messages.length === 0 && !streaming}
>
<DeleteSweepIcon sx={{ fontSize: 15, color: d3roPalette.text.inactive }} />
</IconButton>
</Tooltip>
<Tooltip title={collapsed ? t('meeting.chatExpand') : t('meeting.chatCollapse')}>
<IconButton size="small" onClick={handleToggleCollapse}>
{collapsed ? (
<ExpandLessIcon sx={{ fontSize: 15, color: d3roPalette.text.inactive }} />
) : (
<ExpandMoreIcon sx={{ fontSize: 15, color: d3roPalette.text.inactive }} />
)}
</IconButton>
</Tooltip>
</Box>
{!collapsed && (
<>
{/* 메시지 히스토리 */}
<Box
ref={scrollRef}
sx={{
flex: 1,
overflowY: 'auto',
px: 1.5,
py: 1,
display: 'flex',
flexDirection: 'column',
gap: 0.75,
minHeight: 0,
}}
>
{messages.map((msg, idx) => (
<Box
key={idx}
sx={{
display: 'flex',
justifyContent: msg.role === 'user' ? 'flex-end' : 'flex-start',
}}
>
<Box
sx={{
maxWidth: '80%',
px: 1.25,
py: 0.5,
borderRadius: '6px',
fontFamily: d3roFontMono,
fontSize: d3roTypo.compact.size,
lineHeight: 1.5,
bgcolor:
msg.role === 'user'
? d3roPalette.accent.amberDim
: d3roPalette.bg.elevated,
color: d3roPalette.text.primary,
border: `1px solid ${
msg.role === 'user'
? d3roPalette.accent.amber
: d3roPalette.border.subtle
}`,
wordBreak: 'break-word',
whiteSpace: 'pre-wrap',
}}
>
{msg.content}
</Box>
</Box>
))}
{/* 스트리밍 중 어시스턴트 메시지 */}
{streaming && (
<Box sx={{ display: 'flex', justifyContent: 'flex-start' }}>
<Box
sx={{
maxWidth: '80%',
px: 1.25,
py: 0.5,
borderRadius: '6px',
fontFamily: d3roFontMono,
fontSize: d3roTypo.compact.size,
lineHeight: 1.5,
bgcolor: d3roPalette.bg.elevated,
color: d3roPalette.text.primary,
border: `1px solid ${d3roPalette.border.subtle}`,
wordBreak: 'break-word',
whiteSpace: 'pre-wrap',
}}
>
{streamingContent || <TypingIndicator />}
</Box>
</Box>
)}
</Box>
{/* 스트리밍 진행 표시 */}
{streaming && (
<LinearProgress
sx={{
height: 1,
flexShrink: 0,
bgcolor: d3roPalette.bg.inset,
'& .MuiLinearProgress-bar': { bgcolor: d3roPalette.accent.amber },
}}
/>
)}
{/* 입력 영역 */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
px: 1.5,
py: 0.75,
flexShrink: 0,
borderTop: `1px solid ${d3roPalette.border.subtle}`,
}}
>
<TextField
inputRef={inputRef}
size="small"
fullWidth
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={t('meeting.chatPlaceholder')}
disabled={streaming}
sx={{
'& .MuiInputBase-root': {
fontFamily: d3roFontMono,
fontSize: d3roTypo.compact.size,
bgcolor: d3roPalette.bg.input,
borderRadius: '4px',
},
'& .MuiOutlinedInput-notchedOutline': {
borderColor: d3roPalette.border.default,
},
'&:hover .MuiOutlinedInput-notchedOutline': {
borderColor: d3roPalette.border.strong,
},
'& .Mui-focused .MuiOutlinedInput-notchedOutline': {
borderColor: d3roPalette.accent.amber,
},
}}
/>
<PhysicalButton
size="small"
onClick={handleSend}
disabled={!inputValue.trim() || streaming}
sx={{ flexShrink: 0, height: 36, minWidth: 36, px: 1 }}
>
<SendIcon sx={{ fontSize: 14 }} />
</PhysicalButton>
</Box>
</>
)}
</Box>
)
}

View file

@ -17,6 +17,7 @@ import CheckIcon from '@mui/icons-material/Check'
import { TranscriptTab } from './TranscriptTab' import { TranscriptTab } from './TranscriptTab'
import { DocumentTab } from './DocumentTab' import { DocumentTab } from './DocumentTab'
import { AddDocumentDialog } from './AddDocumentDialog' import { AddDocumentDialog } from './AddDocumentDialog'
import { MeetingChatPanel } from './MeetingChatPanel'
import { PhosphorText } from '../ds/PhosphorText' import { PhosphorText } from '../ds/PhosphorText'
import { d3roPalette, d3roFontMono, d3roTypo } from '../../theme' import { d3roPalette, d3roFontMono, d3roTypo } from '../../theme'
import { useI18n } from '../../i18n' import { useI18n } from '../../i18n'
@ -323,6 +324,9 @@ export function MeetingDetailTabs({
)} )}
</Box> </Box>
{/* AI 채팅 패널 */}
<MeetingChatPanel sessionId={detail.id} />
{/* 문서 생성 다이얼로그 */} {/* 문서 생성 다이얼로그 */}
<AddDocumentDialog <AddDocumentDialog
open={dialogOpen} open={dialogOpen}

View file

@ -2,13 +2,14 @@
// Phase 14.5: 전사 편집 탭 (상세 페이지용) // Phase 14.5: 전사 편집 탭 (상세 페이지용)
import { useState, useCallback } from 'react' import { useState, useCallback } from 'react'
import { Box, Switch, FormControlLabel } from '@mui/material' import { Box, Switch, FormControlLabel, LinearProgress } from '@mui/material'
import { EditableSegment } from './EditableSegment' import { EditableSegment } from './EditableSegment'
import { PhysicalButton } from '../ds/PhysicalButton' import { PhysicalButton } from '../ds/PhysicalButton'
import { PhosphorText } from '../ds/PhosphorText' import { PhosphorText } from '../ds/PhosphorText'
import { d3roPalette, d3roFontMono, d3roTypo } from '../../theme' import { d3roPalette, d3roFontMono, d3roTypo } from '../../theme'
import { useI18n } from '../../i18n' import { useI18n } from '../../i18n'
import FileDownloadIcon from '@mui/icons-material/FileDownload' import FileDownloadIcon from '@mui/icons-material/FileDownload'
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh'
interface TranscriptTabProps { interface TranscriptTabProps {
sessionId: string sessionId: string
@ -25,7 +26,7 @@ type TimelineItem =
| { kind: 'memo'; id: string; timestamp: number; content: string } | { kind: 'memo'; id: string; timestamp: number; content: string }
export function TranscriptTab({ export function TranscriptTab({
sessionId: _sessionId, sessionId,
segments, segments,
rawTranscript, rawTranscript,
editedTranscript, editedTranscript,
@ -35,6 +36,16 @@ export function TranscriptTab({
}: TranscriptTabProps): React.ReactElement { }: TranscriptTabProps): React.ReactElement {
const { t } = useI18n() const { t } = useI18n()
const [showEdited, setShowEdited] = useState(true) const [showEdited, setShowEdited] = useState(true)
const [polishing, setPolishing] = useState(false)
const handlePolish = useCallback(async () => {
setPolishing(true)
const resp = await window.electronAPI.meetingMode.polishTranscript({ sessionId })
if (resp.success) {
onSaveTranscript(resp.data)
}
setPolishing(false)
}, [sessionId, onSaveTranscript])
const handleDownloadTxt = useCallback(() => { const handleDownloadTxt = useCallback(() => {
const content = showEdited && editedTranscript ? editedTranscript : (rawTranscript ?? '') const content = showEdited && editedTranscript ? editedTranscript : (rawTranscript ?? '')
@ -91,6 +102,16 @@ export function TranscriptTab({
</PhosphorText> </PhosphorText>
} }
/> />
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
<PhysicalButton
size="small"
onClick={handlePolish}
disabled={polishing}
sx={{ height: 30, fontSize: d3roTypo.engrave.size }}
>
<AutoFixHighIcon sx={{ fontSize: 13, mr: 0.5 }} />
{polishing ? t('meeting.polishing') : t('meeting.polish')}
</PhysicalButton>
<PhysicalButton <PhysicalButton
size="small" size="small"
onClick={handleDownloadTxt} onClick={handleDownloadTxt}
@ -100,6 +121,19 @@ export function TranscriptTab({
{t('meeting.downloadTranscript')} {t('meeting.downloadTranscript')}
</PhysicalButton> </PhysicalButton>
</Box> </Box>
</Box>
{/* 폴리싱 진행 표시 */}
{polishing && (
<LinearProgress
sx={{
height: 2,
mb: 1,
bgcolor: d3roPalette.bg.inset,
'& .MuiLinearProgress-bar': { bgcolor: d3roPalette.accent.amber },
}}
/>
)}
{/* 타임라인 스크롤 영역 */} {/* 타임라인 스크롤 영역 */}
<Box <Box

View file

@ -253,5 +253,17 @@
"meeting.reportTemplate": "Bericht", "meeting.reportTemplate": "Bericht",
"meeting.ideaNoteTemplate": "Ideennotiz", "meeting.ideaNoteTemplate": "Ideennotiz",
"meeting.customTemplate": "Benutzerdefiniert", "meeting.customTemplate": "Benutzerdefiniert",
"meeting.downloadTranscript": "Transkript herunterladen" "meeting.downloadTranscript": "Transkript herunterladen",
"meeting.polish": "KI Glätten",
"meeting.polishing": "KI glättet Transkript...",
"meeting.polished": "KI Glättung abgeschlossen",
"meeting.mindmapTemplate": "Mindmap",
"meeting.chat": "KI-Chat",
"meeting.chatPlaceholder": "Fragen Sie zum Meeting...",
"meeting.chatSend": "Senden",
"meeting.chatClear": "Chat löschen",
"meeting.copyToClipboard": "In Zwischenablage kopieren",
"meeting.copied": "Kopiert",
"meeting.chatCollapse": "Chat einklappen",
"meeting.chatExpand": "Chat ausklappen"
} }

View file

@ -453,5 +453,17 @@
"meeting.reportTemplate": "Report", "meeting.reportTemplate": "Report",
"meeting.ideaNoteTemplate": "Idea Note", "meeting.ideaNoteTemplate": "Idea Note",
"meeting.customTemplate": "Custom", "meeting.customTemplate": "Custom",
"meeting.downloadTranscript": "Download Transcript" "meeting.downloadTranscript": "Download Transcript",
"meeting.polish": "AI Polish",
"meeting.polishing": "AI is polishing transcript...",
"meeting.polished": "AI Polish complete",
"meeting.mindmapTemplate": "Mind Map",
"meeting.chat": "AI Chat",
"meeting.chatPlaceholder": "Ask about the meeting...",
"meeting.chatSend": "Send",
"meeting.chatClear": "Clear Chat",
"meeting.copyToClipboard": "Copy to Clipboard",
"meeting.copied": "Copied",
"meeting.chatCollapse": "Collapse Chat",
"meeting.chatExpand": "Expand Chat"
} }

View file

@ -253,5 +253,17 @@
"meeting.reportTemplate": "Informe", "meeting.reportTemplate": "Informe",
"meeting.ideaNoteTemplate": "Nota de ideas", "meeting.ideaNoteTemplate": "Nota de ideas",
"meeting.customTemplate": "Personalizado", "meeting.customTemplate": "Personalizado",
"meeting.downloadTranscript": "Descargar transcripción" "meeting.downloadTranscript": "Descargar transcripción",
"meeting.polish": "Pulir con IA",
"meeting.polishing": "IA puliendo transcripción...",
"meeting.polished": "Pulido con IA completado",
"meeting.mindmapTemplate": "Mapa mental",
"meeting.chat": "Chat IA",
"meeting.chatPlaceholder": "Pregunta sobre la reunión...",
"meeting.chatSend": "Enviar",
"meeting.chatClear": "Limpiar chat",
"meeting.copyToClipboard": "Copiar al portapapeles",
"meeting.copied": "Copiado",
"meeting.chatCollapse": "Contraer chat",
"meeting.chatExpand": "Expandir chat"
} }

View file

@ -253,5 +253,17 @@
"meeting.reportTemplate": "Rapport", "meeting.reportTemplate": "Rapport",
"meeting.ideaNoteTemplate": "Note d'idées", "meeting.ideaNoteTemplate": "Note d'idées",
"meeting.customTemplate": "Personnalisé", "meeting.customTemplate": "Personnalisé",
"meeting.downloadTranscript": "Télécharger la transcription" "meeting.downloadTranscript": "Télécharger la transcription",
"meeting.polish": "Peaufiner avec IA",
"meeting.polishing": "L'IA peaufine la transcription...",
"meeting.polished": "Peaufinement IA terminé",
"meeting.mindmapTemplate": "Carte mentale",
"meeting.chat": "Chat IA",
"meeting.chatPlaceholder": "Posez une question sur la réunion...",
"meeting.chatSend": "Envoyer",
"meeting.chatClear": "Effacer le chat",
"meeting.copyToClipboard": "Copier dans le presse-papiers",
"meeting.copied": "Copié",
"meeting.chatCollapse": "Réduire le chat",
"meeting.chatExpand": "Agrandir le chat"
} }

View file

@ -253,5 +253,17 @@
"meeting.reportTemplate": "レポート", "meeting.reportTemplate": "レポート",
"meeting.ideaNoteTemplate": "アイデアノート", "meeting.ideaNoteTemplate": "アイデアノート",
"meeting.customTemplate": "カスタム", "meeting.customTemplate": "カスタム",
"meeting.downloadTranscript": "文字起こしをダウンロード" "meeting.downloadTranscript": "文字起こしをダウンロード",
"meeting.polish": "AI で整える",
"meeting.polishing": "AI が文字起こしを整えています...",
"meeting.polished": "AI 整形完了",
"meeting.mindmapTemplate": "マインドマップ",
"meeting.chat": "AI チャット",
"meeting.chatPlaceholder": "会議について質問してください...",
"meeting.chatSend": "送信",
"meeting.chatClear": "チャットをクリア",
"meeting.copyToClipboard": "クリップボードにコピー",
"meeting.copied": "コピーしました",
"meeting.chatCollapse": "チャットを折り畳む",
"meeting.chatExpand": "チャットを展開"
} }

View file

@ -453,5 +453,17 @@
"meeting.reportTemplate": "보고서", "meeting.reportTemplate": "보고서",
"meeting.ideaNoteTemplate": "아이디어 노트", "meeting.ideaNoteTemplate": "아이디어 노트",
"meeting.customTemplate": "커스텀", "meeting.customTemplate": "커스텀",
"meeting.downloadTranscript": "전사 다운로드" "meeting.downloadTranscript": "전사 다운로드",
"meeting.polish": "AI 다듬기",
"meeting.polishing": "AI가 전사를 다듬는 중...",
"meeting.polished": "AI 다듬기 완료",
"meeting.mindmapTemplate": "마인드맵",
"meeting.chat": "AI 채팅",
"meeting.chatPlaceholder": "회의 내용에 대해 질문하세요...",
"meeting.chatSend": "전송",
"meeting.chatClear": "대화 초기화",
"meeting.copyToClipboard": "클립보드 복사",
"meeting.copied": "복사됨",
"meeting.chatCollapse": "채팅 접기",
"meeting.chatExpand": "채팅 펼치기"
} }

View file

@ -253,5 +253,17 @@
"meeting.reportTemplate": "Relatório", "meeting.reportTemplate": "Relatório",
"meeting.ideaNoteTemplate": "Nota de ideias", "meeting.ideaNoteTemplate": "Nota de ideias",
"meeting.customTemplate": "Personalizado", "meeting.customTemplate": "Personalizado",
"meeting.downloadTranscript": "Baixar transcrição" "meeting.downloadTranscript": "Baixar transcrição",
"meeting.polish": "Polir com IA",
"meeting.polishing": "IA está polindo a transcrição...",
"meeting.polished": "Polimento IA concluído",
"meeting.mindmapTemplate": "Mapa mental",
"meeting.chat": "Chat IA",
"meeting.chatPlaceholder": "Pergunte sobre a reunião...",
"meeting.chatSend": "Enviar",
"meeting.chatClear": "Limpar chat",
"meeting.copyToClipboard": "Copiar para área de transferência",
"meeting.copied": "Copiado",
"meeting.chatCollapse": "Recolher chat",
"meeting.chatExpand": "Expandir chat"
} }

View file

@ -253,5 +253,17 @@
"meeting.reportTemplate": "Отчёт", "meeting.reportTemplate": "Отчёт",
"meeting.ideaNoteTemplate": "Заметка с идеями", "meeting.ideaNoteTemplate": "Заметка с идеями",
"meeting.customTemplate": "Пользовательский", "meeting.customTemplate": "Пользовательский",
"meeting.downloadTranscript": "Скачать транскрипцию" "meeting.downloadTranscript": "Скачать транскрипцию",
"meeting.polish": "Улучшить с ИИ",
"meeting.polishing": "ИИ улучшает транскрипцию...",
"meeting.polished": "Улучшение ИИ завершено",
"meeting.mindmapTemplate": "Карта мыслей",
"meeting.chat": "ИИ Чат",
"meeting.chatPlaceholder": "Задайте вопрос о встрече...",
"meeting.chatSend": "Отправить",
"meeting.chatClear": "Очистить чат",
"meeting.copyToClipboard": "Копировать в буфер обмена",
"meeting.copied": "Скопировано",
"meeting.chatCollapse": "Свернуть чат",
"meeting.chatExpand": "Развернуть чат"
} }

View file

@ -253,5 +253,17 @@
"meeting.reportTemplate": "รายงาน", "meeting.reportTemplate": "รายงาน",
"meeting.ideaNoteTemplate": "บันทึกไอเดีย", "meeting.ideaNoteTemplate": "บันทึกไอเดีย",
"meeting.customTemplate": "กำหนดเอง", "meeting.customTemplate": "กำหนดเอง",
"meeting.downloadTranscript": "ดาวน์โหลดการถอดความ" "meeting.downloadTranscript": "ดาวน์โหลดการถอดความ",
"meeting.polish": "ปรับแต่งด้วย AI",
"meeting.polishing": "AI กำลังปรับแต่งการถอดความ...",
"meeting.polished": "ปรับแต่งด้วย AI เสร็จสิ้น",
"meeting.mindmapTemplate": "แผนที่ความคิด",
"meeting.chat": "แชท AI",
"meeting.chatPlaceholder": "ถามเกี่ยวกับการประชุม...",
"meeting.chatSend": "ส่ง",
"meeting.chatClear": "ล้างแชท",
"meeting.copyToClipboard": "คัดลอกไปยังคลิปบอร์ด",
"meeting.copied": "คัดลอกแล้ว",
"meeting.chatCollapse": "ยุบแชท",
"meeting.chatExpand": "ขยายแชท"
} }

View file

@ -253,5 +253,17 @@
"meeting.reportTemplate": "Báo cáo", "meeting.reportTemplate": "Báo cáo",
"meeting.ideaNoteTemplate": "Ghi chú ý tưởng", "meeting.ideaNoteTemplate": "Ghi chú ý tưởng",
"meeting.customTemplate": "Tùy chỉnh", "meeting.customTemplate": "Tùy chỉnh",
"meeting.downloadTranscript": "Tải bản ghi" "meeting.downloadTranscript": "Tải bản ghi",
"meeting.polish": "Tinh chỉnh bằng AI",
"meeting.polishing": "AI đang tinh chỉnh bản ghi...",
"meeting.polished": "Tinh chỉnh AI hoàn tất",
"meeting.mindmapTemplate": "Sơ đồ tư duy",
"meeting.chat": "Chat AI",
"meeting.chatPlaceholder": "Hỏi về cuộc họp...",
"meeting.chatSend": "Gửi",
"meeting.chatClear": "Xóa chat",
"meeting.copyToClipboard": "Sao chép vào bộ nhớ tạm",
"meeting.copied": "Đã sao chép",
"meeting.chatCollapse": "Thu gọn chat",
"meeting.chatExpand": "Mở rộng chat"
} }

View file

@ -253,5 +253,17 @@
"meeting.reportTemplate": "報告", "meeting.reportTemplate": "報告",
"meeting.ideaNoteTemplate": "創意筆記", "meeting.ideaNoteTemplate": "創意筆記",
"meeting.customTemplate": "自訂", "meeting.customTemplate": "自訂",
"meeting.downloadTranscript": "下載轉錄" "meeting.downloadTranscript": "下載轉錄",
"meeting.polish": "AI 潤飾",
"meeting.polishing": "AI 正在潤飾轉錄...",
"meeting.polished": "AI 潤飾完成",
"meeting.mindmapTemplate": "心智圖",
"meeting.chat": "AI 聊天",
"meeting.chatPlaceholder": "詢問有關會議的內容...",
"meeting.chatSend": "傳送",
"meeting.chatClear": "清空聊天",
"meeting.copyToClipboard": "複製到剪貼簿",
"meeting.copied": "已複製",
"meeting.chatCollapse": "收起聊天",
"meeting.chatExpand": "展開聊天"
} }

View file

@ -253,5 +253,17 @@
"meeting.reportTemplate": "报告", "meeting.reportTemplate": "报告",
"meeting.ideaNoteTemplate": "创意笔记", "meeting.ideaNoteTemplate": "创意笔记",
"meeting.customTemplate": "自定义", "meeting.customTemplate": "自定义",
"meeting.downloadTranscript": "下载转录" "meeting.downloadTranscript": "下载转录",
"meeting.polish": "AI 润色",
"meeting.polishing": "AI 正在润色转录...",
"meeting.polished": "AI 润色完成",
"meeting.mindmapTemplate": "思维导图",
"meeting.chat": "AI 聊天",
"meeting.chatPlaceholder": "询问关于会议的内容...",
"meeting.chatSend": "发送",
"meeting.chatClear": "清空聊天",
"meeting.copyToClipboard": "复制到剪贴板",
"meeting.copied": "已复制",
"meeting.chatCollapse": "收起聊天",
"meeting.chatExpand": "展开聊天"
} }

View file

@ -165,6 +165,8 @@ export enum ErrorCode {
MeetingDocTemplateNotFound = 890, MeetingDocTemplateNotFound = 890,
MeetingDocTemplateBuiltinDelete = 891, MeetingDocTemplateBuiltinDelete = 891,
MeetingDocxExportFailed = 892, MeetingDocxExportFailed = 892,
MeetingPolishFailed = 893,
MeetingChatFailed = 894,
// === Config (800-849) === // === Config (800-849) ===
ConfigReadFailed = 800, ConfigReadFailed = 800,

View file

@ -332,6 +332,7 @@ export const IPC_CHANNELS = {
PROCESSING_PROGRESS: 'meetingMode:processingProgress', PROCESSING_PROGRESS: 'meetingMode:processingProgress',
SESSION_COMPLETED: 'meetingMode:sessionCompleted', SESSION_COMPLETED: 'meetingMode:sessionCompleted',
DOC_GENERATING_PROGRESS: 'meetingMode:docGeneratingProgress', DOC_GENERATING_PROGRESS: 'meetingMode:docGeneratingProgress',
POLISH_TRANSCRIPT: 'meetingMode:polishTranscript',
ERROR: 'meetingMode:error', ERROR: 'meetingMode:error',
}, },
@ -343,6 +344,16 @@ export const IPC_CHANNELS = {
DELETE: 'meetingDocTemplate:delete', DELETE: 'meetingDocTemplate:delete',
}, },
// ── Phase 15: Meeting AI Chat ──
MEETING_CHAT: {
SEND: 'meetingChat:send',
CANCEL: 'meetingChat:cancel',
CLEAR: 'meetingChat:clear',
DELTA: 'meetingChat:delta',
MESSAGE: 'meetingChat:message',
ERROR: 'meetingChat:error',
},
// ── Phase 11: License & Monetization ── // ── Phase 11: License & Monetization ──
LICENSE: { LICENSE: {
GET_INFO: 'license:getInfo', GET_INFO: 'license:getInfo',

View file

@ -1365,7 +1365,7 @@ export interface MeetingProcessingProgress {
// Phase 14.5: Meeting Document Types // Phase 14.5: Meeting Document Types
// ============================================================ // ============================================================
export type MeetingDocTemplateType = 'minutes' | 'report' | 'idea-note' | 'custom' export type MeetingDocTemplateType = 'minutes' | 'report' | 'idea-note' | 'custom' | 'mindmap'
export interface MeetingDocTemplate { export interface MeetingDocTemplate {
id: string id: string
@ -1453,3 +1453,22 @@ export interface UpdateMeetingDocTemplateParams {
export interface DeleteMeetingDocTemplateParams { export interface DeleteMeetingDocTemplateParams {
id: string id: string
} }
// ============================================================
// Phase 15: Meeting AI Chat
// ============================================================
export interface MeetingChatMessage {
role: 'user' | 'assistant'
content: string
timestamp: number
}
export interface MeetingChatSendParams {
sessionId: string
message: string
}
export interface MeetingChatDelta {
token: string
}