diff --git a/public/css/style.css b/public/css/style.css index e8edf03..83f7f2a 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -44,9 +44,16 @@ } /* ─── Reset ──────────────────────────────────────────── */ -*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} -html, body { +html, +body { height: 100%; font-family: var(--font-sans); font-size: 14px; @@ -57,13 +64,22 @@ html, body { } /* ─── Scrollbar ──────────────────────────────────────── */ -::-webkit-scrollbar { width: 6px; } -::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar { + width: 6px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + ::-webkit-scrollbar-thumb { background: var(--bg-active); border-radius: 3px; } -::-webkit-scrollbar-thumb:hover { background: var(--text-muted); } + +::-webkit-scrollbar-thumb:hover { + background: var(--text-muted); +} /* ─── Header ─────────────────────────────────────────── */ .header { @@ -77,7 +93,11 @@ html, body { z-index: 100; } -.header-left { display: flex; align-items: center; gap: 12px; } +.header-left { + display: flex; + align-items: center; + gap: 12px; +} .logo { display: flex; @@ -94,7 +114,11 @@ html, body { -webkit-text-fill-color: transparent; } -.header-right { display: flex; align-items: center; gap: 16px; } +.header-right { + display: flex; + align-items: center; + gap: 16px; +} .connection-status { display: flex; @@ -112,8 +136,14 @@ html, body { transition: background var(--transition-normal); } -.status-dot.connected { background: var(--success); box-shadow: 0 0 6px var(--success); } -.status-dot.error { background: var(--error); } +.status-dot.connected { + background: var(--success); + box-shadow: 0 0 6px var(--success); +} + +.status-dot.error { + background: var(--error); +} /* ─── Main Layout ────────────────────────────────────── */ .main-layout { @@ -181,14 +211,34 @@ html, body { flex-shrink: 0; } -.session-indicator.connected { background: var(--success); box-shadow: 0 0 4px rgba(52, 211, 153, 0.4); } -.session-indicator.connecting { background: var(--warning); animation: pulse 1.5s infinite; } -.session-indicator.disconnected { background: var(--text-muted); } -.session-indicator.error { background: var(--error); } +.session-indicator.connected { + background: var(--success); + box-shadow: 0 0 4px rgba(52, 211, 153, 0.4); +} + +.session-indicator.connecting { + background: var(--warning); + animation: pulse 1.5s infinite; +} + +.session-indicator.disconnected { + background: var(--text-muted); +} + +.session-indicator.error { + background: var(--error); +} @keyframes pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.4; } + + 0%, + 100% { + opacity: 1; + } + + 50% { + opacity: 0.4; + } } .session-info { @@ -223,8 +273,14 @@ html, body { font-size: 14px; } -.session-card:hover .session-remove { opacity: 1; } -.session-remove:hover { color: var(--error); background: rgba(248, 113, 113, 0.1); } +.session-card:hover .session-remove { + opacity: 1; +} + +.session-remove:hover { + color: var(--error); + background: rgba(248, 113, 113, 0.1); +} /* ─── Chat Area ──────────────────────────────────────── */ .chat-area { @@ -317,32 +373,265 @@ html, body { font-size: 13px; } -/* Antigravity 에서 가져온 HTML을 표시하는 컨테이너 */ -.chat-messages .ag-content { - font-family: var(--font-sans); - line-height: 1.6; +/* Antigravity 에서 가져온 구조화 메시지를 렌더링하는 컴포넌트 */ + +/* --- 메시지 공통 --- */ +.chat-messages>* { + margin-bottom: 8px; +} + +/* --- 작업 카드 (Task boundary) --- */ +.msg-card { + border: 1px solid var(--border-subtle); + border-radius: 10px; + overflow: hidden; + background: var(--bg-secondary); + transition: border-color var(--transition-fast); +} + +.msg-card:hover { + border-color: var(--border-medium); +} + +.msg-card-header { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + padding: 10px 14px; +} + +.msg-toggle { + font-size: 11px; + color: var(--text-muted); + width: 14px; + text-align: center; + flex-shrink: 0; + transition: color var(--transition-fast); +} + +.msg-card-header:hover .msg-toggle { color: var(--text-primary); } -.chat-messages .ag-content pre, -.chat-messages .ag-content code { - font-family: var(--font-mono); +.msg-card-title { font-size: 13px; + font-weight: 600; + color: var(--text-primary); } -.chat-messages .ag-content pre { +.msg-card-summary { + width: 100%; + font-size: 12px; + color: var(--text-secondary); + line-height: 1.5; + padding-left: 22px; +} + +.msg-card-body { + border-top: 1px solid var(--border-subtle); + padding: 8px 14px 10px; +} + +.msg-step { + display: flex; + align-items: flex-start; + gap: 8px; + padding: 3px 0; + font-size: 12px; + color: var(--text-secondary); + line-height: 1.4; +} + +.msg-step-icon { + width: 16px; + text-align: center; + flex-shrink: 0; + font-size: 11px; +} + +.msg-step-text { + flex: 1; + min-width: 0; + word-break: break-word; +} + +/* --- 텍스트 메시지 --- */ +.msg-text { + font-size: 13px; + line-height: 1.6; + color: var(--text-primary); + padding: 4px 0; + word-break: break-word; +} + +.msg-text p { + margin: 4px 0; +} + +.msg-text ul, +.msg-text ol { + padding-left: 18px; + margin: 4px 0; +} + +.msg-text li { + margin: 2px 0; +} + +.msg-text strong { + font-weight: 600; +} + +.msg-text a { + color: var(--text-accent); + text-decoration: none; +} + +.msg-text a:hover { + text-decoration: underline; +} + +.msg-text code:not(pre code) { + font-family: var(--font-mono); + font-size: 12px; + background: var(--bg-tertiary); + padding: 1px 5px; + border-radius: 4px; + color: var(--text-accent); +} + +.msg-text pre { background: var(--bg-tertiary); border: 1px solid var(--border-subtle); border-radius: 8px; - padding: 14px; + padding: 10px 12px; overflow-x: auto; - margin: 10px 0; + margin: 6px 0; + font-family: var(--font-mono); + font-size: 12px; + line-height: 1.5; } -.chat-messages .ag-content code { +.msg-text img { + max-width: 320px; + max-height: 200px; + border-radius: 8px; + margin: 4px 0; +} + +/* --- Thought Process --- */ +.msg-thought { + margin: 4px 0; +} + +.msg-thought-btn { + display: flex; + align-items: center; + gap: 6px; + width: 100%; + padding: 6px 10px; + background: none; + border: none; + border-radius: 6px; + color: var(--text-muted); + font-family: var(--font-sans); + font-size: 12px; + cursor: pointer; + transition: all var(--transition-fast); + text-align: left; +} + +.msg-thought-btn:hover { + background: var(--bg-hover); + color: var(--text-secondary); +} + +.msg-thought-icon { + font-size: 14px; +} + +/* --- 코드 블록 --- */ +.msg-code-block { + border: 1px solid var(--border-subtle); + border-radius: 8px; + overflow: hidden; + margin: 4px 0; +} + +.msg-code-lang { + padding: 4px 12px; background: var(--bg-tertiary); - padding: 2px 6px; - border-radius: 4px; + border-bottom: 1px solid var(--border-subtle); + font-family: var(--font-mono); + font-size: 11px; + color: var(--text-muted); + text-transform: lowercase; +} + +.msg-code-block pre { + margin: 0; + padding: 10px 12px; + background: var(--bg-primary); + overflow-x: auto; + font-size: 12px; + line-height: 1.5; +} + +.msg-code-block code { + font-family: var(--font-mono); + font-size: 12px; + color: var(--text-primary); +} + +/* --- 이미지 --- */ +.msg-image { + margin: 4px 0; +} + +.msg-image img { + max-width: 320px; + max-height: 200px; + border-radius: 8px; + border: 1px solid var(--border-subtle); + object-fit: contain; +} + +/* --- 액션 버튼 --- */ +.msg-actions { + display: flex; + gap: 6px; + padding: 4px 0; + flex-wrap: wrap; +} + +.msg-action-btn { + padding: 4px 12px; + background: var(--bg-tertiary); + border: 1px solid var(--border-subtle); + border-radius: 6px; + color: var(--text-secondary); + font-family: var(--font-sans); + font-size: 12px; + cursor: default; + transition: all var(--transition-fast); +} + +.msg-action-btn[style*="cursor: pointer"]:hover { + background: var(--bg-hover); + border-color: var(--border-medium); + color: var(--text-primary); +} + +.msg-action-btn.msg-action-primary { + background: var(--accent-primary); + color: white; + border-color: var(--accent-primary); +} + +.msg-action-btn.msg-action-primary:hover { + background: var(--accent-secondary); + transform: scale(1.02); } /* Chat Input */ @@ -371,8 +660,13 @@ html, body { transition: border-color var(--transition-fast); } -.chat-input-area textarea::placeholder { color: var(--text-muted); } -.chat-input-area textarea:focus { border-color: var(--accent-primary); } +.chat-input-area textarea::placeholder { + color: var(--text-muted); +} + +.chat-input-area textarea:focus { + border-color: var(--accent-primary); +} /* ─── Buttons ────────────────────────────────────────── */ .btn-icon { @@ -430,7 +724,9 @@ html, body { transform: scale(1.05); } -.btn-send:active { transform: scale(0.95); } +.btn-send:active { + transform: scale(0.95); +} .btn { padding: 8px 16px; @@ -448,7 +744,9 @@ html, body { color: white; } -.btn-primary:hover { background: var(--accent-secondary); } +.btn-primary:hover { + background: var(--accent-secondary); +} .btn-secondary { background: var(--bg-tertiary); @@ -456,7 +754,9 @@ html, body { border: 1px solid var(--border-subtle); } -.btn-secondary:hover { background: var(--bg-hover); } +.btn-secondary:hover { + background: var(--bg-hover); +} /* ─── Modal ──────────────────────────────────────────── */ .modal-backdrop { @@ -471,7 +771,15 @@ html, body { animation: fadeIn 0.15s ease; } -@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } +@keyframes fadeIn { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} .modal { background: var(--bg-elevated); @@ -483,7 +791,17 @@ html, body { animation: slideUp 0.2s ease; } -@keyframes slideUp { from { transform: translateY(20px); opacity: 0; } to { transform: none; opacity: 1; } } +@keyframes slideUp { + from { + transform: translateY(20px); + opacity: 0; + } + + to { + transform: none; + opacity: 1; + } +} .modal-header { display: flex; @@ -533,7 +851,9 @@ html, body { transition: border-color var(--transition-fast); } -.form-group input:focus { border-color: var(--accent-primary); } +.form-group input:focus { + border-color: var(--accent-primary); +} .form-hint { font-size: 11px; @@ -605,11 +925,32 @@ html, body { animation-fill-mode: forwards; } -@keyframes slideInRight { from { transform: translateX(100px); opacity: 0; } to { transform: none; opacity: 1; } } -@keyframes fadeOut { to { opacity: 0; transform: translateY(10px); } } +@keyframes slideInRight { + from { + transform: translateX(100px); + opacity: 0; + } -.toast.error { border-color: var(--error); } -.toast.success { border-color: var(--success); } + to { + transform: none; + opacity: 1; + } +} + +@keyframes fadeOut { + to { + opacity: 0; + transform: translateY(10px); + } +} + +.toast.error { + border-color: var(--error); +} + +.toast.success { + border-color: var(--success); +} /* ─── View Tabs (Chat ↔ Mirror) ──────────────────────── */ .view-tabs { @@ -677,5 +1018,4 @@ html, body { font-size: 14px; pointer-events: none; animation: pulse 1.5s infinite; -} - +} \ No newline at end of file diff --git a/public/js/app.js b/public/js/app.js index 54044a8..4103c13 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -98,7 +98,7 @@ case 'chat_update': if (msg.sessionId === sessionPanel.activeSessionId) { - chatPanel.updateChat(msg.html); + chatPanel.updateChat(msg.messages); } break; @@ -129,6 +129,10 @@ case 'screencast_stopped': break; + case 'action_clicked': + // 서버에서의 확인 응답 (토스트는 이미 프론트엔드에서 표시) + break; + case 'error': showToast(msg.message, 'error'); break; @@ -164,6 +168,17 @@ }); }; + chatPanel.onActionClick = (button) => { + sendWs({ + type: 'click_action', + sessionId: sessionPanel.activeSessionId, + label: button.label, + x: button.x, + y: button.y, + }); + showToast(`"${button.label}" 클릭`, 'success'); + }; + // ─── 미러 패널 이벤트 ───────────────────────────── mirrorPanel.onStartScreencast = (sessionId) => { sendWs({ type: 'start_screencast', sessionId }); diff --git a/public/js/chat-panel.js b/public/js/chat-panel.js index 2a8c6e6..fb70cfb 100644 --- a/public/js/chat-panel.js +++ b/public/js/chat-panel.js @@ -1,5 +1,5 @@ /** - * Chat Panel — 채팅 표시/입력 UI 관리 + * Chat Panel — 구조화된 메시지 렌더링 (Antigravity 스타일) */ class ChatPanel { @@ -13,22 +13,19 @@ class ChatPanel { this.sessionStatusEl = document.getElementById('chatSessionStatus'); this.onSendMessage = null; // callback(text) + this.onActionClick = null; // callback(button) - {label, x, y} this.activeSession = null; + this._lastHash = ''; this._setupInput(); } - /** - * 입력 이벤트 바인딩 - */ _setupInput() { - // 자동 높이 조절 this.inputEl.addEventListener('input', () => { this.inputEl.style.height = 'auto'; this.inputEl.style.height = Math.min(this.inputEl.scrollHeight, 120) + 'px'; }); - // Enter로 전송 (Shift+Enter는 줄바꿈) this.inputEl.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); @@ -36,47 +33,27 @@ class ChatPanel { } }); - this.sendBtn.addEventListener('click', () => { - this._sendMessage(); - }); + this.sendBtn.addEventListener('click', () => this._sendMessage()); } _sendMessage() { const text = this.inputEl.value.trim(); if (!text) return; - - if (this.onSendMessage) { - this.onSendMessage(text); - } - - // 입력창 초기화 + if (this.onSendMessage) this.onSendMessage(text); this.inputEl.value = ''; this.inputEl.style.height = 'auto'; } - /** - * 세션 선택 시 UI 표시 - */ showSession(session) { this.activeSession = session; this.emptyEl.style.display = 'none'; this.containerEl.style.display = 'flex'; - this.sessionNameEl.textContent = session.name; this.sessionStatusEl.textContent = session.status === 'connected' ? '● 연결됨' : session.status; - - this.messagesEl.innerHTML = ` -
채팅 데이터를 불러오는 중...
-채팅 데이터를 불러오는 중...
⚠️ 채팅 컨테이너를 찾을 수 없습니다.
- Antigravity에서 채팅을 시작해주세요.
⚠️ 채팅 데이터를 가져올 수 없습니다.
+ Antigravity에서 채팅을 시작해주세요.