Commit 4d6e754f6b1535c4e3ea3e08e0fd6aad3902e623
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
Showing
2 changed files
with
647 additions
and
0 deletions
web/src/router/index.js
| @@ -16,6 +16,11 @@ const routes = [ | @@ -16,6 +16,11 @@ const routes = [ | ||
| 16 | props: true, | 16 | props: true, |
| 17 | }, | 17 | }, |
| 18 | { | 18 | { |
| 19 | + path: '/company', | ||
| 20 | + name: 'CompanyDashboard', | ||
| 21 | + component: () => import('@/views/CompanyDashboard.vue'), | ||
| 22 | + }, | ||
| 23 | + { | ||
| 19 | path: '/okr', | 24 | path: '/okr', |
| 20 | name: 'OkrDashboard', | 25 | name: 'OkrDashboard', |
| 21 | component: () => import('@/views/OkrDashboard.vue'), | 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> |