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