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
|
||||
|
||||
## 현재 상태
|
||||
Phase 1~14 전체 완료 + 랜딩 페이지(site/).
|
||||
Phase 1~14.5 전체 완료 + 랜딩 페이지(site/).
|
||||
차단 이슈: @nut-tree-fork/nut-js 포크 사용
|
||||
|
||||
## 설계 문서 (구현 시 Read 도구로 참조)
|
||||
|
|
@ -50,6 +50,7 @@ Phase10+: MemoService, VoiceCommandService, ScreenContextService, ChainService,
|
|||
Phase12+: FileTranscriptionService, MeetingSummaryService, DictationTemplateService
|
||||
Phase13+: VoiceConversationService, RAGService, VoiceActionService
|
||||
Phase14: MeetingModeService
|
||||
Phase14.5: MeetingDocTemplateService + 다중 문서 생성/편집/내보내기
|
||||
|
||||
## DS 컴포넌트 (src/renderer/components/ds/)
|
||||
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(),
|
||||
sessionId: text('session_id').notNull(),
|
||||
templateType: text('template_type', {
|
||||
enum: ['minutes', 'report', 'idea-note', 'custom'],
|
||||
enum: ['minutes', 'report', 'idea-note', 'custom', 'mindmap'],
|
||||
}).notNull(),
|
||||
title: text('title').notNull(),
|
||||
content: text('content').notNull().default(''),
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import type {
|
|||
MeetingExportParams,
|
||||
MeetingGenerateDocParams,
|
||||
MeetingExportFormat,
|
||||
MeetingChatSendParams,
|
||||
} from '@shared/types'
|
||||
|
||||
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')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,6 +118,32 @@ const BUILTIN_TEMPLATES: MeetingDocTemplate[] = [
|
|||
createdAt: 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 {
|
||||
|
|
|
|||
|
|
@ -65,6 +65,10 @@ class MeetingModeService extends EventEmitter {
|
|||
/** CaptionService session-saved 방지 플래그 */
|
||||
private _meetingModeActive = false
|
||||
|
||||
// Phase 15: Meeting AI Chat
|
||||
private _chatHistory: Array<{ role: 'user' | 'assistant'; content: string }> = []
|
||||
private _chatAbortController: AbortController | null = null
|
||||
|
||||
// ── 공개 접근자 ──
|
||||
|
||||
getState(): MeetingModeState {
|
||||
|
|
@ -823,6 +827,99 @@ class MeetingModeService extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
// ── Phase 15: Auto Polish ──
|
||||
|
||||
async polishTranscript(sessionId: string): Promise<string> {
|
||||
const db = getDatabase()
|
||||
const row = db.select().from(meetingSessions).where(eq(meetingSessions.id, sessionId)).get()
|
||||
if (!row) throw new D3ROError(ErrorCode.MeetingSessionNotFound, `세션 없음: ${sessionId}`)
|
||||
|
||||
const transcript = row.rawTranscript
|
||||
if (!transcript) throw new D3ROError(ErrorCode.MeetingPolishFailed, '전사 텍스트가 없습니다')
|
||||
|
||||
const { getLocalLLMService } = await import('./LocalLLMService')
|
||||
const llm = getLocalLLMService()
|
||||
const result = await llm.generate(transcript, {
|
||||
systemPrompt: '다음 음성 전사 텍스트를 다듬어주세요. 필러 단어(음, 어, 그, 아 등)를 제거하고, 문장 구조를 자연스럽게 교정하되, 원래 의미와 내용은 절대 변경하지 마세요. 타임스탬프 형식 [MM:SS]은 그대로 유지하세요.',
|
||||
temperature: 0.3,
|
||||
})
|
||||
|
||||
db.update(meetingSessions).set({
|
||||
editedTranscript: result.text,
|
||||
updatedAt: Date.now(),
|
||||
}).where(eq(meetingSessions.id, sessionId)).run()
|
||||
|
||||
logger.info(`Auto Polish 완료: sessionId=${sessionId}`)
|
||||
return result.text
|
||||
}
|
||||
|
||||
// ── Phase 15: Meeting AI Chat ──
|
||||
|
||||
async chatWithMeeting(sessionId: string, userMessage: string): Promise<void> {
|
||||
const db = getDatabase()
|
||||
const row = db.select().from(meetingSessions).where(eq(meetingSessions.id, sessionId)).get()
|
||||
if (!row) throw new D3ROError(ErrorCode.MeetingSessionNotFound, `세션 없음: ${sessionId}`)
|
||||
|
||||
const transcript = row.editedTranscript ?? row.rawTranscript ?? ''
|
||||
|
||||
const systemPrompt = `당신은 회의 내용을 분석하는 AI 어시스턴트입니다.
|
||||
아래 회의 전사 내용을 참고하여 사용자의 질문에 정확하게 답변하세요.
|
||||
전사 내용에 없는 것은 "전사 내용에서 확인되지 않습니다"라고 답하세요.
|
||||
|
||||
## 회의 전사
|
||||
${transcript}`
|
||||
|
||||
this._chatHistory.push({ role: 'user', content: userMessage })
|
||||
|
||||
const { getLocalLLMService } = await import('./LocalLLMService')
|
||||
const llm = getLocalLLMService()
|
||||
|
||||
const messages = [
|
||||
{ role: 'system' as const, content: systemPrompt },
|
||||
...this._chatHistory,
|
||||
]
|
||||
|
||||
let fullResponse = ''
|
||||
this._chatAbortController = new AbortController()
|
||||
const signal = this._chatAbortController.signal
|
||||
|
||||
try {
|
||||
const generator = llm.chatStream(messages, { temperature: 0.5 })
|
||||
|
||||
for await (const token of generator) {
|
||||
if (signal.aborted) break
|
||||
fullResponse += token
|
||||
this._sendToRenderer(IPC_CHANNELS.MEETING_CHAT.DELTA, { token })
|
||||
}
|
||||
|
||||
this._chatHistory.push({ role: 'assistant', content: fullResponse })
|
||||
this._sendToRenderer(IPC_CHANNELS.MEETING_CHAT.MESSAGE, {
|
||||
role: 'assistant',
|
||||
content: fullResponse,
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
} catch (err) {
|
||||
if ((err as Error).name !== 'AbortError') {
|
||||
this._sendToRenderer(IPC_CHANNELS.MEETING_CHAT.ERROR, {
|
||||
message: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
this._chatAbortController = null
|
||||
}
|
||||
}
|
||||
|
||||
cancelMeetingChat(): void {
|
||||
if (this._chatAbortController) {
|
||||
this._chatAbortController.abort()
|
||||
this._chatAbortController = null
|
||||
}
|
||||
}
|
||||
|
||||
clearMeetingChatHistory(): void {
|
||||
this._chatHistory = []
|
||||
}
|
||||
|
||||
private _buildPdfHtml(session: MeetingSessionDetail): string {
|
||||
const durationMin = session.durationMs ? Math.round(session.durationMs / 60000) : 0
|
||||
const minutesHtml = session.minutesMarkdown
|
||||
|
|
|
|||
|
|
@ -162,6 +162,9 @@ import type {
|
|||
CreateMeetingDocTemplateParams,
|
||||
UpdateMeetingDocTemplateParams,
|
||||
DeleteMeetingDocTemplateParams,
|
||||
MeetingChatMessage,
|
||||
MeetingChatSendParams,
|
||||
MeetingChatDelta,
|
||||
} from '@shared/types'
|
||||
import { Feature } from '@shared/types'
|
||||
import type { IPCResult } from '@shared/errors'
|
||||
|
|
@ -635,6 +638,25 @@ const electronAPI = {
|
|||
invoke<void>(IPC_CHANNELS.MEETING_MODE.EDIT_SEGMENT, params),
|
||||
onDocGeneratingProgress: (cb: (e: MeetingDocGeneratingProgress) => void): Unsubscribe =>
|
||||
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) ───────────────
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import type { MeetingExportFormat } from '@shared/types'
|
|||
|
||||
interface ExportMenuProps {
|
||||
onExport: (format: MeetingExportFormat) => void
|
||||
onCopyToClipboard?: () => void
|
||||
}
|
||||
|
||||
interface FormatItem {
|
||||
|
|
@ -18,7 +19,7 @@ interface FormatItem {
|
|||
label: string
|
||||
}
|
||||
|
||||
export function ExportMenu({ onExport }: ExportMenuProps): React.ReactElement {
|
||||
export function ExportMenu({ onExport, onCopyToClipboard }: ExportMenuProps): React.ReactElement {
|
||||
const { t } = useI18n()
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null)
|
||||
const open = Boolean(anchorEl)
|
||||
|
|
@ -90,6 +91,22 @@ export function ExportMenu({ onExport }: ExportMenuProps): React.ReactElement {
|
|||
{label}
|
||||
</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>
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
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 { DocumentTab } from './DocumentTab'
|
||||
import { AddDocumentDialog } from './AddDocumentDialog'
|
||||
import { MeetingChatPanel } from './MeetingChatPanel'
|
||||
import { PhosphorText } from '../ds/PhosphorText'
|
||||
import { d3roPalette, d3roFontMono, d3roTypo } from '../../theme'
|
||||
import { useI18n } from '../../i18n'
|
||||
|
|
@ -323,6 +324,9 @@ export function MeetingDetailTabs({
|
|||
)}
|
||||
</Box>
|
||||
|
||||
{/* AI 채팅 패널 */}
|
||||
<MeetingChatPanel sessionId={detail.id} />
|
||||
|
||||
{/* 문서 생성 다이얼로그 */}
|
||||
<AddDocumentDialog
|
||||
open={dialogOpen}
|
||||
|
|
|
|||
|
|
@ -2,13 +2,14 @@
|
|||
// Phase 14.5: 전사 편집 탭 (상세 페이지용)
|
||||
|
||||
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 { PhysicalButton } from '../ds/PhysicalButton'
|
||||
import { PhosphorText } from '../ds/PhosphorText'
|
||||
import { d3roPalette, d3roFontMono, d3roTypo } from '../../theme'
|
||||
import { useI18n } from '../../i18n'
|
||||
import FileDownloadIcon from '@mui/icons-material/FileDownload'
|
||||
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh'
|
||||
|
||||
interface TranscriptTabProps {
|
||||
sessionId: string
|
||||
|
|
@ -25,7 +26,7 @@ type TimelineItem =
|
|||
| { kind: 'memo'; id: string; timestamp: number; content: string }
|
||||
|
||||
export function TranscriptTab({
|
||||
sessionId: _sessionId,
|
||||
sessionId,
|
||||
segments,
|
||||
rawTranscript,
|
||||
editedTranscript,
|
||||
|
|
@ -35,6 +36,16 @@ export function TranscriptTab({
|
|||
}: TranscriptTabProps): React.ReactElement {
|
||||
const { t } = useI18n()
|
||||
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 content = showEdited && editedTranscript ? editedTranscript : (rawTranscript ?? '')
|
||||
|
|
@ -91,16 +102,39 @@ export function TranscriptTab({
|
|||
</PhosphorText>
|
||||
}
|
||||
/>
|
||||
<PhysicalButton
|
||||
size="small"
|
||||
onClick={handleDownloadTxt}
|
||||
sx={{ height: 30, fontSize: d3roTypo.engrave.size }}
|
||||
>
|
||||
<FileDownloadIcon sx={{ fontSize: 13, mr: 0.5 }} />
|
||||
{t('meeting.downloadTranscript')}
|
||||
</PhysicalButton>
|
||||
<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
|
||||
size="small"
|
||||
onClick={handleDownloadTxt}
|
||||
sx={{ height: 30, fontSize: d3roTypo.engrave.size }}
|
||||
>
|
||||
<FileDownloadIcon sx={{ fontSize: 13, mr: 0.5 }} />
|
||||
{t('meeting.downloadTranscript')}
|
||||
</PhysicalButton>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* 폴리싱 진행 표시 */}
|
||||
{polishing && (
|
||||
<LinearProgress
|
||||
sx={{
|
||||
height: 2,
|
||||
mb: 1,
|
||||
bgcolor: d3roPalette.bg.inset,
|
||||
'& .MuiLinearProgress-bar': { bgcolor: d3roPalette.accent.amber },
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 타임라인 스크롤 영역 */}
|
||||
<Box
|
||||
sx={{
|
||||
|
|
|
|||
|
|
@ -253,5 +253,17 @@
|
|||
"meeting.reportTemplate": "Bericht",
|
||||
"meeting.ideaNoteTemplate": "Ideennotiz",
|
||||
"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.ideaNoteTemplate": "Idea Note",
|
||||
"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.ideaNoteTemplate": "Nota de ideas",
|
||||
"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.ideaNoteTemplate": "Note d'idées",
|
||||
"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.ideaNoteTemplate": "アイデアノート",
|
||||
"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.ideaNoteTemplate": "아이디어 노트",
|
||||
"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.ideaNoteTemplate": "Nota de ideias",
|
||||
"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.ideaNoteTemplate": "Заметка с идеями",
|
||||
"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.ideaNoteTemplate": "บันทึกไอเดีย",
|
||||
"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.ideaNoteTemplate": "Ghi chú ý tưởng",
|
||||
"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.ideaNoteTemplate": "創意筆記",
|
||||
"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.ideaNoteTemplate": "创意笔记",
|
||||
"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,
|
||||
MeetingDocTemplateBuiltinDelete = 891,
|
||||
MeetingDocxExportFailed = 892,
|
||||
MeetingPolishFailed = 893,
|
||||
MeetingChatFailed = 894,
|
||||
|
||||
// === Config (800-849) ===
|
||||
ConfigReadFailed = 800,
|
||||
|
|
|
|||
|
|
@ -332,6 +332,7 @@ export const IPC_CHANNELS = {
|
|||
PROCESSING_PROGRESS: 'meetingMode:processingProgress',
|
||||
SESSION_COMPLETED: 'meetingMode:sessionCompleted',
|
||||
DOC_GENERATING_PROGRESS: 'meetingMode:docGeneratingProgress',
|
||||
POLISH_TRANSCRIPT: 'meetingMode:polishTranscript',
|
||||
ERROR: 'meetingMode:error',
|
||||
},
|
||||
|
||||
|
|
@ -343,6 +344,16 @@ export const IPC_CHANNELS = {
|
|||
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 ──
|
||||
LICENSE: {
|
||||
GET_INFO: 'license:getInfo',
|
||||
|
|
|
|||
|
|
@ -1365,7 +1365,7 @@ export interface MeetingProcessingProgress {
|
|||
// 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 {
|
||||
id: string
|
||||
|
|
@ -1453,3 +1453,22 @@ export interface UpdateMeetingDocTemplateParams {
|
|||
export interface DeleteMeetingDocTemplateParams {
|
||||
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