diff --git a/src/features/listen/AssistantView.js b/src/features/listen/AssistantView.js index e1584ca..aab6869 100644 --- a/src/features/listen/AssistantView.js +++ b/src/features/listen/AssistantView.js @@ -1,4 +1,6 @@ import { html, css, LitElement } from '../../assets/lit-core-2.7.4.min.js'; +import './stt/SttView.js'; +import './summary/SummaryView.js'; export class AssistantView extends LitElement { static styles = css` @@ -82,73 +84,6 @@ export class AssistantView extends LitElement { user-select: none; } - /* highlight.js 스타일 추가 */ - .insights-container pre { - background: rgba(0, 0, 0, 0.4) !important; - border-radius: 8px !important; - padding: 12px !important; - margin: 8px 0 !important; - overflow-x: auto !important; - border: 1px solid rgba(255, 255, 255, 0.1) !important; - white-space: pre !important; - word-wrap: normal !important; - word-break: normal !important; - } - - .insights-container code { - font-family: 'Monaco', 'Menlo', 'Consolas', monospace !important; - font-size: 11px !important; - background: transparent !important; - white-space: pre !important; - word-wrap: normal !important; - word-break: normal !important; - } - - .insights-container pre code { - white-space: pre !important; - word-wrap: normal !important; - word-break: normal !important; - display: block !important; - } - - .insights-container p code { - background: rgba(255, 255, 255, 0.1) !important; - padding: 2px 4px !important; - border-radius: 3px !important; - color: #ffd700 !important; - } - - .hljs-keyword { - color: #ff79c6 !important; - } - .hljs-string { - color: #f1fa8c !important; - } - .hljs-comment { - color: #6272a4 !important; - } - .hljs-number { - color: #bd93f9 !important; - } - .hljs-function { - color: #50fa7b !important; - } - .hljs-variable { - color: #8be9fd !important; - } - .hljs-built_in { - color: #ffb86c !important; - } - .hljs-title { - color: #50fa7b !important; - } - .hljs-attr { - color: #50fa7b !important; - } - .hljs-tag { - color: #ff79c6 !important; - } - .assistant-container { display: flex; flex-direction: column; @@ -158,8 +93,6 @@ export class AssistantView extends LitElement { background: rgba(0, 0, 0, 0.6); overflow: hidden; border-radius: 12px; - /* outline: 0.5px rgba(255, 255, 255, 0.5) solid; */ - /* outline-offset: -1px; */ width: 100%; min-height: 200px; } @@ -171,7 +104,7 @@ export class AssistantView extends LitElement { left: 0; right: 0; bottom: 0; - border-radius: 12px; /* Match parent */ + border-radius: 12px; padding: 1px; background: linear-gradient(169deg, rgba(255, 255, 255, 0.17) 0%, rgba(255, 255, 255, 0.08) 50%, rgba(255, 255, 255, 0.17) 100%); -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); @@ -299,8 +232,8 @@ export class AssistantView extends LitElement { height: 24px; flex-shrink: 0; transition: background-color 0.15s ease; - position: relative; /* For icon positioning */ - overflow: hidden; /* Hide overflowing parts of icons during animation */ + position: relative; + overflow: hidden; } .copy-button:hover { @@ -330,213 +263,6 @@ export class AssistantView extends LitElement { transform: translate(-50%, -50%) scale(1); } - .transcription-container { - overflow-y: auto; - padding: 12px 12px 16px 12px; - display: flex; - flex-direction: column; - gap: 8px; - min-height: 150px; - max-height: 600px; - position: relative; - z-index: 1; - flex: 1; - } - - .transcription-container.hidden { - display: none; - } - - .transcription-container::-webkit-scrollbar { - width: 8px; - } - .transcription-container::-webkit-scrollbar-track { - background: rgba(0, 0, 0, 0.1); - border-radius: 4px; - } - .transcription-container::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.3); - border-radius: 4px; - } - .transcription-container::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.5); - } - - .stt-message { - padding: 8px 12px; - border-radius: 12px; - max-width: 80%; - word-wrap: break-word; - word-break: break-word; - line-height: 1.5; - font-size: 13px; - margin-bottom: 4px; - box-sizing: border-box; - } - - .stt-message.them { - background: rgba(255, 255, 255, 0.1); - color: rgba(255, 255, 255, 0.9); - align-self: flex-start; - border-bottom-left-radius: 4px; - margin-right: auto; - } - - .stt-message.me { - background: rgba(0, 122, 255, 0.8); - color: white; - align-self: flex-end; - border-bottom-right-radius: 4px; - margin-left: auto; - } - - .insights-container { - overflow-y: auto; - padding: 12px 16px 16px 16px; - position: relative; - z-index: 1; - min-height: 150px; - max-height: 600px; - flex: 1; - } - - insights-title { - color: rgba(255, 255, 255, 0.8); - font-size: 15px; - font-weight: 500; - font-family: 'Helvetica Neue', sans-serif; - margin: 12px 0 8px 0; - } - - .insights-container.hidden { - display: none; - } - - .insights-container::-webkit-scrollbar { - width: 8px; - } - .insights-container::-webkit-scrollbar-track { - background: rgba(0, 0, 0, 0.1); - border-radius: 4px; - } - .insights-container::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.3); - border-radius: 4px; - } - .insights-container::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.5); - } - - .insights-container h4 { - color: #ffffff; - font-size: 12px; - font-weight: 600; - margin: 12px 0 8px 0; - padding: 4px 8px; - border-radius: 4px; - background: transparent; - cursor: default; - } - - .insights-container h4:hover { - background: transparent; - } - - .insights-container h4:first-child { - margin-top: 0; - } - - .outline-item { - color: #ffffff; - font-size: 11px; - line-height: 1.4; - margin: 4px 0; - padding: 6px 8px; - border-radius: 4px; - background: transparent; - transition: background-color 0.15s ease; - cursor: pointer; - word-wrap: break-word; - } - - .outline-item:hover { - background: rgba(255, 255, 255, 0.1); - } - - .request-item { - color: #ffffff; - font-size: 12px; - line-height: 1.2; - margin: 4px 0; - padding: 6px 8px; - border-radius: 4px; - background: transparent; - cursor: default; - word-wrap: break-word; - transition: background-color 0.15s ease; - } - - .request-item.clickable { - cursor: pointer; - transition: all 0.15s ease; - } - .request-item.clickable:hover { - background: rgba(255, 255, 255, 0.1); - transform: translateX(2px); - } - - /* 마크다운 렌더링된 콘텐츠 스타일 */ - .markdown-content { - color: #ffffff; - font-size: 11px; - line-height: 1.4; - margin: 4px 0; - padding: 6px 8px; - border-radius: 4px; - background: transparent; - cursor: pointer; - word-wrap: break-word; - transition: all 0.15s ease; - } - - .markdown-content:hover { - background: rgba(255, 255, 255, 0.1); - transform: translateX(2px); - } - - .markdown-content p { - margin: 4px 0; - } - - .markdown-content ul, - .markdown-content ol { - margin: 4px 0; - padding-left: 16px; - } - - .markdown-content li { - margin: 2px 0; - } - - .markdown-content a { - color: #8be9fd; - text-decoration: none; - } - - .markdown-content a:hover { - text-decoration: underline; - } - - .markdown-content strong { - font-weight: 600; - color: #f8f8f2; - } - - .markdown-content em { - font-style: italic; - color: #f1fa8c; - } - .timer { font-family: 'Monaco', 'Menlo', monospace; font-size: 10px; @@ -545,10 +271,6 @@ export class AssistantView extends LitElement { `; static properties = { - structuredData: { type: Object }, - // outlines: { type: Array }, - // analysisRequests: { type: Array }, - sttMessages: { type: Array }, viewMode: { type: String }, isHovering: { type: Boolean }, isAnimating: { type: Boolean }, @@ -561,183 +283,66 @@ export class AssistantView extends LitElement { constructor() { super(); - // this.outlines = []; - // this.analysisRequests = []; - this.structuredData = { - summary: [], - topic: { header: '', bullets: [] }, - actions: [], - followUps: [], - }; this.isSessionActive = false; this.hasCompletedRecording = false; - this.sttMessages = []; this.viewMode = 'insights'; this.isHovering = false; this.isAnimating = false; this.elapsedTime = '00:00'; this.captureStartTime = null; this.timerInterval = null; - this.resizeObserver = null; this.adjustHeightThrottle = null; this.isThrottled = false; - this._shouldScrollAfterUpdate = false; - this.messageIdCounter = 0; this.copyState = 'idle'; this.copyTimeout = null; - // 마크다운 라이브러리 초기화 - this.marked = null; - this.hljs = null; - this.isLibrariesLoaded = false; - this.DOMPurify = null; - this.isDOMPurifyLoaded = false; - - // --- Debug Utilities --- - this._debug = { - enabled: false, // Set to false to disable debug messages - interval: null, - counter: 1, - }; - this.handleSttUpdate = this.handleSttUpdate.bind(this); this.adjustWindowHeight = this.adjustWindowHeight.bind(this); - - this.loadLibraries(); } - // --- Debug Utilities --- - _startDebugStream() { - if (!this._debug.enabled) return; - - this._debug.interval = setInterval(() => { - const speaker = this._debug.counter % 2 === 0 ? 'You' : 'Other Person'; - const text = `이것은 ${this._debug.counter}번째 자동 생성 메시지입니다. UI가 자동으로 조절되는지 확인합니다.`; - - this._debug.counter++; - - this.handleSttUpdate(null, { speaker, text, isFinal: true }); - }, 1000); - } - - _stopDebugStream() { - if (this._debug.interval) { - clearInterval(this._debug.interval); + connectedCallback() { + super.connectedCallback(); + // Only start timer if session is active + if (this.isSessionActive) { + this.startTimer(); } - } + if (window.require) { + const { ipcRenderer } = window.require('electron'); + ipcRenderer.on('session-state-changed', (event, { isActive }) => { + const wasActive = this.isSessionActive; + this.isSessionActive = isActive; - async loadLibraries() { - try { - if (!window.marked) { - await this.loadScript('../../assets/marked-4.3.0.min.js'); - } - - if (!window.hljs) { - await this.loadScript('../../assets/highlight-11.9.0.min.js'); - } - - if (!window.DOMPurify) { - await this.loadScript('../../assets/dompurify-3.0.7.min.js'); - } - - this.marked = window.marked; - this.hljs = window.hljs; - this.DOMPurify = window.DOMPurify; - - if (this.marked && this.hljs) { - this.marked.setOptions({ - highlight: (code, lang) => { - if (lang && this.hljs.getLanguage(lang)) { - try { - return this.hljs.highlight(code, { language: lang }).value; - } catch (err) { - console.warn('Highlight error:', err); - } - } - try { - return this.hljs.highlightAuto(code).value; - } catch (err) { - console.warn('Auto highlight error:', err); - } - return code; - }, - breaks: true, - gfm: true, - pedantic: false, - smartypants: false, - xhtml: false, - }); - - this.isLibrariesLoaded = true; - console.log('Markdown libraries loaded successfully'); - } - - if (this.DOMPurify) { - this.isDOMPurifyLoaded = true; - console.log('DOMPurify loaded successfully in AssistantView'); - } - } catch (error) { - console.error('Failed to load libraries:', error); - } - } - - loadScript(src) { - return new Promise((resolve, reject) => { - const script = document.createElement('script'); - script.src = src; - script.onload = resolve; - script.onerror = reject; - document.head.appendChild(script); - }); - } - - parseMarkdown(text) { - if (!text) return ''; - - if (!this.isLibrariesLoaded || !this.marked) { - return text; - } - - try { - return this.marked(text); - } catch (error) { - console.error('Markdown parsing error:', error); - return text; - } - } - - handleMarkdownClick(originalText) { - this.handleRequestClick(originalText); - } - - renderMarkdownContent() { - if (!this.isLibrariesLoaded || !this.marked) { - return; - } - - const markdownElements = this.shadowRoot.querySelectorAll('[data-markdown-id]'); - markdownElements.forEach(element => { - const originalText = element.getAttribute('data-original-text'); - if (originalText) { - try { - let parsedHTML = this.parseMarkdown(originalText); - - if (this.isDOMPurifyLoaded && this.DOMPurify) { - parsedHTML = this.DOMPurify.sanitize(parsedHTML); - - if (this.DOMPurify.removed && this.DOMPurify.removed.length > 0) { - console.warn('Unsafe content detected in insights, showing plain text'); - element.textContent = '⚠️ ' + originalText; - return; - } - } - - element.innerHTML = parsedHTML; - } catch (error) { - console.error('Error rendering markdown for element:', error); - element.textContent = originalText; + if (!wasActive && isActive) { + this.hasCompletedRecording = false; + this.startTimer(); + // Reset child components + this.updateComplete.then(() => { + const sttView = this.shadowRoot.querySelector('stt-view'); + const summaryView = this.shadowRoot.querySelector('summary-view'); + if (sttView) sttView.resetTranscript(); + if (summaryView) summaryView.resetAnalysis(); + }); + this.requestUpdate(); } - } - }); + if (wasActive && !isActive) { + this.hasCompletedRecording = true; + this.stopTimer(); + this.requestUpdate(); + } + }); + } + } + + disconnectedCallback() { + super.disconnectedCallback(); + this.stopTimer(); + + if (this.adjustHeightThrottle) { + clearTimeout(this.adjustHeightThrottle); + this.adjustHeightThrottle = null; + } + if (this.copyTimeout) { + clearTimeout(this.copyTimeout); + } } startTimer() { @@ -766,19 +371,15 @@ export class AssistantView extends LitElement { this.updateComplete .then(() => { const topBar = this.shadowRoot.querySelector('.top-bar'); - const activeContent = - this.viewMode === 'transcript' - ? this.shadowRoot.querySelector('.transcription-container') - : this.shadowRoot.querySelector('.insights-container'); + const activeContent = this.viewMode === 'transcript' + ? this.shadowRoot.querySelector('stt-view') + : this.shadowRoot.querySelector('summary-view'); if (!topBar || !activeContent) return; const topBarHeight = topBar.offsetHeight; - - const contentHeight = activeContent.scrollHeight; - + const contentHeight = activeContent.offsetHeight; const idealHeight = topBarHeight + contentHeight + 20; - const targetHeight = Math.min(700, Math.max(200, idealHeight)); console.log( @@ -808,62 +409,17 @@ export class AssistantView extends LitElement { this.requestUpdate(); } - parseOutlineData() { - const result = { - currentSummary: [], - mainTopicHeading: '', - mainTopicBullets: [], - }; - - if (!this.outlines || this.outlines.length === 0) { - return result; - } - - const allBullets = this.outlines.filter(item => item.startsWith('BULLET::')); - if (allBullets.length > 0) { - result.currentSummary.push(allBullets[0].replace('BULLET::', '').trim()); - } - - const heading = this.outlines.find(item => item.startsWith('HEADING::')); - if (heading) { - result.mainTopicHeading = heading.replace('HEADING::', '').trim(); - } - - if (allBullets.length > 1) { - result.mainTopicBullets = allBullets.slice(1).map(item => item.replace('BULLET::', '').trim()); - } - - return result; - } - async handleCopy() { if (this.copyState === 'copied') return; let textToCopy = ''; if (this.viewMode === 'transcript') { - textToCopy = this.sttMessages.map(msg => `${msg.speaker}: ${msg.text}`).join('\n'); + const sttView = this.shadowRoot.querySelector('stt-view'); + textToCopy = sttView ? sttView.getTranscriptText() : ''; } else { - const data = this.structuredData || { summary: [], topic: { header: '', bullets: [] }, actions: [] }; - let sections = []; - - if (data.summary && data.summary.length > 0) { - sections.push(`Current Summary:\n${data.summary.map(s => `• ${s}`).join('\n')}`); - } - - if (data.topic && data.topic.header && data.topic.bullets.length > 0) { - sections.push(`\n${data.topic.header}:\n${data.topic.bullets.map(b => `• ${b}`).join('\n')}`); - } - - if (data.actions && data.actions.length > 0) { - sections.push(`\nActions:\n${data.actions.map(a => `▸ ${a}`).join('\n')}`); - } - - if (data.followUps && data.followUps.length > 0) { - sections.push(`\nFollow-Ups:\n${data.followUps.map(f => `▸ ${f}`).join('\n')}`); - } - - textToCopy = sections.join('\n\n').trim(); + const summaryView = this.shadowRoot.querySelector('summary-view'); + textToCopy = summaryView ? summaryView.getSummaryText() : ''; } try { @@ -900,177 +456,24 @@ export class AssistantView extends LitElement { }, 16); } - handleSttUpdate(event, { speaker, text, isFinal, isPartial }) { - if (text === undefined) return; + updated(changedProperties) { + super.updated(changedProperties); - const container = this.shadowRoot.querySelector('.transcription-container'); - this._shouldScrollAfterUpdate = container ? container.scrollTop + container.clientHeight >= container.scrollHeight - 10 : false; - - const findLastPartialIdx = spk => { - for (let i = this.sttMessages.length - 1; i >= 0; i--) { - const m = this.sttMessages[i]; - if (m.speaker === spk && m.isPartial) return i; - } - return -1; - }; - - const newMessages = [...this.sttMessages]; - const targetIdx = findLastPartialIdx(speaker); - - if (isPartial) { - if (targetIdx !== -1) { - newMessages[targetIdx] = { - ...newMessages[targetIdx], - text, - isPartial: true, - isFinal: false, - }; - } else { - newMessages.push({ - id: this.messageIdCounter++, - speaker, - text, - isPartial: true, - isFinal: false, - }); - } - } else if (isFinal) { - if (targetIdx !== -1) { - newMessages[targetIdx] = { - ...newMessages[targetIdx], - text, - isPartial: false, - isFinal: true, - }; - } else { - newMessages.push({ - id: this.messageIdCounter++, - speaker, - text, - isPartial: false, - isFinal: true, - }); - } - } - - this.sttMessages = newMessages; - } - - scrollToTranscriptionBottom() { - setTimeout(() => { - const container = this.shadowRoot.querySelector('.transcription-container'); - if (container) { - container.scrollTop = container.scrollHeight; - } - }, 0); - } - - async handleRequestClick(requestText) { - console.log('🔥 Analysis request clicked:', requestText); - - if (window.require) { - const { ipcRenderer } = window.require('electron'); - - try { - const isAskViewVisible = await ipcRenderer.invoke('is-window-visible', 'ask'); - - if (!isAskViewVisible) { - await ipcRenderer.invoke('toggle-feature', 'ask'); - await new Promise(resolve => setTimeout(resolve, 100)); - } - - const result = await ipcRenderer.invoke('send-question-to-ask', requestText); - - if (result.success) { - console.log('✅ Question sent to AskView successfully'); - } else { - console.error('❌ Failed to send question to AskView:', result.error); - } - } catch (error) { - console.error('❌ Error in handleRequestClick:', error); - } + if (changedProperties.has('viewMode')) { + this.adjustWindowHeight(); } } - connectedCallback() { - super.connectedCallback(); - this.startTimer(); - if (window.require) { - const { ipcRenderer } = window.require('electron'); - ipcRenderer.on('stt-update', this.handleSttUpdate); - ipcRenderer.on('session-state-changed', (event, { isActive }) => { - const wasActive = this.isSessionActive; - this.isSessionActive = isActive; - - if (!wasActive && isActive) { - this.hasCompletedRecording = false; - - // 🔄 Reset transcript & analysis when a fresh session starts - this.sttMessages = []; - this.structuredData = { - summary: [], - topic: { header: '', bullets: [] }, - actions: [], - followUps: [], - }; - this.requestUpdate(); - } - if (wasActive && !isActive) { - this.hasCompletedRecording = true; - - this.requestUpdate(); - } - }); - } - this._startDebugStream(); - } - - disconnectedCallback() { - super.disconnectedCallback(); - this.stopTimer(); - - if (this.adjustHeightThrottle) { - clearTimeout(this.adjustHeightThrottle); - this.adjustHeightThrottle = null; - } - if (this.copyTimeout) { - clearTimeout(this.copyTimeout); - } - - if (window.require) { - const { ipcRenderer } = window.require('electron'); - ipcRenderer.removeListener('stt-update', this.handleSttUpdate); - } - - this._stopDebugStream(); + handleSttMessagesUpdated(event) { + // Handle messages update from SttView if needed + this.adjustWindowHeightThrottled(); } firstUpdated() { super.firstUpdated(); - setTimeout(() => this.adjustWindowHeight(), 200); } - updated(changedProperties) { - super.updated(changedProperties); - - this.renderMarkdownContent(); - - if (changedProperties.has('sttMessages')) { - if (this._shouldScrollAfterUpdate) { - this.scrollToTranscriptionBottom(); - this._shouldScrollAfterUpdate = false; - } - this.adjustWindowHeightThrottled(); - } - - if (changedProperties.has('viewMode')) { - this.adjustWindowHeight(); - } else if (changedProperties.has('outlines') || changedProperties.has('analysisRequests') || changedProperties.has('structuredData')) { - this.adjustWindowHeightThrottled(); - } - } - render() { const displayText = this.isHovering ? this.viewMode === 'transcript' @@ -1080,16 +483,6 @@ export class AssistantView extends LitElement { ? `Live insights` : `Glass is Listening ${this.elapsedTime}`; - const data = this.structuredData || { - summary: [], - topic: { header: '', bullets: [] }, - actions: [], - }; - - const getSpeakerClass = speaker => { - return speaker.toLowerCase() === 'me' ? 'me' : 'them'; - }; - return html`
@@ -1131,84 +524,15 @@ export class AssistantView extends LitElement {
-
- ${this.sttMessages.map(msg => html`
${msg.text}
`)} -
+ -
- Current Summary - ${data.summary.length > 0 - ? data.summary - .slice(0, 5) - .map( - (bullet, index) => html` -
this.handleMarkdownClick(bullet)} - > - ${bullet} -
- ` - ) - : html`
No content yet...
`} - ${data.topic.header - ? html` - ${data.topic.header} - ${data.topic.bullets - .slice(0, 3) - .map( - (bullet, index) => html` -
this.handleMarkdownClick(bullet)} - > - ${bullet} -
- ` - )} - ` - : ''} - ${data.actions.length > 0 - ? html` - Actions - ${data.actions - .slice(0, 5) - .map( - (action, index) => html` -
this.handleMarkdownClick(action)} - > - ${action} -
- ` - )} - ` - : ''} - ${this.hasCompletedRecording && data.followUps && data.followUps.length > 0 - ? html` - Follow-Ups - ${data.followUps.map( - (followUp, index) => html` -
this.handleMarkdownClick(followUp)} - > - ${followUp} -
- ` - )} - ` - : ''} -
+ `; } diff --git a/src/features/listen/repositories/index.js b/src/features/listen/repositories/index.js deleted file mode 100644 index 9c0d12f..0000000 --- a/src/features/listen/repositories/index.js +++ /dev/null @@ -1,20 +0,0 @@ -const sqliteRepository = require('./sqlite.repository'); -// const firebaseRepository = require('./firebase.repository'); // Future implementation -const authService = require('../../../common/services/authService'); - -function getRepository() { - // In the future, we can check the user's login status from authService - // const user = authService.getCurrentUser(); - // if (user.isLoggedIn) { - // return firebaseRepository; - // } - return sqliteRepository; -} - -// Directly export functions for ease of use, decided by the strategy -module.exports = { - addTranscript: (...args) => getRepository().addTranscript(...args), - saveSummary: (...args) => getRepository().saveSummary(...args), - getAllTranscriptsBySessionId: (...args) => getRepository().getAllTranscriptsBySessionId(...args), - getSummaryBySessionId: (...args) => getRepository().getSummaryBySessionId(...args), -}; \ No newline at end of file diff --git a/src/features/listen/repositories/sqlite.repository.js b/src/features/listen/repositories/sqlite.repository.js deleted file mode 100644 index 7d293cd..0000000 --- a/src/features/listen/repositories/sqlite.repository.js +++ /dev/null @@ -1,66 +0,0 @@ -const sqliteClient = require('../../../common/services/sqliteClient'); - -function addTranscript({ sessionId, speaker, text }) { - const db = sqliteClient.getDb(); - return new Promise((resolve, reject) => { - const transcriptId = require('crypto').randomUUID(); - const now = Math.floor(Date.now() / 1000); - const query = `INSERT INTO transcripts (id, session_id, start_at, speaker, text, created_at) VALUES (?, ?, ?, ?, ?, ?)`; - db.run(query, [transcriptId, sessionId, now, speaker, text, now], function(err) { - if (err) reject(err); - else resolve({ id: transcriptId }); - }); - }); -} - -function saveSummary({ sessionId, tldr, text, bullet_json, action_json, model = 'gpt-4.1' }) { - const db = sqliteClient.getDb(); - return new Promise((resolve, reject) => { - const now = Math.floor(Date.now() / 1000); - const query = ` - INSERT INTO summaries (session_id, generated_at, model, text, tldr, bullet_json, action_json, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - ON CONFLICT(session_id) DO UPDATE SET - generated_at=excluded.generated_at, - model=excluded.model, - text=excluded.text, - tldr=excluded.tldr, - bullet_json=excluded.bullet_json, - action_json=excluded.action_json, - updated_at=excluded.updated_at - `; - db.run(query, [sessionId, now, model, text, tldr, bullet_json, action_json, now], function(err) { - if (err) reject(err); - else resolve({ changes: this.changes }); - }); - }); -} - -function getAllTranscriptsBySessionId(sessionId) { - const db = sqliteClient.getDb(); - return new Promise((resolve, reject) => { - const query = "SELECT * FROM transcripts WHERE session_id = ? ORDER BY start_at ASC"; - db.all(query, [sessionId], (err, rows) => { - if (err) reject(err); - else resolve(rows); - }); - }); -} - -function getSummaryBySessionId(sessionId) { - const db = sqliteClient.getDb(); - return new Promise((resolve, reject) => { - const query = "SELECT * FROM summaries WHERE session_id = ?"; - db.get(query, [sessionId], (err, row) => { - if (err) reject(err); - else resolve(row || null); - }); - }); -} - -module.exports = { - addTranscript, - saveSummary, - getAllTranscriptsBySessionId, - getSummaryBySessionId -}; \ No newline at end of file diff --git a/src/features/listen/stt/SttView.js b/src/features/listen/stt/SttView.js new file mode 100644 index 0000000..31e2a5a --- /dev/null +++ b/src/features/listen/stt/SttView.js @@ -0,0 +1,228 @@ +import { html, css, LitElement } from '../../../assets/lit-core-2.7.4.min.js'; + +export class SttView extends LitElement { + static styles = css` + :host { + display: block; + width: 100%; + } + + /* Inherit font styles from parent */ + + .transcription-container { + overflow-y: auto; + padding: 12px 12px 16px 12px; + display: flex; + flex-direction: column; + gap: 8px; + min-height: 150px; + max-height: 600px; + position: relative; + z-index: 1; + flex: 1; + } + + /* Visibility handled by parent component */ + + .transcription-container::-webkit-scrollbar { + width: 8px; + } + .transcription-container::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.1); + border-radius: 4px; + } + .transcription-container::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.3); + border-radius: 4px; + } + .transcription-container::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.5); + } + + .stt-message { + padding: 8px 12px; + border-radius: 12px; + max-width: 80%; + word-wrap: break-word; + word-break: break-word; + line-height: 1.5; + font-size: 13px; + margin-bottom: 4px; + box-sizing: border-box; + } + + .stt-message.them { + background: rgba(255, 255, 255, 0.1); + color: rgba(255, 255, 255, 0.9); + align-self: flex-start; + border-bottom-left-radius: 4px; + margin-right: auto; + } + + .stt-message.me { + background: rgba(0, 122, 255, 0.8); + color: white; + align-self: flex-end; + border-bottom-right-radius: 4px; + margin-left: auto; + } + + .empty-state { + display: flex; + align-items: center; + justify-content: center; + height: 100px; + color: rgba(255, 255, 255, 0.6); + font-size: 12px; + font-style: italic; + } + `; + + static properties = { + sttMessages: { type: Array }, + isVisible: { type: Boolean }, + }; + + constructor() { + super(); + this.sttMessages = []; + this.isVisible = true; + this.messageIdCounter = 0; + this._shouldScrollAfterUpdate = false; + + this.handleSttUpdate = this.handleSttUpdate.bind(this); + } + + connectedCallback() { + super.connectedCallback(); + if (window.require) { + const { ipcRenderer } = window.require('electron'); + ipcRenderer.on('stt-update', this.handleSttUpdate); + } + } + + disconnectedCallback() { + super.disconnectedCallback(); + if (window.require) { + const { ipcRenderer } = window.require('electron'); + ipcRenderer.removeListener('stt-update', this.handleSttUpdate); + } + } + + // Handle session reset from parent + resetTranscript() { + this.sttMessages = []; + this.requestUpdate(); + } + + handleSttUpdate(event, { speaker, text, isFinal, isPartial }) { + if (text === undefined) return; + + const container = this.shadowRoot.querySelector('.transcription-container'); + this._shouldScrollAfterUpdate = container ? container.scrollTop + container.clientHeight >= container.scrollHeight - 10 : false; + + const findLastPartialIdx = spk => { + for (let i = this.sttMessages.length - 1; i >= 0; i--) { + const m = this.sttMessages[i]; + if (m.speaker === spk && m.isPartial) return i; + } + return -1; + }; + + const newMessages = [...this.sttMessages]; + const targetIdx = findLastPartialIdx(speaker); + + if (isPartial) { + if (targetIdx !== -1) { + newMessages[targetIdx] = { + ...newMessages[targetIdx], + text, + isPartial: true, + isFinal: false, + }; + } else { + newMessages.push({ + id: this.messageIdCounter++, + speaker, + text, + isPartial: true, + isFinal: false, + }); + } + } else if (isFinal) { + if (targetIdx !== -1) { + newMessages[targetIdx] = { + ...newMessages[targetIdx], + text, + isPartial: false, + isFinal: true, + }; + } else { + newMessages.push({ + id: this.messageIdCounter++, + speaker, + text, + isPartial: false, + isFinal: true, + }); + } + } + + this.sttMessages = newMessages; + + // Notify parent component about message updates + this.dispatchEvent(new CustomEvent('stt-messages-updated', { + detail: { messages: this.sttMessages }, + bubbles: true + })); + } + + scrollToBottom() { + setTimeout(() => { + const container = this.shadowRoot.querySelector('.transcription-container'); + if (container) { + container.scrollTop = container.scrollHeight; + } + }, 0); + } + + getSpeakerClass(speaker) { + return speaker.toLowerCase() === 'me' ? 'me' : 'them'; + } + + getTranscriptText() { + return this.sttMessages.map(msg => `${msg.speaker}: ${msg.text}`).join('\n'); + } + + updated(changedProperties) { + super.updated(changedProperties); + + if (changedProperties.has('sttMessages')) { + if (this._shouldScrollAfterUpdate) { + this.scrollToBottom(); + this._shouldScrollAfterUpdate = false; + } + } + } + + render() { + if (!this.isVisible) { + return html`
`; + } + + return html` +
+ ${this.sttMessages.length === 0 + ? html`
Waiting for speech...
` + : this.sttMessages.map(msg => html` +
+ ${msg.text} +
+ `) + } +
+ `; + } +} + +customElements.define('stt-view', SttView); \ No newline at end of file diff --git a/src/features/listen/summary/SummaryView.js b/src/features/listen/summary/SummaryView.js new file mode 100644 index 0000000..15d79b9 --- /dev/null +++ b/src/features/listen/summary/SummaryView.js @@ -0,0 +1,559 @@ +import { html, css, LitElement } from '../../../assets/lit-core-2.7.4.min.js'; + +export class SummaryView extends LitElement { + static styles = css` + :host { + display: block; + width: 100%; + } + + /* Inherit font styles from parent */ + + /* highlight.js 스타일 추가 */ + .insights-container pre { + background: rgba(0, 0, 0, 0.4) !important; + border-radius: 8px !important; + padding: 12px !important; + margin: 8px 0 !important; + overflow-x: auto !important; + border: 1px solid rgba(255, 255, 255, 0.1) !important; + white-space: pre !important; + word-wrap: normal !important; + word-break: normal !important; + } + + .insights-container code { + font-family: 'Monaco', 'Menlo', 'Consolas', monospace !important; + font-size: 11px !important; + background: transparent !important; + white-space: pre !important; + word-wrap: normal !important; + word-break: normal !important; + } + + .insights-container pre code { + white-space: pre !important; + word-wrap: normal !important; + word-break: normal !important; + display: block !important; + } + + .insights-container p code { + background: rgba(255, 255, 255, 0.1) !important; + padding: 2px 4px !important; + border-radius: 3px !important; + color: #ffd700 !important; + } + + .hljs-keyword { + color: #ff79c6 !important; + } + .hljs-string { + color: #f1fa8c !important; + } + .hljs-comment { + color: #6272a4 !important; + } + .hljs-number { + color: #bd93f9 !important; + } + .hljs-function { + color: #50fa7b !important; + } + .hljs-variable { + color: #8be9fd !important; + } + .hljs-built_in { + color: #ffb86c !important; + } + .hljs-title { + color: #50fa7b !important; + } + .hljs-attr { + color: #50fa7b !important; + } + .hljs-tag { + color: #ff79c6 !important; + } + + .insights-container { + overflow-y: auto; + padding: 12px 16px 16px 16px; + position: relative; + z-index: 1; + min-height: 150px; + max-height: 600px; + flex: 1; + } + + /* Visibility handled by parent component */ + + .insights-container::-webkit-scrollbar { + width: 8px; + } + .insights-container::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.1); + border-radius: 4px; + } + .insights-container::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.3); + border-radius: 4px; + } + .insights-container::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.5); + } + + insights-title { + color: rgba(255, 255, 255, 0.8); + font-size: 15px; + font-weight: 500; + font-family: 'Helvetica Neue', sans-serif; + margin: 12px 0 8px 0; + display: block; + } + + .insights-container h4 { + color: #ffffff; + font-size: 12px; + font-weight: 600; + margin: 12px 0 8px 0; + padding: 4px 8px; + border-radius: 4px; + background: transparent; + cursor: default; + } + + .insights-container h4:hover { + background: transparent; + } + + .insights-container h4:first-child { + margin-top: 0; + } + + .outline-item { + color: #ffffff; + font-size: 11px; + line-height: 1.4; + margin: 4px 0; + padding: 6px 8px; + border-radius: 4px; + background: transparent; + transition: background-color 0.15s ease; + cursor: pointer; + word-wrap: break-word; + } + + .outline-item:hover { + background: rgba(255, 255, 255, 0.1); + } + + .request-item { + color: #ffffff; + font-size: 12px; + line-height: 1.2; + margin: 4px 0; + padding: 6px 8px; + border-radius: 4px; + background: transparent; + cursor: default; + word-wrap: break-word; + transition: background-color 0.15s ease; + } + + .request-item.clickable { + cursor: pointer; + transition: all 0.15s ease; + } + .request-item.clickable:hover { + background: rgba(255, 255, 255, 0.1); + transform: translateX(2px); + } + + /* 마크다운 렌더링된 콘텐츠 스타일 */ + .markdown-content { + color: #ffffff; + font-size: 11px; + line-height: 1.4; + margin: 4px 0; + padding: 6px 8px; + border-radius: 4px; + background: transparent; + cursor: pointer; + word-wrap: break-word; + transition: all 0.15s ease; + } + + .markdown-content:hover { + background: rgba(255, 255, 255, 0.1); + transform: translateX(2px); + } + + .markdown-content p { + margin: 4px 0; + } + + .markdown-content ul, + .markdown-content ol { + margin: 4px 0; + padding-left: 16px; + } + + .markdown-content li { + margin: 2px 0; + } + + .markdown-content a { + color: #8be9fd; + text-decoration: none; + } + + .markdown-content a:hover { + text-decoration: underline; + } + + .markdown-content strong { + font-weight: 600; + color: #f8f8f2; + } + + .markdown-content em { + font-style: italic; + color: #f1fa8c; + } + + .empty-state { + display: flex; + align-items: center; + justify-content: center; + height: 100px; + color: rgba(255, 255, 255, 0.6); + font-size: 12px; + font-style: italic; + } + `; + + static properties = { + structuredData: { type: Object }, + isVisible: { type: Boolean }, + hasCompletedRecording: { type: Boolean }, + }; + + constructor() { + super(); + this.structuredData = { + summary: [], + topic: { header: '', bullets: [] }, + actions: [], + followUps: [], + }; + this.isVisible = true; + this.hasCompletedRecording = false; + + // 마크다운 라이브러리 초기화 + this.marked = null; + this.hljs = null; + this.isLibrariesLoaded = false; + this.DOMPurify = null; + this.isDOMPurifyLoaded = false; + + this.loadLibraries(); + } + + connectedCallback() { + super.connectedCallback(); + if (window.require) { + const { ipcRenderer } = window.require('electron'); + ipcRenderer.on('update-structured-data', (event, data) => { + this.structuredData = data; + this.requestUpdate(); + }); + } + } + + disconnectedCallback() { + super.disconnectedCallback(); + if (window.require) { + const { ipcRenderer } = window.require('electron'); + ipcRenderer.removeAllListeners('update-structured-data'); + } + } + + // Handle session reset from parent + resetAnalysis() { + this.structuredData = { + summary: [], + topic: { header: '', bullets: [] }, + actions: [], + followUps: [], + }; + this.requestUpdate(); + } + + async loadLibraries() { + try { + if (!window.marked) { + await this.loadScript('../../../assets/marked-4.3.0.min.js'); + } + + if (!window.hljs) { + await this.loadScript('../../../assets/highlight-11.9.0.min.js'); + } + + if (!window.DOMPurify) { + await this.loadScript('../../../assets/dompurify-3.0.7.min.js'); + } + + this.marked = window.marked; + this.hljs = window.hljs; + this.DOMPurify = window.DOMPurify; + + if (this.marked && this.hljs) { + this.marked.setOptions({ + highlight: (code, lang) => { + if (lang && this.hljs.getLanguage(lang)) { + try { + return this.hljs.highlight(code, { language: lang }).value; + } catch (err) { + console.warn('Highlight error:', err); + } + } + try { + return this.hljs.highlightAuto(code).value; + } catch (err) { + console.warn('Auto highlight error:', err); + } + return code; + }, + breaks: true, + gfm: true, + pedantic: false, + smartypants: false, + xhtml: false, + }); + + this.isLibrariesLoaded = true; + console.log('Markdown libraries loaded successfully'); + } + + if (this.DOMPurify) { + this.isDOMPurifyLoaded = true; + console.log('DOMPurify loaded successfully in SummaryView'); + } + } catch (error) { + console.error('Failed to load libraries:', error); + } + } + + loadScript(src) { + return new Promise((resolve, reject) => { + const script = document.createElement('script'); + script.src = src; + script.onload = resolve; + script.onerror = reject; + document.head.appendChild(script); + }); + } + + parseMarkdown(text) { + if (!text) return ''; + + if (!this.isLibrariesLoaded || !this.marked) { + return text; + } + + try { + return this.marked(text); + } catch (error) { + console.error('Markdown parsing error:', error); + return text; + } + } + + handleMarkdownClick(originalText) { + this.handleRequestClick(originalText); + } + + renderMarkdownContent() { + if (!this.isLibrariesLoaded || !this.marked) { + return; + } + + const markdownElements = this.shadowRoot.querySelectorAll('[data-markdown-id]'); + markdownElements.forEach(element => { + const originalText = element.getAttribute('data-original-text'); + if (originalText) { + try { + let parsedHTML = this.parseMarkdown(originalText); + + if (this.isDOMPurifyLoaded && this.DOMPurify) { + parsedHTML = this.DOMPurify.sanitize(parsedHTML); + + if (this.DOMPurify.removed && this.DOMPurify.removed.length > 0) { + console.warn('Unsafe content detected in insights, showing plain text'); + element.textContent = '⚠️ ' + originalText; + return; + } + } + + element.innerHTML = parsedHTML; + } catch (error) { + console.error('Error rendering markdown for element:', error); + element.textContent = originalText; + } + } + }); + } + + async handleRequestClick(requestText) { + console.log('🔥 Analysis request clicked:', requestText); + + if (window.require) { + const { ipcRenderer } = window.require('electron'); + + try { + const isAskViewVisible = await ipcRenderer.invoke('is-window-visible', 'ask'); + + if (!isAskViewVisible) { + await ipcRenderer.invoke('toggle-feature', 'ask'); + await new Promise(resolve => setTimeout(resolve, 100)); + } + + const result = await ipcRenderer.invoke('send-question-to-ask', requestText); + + if (result.success) { + console.log('✅ Question sent to AskView successfully'); + } else { + console.error('❌ Failed to send question to AskView:', result.error); + } + } catch (error) { + console.error('❌ Error in handleRequestClick:', error); + } + } + } + + getSummaryText() { + const data = this.structuredData || { summary: [], topic: { header: '', bullets: [] }, actions: [] }; + let sections = []; + + if (data.summary && data.summary.length > 0) { + sections.push(`Current Summary:\n${data.summary.map(s => `• ${s}`).join('\n')}`); + } + + if (data.topic && data.topic.header && data.topic.bullets.length > 0) { + sections.push(`\n${data.topic.header}:\n${data.topic.bullets.map(b => `• ${b}`).join('\n')}`); + } + + if (data.actions && data.actions.length > 0) { + sections.push(`\nActions:\n${data.actions.map(a => `▸ ${a}`).join('\n')}`); + } + + if (data.followUps && data.followUps.length > 0) { + sections.push(`\nFollow-Ups:\n${data.followUps.map(f => `▸ ${f}`).join('\n')}`); + } + + return sections.join('\n\n').trim(); + } + + updated(changedProperties) { + super.updated(changedProperties); + this.renderMarkdownContent(); + } + + render() { + if (!this.isVisible) { + return html`
`; + } + + const data = this.structuredData || { + summary: [], + topic: { header: '', bullets: [] }, + actions: [], + }; + + const hasAnyContent = data.summary.length > 0 || data.topic.bullets.length > 0 || data.actions.length > 0; + + return html` +
+ ${!hasAnyContent + ? html`
No insights yet...
` + : html` + Current Summary + ${data.summary.length > 0 + ? data.summary + .slice(0, 5) + .map( + (bullet, index) => html` +
this.handleMarkdownClick(bullet)} + > + ${bullet} +
+ ` + ) + : html`
No content yet...
`} + ${data.topic.header + ? html` + ${data.topic.header} + ${data.topic.bullets + .slice(0, 3) + .map( + (bullet, index) => html` +
this.handleMarkdownClick(bullet)} + > + ${bullet} +
+ ` + )} + ` + : ''} + ${data.actions.length > 0 + ? html` + Actions + ${data.actions + .slice(0, 5) + .map( + (action, index) => html` +
this.handleMarkdownClick(action)} + > + ${action} +
+ ` + )} + ` + : ''} + ${this.hasCompletedRecording && data.followUps && data.followUps.length > 0 + ? html` + Follow-Ups + ${data.followUps.map( + (followUp, index) => html` +
this.handleMarkdownClick(followUp)} + > + ${followUp} +
+ ` + )} + ` + : ''} + `} +
+ `; + } +} + +customElements.define('summary-view', SummaryView); \ No newline at end of file