From 323b4b964edec3e270480cd29919f0b6d4968010 Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Sun, 26 Apr 2026 16:14:49 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A4=D0=A3=D0=9D=D0=9A=D0=A6=D0=98=D0=98=20-?= =?UTF-8?q?=20=D0=9C=D0=95=D0=96=D0=9F=D0=A0=D0=9E=D0=95=D0=9A=D0=A2=D0=9D?= =?UTF-8?q?=D0=90=D0=AF=20=D0=9A=D0=9E=D0=9C=D0=9C=D0=A3=D0=9D=D0=98=D0=9A?= =?UTF-8?q?=D0=90=D0=A6=D0=98=D0=AF:=20=D0=B8=D0=BD=D1=82=D0=B5=D1=80?= =?UTF-8?q?=D0=B0=D0=BA=D1=82=D0=B8=D0=B2=D0=BD=D1=8B=D0=B9=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B5=D0=B4=D0=BF=D1=80=D0=BE=D1=81=D0=BC=D0=BE=D1=82=D1=80=20?= =?UTF-8?q?Voice=20Tasker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../apps/api/plane/app/views/voice_tasker.py | 90 +- .../components/dropdowns/project/base.tsx | 18 +- .../voice-tasker/global-control.tsx | 899 ++++++++++++++---- plane-src/packages/types/src/ai.ts | 3 + 4 files changed, 801 insertions(+), 209 deletions(-) diff --git a/plane-src/apps/api/plane/app/views/voice_tasker.py b/plane-src/apps/api/plane/app/views/voice_tasker.py index 2e02280..893da9b 100644 --- a/plane-src/apps/api/plane/app/views/voice_tasker.py +++ b/plane-src/apps/api/plane/app/views/voice_tasker.py @@ -691,6 +691,8 @@ def harden_voice_task_intent(parsed, transcript): if parsed.get("intent") != "update_task": return parsed + if parsed.get("target_task_id"): + return parsed target_memory_ref = normalize_string(parsed.get("target_memory_ref"), 80) has_explicit_issue_ref = bool(parse_issue_key_reference(target_memory_ref)) or transcript_has_issue_key_reference( @@ -716,6 +718,9 @@ def harden_voice_task_intent(parsed, transcript): def voice_task_has_safe_existing_task_anchor(draft, transcript): + if draft.get("target_task_id"): + return True + target_memory_ref = normalize_string(draft.get("target_memory_ref"), 80) return bool( parse_issue_key_reference(target_memory_ref) @@ -971,6 +976,26 @@ def resolve_voice_task_assignee(project, draft): return serialize_resolved_assignee(best_member, best_score, "assignee_hint") +def resolve_explicit_voice_task_assignees(project, draft): + if "assignee_ids" not in draft: + return None + + assignee_ids = draft.get("assignee_ids") if isinstance(draft.get("assignee_ids"), list) else [] + if not assignee_ids: + return [] + + member_by_id = { + str(project_member.member_id): project_member.member + for project_member in get_voice_task_project_assignable_members(project) + } + + return [ + serialize_resolved_assignee(member_by_id[assignee_id], 1.0, "explicit_assignee_ids") + for assignee_id in assignee_ids + if assignee_id in member_by_id + ] + + def transcript_requests_all_project_assignees(transcript, draft=None): normalized = normalize_match_value(transcript) assignee_hint = normalize_match_value((draft or {}).get("assignee_hint")) @@ -988,6 +1013,10 @@ def transcript_requests_all_project_assignees(transcript, draft=None): def resolve_voice_task_assignees(project, draft, transcript=None): + explicit_assignees = resolve_explicit_voice_task_assignees(project, draft) + if explicit_assignees is not None: + return explicit_assignees + if transcript_requests_all_project_assignees(transcript, draft): return [ serialize_resolved_assignee(project_member.member, 1.0, "all_project_members") @@ -1081,8 +1110,14 @@ def resolve_voice_task_state(project, draft, allow_default=True): if not project: return None - state_hint = draft.get("state_hint") states = list(State.objects.filter(project=project).order_by("sequence")) + explicit_state_id = normalize_uuid_string(draft.get("state_id")) + if explicit_state_id: + explicit_state = next((state for state in states if str(state.id) == explicit_state_id), None) + if explicit_state: + return serialize_resolved_state(explicit_state, 1.0, "explicit_state_id") + + state_hint = draft.get("state_hint") if state_hint: best_state = None best_score = 0.0 @@ -1452,9 +1487,22 @@ def find_latest_voice_task_issue(memory_sessions, project_id=None): def resolve_voice_task_memory_target(workspace, user, draft, current_session=None, client_context=None, transcript=None): target_memory_ref = normalize_string(draft.get("target_memory_ref"), 80) + explicit_target_task_id = normalize_uuid_string(draft.get("target_task_id")) memory_sessions = get_committed_voice_task_memory_sessions(workspace, user, current_session=current_session) generic_memory_reference = transcript_has_generic_memory_reference(transcript) + if explicit_target_task_id: + target_issue = ( + Issue.issue_objects.filter(workspace=workspace, id=explicit_target_task_id) + .select_related("project") + .first() + ) + if ( + is_voice_task_issue_available(target_issue) + and get_accessible_projects(workspace, user).filter(id=target_issue.project_id).exists() + ): + return target_issue, "explicit_target_task_id", None + if target_memory_ref: target_uuid = None try: @@ -1523,6 +1571,8 @@ def voice_task_has_update_fields(draft, resolution): or draft.get("due_time") or (draft.get("priority") and draft.get("priority") != "none") or draft.get("checklist") + or "assignee_ids" in draft + or draft.get("state_id") or (resolution.get("assignee") and resolution["assignee"]["confidence"] >= VOICE_TASK_ASSIGNEE_MATCH_THRESHOLD) or (draft.get("state_hint") and resolution.get("state") and resolution["state"]["confidence"] >= VOICE_TASK_STATE_MATCH_THRESHOLD) or resolution.get("labels") @@ -1846,7 +1896,13 @@ def build_voice_task_issue_update_payload(issue, draft, resolution, transcript=N if draft.get("priority") and draft["priority"] != "none": payload["priority"] = draft["priority"] - if assignees: + if "assignee_ids" in draft: + payload["assignee_ids"] = [ + assignee["id"] + for assignee in assignees + if assignee and assignee["confidence"] >= VOICE_TASK_ASSIGNEE_MATCH_THRESHOLD + ] + elif assignees: payload["assignee_ids"] = [ assignee["id"] for assignee in assignees @@ -1855,7 +1911,7 @@ def build_voice_task_issue_update_payload(issue, draft, resolution, transcript=N elif assignee and assignee["confidence"] >= VOICE_TASK_ASSIGNEE_MATCH_THRESHOLD: payload["assignee_ids"] = [assignee["id"]] - if (draft.get("state_hint") or project_change) and state and state["confidence"] >= VOICE_TASK_STATE_MATCH_THRESHOLD: + if (draft.get("state_hint") or draft.get("state_id") or project_change) and state and state["confidence"] >= VOICE_TASK_STATE_MATCH_THRESHOLD: payload["state_id"] = state["id"] if labels: @@ -2043,6 +2099,29 @@ def normalize_string(value, max_length=None): return normalized[:max_length] if max_length else normalized +def normalize_uuid_string(value): + normalized = normalize_string(value, 80) + if not normalized: + return None + + try: + return str(uuid.UUID(normalized)) + except (TypeError, ValueError): + return None + + +def normalize_uuid_list(value, limit=50): + if not isinstance(value, list): + return [] + + result = [] + for item in value[:limit]: + normalized = normalize_uuid_string(item) + if normalized and normalized not in result: + result.append(normalized) + return result + + def normalize_string_list(value, limit=20, item_max_length=120): if not isinstance(value, list): return [] @@ -2097,6 +2176,9 @@ def normalize_voice_task_parse(parsed): normalized = { "intent": intent, "target_memory_ref": normalize_string(parsed.get("target_memory_ref"), 80), + "project_id": normalize_uuid_string(parsed.get("project_id")), + "state_id": normalize_uuid_string(parsed.get("state_id")), + "target_task_id": normalize_uuid_string(parsed.get("target_task_id") or parsed.get("target_issue_id")), "project_hint": normalize_string(parsed.get("project_hint"), 255), "state_hint": normalize_string(parsed.get("state_hint"), 120), "assignee_hint": normalize_string(parsed.get("assignee_hint"), 255), @@ -2115,6 +2197,8 @@ def normalize_voice_task_parse(parsed): }, "questions": normalize_string_list(parsed.get("questions"), limit=10, item_max_length=255), } + if "assignee_ids" in parsed: + normalized["assignee_ids"] = normalize_uuid_list(parsed.get("assignee_ids")) return normalized diff --git a/plane-src/apps/web/core/components/dropdowns/project/base.tsx b/plane-src/apps/web/core/components/dropdowns/project/base.tsx index 7c44c29..cea0245 100644 --- a/plane-src/apps/web/core/components/dropdowns/project/base.tsx +++ b/plane-src/apps/web/core/components/dropdowns/project/base.tsx @@ -238,19 +238,19 @@ export const ProjectDropdownBase = observer(function ProjectDropdownBase(props: multiple={multiple} > {isOpen && ( - +
-
+
setQuery(e.target.value)} placeholder={t("search")} @@ -268,9 +268,9 @@ export const ProjectDropdownBase = observer(function ProjectDropdownBase(props: key={option.value} value={option.value} className={({ active, selected }) => - `flex w-full cursor-pointer items-center justify-between gap-2 truncate rounded-sm px-1 py-1.5 select-none ${ - active ? "bg-layer-transparent-hover" : "" - } ${selected ? "text-primary" : "text-secondary"}` + `nodedc-dropdown-option cursor-pointer ${active ? "bg-white/[0.06]" : ""} ${ + selected ? "text-primary" : "text-secondary" + }` } > {({ selected }) => ( @@ -283,10 +283,10 @@ export const ProjectDropdownBase = observer(function ProjectDropdownBase(props: ); }) ) : ( -

{t("no_matching_results")}

+

{t("no_matching_results")}

) ) : ( -

{t("loading")}

+

{t("loading")}

)}
diff --git a/plane-src/apps/web/core/components/voice-tasker/global-control.tsx b/plane-src/apps/web/core/components/voice-tasker/global-control.tsx index 2cba0d1..044b0ce 100644 --- a/plane-src/apps/web/core/components/voice-tasker/global-control.tsx +++ b/plane-src/apps/web/core/components/voice-tasker/global-control.tsx @@ -5,22 +5,57 @@ */ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import type { ElementType, ReactNode } from "react"; import { useParams } from "next/navigation"; import useSWR from "swr"; -import { CheckCircle2, Mic, Pencil, Plus, RotateCcw, Square, Trash2, Upload, X } from "lucide-react"; +import { + CalendarDays, + CheckCircle2, + Clock3, + FileText, + Flag, + FolderKanban, + ListChecks, + Mic, + Pencil, + RotateCcw, + Search, + Square, + Target, + Trash2, + Upload, + UserRound, + X, +} from "lucide-react"; // plane imports import { Button } from "@plane/propel/button"; import { Tooltip } from "@plane/propel/tooltip"; import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import { EIssuesStoreType } from "@plane/types"; -import type { TVoiceTaskCommitResult, TVoiceTaskUploadResult } from "@plane/types"; +import type { + ISearchIssueResponse, + TIssuePriorities, + TVoiceTaskCommitResult, + TVoiceTaskDraft, + TVoiceTaskPriority, + TVoiceTaskUploadResult, +} from "@plane/types"; import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui"; -import { cn } from "@plane/utils"; +import { cn, renderFormattedPayloadDate } from "@plane/utils"; +import { DateDropdown } from "@/components/dropdowns/date"; +import { MemberDropdown } from "@/components/dropdowns/member/dropdown"; +import { PriorityDropdown } from "@/components/dropdowns/priority"; +import { ProjectDropdown } from "@/components/dropdowns/project/dropdown"; +import { StateDropdown } from "@/components/dropdowns/state/dropdown"; +import { IssueIdentifier } from "@/plane-web/components/issues/issue-details/issue-identifier"; import { useIssues } from "@/hooks/store/use-issues"; +import useDebounce from "@/hooks/use-debounce"; // services +import { ProjectService } from "@/services/project"; import { WorkspaceAIService } from "@/services/workspace-ai.service"; const workspaceAIService = new WorkspaceAIService(); +const projectService = new ProjectService(); type TVoiceTaskerStatus = "idle" | "recording" | "uploading" | "success" | "committing" | "committed" | "error"; @@ -31,6 +66,20 @@ const UNAVAILABLE_LABELS = { role_denied: "Voice Task недоступен для вашей роли", } as const; +const PRIORITY_LABELS: Record, string> = { + none: "Без приоритета", + low: "Низкий", + medium: "Средний", + high: "Высокий", + urgent: "Срочный", +}; + +const voiceTaskPropertyButtonClassName = + "nodedc-work-item-property-button !h-8 !min-h-8 !w-full !justify-start !px-3 text-12"; + +const VOICE_TASK_TIME_HOURS = Array.from({ length: 24 }, (_, index) => index.toString().padStart(2, "0")); +const VOICE_TASK_TIME_MINUTES = Array.from({ length: 60 }, (_, index) => index.toString().padStart(2, "0")); + function getSupportedMimeType() { if (typeof MediaRecorder === "undefined") return ""; @@ -62,19 +111,18 @@ function getRouteParam(value: string | string[] | undefined) { } function getCommitButtonLabel(intent?: string) { - if (intent === "update_task") return "Применить изменения"; - if (intent === "delete_task") return "Удалить задачу"; - 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"; + if (status === "committed") return "Опубликовано"; + if (status === "success") return "Черновик готов"; + if (status === "committing") return "Публикуем"; + if (status === "uploading") return "Распознаем"; + if (status === "recording") return "Идет запись"; + if (status === "error") return "Ошибка"; + return "Готово к записи"; } function getCommitSuccessTitle(result: TVoiceTaskCommitResult) { @@ -115,6 +163,304 @@ function getVoiceTaskAssigneesLabel(result: TVoiceTaskUploadResult) { return result.draft?.assignee_hint || "не распознано"; } +function getVoiceTaskAssigneeIds(result: TVoiceTaskUploadResult | null) { + const resolution = result?.resolution as TVoiceTaskResolutionWithAssignees | undefined; + const resolvedAssignees = resolution?.assignees?.length + ? resolution.assignees + : resolution?.assignee + ? [resolution.assignee] + : []; + + return resolvedAssignees.flatMap((assignee) => (assignee?.id ? [assignee.id] : [])); +} + +function getPriorityLabel(priority: TVoiceTaskPriority) { + return priority ? PRIORITY_LABELS[priority] : "Не распознано"; +} + +function parseVoiceTaskTime(value?: string | null) { + const match = value?.match(/^(\d{2}):(\d{2})/); + if (!match) return { hour: null, minute: null }; + + const hour = VOICE_TASK_TIME_HOURS.includes(match[1]) ? match[1] : null; + const minute = VOICE_TASK_TIME_MINUTES.includes(match[2]) ? match[2] : null; + + return { hour, minute }; +} + +type TVoiceTaskTargetOption = { + id: string; + title: string; + projectId: string; + projectIdentifier: string; + sequenceId: number; + typeId?: string | null; +}; + +function getTargetOptionFromResolution(result: TVoiceTaskUploadResult | null): TVoiceTaskTargetOption | null { + const target = result?.resolution?.target_task; + if (!target) return null; + + return { + id: target.id, + title: target.title, + projectId: target.project_id, + projectIdentifier: target.project_identifier, + sequenceId: target.sequence_id, + }; +} + +function getTargetOptionFromSearchIssue(issue: ISearchIssueResponse): TVoiceTaskTargetOption { + return { + id: issue.id, + title: issue.name, + projectId: issue.project_id, + projectIdentifier: issue.project__identifier, + sequenceId: issue.sequence_id, + typeId: issue.type_id, + }; +} + +function VoiceTaskPropertyBlock({ + children, + className, + icon: Icon, + label, +}: { + children: ReactNode; + className?: string; + icon: ElementType; + label: string; +}) { + return ( +
+
+ + {label} +
+ {children} +
+ ); +} + +function VoiceTaskTimePicker({ onChange, value }: { onChange: (value: string | null) => void; value?: string | null }) { + const [isOpen, setIsOpen] = useState(false); + const rootRef = useRef(null); + const selectedTime = parseVoiceTaskTime(value); + const selectedHour = selectedTime.hour ?? "00"; + const selectedMinute = selectedTime.minute ?? "00"; + + useEffect(() => { + if (!isOpen) return; + + const handlePointerDown = (event: PointerEvent) => { + if (!rootRef.current?.contains(event.target as Node)) setIsOpen(false); + }; + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") setIsOpen(false); + }; + + document.addEventListener("pointerdown", handlePointerDown); + document.addEventListener("keydown", handleKeyDown); + + return () => { + document.removeEventListener("pointerdown", handlePointerDown); + document.removeEventListener("keydown", handleKeyDown); + }; + }, [isOpen]); + + const updateTime = (hour: string, minute: string) => onChange(`${hour}:${minute}`); + + return ( +
+ + + {isOpen && ( +
+
+
+ {VOICE_TASK_TIME_HOURS.map((hour) => ( + + ))} +
+
+ {VOICE_TASK_TIME_MINUTES.map((minute) => ( + + ))} +
+
+
+ )} +
+ ); +} + +function VoiceTaskTargetPicker({ + disabled = false, + onChange, + projectId, + selectedIssue, + workspaceSlug, +}: { + disabled?: boolean; + onChange: (issue: TVoiceTaskTargetOption) => void; + projectId: string | null; + selectedIssue: TVoiceTaskTargetOption | null; + workspaceSlug: string; +}) { + const [isOpen, setIsOpen] = useState(false); + const [searchTerm, setSearchTerm] = useState(""); + const [issues, setIssues] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const debouncedSearchTerm = useDebounce(searchTerm, 350); + + useEffect(() => { + if (!isOpen || !projectId) return; + + let isMounted = true; + setIsLoading(true); + projectService + .projectIssuesSearch(workspaceSlug, projectId, { + search: debouncedSearchTerm, + workspace_search: false, + }) + .then((response) => { + if (isMounted) setIssues(response); + }) + .catch(() => { + if (isMounted) setIssues([]); + }) + .finally(() => { + if (isMounted) setIsLoading(false); + }); + + return () => { + isMounted = false; + }; + }, [debouncedSearchTerm, isOpen, projectId, workspaceSlug]); + + return ( +
+ + + {isOpen && projectId && ( +
+
+ + setSearchTerm(event.target.value)} + placeholder="Найти задачу" + /> +
+
+ {isLoading ? ( +
Загрузка задач...
+ ) : issues.length ? ( + issues.map((issue) => ( + + )) + ) : ( +
Задачи не найдены
+ )} +
+
+ )} +
+ ); +} + type Props = { workspaceSlug: string; }; @@ -142,6 +488,7 @@ export function VoiceTaskerGlobalControl({ workspaceSlug }: Props) { const [error, setError] = useState(null); const [parseResult, setParseResult] = useState(null); const [commitResult, setCommitResult] = useState(null); + const [selectedTargetIssue, setSelectedTargetIssue] = useState(null); const mediaRecorderRef = useRef(null); const streamRef = useRef(null); @@ -199,6 +546,7 @@ export function VoiceTaskerGlobalControl({ workspaceSlug }: Props) { setError(null); setParseResult(null); setCommitResult(null); + setSelectedTargetIssue(null); setStatus("idle"); }, [stopRecording]); @@ -308,6 +656,7 @@ export function VoiceTaskerGlobalControl({ workspaceSlug }: Props) { try { const result = await workspaceAIService.uploadVoiceTaskAudio(workspaceSlug, formData); setParseResult(result); + setSelectedTargetIssue(getTargetOptionFromResolution(result)); setStatus("success"); setToast({ type: TOAST_TYPE.SUCCESS, @@ -396,6 +745,40 @@ export function VoiceTaskerGlobalControl({ workspaceSlug }: Props) { } }; + const updateDraft = useCallback((patch: Partial) => { + setParseResult((current) => { + if (!current?.draft) return current; + return { + ...current, + draft: { + ...current.draft, + ...patch, + }, + }; + }); + setCommitResult(null); + }, []); + + const draft = parseResult?.draft; + const selectedProjectId = draft?.project_id ?? parseResult?.resolution?.project?.id ?? null; + const selectedStateId = draft?.state_id ?? parseResult?.resolution?.state?.id ?? null; + const selectedAssigneeIds = draft?.assignee_ids ?? getVoiceTaskAssigneeIds(parseResult); + const selectedTargetTask = selectedTargetIssue ?? getTargetOptionFromResolution(parseResult); + const selectedTargetTaskId = draft?.target_task_id ?? selectedTargetTask?.id ?? null; + const selectedPriority = (draft?.priority ?? "none") as TIssuePriorities; + const warnings = parseResult ? getVoiceTaskWarnings(parseResult) : []; + const canPublishDraft = Boolean( + parseResult?.voice_session_id && + draft && + draft.intent !== "unknown" && + !isUploading && + !isCommitting && + !commitResult?.task_id && + (draft.intent === "create_task" + ? selectedProjectId && draft.title?.trim() + : selectedTargetTaskId && (draft.intent === "delete_task" || selectedProjectId)) + ); + return ( <>
@@ -416,214 +799,336 @@ export function VoiceTaskerGlobalControl({ workspaceSlug }: Props) {
- -
-
-
-

Voice Task

-

Запись до {maxDuration} секунд

-
- -
- -
-
-
-
{formatDuration(duration)}
-
{getCommitStatusLabel(status)}
+ +
+ {!draft ? ( + <> +
+
+

Voice Tasker

+

Запись до {maxDuration} секунд

+
+
-
- -
-
- {audioUrl && !isRecording && ( - - )} - - {error && ( -
- {error} -
- )} - - {parseResult?.draft && ( -
-
- - Draft готов +
+
+
+
{formatDuration(duration)}
+
{getCommitStatusLabel(status)}
+
+
+ +
- {parseResult.transcript && ( -
-
Транскрипт
-

- {parseResult.transcript} -

-
+ {audioUrl && !isRecording && ( + )} -
-
-
Название
-
{parseResult.draft.title || "не распознано"}
+ {error && ( +
+ {error}
-
-
Intent
-
{parseResult.draft.intent}
-
- {parseResult.resolution?.target_task && ( -
-
Целевая задача
-
- {[parseResult.resolution.target_task.key, parseResult.resolution.target_task.title] - .filter(Boolean) - .join(" · ")} + )} +
+ +
+ {audioBlob && !isRecording && ( + + )} + + +
+ + ) : ( + <> +
+
+

Предпросмотр задачи

+
+ +
+ +
+
+
Исходные данные
+ +
+
+
+
{formatDuration(duration)}
+
{getCommitStatusLabel(status)}
+
+
+
- )} - {parseResult.resolution?.project_change && ( -
-
Перенос проекта
-
- {parseResult.resolution.project_change.from.name} ->{" "} - {parseResult.resolution.project_change.to.name} + {audioUrl && !isRecording && ( + + )} +
+ + {parseResult?.transcript && ( +
+
+ + Транскрипт
+

+ {parseResult.transcript} +

)} -
-
Проект
-
- {parseResult.resolution?.project?.name || parseResult.draft.project_hint || "не распознано"} + +
+
+ + Диагностика
-
-
-
Статус
-
- {parseResult.resolution?.state?.name || parseResult.draft.state_hint || "не распознано"} +
+ + intent {formatConfidence(draft.confidence.intent)} + + + project {formatConfidence(draft.confidence.project)} + + + assignee {formatConfidence(draft.confidence.assignee)} + + + task {formatConfidence(draft.confidence.task)} +
-
-
Исполнитель
-
{getVoiceTaskAssigneesLabel(parseResult)}
-
-
-
Срок
-
- {[parseResult.draft.due_date, parseResult.draft.due_time].filter(Boolean).join(" ") || - "не распознано"} -
-
-
-
Приоритет
-
{parseResult.draft.priority || "не распознано"}
-
- {parseResult.draft.description && ( -
-
Описание
-

- {parseResult.draft.description} -

+
+
+ Сформированная карточка
- )} -
- - intent {formatConfidence(parseResult.draft.confidence.intent)} - - - project {formatConfidence(parseResult.draft.confidence.project)} - - - assignee {formatConfidence(parseResult.draft.confidence.assignee)} - - - task {formatConfidence(parseResult.draft.confidence.task)} - - {parseResult.resolution?.project && ( - - resolved project {formatConfidence(parseResult.resolution.project.confidence)} - +
+ + updateDraft({ title: event.target.value || null })} + placeholder="Название задачи" + /> + +