Commit 4d6e754f6b1535c4e3ea3e08e0fd6aad3902e623

Authored by yibo
1 parent 6d03688f

feat(web): add Company Dashboard frontend

- CompanyDashboard.vue: full company management UI
  - Company list grid with status badges
  - Company detail view with tabs (Agents/Pipelines/Goals)
  - Create company modal
  - Hire agent modal with template/training feedback
  - Template library slide-out panel
  - Pipeline flow visualization (step nodes with arrows)
  - Pipeline run history with status
  - OKR goals list
- Route: /company registered in Vue Router
- Build verified: all assets compiled successfully
web/src/router/index.js
... ... @@ -16,6 +16,11 @@ const routes = [
16 16 props: true,
17 17 },
18 18 {
  19 + path: '/company',
  20 + name: 'CompanyDashboard',
  21 + component: () => import('@/views/CompanyDashboard.vue'),
  22 + },
  23 + {
19 24 path: '/okr',
20 25 name: 'OkrDashboard',
21 26 component: () => import('@/views/OkrDashboard.vue'),
... ...
web/src/views/CompanyDashboard.vue 0 → 100644
  1 +<template>
  2 + <div class="company-dashboard">
  3 + <!-- ═══ Top Bar ═══ -->
  4 + <header class="top-bar">
  5 + <div class="top-bar-left">
  6 + <span class="logo">🏢</span>
  7 + <h1 class="title">Company Dashboard</h1>
  8 + </div>
  9 + <div class="top-bar-right">
  10 + <button class="btn-create" @click="showCreateModal = true">
  11 + <span>+</span> 创建公司
  12 + </button>
  13 + <button class="btn-templates" @click="showTemplates = !showTemplates">
  14 + 📦 模板库 ({{ templates.length }})
  15 + </button>
  16 + </div>
  17 + </header>
  18 +
  19 + <!-- ═══ Company List ═══ -->
  20 + <section class="company-grid" v-if="!selectedCompany">
  21 + <div
  22 + v-for="company in companies"
  23 + :key="company.id"
  24 + class="company-card"
  25 + :class="[`status-${company.status}`]"
  26 + @click="selectCompany(company.id)"
  27 + >
  28 + <div class="card-header">
  29 + <span class="card-emoji">🏢</span>
  30 + <span class="card-status" :class="company.status">{{ statusLabel(company.status) }}</span>
  31 + </div>
  32 + <h3 class="card-name">{{ company.name }}</h3>
  33 + <div class="card-meta">
  34 + <span>👥 {{ company.agentCount }} 人</span>
  35 + <span v-if="company.ceo">👔 {{ company.ceo }}</span>
  36 + </div>
  37 + </div>
  38 +
  39 + <!-- Empty state -->
  40 + <div v-if="companies.length === 0" class="empty-state">
  41 + <span class="empty-icon">🏗️</span>
  42 + <p>还没有任何公司</p>
  43 + <button class="btn-create" @click="showCreateModal = true">创建第一家公司</button>
  44 + </div>
  45 + </section>
  46 +
  47 + <!-- ═══ Company Detail ═══ -->
  48 + <section class="company-detail" v-if="selectedCompany">
  49 + <div class="detail-header">
  50 + <button class="btn-back" @click="selectedCompany = null">← 返回列表</button>
  51 + <div class="detail-title-row">
  52 + <h2>{{ selectedCompany.name }}</h2>
  53 + <span class="card-status" :class="selectedCompany.status">{{ statusLabel(selectedCompany.status) }}</span>
  54 + </div>
  55 + <p class="detail-desc">{{ selectedCompany.description || '暂无描述' }}</p>
  56 + </div>
  57 +
  58 + <!-- Tabs -->
  59 + <div class="tabs">
  60 + <button
  61 + v-for="tab in tabs"
  62 + :key="tab.key"
  63 + :class="{ active: activeTab === tab.key }"
  64 + @click="switchTab(tab.key)"
  65 + >
  66 + {{ tab.icon }} {{ tab.label }}
  67 + </button>
  68 + </div>
  69 +
  70 + <!-- Tab: Agents -->
  71 + <div v-if="activeTab === 'agents'" class="tab-content">
  72 + <div class="section-header">
  73 + <h3>团队成员</h3>
  74 + <button class="btn-sm" @click="showHireModal = true">+ 招聘</button>
  75 + </div>
  76 + <div class="agents-grid">
  77 + <div v-for="agent in companyAgents" :key="agent.id" class="agent-card">
  78 + <div class="agent-avatar">{{ agentEmoji(agent.type) }}</div>
  79 + <div class="agent-info">
  80 + <span class="agent-name">{{ agent.name }}</span>
  81 + <span class="agent-type">{{ agent.type }}</span>
  82 + </div>
  83 + <span class="agent-status" :class="agent.status">{{ agent.status }}</span>
  84 + </div>
  85 + <div v-if="companyAgents.length === 0" class="empty-mini">暂无成员</div>
  86 + </div>
  87 + </div>
  88 +
  89 + <!-- Tab: Pipelines -->
  90 + <div v-if="activeTab === 'pipelines'" class="tab-content">
  91 + <div class="section-header">
  92 + <h3>SOP 流水线</h3>
  93 + </div>
  94 + <div class="pipeline-list">
  95 + <div v-for="p in pipelines" :key="p.id" class="pipeline-card" @click="loadPipelineDetail(p.id)">
  96 + <div class="pipeline-header">
  97 + <span class="pipeline-name">⚙️ {{ p.name }}</span>
  98 + <span class="pipeline-steps">{{ p.stepCount }} 步</span>
  99 + </div>
  100 + <span class="card-status" :class="p.status">{{ p.status }}</span>
  101 + </div>
  102 + <div v-if="pipelines.length === 0" class="empty-mini">暂无流水线</div>
  103 + </div>
  104 +
  105 + <!-- Pipeline Detail -->
  106 + <div v-if="selectedPipeline" class="pipeline-detail">
  107 + <h4>{{ selectedPipeline.name }}</h4>
  108 + <div class="step-flow">
  109 + <div
  110 + v-for="(step, idx) in selectedPipeline.steps"
  111 + :key="idx"
  112 + class="step-node"
  113 + >
  114 + <div class="step-index">{{ idx + 1 }}</div>
  115 + <div class="step-info">
  116 + <span class="step-name">{{ step.name }}</span>
  117 + <span class="step-agent">🤖 {{ step.agent }}</span>
  118 + </div>
  119 + <div v-if="idx < selectedPipeline.steps.length - 1" class="step-arrow">→</div>
  120 + </div>
  121 + </div>
  122 + <div v-if="selectedPipeline.cron" class="cron-badge">
  123 + 🔄 自动触发: 每 {{ selectedPipeline.cron.intervalHours }} 小时
  124 + </div>
  125 + </div>
  126 +
  127 + <!-- Runs -->
  128 + <div v-if="pipelineRuns.length > 0" class="runs-section">
  129 + <h4>运行记录</h4>
  130 + <div v-for="run in pipelineRuns" :key="run.runId" class="run-card">
  131 + <div class="run-header">
  132 + <span class="run-id">{{ run.runId }}</span>
  133 + <span class="card-status" :class="run.status">{{ run.status }}</span>
  134 + </div>
  135 + <div class="run-meta">
  136 + <span>📌 {{ run.pipelineName }}</span>
  137 + <span>步骤 {{ run.currentStep + 1 }}/{{ run.totalSteps }}</span>
  138 + <span>🚀 {{ run.trigger }}</span>
  139 + </div>
  140 + </div>
  141 + </div>
  142 + </div>
  143 +
  144 + <!-- Tab: Goals -->
  145 + <div v-if="activeTab === 'goals'" class="tab-content">
  146 + <div class="section-header">
  147 + <h3>OKR 目标</h3>
  148 + </div>
  149 + <div class="goals-list">
  150 + <div v-for="g in goals" :key="g.id" class="goal-card">
  151 + <div class="goal-header">
  152 + <span class="goal-id">{{ g.id }}</span>
  153 + <span class="card-status" :class="g.status">{{ g.status }}</span>
  154 + </div>
  155 + <h4 class="goal-title">{{ g.title }}</h4>
  156 + <div class="goal-meta">
  157 + <span>👤 {{ g.owner }}</span>
  158 + </div>
  159 + </div>
  160 + <div v-if="goals.length === 0" class="empty-mini">暂无目标</div>
  161 + </div>
  162 + </div>
  163 + </section>
  164 +
  165 + <!-- ═══ Create Company Modal ═══ -->
  166 + <div v-if="showCreateModal" class="modal-overlay" @click.self="showCreateModal = false">
  167 + <div class="modal">
  168 + <h3>🏢 创建新公司</h3>
  169 + <label>公司 ID (英文)</label>
  170 + <input v-model="newCompany.id" placeholder="media-co" />
  171 + <label>公司名称</label>
  172 + <input v-model="newCompany.name" placeholder="柠檬自媒体" />
  173 + <label>公司使命</label>
  174 + <textarea v-model="newCompany.description" placeholder="描述公司的使命..." rows="3"></textarea>
  175 + <div class="modal-actions">
  176 + <button class="btn-cancel" @click="showCreateModal = false">取消</button>
  177 + <button class="btn-create" @click="createCompany" :disabled="!newCompany.id || !newCompany.name">创建</button>
  178 + </div>
  179 + </div>
  180 + </div>
  181 +
  182 + <!-- ═══ Hire Modal ═══ -->
  183 + <div v-if="showHireModal" class="modal-overlay" @click.self="showHireModal = false">
  184 + <div class="modal">
  185 + <h3>👔 招聘 Agent</h3>
  186 + <label>角色 ID</label>
  187 + <input v-model="hireForm.role" placeholder="content-writer" />
  188 + <label>角色名称</label>
  189 + <input v-model="hireForm.name" placeholder="内容写手" />
  190 + <label>类型</label>
  191 + <select v-model="hireForm.type">
  192 + <option value="skill">skill (执行层)</option>
  193 + <option value="cxo">cxo (管理层)</option>
  194 + <option value="role">role (职能层)</option>
  195 + </select>
  196 + <div class="modal-actions">
  197 + <button class="btn-cancel" @click="showHireModal = false">取消</button>
  198 + <button class="btn-create" @click="hireAgent" :disabled="!hireForm.role">招聘</button>
  199 + </div>
  200 + <div v-if="hireResult" class="hire-result" :class="hireResult.status">
  201 + {{ hireResult.status === 'hired_from_template' ? '✅ 从模板招聘成功' :
  202 + hireResult.status === 'training' ? '🎓 骨架已创建,需要训练' :
  203 + hireResult.status === 'already_exists' ? '⚠️ 已存在' : hireResult.status }}
  204 + </div>
  205 + </div>
  206 + </div>
  207 +
  208 + <!-- ═══ Templates Panel ═══ -->
  209 + <div v-if="showTemplates" class="templates-panel">
  210 + <div class="panel-header">
  211 + <h3>📦 模板库</h3>
  212 + <button @click="showTemplates = false">✕</button>
  213 + </div>
  214 + <div class="template-list">
  215 + <div v-for="tpl in templates" :key="tpl.id" class="template-card">
  216 + <div class="tpl-name">{{ tpl.name }}</div>
  217 + <div class="tpl-meta">
  218 + <span class="tpl-type">{{ tpl.type }}</span>
  219 + <span v-if="tpl.benchmarkScore" class="tpl-score">⭐ {{ tpl.benchmarkScore }}</span>
  220 + <span class="tpl-used">用 {{ tpl.usedCount }} 次</span>
  221 + </div>
  222 + </div>
  223 + </div>
  224 + </div>
  225 + </div>
  226 +</template>
  227 +
  228 +<script setup>
  229 +import { ref, onMounted } from 'vue'
  230 +import axios from 'axios'
  231 +
  232 +const companies = ref([])
  233 +const selectedCompany = ref(null)
  234 +const companyAgents = ref([])
  235 +const pipelines = ref([])
  236 +const pipelineRuns = ref([])
  237 +const selectedPipeline = ref(null)
  238 +const goals = ref([])
  239 +const templates = ref([])
  240 +const activeTab = ref('agents')
  241 +const showCreateModal = ref(false)
  242 +const showHireModal = ref(false)
  243 +const showTemplates = ref(false)
  244 +
  245 +const tabs = [
  246 + { key: 'agents', icon: '👥', label: '团队' },
  247 + { key: 'pipelines', icon: '⚙️', label: '流水线' },
  248 + { key: 'goals', icon: '🎯', label: 'OKR' },
  249 +]
  250 +
  251 +const newCompany = ref({ id: '', name: '', description: '' })
  252 +const hireForm = ref({ role: '', name: '', type: 'skill' })
  253 +const hireResult = ref(null)
  254 +
  255 +function statusLabel(status) {
  256 + const map = { creating: '创建中', running: '运行中', paused: '已暂停', shutdown: '已关停' }
  257 + return map[status] || status
  258 +}
  259 +
  260 +function agentEmoji(type) {
  261 + const map = { cxo: '👔', role: '🏢', skill: '⚙️', specialist: '🔬' }
  262 + return map[type] || '🤖'
  263 +}
  264 +
  265 +async function loadCompanies() {
  266 + try {
  267 + const { data } = await axios.get('/api/company/list')
  268 + companies.value = data
  269 + } catch (e) { console.error(e) }
  270 +}
  271 +
  272 +async function loadTemplates() {
  273 + try {
  274 + const { data } = await axios.get('/api/company/templates')
  275 + templates.value = data
  276 + } catch (e) { console.error(e) }
  277 +}
  278 +
  279 +async function selectCompany(id) {
  280 + try {
  281 + const { data } = await axios.get(`/api/company/${id}`)
  282 + selectedCompany.value = data
  283 + activeTab.value = 'agents'
  284 + await loadCompanyAgents(id)
  285 + } catch (e) { console.error(e) }
  286 +}
  287 +
  288 +async function loadCompanyAgents(id) {
  289 + try {
  290 + const { data } = await axios.get(`/api/company/${id}/agents`)
  291 + companyAgents.value = data
  292 + } catch (e) { console.error(e) }
  293 +}
  294 +
  295 +async function loadPipelines(id) {
  296 + try {
  297 + const { data } = await axios.get(`/api/company/${id}/pipelines`)
  298 + pipelines.value = data
  299 + selectedPipeline.value = null
  300 + // load runs
  301 + const { data: runs } = await axios.get(`/api/company/${id}/pipelines/runs/list`)
  302 + pipelineRuns.value = runs
  303 + } catch (e) { console.error(e) }
  304 +}
  305 +
  306 +async function loadPipelineDetail(pipelineId) {
  307 + try {
  308 + const { data } = await axios.get(`/api/company/${selectedCompany.value.id}/pipelines/${pipelineId}`)
  309 + selectedPipeline.value = data
  310 + } catch (e) { console.error(e) }
  311 +}
  312 +
  313 +async function loadGoals(id) {
  314 + try {
  315 + const { data } = await axios.get(`/api/company/${id}/goals`)
  316 + goals.value = data
  317 + } catch (e) { console.error(e) }
  318 +}
  319 +
  320 +function switchTab(key) {
  321 + activeTab.value = key
  322 + const id = selectedCompany.value?.id
  323 + if (!id) return
  324 + if (key === 'agents') loadCompanyAgents(id)
  325 + if (key === 'pipelines') loadPipelines(id)
  326 + if (key === 'goals') loadGoals(id)
  327 +}
  328 +
  329 +async function createCompany() {
  330 + try {
  331 + await axios.post('/api/company/create', newCompany.value)
  332 + showCreateModal.value = false
  333 + newCompany.value = { id: '', name: '', description: '' }
  334 + await loadCompanies()
  335 + } catch (e) { alert(e.response?.data?.error || e.message) }
  336 +}
  337 +
  338 +async function hireAgent() {
  339 + try {
  340 + const { data } = await axios.post(`/api/company/${selectedCompany.value.id}/hire`, hireForm.value)
  341 + hireResult.value = data
  342 + await loadCompanyAgents(selectedCompany.value.id)
  343 + setTimeout(() => { hireResult.value = null }, 3000)
  344 + } catch (e) { alert(e.response?.data?.error || e.message) }
  345 +}
  346 +
  347 +onMounted(() => {
  348 + loadCompanies()
  349 + loadTemplates()
  350 +})
  351 +</script>
  352 +
  353 +<style scoped>
  354 +.company-dashboard {
  355 + min-height: 100vh;
  356 + padding: 0;
  357 + background: var(--bg-primary);
  358 +}
  359 +
  360 +/* ═══ Top Bar ═══ */
  361 +.top-bar {
  362 + display: flex;
  363 + justify-content: space-between;
  364 + align-items: center;
  365 + padding: 16px 32px;
  366 + border-bottom: 1px solid var(--border-primary);
  367 + background: var(--bg-secondary);
  368 + backdrop-filter: blur(20px);
  369 + position: sticky; top: 0; z-index: 100;
  370 +}
  371 +.top-bar-left { display: flex; align-items: center; gap: 12px; }
  372 +.logo { font-size: 28px; }
  373 +.title { font-size: 20px; font-weight: 700; color: var(--text-primary); }
  374 +.top-bar-right { display: flex; gap: 12px; }
  375 +
  376 +/* ═══ Buttons ═══ */
  377 +.btn-create {
  378 + background: linear-gradient(135deg, var(--accent), #059669);
  379 + color: #fff; border: none; padding: 8px 20px;
  380 + border-radius: 10px; font-size: 14px; font-weight: 600;
  381 + cursor: pointer; transition: all 0.2s;
  382 + display: flex; align-items: center; gap: 6px;
  383 +}
  384 +.btn-create:hover { transform: translateY(-1px); box-shadow: 0 4px 20px rgba(16,185,129,0.3); }
  385 +.btn-create:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
  386 +
  387 +.btn-templates {
  388 + background: var(--bg-surface); color: var(--text-secondary);
  389 + border: 1px solid var(--border-primary); padding: 8px 16px;
  390 + border-radius: 10px; font-size: 13px; cursor: pointer;
  391 + transition: all 0.2s;
  392 +}
  393 +.btn-templates:hover { background: var(--bg-surface-hover); }
  394 +
  395 +.btn-sm {
  396 + background: var(--accent-bg); color: var(--accent);
  397 + border: 1px solid var(--border-active); padding: 6px 14px;
  398 + border-radius: 8px; font-size: 12px; font-weight: 600;
  399 + cursor: pointer; transition: all 0.2s;
  400 +}
  401 +.btn-sm:hover { background: var(--accent-bg-hover); }
  402 +
  403 +.btn-back {
  404 + background: none; border: none; color: var(--text-tertiary);
  405 + font-size: 14px; cursor: pointer; padding: 4px 0;
  406 + transition: color 0.2s;
  407 +}
  408 +.btn-back:hover { color: var(--accent); }
  409 +
  410 +.btn-cancel {
  411 + background: var(--bg-surface); color: var(--text-secondary);
  412 + border: 1px solid var(--border-primary); padding: 8px 20px;
  413 + border-radius: 10px; font-size: 14px; cursor: pointer;
  414 +}
  415 +
  416 +/* ═══ Company Grid ═══ */
  417 +.company-grid {
  418 + display: grid;
  419 + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  420 + gap: 20px; padding: 32px;
  421 +}
  422 +
  423 +.company-card {
  424 + background: var(--bg-secondary);
  425 + border: 1px solid var(--border-primary);
  426 + border-radius: 16px; padding: 24px; cursor: pointer;
  427 + transition: all 0.25s ease;
  428 +}
  429 +.company-card:hover {
  430 + border-color: var(--border-active);
  431 + transform: translateY(-2px);
  432 + box-shadow: 0 8px 32px rgba(16,185,129,0.08);
  433 +}
  434 +.company-card.status-running { border-left: 3px solid var(--accent); }
  435 +.company-card.status-shutdown { opacity: 0.5; }
  436 +
  437 +.card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
  438 +.card-emoji { font-size: 32px; }
  439 +.card-status {
  440 + padding: 4px 10px; border-radius: 20px;
  441 + font-size: 11px; font-weight: 600; text-transform: uppercase;
  442 +}
  443 +.card-status.running { background: rgba(16,185,129,0.15); color: var(--accent-light); }
  444 +.card-status.creating { background: rgba(59,130,246,0.15); color: var(--blue-text); }
  445 +.card-status.paused { background: rgba(251,191,36,0.15); color: var(--yellow-text); }
  446 +.card-status.shutdown { background: rgba(255,255,255,0.05); color: var(--text-muted); }
  447 +.card-status.active { background: rgba(16,185,129,0.15); color: var(--accent-light); }
  448 +.card-status.completed { background: rgba(16,185,129,0.15); color: var(--accent-light); }
  449 +.card-status.failed { background: rgba(239,68,68,0.15); color: #f87171; }
  450 +.card-status.training { background: rgba(139,92,246,0.15); color: var(--purple-text); }
  451 +.card-status.suspended { background: rgba(255,255,255,0.05); color: var(--text-muted); }
  452 +.card-status.ready { background: rgba(59,130,246,0.15); color: var(--blue-text); }
  453 +
  454 +.card-name { font-size: 18px; font-weight: 700; margin-bottom: 8px; color: var(--text-primary); }
  455 +.card-meta { display: flex; gap: 16px; font-size: 13px; color: var(--text-tertiary); }
  456 +
  457 +/* ═══ Empty State ═══ */
  458 +.empty-state {
  459 + grid-column: 1 / -1;
  460 + display: flex; flex-direction: column; align-items: center; justify-content: center;
  461 + text-align: center; padding: 80px 20px;
  462 + color: var(--text-muted);
  463 +}
  464 +.empty-icon { font-size: 64px; display: block; margin-bottom: 16px; }
  465 +.empty-mini { padding: 32px; text-align: center; color: var(--text-muted); font-size: 14px; }
  466 +
  467 +/* ═══ Company Detail ═══ */
  468 +.company-detail { padding: 32px; }
  469 +.detail-header { margin-bottom: 24px; }
  470 +.detail-title-row { display: flex; align-items: center; gap: 12px; margin: 8px 0; }
  471 +.detail-title-row h2 { font-size: 24px; font-weight: 800; }
  472 +.detail-desc { color: var(--text-tertiary); font-size: 14px; }
  473 +
  474 +/* ═══ Tabs ═══ */
  475 +.tabs {
  476 + display: flex; gap: 4px;
  477 + border-bottom: 1px solid var(--border-primary);
  478 + margin-bottom: 24px;
  479 +}
  480 +.tabs button {
  481 + background: none; border: none; padding: 12px 20px;
  482 + font-size: 14px; font-weight: 500; cursor: pointer;
  483 + color: var(--text-tertiary); border-bottom: 2px solid transparent;
  484 + transition: all 0.2s;
  485 +}
  486 +.tabs button:hover { color: var(--text-primary); }
  487 +.tabs button.active {
  488 + color: var(--accent); border-bottom-color: var(--accent);
  489 +}
  490 +
  491 +.tab-content { animation: fadeIn 0.3s ease; }
  492 +@keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
  493 +
  494 +.section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
  495 +.section-header h3 { font-size: 16px; font-weight: 700; }
  496 +
  497 +/* ═══ Agents Grid ═══ */
  498 +.agents-grid {
  499 + display: grid;
  500 + grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  501 + gap: 12px;
  502 +}
  503 +.agent-card {
  504 + display: flex; align-items: center; gap: 12px;
  505 + background: var(--bg-surface); border: 1px solid var(--border-primary);
  506 + border-radius: 12px; padding: 16px; transition: all 0.2s;
  507 +}
  508 +.agent-card:hover { border-color: var(--border-active); }
  509 +.agent-avatar { font-size: 28px; }
  510 +.agent-info { flex: 1; }
  511 +.agent-name { display: block; font-size: 14px; font-weight: 600; color: var(--text-primary); }
  512 +.agent-type { font-size: 11px; color: var(--text-muted); text-transform: uppercase; }
  513 +.agent-status { font-size: 11px; padding: 3px 8px; border-radius: 12px; font-weight: 600; }
  514 +.agent-status.active { background: rgba(16,185,129,0.15); color: var(--accent-light); }
  515 +.agent-status.training { background: rgba(139,92,246,0.15); color: var(--purple-text); }
  516 +.agent-status.ready { background: rgba(59,130,246,0.15); color: var(--blue-text); }
  517 +.agent-status.suspended { background: rgba(255,255,255,0.05); color: var(--text-muted); }
  518 +
  519 +/* ═══ Pipelines ═══ */
  520 +.pipeline-list { display: flex; flex-direction: column; gap: 8px; margin-bottom: 24px; }
  521 +.pipeline-card {
  522 + display: flex; justify-content: space-between; align-items: center;
  523 + padding: 16px 20px; background: var(--bg-surface);
  524 + border: 1px solid var(--border-primary); border-radius: 12px;
  525 + cursor: pointer; transition: all 0.2s;
  526 +}
  527 +.pipeline-card:hover { border-color: var(--border-active); }
  528 +.pipeline-header { display: flex; align-items: center; gap: 12px; }
  529 +.pipeline-name { font-weight: 600; font-size: 14px; }
  530 +.pipeline-steps { font-size: 12px; color: var(--text-muted); }
  531 +
  532 +.pipeline-detail {
  533 + background: var(--bg-surface); border: 1px solid var(--border-primary);
  534 + border-radius: 12px; padding: 24px; margin-bottom: 24px;
  535 +}
  536 +.pipeline-detail h4 { font-size: 16px; font-weight: 700; margin-bottom: 16px; }
  537 +
  538 +.step-flow { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
  539 +.step-node { display: flex; align-items: center; gap: 8px; }
  540 +.step-index {
  541 + width: 28px; height: 28px; border-radius: 50%;
  542 + background: var(--accent-bg); color: var(--accent);
  543 + display: flex; align-items: center; justify-content: center;
  544 + font-size: 12px; font-weight: 700;
  545 +}
  546 +.step-info { display: flex; flex-direction: column; }
  547 +.step-name { font-size: 13px; font-weight: 600; color: var(--text-primary); }
  548 +.step-agent { font-size: 11px; color: var(--text-muted); }
  549 +.step-arrow { font-size: 18px; color: var(--text-muted); margin: 0 4px; }
  550 +
  551 +.cron-badge {
  552 + margin-top: 12px; padding: 8px 14px;
  553 + background: rgba(139,92,246,0.1); color: var(--purple-text);
  554 + border-radius: 8px; font-size: 12px; font-weight: 600;
  555 + display: inline-block;
  556 +}
  557 +
  558 +.runs-section h4 { font-size: 14px; font-weight: 700; margin-bottom: 12px; color: var(--text-secondary); }
  559 +.run-card {
  560 + padding: 14px 18px; background: var(--bg-surface);
  561 + border: 1px solid var(--border-primary); border-radius: 10px;
  562 + margin-bottom: 8px;
  563 +}
  564 +.run-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
  565 +.run-id { font-size: 12px; font-weight: 600; color: var(--text-secondary); font-family: monospace; }
  566 +.run-meta { display: flex; gap: 16px; font-size: 12px; color: var(--text-muted); }
  567 +
  568 +/* ═══ Goals ═══ */
  569 +.goals-list { display: flex; flex-direction: column; gap: 8px; }
  570 +.goal-card {
  571 + padding: 16px 20px; background: var(--bg-surface);
  572 + border: 1px solid var(--border-primary); border-radius: 12px;
  573 +}
  574 +.goal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
  575 +.goal-id { font-size: 12px; font-weight: 700; color: var(--accent); font-family: monospace; }
  576 +.goal-title { font-size: 15px; font-weight: 600; margin-bottom: 6px; }
  577 +.goal-meta { font-size: 12px; color: var(--text-muted); }
  578 +
  579 +/* ═══ Modal ═══ */
  580 +.modal-overlay {
  581 + position: fixed; inset: 0; background: rgba(0,0,0,0.6);
  582 + display: flex; align-items: center; justify-content: center; z-index: 200;
  583 + backdrop-filter: blur(4px);
  584 +}
  585 +.modal {
  586 + background: var(--bg-secondary); border: 1px solid var(--border-primary);
  587 + border-radius: 20px; padding: 32px; width: 420px; max-width: 90vw;
  588 + animation: slideUp 0.3s ease;
  589 +}
  590 +@keyframes slideUp { from { transform: translateY(20px); opacity: 0; } to { transform: none; opacity: 1; } }
  591 +
  592 +.modal h3 { font-size: 18px; font-weight: 700; margin-bottom: 20px; }
  593 +.modal label {
  594 + display: block; font-size: 12px; font-weight: 600;
  595 + color: var(--text-muted); margin: 12px 0 6px; text-transform: uppercase;
  596 +}
  597 +.modal input, .modal textarea, .modal select {
  598 + width: 100%; padding: 10px 14px; border-radius: 10px;
  599 + border: 1px solid var(--border-primary); background: var(--bg-input);
  600 + color: var(--text-primary); font-size: 14px; font-family: inherit;
  601 + transition: border-color 0.2s;
  602 +}
  603 +.modal input:focus, .modal textarea:focus, .modal select:focus {
  604 + outline: none; border-color: var(--accent);
  605 + background: var(--bg-input-focus);
  606 +}
  607 +.modal-actions { display: flex; gap: 12px; margin-top: 24px; justify-content: flex-end; }
  608 +
  609 +.hire-result {
  610 + margin-top: 12px; padding: 10px 14px; border-radius: 10px;
  611 + font-size: 13px; font-weight: 600; animation: fadeIn 0.3s;
  612 +}
  613 +.hire-result.hired_from_template { background: rgba(16,185,129,0.1); color: var(--accent-light); }
  614 +.hire-result.training { background: rgba(139,92,246,0.1); color: var(--purple-text); }
  615 +.hire-result.already_exists { background: rgba(251,191,36,0.1); color: var(--yellow-text); }
  616 +
  617 +/* ═══ Templates Panel ═══ */
  618 +.templates-panel {
  619 + position: fixed; right: 0; top: 0; bottom: 0; width: 340px;
  620 + background: var(--bg-secondary); border-left: 1px solid var(--border-primary);
  621 + z-index: 150; padding: 24px; overflow-y: auto;
  622 + animation: slideLeft 0.3s ease;
  623 +}
  624 +@keyframes slideLeft { from { transform: translateX(100%); } to { transform: none; } }
  625 +
  626 +.panel-header {
  627 + display: flex; justify-content: space-between; align-items: center;
  628 + margin-bottom: 20px;
  629 +}
  630 +.panel-header h3 { font-size: 16px; font-weight: 700; }
  631 +.panel-header button { background: none; border: none; color: var(--text-muted); cursor: pointer; font-size: 18px; }
  632 +
  633 +.template-list { display: flex; flex-direction: column; gap: 8px; }
  634 +.template-card {
  635 + padding: 14px 16px; background: var(--bg-surface);
  636 + border: 1px solid var(--border-primary); border-radius: 10px;
  637 +}
  638 +.tpl-name { font-size: 14px; font-weight: 600; margin-bottom: 6px; }
  639 +.tpl-meta { display: flex; gap: 10px; font-size: 11px; color: var(--text-muted); }
  640 +.tpl-type { background: var(--bg-surface-hover); padding: 2px 8px; border-radius: 6px; text-transform: uppercase; }
  641 +.tpl-score { color: var(--yellow-text); }
  642 +</style>
... ...