feat(my-deepagent): v0.4 — workflow generator UI + hot-reload + UX polish
브라우저에서 YAML 안 쓰고도 새 워크플로우 템플릿 만들기 + 즉시 등록. + /new.html / index.html / new-workflow.html / runs.html / conversation.html 의 nav·copy·empty-state 정비. A. /new.html UX: - 제목 "새 Run" → "워크플로우 실행 (고급)" - 상단 info-box: "자유 대화는 여기가 아닙니다 → 메인 페이지" - 모든 필드에 한 줄 hint - Persona 오버라이드 <details> 접힘 B. Nav 재정렬 (5 페이지): - "대화" nav-primary, 나머지 nav-secondary (작고 dim) C. 메인 안내 + CSS: - 메인 / 에 "👋 my-deepagent" info-box 추가 - .info-box / .nav-primary / .nav-secondary / .wf-* 신규 스타일 D. Workflow hot-reload: - api/deps.py get_workflows 가 매 요청 mtime 튜플 검사 후 변경 시 reload - lifespan 도 user dir 포함하도록 _load_workflows_combined E. Workflow generator: - POST /api/workflows: CreateWorkflowRequest → WorkflowTemplate validate → <data_dir>/workflows/<name>@<version>.yaml 저장. 중복 409, validation 422. - static/new-workflow.html: 기본 정보 / Roles / Phases / YAML preview - app.js bootstrapWorkflowGenerator: capability chip 토글, role select 동적, 실시간 YAML preview, XSS 정책 유지 테스트 (test_workflow_generator.py, 7 신규): - 페이지 200 + 마크업 - POST happy / 422 (empty roles) / 422 (unknown role) / 409 (dup) - GET hot-reload after POST - GET hot-reload after external file drop 게이트: - ruff / format / mypy: PASS (142 source files) - pytest -q --ignore=tests/integration/test_e2e_workflow.py --ignore=tests/integration/test_openrouter_smoke.py: 709 passed (+7 신규) - 라이브 smoke: / / new.html / new-workflow.html 모두 200, screenshot OK Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -734,6 +734,284 @@ async function renderSessionsList() {
|
||||
}
|
||||
}
|
||||
|
||||
// =============== new-workflow.html (v0.4 generator) ===============
|
||||
|
||||
const _CAPABILITIES = [
|
||||
"spec_write", "code_review", "evidence_check", "log_analysis", "decision",
|
||||
"command_execute", "security_audit", "code_edit", "plan", "verify",
|
||||
];
|
||||
const _BACKENDS = ["openrouter", "anthropic", "ollama_local"];
|
||||
const _RISKS = ["low", "medium", "high"];
|
||||
|
||||
const WF_STATE = {
|
||||
roles: /** @type {Array<{id:string,capabilities:string[],backends:string[],fallbacks:string[]}>} */ ([]),
|
||||
phases: /** @type {Array<{key:string,title:string,risk:string,role:string,instructions:string,artifactPath:string,artifactSchema:string,gates:string,timeout:string,budget:string}>} */ ([]),
|
||||
};
|
||||
|
||||
function _wfFreshRole() {
|
||||
return { id: "", capabilities: [], backends: [], fallbacks: [] };
|
||||
}
|
||||
function _wfFreshPhase() {
|
||||
return {
|
||||
key: "", title: "", risk: "medium", role: "",
|
||||
instructions: "", artifactPath: "", artifactSchema: "",
|
||||
gates: "", timeout: "", budget: "",
|
||||
};
|
||||
}
|
||||
|
||||
function _wfChip(label, checked, onChange) {
|
||||
const lbl = document.createElement("label");
|
||||
lbl.className = "wf-chip";
|
||||
const cb = document.createElement("input");
|
||||
cb.type = "checkbox";
|
||||
cb.checked = checked;
|
||||
cb.addEventListener("change", () => onChange(cb.checked));
|
||||
const span = document.createElement("span");
|
||||
span.textContent = label;
|
||||
lbl.appendChild(cb);
|
||||
lbl.appendChild(span);
|
||||
return lbl;
|
||||
}
|
||||
|
||||
function _wfTextInput(value, placeholder, onChange, type = "text") {
|
||||
const i = document.createElement("input");
|
||||
i.type = type;
|
||||
i.value = value;
|
||||
i.placeholder = placeholder;
|
||||
i.addEventListener("input", () => onChange(i.value));
|
||||
return i;
|
||||
}
|
||||
|
||||
function _wfTextArea(value, placeholder, onChange, rows = 3) {
|
||||
const t = document.createElement("textarea");
|
||||
t.value = value;
|
||||
t.placeholder = placeholder;
|
||||
t.rows = rows;
|
||||
t.addEventListener("input", () => onChange(t.value));
|
||||
return t;
|
||||
}
|
||||
|
||||
function _wfSelect(value, options, onChange) {
|
||||
const s = document.createElement("select");
|
||||
for (const o of options) {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = o;
|
||||
opt.textContent = o;
|
||||
if (o === value) opt.selected = true;
|
||||
s.appendChild(opt);
|
||||
}
|
||||
s.addEventListener("change", () => onChange(s.value));
|
||||
return s;
|
||||
}
|
||||
|
||||
function renderRolesList() {
|
||||
const container = $("#roles-list");
|
||||
if (!container) return;
|
||||
container.replaceChildren();
|
||||
WF_STATE.roles.forEach((role, idx) => {
|
||||
const card = document.createElement("div");
|
||||
card.className = "wf-row-card";
|
||||
const header = document.createElement("div");
|
||||
header.className = "wf-row-header";
|
||||
const title = document.createElement("strong");
|
||||
title.textContent = `Role #${idx + 1}`;
|
||||
const del = document.createElement("button");
|
||||
del.type = "button";
|
||||
del.className = "button-link";
|
||||
del.textContent = "삭제";
|
||||
del.addEventListener("click", () => { WF_STATE.roles.splice(idx, 1); renderRolesList(); renderPreview(); });
|
||||
header.append(title, del);
|
||||
card.appendChild(header);
|
||||
|
||||
const idRow = document.createElement("div");
|
||||
idRow.className = "form-row";
|
||||
const idLbl = document.createElement("label");
|
||||
idLbl.innerHTML = "id <span class='hint'>— phase 가 참조할 키. <code>writer</code> 같은 소문자/숫자/언더스코어</span>";
|
||||
idRow.append(idLbl, _wfTextInput(role.id, "writer", (v) => { role.id = v; renderPreview(); }));
|
||||
card.appendChild(idRow);
|
||||
|
||||
const capRow = document.createElement("div");
|
||||
capRow.className = "form-row";
|
||||
const capLbl = document.createElement("label");
|
||||
capLbl.innerHTML = "required_capabilities <span class='hint'>— persona 가 가져야 할 능력 (최소 1)</span>";
|
||||
const chips = document.createElement("div");
|
||||
chips.className = "chips";
|
||||
for (const c of _CAPABILITIES) {
|
||||
chips.appendChild(_wfChip(c, role.capabilities.includes(c), (on) => {
|
||||
if (on && !role.capabilities.includes(c)) role.capabilities.push(c);
|
||||
else if (!on) role.capabilities = role.capabilities.filter((x) => x !== c);
|
||||
renderPreview();
|
||||
}));
|
||||
}
|
||||
capRow.append(capLbl, chips);
|
||||
card.appendChild(capRow);
|
||||
|
||||
container.appendChild(card);
|
||||
});
|
||||
if (WF_STATE.roles.length === 0) {
|
||||
const empty = document.createElement("div");
|
||||
empty.className = "hint";
|
||||
empty.textContent = "Role 이 1개 이상 필요합니다.";
|
||||
container.appendChild(empty);
|
||||
}
|
||||
}
|
||||
|
||||
function renderPhasesList() {
|
||||
const container = $("#phases-list");
|
||||
if (!container) return;
|
||||
container.replaceChildren();
|
||||
const roleIds = WF_STATE.roles.map((r) => r.id).filter(Boolean);
|
||||
WF_STATE.phases.forEach((phase, idx) => {
|
||||
const card = document.createElement("div");
|
||||
card.className = "wf-row-card";
|
||||
const header = document.createElement("div");
|
||||
header.className = "wf-row-header";
|
||||
const title = document.createElement("strong");
|
||||
title.textContent = `Phase #${idx + 1}`;
|
||||
const del = document.createElement("button");
|
||||
del.type = "button";
|
||||
del.className = "button-link";
|
||||
del.textContent = "삭제";
|
||||
del.addEventListener("click", () => { WF_STATE.phases.splice(idx, 1); renderPhasesList(); renderPreview(); });
|
||||
header.append(title, del);
|
||||
card.appendChild(header);
|
||||
|
||||
const grid = document.createElement("div");
|
||||
grid.className = "form-grid";
|
||||
for (const [label, key, ph] of [
|
||||
["key — 영문 소문자/숫자/언더스코어", "key", "spec"],
|
||||
["title — 표시용 한 줄", "title", "명세 작성"],
|
||||
]) {
|
||||
const r = document.createElement("div");
|
||||
r.className = "form-row";
|
||||
const l = document.createElement("label");
|
||||
l.textContent = label;
|
||||
r.append(l, _wfTextInput(phase[key], ph, (v) => { phase[key] = v; renderPreview(); }));
|
||||
grid.appendChild(r);
|
||||
}
|
||||
card.appendChild(grid);
|
||||
|
||||
const grid2 = document.createElement("div");
|
||||
grid2.className = "form-grid";
|
||||
const riskRow = document.createElement("div");
|
||||
riskRow.className = "form-row";
|
||||
const riskLbl = document.createElement("label");
|
||||
riskLbl.innerHTML = "risk <span class='hint'>— 단계 위험 등급</span>";
|
||||
riskRow.append(riskLbl, _wfSelect(phase.risk, _RISKS, (v) => { phase.risk = v; renderPreview(); }));
|
||||
grid2.appendChild(riskRow);
|
||||
const roleRow = document.createElement("div");
|
||||
roleRow.className = "form-row";
|
||||
const roleLbl = document.createElement("label");
|
||||
roleLbl.innerHTML = "role <span class='hint'>— 위에서 정의한 role id 중 하나</span>";
|
||||
const opts = roleIds.length > 0 ? roleIds : ["(role 을 먼저 정의)"];
|
||||
roleRow.append(roleLbl, _wfSelect(phase.role, opts, (v) => { phase.role = v; renderPreview(); }));
|
||||
grid2.appendChild(roleRow);
|
||||
card.appendChild(grid2);
|
||||
|
||||
const insRow = document.createElement("div");
|
||||
insRow.className = "form-row";
|
||||
const insLbl = document.createElement("label");
|
||||
insLbl.innerHTML = "instructions <span class='hint'>— 최소 10자. 이 phase 가 무엇을 해야 하는지</span>";
|
||||
insRow.append(insLbl, _wfTextArea(phase.instructions,
|
||||
"예: requirements.md 를 읽고 spec.md 를 작성하세요. 한국어 권장.",
|
||||
(v) => { phase.instructions = v; renderPreview(); }, 4));
|
||||
card.appendChild(insRow);
|
||||
|
||||
const grid3 = document.createElement("div");
|
||||
grid3.className = "form-grid";
|
||||
for (const [label, key, ph] of [
|
||||
["expected_artifact.path (선택)", "artifactPath", "artifacts/spec.md"],
|
||||
["expected_artifact.schema (선택)", "artifactSchema", "spec-v1"],
|
||||
]) {
|
||||
const r = document.createElement("div");
|
||||
r.className = "form-row";
|
||||
const l = document.createElement("label");
|
||||
l.textContent = label;
|
||||
r.append(l, _wfTextInput(phase[key], ph, (v) => { phase[key] = v; renderPreview(); }));
|
||||
grid3.appendChild(r);
|
||||
}
|
||||
card.appendChild(grid3);
|
||||
container.appendChild(card);
|
||||
});
|
||||
if (WF_STATE.phases.length === 0) {
|
||||
const empty = document.createElement("div");
|
||||
empty.className = "hint";
|
||||
empty.textContent = "Phase 가 1개 이상 필요합니다.";
|
||||
container.appendChild(empty);
|
||||
}
|
||||
}
|
||||
|
||||
function _wfBuildRequest() {
|
||||
const name = $("#wf-name").value.trim();
|
||||
const version = parseInt($("#wf-version").value, 10);
|
||||
const description = $("#wf-description").value.trim();
|
||||
const roles = WF_STATE.roles.filter((r) => r.id).map((r) => ({
|
||||
id: r.id,
|
||||
required_capabilities: r.capabilities,
|
||||
preferred_backends: r.backends,
|
||||
fallback_personas: r.fallbacks,
|
||||
}));
|
||||
const phases = WF_STATE.phases.filter((p) => p.key).map((p) => {
|
||||
const out = {
|
||||
key: p.key,
|
||||
title: p.title || p.key,
|
||||
risk: p.risk,
|
||||
role: p.role,
|
||||
instructions: p.instructions || "(no instructions)",
|
||||
gates: [],
|
||||
};
|
||||
if (p.artifactPath || p.artifactSchema) {
|
||||
out.expected_artifact = {
|
||||
path: p.artifactPath || "artifacts/output.md",
|
||||
schema: p.artifactSchema || "text",
|
||||
};
|
||||
}
|
||||
return out;
|
||||
});
|
||||
const req = { name, version: isNaN(version) ? 1 : version, roles, phases, default_gates: [] };
|
||||
if (description) req.description = description;
|
||||
return req;
|
||||
}
|
||||
|
||||
function renderPreview() {
|
||||
const pre = $("#wf-preview");
|
||||
if (!pre) return;
|
||||
pre.textContent = JSON.stringify(_wfBuildRequest(), null, 2);
|
||||
}
|
||||
|
||||
async function submitWorkflow(ev) {
|
||||
ev.preventDefault();
|
||||
setError("");
|
||||
$("#success").style.display = "none";
|
||||
const req = _wfBuildRequest();
|
||||
try {
|
||||
const ack = await jsonFetch("/workflows", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(req),
|
||||
});
|
||||
const okBox = $("#success");
|
||||
okBox.textContent = `✅ 저장 완료 → ${ack.path}. 워크플로우 실행 페이지에서 바로 보입니다.`;
|
||||
okBox.style.display = "block";
|
||||
} catch (e) {
|
||||
setError(`저장 실패: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function bootstrapWorkflowGenerator() {
|
||||
WF_STATE.roles = [_wfFreshRole()];
|
||||
WF_STATE.phases = [_wfFreshPhase()];
|
||||
renderRolesList();
|
||||
renderPhasesList();
|
||||
renderPreview();
|
||||
$("#add-role").addEventListener("click", () => { WF_STATE.roles.push(_wfFreshRole()); renderRolesList(); renderPreview(); });
|
||||
$("#add-phase").addEventListener("click", () => { WF_STATE.phases.push(_wfFreshPhase()); renderPhasesList(); renderPreview(); });
|
||||
$("#wf-name").addEventListener("input", renderPreview);
|
||||
$("#wf-version").addEventListener("input", renderPreview);
|
||||
$("#wf-description").addEventListener("input", renderPreview);
|
||||
$("#wf-form").addEventListener("submit", submitWorkflow);
|
||||
}
|
||||
|
||||
// =============== bootstrap ===============
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
@@ -751,5 +1029,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
$("#resume-btn").addEventListener("click", resumeRun);
|
||||
} else if (page === "conversation") {
|
||||
bootstrapConversationPage();
|
||||
} else if (page === "new-workflow") {
|
||||
bootstrapWorkflowGenerator();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -10,9 +10,10 @@
|
||||
<header>
|
||||
<h1><a href="/">my-deepagent</a></h1>
|
||||
<nav>
|
||||
<a href="/">세션 목록</a>
|
||||
<a href="/conversation.html" class="active">대화</a>
|
||||
<a href="/runs.html">Runs (archive)</a>
|
||||
<a href="/" class="nav-primary">세션 목록</a>
|
||||
<a href="/conversation.html" class="active nav-primary">대화</a>
|
||||
<a href="/runs.html" class="nav-secondary">Runs</a>
|
||||
<a href="/new.html" class="nav-secondary">워크플로우 실행</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main class="conversation-main">
|
||||
|
||||
@@ -10,9 +10,10 @@
|
||||
<header>
|
||||
<h1><a href="/">my-deepagent</a></h1>
|
||||
<nav>
|
||||
<a href="/" class="active">대화</a>
|
||||
<a href="/runs.html">Runs (archive)</a>
|
||||
<a href="/new.html">새 Workflow Run</a>
|
||||
<a href="/" class="active nav-primary">대화</a>
|
||||
<a href="/runs.html" class="nav-secondary">Runs</a>
|
||||
<a href="/new.html" class="nav-secondary">워크플로우 실행</a>
|
||||
<a href="/new-workflow.html" class="nav-secondary">+ 템플릿 만들기</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
@@ -23,6 +24,12 @@
|
||||
<span class="page-subtitle">최근 50개 · 빈 화면이면 아래 "새 대화"를 누르세요</span>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<strong>👋 my-deepagent</strong> — OpenRouter 가성비 모델로 돌아가는 Claude Code 스타일 멀티턴 에이전트.
|
||||
대부분의 경우 아래 <strong>"새 대화 시작"</strong>만 누르면 됩니다.
|
||||
<a href="/new.html">여러 단계 자동화</a>가 필요하면 워크플로우, <a href="/new-workflow.html">템플릿 직접 만들기</a>도 가능.
|
||||
</div>
|
||||
|
||||
<div class="action-bar" style="margin-bottom: 12px;">
|
||||
<a class="button primary" href="/conversation.html">▶︎ 새 대화 시작</a>
|
||||
</div>
|
||||
|
||||
99
my-deepagent/static/new-workflow.html
Normal file
99
my-deepagent/static/new-workflow.html
Normal file
@@ -0,0 +1,99 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>my-deepagent · 워크플로우 템플릿 만들기</title>
|
||||
<link rel="stylesheet" href="/static/style.css" />
|
||||
</head>
|
||||
<body data-page="new-workflow">
|
||||
<header>
|
||||
<h1><a href="/">my-deepagent</a></h1>
|
||||
<nav>
|
||||
<a href="/" class="nav-primary">대화</a>
|
||||
<a href="/runs.html" class="nav-secondary">Runs</a>
|
||||
<a href="/new.html" class="nav-secondary">워크플로우 실행</a>
|
||||
<a href="/new-workflow.html" class="active nav-secondary">+ 템플릿 만들기</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
<div id="error" class="error-banner" style="display:none"></div>
|
||||
<div id="success" class="info-box" style="display:none"></div>
|
||||
|
||||
<div class="page-title">
|
||||
<h2>워크플로우 템플릿 만들기</h2>
|
||||
<span class="page-subtitle">phase 시퀀스 + role 정의 → YAML 저장</span>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<strong>📘 워크플로우 = phase 시퀀스</strong><br />
|
||||
예: <code>"명세 작성" → "리뷰" → "검증"</code> 처럼 단계별로 어떤 role(역할)이 어떤
|
||||
산출물을 만들지 정의하는 파일입니다. 저장 후엔 <a href="/new.html">워크플로우 실행</a>
|
||||
페이지의 드롭다운에 자동으로 등장합니다 (서버 재시작 불필요).
|
||||
</div>
|
||||
|
||||
<form id="wf-form" autocomplete="off">
|
||||
|
||||
<!-- 기본 메타 -->
|
||||
<div class="card" style="padding: 20px;">
|
||||
<h3 class="section-title" style="margin-top:0">기본 정보</h3>
|
||||
<div class="form-grid">
|
||||
<div class="form-row">
|
||||
<label for="wf-name">
|
||||
name
|
||||
<span class="hint">— 영문 소문자/숫자/하이픈만. 예: <code>spec-and-review</code></span>
|
||||
</label>
|
||||
<input id="wf-name" type="text" required placeholder="my-workflow" />
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="wf-version">
|
||||
version
|
||||
<span class="hint">— 정수, 1부터</span>
|
||||
</label>
|
||||
<input id="wf-version" type="number" required value="1" min="1" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="wf-description">
|
||||
description
|
||||
<span class="hint">— 한 줄 설명 (선택)</span>
|
||||
</label>
|
||||
<input id="wf-description" type="text" placeholder="이 워크플로우가 무엇을 하는지" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Roles -->
|
||||
<div class="card" style="padding: 20px; margin-top: 16px;">
|
||||
<h3 class="section-title" style="margin-top:0">
|
||||
Roles <span class="hint" style="font-weight:400">— phase 가 참조할 역할 정의</span>
|
||||
</h3>
|
||||
<div id="roles-list"></div>
|
||||
<button type="button" id="add-role" class="button">+ Role 추가</button>
|
||||
</div>
|
||||
|
||||
<!-- Phases -->
|
||||
<div class="card" style="padding: 20px; margin-top: 16px;">
|
||||
<h3 class="section-title" style="margin-top:0">
|
||||
Phases <span class="hint" style="font-weight:400">— 실제 실행되는 단계 순서</span>
|
||||
</h3>
|
||||
<div id="phases-list"></div>
|
||||
<button type="button" id="add-phase" class="button">+ Phase 추가</button>
|
||||
</div>
|
||||
|
||||
<!-- Preview -->
|
||||
<details class="card" style="padding: 16px; margin-top: 16px;">
|
||||
<summary style="cursor:pointer; font-weight:600;">
|
||||
YAML 미리보기 <span class="hint" style="font-weight:400">— 저장될 파일 내용</span>
|
||||
</summary>
|
||||
<pre id="wf-preview" class="mono" style="margin-top:12px; white-space:pre-wrap; font-size:12.5px;"></pre>
|
||||
</details>
|
||||
|
||||
<div class="action-bar">
|
||||
<button type="submit" class="primary">💾 저장 + 등록</button>
|
||||
<a class="button" href="/">취소</a>
|
||||
</div>
|
||||
</form>
|
||||
</main>
|
||||
<script src="/static/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -3,56 +3,85 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>my-deepagent · 새 Run</title>
|
||||
<title>my-deepagent · 워크플로우 실행</title>
|
||||
<link rel="stylesheet" href="/static/style.css" />
|
||||
</head>
|
||||
<body data-page="new">
|
||||
<header>
|
||||
<h1><a href="/">my-deepagent</a></h1>
|
||||
<nav>
|
||||
<a href="/">대화</a>
|
||||
<a href="/runs.html">Runs (archive)</a>
|
||||
<a href="/new.html" class="active">새 Workflow Run</a>
|
||||
<a href="/" class="nav-primary">대화</a>
|
||||
<a href="/runs.html" class="nav-secondary">Runs</a>
|
||||
<a href="/new.html" class="active nav-secondary">워크플로우 실행</a>
|
||||
<a href="/new-workflow.html" class="nav-secondary">+ 템플릿 만들기</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
<div id="error" class="error-banner" style="display:none"></div>
|
||||
|
||||
<div class="page-title">
|
||||
<h2>새 Run 시작</h2>
|
||||
<span class="page-subtitle">워크플로우 + repo + 요구사항</span>
|
||||
<h2>워크플로우 실행 <span class="hint" style="font-size: 12px; vertical-align: middle;">(고급 기능)</span></h2>
|
||||
<span class="page-subtitle">사전 정의된 phase 시퀀스로 자동화된 작업 실행</span>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<strong>💡 자유 대화는 여기가 아닙니다.</strong>
|
||||
그냥 챗봇처럼 쓰고 싶다면 <a href="/">메인 페이지의 "새 대화 시작"</a>을 눌러주세요.
|
||||
이 페이지는 <strong>여러 단계 (예: 명세 → 리뷰 → 검증)</strong> 가 정해진 순서로 자동 실행되는 워크플로우를 시작할 때 씁니다.
|
||||
<br /><br />
|
||||
<strong>새 템플릿을 직접 만들고 싶다면</strong> 우상단 <a href="/new-workflow.html">+ 템플릿 만들기</a>로 가세요.
|
||||
</div>
|
||||
|
||||
<form id="start-form" autocomplete="off">
|
||||
<div class="card" style="padding: 20px;">
|
||||
<div class="form-row">
|
||||
<label for="template">워크플로우 템플릿</label>
|
||||
<label for="template">
|
||||
워크플로우 템플릿
|
||||
<span class="hint">— 무슨 단계를 어떤 순서로 돌릴지 정의한 YAML. 모르면 첫 번째 선택.</span>
|
||||
</label>
|
||||
<select id="template" required></select>
|
||||
</div>
|
||||
|
||||
<div class="form-grid">
|
||||
<div class="form-row">
|
||||
<label for="repo-path">repo 절대경로</label>
|
||||
<label for="repo-path">
|
||||
repo 절대경로
|
||||
<span class="hint">— 작업할 git 저장소 위치 (예: /Users/me/projects/my-thing)</span>
|
||||
</label>
|
||||
<input id="repo-path" type="text" placeholder="/Users/me/projects/my-thing" required />
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="base-branch">base branch</label>
|
||||
<label for="base-branch">
|
||||
base branch
|
||||
<span class="hint">— 작업의 시작점 (보통 main)</span>
|
||||
</label>
|
||||
<input id="base-branch" type="text" value="main" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="requirements">requirements <span class="hint">— 자유 텍스트, 마크다운 OK</span></label>
|
||||
<textarea id="requirements" rows="6" placeholder="이 workflow가 다룰 요구사항을 적어주세요."></textarea>
|
||||
<label for="requirements">
|
||||
requirements
|
||||
<span class="hint">— 이 워크플로우가 다룰 요구사항. 자유 텍스트, 마크다운 OK</span>
|
||||
</label>
|
||||
<textarea id="requirements" rows="6" placeholder="예: wordcount CLI를 만들어줘. python으로, pytest 테스트 포함."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="section-title">Persona 오버라이드 <span class="hint" style="text-transform: none; letter-spacing: 0; font-weight: 400;">(선택, 비우면 자동 선택)</span></h2>
|
||||
<div id="override-fields" class="card"></div>
|
||||
<details class="card" style="margin-top: 16px; padding: 16px;">
|
||||
<summary style="cursor: pointer; font-weight: 600;">
|
||||
Persona 오버라이드 <span class="hint" style="font-weight: 400;">— 비우면 자동 선택 (고급)</span>
|
||||
</summary>
|
||||
<p class="hint" style="margin-top: 12px; font-weight: 400;">
|
||||
각 단계(role)에 어떤 persona(AI 모델 + 시스템 프롬프트)를 쓸지 직접 고르고 싶을 때만 채우세요.
|
||||
비워두면 capability 매칭으로 자동 선택됩니다.
|
||||
</p>
|
||||
<div id="override-fields"></div>
|
||||
</details>
|
||||
|
||||
<div class="action-bar">
|
||||
<button type="submit" class="primary">▶︎ 시작</button>
|
||||
<button type="submit" class="primary">▶︎ 워크플로우 실행</button>
|
||||
<a class="button" href="/">취소</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -10,9 +10,10 @@
|
||||
<header>
|
||||
<h1><a href="/">my-deepagent</a></h1>
|
||||
<nav>
|
||||
<a href="/">대화</a>
|
||||
<a href="/runs.html">Runs (archive)</a>
|
||||
<a href="/new.html">새 Workflow Run</a>
|
||||
<a href="/" class="nav-primary">대화</a>
|
||||
<a href="/runs.html" class="nav-secondary">Runs</a>
|
||||
<a href="/new.html" class="nav-secondary">워크플로우 실행</a>
|
||||
<a href="/new-workflow.html" class="nav-secondary">+ 템플릿 만들기</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
|
||||
@@ -10,9 +10,10 @@
|
||||
<header>
|
||||
<h1><a href="/">my-deepagent</a></h1>
|
||||
<nav>
|
||||
<a href="/">대화</a>
|
||||
<a href="/runs.html" class="active">Runs (archive)</a>
|
||||
<a href="/new.html">새 Workflow Run</a>
|
||||
<a href="/" class="nav-primary">대화</a>
|
||||
<a href="/runs.html" class="active nav-secondary">Runs</a>
|
||||
<a href="/new.html" class="nav-secondary">워크플로우 실행</a>
|
||||
<a href="/new-workflow.html" class="nav-secondary">+ 템플릿 만들기</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
|
||||
@@ -962,3 +962,134 @@ select {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
|
||||
/* =================================================================
|
||||
v0.4 — nav tiers + info-box + empty-state polish
|
||||
================================================================= */
|
||||
|
||||
nav .nav-primary {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
nav .nav-secondary {
|
||||
font-size: 12.5px;
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
nav .nav-secondary:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
nav a.active.nav-primary,
|
||||
nav a.active.nav-secondary {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: rgba(245, 158, 11, 0.08);
|
||||
border: 1px solid rgba(245, 158, 11, 0.3);
|
||||
border-left: 4px solid rgb(245, 158, 11);
|
||||
padding: 14px 18px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
line-height: 1.65;
|
||||
color: rgb(95, 50, 5);
|
||||
}
|
||||
|
||||
.info-box strong {
|
||||
color: rgb(75, 35, 0);
|
||||
}
|
||||
|
||||
.info-box a {
|
||||
color: rgb(180, 70, 30);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
/* details/summary polish */
|
||||
details summary {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
details[open] summary {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
/* index empty state — prominent CTA */
|
||||
.empty-cta {
|
||||
text-align: center;
|
||||
padding: 64px 20px;
|
||||
}
|
||||
|
||||
.empty-cta-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.empty-cta-subtitle {
|
||||
color: var(--text-muted);
|
||||
font-size: 14px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.empty-cta .button {
|
||||
font-size: 15px;
|
||||
padding: 12px 24px;
|
||||
}
|
||||
|
||||
/* =================================================================
|
||||
v0.4 — workflow generator UI
|
||||
================================================================= */
|
||||
|
||||
.wf-row-card {
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 14px 16px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.wf-row-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px dashed var(--border);
|
||||
}
|
||||
|
||||
.button-link {
|
||||
background: none;
|
||||
border: none;
|
||||
color: rgb(180, 70, 30);
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
text-decoration: underline;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
.wf-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
background: rgba(180, 70, 30, 0.06);
|
||||
border: 1px solid rgba(180, 70, 30, 0.2);
|
||||
border-radius: 999px;
|
||||
padding: 3px 10px;
|
||||
font-size: 12.5px;
|
||||
cursor: pointer;
|
||||
margin: 2px 4px 2px 0;
|
||||
}
|
||||
|
||||
.wf-chip input {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wf-chip:has(input:checked) {
|
||||
background: rgba(180, 70, 30, 0.18);
|
||||
border-color: rgba(180, 70, 30, 0.5);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user