Files
hr_promo/app/app.vue
2026-02-14 12:38:46 +07:00

814 lines
33 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div data-theme="acid" class="min-h-screen bg-mesh text-base-content">
<NuxtRouteAnnouncer />
<aside
class="glass-card fixed left-4 top-4 z-20 flex h-[calc(100vh-2rem)] w-[360px] flex-col rounded-3xl p-5"
>
<div class="card glass-pane mb-3 rounded-2xl">
<div class="card-body py-4">
<div class="flex items-center gap-3">
<div class="avatar">
<div class="w-12 rounded-full bg-gradient-to-br from-fuchsia-400 via-violet-400 to-cyan-400 p-[2px]">
<div class="flex h-full w-full items-center justify-center rounded-full bg-base-100">
<span class="text-lg">🤖</span>
</div>
</div>
</div>
<span class="text-sm font-semibold text-left">Хантик</span>
</div>
</div>
</div>
<div class="flex-1 space-y-4 overflow-auto pr-1">
<div
v-for="message in chatHistory"
:key="message.id"
class="chat"
:class="message.from === 'ai' ? 'chat-start' : 'chat-end'"
>
<div v-if="message.from === 'ai'" class="chat-image avatar">
<div class="w-8 rounded-full bg-gradient-to-br from-fuchsia-400 via-violet-400 to-cyan-400 p-[1px]">
<div class="flex h-full w-full items-center justify-center rounded-full bg-base-100">
<span class="text-xs">🤖</span>
</div>
</div>
</div>
<div
class="chat-bubble text-sm"
:class="message.from === 'ai' ? 'glass-pane' : 'bg-sky-500/20'"
@click="message.step !== undefined && goToStep(message.step)"
>
{{ message.text }}
</div>
</div>
</div>
<div class="mt-5">
<div class="card glass-pane rounded-2xl">
<div class="card-body p-3">
<div class="join w-full">
<input class="input input-bordered input-lg join-item w-full" placeholder="Напишите сообщение" />
<button
class="btn btn-primary btn-lg join-item"
type="button"
aria-label="Отправить"
>
<svg viewBox="0 0 24 24" class="h-5 w-5" fill="currentColor" aria-hidden="true">
<path d="M3.4 20.6 22 12 3.4 3.4l1.9 6.4L16 12 5.3 14.2z" />
</svg>
</button>
</div>
</div>
</div>
</div>
</aside>
<main
class="min-h-screen w-full px-4 py-6 md:px-8 lg:pl-[400px]"
>
<section class="glass-card flex min-h-[calc(100vh-3rem)] flex-col gap-6 rounded-3xl p-6">
<div class="flex flex-wrap items-center justify-between gap-4">
<div>
<h1 class="section-title">{{ steps[currentStep].title }}</h1>
</div>
</div>
<div class="w-full">
<div class="steps steps-horizontal w-full">
<button
v-for="(step, index) in steps"
:key="step.id"
class="step cursor-pointer"
:class="index <= currentStep ? 'step-primary' : ''"
type="button"
@click="goToStep(index)"
>
<span class="sr-only">{{ step.title }}</span>
</button>
</div>
</div>
<div v-if="currentStep === 0" class="grid gap-4 lg:grid-cols-[3fr_1fr]">
<div class="space-y-6 text-[18px] leading-relaxed text-slate-900">
<input class="input input-bordered input-lg w-full text-base text-slate-900" value="Head of Growth" />
<div>
<p class="text-lg font-semibold text-slate-900">Общее описание</p>
<p class="mt-3 text-slate-900">
Мы ищем Head of Growth, который построит и масштабирует систему роста продукта: от активации и
первого успеха пользователя до удержания и расширения выручки. В роли нужно соединять продукт,
маркетинг, аналитику и продажи в единую growthмашину, которая стабильно генерирует результат.
</p>
<p class="mt-3 text-slate-900">
Это позиция для человека, который умеет превращать хаотичные идеи в управляемый pipeline
экспериментов, и не боится ответственности за цифры: активацию, конверсию, NRR и LTV.
</p>
</div>
<div>
<p class="text-lg font-semibold text-slate-900">Ключевые зоны ответственности</p>
<p class="mt-3 text-slate-900">
Построить backlog гипотез и систему их приоритизации, запустить регулярные спринты
growthэкспериментов и довести их до измеримого эффекта. Настроить единый аналитический контур
(события, воронки, когорты, атрибуция) и дать команде прозрачный обзор того, где теряются
пользователи и деньги.
</p>
<p class="mt-3 text-slate-900">
Объединить productмаркетинг, performance и productаналитику вокруг одного North Star и
превратить его в рабочий инструмент для всех команд. Отвечать за рост ключевых метрик на уровне
квартала и года.
</p>
</div>
<div>
<p class="text-lg font-semibold text-slate-900">Основные цели на 90 дней</p>
<p class="mt-3 text-slate-900">
Развернуть growthплейбук: определить метрики, собрать список гипотез, настроить процесс
экспериментов и выдать первые устойчивые инкременты в активации и удержании. Сформировать
отчётность и ритм, который позволит команде видеть эффект каждую неделю.
</p>
</div>
<div>
<p class="text-lg font-semibold text-slate-900">Взаимодействие с командами</p>
<p class="mt-3 text-slate-900">
Эта роль работает на пересечении продукта, маркетинга, продаж и CX. Вам нужно держать единый
контекст по воронке, предлагать решения, которые одинаково хорошо живут и в продукте, и в
каналах привлечения.
</p>
</div>
<div>
<p class="text-lg font-semibold text-slate-900">Пример рабочего дня</p>
<p class="mt-3 text-slate-900">
Утро разбор когорт и планирование новых экспериментов с продактом. Днём синки с маркетингом и
аналитикой, согласование приоритетов и ресурса. Вечером фиксация результатов спринта,
подготовка гипотез на следующую итерацию и обновление roadmap.
</p>
</div>
<div>
<p class="text-lg font-semibold text-slate-900">Каким мы видим кандидата</p>
<p class="mt-3 text-slate-900">
Опыт в продуктовой или growthроли с измеримым вкладом в метрики, сильные навыки работы с
данными, умение строить эксперименты и убеждать кроссфункциональные команды работать на один
результат.
</p>
</div>
<div>
<p class="text-lg font-semibold text-slate-900">О компании и продукте</p>
<p class="mt-3 text-slate-900">
Мы B2B SaaS с международной аудиторией, продуктом пользуются тысячи команд в Европе и США.
Сейчас фокус на масштабировании выручки и усилении ретеншна. Роль ключевая, напрямую влияет на
стратегию роста компании.
</p>
</div>
</div>
<div class="space-y-4">
<div class="glass-card relative cursor-pointer rounded-2xl p-5">
<button class="btn btn-ghost btn-circle btn-sm absolute right-4 top-4"></button>
<div class="voice-wave">
<span class="voice-bar"></span>
<span class="voice-bar"></span>
<span class="voice-bar"></span>
<span class="voice-bar"></span>
<span class="voice-bar"></span>
<span class="voice-bar"></span>
<span class="voice-bar"></span>
<span class="voice-bar"></span>
<span class="voice-bar"></span>
<span class="voice-bar"></span>
</div>
<p class="mt-4 text-sm text-slate-300">
Пройдите голосовое интервью мы заполним всё за вас.
</p>
</div>
<div class="glass-card rounded-2xl p-5">
<h3 class="font-display text-base font-semibold sr-only">Целевой профиль кандидата</h3>
<div class="mt-4">
<ClientOnly>
<div ref="vacancyChartEl" class="h-64 w-full"></div>
</ClientOnly>
</div>
</div>
</div>
</div>
<div v-else-if="currentStep === 1" class="grid gap-4">
<div class="mt-2 grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
<div class="glass-card rounded-2xl p-6 min-h-[180px]">
<div class="flex h-full flex-col items-center justify-center gap-4">
<div class="avatar">
<div class="w-16 rounded-full">
<img
alt="Аня"
src="https://i.pravatar.cc/160?img=12"
class="h-full w-full rounded-full object-cover"
loading="lazy"
/>
</div>
</div>
<p class="text-sm font-semibold">Аня · Product</p>
</div>
</div>
<div class="glass-card rounded-2xl p-6 min-h-[180px]">
<div class="flex h-full flex-col items-center justify-center gap-4">
<div class="avatar">
<div class="w-16 rounded-full">
<img
alt="Сергей"
src="https://i.pravatar.cc/160?img=32"
class="h-full w-full rounded-full object-cover"
loading="lazy"
/>
</div>
</div>
<p class="text-sm font-semibold">Сергей · Growth</p>
</div>
</div>
<div class="glass-card rounded-2xl p-6 min-h-[180px]">
<div class="flex h-full flex-col items-center justify-center gap-4">
<div class="avatar">
<div class="w-16 rounded-full">
<img
alt="Кира"
src="https://i.pravatar.cc/160?img=47"
class="h-full w-full rounded-full object-cover"
loading="lazy"
/>
</div>
</div>
<p class="text-sm font-semibold">Кира · Ops</p>
</div>
</div>
<div class="glass-card rounded-2xl border border-dashed border-white/20 p-4">
<div class="flex h-full flex-col justify-between">
<div class="flex flex-1 items-center justify-center">
<button class="btn btn-ghost btn-sm">Добавить видео</button>
</div>
<div class="mt-4 flex w-full items-center justify-between gap-3">
<input class="input input-bordered input-sm flex-1" value="https://hr.example/team-video" />
<button class="btn btn-ghost btn-sm">Поделиться</button>
</div>
</div>
</div>
</div>
</div>
<div v-else-if="currentStep === 2" class="grid gap-4">
<div class="glass-card rounded-2xl p-5">
<div class="flex items-center justify-between">
<h3 class="font-display text-lg font-semibold">Анализ видео</h3>
</div>
<div class="mt-4 flex flex-wrap items-center gap-4">
<span class="loading loading-spinner loading-lg text-info"></span>
<progress class="progress progress-info w-48" value="72" max="100"></progress>
</div>
</div>
<div class="glass-card rounded-2xl p-8">
<div class="flex flex-wrap items-center justify-between gap-3">
<h3 class="font-display text-lg font-semibold sr-only">Требования на основе команды</h3>
</div>
<div class="mt-6">
<ClientOnly>
<div ref="teamChartEl" class="h-80 w-full"></div>
</ClientOnly>
</div>
</div>
</div>
<div v-else-if="currentStep === 3" class="space-y-4">
<div class="flex flex-wrap items-center justify-between gap-3">
<div>
<h3 class="font-display text-lg font-semibold">Критерии совпадений</h3>
</div>
<div class="join rounded-full border border-white/10 p-1">
<button
v-for="method in methodologies"
:key="method.id"
class="btn btn-sm join-item"
:class="currentMethodology === method.id ? 'btn-primary' : 'btn-ghost'"
@click="currentMethodology = method.id"
>
{{ method.label }}
</button>
</div>
</div>
<transition-group name="reorder" tag="div" class="grid gap-4 md:grid-cols-2">
<div
v-for="candidate in sortedCandidates"
:key="candidate.id"
class="glass-card min-h-[320px] rounded-2xl p-4"
>
<div class="flex items-start justify-between">
<div>
<div class="flex items-center gap-3">
<div class="avatar">
<div class="w-10 rounded-full">
<img
:alt="candidate.name"
:src="candidate.avatar"
class="h-full w-full rounded-full object-cover"
loading="lazy"
/>
</div>
</div>
<div>
<h4 class="font-display text-lg font-semibold">{{ candidate.name }}</h4>
</div>
</div>
</div>
<div class="text-sm font-semibold text-fuchsia-600">
{{ candidate.match }}%
</div>
</div>
<div class="mt-4">
<ClientOnly>
<div
class="h-56 w-full"
:ref="setCandidateChartRef(candidate.id)"
></div>
</ClientOnly>
</div>
</div>
</transition-group>
</div>
<div v-else class="space-y-4">
<div class="space-y-3">
<div class="glass-card rounded-2xl p-4">
<div class="flex items-center justify-between gap-4">
<div class="flex items-center gap-3">
<div class="avatar">
<div class="w-10 rounded-full bg-slate-800">
<span class="flex h-full items-center justify-center text-xs">ИС</span>
</div>
</div>
<div>
<p class="text-sm font-semibold">Ирина Соколова · Head of Growth</p>
</div>
</div>
</div>
<div class="mt-3 flex flex-wrap items-center gap-3">
<label class="flex items-center gap-2 text-xs text-slate-300">
<input type="radio" name="meet-1" class="radio radio-sm" />
Сегодня 16:00
</label>
<label class="flex items-center gap-2 text-xs text-slate-300">
<input type="radio" name="meet-1" class="radio radio-sm" />
Завтра 11:30
</label>
<label class="flex items-center gap-2 text-xs text-slate-300">
<input type="radio" name="meet-1" class="radio radio-sm" />
Ср 15:00
</label>
</div>
</div>
<div class="glass-card rounded-2xl p-4">
<div class="flex items-center justify-between gap-4">
<div class="flex items-center gap-3">
<div class="avatar">
<div class="w-10 rounded-full bg-slate-800">
<span class="flex h-full items-center justify-center text-xs">КД</span>
</div>
</div>
<div>
<p class="text-sm font-semibold">Кира Дуброва · Product Growth</p>
</div>
</div>
</div>
<div class="mt-3 flex flex-wrap items-center gap-3">
<label class="flex items-center gap-2 text-xs text-slate-300">
<input type="radio" name="meet-2" class="radio radio-sm" />
Сегодня 18:00
</label>
<label class="flex items-center gap-2 text-xs text-slate-300">
<input type="radio" name="meet-2" class="radio radio-sm" />
Чт 12:00
</label>
<label class="flex items-center gap-2 text-xs text-slate-300">
<input type="radio" name="meet-2" class="radio radio-sm" />
Пт 10:30
</label>
</div>
</div>
<div class="glass-card rounded-2xl p-4">
<div class="flex items-center justify-between gap-4">
<div class="flex items-center gap-3">
<div class="avatar">
<div class="w-10 rounded-full bg-slate-800">
<span class="flex h-full items-center justify-center text-xs">АМ</span>
</div>
</div>
<div>
<p class="text-sm font-semibold">Алексей Марков · Growth Manager</p>
</div>
</div>
</div>
<div class="mt-3 flex flex-wrap items-center gap-3">
<label class="flex items-center gap-2 text-xs text-slate-300">
<input type="radio" name="meet-3" class="radio radio-sm" />
Ср 14:30
</label>
<label class="flex items-center gap-2 text-xs text-slate-300">
<input type="radio" name="meet-3" class="radio radio-sm" />
Чт 16:00
</label>
<label class="flex items-center gap-2 text-xs text-slate-300">
<input type="radio" name="meet-3" class="radio radio-sm" />
Пт 09:30
</label>
</div>
</div>
<div class="flex justify-center">
<button class="btn btn-primary btn-lg">Запланировать встречу</button>
</div>
</div>
</div>
<div class="mt-auto flex justify-end" v-if="currentStep !== steps.length - 1">
<button class="btn btn-primary btn-lg" @click="nextStep">
Далее
</button>
</div>
</section>
</main>
</div>
</template>
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, nextTick, ref, watch } from 'vue'
const steps = [
{
id: 'requirements',
title: 'Сформируйте требования к вакансии',
},
{
id: 'video',
title: 'Загрузите информацию о команде',
},
{
id: 'fingerprint',
title: 'Требования на основе команды',
},
{
id: 'recommendations',
title: 'Ознакомьтесь с кандидатами',
},
{
id: 'matches',
title: 'Запланируйте встречу',
}
]
const route = typeof useRoute === 'function' ? useRoute() : null
const router = typeof useRouter === 'function' ? useRouter() : null
const currentStep = ref(0)
const chatHistory = computed(() => chatByStep.slice(0, currentStep.value + 1).flat())
const goToStep = (index: number) => {
currentStep.value = Math.min(Math.max(index, 0), steps.length - 1)
}
const nextStep = () => {
goToStep(currentStep.value + 1)
}
const methodologies = [
{ id: 'structured', label: 'Структурированный разбор' },
{ id: 'situational', label: 'Ситуационный мэтч' },
{ id: 'role', label: 'Ролевой профиль' }
]
const axisLabelsByMethodology = {
structured: [
'Коммуникация/ясность',
'Решение проблем',
'Взаимодействие',
'Ownership/ответственность',
'Профессиональный опыт'
],
situational: [
'Конфликт-менеджмент',
'Переговоры',
'Командная работа',
'Межличностные навыки',
'Решение проблем в контексте роли'
],
role: ['Знания', 'Навыки', 'Умения', 'Рабочие активности', 'Контекст/условия роли']
} as const
const vacancyProfile = {
labels: [
'Коммуникация/ясность',
'Решение проблем',
'Ownership/ответственность',
'Взаимодействие',
'Темп/инициатива'
],
values: [80, 78, 82, 74, 76]
}
const teamProfile = {
labels: [
'Командная работа',
'Конфликт-менеджмент',
'Межличностные навыки',
'Взаимодействие',
'Контекст/условия роли'
],
values: [81, 73, 77, 75, 74]
}
const candidateProfiles = [
{
id: 1,
name: 'Ирина Соколова',
role: 'Growth Lead · B2B SaaS',
location: 'Берлин / Remote',
avatar: 'https://i.pravatar.cc/120?img=5',
tags: ['Experimentation', 'B2B', 'Lifecycle'],
highlight: 'темп запуска',
matchByMethodology: { structured: 91, situational: 84, role: 83 },
valuesByMethodology: {
structured: [88, 62, 79, 71, 66],
situational: [70, 86, 60, 75, 78],
role: [84, 64, 78, 76, 71]
}
},
{
id: 2,
name: 'Максим Орлов',
role: 'Head of Growth',
location: 'Лондон / Remote',
avatar: 'https://i.pravatar.cc/120?img=15',
tags: ['SQL', 'Funnels', 'Team Lead'],
highlight: 'структура',
matchByMethodology: { structured: 87, situational: 91, role: 90 },
valuesByMethodology: {
structured: [76, 58, 82, 68, 73],
situational: [64, 82, 58, 72, 83],
role: [80, 62, 74, 72, 69]
}
},
{
id: 3,
name: 'Лия Вернер',
role: 'Revenue Strategist',
location: 'Амстердам',
avatar: 'https://i.pravatar.cc/120?img=36',
tags: ['PLG', 'Retention', 'Ops'],
highlight: 'удержание',
matchByMethodology: { structured: 85, situational: 79, role: 77 },
valuesByMethodology: {
structured: [72, 74, 65, 70, 81],
situational: [62, 80, 61, 69, 79],
role: [78, 66, 69, 74, 70]
}
},
{
id: 4,
name: 'Алексей Нестеров',
role: 'Growth Manager',
location: 'Барселона',
avatar: 'https://i.pravatar.cc/120?img=51',
tags: ['Paid', 'Analytics', 'B2B'],
highlight: 'скорость экспериментов',
matchByMethodology: { structured: 83, situational: 88, role: 88 },
valuesByMethodology: {
structured: [70, 60, 78, 64, 69],
situational: [66, 78, 63, 67, 74],
role: [74, 60, 71, 68, 73]
}
},
{
id: 5,
name: 'Анна Прайс',
role: 'Lifecycle Director',
location: 'Прага',
avatar: 'https://i.pravatar.cc/120?img=64',
tags: ['CRM', 'Email', 'Segmentation'],
highlight: 'процессы',
matchByMethodology: { structured: 81, situational: 82, role: 92 },
valuesByMethodology: {
structured: [68, 72, 59, 66, 84],
situational: [60, 76, 57, 70, 77],
role: [70, 58, 75, 66, 68]
}
},
{
id: 6,
name: 'Давид Клайн',
role: 'Growth Partner',
location: 'Цюрих',
avatar: 'https://i.pravatar.cc/120?img=68',
tags: ['Strategy', 'OKR', 'B2B'],
highlight: 'стратегия',
matchByMethodology: { structured: 79, situational: 86, role: 81 },
valuesByMethodology: {
structured: [66, 64, 73, 62, 70],
situational: [62, 74, 56, 68, 72],
role: [68, 56, 73, 64, 66]
}
}
] as const
const currentMethodology = ref<'structured' | 'situational' | 'role'>('structured')
const currentCandidates = computed(() =>
candidateProfiles.map((candidate) => ({
...candidate,
match: candidate.matchByMethodology[currentMethodology.value],
values: candidate.valuesByMethodology[currentMethodology.value]
}))
)
const sortedCandidates = computed(() =>
[...currentCandidates.value].sort((a, b) => b.match - a.match)
)
const currentCandidateAxisLabels = computed(
() => axisLabelsByMethodology[currentMethodology.value]
)
const vacancyChartEl = ref<HTMLDivElement | null>(null)
const teamChartEl = ref<HTMLDivElement | null>(null)
const candidateChartEls = new Map<number, HTMLDivElement>()
const candidateCharts = new Map<number, any>()
let vacancyChart: any = null
let teamChart: any = null
let echarts: any = null
const setCandidateChartRef = (id: number) => (el: HTMLDivElement | null) => {
if (el) {
candidateChartEls.set(id, el)
return
}
candidateChartEls.delete(id)
}
const buildRadarOption = (labels: string[], values: number[]) => ({
tooltip: {
trigger: 'item',
formatter: (params: { value: number[] }) =>
labels.map((label, index) => `${label}: ${params.value[index]}%`).join('<br/>')
},
radar: {
indicator: labels.map((label) => ({ name: label, max: 100 })),
splitNumber: 4,
axisName: { color: '#0f172a', fontSize: 10, fontWeight: 600 },
axisLine: { lineStyle: { color: 'rgba(148, 163, 184, 0.3)' } },
splitLine: { lineStyle: { color: 'rgba(148, 163, 184, 0.2)' } },
splitArea: {
areaStyle: {
color: ['rgba(255, 255, 255, 0.3)', 'rgba(255, 255, 255, 0.6)']
}
}
},
series: [
{
type: 'radar',
data: [
{
value: values,
areaStyle: { color: 'rgba(56, 189, 248, 0.35)' },
lineStyle: { color: 'rgba(56, 189, 248, 0.9)', width: 2 },
symbol: 'circle',
symbolSize: 4
}
]
}
]
})
const ensureEcharts = async () => {
if (echarts) return
const mod = await import('echarts')
echarts = mod.default ?? mod
}
const renderVacancyChart = () => {
if (!echarts || !vacancyChartEl.value) return
if (!vacancyChart) {
vacancyChart = echarts.init(vacancyChartEl.value)
}
vacancyChart.setOption(buildRadarOption(vacancyProfile.labels, vacancyProfile.values))
}
const renderTeamChart = () => {
if (!echarts || !teamChartEl.value) return
if (!teamChart) {
teamChart = echarts.init(teamChartEl.value)
}
teamChart.setOption(buildRadarOption(teamProfile.labels, teamProfile.values))
}
const renderCandidateCharts = () => {
if (!echarts) return
const labels = currentCandidateAxisLabels.value
const candidateIds = new Set(sortedCandidates.value.map((candidate) => candidate.id))
for (const [id, chart] of candidateCharts) {
if (!candidateIds.has(id)) {
chart.dispose()
candidateCharts.delete(id)
}
}
sortedCandidates.value.forEach((candidate) => {
const el = candidateChartEls.get(candidate.id)
if (!el) return
const existing = candidateCharts.get(candidate.id)
if (existing) {
existing.setOption(buildRadarOption(labels, candidate.values))
return
}
const chart = echarts.init(el)
chart.setOption(buildRadarOption(labels, candidate.values))
candidateCharts.set(candidate.id, chart)
})
}
const syncStepFromRoute = () => {
if (!route) return
const raw = Array.isArray(route.query.step) ? route.query.step[0] : route.query.step
const parsed = Number(raw)
if (!Number.isNaN(parsed)) {
goToStep(parsed - 1)
}
}
const handleResize = () => {
vacancyChart?.resize()
teamChart?.resize()
candidateCharts.forEach((chart) => chart.resize())
}
onMounted(async () => {
syncStepFromRoute()
await ensureEcharts()
renderVacancyChart()
renderTeamChart()
renderCandidateCharts()
window.addEventListener('resize', handleResize)
})
watch(
[currentMethodology, currentStep, sortedCandidates],
async () => {
await nextTick()
renderVacancyChart()
renderTeamChart()
renderCandidateCharts()
},
{ flush: 'post' }
)
watch(
() => route?.query?.step,
() => {
syncStepFromRoute()
}
)
watch(
() => currentStep.value,
(value) => {
if (!route || !router) return
const step = String(value + 1)
if (route.query.step !== step) {
router.replace({ query: { ...route.query, step } })
}
},
{ immediate: true }
)
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize)
vacancyChart?.dispose()
teamChart?.dispose()
candidateCharts.forEach((chart) => chart.dispose())
candidateCharts.clear()
})
const chatByStep = [
[
{ id: 'r1', from: 'ai', text: 'Опиши требования к вакансии. Заполни форму.', step: 0 },
{ id: 'r2', from: 'user', text: 'Ок, заполняю.' }
],
[
{ id: 'v1', from: 'ai', text: 'Запиши видео по сценарию и загрузи.', step: 1 },
{ id: 'v2', from: 'user', text: 'Ок, записываю.' }
],
[
{ id: 'f1', from: 'ai', text: 'AI сформировал отпечаток коллектива по видео.', step: 2 },
{ id: 'f2', from: 'user', text: 'Смотрю профиль.' }
],
[
{ id: 'p1', from: 'ai', text: 'Рекомендации готовы. Откликов нет.', step: 3 },
{ id: 'p2', from: 'user', text: 'Покажи топ.' }
],
[
{ id: 'm1', from: 'ai', text: 'Созданы мэтчи. Можно открыть чат или интро.', step: 4 },
{ id: 'm2', from: 'user', text: 'Открываю чат.' }
]
]
</script>