fix(ui): smart display for long convos, action buttons for isBlocking, no middle gap
This commit is contained in:
@@ -489,6 +489,52 @@ body {
|
|||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- 액션 버튼 영역 --- */
|
||||||
|
.msg-actions {
|
||||||
|
background: rgba(239, 68, 68, 0.08);
|
||||||
|
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg-actions-label {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f87171;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg-action-btn {
|
||||||
|
background: var(--accent-primary);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 6px 14px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 6px;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg-action-btn:hover {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg-action-primary {
|
||||||
|
background: var(--accent-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- 상태 메시지 --- */
|
||||||
|
.msg-status {
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 8px 0;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
.msg-step-icon {
|
.msg-step-icon {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -149,41 +149,56 @@
|
|||||||
|
|
||||||
async function refreshTrajectory(sessionId, scrollToBottom = true) {
|
async function refreshTrajectory(sessionId, scrollToBottom = true) {
|
||||||
try {
|
try {
|
||||||
// 1) trajectory (앞부분 ~336개)
|
|
||||||
// 2) cascades (최신 상태: latestNotifyUserStep + latestTaskBoundaryStep)
|
|
||||||
const [trajRes, cascRes] = await Promise.all([
|
const [trajRes, cascRes] = await Promise.all([
|
||||||
fetch(`/api/bridge/trajectory/${sessionId}`),
|
fetch(`/api/bridge/trajectory/${sessionId}`),
|
||||||
fetch('/api/bridge/cascades'),
|
fetch('/api/bridge/cascades'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let messages = [];
|
let messages = [];
|
||||||
|
let isComplete = true; // trajectory가 전체를 포함하는지
|
||||||
|
|
||||||
// trajectory 파싱
|
// trajectory 파싱
|
||||||
if (trajRes.ok) {
|
if (trajRes.ok) {
|
||||||
const trajData = await trajRes.json();
|
const trajData = await trajRes.json();
|
||||||
if (trajData.trajectory?.steps) {
|
if (trajData.trajectory?.steps) {
|
||||||
messages = parseTrajectoryToMessages(trajData.trajectory.steps);
|
|
||||||
|
|
||||||
// trajectory가 전체가 아닌 경우 (numTotalSteps > steps.length)
|
|
||||||
const total = trajData.numTotalSteps || 0;
|
const total = trajData.numTotalSteps || 0;
|
||||||
const got = trajData.trajectory.steps.length;
|
const got = trajData.trajectory.steps.length;
|
||||||
if (total > got) {
|
isComplete = (total <= got);
|
||||||
messages.push({
|
|
||||||
type: 'status',
|
if (isComplete) {
|
||||||
text: `⋯ ${total - got}개 스텝 생략 ⋯`,
|
// ≤336 스텝: 전체 표시 가능
|
||||||
});
|
messages = parseTrajectoryToMessages(trajData.trajectory.steps);
|
||||||
}
|
}
|
||||||
|
// > 336 스텝: trajectory 건너뛰고 cascades 중심으로 표시
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// cascades에서 최신 상태 보강
|
// cascades에서 최신 상태
|
||||||
if (cascRes.ok) {
|
if (cascRes.ok) {
|
||||||
const cascData = await cascRes.json();
|
const cascData = await cascRes.json();
|
||||||
const cascade = (cascData.cascades || cascData)[sessionId];
|
const cascade = (cascData.cascades || cascData)[sessionId];
|
||||||
if (cascade) {
|
if (cascade) {
|
||||||
// latestTaskBoundaryStep → 현재 작업 상태
|
// 긴 대화일 때: cascades 기반 표시
|
||||||
|
if (!isComplete) {
|
||||||
|
// 대화 요약
|
||||||
|
if (cascade.summary) {
|
||||||
|
messages.push({
|
||||||
|
type: 'status',
|
||||||
|
text: `💬 대화 요약: ${cascade.summary}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
messages.push({
|
||||||
|
type: 'status',
|
||||||
|
text: `📊 총 ${cascade.stepCount || '?'}개 스텝 · 최종 입력: ${formatRelativeTime(cascade.lastModifiedTime)}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 현재 Task 상태 (항상 최신)
|
||||||
if (cascade.latestTaskBoundaryStep?.step?.taskBoundary) {
|
if (cascade.latestTaskBoundaryStep?.step?.taskBoundary) {
|
||||||
const tb = cascade.latestTaskBoundaryStep.step.taskBoundary;
|
const tb = cascade.latestTaskBoundaryStep.step.taskBoundary;
|
||||||
|
// 이미 trajectory에서 같은 task가 없는 경우만 추가
|
||||||
|
const lastTask = messages.filter(m => m.type === 'task').pop();
|
||||||
|
if (!lastTask || lastTask.title !== tb.taskName) {
|
||||||
messages.push({
|
messages.push({
|
||||||
type: 'task',
|
type: 'task',
|
||||||
title: tb.taskName || '',
|
title: tb.taskName || '',
|
||||||
@@ -193,15 +208,47 @@
|
|||||||
tools: [],
|
tools: [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// latestNotifyUserStep → 마지막 AI 응답
|
}
|
||||||
if (cascade.latestNotifyUserStep?.step?.notifyUser?.notificationContent) {
|
|
||||||
const content = cascade.latestNotifyUserStep.step.notifyUser.notificationContent;
|
// 최신 AI 응답 (항상 표시)
|
||||||
|
if (cascade.latestNotifyUserStep?.step?.notifyUser) {
|
||||||
|
const nu = cascade.latestNotifyUserStep.step.notifyUser;
|
||||||
|
const content = nu.notificationContent || '';
|
||||||
|
if (content) {
|
||||||
|
// 이미 trajectory에서 같은 내용이 없는 경우만 추가
|
||||||
|
const lastText = messages.filter(m => m.type === 'text').pop();
|
||||||
|
const contentSnip = content.substring(0, 50);
|
||||||
|
if (!lastText || !lastText.html?.includes(contentSnip.replace(/[<>&]/g, ''))) {
|
||||||
messages.push({
|
messages.push({
|
||||||
type: 'text',
|
type: 'text',
|
||||||
html: simpleMarkdown(content),
|
html: simpleMarkdown(content),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🔴 사용자 행동 필요 (isBlocking)
|
||||||
|
if (nu.isBlocking) {
|
||||||
|
messages.push({
|
||||||
|
type: 'actions',
|
||||||
|
label: '⚠️ Antigravity에서 리뷰가 필요합니다',
|
||||||
|
buttons: [
|
||||||
|
{ label: '미러 탭에서 확인', action: 'switch_mirror' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 실행 중 상태 표시
|
||||||
|
if (cascade.status === 'CASCADE_RUN_STATUS_RUNNING') {
|
||||||
|
const lastMsg = messages[messages.length - 1];
|
||||||
|
if (!lastMsg || lastMsg.type !== 'actions') {
|
||||||
|
messages.push({
|
||||||
|
type: 'status',
|
||||||
|
text: '🔄 AI가 작업 중...',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
chatPanel.updateChat(messages);
|
chatPanel.updateChat(messages);
|
||||||
@@ -214,6 +261,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatRelativeTime(isoStr) {
|
||||||
|
if (!isoStr) return '';
|
||||||
|
const diff = Date.now() - new Date(isoStr).getTime();
|
||||||
|
const mins = Math.floor(diff / 60000);
|
||||||
|
if (mins < 1) return '방금 전';
|
||||||
|
if (mins < 60) return `${mins}분 전`;
|
||||||
|
const hrs = Math.floor(mins / 60);
|
||||||
|
if (hrs < 24) return `${hrs}시간 전`;
|
||||||
|
return `${Math.floor(hrs / 24)}일 전`;
|
||||||
|
}
|
||||||
|
|
||||||
// 실시간 갱신 디바운스
|
// 실시간 갱신 디바운스
|
||||||
let refreshTimer = null;
|
let refreshTimer = null;
|
||||||
function scheduleRefresh() {
|
function scheduleRefresh() {
|
||||||
|
|||||||
@@ -304,20 +304,26 @@ class ChatPanel {
|
|||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = 'msg-actions';
|
div.className = 'msg-actions';
|
||||||
|
|
||||||
for (const btn of (msg.buttons || [])) {
|
// 라벨(알림 문구) 표시
|
||||||
const el = document.createElement('button');
|
if (msg.label) {
|
||||||
el.className = 'msg-action-btn';
|
const labelEl = document.createElement('div');
|
||||||
el.textContent = btn.label || btn;
|
labelEl.className = 'msg-actions-label';
|
||||||
|
labelEl.textContent = msg.label;
|
||||||
// Proceed/Review 등 주요 액션은 강조
|
div.appendChild(labelEl);
|
||||||
const label = btn.label || btn;
|
|
||||||
if (['Proceed', 'Approve', 'Accept', 'Yes', 'Allow'].some(k => label.includes(k))) {
|
|
||||||
el.classList.add('msg-action-primary');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 좌표가 있으면 클릭 가능
|
for (const btn of (msg.buttons || [])) {
|
||||||
if (btn.x && btn.y) {
|
const el = document.createElement('button');
|
||||||
|
el.className = 'msg-action-btn msg-action-primary';
|
||||||
|
el.textContent = btn.label || btn;
|
||||||
el.style.cursor = 'pointer';
|
el.style.cursor = 'pointer';
|
||||||
|
|
||||||
|
// action 기반 처리
|
||||||
|
if (btn.action === 'switch_mirror') {
|
||||||
|
el.addEventListener('click', () => {
|
||||||
|
document.getElementById('tabMirror')?.click();
|
||||||
|
});
|
||||||
|
} else if (btn.x && btn.y) {
|
||||||
el.addEventListener('click', () => {
|
el.addEventListener('click', () => {
|
||||||
if (this.onActionClick) {
|
if (this.onActionClick) {
|
||||||
this.onActionClick({ label: btn.label, x: btn.x, y: btn.y });
|
this.onActionClick({ label: btn.label, x: btn.x, y: btn.y });
|
||||||
|
|||||||
Reference in New Issue
Block a user