@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.
@@ -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