feat(frontend): Bridge-based UI — auto-load sessions, cascade content display, Bridge send
This commit is contained in:
161
public/js/app.js
161
public/js/app.js
@@ -49,6 +49,8 @@
|
|||||||
clearTimeout(reconnectTimer);
|
clearTimeout(reconnectTimer);
|
||||||
reconnectTimer = null;
|
reconnectTimer = null;
|
||||||
}
|
}
|
||||||
|
// Bridge 세션 자동 로드
|
||||||
|
loadBridgeSessions();
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onclose = () => {
|
ws.onclose = () => {
|
||||||
@@ -84,11 +86,97 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Bridge 세션 관리 ──────────────────────────────────
|
||||||
|
let bridgeSessions = []; // Bridge에서 가져온 대화 목록
|
||||||
|
let activeBridgeSession = null;
|
||||||
|
|
||||||
|
async function loadBridgeSessions() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/bridge/sessions');
|
||||||
|
if (!res.ok) return;
|
||||||
|
const data = await res.json();
|
||||||
|
bridgeSessions = (data.sessions || []).map(s => ({
|
||||||
|
id: s.id,
|
||||||
|
name: s.title || s.id.substring(0, 8),
|
||||||
|
host: 'bridge',
|
||||||
|
cdpPort: 0,
|
||||||
|
status: 'connected',
|
||||||
|
title: s.title,
|
||||||
|
stepCount: s.stepCount,
|
||||||
|
isBridge: true,
|
||||||
|
}));
|
||||||
|
sessionPanel.update(bridgeSessions);
|
||||||
|
// 활성 세션 없으면 가장 최근 대화 자동 선택
|
||||||
|
if (!sessionPanel.activeSessionId && bridgeSessions.length > 0) {
|
||||||
|
selectBridgeSession(bridgeSessions[0].id);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[Bridge] 세션 로드 실패:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectBridgeSession(sessionId) {
|
||||||
|
activeBridgeSession = sessionId;
|
||||||
|
sessionPanel.setActive(sessionId);
|
||||||
|
const session = bridgeSessions.find(s => s.id === sessionId);
|
||||||
|
if (session) {
|
||||||
|
chatPanel.showSession({ name: session.name, status: 'connected' });
|
||||||
|
}
|
||||||
|
// cascades에서 대화 내용 가져오기
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/bridge/cascades');
|
||||||
|
if (!res.ok) return;
|
||||||
|
const data = await res.json();
|
||||||
|
const cascadeData = data.cascades || data;
|
||||||
|
// 선택된 세션의 데이터 찾기
|
||||||
|
const cascade = cascadeData[sessionId];
|
||||||
|
if (cascade) {
|
||||||
|
const messages = parseCascadeToMessages(cascade);
|
||||||
|
chatPanel.updateChat(messages);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[Bridge] cascade 로드 실패:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cascade 데이터를 ChatPanel이 이해하는 message[] 형식으로 변환
|
||||||
|
*/
|
||||||
|
function parseCascadeToMessages(cascade) {
|
||||||
|
const msgs = [];
|
||||||
|
// latestNotifyUserStep — 마지막 AI 응답
|
||||||
|
if (cascade.latestNotifyUserStep?.step?.notifyUser?.notificationContent) {
|
||||||
|
msgs.push({
|
||||||
|
type: 'text',
|
||||||
|
html: cascade.latestNotifyUserStep.step.notifyUser.notificationContent
|
||||||
|
.replace(/\n/g, '<br>')
|
||||||
|
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
||||||
|
.replace(/`([^`]+)`/g, '<code>$1</code>'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// latestTaskBoundaryStep — 현재 작업 상태
|
||||||
|
if (cascade.latestTaskBoundaryStep?.step?.taskBoundary) {
|
||||||
|
const tb = cascade.latestTaskBoundaryStep.step.taskBoundary;
|
||||||
|
msgs.push({
|
||||||
|
type: 'task',
|
||||||
|
title: tb.taskName || '',
|
||||||
|
summary: tb.taskSummary || '',
|
||||||
|
status: tb.taskStatus || '',
|
||||||
|
mode: tb.mode || '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// summary
|
||||||
|
if (cascade.summary && msgs.length === 0) {
|
||||||
|
msgs.push({ type: 'text', html: `<p>${cascade.summary}</p>` });
|
||||||
|
}
|
||||||
|
return msgs;
|
||||||
|
}
|
||||||
|
|
||||||
// ─── 서버 메시지 핸들러 ───────────────────────────────
|
// ─── 서버 메시지 핸들러 ───────────────────────────────
|
||||||
function handleMessage(msg) {
|
function handleMessage(msg) {
|
||||||
switch (msg.type) {
|
switch (msg.type) {
|
||||||
case 'sessions_list':
|
case 'sessions_list':
|
||||||
sessionPanel.update(msg.sessions);
|
// CDP 세션 (레거시) — Bridge 세션과 병합하지 않음
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'session_switched':
|
case 'session_switched':
|
||||||
@@ -108,6 +196,10 @@
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'bridge_event':
|
||||||
|
handleBridgeEvent(msg);
|
||||||
|
break;
|
||||||
|
|
||||||
case 'screenshot':
|
case 'screenshot':
|
||||||
screenshotImage.src = `data:image/jpeg;base64,${msg.data}`;
|
screenshotImage.src = `data:image/jpeg;base64,${msg.data}`;
|
||||||
screenshotOverlay.style.display = 'flex';
|
screenshotOverlay.style.display = 'flex';
|
||||||
@@ -120,44 +212,35 @@
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'screencast_started':
|
|
||||||
if (!msg.success) {
|
|
||||||
showToast('Screencast 시작 실패', 'error');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'screencast_stopped':
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'action_clicked':
|
|
||||||
// 서버에서의 확인 응답
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'auto_discovered':
|
|
||||||
showToast(`Antigravity 자동 감지: ${msg.session.name}`, 'success');
|
|
||||||
// 활성 세션이 없으면 자동 전환
|
|
||||||
if (!sessionPanel.activeSessionId) {
|
|
||||||
sendWs({ type: 'switch_session', sessionId: msg.session.id });
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'session_lost':
|
|
||||||
showToast('Antigravity 연결 소실', 'warning');
|
|
||||||
if (sessionPanel.activeSessionId === msg.sessionId) {
|
|
||||||
chatPanel.showEmpty();
|
|
||||||
sessionPanel.setActive(null);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'error':
|
case 'error':
|
||||||
showToast(msg.message, 'error');
|
showToast(msg.message, 'error');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleBridgeEvent(msg) {
|
||||||
|
switch (msg.type) {
|
||||||
|
case 'bridge_event':
|
||||||
|
// step_changed → 활성 대화 갱신
|
||||||
|
if (msg.step_changed || msg.sessions) {
|
||||||
|
loadBridgeSessions();
|
||||||
|
}
|
||||||
|
if (msg.new_conversation) {
|
||||||
|
loadBridgeSessions();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ─── 세션 패널 이벤트 ─────────────────────────────────
|
// ─── 세션 패널 이벤트 ─────────────────────────────────
|
||||||
sessionPanel.onSessionSelect = (sessionId) => {
|
sessionPanel.onSessionSelect = (sessionId) => {
|
||||||
sendWs({ type: 'switch_session', sessionId });
|
// Bridge 세션인지 확인
|
||||||
|
const isBridge = bridgeSessions.some(s => s.id === sessionId);
|
||||||
|
if (isBridge) {
|
||||||
|
selectBridgeSession(sessionId);
|
||||||
|
} else {
|
||||||
|
sendWs({ type: 'switch_session', sessionId });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
sessionPanel.onSessionRemove = async (sessionId) => {
|
sessionPanel.onSessionRemove = async (sessionId) => {
|
||||||
@@ -176,7 +259,21 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ─── 채팅 패널 이벤트 ─────────────────────────────────
|
// ─── 채팅 패널 이벤트 ─────────────────────────────────
|
||||||
chatPanel.onSendMessage = (text) => {
|
chatPanel.onSendMessage = async (text) => {
|
||||||
|
if (!sessionPanel.activeSessionId) return;
|
||||||
|
// Bridge 세션이면 Bridge API로 전송
|
||||||
|
const isBridge = bridgeSessions.some(s => s.id === sessionPanel.activeSessionId);
|
||||||
|
if (isBridge) {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/bridge/send', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ message: text, sessionId: sessionPanel.activeSessionId }),
|
||||||
|
});
|
||||||
|
if (!res.ok) showToast('전송 실패', 'error');
|
||||||
|
} catch { showToast('전송 실패', 'error'); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
sendWs({
|
sendWs({
|
||||||
type: 'send_message',
|
type: 'send_message',
|
||||||
sessionId: sessionPanel.activeSessionId,
|
sessionId: sessionPanel.activeSessionId,
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ class SessionPanel {
|
|||||||
<div class="session-indicator ${s.status}"></div>
|
<div class="session-indicator ${s.status}"></div>
|
||||||
<div class="session-info">
|
<div class="session-info">
|
||||||
<div class="session-name">${this._escapeHtml(s.name)}</div>
|
<div class="session-name">${this._escapeHtml(s.name)}</div>
|
||||||
<div class="session-detail">${s.host}:${s.cdpPort} · ${this._statusText(s.status)}</div>
|
<div class="session-detail">${s.isBridge ? `${s.stepCount || 0} steps · 대화` : `${s.host}:${s.cdpPort} · ${this._statusText(s.status)}`}</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="session-remove" data-remove-id="${s.id}" title="세션 제거">✕</button>
|
<button class="session-remove" data-remove-id="${s.id}" title="세션 제거">✕</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user