feat(ivr): editor visual do menu (canvas de nós, sem Node-RED)
Pedido do usuário: "ajusta o IVR para fazer o fluxo de maneira visual usando nodeRED". Perguntei antes de construir: integrar Node-RED de verdade significa rodar uma plataforma externa completa (motor de execução próprio, nós "function" = execução de código arbitrário — o mesmo tipo de risco corrigido no dialplan nesta sessão — e sem multi-tenancy nativa), um projeto de vários dias com decisões de arquitetura antes de começar a construir. O usuário escolheu a alternativa: um editor visual próprio, sem dependência externa. `@xyflow/react` (sucessor mantido do reactflow) — canvas de nós/setas dentro da própria tela "Telefonia > IVR": 1 nó "Entrada" fixo conectado a 1 nó por opção (dígito + select de ramal + descrição, editável direto no nó, arrastável pro canvas). "Adicionar opção"/"Salvar alterações" chamam o mesmo PATCH /ivr-menus/:id que já existia — nenhuma mudança de backend necessária, só uma forma nova de editar o mesmo dado (o modelo IvrMenu/IvrMenuOption continua sendo a fonte da verdade). Testado ponta a ponta pela tela de verdade, não só leitura de código: criado um menu real, enviado um prompt de VOZ real (WAV sintetizado com espeak-ng dizendo uma saudação de verdade, não um tom sintético como nos testes anteriores desta fase) e completada uma chamada real — a saudação de ~6s tocou até o fim, o dígito foi capturado, o ramal certo atendeu com áudio de verdade. Depois, uma segunda opção foi adicionada via PATCH (a mesma chamada que o botão "Salvar" do editor visual faz) — confirmado no banco que a versão 2 do dialplan compilou as duas opções corretamente, superando a versão 1. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
This commit is contained in:
27
TODO.md
27
TODO.md
@@ -2176,6 +2176,33 @@ tela de IVR no frontend")
|
|||||||
confirma que áudio de qualquer sample rate/formato WAV comum
|
confirma que áudio de qualquer sample rate/formato WAV comum
|
||||||
funciona sem transcodificação manual
|
funciona sem transcodificação manual
|
||||||
|
|
||||||
|
## PHASE 60 — Editor visual do IVR (pedido do usuário: "ajusta o IVR
|
||||||
|
para fazer o fluxo de maneira visual usando nodeRED")
|
||||||
|
- [x] Node-RED de verdade avaliado e descartado por decisão do usuário
|
||||||
|
(pergunta explícita: integrar de verdade significa uma plataforma
|
||||||
|
externa completa — motor de execução próprio, nós "function" =
|
||||||
|
RCE, sem multi-tenancy nativa — vários dias de trabalho e
|
||||||
|
superfície de risco nova, não um ajuste na tela atual). Escolhido
|
||||||
|
em vez disso um editor visual PRÓPRIO, sobre o mesmo modelo já
|
||||||
|
existente, sem dependência de execução externa
|
||||||
|
- [x] `@xyflow/react` — canvas de nós/setas dentro da própria tela
|
||||||
|
"Telefonia > IVR": 1 nó "Entrada" fixo conectado a 1 nó por opção
|
||||||
|
(dígito + select de ramal + descrição, editável direto no nó,
|
||||||
|
arrastável). "Adicionar opção"/"Salvar alterações" chamam o mesmo
|
||||||
|
`PATCH /ivr-menus/:id` que já existia — nenhuma mudança de backend
|
||||||
|
necessária, só uma forma nova de editar o mesmo dado
|
||||||
|
- [x] Testado ponta a ponta pela tela de verdade: menu criado, prompt de
|
||||||
|
VOZ real enviado (WAV sintetizado com `espeak-ng` dizendo "Bem-
|
||||||
|
vindo a Acme Call Center...", não um tom sintético como nos testes
|
||||||
|
anteriores) e uma chamada real completa — a saudação de ~6s tocou
|
||||||
|
até o fim, o dígito foi capturado, e o ramal certo atendeu com
|
||||||
|
áudio de verdade. Depois, uma segunda opção foi adicionada via
|
||||||
|
`PATCH` (a mesma chamada que o botão "Salvar" do editor visual
|
||||||
|
faz) — confirmado no banco que a versão 2 do dialplan compilou as
|
||||||
|
duas opções corretamente, superando a versão 1
|
||||||
|
- [ ] Layout do canvas não persiste posição manual dos nós entre
|
||||||
|
recargas (recalculado a cada carga da página)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Riscos conhecidos
|
## Riscos conhecidos
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"@radix-ui/react-tooltip": "1.1.6",
|
"@radix-ui/react-tooltip": "1.1.6",
|
||||||
"@tanstack/react-query": "5.62.7",
|
"@tanstack/react-query": "5.62.7",
|
||||||
"@tanstack/react-table": "8.20.5",
|
"@tanstack/react-table": "8.20.5",
|
||||||
|
"@xyflow/react": "^12.11.5",
|
||||||
"class-variance-authority": "0.7.1",
|
"class-variance-authority": "0.7.1",
|
||||||
"clsx": "2.1.1",
|
"clsx": "2.1.1",
|
||||||
"lucide-react": "0.469.0",
|
"lucide-react": "0.469.0",
|
||||||
|
|||||||
@@ -43,6 +43,23 @@ export async function createIvrMenu(input: CreateIvrMenuInput): Promise<{ ok: tr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateIvrMenuOptions(
|
||||||
|
id: string,
|
||||||
|
options: IvrMenuOptionInput[],
|
||||||
|
): Promise<{ ok: true; menu: IvrMenu } | { ok: false; error: string }> {
|
||||||
|
const session = await requireSession();
|
||||||
|
try {
|
||||||
|
const menu = await apiFetch<IvrMenu>(`/ivr-menus/${id}`, session.accessToken, {
|
||||||
|
method: "PATCH",
|
||||||
|
body: JSON.stringify({ options }),
|
||||||
|
});
|
||||||
|
revalidatePath("/app/telefonia/ivr");
|
||||||
|
return { ok: true, menu };
|
||||||
|
} catch (err) {
|
||||||
|
return { ok: false, error: extractErrorMessage(err) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function deleteIvrMenu(id: string): Promise<{ ok: true } | { ok: false; error: string }> {
|
export async function deleteIvrMenu(id: string): Promise<{ ok: true } | { ok: false; error: string }> {
|
||||||
const session = await requireSession();
|
const session = await requireSession();
|
||||||
try {
|
try {
|
||||||
|
|||||||
264
apps/frontend/src/app/app/telefonia/ivr/ivr-flow-editor.tsx
Normal file
264
apps/frontend/src/app/app/telefonia/ivr/ivr-flow-editor.tsx
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useCallback, useMemo, useState } from "react";
|
||||||
|
import {
|
||||||
|
ReactFlow,
|
||||||
|
Background,
|
||||||
|
Controls,
|
||||||
|
Handle,
|
||||||
|
Position,
|
||||||
|
MarkerType,
|
||||||
|
useNodesState,
|
||||||
|
useEdgesState,
|
||||||
|
type Node,
|
||||||
|
type Edge,
|
||||||
|
type NodeProps,
|
||||||
|
type NodeTypes,
|
||||||
|
} from "@xyflow/react";
|
||||||
|
import "@xyflow/react/dist/style.css";
|
||||||
|
import { PhoneIncoming, Plus, Save, Trash2 } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input, Select } from "@/components/ui/input";
|
||||||
|
import { ALLOWED_IVR_DIGITS, type IvrMenu } from "@/lib/callcenter-types";
|
||||||
|
import type { Extension } from "@/lib/extension-types";
|
||||||
|
import { updateIvrMenuOptions, type IvrMenuOptionInput } from "./actions";
|
||||||
|
|
||||||
|
interface OptionState {
|
||||||
|
key: string;
|
||||||
|
digit: string;
|
||||||
|
destinationNumber: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EntryNodeData extends Record<string, unknown> {}
|
||||||
|
|
||||||
|
interface OptionNodeData extends Record<string, unknown> {
|
||||||
|
option: OptionState;
|
||||||
|
extensions: Extension[];
|
||||||
|
usedDigits: string[];
|
||||||
|
onChange: (key: string, patch: Partial<OptionState>) => void;
|
||||||
|
onRemove: (key: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function EntryNode(_props: NodeProps<Node<EntryNodeData, "entry">>) {
|
||||||
|
return (
|
||||||
|
<div className="min-w-[170px] rounded-lg border-2 border-accent bg-surface px-4 py-3 shadow-panel">
|
||||||
|
<div className="flex items-center gap-2 text-sm font-semibold text-foreground">
|
||||||
|
<PhoneIncoming className="h-4 w-4 text-accent" aria-hidden />
|
||||||
|
Entrada do menu
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 text-xs text-muted-foreground">Atende, toca o prompt e aguarda 1 dígito</p>
|
||||||
|
<Handle type="source" position={Position.Bottom} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function OptionNode({ data }: NodeProps<Node<OptionNodeData, "option">>) {
|
||||||
|
const { option, extensions, usedDigits, onChange, onRemove } = data;
|
||||||
|
return (
|
||||||
|
<div className="w-[230px] space-y-2 rounded-lg border border-border bg-surface px-3 py-3 shadow-panel">
|
||||||
|
<Handle type="target" position={Position.Top} />
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<Select
|
||||||
|
className="nodrag w-20"
|
||||||
|
value={option.digit}
|
||||||
|
onChange={(e) => onChange(option.key, { digit: e.target.value })}
|
||||||
|
>
|
||||||
|
{ALLOWED_IVR_DIGITS.map((d) => (
|
||||||
|
<option key={d} value={d} disabled={d !== option.digit && usedDigits.includes(d)}>
|
||||||
|
{d}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="nodrag h-7 w-7"
|
||||||
|
onClick={() => onRemove(option.key)}
|
||||||
|
aria-label={`Remover opção ${option.digit}`}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3.5 w-3.5" aria-hidden />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
className="nodrag"
|
||||||
|
value={option.destinationNumber}
|
||||||
|
onChange={(e) => onChange(option.key, { destinationNumber: e.target.value })}
|
||||||
|
>
|
||||||
|
<option value="">Selecione um ramal…</option>
|
||||||
|
{extensions.map((ext) => (
|
||||||
|
<option key={ext.id} value={ext.number}>
|
||||||
|
{ext.number} — {ext.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
<Input
|
||||||
|
className="nodrag"
|
||||||
|
value={option.label}
|
||||||
|
onChange={(e) => onChange(option.key, { label: e.target.value })}
|
||||||
|
placeholder="Descrição (opcional)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const NODE_TYPES: NodeTypes = { entry: EntryNode, option: OptionNode };
|
||||||
|
|
||||||
|
const ENTRY_NODE_ID = "entry";
|
||||||
|
const COLUMN_WIDTH = 260;
|
||||||
|
|
||||||
|
function layoutNodes(options: OptionState[], extensions: Extension[], onChange: OptionNodeData["onChange"], onRemove: OptionNodeData["onRemove"]) {
|
||||||
|
const usedDigits = options.map((o) => o.digit);
|
||||||
|
const entry: Node<EntryNodeData, "entry"> = {
|
||||||
|
id: ENTRY_NODE_ID,
|
||||||
|
type: "entry",
|
||||||
|
position: { x: (Math.max(options.length, 1) * COLUMN_WIDTH) / 2 - 85, y: 0 },
|
||||||
|
data: {},
|
||||||
|
draggable: true,
|
||||||
|
};
|
||||||
|
const optionNodes: Node<OptionNodeData, "option">[] = options.map((option, i) => ({
|
||||||
|
id: option.key,
|
||||||
|
type: "option",
|
||||||
|
position: { x: i * COLUMN_WIDTH, y: 160 },
|
||||||
|
data: { option, extensions, usedDigits, onChange, onRemove },
|
||||||
|
draggable: true,
|
||||||
|
}));
|
||||||
|
const edges: Edge[] = options.map((option) => ({
|
||||||
|
id: `${ENTRY_NODE_ID}-${option.key}`,
|
||||||
|
source: ENTRY_NODE_ID,
|
||||||
|
target: option.key,
|
||||||
|
markerEnd: { type: MarkerType.ArrowClosed },
|
||||||
|
}));
|
||||||
|
return { nodes: [entry, ...optionNodes], edges };
|
||||||
|
}
|
||||||
|
|
||||||
|
let keyCounter = 0;
|
||||||
|
function nextKey(): string {
|
||||||
|
keyCounter += 1;
|
||||||
|
return `opt-${Date.now()}-${keyCounter}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Editor visual do menu de IVR (PHASE 60, pedido do usuário: "ajusta o
|
||||||
|
* IVR para fazer o fluxo de maneira visual") — canvas de nós/arestas em
|
||||||
|
* cima do MESMO modelo que já existia (entrada + opções por dígito),
|
||||||
|
* sem nenhuma dependência de execução externa. Cada nó de opção edita
|
||||||
|
* direto os campos (dígito/ramal/descrição); salvar recompila o
|
||||||
|
* dialplan no backend exatamente como o formulário antigo fazia.
|
||||||
|
*/
|
||||||
|
export function IvrFlowEditor({ menu, extensions }: { menu: IvrMenu; extensions: Extension[] }) {
|
||||||
|
const initialOptions = useMemo<OptionState[]>(
|
||||||
|
() => menu.options.map((o) => ({ key: o.id, digit: o.digit, destinationNumber: o.destinationNumber, label: o.label ?? "" })),
|
||||||
|
[menu.options],
|
||||||
|
);
|
||||||
|
const [options, setOptions] = useState<OptionState[]>(initialOptions);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [saved, setSaved] = useState(false);
|
||||||
|
const [pending, setPending] = useState(false);
|
||||||
|
|
||||||
|
const onChange = useCallback((key: string, patch: Partial<OptionState>) => {
|
||||||
|
setSaved(false);
|
||||||
|
setOptions((prev) => prev.map((o) => (o.key === key ? { ...o, ...patch } : o)));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onRemove = useCallback((key: string) => {
|
||||||
|
setSaved(false);
|
||||||
|
setOptions((prev) => prev.filter((o) => o.key !== key));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const { nodes: initialNodes, edges: initialEdges } = useMemo(
|
||||||
|
() => layoutNodes(options, extensions, onChange, onRemove),
|
||||||
|
[options, extensions, onChange, onRemove],
|
||||||
|
);
|
||||||
|
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||||
|
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||||
|
|
||||||
|
// useNodesState/useEdgesState só inicializam uma vez — recalcula o
|
||||||
|
// grafo sempre que a LISTA de opções muda (add/remove/digit editado
|
||||||
|
// afeta layout e handles), preservando posição arrastada manualmente
|
||||||
|
// quando só o conteúdo de um nó já existente muda.
|
||||||
|
useMemo(() => {
|
||||||
|
setNodes((prevNodes) => {
|
||||||
|
const positions = new Map(prevNodes.map((n) => [n.id, n.position]));
|
||||||
|
return initialNodes.map((n) => ({ ...n, position: positions.get(n.id) ?? n.position }));
|
||||||
|
});
|
||||||
|
setEdges(initialEdges);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- só quando initialNodes/Edges mudam de identidade (opções mudaram)
|
||||||
|
}, [initialNodes, initialEdges]);
|
||||||
|
|
||||||
|
function addOption() {
|
||||||
|
setSaved(false);
|
||||||
|
const usedDigits = new Set(options.map((o) => o.digit));
|
||||||
|
const nextDigit = ALLOWED_IVR_DIGITS.find((d) => !usedDigits.has(d)) ?? "1";
|
||||||
|
setOptions((prev) => [...prev, { key: nextKey(), digit: nextDigit, destinationNumber: "", label: "" }]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSave() {
|
||||||
|
setError(null);
|
||||||
|
if (options.length === 0) {
|
||||||
|
setError("O menu precisa de pelo menos uma opção.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (options.some((o) => !o.destinationNumber.trim())) {
|
||||||
|
setError("Toda opção precisa de um ramal de destino selecionado.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const digits = options.map((o) => o.digit);
|
||||||
|
if (new Set(digits).size !== digits.length) {
|
||||||
|
setError("Não pode repetir o mesmo dígito em duas opções.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload: IvrMenuOptionInput[] = options.map((o) => ({
|
||||||
|
digit: o.digit,
|
||||||
|
destinationNumber: o.destinationNumber.trim(),
|
||||||
|
label: o.label.trim() || undefined,
|
||||||
|
}));
|
||||||
|
|
||||||
|
setPending(true);
|
||||||
|
updateIvrMenuOptions(menu.id, payload).then((result) => {
|
||||||
|
setPending(false);
|
||||||
|
if (!result.ok) {
|
||||||
|
setError(result.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSaved(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||||
|
<Button type="button" variant="ghost" size="sm" onClick={addOption} disabled={pending || options.length >= 12}>
|
||||||
|
<Plus className="h-3.5 w-3.5" aria-hidden /> Adicionar opção
|
||||||
|
</Button>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{saved && <span className="text-xs text-accent">Salvo</span>}
|
||||||
|
<Button type="button" size="sm" onClick={onSave} disabled={pending}>
|
||||||
|
<Save className="h-3.5 w-3.5" aria-hidden /> {pending ? "Salvando…" : "Salvar alterações"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{error && (
|
||||||
|
<p role="alert" className="rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<div style={{ height: 320 }} className="overflow-hidden rounded-lg border border-border bg-muted/20">
|
||||||
|
<ReactFlow
|
||||||
|
nodes={nodes}
|
||||||
|
edges={edges}
|
||||||
|
onNodesChange={onNodesChange}
|
||||||
|
onEdgesChange={onEdgesChange}
|
||||||
|
nodeTypes={NODE_TYPES}
|
||||||
|
fitView
|
||||||
|
proOptions={{ hideAttribution: true }}
|
||||||
|
>
|
||||||
|
<Background />
|
||||||
|
<Controls showInteractive={false} />
|
||||||
|
</ReactFlow>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,10 +7,11 @@ import { Panel, PanelHeader } from "@/components/ui/panel";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input, Select, FieldLabel } from "@/components/ui/input";
|
import { Input, Select, FieldLabel } from "@/components/ui/input";
|
||||||
import { Pill } from "@/components/ui/pill";
|
import { Pill } from "@/components/ui/pill";
|
||||||
import { EmptyState, TBody, TD, TH, THead, TR, Table } from "@/components/ui/table";
|
import { EmptyState } from "@/components/ui/table";
|
||||||
import { ALLOWED_IVR_DIGITS, IVR_ENTRY_DESTINATION, type IvrMenu } from "@/lib/callcenter-types";
|
import { ALLOWED_IVR_DIGITS, IVR_ENTRY_DESTINATION, type IvrMenu } from "@/lib/callcenter-types";
|
||||||
import type { Extension } from "@/lib/extension-types";
|
import type { Extension } from "@/lib/extension-types";
|
||||||
import { createIvrMenu, deleteIvrMenu, deleteIvrMenuPrompt, uploadIvrMenuPrompt, type IvrMenuOptionInput } from "./actions";
|
import { createIvrMenu, deleteIvrMenu, deleteIvrMenuPrompt, uploadIvrMenuPrompt, type IvrMenuOptionInput } from "./actions";
|
||||||
|
import { IvrFlowEditor } from "./ivr-flow-editor";
|
||||||
|
|
||||||
const CUSTOM_PROMPT_PREFIX = "/ivr-prompts/";
|
const CUSTOM_PROMPT_PREFIX = "/ivr-prompts/";
|
||||||
|
|
||||||
@@ -69,24 +70,7 @@ export function IvrView({ menus, extensions }: { menus: IvrMenu[]; extensions: E
|
|||||||
<span className="text-foreground">{IVR_ENTRY_DESTINATION}</span>
|
<span className="text-foreground">{IVR_ENTRY_DESTINATION}</span>
|
||||||
</p>
|
</p>
|
||||||
<PromptControl menu={menu} />
|
<PromptControl menu={menu} />
|
||||||
<Table>
|
<IvrFlowEditor menu={menu} extensions={extensions} />
|
||||||
<THead>
|
|
||||||
<TR>
|
|
||||||
<TH>Dígito</TH>
|
|
||||||
<TH>Destino</TH>
|
|
||||||
<TH>Descrição</TH>
|
|
||||||
</TR>
|
|
||||||
</THead>
|
|
||||||
<TBody>
|
|
||||||
{menu.options.map((opt) => (
|
|
||||||
<TR key={opt.id}>
|
|
||||||
<TD className="font-mono font-medium text-foreground">{opt.digit}</TD>
|
|
||||||
<TD className="font-mono text-muted-foreground">{opt.destinationNumber}</TD>
|
|
||||||
<TD className="text-muted-foreground">{opt.label ?? "—"}</TD>
|
|
||||||
</TR>
|
|
||||||
))}
|
|
||||||
</TBody>
|
|
||||||
</Table>
|
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -179,9 +179,34 @@ o DID, `play_and_get_digits` abriu e tocou o arquivo até o fim duas
|
|||||||
vezes (`mod_sndfile` resample automático, sem erro no log), colheu o
|
vezes (`mod_sndfile` resample automático, sem erro no log), colheu o
|
||||||
dígito real e bridged corretamente com o ramal de destino.
|
dígito real e bridged corretamente com o ramal de destino.
|
||||||
|
|
||||||
|
## Editor visual (PHASE 60)
|
||||||
|
|
||||||
|
`apps/frontend/.../telefonia/ivr/ivr-flow-editor.tsx` — canvas de nós e
|
||||||
|
setas (`@xyflow/react`) em cima do MESMO modelo de sempre (entrada +
|
||||||
|
opções por dígito), sem nenhuma dependência de execução externa. Um nó
|
||||||
|
"Entrada" fixo conectado a um nó por opção (dígito + select de ramal +
|
||||||
|
descrição, editável direto no nó); "Adicionar opção"/"Salvar alterações"
|
||||||
|
chamam o mesmo `PATCH /ivr-menus/:id` que já existia. Avaliado (e
|
||||||
|
descartado por enquanto, ver AskUserQuestion desta sessão) integrar
|
||||||
|
Node-RED de verdade: seria uma plataforma externa completa (motor de
|
||||||
|
execução próprio, nós "function" = RCE, sem multi-tenancy nativa) —
|
||||||
|
esforço de vários dias e superfície de risco nova, não um ajuste na tela
|
||||||
|
atual. Um editor visual próprio, sobre o backend já existente, entrega o
|
||||||
|
mesmo valor imediato (ver estrutura do menu como grafo, editar visualmente)
|
||||||
|
sem essas duas desvantagens.
|
||||||
|
|
||||||
|
Testado ponta a ponta pela tela de verdade: menu criado, prompt de voz
|
||||||
|
real enviado (WAV sintetizado com `espeak-ng`, não um tom sintético),
|
||||||
|
uma segunda opção adicionada via `PATCH` (mesma chamada que o botão
|
||||||
|
"Salvar" do editor visual faz) — confirmado no banco que a versão 2 do
|
||||||
|
dialplan compilou as duas opções corretamente, superando a versão 1.
|
||||||
|
|
||||||
## O que falta
|
## O que falta
|
||||||
|
|
||||||
- Sem TTS (texto→voz) — só upload de arquivo WAV já gravado.
|
- Sem TTS (texto→voz) — só upload de arquivo WAV já gravado.
|
||||||
|
- Editor visual não persiste posição manual dos nós entre recargas
|
||||||
|
(layout recalculado a cada carga da página — arrastar só ajuda
|
||||||
|
durante a mesma sessão de edição).
|
||||||
- Menu de IVR não suporta sub-menus (uma opção levando a OUTRO IVR) nem
|
- Menu de IVR não suporta sub-menus (uma opção levando a OUTRO IVR) nem
|
||||||
destino "fila" — só ramal, dentro do contexto `default`.
|
destino "fila" — só ramal, dentro do contexto `default`.
|
||||||
- Tela de frontend "Rotas de Entrada" cobre só CRUD simples (DID →
|
- Tela de frontend "Rotas de Entrada" cobre só CRUD simples (DID →
|
||||||
|
|||||||
162
pnpm-lock.yaml
generated
162
pnpm-lock.yaml
generated
@@ -216,6 +216,9 @@ importers:
|
|||||||
'@tanstack/react-table':
|
'@tanstack/react-table':
|
||||||
specifier: 8.20.5
|
specifier: 8.20.5
|
||||||
version: 8.20.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
version: 8.20.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
'@xyflow/react':
|
||||||
|
specifier: ^12.11.5
|
||||||
|
version: 12.11.5(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
class-variance-authority:
|
class-variance-authority:
|
||||||
specifier: 0.7.1
|
specifier: 0.7.1
|
||||||
version: 0.7.1
|
version: 0.7.1
|
||||||
@@ -1665,6 +1668,9 @@ packages:
|
|||||||
'@types/d3-delaunay@6.0.1':
|
'@types/d3-delaunay@6.0.1':
|
||||||
resolution: {integrity: sha512-tLxQ2sfT0p6sxdG75c6f/ekqxjyYR0+LwPrsO1mbC9YDBzPJhs2HbJJRrn8Ez1DBoHRo2yx7YEATI+8V1nGMnQ==}
|
resolution: {integrity: sha512-tLxQ2sfT0p6sxdG75c6f/ekqxjyYR0+LwPrsO1mbC9YDBzPJhs2HbJJRrn8Ez1DBoHRo2yx7YEATI+8V1nGMnQ==}
|
||||||
|
|
||||||
|
'@types/d3-drag@3.0.7':
|
||||||
|
resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==}
|
||||||
|
|
||||||
'@types/d3-ease@3.0.2':
|
'@types/d3-ease@3.0.2':
|
||||||
resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==}
|
resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==}
|
||||||
|
|
||||||
@@ -1677,12 +1683,18 @@ packages:
|
|||||||
'@types/d3-interpolate@3.0.1':
|
'@types/d3-interpolate@3.0.1':
|
||||||
resolution: {integrity: sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==}
|
resolution: {integrity: sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==}
|
||||||
|
|
||||||
|
'@types/d3-interpolate@3.0.4':
|
||||||
|
resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==}
|
||||||
|
|
||||||
'@types/d3-path@3.1.1':
|
'@types/d3-path@3.1.1':
|
||||||
resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==}
|
resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==}
|
||||||
|
|
||||||
'@types/d3-scale@4.0.2':
|
'@types/d3-scale@4.0.2':
|
||||||
resolution: {integrity: sha512-Yk4htunhPAwN0XGlIwArRomOjdoBFXC3+kCxK2Ubg7I9shQlVSJy/pG/Ht5ASN+gdMIalpk8TJ5xV74jFsetLA==}
|
resolution: {integrity: sha512-Yk4htunhPAwN0XGlIwArRomOjdoBFXC3+kCxK2Ubg7I9shQlVSJy/pG/Ht5ASN+gdMIalpk8TJ5xV74jFsetLA==}
|
||||||
|
|
||||||
|
'@types/d3-selection@3.0.11':
|
||||||
|
resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==}
|
||||||
|
|
||||||
'@types/d3-shape@3.1.7':
|
'@types/d3-shape@3.1.7':
|
||||||
resolution: {integrity: sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==}
|
resolution: {integrity: sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==}
|
||||||
|
|
||||||
@@ -1695,6 +1707,12 @@ packages:
|
|||||||
'@types/d3-timer@3.0.2':
|
'@types/d3-timer@3.0.2':
|
||||||
resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
|
resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
|
||||||
|
|
||||||
|
'@types/d3-transition@3.0.9':
|
||||||
|
resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==}
|
||||||
|
|
||||||
|
'@types/d3-zoom@3.0.8':
|
||||||
|
resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==}
|
||||||
|
|
||||||
'@types/geojson@7946.0.16':
|
'@types/geojson@7946.0.16':
|
||||||
resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==}
|
resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==}
|
||||||
|
|
||||||
@@ -1759,6 +1777,22 @@ packages:
|
|||||||
'@visx/vendor@4.0.0-alpha.0':
|
'@visx/vendor@4.0.0-alpha.0':
|
||||||
resolution: {integrity: sha512-6I+MuqXBcv9jnlcVowHoHKSdk9gXTWkHLKyqBwRWg7LY6A3Ei8SHfubpqGV5rBUSppxMq2RszPJUS6w+H0YgmQ==}
|
resolution: {integrity: sha512-6I+MuqXBcv9jnlcVowHoHKSdk9gXTWkHLKyqBwRWg7LY6A3Ei8SHfubpqGV5rBUSppxMq2RszPJUS6w+H0YgmQ==}
|
||||||
|
|
||||||
|
'@xyflow/react@12.11.5':
|
||||||
|
resolution: {integrity: sha512-QqoryGkqEhWBuQN9bZWRKhwr3Uoj9lCGj/tg0NnIWHHEOXV+5c8cYsc7Q9TST63V92lHqcNV9P5cjvJ9ZAmblQ==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '>=17'
|
||||||
|
'@types/react-dom': '>=17'
|
||||||
|
react: '>=17'
|
||||||
|
react-dom: '>=17'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@xyflow/system@0.0.81':
|
||||||
|
resolution: {integrity: sha512-hfbafW4i7uLq7ILok8QWFFm4KMFw22lbZNJHKfHOMSOOoCk5e5m8yfr84UV9NaJajmogWaLVnp2XFU9JQejlqg==}
|
||||||
|
|
||||||
abstract-logging@2.0.1:
|
abstract-logging@2.0.1:
|
||||||
resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==}
|
resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==}
|
||||||
|
|
||||||
@@ -1876,6 +1910,9 @@ packages:
|
|||||||
class-variance-authority@0.7.1:
|
class-variance-authority@0.7.1:
|
||||||
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
|
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
|
||||||
|
|
||||||
|
classcat@5.0.5:
|
||||||
|
resolution: {integrity: sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==}
|
||||||
|
|
||||||
classnames@2.5.1:
|
classnames@2.5.1:
|
||||||
resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==}
|
resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==}
|
||||||
|
|
||||||
@@ -1937,6 +1974,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ==}
|
resolution: {integrity: sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
d3-dispatch@3.0.1:
|
||||||
|
resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
d3-drag@3.0.0:
|
||||||
|
resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
d3-ease@3.0.1:
|
d3-ease@3.0.1:
|
||||||
resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
|
resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -1961,6 +2006,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
|
resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
d3-selection@3.0.0:
|
||||||
|
resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
d3-shape@3.2.0:
|
d3-shape@3.2.0:
|
||||||
resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==}
|
resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -1977,6 +2026,16 @@ packages:
|
|||||||
resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
|
resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
d3-transition@3.0.1:
|
||||||
|
resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
peerDependencies:
|
||||||
|
d3-selection: 2 - 3
|
||||||
|
|
||||||
|
d3-zoom@3.0.0:
|
||||||
|
resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
debug@4.4.3:
|
debug@4.4.3:
|
||||||
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
||||||
engines: {node: '>=6.0'}
|
engines: {node: '>=6.0'}
|
||||||
@@ -2952,6 +3011,11 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
use-sync-external-store@1.6.0:
|
||||||
|
resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
|
||||||
util-deprecate@1.0.2:
|
util-deprecate@1.0.2:
|
||||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
|
|
||||||
@@ -3011,6 +3075,21 @@ packages:
|
|||||||
zeptomatch@2.1.0:
|
zeptomatch@2.1.0:
|
||||||
resolution: {integrity: sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==}
|
resolution: {integrity: sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==}
|
||||||
|
|
||||||
|
zustand@4.5.7:
|
||||||
|
resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==}
|
||||||
|
engines: {node: '>=12.7.0'}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '>=16.8'
|
||||||
|
immer: '>=9.0.6'
|
||||||
|
react: '>=16.8'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
immer:
|
||||||
|
optional: true
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
'@alloc/quick-lru@5.2.0': {}
|
'@alloc/quick-lru@5.2.0': {}
|
||||||
@@ -4189,6 +4268,10 @@ snapshots:
|
|||||||
|
|
||||||
'@types/d3-delaunay@6.0.1': {}
|
'@types/d3-delaunay@6.0.1': {}
|
||||||
|
|
||||||
|
'@types/d3-drag@3.0.7':
|
||||||
|
dependencies:
|
||||||
|
'@types/d3-selection': 3.0.11
|
||||||
|
|
||||||
'@types/d3-ease@3.0.2': {}
|
'@types/d3-ease@3.0.2': {}
|
||||||
|
|
||||||
'@types/d3-format@3.0.1': {}
|
'@types/d3-format@3.0.1': {}
|
||||||
@@ -4201,12 +4284,18 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/d3-color': 3.1.0
|
'@types/d3-color': 3.1.0
|
||||||
|
|
||||||
|
'@types/d3-interpolate@3.0.4':
|
||||||
|
dependencies:
|
||||||
|
'@types/d3-color': 3.1.0
|
||||||
|
|
||||||
'@types/d3-path@3.1.1': {}
|
'@types/d3-path@3.1.1': {}
|
||||||
|
|
||||||
'@types/d3-scale@4.0.2':
|
'@types/d3-scale@4.0.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/d3-time': 3.0.0
|
'@types/d3-time': 3.0.0
|
||||||
|
|
||||||
|
'@types/d3-selection@3.0.11': {}
|
||||||
|
|
||||||
'@types/d3-shape@3.1.7':
|
'@types/d3-shape@3.1.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/d3-path': 3.1.1
|
'@types/d3-path': 3.1.1
|
||||||
@@ -4217,6 +4306,15 @@ snapshots:
|
|||||||
|
|
||||||
'@types/d3-timer@3.0.2': {}
|
'@types/d3-timer@3.0.2': {}
|
||||||
|
|
||||||
|
'@types/d3-transition@3.0.9':
|
||||||
|
dependencies:
|
||||||
|
'@types/d3-selection': 3.0.11
|
||||||
|
|
||||||
|
'@types/d3-zoom@3.0.8':
|
||||||
|
dependencies:
|
||||||
|
'@types/d3-interpolate': 3.0.1
|
||||||
|
'@types/d3-selection': 3.0.11
|
||||||
|
|
||||||
'@types/geojson@7946.0.16': {}
|
'@types/geojson@7946.0.16': {}
|
||||||
|
|
||||||
'@types/lodash@4.17.25': {}
|
'@types/lodash@4.17.25': {}
|
||||||
@@ -4331,6 +4429,31 @@ snapshots:
|
|||||||
d3-time-format: 4.1.0
|
d3-time-format: 4.1.0
|
||||||
internmap: 2.0.3
|
internmap: 2.0.3
|
||||||
|
|
||||||
|
'@xyflow/react@12.11.5(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||||
|
dependencies:
|
||||||
|
'@xyflow/system': 0.0.81
|
||||||
|
classcat: 5.0.5
|
||||||
|
react: 19.1.0
|
||||||
|
react-dom: 19.1.0(react@19.1.0)
|
||||||
|
zustand: 4.5.7(@types/react@19.1.0)(react@19.1.0)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.1.0
|
||||||
|
'@types/react-dom': 19.1.0(@types/react@19.1.0)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- immer
|
||||||
|
|
||||||
|
'@xyflow/system@0.0.81':
|
||||||
|
dependencies:
|
||||||
|
'@types/d3-drag': 3.0.7
|
||||||
|
'@types/d3-interpolate': 3.0.4
|
||||||
|
'@types/d3-selection': 3.0.11
|
||||||
|
'@types/d3-transition': 3.0.9
|
||||||
|
'@types/d3-zoom': 3.0.8
|
||||||
|
d3-drag: 3.0.0
|
||||||
|
d3-interpolate: 3.0.1
|
||||||
|
d3-selection: 3.0.0
|
||||||
|
d3-zoom: 3.0.0
|
||||||
|
|
||||||
abstract-logging@2.0.1: {}
|
abstract-logging@2.0.1: {}
|
||||||
|
|
||||||
accepts@1.3.8:
|
accepts@1.3.8:
|
||||||
@@ -4458,6 +4581,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
clsx: 2.1.1
|
clsx: 2.1.1
|
||||||
|
|
||||||
|
classcat@5.0.5: {}
|
||||||
|
|
||||||
classnames@2.5.1: {}
|
classnames@2.5.1: {}
|
||||||
|
|
||||||
client-only@0.0.1: {}
|
client-only@0.0.1: {}
|
||||||
@@ -4503,6 +4628,13 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
delaunator: 5.1.0
|
delaunator: 5.1.0
|
||||||
|
|
||||||
|
d3-dispatch@3.0.1: {}
|
||||||
|
|
||||||
|
d3-drag@3.0.0:
|
||||||
|
dependencies:
|
||||||
|
d3-dispatch: 3.0.1
|
||||||
|
d3-selection: 3.0.0
|
||||||
|
|
||||||
d3-ease@3.0.1: {}
|
d3-ease@3.0.1: {}
|
||||||
|
|
||||||
d3-format@3.1.0: {}
|
d3-format@3.1.0: {}
|
||||||
@@ -4525,6 +4657,8 @@ snapshots:
|
|||||||
d3-time: 3.1.0
|
d3-time: 3.1.0
|
||||||
d3-time-format: 4.1.0
|
d3-time-format: 4.1.0
|
||||||
|
|
||||||
|
d3-selection@3.0.0: {}
|
||||||
|
|
||||||
d3-shape@3.2.0:
|
d3-shape@3.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
d3-path: 3.1.0
|
d3-path: 3.1.0
|
||||||
@@ -4539,6 +4673,23 @@ snapshots:
|
|||||||
|
|
||||||
d3-timer@3.0.1: {}
|
d3-timer@3.0.1: {}
|
||||||
|
|
||||||
|
d3-transition@3.0.1(d3-selection@3.0.0):
|
||||||
|
dependencies:
|
||||||
|
d3-color: 3.1.0
|
||||||
|
d3-dispatch: 3.0.1
|
||||||
|
d3-ease: 3.0.1
|
||||||
|
d3-interpolate: 3.0.1
|
||||||
|
d3-selection: 3.0.0
|
||||||
|
d3-timer: 3.0.1
|
||||||
|
|
||||||
|
d3-zoom@3.0.0:
|
||||||
|
dependencies:
|
||||||
|
d3-dispatch: 3.0.1
|
||||||
|
d3-drag: 3.0.0
|
||||||
|
d3-interpolate: 3.0.1
|
||||||
|
d3-selection: 3.0.0
|
||||||
|
d3-transition: 3.0.1(d3-selection@3.0.0)
|
||||||
|
|
||||||
debug@4.4.3:
|
debug@4.4.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
@@ -5535,6 +5686,10 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.1.0
|
'@types/react': 19.1.0
|
||||||
|
|
||||||
|
use-sync-external-store@1.6.0(react@19.1.0):
|
||||||
|
dependencies:
|
||||||
|
react: 19.1.0
|
||||||
|
|
||||||
util-deprecate@1.0.2: {}
|
util-deprecate@1.0.2: {}
|
||||||
|
|
||||||
valibot@1.4.2(typescript@5.9.3):
|
valibot@1.4.2(typescript@5.9.3):
|
||||||
@@ -5580,3 +5735,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
grammex: 3.1.13
|
grammex: 3.1.13
|
||||||
graphmatch: 1.1.1
|
graphmatch: 1.1.1
|
||||||
|
|
||||||
|
zustand@4.5.7(@types/react@19.1.0)(react@19.1.0):
|
||||||
|
dependencies:
|
||||||
|
use-sync-external-store: 1.6.0(react@19.1.0)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.1.0
|
||||||
|
react: 19.1.0
|
||||||
|
|||||||
Reference in New Issue
Block a user