listen feature refactor done
This commit is contained in:
		
							parent
							
								
									80a3c01656
								
							
						
					
					
						commit
						aa9252880b
					
				@ -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`
 | 
			
		||||
            <div class="assistant-container">
 | 
			
		||||
                <div class="top-bar">
 | 
			
		||||
@ -1131,84 +524,15 @@ export class AssistantView extends LitElement {
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <div class="transcription-container ${this.viewMode !== 'transcript' ? 'hidden' : ''}">
 | 
			
		||||
                    ${this.sttMessages.map(msg => html` <div class="stt-message ${getSpeakerClass(msg.speaker)}">${msg.text}</div> `)}
 | 
			
		||||
                </div>
 | 
			
		||||
                <stt-view 
 | 
			
		||||
                    .isVisible=${this.viewMode === 'transcript'}
 | 
			
		||||
                    @stt-messages-updated=${this.handleSttMessagesUpdated}
 | 
			
		||||
                ></stt-view>
 | 
			
		||||
 | 
			
		||||
                <div class="insights-container ${this.viewMode !== 'insights' ? 'hidden' : ''}">
 | 
			
		||||
                    <insights-title>Current Summary</insights-title>
 | 
			
		||||
                    ${data.summary.length > 0
 | 
			
		||||
                        ? data.summary
 | 
			
		||||
                              .slice(0, 5)
 | 
			
		||||
                              .map(
 | 
			
		||||
                                  (bullet, index) => html`
 | 
			
		||||
                                      <div
 | 
			
		||||
                                          class="markdown-content"
 | 
			
		||||
                                          data-markdown-id="summary-${index}"
 | 
			
		||||
                                          data-original-text="${bullet}"
 | 
			
		||||
                                          @click=${() => this.handleMarkdownClick(bullet)}
 | 
			
		||||
                                      >
 | 
			
		||||
                                          ${bullet}
 | 
			
		||||
                                      </div>
 | 
			
		||||
                                  `
 | 
			
		||||
                              )
 | 
			
		||||
                        : html` <div class="request-item">No content yet...</div> `}
 | 
			
		||||
                    ${data.topic.header
 | 
			
		||||
                        ? html`
 | 
			
		||||
                              <insights-title>${data.topic.header}</insights-title>
 | 
			
		||||
                              ${data.topic.bullets
 | 
			
		||||
                                  .slice(0, 3)
 | 
			
		||||
                                  .map(
 | 
			
		||||
                                      (bullet, index) => html`
 | 
			
		||||
                                          <div
 | 
			
		||||
                                              class="markdown-content"
 | 
			
		||||
                                              data-markdown-id="topic-${index}"
 | 
			
		||||
                                              data-original-text="${bullet}"
 | 
			
		||||
                                              @click=${() => this.handleMarkdownClick(bullet)}
 | 
			
		||||
                                          >
 | 
			
		||||
                                              ${bullet}
 | 
			
		||||
                                          </div>
 | 
			
		||||
                                      `
 | 
			
		||||
                                  )}
 | 
			
		||||
                          `
 | 
			
		||||
                        : ''}
 | 
			
		||||
                    ${data.actions.length > 0
 | 
			
		||||
                        ? html`
 | 
			
		||||
                              <insights-title>Actions</insights-title>
 | 
			
		||||
                              ${data.actions
 | 
			
		||||
                                  .slice(0, 5)
 | 
			
		||||
                                  .map(
 | 
			
		||||
                                      (action, index) => html`
 | 
			
		||||
                                          <div
 | 
			
		||||
                                              class="markdown-content"
 | 
			
		||||
                                              data-markdown-id="action-${index}"
 | 
			
		||||
                                              data-original-text="${action}"
 | 
			
		||||
                                              @click=${() => this.handleMarkdownClick(action)}
 | 
			
		||||
                                          >
 | 
			
		||||
                                              ${action}
 | 
			
		||||
                                          </div>
 | 
			
		||||
                                      `
 | 
			
		||||
                                  )}
 | 
			
		||||
                          `
 | 
			
		||||
                        : ''}
 | 
			
		||||
                    ${this.hasCompletedRecording && data.followUps && data.followUps.length > 0
 | 
			
		||||
                        ? html`
 | 
			
		||||
                              <insights-title>Follow-Ups</insights-title>
 | 
			
		||||
                              ${data.followUps.map(
 | 
			
		||||
                                  (followUp, index) => html`
 | 
			
		||||
                                      <div
 | 
			
		||||
                                          class="markdown-content"
 | 
			
		||||
                                          data-markdown-id="followup-${index}"
 | 
			
		||||
                                          data-original-text="${followUp}"
 | 
			
		||||
                                          @click=${() => this.handleMarkdownClick(followUp)}
 | 
			
		||||
                                      >
 | 
			
		||||
                                          ${followUp}
 | 
			
		||||
                                      </div>
 | 
			
		||||
                                  `
 | 
			
		||||
                              )}
 | 
			
		||||
                          `
 | 
			
		||||
                        : ''}
 | 
			
		||||
                </div>
 | 
			
		||||
                <summary-view 
 | 
			
		||||
                    .isVisible=${this.viewMode === 'insights'}
 | 
			
		||||
                    .hasCompletedRecording=${this.hasCompletedRecording}
 | 
			
		||||
                ></summary-view>
 | 
			
		||||
            </div>
 | 
			
		||||
        `;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -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),
 | 
			
		||||
}; 
 | 
			
		||||
@ -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
 | 
			
		||||
}; 
 | 
			
		||||
							
								
								
									
										228
									
								
								src/features/listen/stt/SttView.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										228
									
								
								src/features/listen/stt/SttView.js
									
									
									
									
									
										Normal file
									
								
							@ -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`<div style="display: none;"></div>`;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return html`
 | 
			
		||||
            <div class="transcription-container">
 | 
			
		||||
                ${this.sttMessages.length === 0
 | 
			
		||||
                    ? html`<div class="empty-state">Waiting for speech...</div>`
 | 
			
		||||
                    : this.sttMessages.map(msg => html`
 | 
			
		||||
                        <div class="stt-message ${this.getSpeakerClass(msg.speaker)}">
 | 
			
		||||
                            ${msg.text}
 | 
			
		||||
                        </div>
 | 
			
		||||
                    `)
 | 
			
		||||
                }
 | 
			
		||||
            </div>
 | 
			
		||||
        `;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
customElements.define('stt-view', SttView); 
 | 
			
		||||
							
								
								
									
										559
									
								
								src/features/listen/summary/SummaryView.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										559
									
								
								src/features/listen/summary/SummaryView.js
									
									
									
									
									
										Normal file
									
								
							@ -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`<div style="display: none;"></div>`;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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`
 | 
			
		||||
            <div class="insights-container">
 | 
			
		||||
                ${!hasAnyContent
 | 
			
		||||
                    ? html`<div class="empty-state">No insights yet...</div>`
 | 
			
		||||
                    : html`
 | 
			
		||||
                        <insights-title>Current Summary</insights-title>
 | 
			
		||||
                        ${data.summary.length > 0
 | 
			
		||||
                            ? data.summary
 | 
			
		||||
                                  .slice(0, 5)
 | 
			
		||||
                                  .map(
 | 
			
		||||
                                      (bullet, index) => html`
 | 
			
		||||
                                          <div
 | 
			
		||||
                                              class="markdown-content"
 | 
			
		||||
                                              data-markdown-id="summary-${index}"
 | 
			
		||||
                                              data-original-text="${bullet}"
 | 
			
		||||
                                              @click=${() => this.handleMarkdownClick(bullet)}
 | 
			
		||||
                                          >
 | 
			
		||||
                                              ${bullet}
 | 
			
		||||
                                          </div>
 | 
			
		||||
                                      `
 | 
			
		||||
                                  )
 | 
			
		||||
                            : html` <div class="request-item">No content yet...</div> `}
 | 
			
		||||
                        ${data.topic.header
 | 
			
		||||
                            ? html`
 | 
			
		||||
                                  <insights-title>${data.topic.header}</insights-title>
 | 
			
		||||
                                  ${data.topic.bullets
 | 
			
		||||
                                      .slice(0, 3)
 | 
			
		||||
                                      .map(
 | 
			
		||||
                                          (bullet, index) => html`
 | 
			
		||||
                                              <div
 | 
			
		||||
                                                  class="markdown-content"
 | 
			
		||||
                                                  data-markdown-id="topic-${index}"
 | 
			
		||||
                                                  data-original-text="${bullet}"
 | 
			
		||||
                                                  @click=${() => this.handleMarkdownClick(bullet)}
 | 
			
		||||
                                              >
 | 
			
		||||
                                                  ${bullet}
 | 
			
		||||
                                              </div>
 | 
			
		||||
                                          `
 | 
			
		||||
                                      )}
 | 
			
		||||
                              `
 | 
			
		||||
                            : ''}
 | 
			
		||||
                        ${data.actions.length > 0
 | 
			
		||||
                            ? html`
 | 
			
		||||
                                  <insights-title>Actions</insights-title>
 | 
			
		||||
                                  ${data.actions
 | 
			
		||||
                                      .slice(0, 5)
 | 
			
		||||
                                      .map(
 | 
			
		||||
                                          (action, index) => html`
 | 
			
		||||
                                              <div
 | 
			
		||||
                                                  class="markdown-content"
 | 
			
		||||
                                                  data-markdown-id="action-${index}"
 | 
			
		||||
                                                  data-original-text="${action}"
 | 
			
		||||
                                                  @click=${() => this.handleMarkdownClick(action)}
 | 
			
		||||
                                              >
 | 
			
		||||
                                                  ${action}
 | 
			
		||||
                                              </div>
 | 
			
		||||
                                          `
 | 
			
		||||
                                      )}
 | 
			
		||||
                              `
 | 
			
		||||
                            : ''}
 | 
			
		||||
                        ${this.hasCompletedRecording && data.followUps && data.followUps.length > 0
 | 
			
		||||
                            ? html`
 | 
			
		||||
                                  <insights-title>Follow-Ups</insights-title>
 | 
			
		||||
                                  ${data.followUps.map(
 | 
			
		||||
                                      (followUp, index) => html`
 | 
			
		||||
                                          <div
 | 
			
		||||
                                              class="markdown-content"
 | 
			
		||||
                                              data-markdown-id="followup-${index}"
 | 
			
		||||
                                              data-original-text="${followUp}"
 | 
			
		||||
                                              @click=${() => this.handleMarkdownClick(followUp)}
 | 
			
		||||
                                          >
 | 
			
		||||
                                              ${followUp}
 | 
			
		||||
                                          </div>
 | 
			
		||||
                                      `
 | 
			
		||||
                                  )}
 | 
			
		||||
                              `
 | 
			
		||||
                            : ''}
 | 
			
		||||
                    `}
 | 
			
		||||
            </div>
 | 
			
		||||
        `;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
customElements.define('summary-view', SummaryView); 
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user