listen feature refactor done

This commit is contained in:
samtiz 2025-07-07 06:28:48 +09:00
parent 80a3c01656
commit aa9252880b
5 changed files with 856 additions and 831 deletions

View File

@ -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>
`;
}

View File

@ -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),
};

View File

@ -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
};

View 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);

View 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);