ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: OpenAI pipeline и voice sessions
This commit is contained in:
@@ -6,11 +6,12 @@
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import { Mic, RotateCcw, Square, Upload, X } from "lucide-react";
|
||||
import { CheckCircle2, Mic, RotateCcw, Square, Upload, 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 type { TVoiceTaskUploadResult } from "@plane/types";
|
||||
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
// services
|
||||
@@ -41,6 +42,11 @@ function formatDuration(seconds: number) {
|
||||
return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
function formatConfidence(value?: number) {
|
||||
if (typeof value !== "number") return "0%";
|
||||
return `${Math.round(Math.max(0, Math.min(1, value)) * 100)}%`;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
};
|
||||
@@ -52,6 +58,7 @@ export function VoiceTaskerGlobalControl({ workspaceSlug }: Props) {
|
||||
const [audioBlob, setAudioBlob] = useState<Blob | null>(null);
|
||||
const [audioUrl, setAudioUrl] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [parseResult, setParseResult] = useState<TVoiceTaskUploadResult | null>(null);
|
||||
|
||||
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
|
||||
const streamRef = useRef<MediaStream | null>(null);
|
||||
@@ -106,6 +113,7 @@ export function VoiceTaskerGlobalControl({ workspaceSlug }: Props) {
|
||||
setAudioUrl(null);
|
||||
setDuration(0);
|
||||
setError(null);
|
||||
setParseResult(null);
|
||||
setStatus("idle");
|
||||
}, [stopRecording]);
|
||||
|
||||
@@ -190,6 +198,7 @@ export function VoiceTaskerGlobalControl({ workspaceSlug }: Props) {
|
||||
|
||||
setStatus("uploading");
|
||||
setError(null);
|
||||
setParseResult(null);
|
||||
|
||||
const audioType = audioBlob.type || "audio/webm";
|
||||
const extension = audioType.includes("mp4") ? "m4a" : "webm";
|
||||
@@ -206,12 +215,13 @@ export function VoiceTaskerGlobalControl({ workspaceSlug }: Props) {
|
||||
);
|
||||
|
||||
try {
|
||||
await workspaceAIService.uploadVoiceTaskAudio(workspaceSlug, formData);
|
||||
const result = await workspaceAIService.uploadVoiceTaskAudio(workspaceSlug, formData);
|
||||
setParseResult(result);
|
||||
setStatus("success");
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Аудио отправлено",
|
||||
message: "Backend принял запись. Распознавание будет подключено следующим этапом.",
|
||||
title: "Черновик готов",
|
||||
message: "Transcript и draft получены.",
|
||||
});
|
||||
} catch (err) {
|
||||
const message = typeof err === "object" && err && "error" in err ? String(err.error) : "Не удалось отправить аудио.";
|
||||
@@ -266,7 +276,7 @@ export function VoiceTaskerGlobalControl({ workspaceSlug }: Props) {
|
||||
<div>
|
||||
<div className="text-24 font-semibold text-primary">{formatDuration(duration)}</div>
|
||||
<div className="mt-1 text-12 text-tertiary">
|
||||
{status === "success" ? "Audio uploaded" : isRecording ? "Recording" : "Ready"}
|
||||
{status === "success" ? "Draft parsed" : isUploading ? "Processing" : isRecording ? "Recording" : "Ready"}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -290,6 +300,75 @@ export function VoiceTaskerGlobalControl({ workspaceSlug }: Props) {
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{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" />
|
||||
Draft готов
|
||||
</div>
|
||||
|
||||
{parseResult.transcript && (
|
||||
<div>
|
||||
<div className="text-11 font-medium uppercase text-tertiary">Транскрипт</div>
|
||||
<p className="mt-1 max-h-20 overflow-y-auto text-12 leading-5 text-secondary">
|
||||
{parseResult.transcript}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<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="mt-0.5 text-primary">{parseResult.draft.title || "не распознано"}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-11 font-medium uppercase text-tertiary">Intent</div>
|
||||
<div className="mt-0.5 text-primary">{parseResult.draft.intent}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-11 font-medium uppercase text-tertiary">Проект</div>
|
||||
<div className="mt-0.5 text-primary">{parseResult.draft.project_hint || "не распознано"}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-11 font-medium uppercase text-tertiary">Исполнитель</div>
|
||||
<div className="mt-0.5 text-primary">{parseResult.draft.assignee_hint || "не распознано"}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-11 font-medium uppercase text-tertiary">Срок</div>
|
||||
<div className="mt-0.5 text-primary">
|
||||
{[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="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>
|
||||
<p className="mt-1 max-h-20 overflow-y-auto text-12 leading-5 text-secondary">
|
||||
{parseResult.draft.description}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<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>
|
||||
</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(" · ")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-5 flex flex-wrap justify-end gap-2">
|
||||
|
||||
Reference in New Issue
Block a user