1729 lines
66 KiB
Markdown
1729 lines
66 KiB
Markdown
# ТЕХНИЧЕСКОЕ ЗАДАНИЕ: PMO AI Assistant
|
||
## Автоматизация проектного офиса с использованием искусственного интеллекта
|
||
|
||
---
|
||
|
||
## 1. РЕШАЕМАЯ ЗАДАЧА
|
||
|
||
### 1.1. Бизнес-контекст
|
||
Проектный офис компании ведет множество параллельных проектов, по которым регулярно проводятся встречи различных типов (рабочие, оперативные советы, управляющие советы). Текущие проблемы:
|
||
- **Ручное ведение протоколов** занимает 30-60% времени встречи
|
||
- **Потеря контекста** между встречами — ответственные не помнят статус предыдущих мероприятий
|
||
- **Отсутствие единой базы знаний** по проектам — сложно найти информацию по похожим встречам
|
||
- **Разрозненность данных** — транскрибации в одном месте, протоколы в другом, мероприятия в третьем
|
||
- **Сложность контроля исполнения** — нет автоматического отслеживания просроченных задач
|
||
|
||
### 1.2. Целевое состояние
|
||
Создать **единый AI-powered сервис** для проектного офиса, который:
|
||
- Автоматически транскрибирует аудио встреч (или принимает текстовые исходники)
|
||
- Генерирует структурированные протоколы трех типов с учетом истории проекта
|
||
- Обеспечивает семантический поиск по всем встречам ("найди встречи, где обсуждали риски по модулю авторизации")
|
||
- Формирует актуальный статус проекта и диаграмму Ганта на основе всех мероприятий
|
||
- Позволяет редактировать протоколы как текст, так и через AI-чат
|
||
- Экспортирует протоколы в DOCX для рассылки подписантам
|
||
|
||
### 1.3. Описание сервиса
|
||
**PMO AI Assistant** — это веб-приложение (SPA + REST API), которое:
|
||
- Работает в браузере (Chrome, Firefox, Edge, Safari последних версий)
|
||
- Не требует установки на рабочие станции пользователей
|
||
- Интегрируется с корпоративными ВКС (MTS Link, Bitrix24, Яндекс.Телемост) для автоматической загрузки транскрибаций
|
||
- Использует LLM (OpenAI-совместимые API) для генерации протоколов
|
||
- Хранит данные в PostgreSQL с поддержкой векторного поиска (pgvector)
|
||
- Может работать как с облачными, так и с локальными моделями векторизации
|
||
|
||
---
|
||
|
||
## 2. СТРАНИЦЫ SPA И ФУНКЦИОНАЛ
|
||
|
||
### 2.1. Структура приложения
|
||
|
||
```
|
||
/ → Дашборд (обзор всех проектов)
|
||
/projects → Таблица проектов (список)
|
||
/projects/:id → Карточка проекта (детали)
|
||
/meetings → Таблица встреч (список)
|
||
/meetings/:id → Карточка встречи (работа с протоколом)
|
||
/settings → Настройки системы
|
||
/settings/prompts → Управление промптами
|
||
/settings/users → Управление пользователями
|
||
/settings/integrations → Настройки интеграций (ВКС)
|
||
```
|
||
|
||
### 2.2. Детальное описание страниц
|
||
|
||
#### **Дашборд (/)**
|
||
**Назначение:** Быстрый обзор состояния всех проектов и недавних встреч.
|
||
|
||
**Элементы:**
|
||
- **Карточки проектов** (Bento Grid):
|
||
- Краткое имя проекта
|
||
- Текущий этап (цветовой индикатор)
|
||
- AI-статус (краткое резюме от ИИ)
|
||
- Количество активных/просроченных мероприятий
|
||
- Дата последней встречи
|
||
- **Лента последних встреч** (Timeline):
|
||
- Дата, проект, тип встречи
|
||
- Краткое резюме (первые 2 предложения)
|
||
- Статус протокола (черновик / утвержден)
|
||
- **Виджет "Требует внимания"**:
|
||
- Просроченные мероприятия
|
||
- Проекты без встреч > 14 дней
|
||
- Нерешенные вопросы с высоким приоритетом
|
||
|
||
#### **Таблица проектов (/projects)**
|
||
**Назначение:** Управление списком проектов, быстрый поиск.
|
||
|
||
**Элементы:**
|
||
- **Таблица с колонками:**
|
||
- Краткое имя (кликабельно → переход в карточку)
|
||
- Полное наименование
|
||
- Этап (badge с цветом)
|
||
- Ответственный ПМ
|
||
- AI-статус (краткий текст)
|
||
- Дата создания
|
||
- Действия (⋮ меню: редактировать, архивировать)
|
||
- **Панель фильтров (слева):**
|
||
- Текстовый поиск (по всем полям)
|
||
- Фильтр по этапу (multi-select)
|
||
- Фильтр по ответственному
|
||
- Фильтр по дате создания
|
||
- **Кнопка "+ Новый проект"** (открывает модальное окно)
|
||
|
||
#### **Карточка проекта (/projects/:id)**
|
||
**Назначение:** Детальная информация о проекте, история встреч, визуализация.
|
||
|
||
**Вкладки:**
|
||
1. **Обзор:**
|
||
- Карточка проекта (все поля, редактируемые)
|
||
- AI-статус (большой блок, кнопка "Обновить статус")
|
||
- Диаграмма Ганта (Mermaid.js, интерактивная)
|
||
- Статистика: всего встреч, активных мероприятий, просроченных
|
||
2. **Встречи:**
|
||
- Таблица встреч этого проекта (отфильтрованная)
|
||
- Кнопка "+ Новая встреча"
|
||
3. **Мероприятия:**
|
||
- Сводная таблица всех мероприятий по проекту
|
||
- Фильтры: статус, ответственный, срок
|
||
- Экспорт в Excel
|
||
|
||
#### **Таблица встреч (/meetings)**
|
||
**Назначение:** Управление всеми встречами, семантический поиск.
|
||
|
||
**Элементы:**
|
||
- **Таблица с колонками:**
|
||
- Дата
|
||
- Проект (ссылка)
|
||
- Тип встречи (badge)
|
||
- Место
|
||
- Ответственный
|
||
- Краткое резюме (первые 100 символов)
|
||
- Статус протокола
|
||
- **Панель фильтров:**
|
||
- **Векторный поиск** (текстовое поле: "О чем были встречи?")
|
||
- Фильтр по проекту
|
||
- Фильтр по дате (range picker)
|
||
- Фильтр по ответственному
|
||
- Фильтр по типу встречи
|
||
- **Кнопка "+ Новая встреча"**
|
||
|
||
#### **Карточка встречи (/meetings/:id)**
|
||
**Назначение:** Основная рабочая область — загрузка исходников, генерация и редактирование протокола.
|
||
|
||
**Вкладки:**
|
||
1. **Исходник:**
|
||
- Если аудио: плеер + кнопка "Транскрибировать" (показывает прогресс)
|
||
- Если текст: редактор транскрибации (textarea с подсветкой синтаксиса)
|
||
- Кнопка "Сохранить исходник"
|
||
2. **Резюме:**
|
||
- Блок с универсальным резюме (генерируется автоматически)
|
||
- Кнопка "✏️ Редактировать" (переключает в режим редактирования)
|
||
- Кнопка "Перегенерировать"
|
||
3. **Протокол:**
|
||
- **Три кнопки генерации** (с иконками):
|
||
- 📋 Протокол встречи
|
||
- ⚡ Протокол оперативного совета
|
||
- 👔 Протокол управляющего совета
|
||
- **Markdown редактор** (md-editor-v3):
|
||
- Split view (редактор + превью)
|
||
- Панель инструментов (жирный, списки, таблицы)
|
||
- Возможность вставки таблиц (для перечня мероприятий)
|
||
- **AI-чат (справа, collapsible):**
|
||
- Выделяешь текст в протоколе → пишешь в чат: "Сделай более формальным"
|
||
- ИИ стримит ответ, можно принять или отклонить
|
||
- **Кнопки действий:**
|
||
- 💾 Сохранить черновик
|
||
- ✅ Утвердить протокол (триггерит обновление статуса проекта)
|
||
- 📥 Экспорт в DOCX
|
||
|
||
#### **Настройки (/settings)**
|
||
**Вкладки:**
|
||
1. **Проекты:** Управление справочником проектов (CRUD)
|
||
2. **Пользователи:** Управление доступами (роли: admin, pmo, viewer)
|
||
3. **Промпты:** Редактирование системных промптов для генерации протоколов
|
||
4. **Интеграции:** Настройка API-ключей для ВКС (MTS Link, Bitrix24, Яндекс.Телемост)
|
||
5. **Векторизация:** Выбор режима (OpenAI API / Локальная модель)
|
||
|
||
---
|
||
|
||
## 3. ИТОГОВЫЙ ФУНКЦИОНАЛ (MVP)
|
||
|
||
### 3.1. Функционал первого этапа (MVP)
|
||
✅ Управление проектами (CRUD, поиск, фильтры)
|
||
✅ Управление встречами (CRUD, загрузка аудио/текста)
|
||
✅ Транскрибация аудио через Whisper API (синхронно)
|
||
✅ Генерация универсального резюме встречи
|
||
✅ Генерация протоколов трех типов (с учетом истории проекта)
|
||
✅ Редактирование протоколов в Markdown
|
||
✅ AI-чат для редактирования выделенного текста (стриминг)
|
||
✅ Экспорт протокола в DOCX
|
||
✅ Векторный поиск по встречам (OpenAI embeddings / локальная модель)
|
||
✅ AI-статус проекта (генерируется при утверждении протокола)
|
||
✅ Диаграмма Ганта (Mermaid.js)
|
||
✅ Интеграция с одной ВКС (например, Яндекс.Телемост)
|
||
|
||
### 3.2. Функционал второго этапа
|
||
⏳ Очереди задач (BullMQ + Redis) для асинхронной транскрибации и генерации
|
||
⏳ WebSocket-уведомления о готовности транскрибации
|
||
⏳ Интеграция с остальными ВКС (MTS Link, Bitrix24)
|
||
⏳ Расширенная аналитика (дашборды, графики)
|
||
⏳ Мобильная адаптация
|
||
⏳ Мультиязычность (i18n)
|
||
|
||
---
|
||
|
||
## 4. АРХИТЕКТУРА И СТЕК ТЕХНОЛОГИЙ
|
||
|
||
### 4.1. Backend
|
||
- **Runtime:** Node.js 20+ (LTS)
|
||
- **Framework:** Express.js 4.x
|
||
- **ORM:** Drizzle ORM (TypeScript-first, легкая, с декларативными миграциями)
|
||
- **База данных:** PostgreSQL 15+ с расширением `pgvector`
|
||
- **Валидация:** Zod (для runtime-проверок)
|
||
- **ИИ-клиент:** OpenAI SDK (официальный)
|
||
- **Локальная векторизация:** @xenova/transformers (Hugging Face, ONNX Runtime)
|
||
- **Генерация DOCX:** docx (библиотека для создания Word-документов)
|
||
- **Аутентификация:** JWT (jsonwebtoken + bcrypt)
|
||
- **Логирование:** pino (быстрый структурированный логгер)
|
||
|
||
### 4.2. Frontend
|
||
- **Framework:** Vue 3 (Composition API, `<script setup>`)
|
||
- **UI Kit:** Vuetify 3 (Material Design 3)
|
||
- **State Management:** Pinia
|
||
- **Markdown редактор:** md-editor-v3
|
||
- **Визуализация:** mermaid (для диаграммы Ганта)
|
||
- **HTTP клиент:** axios
|
||
- **Стриминг:** Fetch API + ReadableStream (для SSE)
|
||
- **Роутинг:** Vue Router 4
|
||
- **Формы:** VeeValidate + Zod (для валидации)
|
||
|
||
### 4.3. Инфраструктура
|
||
- **Контейнеризация:** Docker + Docker Compose
|
||
- **Сервисы:**
|
||
- `postgres` (PostgreSQL 15 + pgvector)
|
||
- `backend` (Node.js + Express)
|
||
- `frontend` (Nginx для раздачи статики + reverse proxy)
|
||
- **Переменные окружения:** `.env` файл (ключи API, настройки БД)
|
||
|
||
### 4.4. Deployment
|
||
- Frontend собирается через Vite в папку `dist`
|
||
- Nginx отдает статику из `dist` и проксирует `/api/*` на backend
|
||
- PostgreSQL работает в отдельном контейнере с volume для данных
|
||
|
||
---
|
||
|
||
## 5. МОДЕЛЬ ДАННЫХ (PostgreSQL)
|
||
|
||
### 5.1. Принципы проектирования
|
||
1. **Реляционное ядро** — поля, по которым часто фильтруем/сортируем/делаем JOIN
|
||
2. **JSONB для гибкости** — "подушки безопасности" для будущих изменений
|
||
3. **Векторные поля** — для семантического поиска
|
||
4. **Аудит** — `created_at`, `updated_at` во всех таблицах
|
||
5. **Soft Delete** — флаг `is_deleted` вместо физического удаления
|
||
|
||
### 5.2. Таблица `users` (Пользователи)
|
||
```sql
|
||
CREATE TABLE users (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
email VARCHAR(255) UNIQUE NOT NULL,
|
||
password_hash VARCHAR(255) NOT NULL,
|
||
full_name VARCHAR(255) NOT NULL,
|
||
role VARCHAR(50) NOT NULL DEFAULT 'viewer', -- 'admin', 'pmo', 'viewer'
|
||
|
||
-- Подушка безопасности: дополнительные настройки пользователя
|
||
settings JSONB NOT NULL DEFAULT '{}',
|
||
-- Пример: {"theme": "dark", "notifications": true, "language": "ru"}
|
||
|
||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
);
|
||
|
||
-- Индексы
|
||
CREATE INDEX idx_users_email ON users(email);
|
||
CREATE INDEX idx_users_role ON users(role);
|
||
```
|
||
|
||
### 5.3. Таблица `projects` (Проекты)
|
||
```sql
|
||
CREATE TABLE projects (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
short_name VARCHAR(100) NOT NULL,
|
||
full_name VARCHAR(500) NOT NULL,
|
||
stage VARCHAR(50) NOT NULL DEFAULT 'preparation',
|
||
-- 'preparation', 'design', 'development', 'pre_ope', 'ope'
|
||
|
||
pmo_responsible_id UUID REFERENCES users(id),
|
||
|
||
-- Подушка безопасности: все расширяемые данные
|
||
metadata JSONB NOT NULL DEFAULT '{}',
|
||
-- Пример содержимого:
|
||
-- {
|
||
-- "customer": "Газпром",
|
||
-- "signatories": ["Иванов И.И.", "Петров П.П."],
|
||
-- "budget": 1000000,
|
||
-- "custom_fields": {"priority": "high", "department": "IT"}
|
||
-- }
|
||
|
||
-- AI-статус проекта (генерируется ИИ)
|
||
ai_status TEXT,
|
||
|
||
-- Код Mermaid для диаграммы Ганта
|
||
gantt_mermaid TEXT,
|
||
|
||
-- Подушка безопасности: настройки интеграций для проекта
|
||
integration_settings JSONB NOT NULL DEFAULT '{}',
|
||
-- Пример: {"vcs_provider": "yandex", "vcs_meeting_id": "12345"}
|
||
|
||
is_deleted BOOLEAN NOT NULL DEFAULT false,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
);
|
||
|
||
-- Индексы
|
||
CREATE INDEX idx_projects_short_name ON projects(short_name);
|
||
CREATE INDEX idx_projects_stage ON projects(stage);
|
||
CREATE INDEX idx_projects_pmo_responsible ON projects(pmo_responsible_id);
|
||
CREATE INDEX idx_projects_metadata ON projects USING GIN (metadata);
|
||
CREATE INDEX idx_projects_full_text ON projects USING GIN (
|
||
to_tsvector('russian', short_name || ' ' || full_name || ' ' || COALESCE(ai_status, ''))
|
||
);
|
||
|
||
-- Триггер для автоматического обновления updated_at
|
||
CREATE TRIGGER update_projects_updated_at
|
||
BEFORE UPDATE ON projects
|
||
FOR EACH ROW
|
||
EXECUTE FUNCTION update_updated_at_column();
|
||
```
|
||
|
||
### 5.4. Таблица `meetings` (Встречи)
|
||
```sql
|
||
CREATE TABLE meetings (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||
meeting_date DATE NOT NULL,
|
||
location VARCHAR(200),
|
||
type VARCHAR(50) NOT NULL DEFAULT 'standard',
|
||
-- 'standard', 'op_council', 'steering_council'
|
||
|
||
pmo_responsible_id UUID REFERENCES users(id),
|
||
|
||
-- Тип исходника
|
||
source_type VARCHAR(50) NOT NULL DEFAULT 'text',
|
||
-- 'audio', 'text', 'vcs_integration'
|
||
|
||
-- Исходные данные
|
||
raw_transcript TEXT,
|
||
audio_file_url VARCHAR(500), -- путь к аудиофайлу в storage
|
||
|
||
-- AI-обработанные данные
|
||
summary TEXT, -- универсальное резюме
|
||
protocol_markdown TEXT, -- итоговый протокол в Markdown
|
||
|
||
-- Подушка безопасности: структурированные данные протокола
|
||
protocol_data JSONB NOT NULL DEFAULT '{}',
|
||
-- Пример содержимого:
|
||
-- {
|
||
-- "agenda": ["Пункт 1", "Пункт 2"],
|
||
-- "decisions": ["Решили 1", "Решили 2"],
|
||
-- "action_items": [
|
||
-- {
|
||
-- "task": "Подготовить ТЗ",
|
||
-- "deadline": "2026-07-01",
|
||
-- "assignee": "Иванов И.И.",
|
||
-- "status": "active"
|
||
-- }
|
||
-- ],
|
||
-- "unresolved_issues": ["Нет бюджета на Q3"],
|
||
-- "previous_results": ["Мероприятие 1 выполнено"]
|
||
-- }
|
||
|
||
-- Статус протокола
|
||
protocol_status VARCHAR(50) NOT NULL DEFAULT 'draft',
|
||
-- 'draft', 'approved'
|
||
|
||
-- Подушка безопасности: метаданные источника
|
||
source_metadata JSONB NOT NULL DEFAULT '{}',
|
||
-- Пример: {"vcs_provider": "yandex", "recording_url": "...", "duration": 3600}
|
||
|
||
-- Подушка безопасности: сырой ответ ИИ (для отладки)
|
||
ai_raw_response JSONB,
|
||
|
||
-- Векторное представление транскрибации (для семантического поиска)
|
||
transcript_embedding vector(1536), -- 1536 для text-embedding-3-small
|
||
|
||
is_deleted BOOLEAN NOT NULL DEFAULT false,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
);
|
||
|
||
-- Индексы
|
||
CREATE INDEX idx_meetings_project ON meetings(project_id);
|
||
CREATE INDEX idx_meetings_date ON meetings(meeting_date);
|
||
CREATE INDEX idx_meetings_type ON meetings(type);
|
||
CREATE INDEX idx_meetings_status ON meetings(protocol_status);
|
||
CREATE INDEX idx_meetings_protocol_data ON meetings USING GIN (protocol_data);
|
||
|
||
-- Векторный индекс (IVFFlat, можно заменить на HNSW для больших данных)
|
||
CREATE INDEX idx_meetings_embedding ON meetings
|
||
USING ivfflat (transcript_embedding vector_cosine_ops)
|
||
WITH (lists = 100);
|
||
|
||
-- Триггер для updated_at
|
||
CREATE TRIGGER update_meetings_updated_at
|
||
BEFORE UPDATE ON meetings
|
||
FOR EACH ROW
|
||
EXECUTE FUNCTION update_updated_at_column();
|
||
```
|
||
|
||
### 5.5. Таблица `prompt_templates` (Шаблоны промптов)
|
||
```sql
|
||
CREATE TABLE prompt_templates (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
key VARCHAR(100) UNIQUE NOT NULL,
|
||
-- 'gen_standard_protocol', 'gen_op_council_protocol',
|
||
-- 'gen_steering_protocol', 'gen_summary', 'gen_project_status'
|
||
|
||
name VARCHAR(255) NOT NULL,
|
||
description TEXT,
|
||
|
||
system_prompt TEXT NOT NULL,
|
||
user_prompt_template TEXT NOT NULL,
|
||
-- С плейсхолдерами: {{transcript}}, {{previous_protocols}}, {{project_status}}
|
||
|
||
-- Подушка безопасности: дополнительные параметры для ИИ
|
||
ai_parameters JSONB NOT NULL DEFAULT '{}',
|
||
-- Пример: {"temperature": 0.7, "max_tokens": 2000, "top_p": 0.9}
|
||
|
||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
);
|
||
|
||
-- Индексы
|
||
CREATE INDEX idx_prompt_templates_key ON prompt_templates(key);
|
||
```
|
||
|
||
### 5.6. Таблица `audit_log` (Журнал действий)
|
||
```sql
|
||
CREATE TABLE audit_log (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
user_id UUID REFERENCES users(id),
|
||
action VARCHAR(100) NOT NULL,
|
||
-- 'project_created', 'meeting_approved', 'protocol_edited'
|
||
|
||
entity_type VARCHAR(50) NOT NULL, -- 'project', 'meeting'
|
||
entity_id UUID NOT NULL,
|
||
|
||
-- Подушка безопасности: дополнительные данные действия
|
||
details JSONB NOT NULL DEFAULT '{}',
|
||
-- Пример: {"old_status": "draft", "new_status": "approved"}
|
||
|
||
ip_address INET,
|
||
user_agent TEXT,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
);
|
||
|
||
-- Индексы
|
||
CREATE INDEX idx_audit_log_user ON audit_log(user_id);
|
||
CREATE INDEX idx_audit_log_entity ON audit_log(entity_type, entity_id);
|
||
CREATE INDEX idx_audit_log_created_at ON audit_log(created_at DESC);
|
||
```
|
||
|
||
### 5.7. Вспомогательная функция для `updated_at`
|
||
```sql
|
||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||
RETURNS TRIGGER AS $$
|
||
BEGIN
|
||
NEW.updated_at = NOW();
|
||
RETURN NEW;
|
||
END;
|
||
$$ LANGUAGE plpgsql;
|
||
```
|
||
|
||
---
|
||
|
||
## 6. ДИЗАЙН И UI/UX BEST PRACTICES 2026
|
||
|
||
### 6.1. Визуальный стиль: "Modern Minimal AI-Native"
|
||
|
||
**Ключевые принципы:**
|
||
- **Воздух и пространство** — много white space, элементы не прижаты друг к другу
|
||
- **Глубина через тени** — subtle shadows для создания иерархии (не flat design)
|
||
- **Скругленные углы** — 12-16px для карточек, 8px для кнопок
|
||
- **Переменные шрифты** — Inter Variable или Geist (оптимизированы для UI)
|
||
- **Цветовая палитра** — нейтральные серые + акцентный цвет (синий/фиолетовый для AI)
|
||
|
||
**Цветовая схема:**
|
||
```css
|
||
/* Light Mode */
|
||
--bg-primary: #FAFAFA;
|
||
--bg-secondary: #FFFFFF;
|
||
--bg-tertiary: #F5F5F5;
|
||
--text-primary: #1A1A1A;
|
||
--text-secondary: #6B7280;
|
||
--border: #E5E7EB;
|
||
--accent: #6366F1; /* Индиго для AI-элементов */
|
||
--success: #10B981;
|
||
--warning: #F59E0B;
|
||
--error: #EF4444;
|
||
|
||
/* Dark Mode */
|
||
--bg-primary: #0A0A0A;
|
||
--bg-secondary: #171717;
|
||
--bg-tertiary: #262626;
|
||
--text-primary: #FAFAFA;
|
||
--text-secondary: #A3A3A3;
|
||
--border: #2E2E2E;
|
||
```
|
||
|
||
### 6.2. Layout Best Practices 2026
|
||
|
||
#### **Bento Grid для дашборда**
|
||
```
|
||
┌─────────────────┬─────────┬─────────┐
|
||
│ │ Статус │ Актив. │
|
||
│ Проект 1 │ ██████ │ 12 │
|
||
│ AI-статус... │ │ │
|
||
├─────────────────┼─────────┴─────────┤
|
||
│ │ │
|
||
│ Проект 2 │ Проект 3 │
|
||
│ │ │
|
||
└─────────────────┴─────────────────────┘
|
||
```
|
||
- Карточки разных размеров создают визуальный интерес
|
||
- Каждая карточка — самостоятельный виджет
|
||
|
||
#### **Sidebar Navigation (слева)**
|
||
```
|
||
┌──────┬──────────────────────────────┐
|
||
│ 🏠 │ │
|
||
│ 📁 │ Основной контент │
|
||
│ 📅 │ │
|
||
│ ⚙️ │ │
|
||
│ │ │
|
||
│ 👤 │ │
|
||
└──────┴──────────────────────────────┘
|
||
```
|
||
- Компактная (64px), иконки + tooltip при hover
|
||
- Активный пункт подсвечен акцентным цветом
|
||
|
||
#### **Split View для работы с протоколом**
|
||
```
|
||
┌──────────────────────────────────────┐
|
||
│ Вкладки: Исходник | Резюме | Прот. │
|
||
├──────────────────────────┬───────────┤
|
||
│ │ 🤖 AI-чат │
|
||
│ Markdown редактор │ │
|
||
│ (md-editor-v3) │ [Ввод] │
|
||
│ │ │
|
||
│ │ │
|
||
└──────────────────────────┴───────────┘
|
||
```
|
||
- AI-чат collapsible (можно скрыть)
|
||
- При выделении текста в редакторе — появляется floating toolbar с кнопкой "Спросить ИИ"
|
||
|
||
### 6.3. Компоненты и паттерны
|
||
|
||
#### **Карточки проектов**
|
||
- Hover-эффект: легкое поднятие (translateY: -2px) + усиление тени
|
||
- Badge для этапа проекта (цветной pill)
|
||
- AI-статус — курсивом, с иконкой ✨
|
||
- Прогресс-бар для активных мероприятий
|
||
|
||
#### **Таблицы**
|
||
- Sticky header (прилипает при скролле)
|
||
- Ze- Zeбра (чередование строк) — повышает читаемость больших таблиц
|
||
- Row actions — иконки действий появляются при hover над строкой (не загромождают UI)
|
||
- Inline editing — двойной клик по ячейке → редактирование (для быстрых правок)
|
||
- Skeleton loaders вместо спиннеров — плавнее восприятие загрузки
|
||
- Empty state — иллюстрация + призыв к действию ("Создайте первый проект")
|
||
|
||
#### **Формы и ввод данных**
|
||
- Floating labels (лейбл поднимается при фокусе)
|
||
- Inline validation — ошибка появляется под полем сразу после blur
|
||
- Auto-save черновиков (каждые 30 сек) с индикатором "Сохранено ✓"
|
||
- Keyboard shortcuts — `Ctrl+S` сохранить, `Ctrl+Enter` отправить в чат
|
||
- Drag & drop для загрузки файлов с визуальной зоной
|
||
|
||
#### **AI-индикаторы и состояния**
|
||
- **Пульсирующая иконка ✨** во время генерации ИИ (не спиннер, а мягкая анимация)
|
||
- **Streaming text** — текст появляется посимвольно с курсором
|
||
- **Confidence badge** — "ИИ-черновик" (желтый) vs "Утверждено" (зеленый)
|
||
- **Skeleton для AI-блоков** — имитация структуры текста до загрузки
|
||
|
||
#### **Toast-уведомления**
|
||
- Появляются в правом верхнем углу
|
||
- Автоматически исчезают через 5 секунд (успех) или требуют закрытия (ошибка)
|
||
- Группируются, если их много
|
||
- С иконками и цветовой кодировкой
|
||
|
||
### 6.4. Типографика
|
||
|
||
**Шрифтовая пара:**
|
||
- **Заголовки:** Inter Variable (600-700 weight)
|
||
- **Основной текст:** Inter Variable (400-500 weight)
|
||
- **Моноширинный (код, JSON):** JetBrains Mono
|
||
|
||
**Размерная шкала:**
|
||
```
|
||
H1: 32px / 40px line-height (заголовки страниц)
|
||
H2: 24px / 32px (заголовки секций)
|
||
H3: 20px / 28px (заголовки карточек)
|
||
Body Large: 16px / 24px (основной текст)
|
||
Body: 14px / 20px (таблицы, формы)
|
||
Caption: 12px / 16px (подписи, метаданные)
|
||
```
|
||
|
||
### 6.5. Анимации и микро-взаимодействия
|
||
|
||
**Принцип:** "Animations should be functional, not decorative"
|
||
|
||
- **Длительность:** 150-250ms для UI-элементов, 300-500ms для переходов между страницами
|
||
- **Easing:** `cubic-bezier(0.4, 0, 0.2, 1)` (Material standard)
|
||
- **Hover states:** плавное изменение фона/тени (150ms)
|
||
- **Modal open:** fade + scale (0.95 → 1.0) за 200ms
|
||
- **Tab switch:** slide + fade контент (200ms)
|
||
- **AI streaming:** typewriter effect с задержкой 20-30ms между символами
|
||
|
||
**Reduced motion:** Учитывать `prefers-reduced-motion` — отключать анимации для пользователей с соответствующей настройкой ОС.
|
||
|
||
---
|
||
|
||
## 7. ВЕКТОРИЗАЦИЯ: ДВА РЕЖИМА РАБОТЫ
|
||
|
||
### 7.1. Архитектура модуля векторизации
|
||
|
||
Создаем абстрактный интерфейс `EmbeddingProvider`, который имеет две реализации. Это позволяет переключать режимы через настройки без изменения бизнес-логики.
|
||
|
||
```typescript
|
||
// src/modules/embeddings/types.ts
|
||
export interface EmbeddingProvider {
|
||
name: string;
|
||
dimension: number;
|
||
embed(text: string): Promise<number[]>;
|
||
embedBatch(texts: string[]): Promise<number[][]>;
|
||
isAvailable(): Promise<boolean>;
|
||
}
|
||
|
||
// src/modules/embeddings/index.ts
|
||
export class EmbeddingService {
|
||
private provider: EmbeddingProvider;
|
||
|
||
constructor(mode: 'openai' | 'local') {
|
||
this.provider = mode === 'openai'
|
||
? new OpenAIEmbeddingProvider()
|
||
: new LocalEmbeddingProvider();
|
||
}
|
||
|
||
async embed(text: string): Promise<number[]> {
|
||
return this.provider.embed(text);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 7.2. Режим 1: OpenAI API
|
||
|
||
**Когда использовать:**
|
||
- Есть доступ к интернету
|
||
- Нужно максимальное качество эмбеддингов
|
||
- Готовы платить за API (~$0.02 за 1M токенов для text-embedding-3-small)
|
||
|
||
**Реализация:**
|
||
```typescript
|
||
// src/modules/embeddings/openai.provider.ts
|
||
import OpenAI from 'openai';
|
||
|
||
export class OpenAIEmbeddingProvider implements EmbeddingProvider {
|
||
name = 'openai';
|
||
dimension = 1536; // text-embedding-3-small
|
||
|
||
private client: OpenAI;
|
||
private model = 'text-embedding-3-small';
|
||
|
||
constructor() {
|
||
this.client = new OpenAI({
|
||
apiKey: process.env.OPENAI_API_KEY,
|
||
baseURL: process.env.OPENAI_BASE_URL, // для прокси/совместимых API
|
||
});
|
||
}
|
||
|
||
async embed(text: string): Promise<number[]> {
|
||
const response = await this.client.embeddings.create({
|
||
model: this.model,
|
||
input: this.preprocessText(text),
|
||
});
|
||
return response.data[0].embedding;
|
||
}
|
||
|
||
async embedBatch(texts: string[]): Promise<number[][]> {
|
||
// OpenAI поддерживает batch до 2048 элементов
|
||
const chunks = this.chunkArray(texts, 100);
|
||
const results = [];
|
||
|
||
for (const chunk of chunks) {
|
||
const response = await this.client.embeddings.create({
|
||
model: this.model,
|
||
input: chunk.map(t => this.preprocessText(t)),
|
||
});
|
||
results.push(...response.data.map(d => d.embedding));
|
||
}
|
||
|
||
return results;
|
||
}
|
||
|
||
private preprocessText(text: string): string {
|
||
// Очистка, нормализация, удаление спецсимволов
|
||
return text
|
||
.replace(/\s+/g, ' ')
|
||
.trim()
|
||
.slice(0, 8000); // лимит токенов
|
||
}
|
||
|
||
async isAvailable(): Promise<boolean> {
|
||
try {
|
||
await this.embed('test');
|
||
return true;
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**Плюсы:**
|
||
- ✅ Высочайшее качество эмбеддингов
|
||
- ✅ Поддержка русского языка из коробки
|
||
- ✅ Не требует ресурсов сервера
|
||
- ✅ Быстрая интеграция
|
||
|
||
**Минусы:**
|
||
- ❌ Зависимость от интернета
|
||
- ❌ Стоимость (хоть и небольшая)
|
||
- ❌ Данные уходят во внешнее API (для sensitive данных может быть критично)
|
||
- ❌ Rate limits
|
||
|
||
### 7.3. Режим 2: Локальная модель через @xenova/transformers
|
||
|
||
**Когда использовать:**
|
||
- Работа в закрытом контуре (без интернета)
|
||
- Требования к безопасности данных (ничего не покидает сервер)
|
||
- Нужно избежать затрат на API
|
||
- Готовы пожертвовать немного качества
|
||
|
||
**Реализация:**
|
||
```typescript
|
||
// src/modules/embeddings/local.provider.ts
|
||
import { pipeline, env } from '@xenova/transformers';
|
||
|
||
// Отключаем загрузку моделей из интернета при первом запуске
|
||
// Модели предзагружены в ./models/
|
||
env.localModelPath = './models';
|
||
env.allowRemoteModels = false;
|
||
|
||
export class LocalEmbeddingProvider implements EmbeddingProvider {
|
||
name = 'local';
|
||
dimension = 384; // для all-MiniLM-L6-v2
|
||
|
||
private embedder: any = null;
|
||
private modelName = 'Xenova/all-MiniLM-L6-v2';
|
||
// Альтернативы для русского:
|
||
// - 'ai-forever/sbert_large_nlu_ru' (русскоязычная, 1024 dim)
|
||
// - 'sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2' (мультиязычная, 384 dim)
|
||
|
||
async initialize(): Promise<void> {
|
||
if (this.embedder) return;
|
||
|
||
console.log(`Loading embedding model: ${this.modelName}...`);
|
||
this.embedder = await pipeline(
|
||
'feature-extraction',
|
||
this.modelName,
|
||
{ quantized: true } // квантованная модель — быстрее и меньше
|
||
);
|
||
console.log('Embedding model loaded');
|
||
}
|
||
|
||
async embed(text: string): Promise<number[]> {
|
||
await this.initialize();
|
||
|
||
const output = await this.embedder(this.preprocessText(text), {
|
||
pooling: 'mean',
|
||
normalize: true,
|
||
});
|
||
|
||
return Array.from(output.data);
|
||
}
|
||
|
||
async embedBatch(texts: string[]): Promise<number[][]> {
|
||
await this.initialize();
|
||
|
||
// Обрабатываем батчами по 32 (оптимально для CPU)
|
||
const results = [];
|
||
for (let i = 0; i < texts.length; i += 32) {
|
||
const batch = texts.slice(i, i + 32);
|
||
const output = await this.embedder(batch, {
|
||
pooling: 'mean',
|
||
normalize: true,
|
||
});
|
||
|
||
// output имеет shape [batch_size, dimension]
|
||
for (let j = 0; j < batch.length; j++) {
|
||
const embedding = output[j];
|
||
results.push(Array.from(embedding.data));
|
||
}
|
||
}
|
||
|
||
return results;
|
||
}
|
||
|
||
private preprocessText(text: string): string {
|
||
return text.replace(/\s+/g, ' ').trim().slice(0, 512);
|
||
}
|
||
|
||
async isAvailable(): Promise<boolean> {
|
||
try {
|
||
await this.initialize();
|
||
return true;
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**Важные нюансы локальной модели:**
|
||
|
||
1. **Выбор модели:**
|
||
- **all-MiniLM-L6-v2** (384 dim) — быстрая, легкая (~80MB), хорошее качество для английского
|
||
- **paraphrase-multilingual-MiniLM-L12-v2** (384 dim) — мультиязычная, хорошо работает с русским
|
||
- **sbert_large_nlu_ru** (1024 dim) — специализированная русская модель, лучшее качество для русского, но тяжелее (~1.3GB)
|
||
|
||
2. **Предзагрузка моделей:**
|
||
```bash
|
||
# Скрипт для предзагрузки модели в docker-образ
|
||
# scripts/download-models.js
|
||
const { downloadFile } = require('@xenova/transformers');
|
||
|
||
async function downloadModels() {
|
||
await downloadFile('Xenova/paraphrase-multilingual-MiniLM-L12-v2');
|
||
}
|
||
|
||
downloadModels();
|
||
```
|
||
|
||
3. **Производительность:**
|
||
- CPU: ~50-100ms на один текст (зависит от длины)
|
||
- RAM: ~500MB-1.5GB (зависит от модели)
|
||
- Для batch из 100 текстов: ~5-10 секунд
|
||
|
||
4. **Переменная размерность:**
|
||
- При смене модели нужно пересоздать все эмбеддинги в БД
|
||
- Поле `transcript_embedding` должно иметь размерность, соответствующую модели
|
||
- Решение: хранить размерность в настройках и делать миграцию при смене
|
||
|
||
### 7.4. Переключение режимов через настройки
|
||
|
||
```typescript
|
||
// src/config/embeddings.config.ts
|
||
export const embeddingsConfig = {
|
||
mode: process.env.EMBEDDING_MODE as 'openai' | 'local', // 'openai' | 'local'
|
||
|
||
openai: {
|
||
model: process.env.OPENAI_EMBEDDING_MODEL || 'text-embedding-3-small',
|
||
dimension: 1536,
|
||
},
|
||
|
||
local: {
|
||
model: process.env.LOCAL_EMBEDDING_MODEL || 'Xenova/paraphrase-multilingual-MiniLM-L12-v2',
|
||
dimension: 384,
|
||
quantized: true,
|
||
},
|
||
};
|
||
```
|
||
|
||
**⚠️ Важно:** При смене режима нужно **пересоздать все эмбеддинги** в БД. Добавляем в админку кнопку "Перегенерировать все эмбеддинги" (запускает фоновую задачу).
|
||
|
||
---
|
||
|
||
## 8. API КОНТРАКТЫ (REST)
|
||
|
||
### 8.1. Общие принципы
|
||
- Все ответы в формате `{ data: ..., meta: ... }` или `{ error: { code, message } }`
|
||
- Пагинация: `?page=1&limit=20` → ответ включает `meta: { total, page, limit, pages }`
|
||
- Аутентификация: `Authorization: Bearer <jwt>`
|
||
- Content-Type: `application/json` (для файлов — `multipart/form-data`)
|
||
|
||
### 8.2. Auth API
|
||
|
||
```
|
||
POST /api/auth/login
|
||
Body: { email, password }
|
||
Response: { data: { token, user } }
|
||
|
||
POST /api/auth/logout
|
||
Headers: Authorization: Bearer <token>
|
||
Response: { data: { success: true } }
|
||
|
||
GET /api/auth/me
|
||
Headers: Authorization: Bearer <token>
|
||
Response: { data: { id, email, fullName, role } }
|
||
```
|
||
|
||
### 8.3. Projects API
|
||
|
||
```
|
||
GET /api/projects
|
||
Query: ?search=...&stage=...&pmoResponsibleId=...&page=1&limit=20
|
||
Response: {
|
||
data: [{ id, shortName, fullName, stage, aiStatus, ... }],
|
||
meta: { total, page, limit, pages }
|
||
}
|
||
|
||
POST /api/projects
|
||
Body: { shortName, fullName, stage, pmoResponsibleId, metadata }
|
||
Response: { data: { project } }
|
||
|
||
GET /api/projects/:id
|
||
Response: { data: { project, stats: { meetingsCount, activeTasks, overdueTasks } } }
|
||
|
||
PUT /api/projects/:id
|
||
Body: { ...fields }
|
||
Response: { data: { project } }
|
||
|
||
DELETE /api/projects/:id (soft delete)
|
||
Response: { data: { success: true } }
|
||
|
||
POST /api/projects/:id/regenerate-status
|
||
Response: { data: { aiStatus, ganttMermaid } }
|
||
```
|
||
|
||
### 8.4. Meetings API
|
||
|
||
```
|
||
GET /api/meetings
|
||
Query: ?projectId=...&vectorSearch=...&dateFrom=...&dateTo=...&type=...&page=1&limit=20
|
||
Response: { data: [...], meta: {...} }
|
||
|
||
POST /api/meetings
|
||
Body (multipart/form-data):
|
||
- projectId: UUID
|
||
- meetingDate: Date
|
||
- type: 'standard' | 'op_council' | 'steering_council'
|
||
- sourceType: 'audio' | 'text' | 'vcs_integration'
|
||
- audioFile: File (опционально)
|
||
- rawTranscript: string (если sourceType='text')
|
||
Response: { data: { meeting } }
|
||
|
||
GET /api/meetings/:id
|
||
Response: { data: { meeting, project, previousMeetings } }
|
||
|
||
PUT /api/meetings/:id
|
||
Body: { ...fields }
|
||
Response: { data: { meeting } }
|
||
|
||
POST /api/meetings/:id/transcribe
|
||
Response: { data: { taskId, status: 'processing' } }
|
||
# Синхронно для MVP (возвращает результат сразу)
|
||
|
||
POST /api/meetings/:id/generate-summary
|
||
Response: { data: { summary } }
|
||
|
||
POST /api/meetings/:id/generate-protocol
|
||
Body: { type: 'standard' | 'op_council' | 'steering_council' }
|
||
Response: { data: { protocolMarkdown, protocolData } }
|
||
|
||
POST /api/meetings/:id/approve
|
||
Response: { data: { meeting, projectAiStatus } }
|
||
|
||
GET /api/meetings/:id/export-docx
|
||
Response: File (application/vnd.openxmlformats-officedocument.wordprocessingml.document)
|
||
```
|
||
|
||
### 8.5. AI Chat API (Server-Sent Events)
|
||
|
||
```
|
||
POST /api/ai/chat
|
||
Headers:
|
||
- Authorization: Bearer <token>
|
||
- Accept: text/event-stream
|
||
Body: {
|
||
context: "...", // выделенный текст из протокола
|
||
instruction: "Сделай более формальным",
|
||
meetingId: UUID
|
||
}
|
||
Response: SSE stream
|
||
event: token
|
||
data: {"text": "Новая"}
|
||
|
||
event: token
|
||
data: {"text": " формулировка"}
|
||
|
||
event: done
|
||
data: {"fullText": "..."}
|
||
```
|
||
|
||
**Реализация на бэкенде:**
|
||
```typescript
|
||
// src/routes/ai.chat.ts
|
||
router.post('/chat', authMiddleware, async (req, res) => {
|
||
res.setHeader('Content-Type', 'text/event-stream');
|
||
res.setHeader('Cache-Control', 'no-cache');
|
||
res.setHeader('Connection', 'keep-alive');
|
||
|
||
const { context, instruction, meetingId } = req.body;
|
||
|
||
const stream = await openai.chat.completions.create({
|
||
model: 'gpt-4o-mini',
|
||
messages: [
|
||
{ role: 'system', content: 'Ты помощник в редактировании протоколов...' },
|
||
{ role: 'user', content: `Текст: ${context}\n\nИнструкция: ${instruction}` }
|
||
],
|
||
stream: true,
|
||
});
|
||
|
||
let fullText = '';
|
||
for await (const chunk of stream) {
|
||
const text = chunk.choices[0]?.delta?.content || '';
|
||
fullText += text;
|
||
res.write(`event: token\ndata: ${JSON.stringify({ text })}\n\n`);
|
||
}
|
||
|
||
res.write(`event: done\ndata: ${JSON.stringify({ fullText })}\n\n`);
|
||
res.end();
|
||
});
|
||
```
|
||
|
||
**Реализация на фронте:**
|
||
```typescript
|
||
// src/composables/useAiChat.ts
|
||
export function useAiChat() {
|
||
const streamingText = ref('');
|
||
const isLoading = ref(false);
|
||
|
||
async function sendChat(context: string, instruction: string) {
|
||
isLoading.value = true;
|
||
streamingText.value = '';
|
||
|
||
const response = await fetch('/api/ai/chat', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${token}`,
|
||
'Accept': 'text/event-stream',
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({ context, instruction }),
|
||
});
|
||
|
||
const reader = response.body!.getReader();
|
||
const decoder = new TextDecoder();
|
||
|
||
while (true) {
|
||
const { done, value } = await reader.read();
|
||
if (done) break;
|
||
|
||
const chunk = decoder.decode(value);
|
||
const lines = chunk.split('\n');
|
||
|
||
for (const line of lines) {
|
||
if (line.startsWith('data: ')) {
|
||
const data = JSON.parse(line.slice(6));
|
||
if (data.text) {
|
||
streamingText.value += data.text;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
isLoading.value = false;
|
||
}
|
||
|
||
return { streamingText, isLoading, sendChat };
|
||
}
|
||
```
|
||
|
||
### 8.6. Prompts API
|
||
|
||
```
|
||
GET /api/prompts
|
||
Response: { data: [{ key, name, systemPrompt, userPromptTemplate }] }
|
||
|
||
PUT /api/prompts/:key
|
||
Body: { systemPrompt, userPromptTemplate, aiParameters }
|
||
Response: { data: { prompt } }
|
||
|
||
POST /api/prompts/:key/test
|
||
Body: { testTranscript: "..." }
|
||
Response: { data: { result: "..." } }
|
||
```
|
||
|
||
### 8.7. Settings API
|
||
|
||
```
|
||
GET /api/settings/embeddings
|
||
Response: { data: { mode, availableProviders, currentDimension } }
|
||
|
||
PUT /api/settings/embeddings
|
||
Body: { mode: 'openai' | 'local' }
|
||
Response: { data: { success: true, requiresReindex: true } }
|
||
|
||
POST /api/settings/embeddings/reindex
|
||
Response: { data: { taskId, status: 'processing' } }
|
||
```
|
||
|
||
---
|
||
|
||
## 9. РАБОТА С ИИ: ПРОМПТЫ И RAG
|
||
|
||
### 9.1. Структура промптов
|
||
|
||
Все промпты хранятся в таблице `prompt_templates` и редактируются через админку. Это позволяет настраивать поведение ИИ без изменения кода.
|
||
|
||
**Базовые промпты:**
|
||
|
||
#### `gen_summary` (Универсальное резюме)
|
||
```
|
||
System: Ты — ассистент проектного офиса. Твоя задача — создать краткое,
|
||
информативное резюме встречи на основе транскрибации. Резюме должно быть
|
||
понятно человеку, который не присутствовал на встрече.
|
||
|
||
User: Транскрибация встречи:
|
||
{{transcript}}
|
||
|
||
Создай резюме в 3-5 предложениях, выдели:
|
||
1. Основную тему обсуждения
|
||
2. Ключевые решения
|
||
3. Главные проблемы или риски
|
||
```
|
||
|
||
#### `gen_standard_protocol` (Протокол обычной встречи)
|
||
```
|
||
System: Ты — секретарь проектного офиса. Создай структурированный протокол
|
||
встречи в формате Markdown. Будь точен, формален, не добавляй информацию,
|
||
которой нет в транскрибации.
|
||
|
||
User: Транскрибация встречи:
|
||
{{transcript}}
|
||
|
||
Сформируй протокол со следующей структурой:
|
||
|
||
# Протокол встречи
|
||
|
||
## Повестка дня
|
||
- Пункт 1
|
||
- Пункт 2
|
||
|
||
## Решили
|
||
1. Решение 1
|
||
2. Решение 2
|
||
|
||
## Перечень мероприятий
|
||
| № | Мероприятие | Срок | Ответственный |
|
||
|---|-------------|------|---------------|
|
||
| 1 | ... | ... | ... |
|
||
|
||
## Проблемные и нерешенные вопросы
|
||
- Вопрос 1
|
||
- Вопрос 2
|
||
|
||
ВАЖНО:
|
||
- Если в транскрибации нет четкого срока или ответственного — укажи "уточнить"
|
||
- Не выдумывай информацию, которой нет в тексте
|
||
- Используй деловой стиль
|
||
```
|
||
|
||
#### `gen_op_council_protocol` (Протокол оперативного совета)
|
||
```
|
||
System: Ты — секретарь оперативного совета проекта. Особенность протокола
|
||
оперсовета — наличие блока "Результаты предыдущих мероприятий". Сравни
|
||
текущую транскрибацию с предыдущими протоколами и определи статус каждого
|
||
мероприятия.
|
||
|
||
User: Текущая транскрибация:
|
||
{{transcript}}
|
||
|
||
Предыдущие мероприятия (из прошлых протоколов):
|
||
{{previous_action_items}}
|
||
|
||
Сформируй протокол:
|
||
|
||
# Протокол оперативного совета
|
||
|
||
## Повестка дня
|
||
...
|
||
|
||
## Результаты предыдущих мероприятий
|
||
| Мероприятие | Срок | Ответственный | Статус | Комментарий |
|
||
|-------------|------|---------------|--------|-------------|
|
||
| ... | ... | ... | ✅/⏳/❌ | ... |
|
||
|
||
Статусы:
|
||
- ✅ Выполнено
|
||
- ⏳ В работе / Просрочено
|
||
- ❌ Не выполнено / Отменено
|
||
|
||
## Решили
|
||
...
|
||
|
||
## Перечень мероприятий
|
||
| № | Мероприятие | Срок | Ответственный |
|
||
|---|-------------|------|---------------|
|
||
| 1 | ... | ... | ... |
|
||
|
||
## Проблемные и нерешенные вопросы
|
||
...
|
||
```
|
||
|
||
#### `gen_steering_protocol` (Протокол управляющего совета)
|
||
```
|
||
System: Ты — секретарь управляющего совета проекта. Протокол предназначен
|
||
для высшего руководства. Будь лаконичен, фокусируйся на стратегических
|
||
вопросах, рисках и ключевых решениях.
|
||
|
||
User: Текущая транскрибация:
|
||
{{transcript}}
|
||
|
||
Текущий статус проекта (от ИИ):
|
||
{{project_ai_status}}
|
||
|
||
Открытые вопросы из предыдущих встреч:
|
||
{{unresolved_issues}}
|
||
|
||
Сформируй протокол:
|
||
|
||
# Протокол управляющего совета
|
||
|
||
## Повестка дня
|
||
...
|
||
|
||
## Текущий статус проекта
|
||
(краткое резюме на основе AI-статуса)
|
||
|
||
## Проблемы и открытые вопросы
|
||
(агрегированный список из unresolved_issues + новые из транскрибации)
|
||
|
||
## Решили
|
||
(стратегические решения)
|
||
|
||
## Перечень мероприятий
|
||
| № | Мероприятие | Срок | Ответственный |
|
||
|---|-------------|------|---------------|
|
||
|
||
## Проблемные и нерешенные вопросы
|
||
(только критичные, требующие эскалации)
|
||
```
|
||
|
||
#### `gen_project_status` (AI-статус проекта)
|
||
```
|
||
System: Ты — аналитик проектного офиса. На основе всех протоколов и
|
||
мероприятий проекта сформулируй краткий актуальный статус (3-4 предложения).
|
||
|
||
User: Проект: {{project_name}}
|
||
Все протоколы и мероприятия:
|
||
{{all_meetings_summary}}
|
||
|
||
Сформируй статус, включающий:
|
||
1. Текущий фокус проекта
|
||
2. Ключевые достижения
|
||
3. Главные риски
|
||
4. Следующие шаги
|
||
```
|
||
|
||
### 9.2. RAG-логика (Retrieval-Augmented Generation)
|
||
|
||
Перед генерацией протокола бэкенд собирает контекст:
|
||
|
||
```typescript
|
||
// src/services/protocol.generator.ts
|
||
export class ProtocolGenerator {
|
||
async generate(meetingId: string, type: ProtocolType) {
|
||
const meeting = await db.query.meetings.findFirst({
|
||
where: eq(meetings.id, meetingId),
|
||
with: { project: true }
|
||
});
|
||
|
||
let context = {
|
||
transcript: meeting.rawTranscript,
|
||
previousActionItems: [],
|
||
unresolvedIssues: [],
|
||
projectAiStatus: meeting.project.aiStatus,
|
||
};
|
||
|
||
if (type === 'op_council' || type === 'steering_council') {
|
||
// Получаем предыдущие встречи этого проекта
|
||
const previousMeetings = await db.query.meetings.findMany({
|
||
where: and(
|
||
eq(meetings.projectId, meeting.projectId),
|
||
lt(meetings.meetingDate, meeting.meetingDate),
|
||
eq(meetings.protocolStatus, 'approved')
|
||
),
|
||
orderBy: desc(meetings.meetingDate),
|
||
limit: 5, // последние 5 встреч
|
||
});
|
||
|
||
// Извлекаем мероприятия из protocol_data
|
||
context.previousActionItems = previousMeetings.flatMap(m =>
|
||
m.protocolData?.action_items || []
|
||
);
|
||
|
||
// Извлекаем нерешенные вопросы
|
||
context.unresolvedIssues = previousMeetings.flatMap(m =>
|
||
m.protocolData?.unresolved_issues || []
|
||
);
|
||
}
|
||
|
||
// Считаем токены и обрезаем контекст, если нужно
|
||
const tokenCount = countTokens(JSON.stringify(context));
|
||
if (tokenCount > 100000) {
|
||
context = this.truncateContext(context, 100000);
|
||
}
|
||
|
||
// Загружаем шаблон промпта
|
||
const template = await db.query.promptTemplates.findFirst({
|
||
where: eq(promptTemplates.key, `gen_${type}_protocol`)
|
||
});
|
||
|
||
// Заполняем плейсхолдеры
|
||
const userPrompt = this.fillTemplate(template.userPromptTemplate, context);
|
||
|
||
// Вызываем LLM
|
||
const response = await openai.chat.completions.create({
|
||
model: 'gpt-4o-mini',
|
||
messages: [
|
||
{ role: 'system', content: template.systemPrompt },
|
||
{ role: 'user', content: userPrompt }
|
||
],
|
||
temperature: template.aiParameters?.temperature || 0.3,
|
||
max_tokens: template.aiParameters?.max_tokens || 4000,
|
||
});
|
||
|
||
const markdown = response.choices[0].message.content;
|
||
|
||
// Парсим Markdown в структурированные данные
|
||
const protocolData = this.parseProtocolMarkdown(markdown);
|
||
|
||
return { markdown, protocolData };
|
||
}
|
||
|
||
private parseProtocolMarkdown(markdown: string): ProtocolData {
|
||
// Парсим таблицы, списки и заголовки
|
||
// Используем библиотеку marked или собственный парсер
|
||
// ...
|
||
return {
|
||
agenda: [],
|
||
decisions: [],
|
||
action_items: [],
|
||
unresolved_issues: [],
|
||
};
|
||
}
|
||
}
|
||
```
|
||
|
||
### 9.3. Обработка ошибок ИИ
|
||
|
||
```typescript
|
||
// src/utils/aiErrorHandler.ts
|
||
export async function withRetry<T>(
|
||
fn: () => Promise<T>,
|
||
maxRetries = 3
|
||
): Promise<T> {
|
||
let lastError: Error;
|
||
|
||
for (let i = 0; i < maxRetries; i++) {
|
||
try {
|
||
return await fn();
|
||
} catch (error) {
|
||
lastError = error;
|
||
|
||
if (error.status === 429) {
|
||
// Rate limit — ждем и повторяем
|
||
await sleep(Math.pow(2, i) * 1000);
|
||
continue;
|
||
}
|
||
|
||
if (error.status >= 500) {
|
||
// Server error — повторяем
|
||
await sleep(1000);
|
||
continue;
|
||
}
|
||
|
||
// Client error (400, 401) — не повторяем
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
throw lastError;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 10. ИНТЕГРАЦИЯ С ВКС
|
||
|
||
### 10.1. Архитектура адаптеров
|
||
|
||
Используем паттерн **Strategy** для поддержки разных провайдеров ВКС.
|
||
|
||
```typescript
|
||
// src/integrations/vcs/base.ts
|
||
export abstract class VCSProvider {
|
||
abstract name: string;
|
||
abstract getTranscript(meetingId: string): Promise<VCSTranscript>;
|
||
abstract testConnection(): Promise<boolean>;
|
||
}
|
||
|
||
export interface VCSTranscript {
|
||
text: string;
|
||
metadata: {
|
||
duration?: number;
|
||
participants?: string[];
|
||
recordingUrl?: string;
|
||
};
|
||
}
|
||
|
||
// src/integrations/vcs/yandex.ts
|
||
export class YandexTelemostProvider extends VCSProvider {
|
||
name = 'yandex';
|
||
|
||
constructor(private apiKey: string) {}
|
||
|
||
async getTranscript(meetingId: string): Promise<VCSTranscript> {
|
||
// API Яндекс.Телемост
|
||
const response = await fetch(
|
||
`https://api.telemost.yandex.net/v1/meetings/${meetingId}/transcript`,
|
||
{
|
||
headers: { 'Authorization': `Bearer ${this.apiKey}` }
|
||
}
|
||
);
|
||
|
||
const data = await response.json();
|
||
|
||
return {
|
||
text: data.transcript.text,
|
||
metadata: {
|
||
duration: data.duration,
|
||
participants: data.participants.map(p => p.name),
|
||
recordingUrl: data.recording_url,
|
||
}
|
||
};
|
||
}
|
||
|
||
async testConnection(): Promise<boolean> {
|
||
try {
|
||
await fetch('https://api.telemost.yandex.net/v1/status', {
|
||
headers: { 'Authorization': `Bearer ${this.apiKey}` }
|
||
});
|
||
return true;
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
|
||
// src/integrations/vcs/registry.ts
|
||
export class VCSRegistry {
|
||
private providers = new Map<string, VCSProvider>();
|
||
|
||
register(provider: VCSProvider) {
|
||
this.providers.set(provider.name, provider);
|
||
}
|
||
|
||
get(name: string): VCSProvider | undefined {
|
||
return this.providers.get(name);
|
||
}
|
||
|
||
list(): string[] {
|
||
return Array.from(this.providers.keys());
|
||
}
|
||
}
|
||
```
|
||
|
||
### 10.2. Использование в API
|
||
|
||
```typescript
|
||
// src/routes/meetings.ts
|
||
router.post('/:id/import-from-vcs', async (req, res) => {
|
||
const { id } = req.params;
|
||
const { vcsProvider, vcsMeetingId } = req.body;
|
||
|
||
const provider = vcsRegistry.get(vcsProvider);
|
||
if (!provider) {
|
||
return res.status(400).json({ error: { code: 'UNKNOWN_PROVIDER', message: '...' } });
|
||
}
|
||
|
||
const transcript = await provider.getTranscript(vcsMeetingId);
|
||
|
||
// Сохраняем в meeting
|
||
await db.update(meetings)
|
||
.set({
|
||
rawTranscript: transcript.text,
|
||
sourceType: 'vcs_integration',
|
||
sourceMetadata: transcript.metadata,
|
||
})
|
||
.where(eq(meetings.id, id));
|
||
|
||
res.json({ data: { success: true } });
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
## 11. БЕЗОПАСНОСТЬ
|
||
|
||
### 11.1. Аутентификация и авторизация
|
||
|
||
```typescript
|
||
// src/middleware/auth.ts
|
||
export function authMiddleware(roles?: UserRole[]) {
|
||
return (req, res, next) => {
|
||
const token = req.headers.authorization?.replace('Bearer ', '');
|
||
|
||
if (!token) {
|
||
return res.status(401).json({ error: { code: 'UNAUTHORIZED', message: '...' } });
|
||
}
|
||
|
||
try {
|
||
const payload = jwt.verify(token, process.env.JWT_SECRET) as JWTPayload;
|
||
req.user = payload;
|
||
|
||
if (roles && !roles.includes(payload.role)) {
|
||
return res.status(403).json({ error: { code: 'FORBIDDEN', message: '...' } });
|
||
}
|
||
|
||
next();
|
||
} catch {
|
||
return res.status(401).json({ error: { code: 'INVALID_TOKEN', message: '...' } });
|
||
}
|
||
};
|
||
}
|
||
|
||
// Роли:
|
||
// - admin: полный доступ, управление настройками и пользователями
|
||
// - pmo: создание/редактирование проектов и встреч, утверждение протоколов
|
||
// - viewer: только просмотр
|
||
```
|
||
|
||
### 11.2. Валидация входных данных
|
||
|
||
```typescript
|
||
// src/validators/meeting.validator.ts
|
||
import { z } from 'zod';
|
||
|
||
export const createMeetingSchema = z.object({
|
||
projectId: z.string().uuid(),
|
||
meetingDate: z.string().date(),
|
||
location: z.string().max(200).optional(),
|
||
type: z.enum(['standard', 'op_council', 'steering_council']),
|
||
sourceType: z.enum(['audio', 'text', 'vcs_integration']),
|
||
rawTranscript: z.string().optional(),
|
||
});
|
||
|
||
export function validate(schema: z.ZodSchema) {
|
||
return (req, res, next) => {
|
||
try {
|
||
req.body = schema.parse(req.body);
|
||
next();
|
||
} catch (error) {
|
||
if (error instanceof z.ZodError) {
|
||
return res.status(400).json({
|
||
error: {
|
||
code: 'VALIDATION_ERROR',
|
||
message: 'Invalid input',
|
||
details: error.errors
|
||
}
|
||
});
|
||
}
|
||
next(error);
|
||
}
|
||
};
|
||
}
|
||
```
|
||
|
||
### 11.3. Защита от инъекций и XSS
|
||
|
||
- **SQL-инъекции:** Используем Drizzle ORM с параметризованными запросами (никакого raw SQL с конкатенацией)
|
||
- **XSS:** Все пользовательские данные экранируются на фронте (Vue автоматически экранирует интерполяции)
|
||
- **Markdown:** Используем `DOMPurify` для санитизации HTML, сгенерированного из Markdown
|
||
- **File uploads:** Ограничение по типу (только `.mp3`, `.wav`, `.txt`, `.eml`) и размеру (до 100MB)
|
||
|
||
### 11.4. Rate Limiting
|
||
|
||
```typescript
|
||
// src/middleware/rateLimit.ts
|
||
import rateLimit from 'express-rate-limit';
|
||
|
||
export const apiLimiter = rateLimit({
|
||
windowMs: 15 * 60 * 1000, // 15 минут
|
||
max: 100, // 100 запросов с одного IP
|
||
message: { error: { code: 'RATE_LIMIT', message: 'Too many requests' } }
|
||
});
|
||
|
||
export const aiLimiter = rateLimit({
|
||
windowMs: 60 * 1000, // 1 минута
|
||
max: 10, // 10 AI-запросов в минуту (дорогая операция)
|
||
});
|
||
```
|
||
|
||
### 11.5. Логирование и аудит
|
||
|
||
Все критичные действия логируются в таблицу `audit_log`:
|
||
- Создание/удаление проектов
|
||
- Утверждение протоколов
|
||
- Изменение настроек промптов
|
||
- Попытки несанкционированного доступа
|
||
|
||
---
|
||
|
||
## 12. ПЛАН РАЗРАБОТКИ (ROADMAP)
|
||
|
||
### Этап 1: Фундамент (2 недели)
|
||
**Неделя 1:**
|
||
- ✅ Настройка проекта (monorepo: backend + frontend)
|
||
- ✅ Docker Compose (Postgres + pgvector + backend + frontend)
|
||
- ✅ Drizzle ORM + миграции (все таблицы)
|
||
- ✅ Auth (login, JWT, middleware)
|
||
- ✅ CRUD для Projects и Meetings (API + фронт)
|
||
- ✅ Базовый UI: таблицы, формы, роутинг
|
||
|
||
**Неделя 2:**
|
||
- ✅ Карточка проекта (вкладки, статистика)
|
||
- ✅ Карточка встречи (вкладка "Исходник")
|
||
- ✅ Markdown редактор (md-editor-v3)
|
||
- ✅ Экспорт в DOCX (базовый, без ИИ)
|
||
- ✅ Интеграция с одной ВКС (Яндекс.Телемост)
|
||
|
||
### Этап 2: ИИ и генерация (2 недели)
|
||
**Неделя 3:**
|
||
- ✅ Интеграция OpenAI SDK
|
||
- ✅ Транскрибация аудио (синхронно через Whisper API)
|
||
- ✅ Генерация универсального резюме
|
||
- ✅ Генерация протокола обычной встречи
|
||
- ✅ AI-чат (SSE стриминг)
|
||
|
||
**Неделя 4:**
|
||
- ✅ Генерация протоколов опер. и управляющего советов (RAG)
|
||
- ✅ AI-статус проекта
|
||
- ✅ Диаграмма Ганта (Mermaid.js)
|
||
- ✅ Управление промптами через админку
|
||
|
||
### Этап 3: Векторный поиск и полировка (1 неделя)
|
||
**Неделя 5:**
|
||
- ✅ Модуль векторизации (OpenAI + локальная модель)
|
||
- ✅ Векторный поиск по встречам
|
||
- ✅ UI/UX полировка (анимации, dark mode, адаптив)
|
||
- ✅ Обработка ошибок, тосты, лоадеры
|
||
- ✅ Документация (README, API docs)
|
||
|
||
### Этап 4: Тестирование и релиз (1 неделя)
|
||
**Неделя 6:**
|
||
- ✅ E2E тесты (Playwright)
|
||
- ✅ Нагрузочное тестирование
|
||
- ✅ Security audit
|
||
- ✅ Deployment на prod
|
||
- ✅ Обучение пользователей
|
||
|
||
**Итого: 6 недель до MVP**
|
||
|
||
---
|
||
|
||
## 13. DOCKER COMPOSE (ФИНАЛЬНАЯ КОНФИГУРАЦИЯ)
|
||
|
||
```yaml
|
||
# docker-compose.yml
|
||
version: '3.8'
|
||
|
||
services:
|
||
postgres:
|
||
image: pgvector/pgvector:pg15
|
||
environment:
|
||
POSTGRES_DB: pmo_ai
|
||
POSTGRES_USER: ${DB_USER}
|
||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||
volumes:
|
||
- postgres_data:/var/lib/postgresql/data
|
||
- ./scripts/init-db.sql:/docker-entrypoint-initdb.d/init.sql
|
||
ports:
|
||
- "5432:5432"
|
||
healthcheck:
|
||
test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
|
||
interval: 10s
|
||
timeout: 5s
|
||
retries: 5
|
||
|
||
backend:
|
||
build:
|
||
context: ./backend
|
||
dockerfile: Dockerfile
|
||
environment:
|
||
- NODE_ENV=production
|
||
- DB_HOST=postgres
|
||
- DB_PORT=5432
|
||
- DB_NAME=pmo_ai
|
||
- DB_USER=${DB_USER}
|
||
- DB_PASSWORD=${DB_PASSWORD}
|
||
- JWT_SECRET=${JWT_SECRET}
|
||
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
||
- OPENAI_BASE_URL=${OPENAI_BASE_URL}
|
||
- EMBEDDING_MODE=${EMBEDDING_MODE:-openai}
|
||
volumes:
|
||
- ./models:/app/models # для локальных моделей
|
||
- uploads:/app/uploads
|
||
ports:
|
||
- "3000:3000"
|
||
depends_on:
|
||
postgres:
|
||
condition: service_healthy
|
||
|
||
frontend:
|
||
build:
|
||
context: ./frontend
|
||
dockerfile: Dockerfile
|
||
args:
|
||
- VITE_API_URL=/api
|
||
ports:
|
||
- "80:80"
|
||
depends_on:
|
||
- backend
|
||
|
||
volumes:
|
||
postgres_data:
|
||
uploads:
|
||
```
|
||
|
||
---
|
||
|
||
## 14. ЗАКЛЮЧЕНИЕ
|
||
|
||
Мы спроектировали **современную, масштабируемую архитектуру** для PMO AI Assistant, которая:
|
||
|
||
✅ **Решает бизнес-задачу** — автоматизация 80% рутинной работы проектного офиса
|
||
✅ **Использует лучшие практики 2026** — PostgreSQL + pgvector, Vue 3 Composition API, SSE для стриминга
|
||
✅ **Гибкая к изменениям** — JSONB "подушки безопасности", редактируемые промпты
|
||
✅ **Два режима векторизации** — облачный (OpenAI) и локальный (transformers) для работы в закрытом контуре
|
||
✅ **Современный UI** — Bento Grid, dark mode, плавные анимации, AI-native дизайн
|
||
✅ **Безопасная** — JWT auth, RBAC, rate limiting, аудит
|
||
✅ **Готова к масштабированию** — модульная архитектура, легко добавить новые ВКС, новые типы протоколов
|
||
|
||
**Следующие шаги:**
|
||
1. Утвердить ТЗ с заказчиком
|
||
2. Настроить репозиторий и CI/CD
|
||
3. Начать Этап 1 (Фундамент)
|