526 lines
12 KiB
Vue
526 lines
12 KiB
Vue
<script setup lang="ts">
|
|
import { computed, ref, onMounted, onUnmounted, watch } from 'vue';
|
|
import { mdiRobotOutline, mdiCheckCircleOutline, mdiCodeBraces, mdiMessageTextOutline } from '@mdi/js';
|
|
|
|
const { t } = useI18n();
|
|
|
|
// ─── State machine for demo cycle ───
|
|
type DemoState = 'idle' | 'working' | 'reviewing' | 'done';
|
|
const state = ref<DemoState>('idle');
|
|
|
|
// ─── Animated task text ───
|
|
const currentTask = ref('');
|
|
const taskFading = ref(false);
|
|
const taskMessages = computed(() => [
|
|
t('hero.demo.activity.authMiddleware'),
|
|
t('hero.demo.activity.unitTests'),
|
|
t('hero.demo.activity.reviewPr'),
|
|
t('hero.demo.activity.ciPipeline'),
|
|
t('hero.demo.activity.refactorDatabase'),
|
|
]);
|
|
let taskIndex = 0;
|
|
let charTimer: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
// ─── Agent activity indicators ───
|
|
const agents = ref([
|
|
{ name: 'Lead', color: '#00f0ff', status: 'idle' as string, icon: mdiRobotOutline },
|
|
{ name: 'Dev-1', color: '#ff00ff', status: 'idle' as string, icon: mdiCodeBraces },
|
|
{ name: 'Dev-2', color: '#39ff14', status: 'idle' as string, icon: mdiMessageTextOutline },
|
|
]);
|
|
|
|
// ─── Kanban mini-board ───
|
|
const kanbanTasks = ref([
|
|
{ id: 1, text: t('hero.demo.tasks.authApi'), col: 'todo' as string },
|
|
{ id: 2, text: t('hero.demo.tasks.unitTests'), col: 'todo' as string },
|
|
{ id: 3, text: t('hero.demo.tasks.ciSetup'), col: 'todo' as string },
|
|
]);
|
|
|
|
function typeNextChar(text: string, index: number) {
|
|
if (index >= text.length) { charTimer = null; return; }
|
|
currentTask.value = text.slice(0, index + 1);
|
|
const ch = text[index];
|
|
let delay = 30;
|
|
if (ch === '.' || ch === ',') delay = 100;
|
|
else if (ch === ' ') delay = 10;
|
|
charTimer = setTimeout(() => typeNextChar(text, index + 1), delay);
|
|
}
|
|
|
|
function stopTextAnimation() {
|
|
if (charTimer) { clearTimeout(charTimer); charTimer = null; }
|
|
}
|
|
|
|
// ─── Timer management ───
|
|
const timers: number[] = [];
|
|
function safeTimeout(fn: () => void, ms: number) {
|
|
const id = window.setTimeout(fn, ms);
|
|
timers.push(id);
|
|
return id;
|
|
}
|
|
function clearAllTimers() {
|
|
timers.forEach(clearTimeout);
|
|
timers.length = 0;
|
|
stopTextAnimation();
|
|
}
|
|
|
|
// ─── IntersectionObserver ───
|
|
const containerRef = ref<HTMLElement | null>(null);
|
|
const isVisible = ref(false);
|
|
let intObserver: IntersectionObserver | null = null;
|
|
|
|
// ─── Demo cycle ───
|
|
let cycleRunning = false;
|
|
|
|
function runCycle() {
|
|
if (!cycleRunning) return;
|
|
|
|
// Reset
|
|
state.value = 'idle';
|
|
currentTask.value = '';
|
|
taskFading.value = false;
|
|
kanbanTasks.value = [
|
|
{ id: 1, text: t('hero.demo.tasks.authApi'), col: 'todo' },
|
|
{ id: 2, text: t('hero.demo.tasks.unitTests'), col: 'todo' },
|
|
{ id: 3, text: t('hero.demo.tasks.ciSetup'), col: 'todo' },
|
|
];
|
|
agents.value.forEach(a => a.status = 'idle');
|
|
|
|
safeTimeout(() => {
|
|
if (!cycleRunning) return;
|
|
|
|
// Phase 1: Working
|
|
state.value = 'working';
|
|
agents.value[0].status = 'active';
|
|
agents.value[1].status = 'active';
|
|
kanbanTasks.value[0].col = 'progress';
|
|
|
|
const messages = taskMessages.value;
|
|
const task = messages[taskIndex % messages.length];
|
|
taskIndex++;
|
|
typeNextChar(task, 0);
|
|
|
|
safeTimeout(() => {
|
|
if (!cycleRunning) return;
|
|
kanbanTasks.value[1].col = 'progress';
|
|
agents.value[2].status = 'active';
|
|
}, 1200);
|
|
|
|
safeTimeout(() => {
|
|
if (!cycleRunning) return;
|
|
|
|
// Phase 2: Reviewing
|
|
state.value = 'reviewing';
|
|
kanbanTasks.value[0].col = 'review';
|
|
agents.value[0].status = 'reviewing';
|
|
|
|
safeTimeout(() => {
|
|
if (!cycleRunning) return;
|
|
|
|
// Phase 3: Done
|
|
state.value = 'done';
|
|
kanbanTasks.value[0].col = 'done';
|
|
kanbanTasks.value[1].col = 'review';
|
|
agents.value[0].status = 'done';
|
|
agents.value[1].status = 'reviewing';
|
|
|
|
safeTimeout(() => {
|
|
if (!cycleRunning) return;
|
|
kanbanTasks.value[1].col = 'done';
|
|
kanbanTasks.value[2].col = 'progress';
|
|
agents.value[1].status = 'done';
|
|
|
|
safeTimeout(() => {
|
|
taskFading.value = true;
|
|
safeTimeout(() => {
|
|
if (cycleRunning) runCycle();
|
|
}, 800);
|
|
}, 2000);
|
|
}, 1500);
|
|
}, 1500);
|
|
}, 2500);
|
|
}, 1500);
|
|
}
|
|
|
|
function startDemo() {
|
|
if (cycleRunning) return;
|
|
cycleRunning = true;
|
|
runCycle();
|
|
}
|
|
|
|
function stopDemo() {
|
|
cycleRunning = false;
|
|
clearAllTimers();
|
|
state.value = 'idle';
|
|
currentTask.value = '';
|
|
taskFading.value = false;
|
|
}
|
|
|
|
watch(isVisible, (visible) => {
|
|
if (visible) startDemo();
|
|
else stopDemo();
|
|
});
|
|
|
|
onMounted(() => {
|
|
intObserver = new IntersectionObserver(
|
|
([entry]) => { isVisible.value = entry.isIntersecting; },
|
|
{ threshold: 0.1 },
|
|
);
|
|
if (containerRef.value) intObserver.observe(containerRef.value);
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
stopDemo();
|
|
if (intObserver) { intObserver.disconnect(); intObserver = null; }
|
|
});
|
|
|
|
function colColor(col: string) {
|
|
switch (col) {
|
|
case 'todo': return '#64748b';
|
|
case 'progress': return '#00f0ff';
|
|
case 'review': return '#ffd700';
|
|
case 'done': return '#39ff14';
|
|
default: return '#64748b';
|
|
}
|
|
}
|
|
|
|
function colLabel(col: string) {
|
|
switch (col) {
|
|
case 'todo': return t('hero.demo.columns.todo');
|
|
case 'progress': return t('hero.demo.columns.progress');
|
|
case 'review': return t('hero.demo.columns.review');
|
|
case 'done': return t('hero.demo.columns.done');
|
|
default: return col.toUpperCase();
|
|
}
|
|
}
|
|
|
|
function statusDotColor(status: string) {
|
|
switch (status) {
|
|
case 'active': return '#00f0ff';
|
|
case 'reviewing': return '#ffd700';
|
|
case 'done': return '#39ff14';
|
|
default: return '#64748b';
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div ref="containerRef" class="hero-demo" role="img" :aria-label="t('hero.demo.ariaLabel')">
|
|
<div class="hero-demo__content">
|
|
<!-- Header -->
|
|
<div class="hero-demo__header">
|
|
<div class="hero-demo__title-row">
|
|
<span class="hero-demo__title">Agent Teams</span>
|
|
<span class="hero-demo__badge-live">
|
|
<span class="hero-demo__live-dot" />
|
|
{{ t('hero.demo.live') }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Agents row -->
|
|
<div class="hero-demo__agents">
|
|
<div
|
|
v-for="agent in agents"
|
|
:key="agent.name"
|
|
class="hero-demo__agent"
|
|
>
|
|
<div class="hero-demo__agent-avatar" :style="{ borderColor: agent.color }">
|
|
<v-icon :icon="agent.icon" size="16" :style="{ color: agent.color }" />
|
|
</div>
|
|
<span class="hero-demo__agent-name">{{ agent.name }}</span>
|
|
<span
|
|
class="hero-demo__agent-dot"
|
|
:style="{ background: statusDotColor(agent.status) }"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Mini kanban -->
|
|
<div class="hero-demo__kanban">
|
|
<div v-for="col in ['todo', 'progress', 'review', 'done']" :key="col" class="hero-demo__kanban-col">
|
|
<div class="hero-demo__kanban-label" :style="{ color: colColor(col) }">
|
|
{{ colLabel(col) }}
|
|
</div>
|
|
<div class="hero-demo__kanban-cards">
|
|
<TransitionGroup name="kanban-card">
|
|
<div
|
|
v-for="task in kanbanTasks.filter(t => t.col === col)"
|
|
:key="task.id"
|
|
class="hero-demo__kanban-card"
|
|
:style="{ borderLeftColor: colColor(col) }"
|
|
>
|
|
{{ task.text }}
|
|
</div>
|
|
</TransitionGroup>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Activity log -->
|
|
<div class="hero-demo__log">
|
|
<div class="hero-demo__log-line">
|
|
<v-icon :icon="mdiCheckCircleOutline" size="14" style="color: #39ff14; flex-shrink: 0" />
|
|
<span
|
|
class="hero-demo__log-text"
|
|
:class="{ 'hero-demo__log-text--fading': taskFading }"
|
|
>{{ currentTask || t('hero.demo.waiting') }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.hero-demo {
|
|
position: relative;
|
|
z-index: 1;
|
|
border-radius: 16px;
|
|
background: rgba(10, 10, 15, 0.95);
|
|
backdrop-filter: blur(20px);
|
|
-webkit-backdrop-filter: blur(20px);
|
|
border: 1px solid rgba(0, 240, 255, 0.15);
|
|
overflow: hidden;
|
|
min-height: 330px;
|
|
box-shadow:
|
|
0 20px 60px rgba(0, 0, 0, 0.6),
|
|
0 0 30px rgba(0, 240, 255, 0.05),
|
|
inset 0 1px 0 rgba(0, 240, 255, 0.1);
|
|
}
|
|
|
|
.hero-demo__content {
|
|
position: relative;
|
|
z-index: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
padding: 16px;
|
|
min-height: 330px;
|
|
gap: 12px;
|
|
}
|
|
|
|
/* ─── Header ─── */
|
|
.hero-demo__header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.hero-demo__title-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
width: 100%;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.hero-demo__title {
|
|
font-size: 15px;
|
|
font-weight: 700;
|
|
color: #e0e6ff;
|
|
font-family: "JetBrains Mono", monospace;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
.hero-demo__badge-live {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
padding: 3px 10px;
|
|
border-radius: 100px;
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.1em;
|
|
color: #39ff14;
|
|
background: rgba(57, 255, 20, 0.1);
|
|
border: 1px solid rgba(57, 255, 20, 0.2);
|
|
}
|
|
|
|
.hero-demo__live-dot {
|
|
width: 6px;
|
|
height: 6px;
|
|
border-radius: 50%;
|
|
background: #39ff14;
|
|
animation: livePulse 2s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes livePulse {
|
|
0%, 100% { opacity: 1; box-shadow: 0 0 4px rgba(57, 255, 20, 0.6); }
|
|
50% { opacity: 0.4; box-shadow: none; }
|
|
}
|
|
|
|
/* ─── Agents ─── */
|
|
.hero-demo__agents {
|
|
display: flex;
|
|
gap: 12px;
|
|
}
|
|
|
|
.hero-demo__agent {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 6px 10px;
|
|
border-radius: 8px;
|
|
background: rgba(255, 255, 255, 0.03);
|
|
border: 1px solid rgba(255, 255, 255, 0.06);
|
|
flex: 1;
|
|
}
|
|
|
|
.hero-demo__agent-avatar {
|
|
width: 28px;
|
|
height: 28px;
|
|
border-radius: 50%;
|
|
border: 1.5px solid;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: rgba(0, 0, 0, 0.3);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.hero-demo__agent-name {
|
|
font-size: 11px;
|
|
color: #a0a8c0;
|
|
font-weight: 600;
|
|
font-family: "JetBrains Mono", monospace;
|
|
}
|
|
|
|
.hero-demo__agent-dot {
|
|
width: 6px;
|
|
height: 6px;
|
|
border-radius: 50%;
|
|
margin-left: auto;
|
|
transition: background 0.3s ease;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* ─── Kanban ─── */
|
|
.hero-demo__kanban {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, 1fr);
|
|
gap: 6px;
|
|
flex: 1;
|
|
}
|
|
|
|
.hero-demo__kanban-col {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
}
|
|
|
|
.hero-demo__kanban-label {
|
|
font-size: 9px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
padding: 4px 0;
|
|
font-family: "JetBrains Mono", monospace;
|
|
opacity: 0.7;
|
|
}
|
|
|
|
.hero-demo__kanban-cards {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 3px;
|
|
min-height: 60px;
|
|
}
|
|
|
|
.hero-demo__kanban-card {
|
|
font-size: 10px;
|
|
padding: 6px 8px;
|
|
border-radius: 6px;
|
|
background: rgba(255, 255, 255, 0.04);
|
|
border-left: 2px solid;
|
|
color: #c8d6e5;
|
|
transition: all 0.4s ease;
|
|
font-family: "JetBrains Mono", monospace;
|
|
}
|
|
|
|
/* Card transition */
|
|
.kanban-card-enter-active,
|
|
.kanban-card-leave-active {
|
|
transition: all 0.4s ease;
|
|
}
|
|
|
|
.kanban-card-enter-from {
|
|
opacity: 0;
|
|
transform: translateX(-8px);
|
|
}
|
|
|
|
.kanban-card-leave-to {
|
|
opacity: 0;
|
|
transform: translateX(8px);
|
|
}
|
|
|
|
/* ─── Log ─── */
|
|
.hero-demo__log {
|
|
padding: 8px 10px;
|
|
border-radius: 8px;
|
|
background: rgba(0, 0, 0, 0.3);
|
|
border: 1px solid rgba(0, 240, 255, 0.08);
|
|
}
|
|
|
|
.hero-demo__log-line {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.hero-demo__log-text {
|
|
font-size: 12px;
|
|
color: #a0a8c0;
|
|
font-family: "JetBrains Mono", monospace;
|
|
transition: opacity 0.5s ease;
|
|
}
|
|
|
|
.hero-demo__log-text--fading {
|
|
opacity: 0;
|
|
}
|
|
|
|
/* ─── Responsive ─── */
|
|
@media (max-width: 960px) {
|
|
.hero-demo {
|
|
max-width: 460px;
|
|
margin: 0 auto;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 600px) {
|
|
.hero-demo {
|
|
border-radius: 12px;
|
|
min-height: 280px;
|
|
}
|
|
|
|
.hero-demo__content {
|
|
padding: 12px;
|
|
min-height: 280px;
|
|
gap: 8px;
|
|
}
|
|
|
|
.hero-demo__title {
|
|
font-size: 13px;
|
|
}
|
|
|
|
.hero-demo__agents {
|
|
gap: 6px;
|
|
}
|
|
|
|
.hero-demo__agent {
|
|
padding: 4px 6px;
|
|
}
|
|
|
|
.hero-demo__agent-name {
|
|
font-size: 9px;
|
|
}
|
|
|
|
.hero-demo__kanban-label {
|
|
font-size: 8px;
|
|
}
|
|
|
|
.hero-demo__kanban-card {
|
|
font-size: 9px;
|
|
padding: 4px 6px;
|
|
}
|
|
|
|
.hero-demo__log-text {
|
|
font-size: 10px;
|
|
}
|
|
}
|
|
</style>
|