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);
|
||||
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;
|
||||
|
||||
_stopDebugStream() {
|
||||
if (this._debug.interval) {
|
||||
clearInterval(this._debug.interval);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
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.isLibrariesLoaded = true;
|
||||
console.log('Markdown libraries loaded successfully');
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
if (this.DOMPurify) {
|
||||
this.isDOMPurifyLoaded = true;
|
||||
console.log('DOMPurify loaded successfully in AssistantView');
|
||||
if (wasActive && !isActive) {
|
||||
this.hasCompletedRecording = true;
|
||||
this.stopTimer();
|
||||
this.requestUpdate();
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.stopTimer();
|
||||
|
||||
handleMarkdownClick(originalText) {
|
||||
this.handleRequestClick(originalText);
|
||||
if (this.adjustHeightThrottle) {
|
||||
clearTimeout(this.adjustHeightThrottle);
|
||||
this.adjustHeightThrottle = null;
|
||||
}
|
||||
|
||||
renderMarkdownContent() {
|
||||
if (!this.isLibrariesLoaded || !this.marked) {
|
||||
return;
|
||||
if (this.copyTimeout) {
|
||||
clearTimeout(this.copyTimeout);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
if (changedProperties.has('viewMode')) {
|
||||
this.adjustWindowHeight();
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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