@meridial/react 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +134 -0
- package/dist/chunk-QCWLFL7O.js +4294 -0
- package/dist/chunk-QCWLFL7O.js.map +1 -0
- package/dist/chunk-VDQAVB4N.js +1754 -0
- package/dist/chunk-VDQAVB4N.js.map +1 -0
- package/dist/chunk-WCRZUGN4.js +1933 -0
- package/dist/chunk-WCRZUGN4.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/recorder.d.ts +11 -0
- package/dist/recorder.js +9 -0
- package/dist/recorder.js.map +1 -0
- package/dist/styles.css +6012 -0
- package/dist/voicebox-DCgECemo.d.ts +27 -0
- package/dist/voicebox.d.ts +3 -0
- package/dist/voicebox.js +9 -0
- package/dist/voicebox.js.map +1 -0
- package/package.json +68 -0
|
@@ -0,0 +1,1754 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Button,
|
|
3
|
+
DragHandle,
|
|
4
|
+
MIN_DESC_LENGTH,
|
|
5
|
+
STORAGE_KEYS,
|
|
6
|
+
Separator,
|
|
7
|
+
apiWorkflowsResponseSchema,
|
|
8
|
+
cn,
|
|
9
|
+
generateWorkflowName,
|
|
10
|
+
useElementTracker,
|
|
11
|
+
workflowSchema
|
|
12
|
+
} from "./chunk-QCWLFL7O.js";
|
|
13
|
+
|
|
14
|
+
// src/recorder.tsx
|
|
15
|
+
import { useRef as useRef7, useState as useState7, useCallback as useCallback6, useEffect as useEffect4 } from "react";
|
|
16
|
+
import { useDraggable as useDraggable2 } from "@neodrag/react";
|
|
17
|
+
import { motion as motion3, AnimatePresence as AnimatePresence2 } from "motion/react";
|
|
18
|
+
|
|
19
|
+
// ../ui/src/components/waveform.tsx
|
|
20
|
+
import { useEffect, useRef } from "react";
|
|
21
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
22
|
+
var Waveform = ({
|
|
23
|
+
active = false,
|
|
24
|
+
processing = false,
|
|
25
|
+
deviceId,
|
|
26
|
+
barWidth = 3,
|
|
27
|
+
barGap = 1,
|
|
28
|
+
barRadius = 1.5,
|
|
29
|
+
barColor,
|
|
30
|
+
fadeEdges = true,
|
|
31
|
+
fadeWidth = 24,
|
|
32
|
+
barHeight: baseBarHeight = 4,
|
|
33
|
+
height = 64,
|
|
34
|
+
sensitivity = 1,
|
|
35
|
+
smoothingTimeConstant = 0.8,
|
|
36
|
+
fftSize = 256,
|
|
37
|
+
historySize = 60,
|
|
38
|
+
updateRate = 30,
|
|
39
|
+
mode = "static",
|
|
40
|
+
onError,
|
|
41
|
+
onStreamReady,
|
|
42
|
+
onStreamEnd,
|
|
43
|
+
className,
|
|
44
|
+
...props
|
|
45
|
+
}) => {
|
|
46
|
+
const canvasRef = useRef(null);
|
|
47
|
+
const containerRef = useRef(null);
|
|
48
|
+
const historyRef = useRef([]);
|
|
49
|
+
const analyserRef = useRef(null);
|
|
50
|
+
const audioContextRef = useRef(null);
|
|
51
|
+
const streamRef = useRef(null);
|
|
52
|
+
const animationRef = useRef(0);
|
|
53
|
+
const lastUpdateRef = useRef(0);
|
|
54
|
+
const processingAnimationRef = useRef(null);
|
|
55
|
+
const lastActiveDataRef = useRef([]);
|
|
56
|
+
const transitionProgressRef = useRef(0);
|
|
57
|
+
const staticBarsRef = useRef([]);
|
|
58
|
+
const needsRedrawRef = useRef(true);
|
|
59
|
+
const gradientCacheRef = useRef(null);
|
|
60
|
+
const lastWidthRef = useRef(0);
|
|
61
|
+
const heightStyle = typeof height === "number" ? `${height}px` : height;
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
const canvas = canvasRef.current;
|
|
64
|
+
const container = containerRef.current;
|
|
65
|
+
if (!canvas || !container) return;
|
|
66
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
67
|
+
const rect = container.getBoundingClientRect();
|
|
68
|
+
const dpr = window.devicePixelRatio || 1;
|
|
69
|
+
canvas.width = rect.width * dpr;
|
|
70
|
+
canvas.height = rect.height * dpr;
|
|
71
|
+
canvas.style.width = `${rect.width}px`;
|
|
72
|
+
canvas.style.height = `${rect.height}px`;
|
|
73
|
+
const ctx = canvas.getContext("2d");
|
|
74
|
+
if (ctx) {
|
|
75
|
+
ctx.scale(dpr, dpr);
|
|
76
|
+
}
|
|
77
|
+
gradientCacheRef.current = null;
|
|
78
|
+
lastWidthRef.current = rect.width;
|
|
79
|
+
needsRedrawRef.current = true;
|
|
80
|
+
});
|
|
81
|
+
resizeObserver.observe(container);
|
|
82
|
+
return () => resizeObserver.disconnect();
|
|
83
|
+
}, []);
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
if (processing && !active) {
|
|
86
|
+
let time = 0;
|
|
87
|
+
transitionProgressRef.current = 0;
|
|
88
|
+
const animateProcessing = () => {
|
|
89
|
+
time += 0.03;
|
|
90
|
+
transitionProgressRef.current = Math.min(
|
|
91
|
+
1,
|
|
92
|
+
transitionProgressRef.current + 0.02
|
|
93
|
+
);
|
|
94
|
+
const processingData = [];
|
|
95
|
+
const barCount = Math.floor(
|
|
96
|
+
(containerRef.current?.getBoundingClientRect().width || 200) / (barWidth + barGap)
|
|
97
|
+
);
|
|
98
|
+
if (mode === "static") {
|
|
99
|
+
const halfCount = Math.floor(barCount / 2);
|
|
100
|
+
for (let i = 0; i < barCount; i++) {
|
|
101
|
+
const normalizedPosition = (i - halfCount) / halfCount;
|
|
102
|
+
const centerWeight = 1 - Math.abs(normalizedPosition) * 0.4;
|
|
103
|
+
const wave1 = Math.sin(time * 1.5 + normalizedPosition * 3) * 0.25;
|
|
104
|
+
const wave2 = Math.sin(time * 0.8 - normalizedPosition * 2) * 0.2;
|
|
105
|
+
const wave3 = Math.cos(time * 2 + normalizedPosition) * 0.15;
|
|
106
|
+
const combinedWave = wave1 + wave2 + wave3;
|
|
107
|
+
const processingValue = (0.2 + combinedWave) * centerWeight;
|
|
108
|
+
let finalValue = processingValue;
|
|
109
|
+
if (lastActiveDataRef.current.length > 0 && transitionProgressRef.current < 1) {
|
|
110
|
+
const lastDataIndex = Math.min(
|
|
111
|
+
i,
|
|
112
|
+
lastActiveDataRef.current.length - 1
|
|
113
|
+
);
|
|
114
|
+
const lastValue = lastActiveDataRef.current[lastDataIndex] || 0;
|
|
115
|
+
finalValue = lastValue * (1 - transitionProgressRef.current) + processingValue * transitionProgressRef.current;
|
|
116
|
+
}
|
|
117
|
+
processingData.push(Math.max(0.05, Math.min(1, finalValue)));
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
for (let i = 0; i < barCount; i++) {
|
|
121
|
+
const normalizedPosition = (i - barCount / 2) / (barCount / 2);
|
|
122
|
+
const centerWeight = 1 - Math.abs(normalizedPosition) * 0.4;
|
|
123
|
+
const wave1 = Math.sin(time * 1.5 + i * 0.15) * 0.25;
|
|
124
|
+
const wave2 = Math.sin(time * 0.8 - i * 0.1) * 0.2;
|
|
125
|
+
const wave3 = Math.cos(time * 2 + i * 0.05) * 0.15;
|
|
126
|
+
const combinedWave = wave1 + wave2 + wave3;
|
|
127
|
+
const processingValue = (0.2 + combinedWave) * centerWeight;
|
|
128
|
+
let finalValue = processingValue;
|
|
129
|
+
if (lastActiveDataRef.current.length > 0 && transitionProgressRef.current < 1) {
|
|
130
|
+
const lastDataIndex = Math.floor(
|
|
131
|
+
i / barCount * lastActiveDataRef.current.length
|
|
132
|
+
);
|
|
133
|
+
const lastValue = lastActiveDataRef.current[lastDataIndex] || 0;
|
|
134
|
+
finalValue = lastValue * (1 - transitionProgressRef.current) + processingValue * transitionProgressRef.current;
|
|
135
|
+
}
|
|
136
|
+
processingData.push(Math.max(0.05, Math.min(1, finalValue)));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (mode === "static") {
|
|
140
|
+
staticBarsRef.current = processingData;
|
|
141
|
+
} else {
|
|
142
|
+
historyRef.current = processingData;
|
|
143
|
+
}
|
|
144
|
+
needsRedrawRef.current = true;
|
|
145
|
+
processingAnimationRef.current = requestAnimationFrame(animateProcessing);
|
|
146
|
+
};
|
|
147
|
+
animateProcessing();
|
|
148
|
+
return () => {
|
|
149
|
+
if (processingAnimationRef.current) {
|
|
150
|
+
cancelAnimationFrame(processingAnimationRef.current);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
} else if (!active && !processing) {
|
|
154
|
+
const hasData = mode === "static" ? staticBarsRef.current.length > 0 : historyRef.current.length > 0;
|
|
155
|
+
if (hasData) {
|
|
156
|
+
let fadeProgress = 0;
|
|
157
|
+
const fadeToIdle = () => {
|
|
158
|
+
fadeProgress += 0.03;
|
|
159
|
+
if (fadeProgress < 1) {
|
|
160
|
+
if (mode === "static") {
|
|
161
|
+
staticBarsRef.current = staticBarsRef.current.map(
|
|
162
|
+
(value) => value * (1 - fadeProgress)
|
|
163
|
+
);
|
|
164
|
+
} else {
|
|
165
|
+
historyRef.current = historyRef.current.map(
|
|
166
|
+
(value) => value * (1 - fadeProgress)
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
needsRedrawRef.current = true;
|
|
170
|
+
requestAnimationFrame(fadeToIdle);
|
|
171
|
+
} else {
|
|
172
|
+
if (mode === "static") {
|
|
173
|
+
staticBarsRef.current = [];
|
|
174
|
+
} else {
|
|
175
|
+
historyRef.current = [];
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
fadeToIdle();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}, [processing, active, barWidth, barGap, mode]);
|
|
183
|
+
useEffect(() => {
|
|
184
|
+
if (!active) {
|
|
185
|
+
if (streamRef.current) {
|
|
186
|
+
streamRef.current.getTracks().forEach((track) => track.stop());
|
|
187
|
+
streamRef.current = null;
|
|
188
|
+
onStreamEnd?.();
|
|
189
|
+
}
|
|
190
|
+
if (audioContextRef.current && audioContextRef.current.state !== "closed") {
|
|
191
|
+
audioContextRef.current.close();
|
|
192
|
+
audioContextRef.current = null;
|
|
193
|
+
}
|
|
194
|
+
if (animationRef.current) {
|
|
195
|
+
cancelAnimationFrame(animationRef.current);
|
|
196
|
+
animationRef.current = 0;
|
|
197
|
+
}
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const setupMicrophone = async () => {
|
|
201
|
+
try {
|
|
202
|
+
const stream = await navigator.mediaDevices.getUserMedia({
|
|
203
|
+
audio: deviceId ? {
|
|
204
|
+
deviceId: { exact: deviceId },
|
|
205
|
+
echoCancellation: true,
|
|
206
|
+
noiseSuppression: true,
|
|
207
|
+
autoGainControl: true
|
|
208
|
+
} : {
|
|
209
|
+
echoCancellation: true,
|
|
210
|
+
noiseSuppression: true,
|
|
211
|
+
autoGainControl: true
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
streamRef.current = stream;
|
|
215
|
+
onStreamReady?.(stream);
|
|
216
|
+
const AudioContextConstructor = window.AudioContext || window.webkitAudioContext;
|
|
217
|
+
const audioContext = new AudioContextConstructor();
|
|
218
|
+
const analyser = audioContext.createAnalyser();
|
|
219
|
+
analyser.fftSize = fftSize;
|
|
220
|
+
analyser.smoothingTimeConstant = smoothingTimeConstant;
|
|
221
|
+
const source = audioContext.createMediaStreamSource(stream);
|
|
222
|
+
source.connect(analyser);
|
|
223
|
+
audioContextRef.current = audioContext;
|
|
224
|
+
analyserRef.current = analyser;
|
|
225
|
+
historyRef.current = [];
|
|
226
|
+
} catch (error) {
|
|
227
|
+
onError?.(error);
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
setupMicrophone();
|
|
231
|
+
return () => {
|
|
232
|
+
if (streamRef.current) {
|
|
233
|
+
streamRef.current.getTracks().forEach((track) => track.stop());
|
|
234
|
+
streamRef.current = null;
|
|
235
|
+
onStreamEnd?.();
|
|
236
|
+
}
|
|
237
|
+
if (audioContextRef.current && audioContextRef.current.state !== "closed") {
|
|
238
|
+
audioContextRef.current.close();
|
|
239
|
+
audioContextRef.current = null;
|
|
240
|
+
}
|
|
241
|
+
if (animationRef.current) {
|
|
242
|
+
cancelAnimationFrame(animationRef.current);
|
|
243
|
+
animationRef.current = 0;
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
}, [
|
|
247
|
+
active,
|
|
248
|
+
deviceId,
|
|
249
|
+
fftSize,
|
|
250
|
+
smoothingTimeConstant,
|
|
251
|
+
onError,
|
|
252
|
+
onStreamReady,
|
|
253
|
+
onStreamEnd
|
|
254
|
+
]);
|
|
255
|
+
useEffect(() => {
|
|
256
|
+
const canvas = canvasRef.current;
|
|
257
|
+
if (!canvas) return;
|
|
258
|
+
const ctx = canvas.getContext("2d");
|
|
259
|
+
if (!ctx) return;
|
|
260
|
+
let rafId;
|
|
261
|
+
const animate = (currentTime) => {
|
|
262
|
+
const rect = canvas.getBoundingClientRect();
|
|
263
|
+
if (active && currentTime - lastUpdateRef.current > updateRate) {
|
|
264
|
+
lastUpdateRef.current = currentTime;
|
|
265
|
+
if (analyserRef.current) {
|
|
266
|
+
const dataArray = new Uint8Array(
|
|
267
|
+
analyserRef.current.frequencyBinCount
|
|
268
|
+
);
|
|
269
|
+
analyserRef.current.getByteFrequencyData(dataArray);
|
|
270
|
+
if (mode === "static") {
|
|
271
|
+
const startFreq = Math.floor(dataArray.length * 0.05);
|
|
272
|
+
const endFreq = Math.floor(dataArray.length * 0.4);
|
|
273
|
+
const relevantData = dataArray.slice(startFreq, endFreq);
|
|
274
|
+
const barCount2 = Math.floor(rect.width / (barWidth + barGap));
|
|
275
|
+
const halfCount = Math.floor(barCount2 / 2);
|
|
276
|
+
const newBars = [];
|
|
277
|
+
for (let i = halfCount - 1; i >= 0; i--) {
|
|
278
|
+
const dataIndex = Math.floor(
|
|
279
|
+
i / halfCount * relevantData.length
|
|
280
|
+
);
|
|
281
|
+
const source = relevantData[dataIndex] ?? 0;
|
|
282
|
+
const value = Math.min(1, source / 255 * sensitivity);
|
|
283
|
+
newBars.push(Math.max(0.05, value));
|
|
284
|
+
}
|
|
285
|
+
for (let i = 0; i < halfCount; i++) {
|
|
286
|
+
const dataIndex = Math.floor(
|
|
287
|
+
i / halfCount * relevantData.length
|
|
288
|
+
);
|
|
289
|
+
const source = relevantData[dataIndex] ?? 0;
|
|
290
|
+
const value = Math.min(1, source / 255 * sensitivity);
|
|
291
|
+
newBars.push(Math.max(0.05, value));
|
|
292
|
+
}
|
|
293
|
+
staticBarsRef.current = newBars;
|
|
294
|
+
lastActiveDataRef.current = newBars;
|
|
295
|
+
} else {
|
|
296
|
+
let sum = 0;
|
|
297
|
+
const startFreq = Math.floor(dataArray.length * 0.05);
|
|
298
|
+
const endFreq = Math.floor(dataArray.length * 0.4);
|
|
299
|
+
const relevantData = dataArray.slice(startFreq, endFreq);
|
|
300
|
+
for (let i = 0; i < relevantData.length; i++) {
|
|
301
|
+
const sample = relevantData[i] ?? 0;
|
|
302
|
+
sum += sample;
|
|
303
|
+
}
|
|
304
|
+
const average = sum / (relevantData.length || 1) / 255 * sensitivity;
|
|
305
|
+
historyRef.current.push(Math.min(1, Math.max(0.05, average)));
|
|
306
|
+
lastActiveDataRef.current = [...historyRef.current];
|
|
307
|
+
if (historyRef.current.length > historySize) {
|
|
308
|
+
historyRef.current.shift();
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
needsRedrawRef.current = true;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (!needsRedrawRef.current && !active) {
|
|
315
|
+
rafId = requestAnimationFrame(animate);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
needsRedrawRef.current = active;
|
|
319
|
+
ctx.clearRect(0, 0, rect.width, rect.height);
|
|
320
|
+
const computedBarColor = barColor || (() => {
|
|
321
|
+
const style = getComputedStyle(canvas);
|
|
322
|
+
const color = style.color;
|
|
323
|
+
return color || "#000";
|
|
324
|
+
})();
|
|
325
|
+
const step = barWidth + barGap;
|
|
326
|
+
const barCount = Math.floor(rect.width / step);
|
|
327
|
+
const centerY = rect.height / 2;
|
|
328
|
+
if (mode === "static") {
|
|
329
|
+
const dataToRender = processing ? staticBarsRef.current : active ? staticBarsRef.current : staticBarsRef.current.length > 0 ? staticBarsRef.current : [];
|
|
330
|
+
for (let i = 0; i < barCount && i < dataToRender.length; i++) {
|
|
331
|
+
const value = dataToRender[i] || 0.1;
|
|
332
|
+
const x = i * step;
|
|
333
|
+
const barHeight = Math.max(baseBarHeight, value * rect.height * 0.8);
|
|
334
|
+
const y = centerY - barHeight / 2;
|
|
335
|
+
ctx.fillStyle = computedBarColor;
|
|
336
|
+
ctx.globalAlpha = 0.4 + value * 0.6;
|
|
337
|
+
if (barRadius > 0) {
|
|
338
|
+
ctx.beginPath();
|
|
339
|
+
ctx.roundRect(x, y, barWidth, barHeight, barRadius);
|
|
340
|
+
ctx.fill();
|
|
341
|
+
} else {
|
|
342
|
+
ctx.fillRect(x, y, barWidth, barHeight);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
} else {
|
|
346
|
+
for (let i = 0; i < barCount && i < historyRef.current.length; i++) {
|
|
347
|
+
const dataIndex = historyRef.current.length - 1 - i;
|
|
348
|
+
const value = historyRef.current[dataIndex] || 0.1;
|
|
349
|
+
const x = rect.width - (i + 1) * step;
|
|
350
|
+
const barHeight = Math.max(baseBarHeight, value * rect.height * 0.8);
|
|
351
|
+
const y = centerY - barHeight / 2;
|
|
352
|
+
ctx.fillStyle = computedBarColor;
|
|
353
|
+
ctx.globalAlpha = 0.4 + value * 0.6;
|
|
354
|
+
if (barRadius > 0) {
|
|
355
|
+
ctx.beginPath();
|
|
356
|
+
ctx.roundRect(x, y, barWidth, barHeight, barRadius);
|
|
357
|
+
ctx.fill();
|
|
358
|
+
} else {
|
|
359
|
+
ctx.fillRect(x, y, barWidth, barHeight);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (fadeEdges && fadeWidth > 0 && rect.width > 0) {
|
|
364
|
+
if (!gradientCacheRef.current || lastWidthRef.current !== rect.width) {
|
|
365
|
+
const gradient = ctx.createLinearGradient(0, 0, rect.width, 0);
|
|
366
|
+
const fadePercent = Math.min(0.3, fadeWidth / rect.width);
|
|
367
|
+
gradient.addColorStop(0, "rgba(255,255,255,1)");
|
|
368
|
+
gradient.addColorStop(fadePercent, "rgba(255,255,255,0)");
|
|
369
|
+
gradient.addColorStop(1 - fadePercent, "rgba(255,255,255,0)");
|
|
370
|
+
gradient.addColorStop(1, "rgba(255,255,255,1)");
|
|
371
|
+
gradientCacheRef.current = gradient;
|
|
372
|
+
lastWidthRef.current = rect.width;
|
|
373
|
+
}
|
|
374
|
+
ctx.globalCompositeOperation = "destination-out";
|
|
375
|
+
ctx.fillStyle = gradientCacheRef.current;
|
|
376
|
+
ctx.fillRect(0, 0, rect.width, rect.height);
|
|
377
|
+
ctx.globalCompositeOperation = "source-over";
|
|
378
|
+
}
|
|
379
|
+
ctx.globalAlpha = 1;
|
|
380
|
+
rafId = requestAnimationFrame(animate);
|
|
381
|
+
};
|
|
382
|
+
rafId = requestAnimationFrame(animate);
|
|
383
|
+
return () => {
|
|
384
|
+
if (rafId) {
|
|
385
|
+
cancelAnimationFrame(rafId);
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
}, [
|
|
389
|
+
active,
|
|
390
|
+
processing,
|
|
391
|
+
sensitivity,
|
|
392
|
+
updateRate,
|
|
393
|
+
historySize,
|
|
394
|
+
barWidth,
|
|
395
|
+
baseBarHeight,
|
|
396
|
+
barGap,
|
|
397
|
+
barRadius,
|
|
398
|
+
barColor,
|
|
399
|
+
fadeEdges,
|
|
400
|
+
fadeWidth,
|
|
401
|
+
mode
|
|
402
|
+
]);
|
|
403
|
+
return /* @__PURE__ */ jsxs(
|
|
404
|
+
"div",
|
|
405
|
+
{
|
|
406
|
+
className: cn("relative h-full w-full", className),
|
|
407
|
+
ref: containerRef,
|
|
408
|
+
style: { height: heightStyle },
|
|
409
|
+
"aria-label": active ? "Live audio waveform" : processing ? "Processing audio" : "Audio waveform idle",
|
|
410
|
+
role: "img",
|
|
411
|
+
...props,
|
|
412
|
+
children: [
|
|
413
|
+
!active && !processing && /* @__PURE__ */ jsx("div", { className: "absolute top-1/2 right-0 left-0 -translate-y-1/2 border-t-2 border-dotted border-muted-foreground/20" }),
|
|
414
|
+
/* @__PURE__ */ jsx(
|
|
415
|
+
"canvas",
|
|
416
|
+
{
|
|
417
|
+
className: "block h-full w-full",
|
|
418
|
+
ref: canvasRef,
|
|
419
|
+
"aria-hidden": "true"
|
|
420
|
+
}
|
|
421
|
+
)
|
|
422
|
+
]
|
|
423
|
+
}
|
|
424
|
+
);
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
// src/hooks/use-recorder-state.ts
|
|
428
|
+
import { useReducer } from "react";
|
|
429
|
+
function recorderReducer(state, action) {
|
|
430
|
+
switch (action.type) {
|
|
431
|
+
case "START_RECORDING":
|
|
432
|
+
return { status: "recording", stepCount: 0, startTime: Date.now() };
|
|
433
|
+
case "STEP_CAPTURED":
|
|
434
|
+
if (state.status === "recording") {
|
|
435
|
+
return { ...state, stepCount: state.stepCount + 1 };
|
|
436
|
+
}
|
|
437
|
+
return state;
|
|
438
|
+
case "PAUSE":
|
|
439
|
+
if (state.status === "recording") {
|
|
440
|
+
return { status: "paused", stepCount: state.stepCount };
|
|
441
|
+
}
|
|
442
|
+
return state;
|
|
443
|
+
case "RESUME":
|
|
444
|
+
if (state.status === "paused") {
|
|
445
|
+
return {
|
|
446
|
+
status: "recording",
|
|
447
|
+
stepCount: state.stepCount,
|
|
448
|
+
startTime: Date.now()
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
return state;
|
|
452
|
+
case "SAVE":
|
|
453
|
+
if (state.status === "paused") {
|
|
454
|
+
return { status: "saving" };
|
|
455
|
+
}
|
|
456
|
+
return state;
|
|
457
|
+
case "SAVE_COMPLETE":
|
|
458
|
+
return { status: "idle" };
|
|
459
|
+
default:
|
|
460
|
+
return state;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
var initialState = { status: "idle" };
|
|
464
|
+
function useRecorderState() {
|
|
465
|
+
return useReducer(recorderReducer, initialState);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// src/hooks/use-audio-recorder.ts
|
|
469
|
+
import { useRef as useRef2, useState, useCallback } from "react";
|
|
470
|
+
function useAudioRecorder() {
|
|
471
|
+
const mediaRecorderRef = useRef2(null);
|
|
472
|
+
const chunksRef = useRef2([]);
|
|
473
|
+
const [audioBlob, setAudioBlob] = useState(null);
|
|
474
|
+
const start = useCallback((stream) => {
|
|
475
|
+
chunksRef.current = [];
|
|
476
|
+
setAudioBlob(null);
|
|
477
|
+
const recorder = new MediaRecorder(stream);
|
|
478
|
+
recorder.ondataavailable = (e) => {
|
|
479
|
+
if (e.data.size > 0) {
|
|
480
|
+
chunksRef.current.push(e.data);
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
recorder.onstop = () => {
|
|
484
|
+
const blob = new Blob(chunksRef.current, { type: "audio/webm" });
|
|
485
|
+
setAudioBlob(blob);
|
|
486
|
+
};
|
|
487
|
+
recorder.start();
|
|
488
|
+
mediaRecorderRef.current = recorder;
|
|
489
|
+
}, []);
|
|
490
|
+
const stop = useCallback(() => {
|
|
491
|
+
if (mediaRecorderRef.current && mediaRecorderRef.current.state !== "inactive") {
|
|
492
|
+
mediaRecorderRef.current.stop();
|
|
493
|
+
}
|
|
494
|
+
}, []);
|
|
495
|
+
const pause = useCallback(() => {
|
|
496
|
+
if (mediaRecorderRef.current && mediaRecorderRef.current.state === "recording") {
|
|
497
|
+
mediaRecorderRef.current.pause();
|
|
498
|
+
}
|
|
499
|
+
}, []);
|
|
500
|
+
const resume = useCallback(() => {
|
|
501
|
+
if (mediaRecorderRef.current && mediaRecorderRef.current.state === "paused") {
|
|
502
|
+
mediaRecorderRef.current.resume();
|
|
503
|
+
}
|
|
504
|
+
}, []);
|
|
505
|
+
return { start, stop, pause, resume, audioBlob };
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// src/hooks/use-click-capture.ts
|
|
509
|
+
import { useEffect as useEffect2, useCallback as useCallback2 } from "react";
|
|
510
|
+
function getStableElementId(el) {
|
|
511
|
+
const meridialId = el.getAttribute("data-meridial-id");
|
|
512
|
+
if (meridialId) return `[data-meridial-id="${meridialId}"]`;
|
|
513
|
+
if (el.id) return `#${el.id}`;
|
|
514
|
+
return getCssPath(el);
|
|
515
|
+
}
|
|
516
|
+
function getCssPath(el) {
|
|
517
|
+
const parts = [];
|
|
518
|
+
let current = el;
|
|
519
|
+
while (current && current !== document.body) {
|
|
520
|
+
let selector = current.tagName.toLowerCase();
|
|
521
|
+
if (current.id) {
|
|
522
|
+
parts.unshift(`#${current.id}`);
|
|
523
|
+
break;
|
|
524
|
+
}
|
|
525
|
+
const parent = current.parentElement;
|
|
526
|
+
if (parent) {
|
|
527
|
+
const siblings = Array.from(parent.children).filter(
|
|
528
|
+
(child) => child.tagName === current.tagName
|
|
529
|
+
);
|
|
530
|
+
if (siblings.length > 1) {
|
|
531
|
+
const index = siblings.indexOf(current) + 1;
|
|
532
|
+
selector += `:nth-of-type(${index})`;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
parts.unshift(selector);
|
|
536
|
+
current = current.parentElement;
|
|
537
|
+
}
|
|
538
|
+
return parts.join(" > ");
|
|
539
|
+
}
|
|
540
|
+
function getElementLabel(el) {
|
|
541
|
+
const ariaLabel = el.getAttribute("aria-label");
|
|
542
|
+
if (ariaLabel) return ariaLabel;
|
|
543
|
+
const placeholder = el.getAttribute("placeholder");
|
|
544
|
+
if (placeholder) return placeholder;
|
|
545
|
+
const text = el.innerText?.trim();
|
|
546
|
+
if (text && text.length <= 50) return text;
|
|
547
|
+
const tag = el.tagName.toLowerCase();
|
|
548
|
+
return el.id ? `${tag}#${el.id}` : tag;
|
|
549
|
+
}
|
|
550
|
+
function useClickCapture({
|
|
551
|
+
enabled,
|
|
552
|
+
onCapture
|
|
553
|
+
}) {
|
|
554
|
+
const handleClick = useCallback2(
|
|
555
|
+
(e) => {
|
|
556
|
+
const target = e.target;
|
|
557
|
+
if (!target || target.closest("[data-meridial-ui]")) return;
|
|
558
|
+
const el = target.closest("[data-meridial-id]") ?? target;
|
|
559
|
+
const elementId = getStableElementId(el);
|
|
560
|
+
const elementLabel = getElementLabel(el);
|
|
561
|
+
const urlPath = window.location.pathname + window.location.search;
|
|
562
|
+
onCapture({ elementId, elementLabel, urlPath, description: "" });
|
|
563
|
+
},
|
|
564
|
+
[onCapture]
|
|
565
|
+
);
|
|
566
|
+
useEffect2(() => {
|
|
567
|
+
if (!enabled) return;
|
|
568
|
+
document.addEventListener("click", handleClick, true);
|
|
569
|
+
return () => {
|
|
570
|
+
document.removeEventListener("click", handleClick, true);
|
|
571
|
+
};
|
|
572
|
+
}, [enabled, handleClick]);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// src/hooks/use-workflows.ts
|
|
576
|
+
import { useState as useState2, useCallback as useCallback3, useMemo } from "react";
|
|
577
|
+
|
|
578
|
+
// src/storage.ts
|
|
579
|
+
import { z } from "zod";
|
|
580
|
+
var workflowsArraySchema = z.array(workflowSchema);
|
|
581
|
+
var isBrowser = typeof window !== "undefined";
|
|
582
|
+
function loadWorkflows() {
|
|
583
|
+
if (!isBrowser) return [];
|
|
584
|
+
try {
|
|
585
|
+
const raw = localStorage.getItem(STORAGE_KEYS.workflows);
|
|
586
|
+
if (!raw) return [];
|
|
587
|
+
const parsed = JSON.parse(raw);
|
|
588
|
+
const result = workflowsArraySchema.safeParse(parsed);
|
|
589
|
+
if (!result.success) {
|
|
590
|
+
if (Array.isArray(parsed)) {
|
|
591
|
+
const valid = parsed.filter(
|
|
592
|
+
(item) => workflowSchema.safeParse(item).success
|
|
593
|
+
);
|
|
594
|
+
const validWorkflows = valid.map((item) => workflowSchema.parse(item));
|
|
595
|
+
saveWorkflows(validWorkflows);
|
|
596
|
+
return validWorkflows;
|
|
597
|
+
}
|
|
598
|
+
localStorage.removeItem(STORAGE_KEYS.workflows);
|
|
599
|
+
return [];
|
|
600
|
+
}
|
|
601
|
+
return result.data;
|
|
602
|
+
} catch {
|
|
603
|
+
localStorage.removeItem(STORAGE_KEYS.workflows);
|
|
604
|
+
return [];
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
function saveWorkflows(workflows) {
|
|
608
|
+
if (!isBrowser) return;
|
|
609
|
+
localStorage.setItem(STORAGE_KEYS.workflows, JSON.stringify(workflows));
|
|
610
|
+
}
|
|
611
|
+
function addWorkflow(workflow) {
|
|
612
|
+
const workflows = loadWorkflows();
|
|
613
|
+
workflows.push(workflow);
|
|
614
|
+
saveWorkflows(workflows);
|
|
615
|
+
}
|
|
616
|
+
function updateWorkflow(id, updates) {
|
|
617
|
+
const workflows = loadWorkflows();
|
|
618
|
+
const index = workflows.findIndex((w) => w.id === id);
|
|
619
|
+
if (index !== -1) {
|
|
620
|
+
workflows[index] = { ...workflows[index], ...updates };
|
|
621
|
+
saveWorkflows(workflows);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
function deleteWorkflow(id) {
|
|
625
|
+
const workflows = loadWorkflows().filter((w) => w.id !== id);
|
|
626
|
+
saveWorkflows(workflows);
|
|
627
|
+
}
|
|
628
|
+
function getWorkflow(id) {
|
|
629
|
+
return loadWorkflows().find((w) => w.id === id);
|
|
630
|
+
}
|
|
631
|
+
var DB_NAME = "meridial";
|
|
632
|
+
var AUDIO_STORE = "audio";
|
|
633
|
+
var DB_VERSION = 1;
|
|
634
|
+
function openDB() {
|
|
635
|
+
return new Promise((resolve, reject) => {
|
|
636
|
+
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
637
|
+
request.onupgradeneeded = () => {
|
|
638
|
+
const db = request.result;
|
|
639
|
+
if (!db.objectStoreNames.contains(AUDIO_STORE)) {
|
|
640
|
+
db.createObjectStore(AUDIO_STORE);
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
request.onsuccess = () => resolve(request.result);
|
|
644
|
+
request.onerror = () => reject(request.error);
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
async function saveAudio(key, blob) {
|
|
648
|
+
const db = await openDB();
|
|
649
|
+
return new Promise((resolve, reject) => {
|
|
650
|
+
const tx = db.transaction(AUDIO_STORE, "readwrite");
|
|
651
|
+
tx.objectStore(AUDIO_STORE).put(blob, key);
|
|
652
|
+
tx.oncomplete = () => resolve();
|
|
653
|
+
tx.onerror = () => reject(tx.error);
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
async function loadAudio(key) {
|
|
657
|
+
const db = await openDB();
|
|
658
|
+
return new Promise((resolve, reject) => {
|
|
659
|
+
const tx = db.transaction(AUDIO_STORE, "readonly");
|
|
660
|
+
const request = tx.objectStore(AUDIO_STORE).get(key);
|
|
661
|
+
request.onsuccess = () => resolve(request.result);
|
|
662
|
+
request.onerror = () => reject(request.error);
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
async function deleteAudio(key) {
|
|
666
|
+
const db = await openDB();
|
|
667
|
+
return new Promise((resolve, reject) => {
|
|
668
|
+
const tx = db.transaction(AUDIO_STORE, "readwrite");
|
|
669
|
+
tx.objectStore(AUDIO_STORE).delete(key);
|
|
670
|
+
tx.oncomplete = () => resolve();
|
|
671
|
+
tx.onerror = () => reject(tx.error);
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// src/hooks/use-workflows.ts
|
|
676
|
+
var PAGE_SIZE = 5;
|
|
677
|
+
function useWorkflows() {
|
|
678
|
+
const [workflows, setWorkflows] = useState2(
|
|
679
|
+
() => loadWorkflows()
|
|
680
|
+
);
|
|
681
|
+
const [visibleCount, setVisibleCount] = useState2(PAGE_SIZE);
|
|
682
|
+
const sorted = useMemo(() => {
|
|
683
|
+
const unconfigured = workflows.filter((w) => !w.configured);
|
|
684
|
+
const configured = workflows.filter((w) => w.configured);
|
|
685
|
+
return [...unconfigured, ...configured];
|
|
686
|
+
}, [workflows]);
|
|
687
|
+
const hasUnconfigured = useMemo(
|
|
688
|
+
() => workflows.some((w) => !w.configured),
|
|
689
|
+
[workflows]
|
|
690
|
+
);
|
|
691
|
+
const visibleWorkflows = useMemo(
|
|
692
|
+
() => sorted.slice(0, visibleCount),
|
|
693
|
+
[sorted, visibleCount]
|
|
694
|
+
);
|
|
695
|
+
const hasMore = sorted.length > visibleCount;
|
|
696
|
+
const remainingCount = sorted.length - visibleCount;
|
|
697
|
+
const loadMore = useCallback3(() => {
|
|
698
|
+
setVisibleCount((c) => c + PAGE_SIZE);
|
|
699
|
+
}, []);
|
|
700
|
+
const refresh = useCallback3(() => {
|
|
701
|
+
setWorkflows(loadWorkflows());
|
|
702
|
+
}, []);
|
|
703
|
+
const addWorkflow2 = useCallback3((workflow) => {
|
|
704
|
+
addWorkflow(workflow);
|
|
705
|
+
setWorkflows(loadWorkflows());
|
|
706
|
+
}, []);
|
|
707
|
+
const updateWorkflow2 = useCallback3(
|
|
708
|
+
(id, updates) => {
|
|
709
|
+
updateWorkflow(id, updates);
|
|
710
|
+
setWorkflows(loadWorkflows());
|
|
711
|
+
},
|
|
712
|
+
[]
|
|
713
|
+
);
|
|
714
|
+
const deleteWorkflow2 = useCallback3((id) => {
|
|
715
|
+
deleteWorkflow(id);
|
|
716
|
+
setWorkflows(loadWorkflows());
|
|
717
|
+
}, []);
|
|
718
|
+
const getWorkflow2 = useCallback3((id) => {
|
|
719
|
+
return getWorkflow(id);
|
|
720
|
+
}, []);
|
|
721
|
+
return {
|
|
722
|
+
workflows,
|
|
723
|
+
hasUnconfigured,
|
|
724
|
+
visibleWorkflows,
|
|
725
|
+
loadMore,
|
|
726
|
+
hasMore,
|
|
727
|
+
remainingCount,
|
|
728
|
+
addWorkflow: addWorkflow2,
|
|
729
|
+
updateWorkflow: updateWorkflow2,
|
|
730
|
+
deleteWorkflow: deleteWorkflow2,
|
|
731
|
+
getWorkflow: getWorkflow2,
|
|
732
|
+
refresh
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// src/components/action-button.tsx
|
|
737
|
+
import { HugeiconsIcon } from "@hugeicons/react";
|
|
738
|
+
import {
|
|
739
|
+
ArrowDown01Icon,
|
|
740
|
+
Alert01Icon,
|
|
741
|
+
StopIcon,
|
|
742
|
+
PlayIcon,
|
|
743
|
+
Loading03Icon
|
|
744
|
+
} from "@hugeicons/core-free-icons";
|
|
745
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
746
|
+
function ActionButton({
|
|
747
|
+
state,
|
|
748
|
+
openPanel,
|
|
749
|
+
hasUnconfigured,
|
|
750
|
+
onClick
|
|
751
|
+
}) {
|
|
752
|
+
let icon = ArrowDown01Icon;
|
|
753
|
+
let className = "text-muted-foreground";
|
|
754
|
+
let spin = false;
|
|
755
|
+
if (state.status === "idle") {
|
|
756
|
+
if (hasUnconfigured) {
|
|
757
|
+
icon = Alert01Icon;
|
|
758
|
+
className = "text-amber-500 fill-amber-500/20";
|
|
759
|
+
} else {
|
|
760
|
+
icon = ArrowDown01Icon;
|
|
761
|
+
className = cn(
|
|
762
|
+
"fill-none text-muted-foreground transition-transform duration-200",
|
|
763
|
+
openPanel ? "rotate-180" : "rotate-0"
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
} else if (state.status === "recording") {
|
|
767
|
+
icon = StopIcon;
|
|
768
|
+
className = "text-red-500 fill-destructive/50";
|
|
769
|
+
} else if (state.status === "paused") {
|
|
770
|
+
icon = PlayIcon;
|
|
771
|
+
className = "text-emerald-500 fill-emerald-500/20";
|
|
772
|
+
} else if (state.status === "saving") {
|
|
773
|
+
icon = Loading03Icon;
|
|
774
|
+
className = "text-muted-foreground";
|
|
775
|
+
spin = true;
|
|
776
|
+
}
|
|
777
|
+
return /* @__PURE__ */ jsx2(
|
|
778
|
+
"button",
|
|
779
|
+
{
|
|
780
|
+
"data-meridial-ui": true,
|
|
781
|
+
onClick,
|
|
782
|
+
disabled: state.status === "saving",
|
|
783
|
+
className: cn("flex items-center justify-center px-2.5", className, {
|
|
784
|
+
"cursor-not-allowed": state.status === "saving",
|
|
785
|
+
"cursor-pointer hover:opacity-80 disabled:cursor-not-allowed disabled:opacity-50": state.status !== "saving"
|
|
786
|
+
}),
|
|
787
|
+
children: /* @__PURE__ */ jsx2(
|
|
788
|
+
HugeiconsIcon,
|
|
789
|
+
{
|
|
790
|
+
icon,
|
|
791
|
+
size: 18,
|
|
792
|
+
className: spin ? "animate-spin" : "",
|
|
793
|
+
fill: className
|
|
794
|
+
}
|
|
795
|
+
)
|
|
796
|
+
}
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// src/components/step-badge.tsx
|
|
801
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
802
|
+
function StepBadge({ count }) {
|
|
803
|
+
return /* @__PURE__ */ jsx3(
|
|
804
|
+
"div",
|
|
805
|
+
{
|
|
806
|
+
"data-meridial-ui": true,
|
|
807
|
+
className: "absolute right-1 bottom-1 flex h-5 min-w-5 items-center justify-center rounded-full bg-foreground px-1 text-xs font-medium text-background",
|
|
808
|
+
children: count
|
|
809
|
+
}
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// src/components/workflow-row.tsx
|
|
814
|
+
import { useState as useState3, useRef as useRef3, useEffect as useEffect3 } from "react";
|
|
815
|
+
import { HugeiconsIcon as HugeiconsIcon2 } from "@hugeicons/react";
|
|
816
|
+
import {
|
|
817
|
+
Alert01Icon as Alert01Icon2,
|
|
818
|
+
CheckmarkCircle02Icon,
|
|
819
|
+
Delete01Icon,
|
|
820
|
+
Settings01Icon
|
|
821
|
+
} from "@hugeicons/core-free-icons";
|
|
822
|
+
import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
823
|
+
function WorkflowRow({
|
|
824
|
+
workflow,
|
|
825
|
+
onRename,
|
|
826
|
+
onDelete,
|
|
827
|
+
onConfigure
|
|
828
|
+
}) {
|
|
829
|
+
const [editing, setEditing] = useState3(false);
|
|
830
|
+
const [name, setName] = useState3(workflow.name);
|
|
831
|
+
const inputRef = useRef3(null);
|
|
832
|
+
useEffect3(() => {
|
|
833
|
+
if (editing && inputRef.current) {
|
|
834
|
+
inputRef.current.focus();
|
|
835
|
+
inputRef.current.select();
|
|
836
|
+
}
|
|
837
|
+
}, [editing]);
|
|
838
|
+
const commitRename = () => {
|
|
839
|
+
const trimmed = name.trim();
|
|
840
|
+
if (trimmed && trimmed !== workflow.name) {
|
|
841
|
+
onRename(workflow.id, trimmed);
|
|
842
|
+
} else {
|
|
843
|
+
setName(workflow.name);
|
|
844
|
+
}
|
|
845
|
+
setEditing(false);
|
|
846
|
+
};
|
|
847
|
+
return /* @__PURE__ */ jsxs2(
|
|
848
|
+
"div",
|
|
849
|
+
{
|
|
850
|
+
"data-meridial-ui": true,
|
|
851
|
+
className: "flex items-center gap-2 px-3 py-2 hover:bg-muted/50",
|
|
852
|
+
children: [
|
|
853
|
+
/* @__PURE__ */ jsx4(
|
|
854
|
+
HugeiconsIcon2,
|
|
855
|
+
{
|
|
856
|
+
icon: workflow.configured ? CheckmarkCircle02Icon : Alert01Icon2,
|
|
857
|
+
size: 16,
|
|
858
|
+
className: workflow.configured ? "shrink-0 fill-green-500/20 text-green-500" : "shrink-0 fill-amber-500/20 text-amber-500"
|
|
859
|
+
}
|
|
860
|
+
),
|
|
861
|
+
editing ? /* @__PURE__ */ jsx4(
|
|
862
|
+
"input",
|
|
863
|
+
{
|
|
864
|
+
ref: inputRef,
|
|
865
|
+
"data-meridial-ui": true,
|
|
866
|
+
value: name,
|
|
867
|
+
onChange: (e) => setName(e.target.value),
|
|
868
|
+
onBlur: commitRename,
|
|
869
|
+
onKeyDown: (e) => {
|
|
870
|
+
if (e.key === "Enter") commitRename();
|
|
871
|
+
if (e.key === "Escape") {
|
|
872
|
+
setName(workflow.name);
|
|
873
|
+
setEditing(false);
|
|
874
|
+
}
|
|
875
|
+
},
|
|
876
|
+
className: "min-w-0 flex-1 rounded border border-border bg-background px-1.5 py-0.5 text-sm outline-none"
|
|
877
|
+
}
|
|
878
|
+
) : /* @__PURE__ */ jsx4(
|
|
879
|
+
"button",
|
|
880
|
+
{
|
|
881
|
+
"data-meridial-ui": true,
|
|
882
|
+
onClick: () => {
|
|
883
|
+
if (workflow.configured) return;
|
|
884
|
+
setEditing(true);
|
|
885
|
+
},
|
|
886
|
+
className: "min-w-0 flex-1 cursor-pointer truncate text-left text-sm",
|
|
887
|
+
children: workflow.name
|
|
888
|
+
}
|
|
889
|
+
),
|
|
890
|
+
!workflow.configured && /* @__PURE__ */ jsx4(
|
|
891
|
+
"button",
|
|
892
|
+
{
|
|
893
|
+
"data-meridial-ui": true,
|
|
894
|
+
onClick: () => onDelete(workflow.id),
|
|
895
|
+
className: "shrink-0 cursor-pointer p-0.5 text-muted-foreground hover:text-destructive",
|
|
896
|
+
children: /* @__PURE__ */ jsx4(HugeiconsIcon2, { icon: Delete01Icon, size: 14 })
|
|
897
|
+
}
|
|
898
|
+
),
|
|
899
|
+
/* @__PURE__ */ jsx4(
|
|
900
|
+
"button",
|
|
901
|
+
{
|
|
902
|
+
"data-meridial-ui": true,
|
|
903
|
+
onClick: () => onConfigure(workflow.id),
|
|
904
|
+
className: cn(
|
|
905
|
+
"shrink-0 cursor-pointer p-0.5",
|
|
906
|
+
workflow.configured ? "text-muted-foreground hover:text-foreground" : "text-amber-500 hover:text-amber-500/80"
|
|
907
|
+
),
|
|
908
|
+
children: /* @__PURE__ */ jsx4(HugeiconsIcon2, { icon: Settings01Icon, size: 14 })
|
|
909
|
+
}
|
|
910
|
+
)
|
|
911
|
+
]
|
|
912
|
+
}
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// src/components/workflow-list-panel.tsx
|
|
917
|
+
import { Fragment, jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
918
|
+
function WorkflowListPanel({
|
|
919
|
+
workflows,
|
|
920
|
+
hasMore,
|
|
921
|
+
remainingCount,
|
|
922
|
+
onLoadMore,
|
|
923
|
+
onRename,
|
|
924
|
+
onDelete,
|
|
925
|
+
onConfigure
|
|
926
|
+
}) {
|
|
927
|
+
return /* @__PURE__ */ jsx5("div", { "data-meridial-ui": true, className: "max-h-60 overflow-y-auto border-b bg-card", children: workflows.length === 0 ? /* @__PURE__ */ jsx5("p", { className: "px-3 py-4 text-center text-sm text-muted-foreground", children: "No workflow recorded" }) : /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
928
|
+
workflows.map((w) => /* @__PURE__ */ jsx5(
|
|
929
|
+
WorkflowRow,
|
|
930
|
+
{
|
|
931
|
+
workflow: w,
|
|
932
|
+
onRename,
|
|
933
|
+
onDelete,
|
|
934
|
+
onConfigure
|
|
935
|
+
},
|
|
936
|
+
w.id
|
|
937
|
+
)),
|
|
938
|
+
hasMore && /* @__PURE__ */ jsxs3(
|
|
939
|
+
"button",
|
|
940
|
+
{
|
|
941
|
+
"data-meridial-ui": true,
|
|
942
|
+
onClick: onLoadMore,
|
|
943
|
+
className: "w-full cursor-pointer px-3 py-2 text-center text-sm text-muted-foreground hover:bg-muted/50 hover:text-foreground",
|
|
944
|
+
children: [
|
|
945
|
+
remainingCount,
|
|
946
|
+
" More\u2026"
|
|
947
|
+
]
|
|
948
|
+
}
|
|
949
|
+
)
|
|
950
|
+
] }) });
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// src/workflow-editor.tsx
|
|
954
|
+
import { useRef as useRef6, useState as useState6, useCallback as useCallback5 } from "react";
|
|
955
|
+
import {
|
|
956
|
+
MultiplicationSignIcon,
|
|
957
|
+
Cursor02Icon,
|
|
958
|
+
ArrowLeft01Icon,
|
|
959
|
+
ArrowRight01Icon,
|
|
960
|
+
Loading03Icon as Loading03Icon2,
|
|
961
|
+
Tick01Icon
|
|
962
|
+
} from "@hugeicons/core-free-icons";
|
|
963
|
+
import { HugeiconsIcon as HugeiconsIcon4 } from "@hugeicons/react";
|
|
964
|
+
import { createPortal as createPortal2 } from "react-dom";
|
|
965
|
+
import { useDraggable } from "@neodrag/react";
|
|
966
|
+
|
|
967
|
+
// src/hooks/use-position.ts
|
|
968
|
+
import { useState as useState4, useCallback as useCallback4 } from "react";
|
|
969
|
+
function usePosition(storageKey, defaultPos = { x: 0, y: 0 }) {
|
|
970
|
+
const [position] = useState4(() => {
|
|
971
|
+
if (typeof window === "undefined") return defaultPos;
|
|
972
|
+
try {
|
|
973
|
+
const raw = localStorage.getItem(storageKey);
|
|
974
|
+
if (raw) {
|
|
975
|
+
const parsed = JSON.parse(raw);
|
|
976
|
+
if (typeof parsed.x === "number" && typeof parsed.y === "number") {
|
|
977
|
+
return parsed;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
} catch {
|
|
981
|
+
}
|
|
982
|
+
return defaultPos;
|
|
983
|
+
});
|
|
984
|
+
const updatePosition = useCallback4(
|
|
985
|
+
(pos) => {
|
|
986
|
+
try {
|
|
987
|
+
localStorage.setItem(storageKey, JSON.stringify(pos));
|
|
988
|
+
} catch {
|
|
989
|
+
}
|
|
990
|
+
},
|
|
991
|
+
[storageKey]
|
|
992
|
+
);
|
|
993
|
+
return { position, updatePosition };
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// src/components/cursor-guide.tsx
|
|
997
|
+
import { useRef as useRef4 } from "react";
|
|
998
|
+
import { createPortal } from "react-dom";
|
|
999
|
+
import {
|
|
1000
|
+
motion,
|
|
1001
|
+
useMotionValue,
|
|
1002
|
+
useSpring,
|
|
1003
|
+
AnimatePresence
|
|
1004
|
+
} from "motion/react";
|
|
1005
|
+
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
1006
|
+
var SPRING_CONFIG = { stiffness: 120, damping: 20, mass: 0.8 };
|
|
1007
|
+
var OFFSET = -16;
|
|
1008
|
+
function CursorGuide({
|
|
1009
|
+
selector,
|
|
1010
|
+
urlPath,
|
|
1011
|
+
children,
|
|
1012
|
+
onError,
|
|
1013
|
+
onClick
|
|
1014
|
+
}) {
|
|
1015
|
+
const { rect } = useElementTracker({
|
|
1016
|
+
selector,
|
|
1017
|
+
urlPath,
|
|
1018
|
+
onError: onError ? (msg) => {
|
|
1019
|
+
if (msg !== null) onError(msg);
|
|
1020
|
+
} : void 0,
|
|
1021
|
+
onClick
|
|
1022
|
+
});
|
|
1023
|
+
const hasAnimated = useRef4(false);
|
|
1024
|
+
const springX = useSpring(useMotionValue(0), SPRING_CONFIG);
|
|
1025
|
+
const springY = useSpring(useMotionValue(0), SPRING_CONFIG);
|
|
1026
|
+
if (rect) {
|
|
1027
|
+
const targetX = rect.right + OFFSET;
|
|
1028
|
+
const targetY = rect.bottom + OFFSET;
|
|
1029
|
+
if (!hasAnimated.current) {
|
|
1030
|
+
springX.jump(targetX);
|
|
1031
|
+
springY.jump(targetY);
|
|
1032
|
+
hasAnimated.current = true;
|
|
1033
|
+
} else {
|
|
1034
|
+
springX.set(targetX);
|
|
1035
|
+
springY.set(targetY);
|
|
1036
|
+
}
|
|
1037
|
+
} else {
|
|
1038
|
+
hasAnimated.current = false;
|
|
1039
|
+
}
|
|
1040
|
+
return createPortal(
|
|
1041
|
+
/* @__PURE__ */ jsx6(AnimatePresence, { children: rect && /* @__PURE__ */ jsx6(
|
|
1042
|
+
motion.div,
|
|
1043
|
+
{
|
|
1044
|
+
"data-meridial-ui": true,
|
|
1045
|
+
className: "pointer-events-none fixed z-[99999]",
|
|
1046
|
+
style: { top: springY, left: springX },
|
|
1047
|
+
initial: { opacity: 0, scale: 0.6 },
|
|
1048
|
+
animate: { opacity: 1, scale: 1 },
|
|
1049
|
+
exit: { opacity: 0, scale: 0.6 },
|
|
1050
|
+
transition: { duration: 0.2 },
|
|
1051
|
+
children
|
|
1052
|
+
}
|
|
1053
|
+
) }),
|
|
1054
|
+
document.body
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// src/components/delete-step-button.tsx
|
|
1059
|
+
import { useRef as useRef5, useState as useState5 } from "react";
|
|
1060
|
+
import { motion as motion2, useAnimation } from "motion/react";
|
|
1061
|
+
import { HugeiconsIcon as HugeiconsIcon3 } from "@hugeicons/react";
|
|
1062
|
+
import { Delete01Icon as Delete01Icon2 } from "@hugeicons/core-free-icons";
|
|
1063
|
+
import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1064
|
+
function DeleteStepButton({
|
|
1065
|
+
holdDuration = 3e3,
|
|
1066
|
+
onConfirm
|
|
1067
|
+
}) {
|
|
1068
|
+
const [isHolding, setIsHolding] = useState5(false);
|
|
1069
|
+
const controls = useAnimation();
|
|
1070
|
+
const holdingRef = useRef5(false);
|
|
1071
|
+
async function handleHoldStart() {
|
|
1072
|
+
holdingRef.current = true;
|
|
1073
|
+
setIsHolding(true);
|
|
1074
|
+
controls.set({ width: "5%" });
|
|
1075
|
+
await controls.start({
|
|
1076
|
+
width: "100%",
|
|
1077
|
+
transition: {
|
|
1078
|
+
duration: holdDuration / 1e3,
|
|
1079
|
+
ease: "linear"
|
|
1080
|
+
}
|
|
1081
|
+
});
|
|
1082
|
+
if (holdingRef.current) {
|
|
1083
|
+
holdingRef.current = false;
|
|
1084
|
+
setIsHolding(false);
|
|
1085
|
+
onConfirm();
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
function handleHoldEnd() {
|
|
1089
|
+
holdingRef.current = false;
|
|
1090
|
+
setIsHolding(false);
|
|
1091
|
+
controls.stop();
|
|
1092
|
+
controls.start({
|
|
1093
|
+
width: "0%",
|
|
1094
|
+
transition: { duration: 0.1 }
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
return /* @__PURE__ */ jsxs4(
|
|
1098
|
+
Button,
|
|
1099
|
+
{
|
|
1100
|
+
type: "button",
|
|
1101
|
+
onMouseDown: handleHoldStart,
|
|
1102
|
+
onMouseLeave: handleHoldEnd,
|
|
1103
|
+
onMouseUp: handleHoldEnd,
|
|
1104
|
+
onTouchCancel: handleHoldEnd,
|
|
1105
|
+
onTouchEnd: handleHoldEnd,
|
|
1106
|
+
onTouchStart: handleHoldStart,
|
|
1107
|
+
variant: "destructive",
|
|
1108
|
+
className: "relative w-42",
|
|
1109
|
+
children: [
|
|
1110
|
+
/* @__PURE__ */ jsx7(
|
|
1111
|
+
motion2.div,
|
|
1112
|
+
{
|
|
1113
|
+
animate: controls,
|
|
1114
|
+
className: "absolute top-0 left-0 h-full rounded-tl-lg rounded-bl-lg bg-red-200/40 dark:bg-red-300/40",
|
|
1115
|
+
initial: { width: "0%" }
|
|
1116
|
+
}
|
|
1117
|
+
),
|
|
1118
|
+
/* @__PURE__ */ jsxs4("span", { className: "relative z-10 flex items-center gap-1", children: [
|
|
1119
|
+
/* @__PURE__ */ jsx7(HugeiconsIcon3, { icon: Delete01Icon2, size: 14 }),
|
|
1120
|
+
isHolding ? "Release to cancel" : "Hold to delete"
|
|
1121
|
+
] })
|
|
1122
|
+
]
|
|
1123
|
+
}
|
|
1124
|
+
);
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// src/workflow-editor.tsx
|
|
1128
|
+
import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1129
|
+
function WorkflowEditor({
|
|
1130
|
+
baseUrl = "",
|
|
1131
|
+
workflowId,
|
|
1132
|
+
workflow: workflowProp,
|
|
1133
|
+
publishableKey,
|
|
1134
|
+
cursor,
|
|
1135
|
+
onClose,
|
|
1136
|
+
onWorkflowDeleted,
|
|
1137
|
+
onError
|
|
1138
|
+
}) {
|
|
1139
|
+
const draggableRef = useRef6(null);
|
|
1140
|
+
const { position, updatePosition } = usePosition(STORAGE_KEYS.editorPos);
|
|
1141
|
+
useDraggable(draggableRef, {
|
|
1142
|
+
handle: "[data-meridial-drag-handle]",
|
|
1143
|
+
defaultPosition: position,
|
|
1144
|
+
onDragEnd: ({ offsetX, offsetY }) => {
|
|
1145
|
+
updatePosition({ x: offsetX, y: offsetY });
|
|
1146
|
+
}
|
|
1147
|
+
});
|
|
1148
|
+
const [workflowState, setWorkflowState] = useState6(
|
|
1149
|
+
() => workflowProp ?? getWorkflow(workflowId) ?? null
|
|
1150
|
+
);
|
|
1151
|
+
const isConfigured = workflowState?.configured ?? false;
|
|
1152
|
+
const initialDescriptions = (() => {
|
|
1153
|
+
if (!workflowState) return {};
|
|
1154
|
+
const map = {};
|
|
1155
|
+
for (const step of workflowState.steps) {
|
|
1156
|
+
map[step.id] = step.description;
|
|
1157
|
+
}
|
|
1158
|
+
return map;
|
|
1159
|
+
})();
|
|
1160
|
+
const originalDescriptions = useRef6(initialDescriptions);
|
|
1161
|
+
const [currentIndex, setCurrentIndex] = useState6(0);
|
|
1162
|
+
const [descriptions, setDescriptions] = useState6(initialDescriptions);
|
|
1163
|
+
const [saving, setSaving] = useState6(false);
|
|
1164
|
+
const [savingChanges, setSavingChanges] = useState6(false);
|
|
1165
|
+
const steps = workflowState?.steps ?? [];
|
|
1166
|
+
const currentStep = steps[currentIndex];
|
|
1167
|
+
const currentDesc = currentStep ? descriptions[currentStep.id] ?? "" : "";
|
|
1168
|
+
const completedCount = steps.filter(
|
|
1169
|
+
(s) => (descriptions[s.id] ?? "").length >= MIN_DESC_LENGTH
|
|
1170
|
+
).length;
|
|
1171
|
+
const hasChanges = isConfigured && steps.some(
|
|
1172
|
+
(s) => (descriptions[s.id] ?? "") !== (originalDescriptions.current[s.id] ?? "")
|
|
1173
|
+
);
|
|
1174
|
+
const updateDescription = useCallback5(
|
|
1175
|
+
(value) => {
|
|
1176
|
+
if (!currentStep) return;
|
|
1177
|
+
setDescriptions((prev) => ({ ...prev, [currentStep.id]: value }));
|
|
1178
|
+
},
|
|
1179
|
+
[currentStep]
|
|
1180
|
+
);
|
|
1181
|
+
const handleDeleteStep = useCallback5(async () => {
|
|
1182
|
+
if (!workflowState || !currentStep) return;
|
|
1183
|
+
if (isConfigured && publishableKey) {
|
|
1184
|
+
try {
|
|
1185
|
+
const res = await fetch(
|
|
1186
|
+
`${baseUrl}/api/workflows/${workflowState.id}`,
|
|
1187
|
+
{
|
|
1188
|
+
method: "PATCH",
|
|
1189
|
+
headers: {
|
|
1190
|
+
"Content-Type": "application/json",
|
|
1191
|
+
Authorization: `Bearer ${publishableKey}`
|
|
1192
|
+
},
|
|
1193
|
+
body: JSON.stringify({ deleteStepId: currentStep.id })
|
|
1194
|
+
}
|
|
1195
|
+
);
|
|
1196
|
+
if (!res.ok) {
|
|
1197
|
+
const data2 = await res.json().catch(() => ({}));
|
|
1198
|
+
throw new Error(data2?.error || `Failed (${res.status})`);
|
|
1199
|
+
}
|
|
1200
|
+
const data = await res.json();
|
|
1201
|
+
if (data.deleted) {
|
|
1202
|
+
onWorkflowDeleted(workflowState.id);
|
|
1203
|
+
onClose();
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
} catch (err) {
|
|
1207
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1208
|
+
onError?.(msg);
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
} else {
|
|
1212
|
+
if (workflowState.steps.length === 1) {
|
|
1213
|
+
deleteWorkflow(workflowState.id);
|
|
1214
|
+
if (workflowState.audioKey) {
|
|
1215
|
+
deleteAudio(workflowState.audioKey).catch(() => {
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
onWorkflowDeleted(workflowState.id);
|
|
1219
|
+
onClose();
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
updateWorkflow(workflowState.id, {
|
|
1223
|
+
steps: workflowState.steps.filter((s) => s.id !== currentStep.id)
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
const newSteps = workflowState.steps.filter((s) => s.id !== currentStep.id);
|
|
1227
|
+
setWorkflowState({ ...workflowState, steps: newSteps });
|
|
1228
|
+
setCurrentIndex((prev) => Math.min(prev, newSteps.length - 1));
|
|
1229
|
+
}, [
|
|
1230
|
+
workflowState,
|
|
1231
|
+
currentStep,
|
|
1232
|
+
isConfigured,
|
|
1233
|
+
publishableKey,
|
|
1234
|
+
onClose,
|
|
1235
|
+
onWorkflowDeleted
|
|
1236
|
+
]);
|
|
1237
|
+
const handleSaveChanges = useCallback5(async () => {
|
|
1238
|
+
if (!workflowState || !publishableKey || !isConfigured) return;
|
|
1239
|
+
setSavingChanges(true);
|
|
1240
|
+
try {
|
|
1241
|
+
const updatedSteps = steps.map((s) => ({
|
|
1242
|
+
...s,
|
|
1243
|
+
description: descriptions[s.id] ?? s.description
|
|
1244
|
+
}));
|
|
1245
|
+
const res = await fetch(`${baseUrl}/api/workflows/${workflowState.id}`, {
|
|
1246
|
+
method: "PATCH",
|
|
1247
|
+
headers: {
|
|
1248
|
+
"Content-Type": "application/json",
|
|
1249
|
+
Authorization: `Bearer ${publishableKey}`
|
|
1250
|
+
},
|
|
1251
|
+
body: JSON.stringify({ steps: updatedSteps })
|
|
1252
|
+
});
|
|
1253
|
+
if (!res.ok) {
|
|
1254
|
+
const data = await res.json().catch(() => ({}));
|
|
1255
|
+
throw new Error(data?.error || `Failed (${res.status})`);
|
|
1256
|
+
}
|
|
1257
|
+
setWorkflowState({ ...workflowState, steps: updatedSteps });
|
|
1258
|
+
const newBaseline = {};
|
|
1259
|
+
for (const s of updatedSteps) {
|
|
1260
|
+
newBaseline[s.id] = s.description;
|
|
1261
|
+
}
|
|
1262
|
+
originalDescriptions.current = newBaseline;
|
|
1263
|
+
} catch (err) {
|
|
1264
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1265
|
+
onError?.(msg);
|
|
1266
|
+
} finally {
|
|
1267
|
+
setSavingChanges(false);
|
|
1268
|
+
}
|
|
1269
|
+
}, [workflowState, publishableKey, isConfigured, steps, descriptions]);
|
|
1270
|
+
const handleBack = useCallback5(() => {
|
|
1271
|
+
setCurrentIndex((prev) => Math.max(0, prev - 1));
|
|
1272
|
+
}, []);
|
|
1273
|
+
const uploadAndFinish = useCallback5(async () => {
|
|
1274
|
+
const wf = workflowState;
|
|
1275
|
+
const finalSteps = steps.map((s) => ({
|
|
1276
|
+
...s,
|
|
1277
|
+
description: descriptions[s.id] ?? s.description
|
|
1278
|
+
}));
|
|
1279
|
+
const finish = () => {
|
|
1280
|
+
deleteWorkflow(wf.id);
|
|
1281
|
+
if (wf.audioKey) {
|
|
1282
|
+
deleteAudio(wf.audioKey).catch(() => {
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1285
|
+
onWorkflowDeleted(wf.id);
|
|
1286
|
+
setTimeout(() => {
|
|
1287
|
+
setSaving(false);
|
|
1288
|
+
onClose();
|
|
1289
|
+
}, 300);
|
|
1290
|
+
};
|
|
1291
|
+
if (publishableKey && wf.audioKey) {
|
|
1292
|
+
try {
|
|
1293
|
+
const audioBlob = await loadAudio(wf.audioKey);
|
|
1294
|
+
const presignRes = await fetch(`${baseUrl}/api/upload/presigned-url`, {
|
|
1295
|
+
method: "POST",
|
|
1296
|
+
headers: {
|
|
1297
|
+
"Content-Type": "application/json",
|
|
1298
|
+
Authorization: `Bearer ${publishableKey}`
|
|
1299
|
+
},
|
|
1300
|
+
body: JSON.stringify({ workflowId: wf.id })
|
|
1301
|
+
});
|
|
1302
|
+
if (!presignRes.ok) {
|
|
1303
|
+
throw new Error(
|
|
1304
|
+
`[Meridial] Failed to get presigned URL (status ${presignRes.status})`
|
|
1305
|
+
);
|
|
1306
|
+
}
|
|
1307
|
+
const { uploadUrl } = await presignRes.json();
|
|
1308
|
+
if (audioBlob) {
|
|
1309
|
+
const uploadRes = await fetch(uploadUrl, {
|
|
1310
|
+
method: "PUT",
|
|
1311
|
+
headers: { "Content-Type": "audio/webm" },
|
|
1312
|
+
body: audioBlob
|
|
1313
|
+
});
|
|
1314
|
+
if (!uploadRes.ok) {
|
|
1315
|
+
throw new Error(
|
|
1316
|
+
`[Meridial] Failed to upload audio blob (status ${uploadRes.status})`
|
|
1317
|
+
);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
await fetch(`${baseUrl}/api/upload/complete`, {
|
|
1321
|
+
method: "POST",
|
|
1322
|
+
headers: {
|
|
1323
|
+
"Content-Type": "application/json",
|
|
1324
|
+
Authorization: `Bearer ${publishableKey}`
|
|
1325
|
+
},
|
|
1326
|
+
body: JSON.stringify({
|
|
1327
|
+
workflowId: wf.id,
|
|
1328
|
+
name: wf.name,
|
|
1329
|
+
description: "",
|
|
1330
|
+
steps: finalSteps
|
|
1331
|
+
})
|
|
1332
|
+
});
|
|
1333
|
+
finish();
|
|
1334
|
+
} catch (err) {
|
|
1335
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1336
|
+
onError?.(msg);
|
|
1337
|
+
setSaving(false);
|
|
1338
|
+
}
|
|
1339
|
+
} else {
|
|
1340
|
+
finish();
|
|
1341
|
+
}
|
|
1342
|
+
}, [
|
|
1343
|
+
workflowState,
|
|
1344
|
+
steps,
|
|
1345
|
+
descriptions,
|
|
1346
|
+
publishableKey,
|
|
1347
|
+
onClose,
|
|
1348
|
+
onWorkflowDeleted,
|
|
1349
|
+
baseUrl
|
|
1350
|
+
]);
|
|
1351
|
+
const handleNext = useCallback5(() => {
|
|
1352
|
+
const isLast = currentIndex === steps.length - 1;
|
|
1353
|
+
if (!isLast) {
|
|
1354
|
+
setCurrentIndex((prev) => prev + 1);
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
if (isConfigured) {
|
|
1358
|
+
onClose();
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
setSaving(true);
|
|
1362
|
+
uploadAndFinish();
|
|
1363
|
+
}, [isConfigured, currentIndex, steps, uploadAndFinish, onClose]);
|
|
1364
|
+
if (!workflowState || !currentStep) return null;
|
|
1365
|
+
const isLastStep = currentIndex === steps.length - 1;
|
|
1366
|
+
const canAdvance = currentDesc.length >= MIN_DESC_LENGTH;
|
|
1367
|
+
const allConfigured = completedCount === steps.length && steps.length > 0;
|
|
1368
|
+
const isConfiguredSaveMode = isConfigured && hasChanges;
|
|
1369
|
+
const primaryDisabled = isConfiguredSaveMode ? savingChanges : isLastStep ? isConfigured ? false : !allConfigured || saving : !canAdvance;
|
|
1370
|
+
const primaryAction = isConfiguredSaveMode ? handleSaveChanges : handleNext;
|
|
1371
|
+
return createPortal2(
|
|
1372
|
+
/* @__PURE__ */ jsxs5(
|
|
1373
|
+
"div",
|
|
1374
|
+
{
|
|
1375
|
+
ref: draggableRef,
|
|
1376
|
+
"data-meridial-ui": true,
|
|
1377
|
+
className: "group fixed bottom-4 left-1/2 z-[99998] w-[500px] -translate-x-1/2 rounded border border-border bg-card shadow-md",
|
|
1378
|
+
children: [
|
|
1379
|
+
/* @__PURE__ */ jsxs5("div", { className: "flex items-center justify-between", children: [
|
|
1380
|
+
/* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-2 p-2", children: [
|
|
1381
|
+
/* @__PURE__ */ jsx8(DragHandle, {}),
|
|
1382
|
+
/* @__PURE__ */ jsx8("span", { className: "text-sm text-muted-foreground", children: currentStep.elementLabel })
|
|
1383
|
+
] }),
|
|
1384
|
+
/* @__PURE__ */ jsx8(Button, { variant: "ghost", size: "icon", onClick: onClose, children: /* @__PURE__ */ jsx8(
|
|
1385
|
+
HugeiconsIcon4,
|
|
1386
|
+
{
|
|
1387
|
+
icon: MultiplicationSignIcon,
|
|
1388
|
+
strokeWidth: 2,
|
|
1389
|
+
size: 16
|
|
1390
|
+
}
|
|
1391
|
+
) })
|
|
1392
|
+
] }),
|
|
1393
|
+
/* @__PURE__ */ jsx8(
|
|
1394
|
+
"textarea",
|
|
1395
|
+
{
|
|
1396
|
+
"data-meridial-ui": true,
|
|
1397
|
+
value: currentDesc,
|
|
1398
|
+
onChange: (e) => updateDescription(e.target.value),
|
|
1399
|
+
placeholder: "Add or edit existing step description",
|
|
1400
|
+
rows: 3,
|
|
1401
|
+
className: "-mb-2 w-full resize-none border-y bg-muted/50 px-3 py-2 text-sm text-foreground outline-none placeholder:text-muted-foreground"
|
|
1402
|
+
}
|
|
1403
|
+
),
|
|
1404
|
+
/* @__PURE__ */ jsxs5("div", { "data-meridial-ui": true, className: "flex items-center justify-between p-2", children: [
|
|
1405
|
+
/* @__PURE__ */ jsxs5("span", { className: "text-xs text-muted-foreground", children: [
|
|
1406
|
+
completedCount,
|
|
1407
|
+
" / ",
|
|
1408
|
+
steps.length,
|
|
1409
|
+
" Steps Completed"
|
|
1410
|
+
] }),
|
|
1411
|
+
/* @__PURE__ */ jsxs5("div", { className: "flex gap-2", children: [
|
|
1412
|
+
/* @__PURE__ */ jsx8(DeleteStepButton, { onConfirm: handleDeleteStep }),
|
|
1413
|
+
/* @__PURE__ */ jsxs5(
|
|
1414
|
+
Button,
|
|
1415
|
+
{
|
|
1416
|
+
variant: "outline",
|
|
1417
|
+
"data-meridial-ui": true,
|
|
1418
|
+
onClick: handleBack,
|
|
1419
|
+
disabled: currentIndex === 0,
|
|
1420
|
+
title: "Back",
|
|
1421
|
+
children: [
|
|
1422
|
+
/* @__PURE__ */ jsx8(HugeiconsIcon4, { icon: ArrowLeft01Icon, size: 16 }),
|
|
1423
|
+
/* @__PURE__ */ jsx8("span", { className: "text-xs", children: "Back" })
|
|
1424
|
+
]
|
|
1425
|
+
}
|
|
1426
|
+
),
|
|
1427
|
+
/* @__PURE__ */ jsx8(
|
|
1428
|
+
Button,
|
|
1429
|
+
{
|
|
1430
|
+
variant: "default",
|
|
1431
|
+
"data-meridial-ui": true,
|
|
1432
|
+
onClick: primaryAction,
|
|
1433
|
+
disabled: primaryDisabled,
|
|
1434
|
+
children: isConfiguredSaveMode ? savingChanges ? /* @__PURE__ */ jsxs5(Fragment2, { children: [
|
|
1435
|
+
/* @__PURE__ */ jsx8(
|
|
1436
|
+
HugeiconsIcon4,
|
|
1437
|
+
{
|
|
1438
|
+
icon: Loading03Icon2,
|
|
1439
|
+
size: 14,
|
|
1440
|
+
className: "animate-spin"
|
|
1441
|
+
}
|
|
1442
|
+
),
|
|
1443
|
+
"Saving Workflow"
|
|
1444
|
+
] }) : "Save Workflow" : saving ? /* @__PURE__ */ jsx8(
|
|
1445
|
+
HugeiconsIcon4,
|
|
1446
|
+
{
|
|
1447
|
+
icon: Loading03Icon2,
|
|
1448
|
+
size: 14,
|
|
1449
|
+
className: "animate-spin"
|
|
1450
|
+
}
|
|
1451
|
+
) : isLastStep ? /* @__PURE__ */ jsxs5(Fragment2, { children: [
|
|
1452
|
+
"Finish",
|
|
1453
|
+
/* @__PURE__ */ jsx8(HugeiconsIcon4, { icon: Tick01Icon, size: 14 })
|
|
1454
|
+
] }) : /* @__PURE__ */ jsxs5(Fragment2, { children: [
|
|
1455
|
+
"Next",
|
|
1456
|
+
/* @__PURE__ */ jsx8(HugeiconsIcon4, { icon: ArrowRight01Icon, size: 14 })
|
|
1457
|
+
] })
|
|
1458
|
+
}
|
|
1459
|
+
)
|
|
1460
|
+
] })
|
|
1461
|
+
] }),
|
|
1462
|
+
/* @__PURE__ */ jsx8(
|
|
1463
|
+
CursorGuide,
|
|
1464
|
+
{
|
|
1465
|
+
selector: currentStep.elementId,
|
|
1466
|
+
urlPath: currentStep.urlPath,
|
|
1467
|
+
onError,
|
|
1468
|
+
onClick: isConfigured ? primaryAction : void 0,
|
|
1469
|
+
children: cursor ?? /* @__PURE__ */ jsx8(
|
|
1470
|
+
HugeiconsIcon4,
|
|
1471
|
+
{
|
|
1472
|
+
icon: Cursor02Icon,
|
|
1473
|
+
strokeWidth: 2,
|
|
1474
|
+
size: 32,
|
|
1475
|
+
className: "fill-primary/80 text-primary"
|
|
1476
|
+
}
|
|
1477
|
+
)
|
|
1478
|
+
}
|
|
1479
|
+
)
|
|
1480
|
+
]
|
|
1481
|
+
}
|
|
1482
|
+
),
|
|
1483
|
+
document.body
|
|
1484
|
+
);
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
// src/recorder.tsx
|
|
1488
|
+
import { Fragment as Fragment3, jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1489
|
+
function Recorder({
|
|
1490
|
+
baseUrl = "",
|
|
1491
|
+
publishableKey,
|
|
1492
|
+
cursor,
|
|
1493
|
+
onError
|
|
1494
|
+
}) {
|
|
1495
|
+
const draggableRef = useRef7(null);
|
|
1496
|
+
const keylessWarned = useRef7(false);
|
|
1497
|
+
useDraggable2(draggableRef);
|
|
1498
|
+
const [state, dispatch] = useRecorderState();
|
|
1499
|
+
const audio = useAudioRecorder();
|
|
1500
|
+
const wf = useWorkflows();
|
|
1501
|
+
const [showPanel, setShowPanel] = useState7(false);
|
|
1502
|
+
const [editorWorkflowId, setPlayerWorkflowId] = useState7(null);
|
|
1503
|
+
const stepsRef = useRef7([]);
|
|
1504
|
+
const streamRef = useRef7(null);
|
|
1505
|
+
const [serverWorkflows, setServerWorkflows] = useState7([]);
|
|
1506
|
+
const isRecording = state.status === "recording";
|
|
1507
|
+
const isPaused = state.status === "paused";
|
|
1508
|
+
useEffect4(() => {
|
|
1509
|
+
if (!publishableKey && !keylessWarned.current) {
|
|
1510
|
+
keylessWarned.current = true;
|
|
1511
|
+
console.warn(
|
|
1512
|
+
"[Meridial] No publishableKey provided. Workflows will only be saved locally."
|
|
1513
|
+
);
|
|
1514
|
+
}
|
|
1515
|
+
}, [publishableKey]);
|
|
1516
|
+
useEffect4(() => {
|
|
1517
|
+
if (!publishableKey) return;
|
|
1518
|
+
fetch(`${baseUrl}/api/workflows`, {
|
|
1519
|
+
headers: { Authorization: `Bearer ${publishableKey}` }
|
|
1520
|
+
}).then(async (res) => {
|
|
1521
|
+
const data = await res.json();
|
|
1522
|
+
if (!res.ok) {
|
|
1523
|
+
const msg = data?.error || `Request failed (${res.status})`;
|
|
1524
|
+
onError?.(msg);
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
if (!data.workflows) return;
|
|
1528
|
+
const parsed = apiWorkflowsResponseSchema.safeParse(data);
|
|
1529
|
+
if (!parsed.success) {
|
|
1530
|
+
onError?.("Invalid workflows response");
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
if (parsed.data.error) {
|
|
1534
|
+
onError?.(parsed.data.error);
|
|
1535
|
+
return;
|
|
1536
|
+
}
|
|
1537
|
+
const workflows = parsed.data.workflows ?? [];
|
|
1538
|
+
const mapped = [];
|
|
1539
|
+
for (const w of workflows) {
|
|
1540
|
+
const workflow = workflowSchema.safeParse({
|
|
1541
|
+
id: w.id,
|
|
1542
|
+
name: w.name,
|
|
1543
|
+
steps: w.steps,
|
|
1544
|
+
configured: true,
|
|
1545
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1546
|
+
});
|
|
1547
|
+
if (workflow.success) {
|
|
1548
|
+
mapped.push(workflow.data);
|
|
1549
|
+
} else {
|
|
1550
|
+
console.error(
|
|
1551
|
+
"[Meridial] Invalid workflow shape:",
|
|
1552
|
+
w.id,
|
|
1553
|
+
workflow.error
|
|
1554
|
+
);
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
setServerWorkflows(mapped);
|
|
1558
|
+
}).catch((err) => {
|
|
1559
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1560
|
+
onError?.(msg);
|
|
1561
|
+
});
|
|
1562
|
+
}, [publishableKey]);
|
|
1563
|
+
const getLastElementId = useCallback6(
|
|
1564
|
+
() => stepsRef.current[stepsRef.current.length - 1]?.elementId,
|
|
1565
|
+
[]
|
|
1566
|
+
);
|
|
1567
|
+
useClickCapture({
|
|
1568
|
+
enabled: isRecording,
|
|
1569
|
+
onCapture: (step) => {
|
|
1570
|
+
if (getLastElementId() === step.elementId) return;
|
|
1571
|
+
stepsRef.current.push(step);
|
|
1572
|
+
dispatch({ type: "STEP_CAPTURED" });
|
|
1573
|
+
}
|
|
1574
|
+
});
|
|
1575
|
+
const handleStreamReady = useCallback6(
|
|
1576
|
+
(stream) => {
|
|
1577
|
+
streamRef.current = stream;
|
|
1578
|
+
audio.start(stream);
|
|
1579
|
+
},
|
|
1580
|
+
[audio.start]
|
|
1581
|
+
);
|
|
1582
|
+
const handleStartRecording = useCallback6(() => {
|
|
1583
|
+
stepsRef.current = [];
|
|
1584
|
+
dispatch({ type: "START_RECORDING" });
|
|
1585
|
+
}, [dispatch]);
|
|
1586
|
+
const handleActionClick = useCallback6(() => {
|
|
1587
|
+
if (state.status === "idle") {
|
|
1588
|
+
setShowPanel((prev) => !prev);
|
|
1589
|
+
} else if (state.status === "recording") {
|
|
1590
|
+
dispatch({ type: "PAUSE" });
|
|
1591
|
+
audio.pause();
|
|
1592
|
+
} else if (state.status === "paused") {
|
|
1593
|
+
dispatch({ type: "RESUME" });
|
|
1594
|
+
audio.resume();
|
|
1595
|
+
}
|
|
1596
|
+
}, [state.status, dispatch, audio]);
|
|
1597
|
+
const handleSave = useCallback6(async () => {
|
|
1598
|
+
dispatch({ type: "SAVE" });
|
|
1599
|
+
audio.stop();
|
|
1600
|
+
const id = crypto.randomUUID();
|
|
1601
|
+
const audioKey = `audio-${id}`;
|
|
1602
|
+
const steps = stepsRef.current.map((s) => ({
|
|
1603
|
+
...s,
|
|
1604
|
+
id: crypto.randomUUID()
|
|
1605
|
+
}));
|
|
1606
|
+
wf.addWorkflow({
|
|
1607
|
+
id,
|
|
1608
|
+
name: generateWorkflowName(),
|
|
1609
|
+
steps,
|
|
1610
|
+
audioKey,
|
|
1611
|
+
configured: false,
|
|
1612
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1613
|
+
});
|
|
1614
|
+
setTimeout(async () => {
|
|
1615
|
+
if (audio.audioBlob) {
|
|
1616
|
+
await saveAudio(audioKey, audio.audioBlob).catch(() => {
|
|
1617
|
+
});
|
|
1618
|
+
}
|
|
1619
|
+
dispatch({ type: "SAVE_COMPLETE" });
|
|
1620
|
+
}, 200);
|
|
1621
|
+
}, [dispatch, audio, wf, publishableKey, baseUrl]);
|
|
1622
|
+
const handleRename = useCallback6(
|
|
1623
|
+
(id, name) => {
|
|
1624
|
+
wf.updateWorkflow(id, { name });
|
|
1625
|
+
},
|
|
1626
|
+
[wf]
|
|
1627
|
+
);
|
|
1628
|
+
const handleDelete = useCallback6(
|
|
1629
|
+
(id) => {
|
|
1630
|
+
const workflow = wf.getWorkflow(id);
|
|
1631
|
+
if (workflow?.audioKey) {
|
|
1632
|
+
deleteAudio(workflow.audioKey).catch(() => {
|
|
1633
|
+
});
|
|
1634
|
+
}
|
|
1635
|
+
wf.deleteWorkflow(id);
|
|
1636
|
+
},
|
|
1637
|
+
[wf]
|
|
1638
|
+
);
|
|
1639
|
+
const handleConfigure = useCallback6((id) => {
|
|
1640
|
+
setPlayerWorkflowId(id);
|
|
1641
|
+
setShowPanel(false);
|
|
1642
|
+
}, []);
|
|
1643
|
+
const handleEditorClose = useCallback6(() => {
|
|
1644
|
+
setPlayerWorkflowId(null);
|
|
1645
|
+
}, []);
|
|
1646
|
+
const handleWorkflowDeleted = useCallback6(() => {
|
|
1647
|
+
wf.refresh();
|
|
1648
|
+
}, [wf]);
|
|
1649
|
+
const allWorkflows = [
|
|
1650
|
+
...wf.visibleWorkflows,
|
|
1651
|
+
...serverWorkflows.filter(
|
|
1652
|
+
(sw) => !wf.visibleWorkflows.some((lw) => lw.id === sw.id)
|
|
1653
|
+
)
|
|
1654
|
+
];
|
|
1655
|
+
return /* @__PURE__ */ jsxs6(Fragment3, { children: [
|
|
1656
|
+
/* @__PURE__ */ jsxs6(
|
|
1657
|
+
"div",
|
|
1658
|
+
{
|
|
1659
|
+
ref: draggableRef,
|
|
1660
|
+
"data-meridial-ui": true,
|
|
1661
|
+
className: "fixed bottom-4 left-4 z-50 w-64 items-stretch rounded border border-border bg-card shadow-md",
|
|
1662
|
+
children: [
|
|
1663
|
+
/* @__PURE__ */ jsx9(AnimatePresence2, { initial: false, children: showPanel && state.status === "idle" && /* @__PURE__ */ jsx9(
|
|
1664
|
+
motion3.div,
|
|
1665
|
+
{
|
|
1666
|
+
initial: { height: 0, opacity: 0 },
|
|
1667
|
+
animate: { height: "auto", opacity: 1 },
|
|
1668
|
+
exit: { height: 0, opacity: 0 },
|
|
1669
|
+
transition: { duration: 0.24, ease: "easeOut" },
|
|
1670
|
+
style: { overflow: "hidden" },
|
|
1671
|
+
children: /* @__PURE__ */ jsx9(
|
|
1672
|
+
WorkflowListPanel,
|
|
1673
|
+
{
|
|
1674
|
+
workflows: allWorkflows,
|
|
1675
|
+
hasMore: wf.hasMore,
|
|
1676
|
+
remainingCount: wf.remainingCount,
|
|
1677
|
+
onLoadMore: wf.loadMore,
|
|
1678
|
+
onRename: handleRename,
|
|
1679
|
+
onDelete: handleDelete,
|
|
1680
|
+
onConfigure: handleConfigure
|
|
1681
|
+
}
|
|
1682
|
+
)
|
|
1683
|
+
}
|
|
1684
|
+
) }),
|
|
1685
|
+
/* @__PURE__ */ jsxs6("div", { "data-meridial-ui": true, className: "flex h-12 items-center", children: [
|
|
1686
|
+
/* @__PURE__ */ jsx9(DragHandle, { className: "px-2" }),
|
|
1687
|
+
/* @__PURE__ */ jsx9(Separator, { orientation: "vertical", className: "h-full" }),
|
|
1688
|
+
state.status === "idle" ? /* @__PURE__ */ jsx9(
|
|
1689
|
+
"button",
|
|
1690
|
+
{
|
|
1691
|
+
"data-meridial-ui": true,
|
|
1692
|
+
onClick: handleStartRecording,
|
|
1693
|
+
className: "w-full cursor-pointer px-4 text-sm tracking-wide text-foreground uppercase",
|
|
1694
|
+
children: "Start Recording"
|
|
1695
|
+
}
|
|
1696
|
+
) : /* @__PURE__ */ jsxs6("div", { className: "relative h-full w-full", children: [
|
|
1697
|
+
isRecording && /* @__PURE__ */ jsx9(
|
|
1698
|
+
Waveform,
|
|
1699
|
+
{
|
|
1700
|
+
"data-meridial-ui": true,
|
|
1701
|
+
active: isRecording,
|
|
1702
|
+
height: 48,
|
|
1703
|
+
mode: "static",
|
|
1704
|
+
barWidth: 3,
|
|
1705
|
+
barGap: 1,
|
|
1706
|
+
barRadius: 1.5,
|
|
1707
|
+
onStreamReady: handleStreamReady,
|
|
1708
|
+
className: "text-primary"
|
|
1709
|
+
}
|
|
1710
|
+
),
|
|
1711
|
+
isPaused && /* @__PURE__ */ jsx9(
|
|
1712
|
+
"button",
|
|
1713
|
+
{
|
|
1714
|
+
"data-meridial-ui": true,
|
|
1715
|
+
onClick: handleSave,
|
|
1716
|
+
className: "absolute inset-0 flex cursor-pointer items-center justify-center text-sm backdrop-blur-[1px]",
|
|
1717
|
+
children: "SAVE WORKFLOW"
|
|
1718
|
+
}
|
|
1719
|
+
),
|
|
1720
|
+
isRecording && state.stepCount > 0 && /* @__PURE__ */ jsx9(StepBadge, { count: state.stepCount })
|
|
1721
|
+
] }),
|
|
1722
|
+
/* @__PURE__ */ jsx9(Separator, { orientation: "vertical", className: "h-full" }),
|
|
1723
|
+
/* @__PURE__ */ jsx9(
|
|
1724
|
+
ActionButton,
|
|
1725
|
+
{
|
|
1726
|
+
openPanel: showPanel,
|
|
1727
|
+
state,
|
|
1728
|
+
hasUnconfigured: wf.hasUnconfigured,
|
|
1729
|
+
onClick: handleActionClick
|
|
1730
|
+
}
|
|
1731
|
+
)
|
|
1732
|
+
] })
|
|
1733
|
+
]
|
|
1734
|
+
}
|
|
1735
|
+
),
|
|
1736
|
+
editorWorkflowId && /* @__PURE__ */ jsx9(
|
|
1737
|
+
WorkflowEditor,
|
|
1738
|
+
{
|
|
1739
|
+
workflowId: editorWorkflowId,
|
|
1740
|
+
workflow: allWorkflows.find((w) => w.id === editorWorkflowId),
|
|
1741
|
+
publishableKey,
|
|
1742
|
+
cursor,
|
|
1743
|
+
onClose: handleEditorClose,
|
|
1744
|
+
onWorkflowDeleted: handleWorkflowDeleted,
|
|
1745
|
+
onError
|
|
1746
|
+
}
|
|
1747
|
+
)
|
|
1748
|
+
] });
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
export {
|
|
1752
|
+
Recorder
|
|
1753
|
+
};
|
|
1754
|
+
//# sourceMappingURL=chunk-VDQAVB4N.js.map
|