Files
dev-puppeteer/my-deepagent/static/style.css
chungyeong e326c07dcb feat(my-deepagent): v0.3 PR #8 — conversation-centric Web GUI (/conversation.html)
Workflow run 페이지를 archive 로 격하시키고, 사용자가 처음 보는 화면을
chat-style 대화 thread 로 전환.  Claude Code 의 Web GUI 와 동일한 UX.

핵심 동작:
- 새 페이지 `/conversation.html` 에서 세션을 picker 로 고르거나 "새 대화"
  버튼으로 만들고 메시지 입력.  Cmd/Ctrl+Enter 로 전송.
- POST /api/sessions/{id}/messages 가 user MessageRow 를 영속한 즉시 200 응답
  후 `asyncio.create_task(invoke_session_agent(...))` 로 백그라운드 invoke 발사.
- 백그라운드 task 는 lifespan 에서 1회 열어둔 LangGraph saver 를 재사용하고
  agent.ainvoke → assistant MessageRow 영속 → 자동 compaction 까지 처리.
- 기존 SSE 스트림 (`/api/sessions/{id}/stream`) 이 새 메시지를 push,
  프론트엔드의 `EventSource` 가 받아 thread 에 렌더.

신규 / 수정 파일:
- `static/conversation.html` (신규): chat UI 마크업.  data-page="conversation".
- `static/app.js`: 새 페이지 핸들러 `bootstrapConversationPage` +
  세션 picker + 메시지 thread 렌더 + SSE 구독 + Cmd/Ctrl+Enter 단축키.
  XSS 정책 동일: 모든 사용자 콘텐츠는 `textContent` 만 사용.
- `static/style.css`: `.messages-thread`, `.msg-bubble`, `.conv-topbar`,
  `.conv-input-bar` 등 chat UI 스타일.
- `api/app.py`: lifespan 에서 LangGraph saver 를 1회 열어 `app.state.saver`
  에 보관 (Postgres 일 때만).
- `api/agent_runner.py` (신규): `invoke_session_agent(...)` — REPL 의
  `InteractiveSession + _invoke_and_stream` 와 동일한 stack 을 HTTP background
  context 용으로 재구성.  실패는 로깅 + return.
- `api/routes/sessions.py`: POST /messages 가 background task 발사 + ref 를
  `app.state.pending_invocations` set 에 보관 (RUF006 / GC drop 방지).

테스트 (`tests/integration/test_conversation_gui.py`, 4 케이스):
- GET /conversation.html → 200 + 필수 마크업
- POST /messages → 200 + user row 영속 + 스텁 runner 호출 확인
- 백그라운드 task ref 가 `pending_invocations` 에 잡혀있고 완료 후 자동 discard
- 스텁 runner 가 assistant row 영속 → user + assistant 시퀀스 검증

게이트:
- ruff check / format --check / mypy: PASS
- pytest -q --ignore=tests/integration/test_e2e_workflow.py
  --ignore=tests/integration/test_openrouter_smoke.py: 675 passed (4 신규 포함)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 21:03:09 +09:00

965 lines
18 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* my-deepagent Web GUI — v0.2 PR #3 (Claude design pass).
*
* Visual reference: Anthropic / Claude design language
* - Warm cream paper background (#F5F0E8 family)
* - Charcoal warm-tinted ink for body
* - Tiempos / Source Serif headings (serif heads, sans body)
* - Rust / terracotta accent (#CC5500 family)
* - Generous whitespace, soft 1px borders, minimal shadow
* - Rounded corners 612 px, no heavy elevation
*
* Vanilla CSS only. No framework, no build system.
*/
/* ---------- Reset + tokens ---------- */
*,
*::before,
*::after {
box-sizing: border-box;
}
:root {
/* Paper / surface */
--bg: #f5f0e8;
--surface-1: #fbfaf6;
--surface-2: #efe9de;
--surface-3: #e7e0d3;
--surface-hover: #eee7da;
/* Borders */
--border: #e2dccf;
--border-strong: #cfc7b6;
/* Ink */
--text-primary: #1b1916;
--text-secondary: #54514a;
--text-muted: #8a857a;
--text-faint: #b6b0a3;
/* Accents (rust + supporting palette) */
--accent: #c14a1a;
--accent-hover: #a73d12;
--accent-bg: rgba(193, 74, 26, 0.1);
--success: #4a6f2a;
--success-bg: rgba(74, 111, 42, 0.1);
--warning: #a86c1f;
--warning-bg: rgba(168, 108, 31, 0.12);
--danger: #a33419;
--danger-bg: rgba(163, 52, 25, 0.1);
--info: #4a5d8c;
--info-bg: rgba(74, 93, 140, 0.1);
/* Type */
--font-serif: "Tiempos Headline", "Source Serif Pro", "IBM Plex Serif",
"Georgia", "Apple SD Gothic Neo", "Noto Serif KR", serif;
--font-sans: "Styrene B", "Styrene A", "Inter", "Pretendard",
-apple-system, BlinkMacSystemFont, "Apple SD Gothic Neo",
"Noto Sans KR", "Segoe UI", Helvetica, Arial, sans-serif;
--font-mono: "JetBrains Mono", "SF Mono", "Menlo", "Monaco", "Consolas",
monospace;
/* Geometry */
--radius-sm: 6px;
--radius: 8px;
--radius-lg: 12px;
--shadow-card: 0 1px 0 rgba(28, 25, 22, 0.04);
}
/* ---------- Base ---------- */
html,
body {
margin: 0;
padding: 0;
background: var(--bg);
color: var(--text-primary);
font-family: var(--font-sans);
font-size: 15px;
line-height: 1.55;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-variant-numeric: tabular-nums;
}
body {
min-height: 100vh;
display: flex;
flex-direction: column;
}
a {
color: var(--accent);
text-decoration: none;
transition: color 0.15s ease;
}
a:hover {
color: var(--accent-hover);
text-decoration: underline;
text-decoration-thickness: 1px;
text-underline-offset: 3px;
}
code,
kbd,
.mono {
font-family: var(--font-mono);
font-size: 0.86em;
letter-spacing: -0.005em;
}
::selection {
background: var(--accent);
color: var(--surface-1);
}
/* ---------- Header / nav ---------- */
header {
background: transparent;
border-bottom: 1px solid var(--border);
padding: 20px 36px;
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 24px;
}
header h1 {
margin: 0;
font-family: var(--font-serif);
font-size: 22px;
font-weight: 500;
letter-spacing: -0.02em;
color: var(--text-primary);
}
header h1 a {
color: inherit;
}
header h1 a:hover {
text-decoration: none;
color: var(--accent);
}
header nav {
display: flex;
gap: 4px;
align-self: center;
}
header nav a {
color: var(--text-secondary);
padding: 6px 12px;
border-radius: var(--radius-sm);
font-size: 14px;
font-weight: 400;
transition: all 0.15s ease;
}
header nav a:hover {
color: var(--text-primary);
background: var(--surface-2);
text-decoration: none;
}
header nav a.active {
color: var(--accent);
background: var(--accent-bg);
}
/* ---------- Main ---------- */
main {
flex: 1;
max-width: 1180px;
width: 100%;
margin: 0 auto;
padding: 40px 36px 64px;
}
.page-title {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 16px;
margin-bottom: 28px;
border-bottom: 1px solid var(--border);
padding-bottom: 16px;
}
.page-title h2 {
margin: 0;
font-family: var(--font-serif);
font-size: 30px;
font-weight: 500;
letter-spacing: -0.02em;
text-transform: none;
color: var(--text-primary);
}
.page-subtitle {
color: var(--text-muted);
font-size: 14px;
}
h2 {
margin: 36px 0 14px;
font-family: var(--font-serif);
font-size: 18px;
font-weight: 500;
letter-spacing: -0.01em;
color: var(--text-primary);
text-transform: none;
}
h2.section-title {
display: flex;
align-items: baseline;
gap: 10px;
}
h2.section-title .hint {
font-family: var(--font-sans);
font-size: 13px;
color: var(--text-muted);
font-weight: 400;
margin-left: 4px;
}
/* ---------- Cards / tables ---------- */
.card {
background: var(--surface-1);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow-card);
overflow: hidden;
}
table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
font-size: 14px;
}
th,
td {
text-align: left;
padding: 14px 18px;
border-bottom: 1px solid var(--border);
vertical-align: middle;
}
tbody tr:last-child td {
border-bottom: none;
}
tbody tr {
transition: background 0.12s ease;
}
tbody tr:hover {
background: var(--surface-2);
}
th {
background: var(--surface-2);
color: var(--text-secondary);
font-weight: 500;
font-size: 12px;
letter-spacing: 0.03em;
text-transform: uppercase;
padding-top: 11px;
padding-bottom: 11px;
border-bottom: 1px solid var(--border-strong);
}
td .mono {
color: var(--text-secondary);
}
td a {
font-weight: 500;
}
/* ---------- State badges (pill) ---------- */
.badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 3px 11px;
border-radius: 999px;
font-size: 12px;
font-weight: 500;
letter-spacing: 0.01em;
border: 1px solid transparent;
font-variant-numeric: tabular-nums;
}
.badge::before {
content: "";
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
flex-shrink: 0;
}
.badge.state-completed,
.badge.state-ok {
color: var(--success);
background: var(--success-bg);
border-color: rgba(74, 111, 42, 0.2);
}
.badge.state-running,
.badge.state-executing,
.badge.state-validating,
.badge.state-awaiting_artifact,
.badge.state-awaiting_approval {
color: var(--warning);
background: var(--warning-bg);
border-color: rgba(168, 108, 31, 0.22);
}
.badge.state-failed,
.badge.state-aborted {
color: var(--danger);
background: var(--danger-bg);
border-color: rgba(163, 52, 25, 0.2);
}
.badge.state-pending,
.badge.state-created,
.badge.state-bound,
.badge.state-planning,
.badge.state-paused,
.badge.state-skipped {
color: var(--info);
background: var(--info-bg);
border-color: rgba(74, 93, 140, 0.2);
}
.badge.state-running::before,
.badge.state-executing::before,
.badge.state-validating::before,
.badge.state-awaiting_artifact::before {
animation: pulse 1.6s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.45; transform: scale(0.85); }
}
/* ---------- Buttons ---------- */
button,
.button {
appearance: none;
background: var(--surface-1);
color: var(--text-primary);
border: 1px solid var(--border-strong);
border-radius: var(--radius-sm);
padding: 8px 16px;
font-family: inherit;
font-size: 13.5px;
font-weight: 500;
letter-spacing: -0.005em;
cursor: pointer;
transition: all 0.15s ease;
display: inline-flex;
align-items: center;
gap: 6px;
text-decoration: none;
}
button:hover:not(:disabled),
.button:hover {
background: var(--surface-hover);
border-color: var(--text-faint);
color: var(--text-primary);
text-decoration: none;
}
button:active:not(:disabled) {
transform: translateY(0.5px);
}
button:disabled {
opacity: 0.4;
cursor: not-allowed;
}
button.primary {
background: var(--accent);
border-color: var(--accent);
color: var(--surface-1);
font-weight: 500;
}
button.primary:hover:not(:disabled) {
background: var(--accent-hover);
border-color: var(--accent-hover);
color: var(--surface-1);
}
button.danger {
background: transparent;
color: var(--danger);
border-color: rgba(163, 52, 25, 0.32);
}
button.danger:hover:not(:disabled) {
background: var(--danger-bg);
border-color: var(--danger);
}
/* ---------- Forms ---------- */
label {
display: block;
margin: 0 0 6px;
color: var(--text-secondary);
font-size: 13px;
font-weight: 500;
}
label .hint {
color: var(--text-muted);
font-weight: 400;
margin-left: 6px;
font-size: 12.5px;
}
input[type="text"],
input[type="number"],
textarea,
select {
width: 100%;
background: var(--surface-1);
color: var(--text-primary);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 10px 13px;
font-family: inherit;
font-size: 14px;
line-height: 1.5;
transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
}
input:focus,
textarea:focus,
select:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-bg);
}
input::placeholder,
textarea::placeholder {
color: var(--text-faint);
}
textarea {
resize: vertical;
min-height: 110px;
font-family: var(--font-mono);
font-size: 13px;
line-height: 1.6;
}
select {
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg width='10' height='6' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%2354514a' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 14px center;
padding-right: 36px;
}
.form-row {
margin-bottom: 18px;
}
.form-grid {
display: grid;
grid-template-columns: 1fr 220px;
gap: 16px;
}
.action-bar {
display: flex;
gap: 8px;
margin: 28px 0 0;
padding-top: 18px;
border-top: 1px solid var(--border);
}
.action-bar.no-top-border {
border-top: none;
padding-top: 0;
}
/* ---------- Meta panel (key/value lists) ---------- */
.meta-panel {
background: var(--surface-1);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 4px 0;
box-shadow: var(--shadow-card);
}
.meta-row {
display: grid;
grid-template-columns: 150px 1fr;
gap: 18px;
padding: 12px 18px;
border-bottom: 1px solid var(--border);
align-items: center;
}
.meta-row:last-child {
border-bottom: none;
}
.meta-row .key {
color: var(--text-muted);
font-size: 12.5px;
font-weight: 500;
letter-spacing: 0.02em;
text-transform: uppercase;
}
.meta-row .value {
color: var(--text-primary);
font-size: 14px;
word-break: break-all;
}
.meta-row .value.mono {
font-family: var(--font-mono);
font-size: 12.5px;
color: var(--text-secondary);
}
.meta-row .value.dim {
color: var(--text-muted);
}
/* ---------- Budget summary cards ---------- */
.budget-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 14px;
}
.budget-card {
background: var(--surface-1);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 18px;
box-shadow: var(--shadow-card);
}
.budget-card .scope {
color: var(--text-muted);
font-size: 12px;
font-weight: 500;
letter-spacing: 0.04em;
text-transform: uppercase;
margin-bottom: 8px;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.budget-card .amount {
font-family: var(--font-serif);
font-size: 26px;
font-weight: 500;
letter-spacing: -0.02em;
color: var(--text-primary);
}
.budget-card .amount.warn { color: var(--warning); }
.budget-card .amount.over { color: var(--danger); }
.budget-card .cap {
font-family: var(--font-sans);
color: var(--text-muted);
font-size: 13px;
font-weight: 400;
margin-left: 6px;
}
.budget-card .bar {
height: 4px;
background: var(--surface-3);
border-radius: 2px;
margin-top: 12px;
overflow: hidden;
}
.budget-card .bar > div {
height: 100%;
background: var(--success);
transition: width 0.3s ease;
}
.budget-card .bar.warn > div { background: var(--warning); }
.budget-card .bar.over > div { background: var(--danger); }
/* ---------- Event log (SSE) ---------- */
.events {
background: var(--surface-1);
border: 1px solid var(--border);
border-radius: var(--radius);
max-height: 60vh;
overflow-y: auto;
padding: 8px 0;
font-family: var(--font-mono);
font-size: 12.5px;
box-shadow: var(--shadow-card);
}
.event-line {
display: grid;
grid-template-columns: 80px 1fr;
gap: 14px;
padding: 6px 18px;
border-bottom: 1px solid transparent;
}
.event-line:hover {
background: var(--surface-2);
}
.event-line .ts {
color: var(--text-faint);
font-size: 11.5px;
}
.event-line .body .type {
color: var(--accent);
font-weight: 500;
}
.event-line .body .payload {
color: var(--text-muted);
margin-left: 8px;
font-size: 11.5px;
word-break: break-all;
}
.event-line.run-completed .body .type { color: var(--success); }
.event-line.run-failed .body .type,
.event-line.run-aborted .body .type { color: var(--danger); }
.event-line.run-resumed .body .type { color: var(--warning); }
.events::-webkit-scrollbar {
width: 10px;
}
.events::-webkit-scrollbar-track {
background: transparent;
}
.events::-webkit-scrollbar-thumb {
background: var(--border-strong);
border-radius: 5px;
border: 2px solid var(--surface-1);
}
/* ---------- Empty / error states ---------- */
.empty {
padding: 56px 24px;
text-align: center;
color: var(--text-muted);
font-size: 14px;
}
.empty .empty-icon {
font-family: var(--font-serif);
font-size: 30px;
margin-bottom: 10px;
opacity: 0.5;
color: var(--accent);
}
.empty .cta {
margin-top: 18px;
}
.error-banner {
background: var(--danger-bg);
border: 1px solid rgba(163, 52, 25, 0.28);
border-radius: var(--radius);
padding: 12px 16px;
margin-bottom: 16px;
color: var(--danger);
font-size: 13.5px;
display: flex;
align-items: center;
gap: 10px;
}
.error-banner::before {
content: "!";
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
background: var(--danger);
color: var(--surface-1);
border-radius: 50%;
font-weight: 700;
font-size: 11px;
flex-shrink: 0;
font-family: var(--font-sans);
}
/* ---------- Tag chip (per-role override input) ---------- */
.chips {
display: grid;
grid-template-columns: 160px 1fr;
gap: 14px;
padding: 14px 18px;
border-bottom: 1px solid var(--border);
align-items: center;
}
.chips:last-child {
border-bottom: none;
}
.chips .role {
font-size: 13px;
color: var(--text-secondary);
font-weight: 500;
}
.chips .role .hint {
display: block;
font-size: 12px;
color: var(--text-muted);
font-weight: 400;
margin-top: 2px;
}
.chips input {
font-family: var(--font-mono);
font-size: 12.5px;
}
/* ---------- Responsive ---------- */
@media (max-width: 720px) {
main { padding: 24px 16px 40px; }
header { padding: 16px 16px; }
.page-title h2 { font-size: 24px; }
.form-grid { grid-template-columns: 1fr; }
.meta-row { grid-template-columns: 1fr; gap: 4px; padding: 12px 14px; }
.event-line { grid-template-columns: 1fr; gap: 2px; }
.chips { grid-template-columns: 1fr; gap: 6px; }
}
/* =================================================================
v0.3 PR #8 — Conversation page
================================================================= */
.conversation-main {
display: flex;
flex-direction: column;
min-height: calc(100vh - 80px);
padding-bottom: 0;
}
.conv-topbar {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 8px;
margin-bottom: 12px;
flex-wrap: wrap;
}
.conv-label {
font-size: 13px;
color: var(--text-muted);
font-weight: 600;
}
.conv-picker {
flex: 1;
min-width: 240px;
padding: 6px 10px;
font-family: var(--font-mono);
font-size: 13px;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--bg);
}
.conv-action-btn {
padding: 6px 14px;
font-size: 13px;
background: var(--accent);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
}
.conv-action-btn:hover { filter: brightness(1.08); }
.conv-session-state {
font-size: 11px;
padding: 2px 8px;
border-radius: 999px;
text-transform: lowercase;
letter-spacing: 0.04em;
}
.conv-session-state.state-active {
background: rgba(34,197,94,0.12);
color: rgb(22,163,74);
}
.conv-session-state.state-ended {
background: rgba(100,116,139,0.12);
color: rgb(71,85,105);
}
.messages-thread {
flex: 1;
overflow-y: auto;
padding: 16px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--bg);
margin-bottom: 12px;
display: flex;
flex-direction: column;
gap: 12px;
}
.conv-empty {
color: var(--text-muted);
text-align: center;
padding: 40px 16px;
font-size: 13px;
}
.msg-bubble {
max-width: 80%;
padding: 10px 14px;
border-radius: 12px;
font-size: 14px;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
}
.msg-bubble.role-user {
align-self: flex-end;
background: var(--accent);
color: white;
}
.msg-bubble.role-assistant {
align-self: flex-start;
background: var(--bg-card);
border: 1px solid var(--border);
}
.msg-bubble.role-system {
align-self: center;
max-width: 90%;
font-style: italic;
font-size: 12.5px;
background: rgba(245,158,11,0.08);
border: 1px dashed rgba(245,158,11,0.4);
color: rgb(120,53,15);
}
.msg-bubble.pending {
opacity: 0.6;
font-size: 20px;
padding: 6px 14px;
}
.msg-meta {
display: flex;
align-items: center;
gap: 8px;
font-size: 11px;
opacity: 0.6;
margin-bottom: 4px;
}
.msg-role {
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.conv-input-bar {
display: flex;
gap: 8px;
padding: 12px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 8px;
}
.conv-input-bar textarea {
flex: 1;
font-family: var(--font-body);
font-size: 14px;
padding: 8px 10px;
border: 1px solid var(--border);
border-radius: 6px;
resize: vertical;
min-height: 44px;
}
.conv-input-bar textarea:disabled {
background: var(--bg);
opacity: 0.5;
}
.conv-input-bar button {
padding: 0 18px;
font-size: 13px;
background: var(--accent);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
}
.conv-input-bar button:disabled {
opacity: 0.4;
cursor: not-allowed;
}