ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: hardening Voice Tasker routing, сроков и transcript
This commit is contained in:
parent
d3b47326da
commit
597480adb9
|
|
@ -52,11 +52,12 @@ Voice Tasker не создает отдельную модель задачи.
|
|||
|
||||
1. workspace AI settings;
|
||||
2. encrypted workspace credentials;
|
||||
3. voice sessions;
|
||||
4. voice memory.
|
||||
3. voice sessions.
|
||||
|
||||
Не создавать отдельные `VoiceTask`, `VoiceProject`, `VoiceUser`, `VoiceInbox` как обязательные бизнес-сущности.
|
||||
|
||||
Voice memory в MVP не является отдельной бизнес-сущностью и не требует отдельной таблицы. Последние voice-действия восстанавливаются из `voice_task_sessions.created_task_id`, `voice_task_sessions.updated_task_id`, `parsed_json`, `created_at`, `updated_at`. Отдельная `voice_task_memory` допустима только как будущая оптимизация, если session-backed memory перестанет закрывать продуктовый сценарий.
|
||||
|
||||
### 2.2. Backend-only OpenAI
|
||||
|
||||
Frontend:
|
||||
|
|
@ -75,7 +76,7 @@ Backend:
|
|||
- валидирует JSON;
|
||||
- резолвит project/member;
|
||||
- создает/редактирует work item через внутренние Plane модели/serializer/service;
|
||||
- пишет voice session и memory.
|
||||
- пишет voice session и session-backed memory.
|
||||
|
||||
### 2.3. Не использовать внешний Plane REST API
|
||||
|
||||
|
|
@ -148,10 +149,27 @@ Native поля "deadline time" у work item сейчас нет.
|
|||
2. фраза "до 15:00" сохраняется в draft как `due_time`;
|
||||
3. при commit `due_time` не создает новое поле в `Issue`;
|
||||
4. если время важно, оно добавляется в `description_html` отдельной строкой, например: `Ориентир по времени: до 15:00`;
|
||||
5. в `voice_task_sessions.parsed_json` время сохраняется как структурированное значение для будущей миграции.
|
||||
5. относительные сроки вида "на две недели вперед", "на месяц назад", "через два месяца и две недели", "на год перенеси" нормализуются backend rule-based слоем относительно даты пользователя/workspace;
|
||||
6. если задача уже имеет срок и команда звучит как перенос/сдвиг, допустимо считать от текущего `Issue.target_date`;
|
||||
7. в `voice_task_sessions.parsed_json` время сохраняется как структурированное значение для будущей миграции.
|
||||
|
||||
Отдельное native поле дедлайна со временем (`target_datetime`, `due_at` или аналог) не входит в MVP и выносится в backlog как архитектурное расширение.
|
||||
|
||||
### 3.5. Статус задачи
|
||||
|
||||
В текущей модели Plane статус work item хранится через `Issue.state_id` и проектные `State`.
|
||||
|
||||
MVP-правило:
|
||||
|
||||
1. если пользователь явно говорит "в работе", "в реализации", "в реализацию", "активный статус", "в активном статусе" - resolver выбирает state из group `started`;
|
||||
2. если пользователь явно называет статус проекта - resolver делает exact/fuzzy match по имени state;
|
||||
3. если модель не вернула `state_hint`, backend делает ограниченный fallback по явной status-фразе из transcript;
|
||||
4. если статус не назван, backend не должен отдавать выбор случайному default/последнему статусу Plane;
|
||||
5. default для новой voice-задачи: первый `unstarted` state проекта, затем первый `started`, затем backlog только как последний fallback;
|
||||
6. `completed`/`cancelled` допустимы только при явном status/state hint пользователя.
|
||||
|
||||
Цель: voice-create не должен создавать задачи в закрытых/отложенных статусах и не должен попадать в backlog, если в проекте есть нормальный open-state.
|
||||
|
||||
---
|
||||
|
||||
## 4. Пользовательские сценарии
|
||||
|
|
@ -170,6 +188,7 @@ Parser возвращает draft:
|
|||
{
|
||||
"intent": "create_task",
|
||||
"project_hint": "контур бухгалтерии",
|
||||
"state_hint": null,
|
||||
"assignee_hint": "Настя / бухгалтер Настя",
|
||||
"title": "Подготовить декларацию по НДС",
|
||||
"description": "Необходимо подготовить декларацию по НДС.",
|
||||
|
|
@ -202,10 +221,13 @@ Commit маппит draft в Plane:
|
|||
|
||||
MVP-правило:
|
||||
|
||||
1. если `project_confidence >= 0.8`, можно auto-create;
|
||||
2. если проект не найден уверенно, показываем preview с ручным выбором project;
|
||||
3. если admin заранее указал `default_project_id`, можно предложить его как fallback;
|
||||
4. если fallback project не задан, задачу не создаем автоматически.
|
||||
1. backend сначала ищет проект в самом transcript по именам, identifiers и утвержденным алиасам;
|
||||
2. transcript-резолв имеет приоритет над `project_hint`, потому что модель может галлюцинировать текущий проект;
|
||||
3. если `project_hint` найден уверенно и не конфликтует с transcript, можно auto-create;
|
||||
4. если пользователь явно назвал контур/проект, но resolver не нашел его уверенно, запрещено тихо fallback-иться в current/default project;
|
||||
5. если проект не найден уверенно, показываем preview с ручным выбором project;
|
||||
6. если admin заранее указал `default_project_id`, можно использовать его только когда пользователь не называл другой проект явно;
|
||||
7. если fallback project не задан, задачу не создаем автоматически.
|
||||
|
||||
Не создавать "общую помойку" автоматически.
|
||||
|
||||
|
|
@ -235,7 +257,16 @@ MVP-правило:
|
|||
3. показывает preview изменения, если confidence низкий;
|
||||
4. меняет `Issue.target_date`;
|
||||
5. сохраняет `due_time` в description note / parsed JSON;
|
||||
6. пишет новое действие в voice memory.
|
||||
6. пишет новое действие в session-backed memory.
|
||||
|
||||
Если пользователь говорит "переложи последнюю задачу в проект X", это остается `update_task`, но backend должен:
|
||||
|
||||
1. найти target issue через session-backed memory;
|
||||
2. отдельно зарезолвить целевой project из transcript/project hint;
|
||||
3. показать в preview `project_change.from -> project_change.to`;
|
||||
4. при commit перенести обычную `Issue` в целевой `Project`, выдать ей новый `sequence_id` целевого проекта и валидный `State`;
|
||||
5. для фраз "из Бухгалтерии в Менеджмент" считать project после destination-маркера "в/во/на/to" целевым, а project после "из/from" исходным контекстом;
|
||||
6. не считать update успешным, если целевой project не найден или у пользователя нет прав создать/редактировать задачу в целевом проекте.
|
||||
|
||||
### 4.5. Удаление последней voice-задачи
|
||||
|
||||
|
|
@ -249,7 +280,7 @@ MVP-правило:
|
|||
|
||||
- удаление всегда требует confirmation modal;
|
||||
- backend повторно проверяет права на удаление;
|
||||
- действие пишется в voice memory;
|
||||
- действие пишется в session-backed memory;
|
||||
- предпочтительно использовать тот же delete path, что обычный work item, чтобы сохранился activity log.
|
||||
|
||||
---
|
||||
|
|
@ -294,8 +325,8 @@ recording - идет запись
|
|||
uploading - отправка аудио
|
||||
processing - транскрибация и разбор
|
||||
success - draft разобран
|
||||
committing - создание задачи
|
||||
committed - задача создана / обновлена
|
||||
committing - применение voice-действия
|
||||
committed - задача создана / обновлена / удалена
|
||||
error - ошибка
|
||||
```
|
||||
|
||||
|
|
@ -316,7 +347,7 @@ error - ошибка
|
|||
|
||||
Кнопки:
|
||||
|
||||
- `Создать задачу` / `Применить изменения`;
|
||||
- `Создать задачу` / `Применить изменения` / `Удалить задачу`;
|
||||
- `Редактировать`;
|
||||
- `Отмена`.
|
||||
|
||||
|
|
@ -397,7 +428,6 @@ Backend:
|
|||
GET /api/workspaces/:workspaceSlug/voice-task/preflight
|
||||
POST /api/workspaces/:workspaceSlug/voice-task/parse
|
||||
POST /api/workspaces/:workspaceSlug/voice-task/commit
|
||||
POST /api/workspaces/:workspaceSlug/voice-task/resolve-command
|
||||
```
|
||||
|
||||
### 7.1. Preflight
|
||||
|
|
@ -473,6 +503,7 @@ Response:
|
|||
"intent": "create_task",
|
||||
"target_memory_ref": null,
|
||||
"project_hint": "контур бухгалтерии",
|
||||
"state_hint": null,
|
||||
"assignee_hint": "Настя",
|
||||
"title": "Подготовить декларацию по НДС",
|
||||
"description": "Необходимо подготовить декларацию по НДС.",
|
||||
|
|
@ -497,6 +528,13 @@ Response:
|
|||
"confidence": 0.91,
|
||||
"source": "project_hint"
|
||||
},
|
||||
"state": {
|
||||
"id": "state_uuid",
|
||||
"name": "К выполнению",
|
||||
"group": "unstarted",
|
||||
"confidence": 0.65,
|
||||
"source": "default_open_state"
|
||||
},
|
||||
"assignee": {
|
||||
"id": "user_uuid",
|
||||
"name": "Настя",
|
||||
|
|
@ -510,6 +548,7 @@ Response:
|
|||
"name": "voice"
|
||||
}
|
||||
],
|
||||
"target_task": null,
|
||||
"warnings": [],
|
||||
"can_commit": true
|
||||
},
|
||||
|
|
@ -536,11 +575,12 @@ Payload:
|
|||
```json
|
||||
{
|
||||
"voice_session_id": "uuid",
|
||||
"action": "create_task",
|
||||
"action": "create_task | update_task | delete_task",
|
||||
"draft": {
|
||||
"title": "Подготовить декларацию по НДС",
|
||||
"description": "Необходимо подготовить декларацию по НДС.",
|
||||
"project_id": "project_uuid",
|
||||
"state_hint": null,
|
||||
"assignee_ids": ["user_uuid"],
|
||||
"due_date": "2026-04-24",
|
||||
"due_time": "15:00",
|
||||
|
|
@ -558,6 +598,7 @@ Internal Plane payload:
|
|||
"description_html": "<p>Необходимо подготовить декларацию по НДС.</p><p><strong>Ориентир по времени:</strong> до 15:00</p>",
|
||||
"target_date": "2026-04-24",
|
||||
"priority": "high",
|
||||
"state_id": "state_uuid",
|
||||
"assignees": ["user_uuid"],
|
||||
"labels": ["label_uuid"]
|
||||
}
|
||||
|
|
@ -583,6 +624,13 @@ Response:
|
|||
"confidence": 0.91,
|
||||
"source": "project_hint"
|
||||
},
|
||||
"state": {
|
||||
"id": "state_uuid",
|
||||
"name": "К выполнению",
|
||||
"group": "unstarted",
|
||||
"confidence": 0.65,
|
||||
"source": "default_open_state"
|
||||
},
|
||||
"assignee": {
|
||||
"id": "user_uuid",
|
||||
"name": "Настя",
|
||||
|
|
@ -591,13 +639,20 @@ Response:
|
|||
"source": "assignee_hint"
|
||||
},
|
||||
"labels": [{ "id": "label_uuid", "name": "voice" }],
|
||||
"target_task": null,
|
||||
"warnings": [],
|
||||
"can_commit": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
На Stage 4 commit поддерживает только `create_task`. `update_task` и `delete_task` остаются в следующих этапах, потому что требуют voice memory и отдельного confirmation policy.
|
||||
Commit поддерживает:
|
||||
|
||||
- `create_task` - создает обычную `Issue`;
|
||||
- `update_task` - находит target issue через session-backed memory и применяет частичный update;
|
||||
- `delete_task` - находит target issue через session-backed memory и удаляет через тот же delete path, что обычный work item.
|
||||
|
||||
Для `update_task`/`delete_task` response содержит `resolution.target_task` с ключом, названием, проектом и источником резолва.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -675,28 +730,32 @@ Audio file в MVP не хранить после обработки.
|
|||
|
||||
Transcript и parsed JSON хранить для поддержки preview, отладки и memory. Retention policy нужно вынести в отдельную настройку после MVP.
|
||||
|
||||
### 8.4. `voice_task_memory`
|
||||
### 8.4. Session-backed voice memory
|
||||
|
||||
Поля:
|
||||
В MVP отдельная таблица `voice_task_memory` не создается.
|
||||
|
||||
Источник memory:
|
||||
|
||||
```txt
|
||||
id
|
||||
workspace_id
|
||||
user_id
|
||||
task_id
|
||||
voice_session_id
|
||||
action
|
||||
summary
|
||||
created_at
|
||||
voice_task_sessions.workspace_id
|
||||
voice_task_sessions.user_id
|
||||
voice_task_sessions.intent
|
||||
voice_task_sessions.parsed_json
|
||||
voice_task_sessions.created_task_id
|
||||
voice_task_sessions.updated_task_id
|
||||
voice_task_sessions.created_at
|
||||
voice_task_sessions.updated_at
|
||||
```
|
||||
|
||||
Использование:
|
||||
|
||||
- хранить последние N voice-действий пользователя;
|
||||
- N по умолчанию: 10;
|
||||
- N по умолчанию для parser context: 5;
|
||||
- резолвить "последняя задача", "предыдущая задача", "та задача в бухгалтерии";
|
||||
- не использовать memory как источник истины вместо `Issue`.
|
||||
|
||||
Если в будущем понадобится отдельная агрегированная history/memory-таблица, она должна быть добавлена отдельным архитектурным этапом и не должна дублировать `Issue` как источник истины.
|
||||
|
||||
---
|
||||
|
||||
## 9. OpenAI pipeline
|
||||
|
|
@ -741,7 +800,26 @@ Input:
|
|||
"transcript": "...",
|
||||
"workspace_projects": [],
|
||||
"workspace_members": [],
|
||||
"recent_voice_memory": [],
|
||||
"recent_voice_memory": [
|
||||
{
|
||||
"voice_session_id": "uuid",
|
||||
"intent": "create_task",
|
||||
"title": "Подготовить декларацию по НДС",
|
||||
"project_hint": "Бухгалтерия",
|
||||
"target_task": {
|
||||
"id": "issue_uuid",
|
||||
"title": "Подготовить декларацию по НДС",
|
||||
"key": "BUH-128",
|
||||
"project_id": "project_uuid",
|
||||
"project_name": "Бухгалтерия",
|
||||
"project_identifier": "BUH",
|
||||
"sequence_id": 128,
|
||||
"source": "recent_voice_memory",
|
||||
"voice_session_id": "uuid"
|
||||
},
|
||||
"created_at": "2026-04-24T10:00:00+03:00"
|
||||
}
|
||||
],
|
||||
"current_date": "2026-04-24",
|
||||
"timezone": "Europe/Moscow"
|
||||
}
|
||||
|
|
@ -752,7 +830,7 @@ Output строго JSON:
|
|||
```json
|
||||
{
|
||||
"intent": "create_task | update_task | delete_task | unknown",
|
||||
"target_memory_ref": "last_task | previous_task | explicit_task | null",
|
||||
"target_memory_ref": "voice_session_id | issue_key | issue_id | null",
|
||||
"project_hint": "string | null",
|
||||
"assignee_hint": "string | null",
|
||||
"title": "string | null",
|
||||
|
|
@ -789,20 +867,37 @@ Return JSON only.
|
|||
Вход:
|
||||
|
||||
- `project_hint`;
|
||||
- transcript;
|
||||
- список проектов workspace, доступных пользователю;
|
||||
- `current_project_id`, если пользователь находится внутри проекта;
|
||||
- `default_project_id` из settings.
|
||||
|
||||
Логика:
|
||||
|
||||
1. exact match по имени/identifier;
|
||||
2. fuzzy match по имени;
|
||||
3. current project как слабый fallback;
|
||||
4. default project как fallback, если задан;
|
||||
5. если confidence низкий - preview с ручным выбором.
|
||||
1. exact/alias match по transcript: имя проекта, identifier, короткие алиасы (`MGR`, `BUH`, `CODEX`) и согласованные русские UX-формы вроде "контур менеджмент";
|
||||
2. transcript match имеет приоритет над `project_hint`;
|
||||
3. exact/fuzzy match по `project_hint` и candidates проекта;
|
||||
4. current project как слабый fallback только если пользователь не назвал проект/контур явно;
|
||||
5. default project как fallback только если пользователь не назвал проект/контур явно;
|
||||
6. если confidence низкий - preview с ручным выбором;
|
||||
7. если transcript содержит явную маршрутизацию проекта, но model `project_hint` указывает на проект, которого нет в transcript, auto-commit блокируется;
|
||||
8. при наличии source/destination фразы выбирать destination project: "из Бухгалтерии в Менеджмент" -> target `Менеджмент`, "из Менеджмента в Бухгалтерию" -> target `Бухгалтерия`.
|
||||
|
||||
Не зашивать термин "контур" как обязательный. Для NODE DC это важный UX-термин, но технически это обычный `Project`.
|
||||
|
||||
### 10.1.1. Перенос задачи между проектами
|
||||
|
||||
Команда вида "переложи последнюю задачу в проект Менеджмент" не создает новую задачу.
|
||||
|
||||
Правило:
|
||||
|
||||
1. target issue находится через session-backed memory;
|
||||
2. целевой project резолвится тем же project resolver, но без current/default fallback;
|
||||
3. если целевой project отличается от исходного, response содержит `resolution.project_change`;
|
||||
4. commit переносит `Issue.project_id`, выдает новый `sequence_id` целевого проекта через `IssueSequence`, выбирает state целевого проекта по явному `state_hint`, group исходного state или default open-state;
|
||||
5. assignees/labels сохраняются только если они валидны в целевом project;
|
||||
6. при неуверенном project resolver или недостатке прав перенос не выполняется.
|
||||
|
||||
### 10.2. Assignee resolver
|
||||
|
||||
Вход:
|
||||
|
|
@ -820,15 +915,60 @@ Return JSON only.
|
|||
|
||||
Назначать можно только пользователей, которые состоят в project и имеют достаточную роль.
|
||||
|
||||
### 10.3. Date resolver
|
||||
### 10.3. State resolver
|
||||
|
||||
Вход:
|
||||
|
||||
- `state_hint`;
|
||||
- states выбранного project;
|
||||
- transcript как fallback, если модель не вернула `state_hint`;
|
||||
- group state: `backlog`, `unstarted`, `started`, `completed`, `cancelled`.
|
||||
|
||||
Логика:
|
||||
|
||||
1. exact/fuzzy match по имени state;
|
||||
2. словари синонимов для group:
|
||||
- "в работе", "в реализации", "в реализацию", "активный", "активном статусе" -> `started`;
|
||||
- "к выполнению", "todo", "новая" -> `unstarted`;
|
||||
- "бэклог", "backlog" -> `backlog`;
|
||||
- "готово", "закрыто" -> `completed`;
|
||||
3. если `state_hint` отсутствует, но transcript содержит явную status-фразу, использовать ее как backend fallback;
|
||||
4. если `state_hint` отсутствует при создании - выбрать default open-state: сначала `unstarted`, затем `started`, затем backlog;
|
||||
5. не выбирать `completed`/`cancelled` без явного state hint пользователя.
|
||||
|
||||
### 10.4. Date resolver
|
||||
|
||||
MVP:
|
||||
|
||||
- сегодня;
|
||||
- завтра;
|
||||
- послезавтра / вчера / позавчера;
|
||||
- `N` дней/недель/месяцев/лет вперед;
|
||||
- `N` дней/недель/месяцев/лет назад;
|
||||
- сложные интервалы: "два месяца и две недели";
|
||||
- числительные цифрами и частые русские словоформы: "2 недели", "две недели", "пару дней";
|
||||
- защита от ложных матчей внутри слов: "последней" не считается как "дней";
|
||||
- конкретная дата;
|
||||
- конкретное время как `due_time` note.
|
||||
|
||||
Date resolver обязан работать после OpenAI parser как deterministic fallback. Если модель уже вернула валидный `due_date`, backend его не переписывает.
|
||||
|
||||
### 10.5. Voice task representation in Issue
|
||||
|
||||
Voice-created/voice-updated work item остается обычной `Issue`, но получает техническую маркировку:
|
||||
|
||||
- `Issue.external_source = voice_tasker`;
|
||||
- `Issue.external_id = voice_session_id`.
|
||||
|
||||
Описание voice-created задачи формируется многоуровнево:
|
||||
|
||||
1. источник `Voice Tasker`;
|
||||
2. подробная постановка из parser `description`;
|
||||
3. декомпозиция из `checklist`, если она есть;
|
||||
4. исходная транскрибация пользователя отдельным блоком `Исходная транскрибация пользователя`.
|
||||
|
||||
Kanban/list UI может использовать `external_source=voice_tasker` для отличимого отображения voice-задач без создания отдельной бизнес-сущности.
|
||||
|
||||
Backlog:
|
||||
|
||||
- к пятнице;
|
||||
|
|
@ -927,7 +1067,7 @@ MVP:
|
|||
- хранить transcript N дней;
|
||||
- очищать transcript после commit;
|
||||
- хранить только parsed JSON;
|
||||
- отключать voice memory для sensitive workspace.
|
||||
- отключать session-backed voice memory для sensitive workspace.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -991,6 +1131,7 @@ voice_task.error
|
|||
### Stage 4 - Preview и создание задачи
|
||||
|
||||
- project resolver;
|
||||
- state resolver и безопасный default open-state;
|
||||
- assignee resolver;
|
||||
- date resolver MVP;
|
||||
- preview modal;
|
||||
|
|
@ -998,14 +1139,19 @@ voice_task.error
|
|||
- создание `Issue` через внутренний Plane layer;
|
||||
- activity log/model activity как у обычного work item;
|
||||
- точечное обновление активного issue-store после commit без reload/polling;
|
||||
- `voice_task_memory` для created action.
|
||||
- session-backed memory для created action.
|
||||
|
||||
### Stage 5 - Memory commands
|
||||
|
||||
- update last task;
|
||||
- delete last task with confirmation;
|
||||
- append description/checklist to last task;
|
||||
- memory resolver для "последняя/предыдущая/та задача".
|
||||
- memory resolver для "последняя/предыдущая/та задача";
|
||||
- transcript-first project routing и базовые project aliases;
|
||||
- перенос последней voice-задачи между проектами через обычную `Issue`.
|
||||
- относительные сроки русским естественным языком;
|
||||
- маркировка voice-задач через `Issue.external_source`;
|
||||
- сохранение полного transcript в description_html созданной/обновленной задачи.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -1017,7 +1163,7 @@ voice_task.error
|
|||
- Voice Inbox как отдельный управляемый fallback project/triage flow;
|
||||
- Redis/Celery-backed VoiceTaskQueue;
|
||||
- transcript retention policies;
|
||||
- project/member aliases;
|
||||
- расширяемые project/member aliases в настройках workspace;
|
||||
- выбранные роли beyond `all members/admins only`;
|
||||
- monthly budget/soft cap;
|
||||
- multi-provider AI;
|
||||
|
|
@ -1039,18 +1185,22 @@ voice_task.error
|
|||
7. Backend транскрибирует через OpenAI.
|
||||
8. Backend формирует валидный structured draft.
|
||||
9. Project resolver выбирает project или требует ручной выбор.
|
||||
10. Assignee resolver назначает только уверенно найденного project member.
|
||||
11. Если assignee не найден - задача может быть создана без assignee.
|
||||
12. Commit создает обычную `Issue` через внутренний Plane backend layer.
|
||||
13. После commit активная доска/список обновляется без reload страницы.
|
||||
14. `due_date` маппится в `target_date`.
|
||||
15. `due_time` сохраняется как note в description и parsed JSON, без новой колонки в MVP.
|
||||
15. Voice session сохраняется.
|
||||
16. Последняя voice-задача сохраняется в memory.
|
||||
17. Update last task работает минимум для `target_date` и description.
|
||||
18. Delete last task требует confirmation.
|
||||
19. User/workspace limits работают.
|
||||
20. OpenAI errors показываются пользователю как нормальные сообщения без raw stack trace.
|
||||
10. State resolver выбирает явный state или безопасный default open-state.
|
||||
11. Assignee resolver назначает только уверенно найденного project member.
|
||||
12. Если assignee не найден - задача может быть создана без assignee.
|
||||
13. Commit создает обычную `Issue` через внутренний Plane backend layer.
|
||||
14. После commit активная доска/список обновляется без reload страницы.
|
||||
15. `due_date` маппится в `target_date`.
|
||||
16. `due_time` сохраняется как note в description и parsed JSON, без новой колонки в MVP.
|
||||
17. Voice session сохраняется.
|
||||
18. Последняя voice-задача сохраняется в session-backed memory.
|
||||
19. Update last task работает минимум для `target_date`, state и description.
|
||||
20. Delete last task требует confirmation.
|
||||
21. User/workspace limits работают.
|
||||
22. OpenAI errors показываются пользователю как нормальные сообщения без raw stack trace.
|
||||
23. Относительная дата "на две недели вперед" меняет `Issue.target_date` без ручного ввода ISO-даты.
|
||||
24. Перенос "из Бухгалтерии в Менеджмент" и "из Менеджмента в Бухгалтерию" реально меняет `Issue.project_id` и `task_key`.
|
||||
25. Voice-created task имеет `external_source=voice_tasker` и хранит исходный transcript в description.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -799,6 +799,8 @@ class IssueSerializer(DynamicBaseSerializer):
|
|||
"link_count",
|
||||
"is_draft",
|
||||
"archived_at",
|
||||
"external_source",
|
||||
"external_id",
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
|
|
@ -852,6 +854,8 @@ class IssueListDetailSerializer(serializers.Serializer):
|
|||
"updated_by": instance.updated_by_id,
|
||||
"is_draft": instance.is_draft,
|
||||
"archived_at": instance.archived_at,
|
||||
"external_source": instance.external_source,
|
||||
"external_id": instance.external_id,
|
||||
"source_project_name": getattr(instance, "source_project_name", None),
|
||||
# Computed fields
|
||||
"cycle_id": instance.cycle_id,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -12,7 +12,7 @@ import { observer } from "mobx-react";
|
|||
import { useParams } from "next/navigation";
|
||||
import { useOutsideClickDetector } from "@plane/hooks";
|
||||
// plane helpers
|
||||
import { MoreHorizontal } from "lucide-react";
|
||||
import { Mic, MoreHorizontal } from "lucide-react";
|
||||
// types
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
|
|
@ -99,6 +99,7 @@ const KanbanIssueDetailsBlock = observer(function KanbanIssueDetailsBlock(props:
|
|||
|
||||
// derived values
|
||||
const subIssueCount = issue?.sub_issues_count ?? 0;
|
||||
const isVoiceTask = issue?.external_source === "voice_tasker";
|
||||
|
||||
const handleEventPropagation = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
|
|
@ -151,6 +152,13 @@ const KanbanIssueDetailsBlock = observer(function KanbanIssueDetailsBlock(props:
|
|||
</div>
|
||||
</Tooltip>
|
||||
|
||||
{isVoiceTask && (
|
||||
<div className="inline-flex max-w-full items-center gap-1 rounded border border-pink-500/30 bg-pink-500/10 px-1.5 py-0.5 text-11 font-medium text-pink-300">
|
||||
<Mic className="h-3 w-3 shrink-0" />
|
||||
<span>Voice</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<IssueProperties
|
||||
className="flex flex-wrap items-center gap-2 pt-1.5 whitespace-nowrap text-tertiary"
|
||||
issue={issue}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
import { CheckCircle2, Mic, Plus, RotateCcw, Square, Upload, X } from "lucide-react";
|
||||
import { CheckCircle2, Mic, Pencil, Plus, RotateCcw, Square, Trash2, Upload, X } from "lucide-react";
|
||||
// plane imports
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
|
|
@ -61,6 +61,43 @@ function getRouteParam(value: string | string[] | undefined) {
|
|||
return value?.toString();
|
||||
}
|
||||
|
||||
function getCommitButtonLabel(intent?: string) {
|
||||
if (intent === "update_task") return "Применить изменения";
|
||||
if (intent === "delete_task") return "Удалить задачу";
|
||||
return "Создать задачу";
|
||||
}
|
||||
|
||||
function getCommitStatusLabel(status: TVoiceTaskerStatus) {
|
||||
if (status === "committed") return "Committed";
|
||||
if (status === "success") return "Draft parsed";
|
||||
if (status === "committing") return "Committing";
|
||||
if (status === "uploading") return "Processing";
|
||||
if (status === "recording") return "Recording";
|
||||
if (status === "error") return "Error";
|
||||
return "Ready";
|
||||
}
|
||||
|
||||
function getCommitSuccessTitle(result: TVoiceTaskCommitResult) {
|
||||
if (result.status === "updated") return "Задача обновлена";
|
||||
if (result.status === "deleted") return "Задача удалена";
|
||||
return "Задача создана";
|
||||
}
|
||||
|
||||
function getCommitSuccessMessage(result: TVoiceTaskCommitResult) {
|
||||
if (result.task_key && result.status === "updated") return `Обновлена ${result.task_key}`;
|
||||
if (result.task_key && result.status === "deleted") return `Удалена ${result.task_key}`;
|
||||
if (result.task_key) return `Создана ${result.task_key}`;
|
||||
if (result.status === "updated") return "Work item обновлен.";
|
||||
if (result.status === "deleted") return "Work item удален.";
|
||||
return "Work item создан.";
|
||||
}
|
||||
|
||||
function getVoiceTaskWarnings(result: TVoiceTaskUploadResult) {
|
||||
return Array.from(
|
||||
new Set([...(result.warnings ?? []), ...(result.resolution?.warnings ?? []), ...(result.draft?.questions ?? [])])
|
||||
);
|
||||
}
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
|
@ -174,7 +211,11 @@ export function VoiceTaskerGlobalControl({ workspaceSlug }: Props) {
|
|||
}, [audioBlob]);
|
||||
|
||||
const startRecording = async () => {
|
||||
if (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia || typeof MediaRecorder === "undefined") {
|
||||
if (
|
||||
typeof navigator === "undefined" ||
|
||||
!navigator.mediaDevices?.getUserMedia ||
|
||||
typeof MediaRecorder === "undefined"
|
||||
) {
|
||||
setError("Браузер не поддерживает запись аудио.");
|
||||
setStatus("error");
|
||||
return;
|
||||
|
|
@ -230,6 +271,7 @@ export function VoiceTaskerGlobalControl({ workspaceSlug }: Props) {
|
|||
setStatus("uploading");
|
||||
setError(null);
|
||||
setParseResult(null);
|
||||
setCommitResult(null);
|
||||
|
||||
const audioType = audioBlob.type || "audio/webm";
|
||||
const extension = audioType.includes("mp4") ? "m4a" : "webm";
|
||||
|
|
@ -256,7 +298,8 @@ export function VoiceTaskerGlobalControl({ workspaceSlug }: Props) {
|
|||
message: "Transcript и draft получены.",
|
||||
});
|
||||
} catch (err) {
|
||||
const message = typeof err === "object" && err && "error" in err ? String(err.error) : "Не удалось отправить аудио.";
|
||||
const message =
|
||||
typeof err === "object" && err && "error" in err ? String(err.error) : "Не удалось отправить аудио.";
|
||||
setError(message);
|
||||
setStatus("error");
|
||||
setToast({
|
||||
|
|
@ -299,13 +342,23 @@ export function VoiceTaskerGlobalControl({ workspaceSlug }: Props) {
|
|||
const commitVoiceTask = async () => {
|
||||
if (!parseResult?.voice_session_id || !parseResult.draft) return;
|
||||
|
||||
const action = parseResult.draft.intent;
|
||||
if (action === "unknown") return;
|
||||
|
||||
if (action === "delete_task") {
|
||||
const targetTitle =
|
||||
parseResult.resolution?.target_task?.key || parseResult.resolution?.target_task?.title || "последнюю задачу";
|
||||
const confirmed = window.confirm(`Удалить ${targetTitle}?`);
|
||||
if (!confirmed) return;
|
||||
}
|
||||
|
||||
setStatus("committing");
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const result = await workspaceAIService.commitVoiceTask(workspaceSlug, {
|
||||
voice_session_id: parseResult.voice_session_id,
|
||||
action: "create_task",
|
||||
action,
|
||||
draft: parseResult.draft,
|
||||
});
|
||||
await refreshVisibleIssueStores(result.project_id);
|
||||
|
|
@ -313,16 +366,17 @@ export function VoiceTaskerGlobalControl({ workspaceSlug }: Props) {
|
|||
setStatus("committed");
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Задача создана",
|
||||
message: result.task_key ? `Создана ${result.task_key}` : "Work item создан.",
|
||||
title: getCommitSuccessTitle(result),
|
||||
message: getCommitSuccessMessage(result),
|
||||
});
|
||||
} catch (err) {
|
||||
const message = typeof err === "object" && err && "error" in err ? String(err.error) : "Не удалось создать задачу.";
|
||||
const message =
|
||||
typeof err === "object" && err && "error" in err ? String(err.error) : "Не удалось применить Voice Task.";
|
||||
setError(message);
|
||||
setStatus("error");
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Задача не создана",
|
||||
title: "Voice Task не применен",
|
||||
message,
|
||||
});
|
||||
}
|
||||
|
|
@ -330,14 +384,14 @@ export function VoiceTaskerGlobalControl({ workspaceSlug }: Props) {
|
|||
|
||||
return (
|
||||
<>
|
||||
<div className="pointer-events-none fixed right-4 z-[29] bottom-[calc(var(--nodedc-bottom-dock-offset,0px)+1rem)]">
|
||||
<div className="pointer-events-none fixed right-4 bottom-[calc(var(--nodedc-bottom-dock-offset,0px)+1rem)] z-[29]">
|
||||
<Tooltip tooltipContent={tooltipContent} position="left">
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"pointer-events-auto flex size-11 items-center justify-center rounded-full border-[0.5px] shadow-lg transition",
|
||||
"shadow-lg pointer-events-auto flex size-11 items-center justify-center rounded-full border-[0.5px] transition",
|
||||
isAvailable
|
||||
? "border-pink-500/40 bg-pink-500 text-white hover:bg-pink-600"
|
||||
? "border-pink-500/40 bg-pink-500 hover:bg-pink-600 text-white"
|
||||
: "cursor-not-allowed border-subtle bg-layer-2 text-tertiary"
|
||||
)}
|
||||
disabled={!isAvailable}
|
||||
|
|
@ -368,19 +422,7 @@ export function VoiceTaskerGlobalControl({ workspaceSlug }: Props) {
|
|||
<div className="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<div className="text-24 font-semibold text-primary">{formatDuration(duration)}</div>
|
||||
<div className="mt-1 text-12 text-tertiary">
|
||||
{status === "committed"
|
||||
? "Created"
|
||||
: status === "success"
|
||||
? "Draft parsed"
|
||||
: isCommitting
|
||||
? "Creating"
|
||||
: isUploading
|
||||
? "Processing"
|
||||
: isRecording
|
||||
? "Recording"
|
||||
: "Ready"}
|
||||
</div>
|
||||
<div className="mt-1 text-12 text-tertiary">{getCommitStatusLabel(status)}</div>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
|
|
@ -399,7 +441,7 @@ export function VoiceTaskerGlobalControl({ workspaceSlug }: Props) {
|
|||
)}
|
||||
|
||||
{error && (
|
||||
<div className="mt-4 rounded-md border-[0.5px] border-red-500/30 bg-red-500/10 px-3 py-2 text-12 text-red-500">
|
||||
<div className="border-red-500/30 bg-red-500/10 text-red-500 mt-4 rounded-md border-[0.5px] px-3 py-2 text-12">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -407,13 +449,13 @@ export function VoiceTaskerGlobalControl({ workspaceSlug }: Props) {
|
|||
{parseResult?.draft && (
|
||||
<div className="mt-4 space-y-3 rounded-md border-[0.5px] border-subtle bg-layer-2 p-3">
|
||||
<div className="flex items-center gap-2 text-13 font-medium text-primary">
|
||||
<CheckCircle2 className="size-4 text-green-500" />
|
||||
<CheckCircle2 className="text-green-500 size-4" />
|
||||
Draft готов
|
||||
</div>
|
||||
|
||||
{parseResult.transcript && (
|
||||
<div>
|
||||
<div className="text-11 font-medium uppercase text-tertiary">Транскрипт</div>
|
||||
<div className="text-11 font-medium text-tertiary uppercase">Транскрипт</div>
|
||||
<p className="mt-1 max-h-20 overflow-y-auto text-12 leading-5 text-secondary">
|
||||
{parseResult.transcript}
|
||||
</p>
|
||||
|
|
@ -422,40 +464,66 @@ export function VoiceTaskerGlobalControl({ workspaceSlug }: Props) {
|
|||
|
||||
<div className="grid grid-cols-1 gap-2 text-12 sm:grid-cols-2">
|
||||
<div>
|
||||
<div className="text-11 font-medium uppercase text-tertiary">Название</div>
|
||||
<div className="text-11 font-medium text-tertiary uppercase">Название</div>
|
||||
<div className="mt-0.5 text-primary">{parseResult.draft.title || "не распознано"}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-11 font-medium uppercase text-tertiary">Intent</div>
|
||||
<div className="text-11 font-medium text-tertiary uppercase">Intent</div>
|
||||
<div className="mt-0.5 text-primary">{parseResult.draft.intent}</div>
|
||||
</div>
|
||||
{parseResult.resolution?.target_task && (
|
||||
<div className="sm:col-span-2">
|
||||
<div className="text-11 font-medium text-tertiary uppercase">Целевая задача</div>
|
||||
<div className="mt-0.5 text-primary">
|
||||
{[parseResult.resolution.target_task.key, parseResult.resolution.target_task.title]
|
||||
.filter(Boolean)
|
||||
.join(" · ")}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{parseResult.resolution?.project_change && (
|
||||
<div className="sm:col-span-2">
|
||||
<div className="text-11 font-medium text-tertiary uppercase">Перенос проекта</div>
|
||||
<div className="mt-0.5 text-primary">
|
||||
{parseResult.resolution.project_change.from.name} ->{" "}
|
||||
{parseResult.resolution.project_change.to.name}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<div className="text-11 font-medium uppercase text-tertiary">Проект</div>
|
||||
<div className="text-11 font-medium text-tertiary uppercase">Проект</div>
|
||||
<div className="mt-0.5 text-primary">
|
||||
{parseResult.resolution?.project?.name || parseResult.draft.project_hint || "не распознано"}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-11 font-medium uppercase text-tertiary">Исполнитель</div>
|
||||
<div className="text-11 font-medium text-tertiary uppercase">Статус</div>
|
||||
<div className="mt-0.5 text-primary">
|
||||
{parseResult.resolution?.state?.name || parseResult.draft.state_hint || "не распознано"}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-11 font-medium text-tertiary uppercase">Исполнитель</div>
|
||||
<div className="mt-0.5 text-primary">
|
||||
{parseResult.resolution?.assignee?.name || parseResult.draft.assignee_hint || "не распознано"}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-11 font-medium uppercase text-tertiary">Срок</div>
|
||||
<div className="text-11 font-medium text-tertiary uppercase">Срок</div>
|
||||
<div className="mt-0.5 text-primary">
|
||||
{[parseResult.draft.due_date, parseResult.draft.due_time].filter(Boolean).join(" ") || "не распознано"}
|
||||
{[parseResult.draft.due_date, parseResult.draft.due_time].filter(Boolean).join(" ") ||
|
||||
"не распознано"}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-11 font-medium uppercase text-tertiary">Приоритет</div>
|
||||
<div className="text-11 font-medium text-tertiary uppercase">Приоритет</div>
|
||||
<div className="mt-0.5 text-primary">{parseResult.draft.priority || "не распознано"}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{parseResult.draft.description && (
|
||||
<div>
|
||||
<div className="text-11 font-medium uppercase text-tertiary">Описание</div>
|
||||
<div className="text-11 font-medium text-tertiary uppercase">Описание</div>
|
||||
<p className="mt-1 max-h-20 overflow-y-auto text-12 leading-5 text-secondary">
|
||||
{parseResult.draft.description}
|
||||
</p>
|
||||
|
|
@ -463,10 +531,18 @@ export function VoiceTaskerGlobalControl({ workspaceSlug }: Props) {
|
|||
)}
|
||||
|
||||
<div className="flex flex-wrap gap-1.5 text-11 text-secondary">
|
||||
<span className="rounded bg-layer-1 px-2 py-1">intent {formatConfidence(parseResult.draft.confidence.intent)}</span>
|
||||
<span className="rounded bg-layer-1 px-2 py-1">project {formatConfidence(parseResult.draft.confidence.project)}</span>
|
||||
<span className="rounded bg-layer-1 px-2 py-1">assignee {formatConfidence(parseResult.draft.confidence.assignee)}</span>
|
||||
<span className="rounded bg-layer-1 px-2 py-1">task {formatConfidence(parseResult.draft.confidence.task)}</span>
|
||||
<span className="rounded bg-layer-1 px-2 py-1">
|
||||
intent {formatConfidence(parseResult.draft.confidence.intent)}
|
||||
</span>
|
||||
<span className="rounded bg-layer-1 px-2 py-1">
|
||||
project {formatConfidence(parseResult.draft.confidence.project)}
|
||||
</span>
|
||||
<span className="rounded bg-layer-1 px-2 py-1">
|
||||
assignee {formatConfidence(parseResult.draft.confidence.assignee)}
|
||||
</span>
|
||||
<span className="rounded bg-layer-1 px-2 py-1">
|
||||
task {formatConfidence(parseResult.draft.confidence.task)}
|
||||
</span>
|
||||
{parseResult.resolution?.project && (
|
||||
<span className="rounded bg-layer-1 px-2 py-1">
|
||||
resolved project {formatConfidence(parseResult.resolution.project.confidence)}
|
||||
|
|
@ -474,15 +550,15 @@ export function VoiceTaskerGlobalControl({ workspaceSlug }: Props) {
|
|||
)}
|
||||
</div>
|
||||
|
||||
{Boolean(parseResult.warnings?.length || parseResult.draft.questions.length) && (
|
||||
<div className="rounded border-[0.5px] border-yellow-500/30 bg-yellow-500/10 px-3 py-2 text-11 text-yellow-600">
|
||||
{[...(parseResult.warnings ?? []), ...parseResult.draft.questions].join(" · ")}
|
||||
{Boolean(getVoiceTaskWarnings(parseResult).length) && (
|
||||
<div className="border-yellow-500/30 bg-yellow-500/10 text-yellow-600 rounded border-[0.5px] px-3 py-2 text-11">
|
||||
{getVoiceTaskWarnings(parseResult).join(" · ")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{commitResult?.task_key && (
|
||||
<div className="rounded border-[0.5px] border-green-500/30 bg-green-500/10 px-3 py-2 text-12 text-green-600">
|
||||
Создана задача {commitResult.task_key}
|
||||
<div className="border-green-500/30 bg-green-500/10 text-green-600 rounded border-[0.5px] px-3 py-2 text-12">
|
||||
{getCommitSuccessMessage(commitResult)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -505,26 +581,34 @@ export function VoiceTaskerGlobalControl({ workspaceSlug }: Props) {
|
|||
{isRecording ? <Square className="mr-2 size-4" /> : <Mic className="mr-2 size-4" />}
|
||||
{isRecording ? "Стоп" : "Записать"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
onClick={uploadAudio}
|
||||
loading={isUploading}
|
||||
disabled={!audioBlob || isRecording || isCommitting}
|
||||
>
|
||||
<Upload className="mr-2 size-4" />
|
||||
Отправить
|
||||
</Button>
|
||||
{parseResult?.draft?.intent === "create_task" && !commitResult?.task_id && (
|
||||
{!parseResult?.draft && (
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
onClick={uploadAudio}
|
||||
loading={isUploading}
|
||||
disabled={!audioBlob || isRecording || isCommitting}
|
||||
>
|
||||
<Upload className="mr-2 size-4" />
|
||||
Отправить
|
||||
</Button>
|
||||
)}
|
||||
{parseResult?.draft?.intent && parseResult.draft.intent !== "unknown" && !commitResult?.task_id && (
|
||||
<Button
|
||||
variant={parseResult.draft.intent === "delete_task" ? "error-fill" : "primary"}
|
||||
size="lg"
|
||||
onClick={commitVoiceTask}
|
||||
loading={isCommitting}
|
||||
disabled={!parseResult.voice_session_id || !parseResult.resolution?.can_commit || isUploading}
|
||||
>
|
||||
<Plus className="mr-2 size-4" />
|
||||
Создать задачу
|
||||
{parseResult.draft.intent === "update_task" ? (
|
||||
<Pencil className="mr-2 size-4" />
|
||||
) : parseResult.draft.intent === "delete_task" ? (
|
||||
<Trash2 className="mr-2 size-4" />
|
||||
) : (
|
||||
<Plus className="mr-2 size-4" />
|
||||
)}
|
||||
{getCommitButtonLabel(parseResult.draft.intent)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -29,10 +29,7 @@ export class WorkspaceAIService extends APIService {
|
|||
});
|
||||
}
|
||||
|
||||
async updateSettings(
|
||||
workspaceSlug: string,
|
||||
data: TWorkspaceAISettingsPayload
|
||||
): Promise<TWorkspaceAISettings> {
|
||||
async updateSettings(workspaceSlug: string, data: TWorkspaceAISettingsPayload): Promise<TWorkspaceAISettings> {
|
||||
return this.patch(`/api/workspaces/${workspaceSlug}/voice-tasker/settings/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
|
|
@ -68,7 +65,7 @@ export class WorkspaceAIService extends APIService {
|
|||
workspaceSlug: string,
|
||||
data: {
|
||||
voice_session_id: string;
|
||||
action: "create_task";
|
||||
action: "create_task" | "update_task" | "delete_task";
|
||||
draft?: TVoiceTaskDraft;
|
||||
}
|
||||
): Promise<TVoiceTaskCommitResult> {
|
||||
|
|
|
|||
|
|
@ -26,11 +26,29 @@ http {
|
|||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
|
||||
location = /sw.js {
|
||||
root /usr/share/nginx/html;
|
||||
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location = /index.html {
|
||||
root /usr/share/nginx/html;
|
||||
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location ~* ^/assets/.+\.(js|css|png|jpg|jpeg|gif|svg|webp|ico|woff|woff2)$ {
|
||||
root /usr/share/nginx/html;
|
||||
add_header Cache-Control "public, max-age=31536000, immutable" always;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ export type TVoiceTaskDraft = {
|
|||
target_memory_ref: string | null;
|
||||
project_id?: string | null;
|
||||
project_hint: string | null;
|
||||
state_hint: string | null;
|
||||
assignee_hint: string | null;
|
||||
title: string | null;
|
||||
description: string | null;
|
||||
|
|
@ -102,13 +103,7 @@ export type TVoiceTaskDraft = {
|
|||
};
|
||||
|
||||
export type TVoiceTaskResolution = {
|
||||
project: {
|
||||
id: string;
|
||||
name: string;
|
||||
identifier: string;
|
||||
confidence: number;
|
||||
source: string | null;
|
||||
} | null;
|
||||
project: TVoiceTaskResolvedProject | null;
|
||||
assignee: {
|
||||
id: string;
|
||||
name: string;
|
||||
|
|
@ -120,10 +115,40 @@ export type TVoiceTaskResolution = {
|
|||
id: string;
|
||||
name: string;
|
||||
}[];
|
||||
state: {
|
||||
id: string;
|
||||
name: string;
|
||||
group: string;
|
||||
confidence: number;
|
||||
source: string | null;
|
||||
} | null;
|
||||
target_task: {
|
||||
id: string;
|
||||
title: string;
|
||||
key: string | null;
|
||||
project_id: string;
|
||||
project_name: string;
|
||||
project_identifier: string;
|
||||
sequence_id: number;
|
||||
source: string | null;
|
||||
voice_session_id: string | null;
|
||||
} | null;
|
||||
project_change: {
|
||||
from: TVoiceTaskResolvedProject;
|
||||
to: TVoiceTaskResolvedProject;
|
||||
} | null;
|
||||
warnings: string[];
|
||||
can_commit: boolean;
|
||||
};
|
||||
|
||||
export type TVoiceTaskResolvedProject = {
|
||||
id: string;
|
||||
name: string;
|
||||
identifier: string;
|
||||
confidence: number;
|
||||
source: string | null;
|
||||
};
|
||||
|
||||
export type TVoiceTaskUploadResult = {
|
||||
ok: boolean;
|
||||
status?: "uploaded" | "parsed";
|
||||
|
|
@ -151,7 +176,7 @@ export type TVoiceTaskUploadResult = {
|
|||
|
||||
export type TVoiceTaskCommitResult = {
|
||||
ok: boolean;
|
||||
status?: "created";
|
||||
status?: "created" | "updated" | "deleted";
|
||||
voice_session_id?: string;
|
||||
task_id?: string;
|
||||
task_key?: string;
|
||||
|
|
|
|||
|
|
@ -78,6 +78,8 @@ export type TBaseIssue = {
|
|||
is_draft: boolean;
|
||||
is_epic?: boolean;
|
||||
is_intake?: boolean;
|
||||
external_source?: string | null;
|
||||
external_id?: string | null;
|
||||
};
|
||||
|
||||
type IssueRelation = {
|
||||
|
|
|
|||
Loading…
Reference in New Issue