Files
hr_promo/app/app.vue
Ruslan Bakiev 5e7ef93c12 Initial commit
2026-02-14 10:03:13 +07:00

680 lines
29 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">
<NuxtRouteAnnouncer />
<aside
class="glass-card fixed left-4 top-4 z-20 flex h-[calc(100vh-2rem)] w-[320px] flex-col rounded-3xl p-5"
>
<div class="mb-4 flex items-center justify-between">
<div>
<h2 class="font-display text-lg font-semibold">Навигатор вакансии</h2>
</div>
</div>
<div class="glass-pane mb-4 rounded-2xl p-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>
<div>
<p class="text-sm font-semibold">Хантик</p>
<p class="text-xs text-slate-400">AI-ассистент вакансии</p>
</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-slate-800">
<span class="flex h-full items-center justify-center text-xs">AI</span>
</div>
</div>
<div
class="chat-bubble text-sm"
:class="message.from === 'ai' ? 'glass-pane' : 'bg-sky-500/20'"
@click="message.step !== undefined && (currentStep = message.step)"
>
{{ message.text }}
</div>
</div>
</div>
<div class="mt-5">
<label class="input input-bordered glass-pane flex items-center gap-2 rounded-xl">
<span class="text-xs text-slate-400">Вы:</span>
<input class="grow text-base" placeholder="Напишите уточнение" />
<button class="btn btn-primary btn-lg">Отправить</button>
</label>
</div>
</aside>
<main
class="min-h-screen w-full px-4 py-6 md:px-8 lg:pl-[360px]"
>
<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 v-if="currentStep === 0" class="grid gap-4">
<div class="glass-pane rounded-2xl p-5">
<div class="flex flex-wrap items-center justify-between gap-3">
<div>
<h3 class="font-display text-lg font-semibold">Должностная инструкция</h3>
</div>
<button class="btn btn-lg btn-ghost">Пройти голосовое интервью</button>
</div>
<div class="mt-5 space-y-4">
<label class="form-control">
<span class="label-text text-xs uppercase tracking-[0.2em] text-slate-400">Роль</span>
<input class="input input-bordered glass-pane" value="Head of Growth" />
</label>
<div class="glass-card rounded-2xl p-4">
<div class="flex flex-wrap items-center justify-between gap-3">
<div>
<p class="text-xs uppercase tracking-[0.2em] text-slate-400">Голосовое интервью</p>
</div>
</div>
<div class="mt-4 rounded-2xl border border-sky-300/30 bg-gradient-to-br from-slate-900/70 via-slate-800/40 to-slate-900/80 p-4">
<div class="flex items-center justify-between">
<div class="space-y-1">
<p class="text-sm font-semibold">Сессия интервью</p>
</div>
<span class="text-xs text-slate-400">00:00</span>
</div>
<div class="mt-4 grid gap-2">
<div class="h-2 w-full overflow-hidden rounded-full bg-slate-800/80">
<div class="h-full w-2/5 rounded-full bg-gradient-to-r from-sky-400 to-cyan-300"></div>
</div>
</div>
<div class="mt-4 flex items-center justify-center">
<button class="btn btn-ghost btn-circle btn-lg"></button>
</div>
</div>
</div>
<div class="glass-pane rounded-2xl p-5">
<div class="mt-4 space-y-4">
<div>
<p class="text-sm font-semibold">Общее описание</p>
<p class="text-sm text-slate-300">
Роль отвечает за системный рост продуктовой воронки от активации до удержания. Основной фокус
масштабируемые эксперименты, которые увеличивают выручку и улучшают ключевые метрики продукта.
</p>
</div>
<div>
<p class="text-sm font-semibold">Основные цели</p>
<ul class="mt-2 list-disc space-y-1 pl-5 text-sm text-slate-300">
<li>Запускать 35 гипотез в месяц и доводить их до измеримых результатов.</li>
<li>Синхронизировать маркетинг, продукт и продажи вокруг единого North Star.</li>
<li>Настроить аналитическую систему, которая показывает, где теряем пользователей.</li>
</ul>
</div>
<div>
<p class="text-sm font-semibold">Пример рабочего дня</p>
<p class="text-sm text-slate-300">
Утро разбор когорт и планирование экспериментов с продактом. Днем созвоны с командами
маркетинга и аналитики, согласование приоритетов. Вечером подготовка отчета по результатам
спринта и формирование гипотез на следующую неделю.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div v-else-if="currentStep === 1" class="grid gap-4">
<div class="glass-pane rounded-2xl p-5">
<h3 class="font-display text-lg font-semibold">Видео по сценарию</h3>
<ol class="mt-4 list-decimal space-y-2 pl-5 text-sm text-slate-300">
<li>Контекст продукта и стадии роста.</li>
<li>Что значит успех для роли в первые 90 дней.</li>
<li>Как выглядит идеальное взаимодействие в команде.</li>
</ol>
<div class="mt-5 rounded-2xl border border-dashed border-white/20 p-6 text-center">
<p class="text-sm text-slate-300">Перетащите видео (до 3 минут) или загрузите вручную</p>
<button class="btn btn-lg btn-ghost mt-3">Загрузить видео</button>
</div>
<div class="mt-6">
<h4 class="text-sm font-semibold uppercase tracking-[0.2em] text-slate-400">Видео команды</h4>
<div class="mt-4 grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
<div class="glass-card rounded-2xl p-4">
<div class="flex items-start justify-between">
<div>
<p class="text-sm font-semibold">Аня · Product</p>
<p class="text-xs text-slate-400">1:12 · Вовлеченность</p>
</div>
<span class="badge badge-outline">Готово</span>
</div>
<div class="mt-3 h-24 rounded-xl bg-gradient-to-br from-slate-800/60 to-slate-900/80"></div>
</div>
<div class="glass-card rounded-2xl p-4">
<div class="flex items-start justify-between">
<div>
<p class="text-sm font-semibold">Сергей · Growth</p>
<p class="text-xs text-slate-400">1:48 · Фокус</p>
</div>
<span class="badge badge-outline">Готово</span>
</div>
<div class="mt-3 h-24 rounded-xl bg-gradient-to-br from-slate-800/60 to-slate-900/80"></div>
</div>
<div class="glass-card rounded-2xl p-4">
<div class="flex items-start justify-between">
<div>
<p class="text-sm font-semibold">Кира · Ops</p>
<p class="text-xs text-slate-400">0:56 · Структура</p>
</div>
<span class="badge badge-outline">Готово</span>
</div>
<div class="mt-3 h-24 rounded-xl bg-gradient-to-br from-slate-800/60 to-slate-900/80"></div>
</div>
</div>
</div>
</div>
</div>
<div v-else-if="currentStep === 2" class="grid gap-4">
<div class="glass-pane 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-pane rounded-2xl p-5">
<h3 class="font-display text-lg font-semibold">Карточка коллектива</h3>
<div class="mt-4 grid gap-4 md:grid-cols-[1.1fr_0.9fr]">
<div class="space-y-3">
<div class="flex items-center justify-between">
<span class="text-sm">Тональность</span>
<div class="badge badge-outline">Дружелюбная + прямолинейная</div>
</div>
<div class="flex items-center justify-between">
<span class="text-sm">Частота синков</span>
<div class="badge badge-outline">2-3 раза в неделю</div>
</div>
<div class="flex items-center justify-between">
<span class="text-sm">Решения</span>
<div class="badge badge-outline">Через эксперименты</div>
</div>
</div>
<div class="glass-card rounded-2xl p-4">
<p class="text-xs uppercase tracking-[0.2em] text-slate-400">Группа</p>
<p class="mt-2 text-lg font-semibold">Growth Core</p>
<p class="text-xs text-slate-400">4 участника · Ритм 2-3 синка</p>
<div class="mt-3 flex flex-wrap gap-2">
<span class="badge badge-outline">Темп</span>
<span class="badge badge-outline">Эксперименты</span>
<span class="badge badge-outline">Прямота</span>
</div>
</div>
</div>
</div>
</div>
<div v-else-if="currentStep === 3" class="space-y-4">
<div class="glass-pane rounded-2xl p-5">
<div class="flex flex-wrap items-center justify-between gap-3">
<div>
<h3 class="font-display text-lg font-semibold">Рекомендации кандидатов</h3>
</div>
</div>
</div>
<div class="glass-pane rounded-2xl p-5">
<div class="flex flex-wrap items-center justify-between gap-3">
<div>
<h3 class="font-display text-lg font-semibold">Методология совпадений</h3>
</div>
<div class="tabs tabs-boxed bg-transparent">
<a
v-for="method in methodologies"
:key="method.id"
class="tab"
:class="currentMethodology === method.id ? 'tab-active' : ''"
@click="currentMethodology = method.id"
>
{{ method.label }}
</a>
</div>
</div>
<div class="mt-5 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
<div
v-for="candidate in currentCandidates"
:key="candidate.id"
class="glass-card rounded-2xl p-4"
>
<div class="flex items-start justify-between">
<div>
<h4 class="font-display text-lg font-semibold">{{ candidate.name }}</h4>
<p class="text-sm text-slate-300">{{ candidate.role }}</p>
<p class="text-xs text-slate-400">{{ candidate.location }}</p>
</div>
<div class="radial-progress text-sky-300" :style="`--value:${candidate.match}; --size:3.2rem;`">
<span class="text-xs text-slate-100">{{ candidate.match }}%</span>
</div>
</div>
<div class="mt-4 grid grid-cols-[90px_1fr] gap-3">
<svg viewBox="0 0 100 100" class="h-24 w-24">
<circle cx="50" cy="50" r="38" fill="none" stroke="rgba(148, 163, 184, 0.2)" />
<circle cx="50" cy="50" r="24" fill="none" stroke="rgba(148, 163, 184, 0.2)" />
<circle cx="50" cy="50" r="10" fill="none" stroke="rgba(148, 163, 184, 0.2)" />
<g v-for="(axis, index) in radarAxes" :key="axis">
<line
:x1="50"
:y1="50"
:x2="axisPoints[index].x"
:y2="axisPoints[index].y"
stroke="rgba(148, 163, 184, 0.25)"
/>
</g>
<polygon
:points="radarPoints(candidate.values)"
fill="rgba(125, 211, 252, 0.35)"
stroke="rgba(125, 211, 252, 0.9)"
stroke-width="1.5"
/>
</svg>
<div class="space-y-2">
<div class="flex flex-wrap gap-2">
<span v-for="tag in candidate.tags" :key="tag" class="badge badge-outline">
{{ tag }}
</span>
</div>
<p class="text-xs text-slate-400">Сильнее всего: {{ candidate.highlight }}</p>
</div>
</div>
<div class="mt-4 flex items-center justify-between">
<div class="flex gap-2">
<button class="btn btn-lg btn-ghost">Профиль</button>
<button class="btn btn-lg btn-ghost">Сравнить</button>
</div>
<button class="btn btn-lg btn-ghost">Запросить интро</button>
</div>
</div>
</div>
</div>
</div>
<div v-else class="space-y-4">
<div class="glass-pane rounded-2xl p-5">
<div class="flex flex-wrap items-center justify-between gap-3">
<div>
<h3 class="font-display text-lg font-semibold">Мэтчи и диалоги</h3>
</div>
<button class="btn btn-lg btn-ghost">Сгенерировать ссылку для встреч</button>
</div>
<div class="mt-5 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>
<p class="text-xs text-slate-400">Последнее: «Готова к короткому созвону» · 2ч назад</p>
</div>
</div>
<span class="badge badge-success badge-outline">Match 91%</span>
</div>
<div class="mt-3 flex flex-wrap gap-2">
<button class="btn btn-lg btn-ghost">Открыть диалог</button>
<button class="btn btn-lg btn-ghost">Назначить встречу</button>
<button class="btn btn-lg btn-ghost">Предложить слоты</button>
</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>
<p class="text-xs text-slate-400">Последнее: «Вижу слоты на четверг» · 5ч назад</p>
</div>
</div>
<span class="badge badge-success badge-outline">Match 92%</span>
</div>
<div class="mt-3 flex flex-wrap gap-2">
<button class="btn btn-lg btn-ghost">Открыть диалог</button>
<button class="btn btn-lg btn-ghost">Назначить встречу</button>
<button class="btn btn-lg btn-ghost">Предложить слоты</button>
</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>
<p class="text-xs text-slate-400">Последнее: «Можно быстро согласовать календарь» · 1д назад</p>
</div>
</div>
<span class="badge badge-success badge-outline">Match 88%</span>
</div>
<div class="mt-3 flex flex-wrap gap-2">
<button class="btn btn-lg btn-ghost">Открыть диалог</button>
<button class="btn btn-lg btn-ghost">Назначить встречу</button>
<button class="btn btn-lg btn-ghost">Предложить слоты</button>
</div>
</div>
</div>
</div>
</div>
<div class="mt-auto flex justify-end">
<button class="btn btn-primary btn-lg" @click="nextStep">
Далее
</button>
</div>
</section>
</main>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
const steps = [
{
id: 'requirements',
title: 'Функциональные требования к вакансии',
},
{
id: 'video',
title: 'Видео по сценарию',
},
{
id: 'fingerprint',
title: 'AI-отпечаток коллектива',
},
{
id: 'recommendations',
title: 'Рекомендации кандидатов',
},
{
id: 'matches',
title: 'Мэтчи и чат',
}
]
const currentStep = ref(0)
const chatHistory = computed(() => chatByStep.slice(0, currentStep.value + 1).flat())
const nextStep = () => {
currentStep.value = Math.min(currentStep.value + 1, steps.length - 1)
}
const methodologies = [
{ id: 'coaching', label: 'Бизнес-коучинг' },
{ id: 'therapy', label: 'Психотерапия' },
{ id: 'product', label: 'Продуктовый мэтч' }
]
const candidatesByMethodology = {
coaching: [
{
id: 1,
name: 'Ирина Соколова',
role: 'Growth Lead · B2B SaaS',
location: 'Берлин / Remote',
match: 91,
values: [88, 62, 79, 71, 66],
tags: ['Experimentation', 'B2B', 'Lifecycle'],
highlight: 'темп запуска'
},
{
id: 2,
name: 'Максим Орлов',
role: 'Head of Growth',
location: 'Лондон / Remote',
match: 87,
values: [76, 58, 82, 68, 73],
tags: ['SQL', 'Funnels', 'Team Lead'],
highlight: 'структура'
},
{
id: 3,
name: 'Лия Вернер',
role: 'Revenue Strategist',
location: 'Амстердам',
match: 85,
values: [72, 74, 65, 70, 81],
tags: ['PLG', 'Retention', 'Ops'],
highlight: 'удержание'
},
{
id: 4,
name: 'Алексей Нестеров',
role: 'Growth Manager',
location: 'Барселона',
match: 83,
values: [70, 60, 78, 64, 69],
tags: ['Paid', 'Analytics', 'B2B'],
highlight: 'скорость экспериментов'
},
{
id: 5,
name: 'Анна Прайс',
role: 'Lifecycle Director',
location: 'Прага',
match: 81,
values: [68, 72, 59, 66, 84],
tags: ['CRM', 'Email', 'Segmentation'],
highlight: 'процессы'
},
{
id: 6,
name: 'Давид Клайн',
role: 'Growth Partner',
location: 'Цюрих',
match: 79,
values: [66, 64, 73, 62, 70],
tags: ['Strategy', 'OKR', 'B2B'],
highlight: 'стратегия'
}
],
therapy: [
{
id: 1,
name: 'Юлия Ланская',
role: 'Growth Coach',
location: 'Варшава',
match: 89,
values: [70, 86, 60, 75, 78],
tags: ['Care', 'Leadership', 'Empathy'],
highlight: 'эмпатия'
},
{
id: 2,
name: 'Николай Протасов',
role: 'Team Therapist',
location: 'Мюнхен',
match: 86,
values: [64, 82, 58, 72, 83],
tags: ['Feedback', 'Culture', 'People'],
highlight: 'культура'
},
{
id: 3,
name: 'Леа Хольм',
role: 'Org Development',
location: 'Копенгаген',
match: 84,
values: [62, 80, 61, 69, 79],
tags: ['Mentoring', 'Soft Skills', 'Sync'],
highlight: 'синхронизация'
},
{
id: 4,
name: 'Роман Игнатьев',
role: 'Leadership Coach',
location: 'Рига',
match: 82,
values: [66, 78, 63, 67, 74],
tags: ['Coaching', 'Trust', 'Care'],
highlight: 'поддержка'
},
{
id: 5,
name: 'София Брукс',
role: 'Culture Partner',
location: 'Лиссабон',
match: 80,
values: [60, 76, 57, 70, 77],
tags: ['Culture', 'Energy', 'Rhythm'],
highlight: 'тональность'
},
{
id: 6,
name: 'Тимур Аббасов',
role: 'People Growth',
location: 'Стамбул',
match: 78,
values: [62, 74, 56, 68, 72],
tags: ['Well-being', 'Coaching', 'Values'],
highlight: 'ценности'
}
],
product: [
{
id: 1,
name: 'Кира Дуброва',
role: 'Product Growth',
location: 'Стокгольм',
match: 92,
values: [84, 64, 78, 76, 71],
tags: ['PLG', 'Activation', 'Metrics'],
highlight: 'продуктовый мэтч'
},
{
id: 2,
name: 'Сергей Пак',
role: 'Growth PM',
location: 'Хельсинки',
match: 88,
values: [80, 62, 74, 72, 69],
tags: ['Experiment', 'North Star', 'Team'],
highlight: 'структура роста'
},
{
id: 3,
name: 'Мария Йенс',
role: 'Product Strategy',
location: 'Вена',
match: 86,
values: [78, 66, 69, 74, 70],
tags: ['Insight', 'Journey', 'B2B'],
highlight: 'customer insight'
},
{
id: 4,
name: 'Эмиль Громов',
role: 'Lifecycle PM',
location: 'Таллин',
match: 84,
values: [74, 60, 71, 68, 73],
tags: ['Retention', 'PLG', 'SQL'],
highlight: 'ретеншн'
},
{
id: 5,
name: 'Дарья Нор',
role: 'Growth Analyst',
location: 'Краков',
match: 82,
values: [70, 58, 75, 66, 68],
tags: ['Analytics', 'BI', 'Funnels'],
highlight: 'данные'
},
{
id: 6,
name: 'Феликс Штайн',
role: 'Growth Ops',
location: 'Женева',
match: 80,
values: [68, 56, 73, 64, 66],
tags: ['Process', 'Automation', 'B2B'],
highlight: 'операционка'
}
]
} as const
const currentMethodology = ref<keyof typeof candidatesByMethodology>('coaching')
const currentCandidates = computed(() => candidatesByMethodology[currentMethodology.value])
const radarAxes = ['Стратегия', 'Эмпатия', 'Темп', 'Креатив', 'Стабильность']
const axisPoints = radarAxes.map((_, index) => {
const angle = (-90 + (360 / radarAxes.length) * index) * (Math.PI / 180)
const radius = 38
return {
x: 50 + radius * Math.cos(angle),
y: 50 + radius * Math.sin(angle)
}
})
const radarPoints = (values: number[]) => {
const radius = 38
return values
.map((value, index) => {
const angle = (-90 + (360 / values.length) * index) * (Math.PI / 180)
const r = (value / 100) * radius
const x = 50 + r * Math.cos(angle)
const y = 50 + r * Math.sin(angle)
return `${x.toFixed(1)},${y.toFixed(1)}`
})
.join(' ')
}
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>