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:
parent
d4928ffa60
commit
2b17bf47b7
26 changed files with 980 additions and 26 deletions
|
|
@ -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,
|
||||||
|
|
|
||||||
205
docs/phases/phase-15-meeting-pro.md
Normal file
205
docs/phases/phase-15-meeting-pro.md
Normal 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 |
|
||||||
|
|
@ -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(''),
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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) ───────────────
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
325
src/renderer/components/meeting/MeetingChatPanel.tsx
Normal file
325
src/renderer/components/meeting/MeetingChatPanel.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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,16 +102,39 @@ export function TranscriptTab({
|
||||||
</PhosphorText>
|
</PhosphorText>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<PhysicalButton
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
|
||||||
size="small"
|
<PhysicalButton
|
||||||
onClick={handleDownloadTxt}
|
size="small"
|
||||||
sx={{ height: 30, fontSize: d3roTypo.engrave.size }}
|
onClick={handlePolish}
|
||||||
>
|
disabled={polishing}
|
||||||
<FileDownloadIcon sx={{ fontSize: 13, mr: 0.5 }} />
|
sx={{ height: 30, fontSize: d3roTypo.engrave.size }}
|
||||||
{t('meeting.downloadTranscript')}
|
>
|
||||||
</PhysicalButton>
|
<AutoFixHighIcon sx={{ fontSize: 13, mr: 0.5 }} />
|
||||||
|
{polishing ? t('meeting.polishing') : t('meeting.polish')}
|
||||||
|
</PhysicalButton>
|
||||||
|
<PhysicalButton
|
||||||
|
size="small"
|
||||||
|
onClick={handleDownloadTxt}
|
||||||
|
sx={{ height: 30, fontSize: d3roTypo.engrave.size }}
|
||||||
|
>
|
||||||
|
<FileDownloadIcon sx={{ fontSize: 13, mr: 0.5 }} />
|
||||||
|
{t('meeting.downloadTranscript')}
|
||||||
|
</PhysicalButton>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* 폴리싱 진행 표시 */}
|
||||||
|
{polishing && (
|
||||||
|
<LinearProgress
|
||||||
|
sx={{
|
||||||
|
height: 2,
|
||||||
|
mb: 1,
|
||||||
|
bgcolor: d3roPalette.bg.inset,
|
||||||
|
'& .MuiLinearProgress-bar': { bgcolor: d3roPalette.accent.amber },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 타임라인 스크롤 영역 */}
|
{/* 타임라인 스크롤 영역 */}
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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": "チャットを展開"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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": "채팅 펼치기"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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": "Развернуть чат"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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": "ขยายแชท"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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": "展開聊天"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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": "展开聊天"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue