chore: import upstream snapshot with attribution
This commit is contained in:
File diff suppressed because one or more lines are too long
+188
@@ -0,0 +1,188 @@
|
||||
"use client";
|
||||
|
||||
import { animate } from "motion";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
import { cn } from "@/utils/cn";
|
||||
import initCanvas from "@/utils/init-canvas";
|
||||
|
||||
export default function EndpointsCrawl({
|
||||
active,
|
||||
alwaysHeat = false,
|
||||
triggerOnHover = false,
|
||||
size = 20,
|
||||
}: {
|
||||
active?: boolean;
|
||||
alwaysHeat?: boolean;
|
||||
triggerOnHover?: boolean;
|
||||
size?: number;
|
||||
}) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
const fnRefs = useRef<{
|
||||
activate: () => void;
|
||||
deactivate: () => void;
|
||||
}>({ activate: () => {}, deactivate: () => {} });
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = initCanvas(canvas);
|
||||
|
||||
let isRunning = false;
|
||||
let isActive = false;
|
||||
|
||||
let activeGroup = 0;
|
||||
const rowAlphas = [0.2, 0.4, 1, 0.04];
|
||||
|
||||
const grid = [
|
||||
[24],
|
||||
[16, 18, 30, 32],
|
||||
[8, 12, 36, 40],
|
||||
[0, 3, 6, 21, 27, 42, 45, 48],
|
||||
];
|
||||
|
||||
const scaler = size / 20;
|
||||
|
||||
const render = () => {
|
||||
ctx.fillStyle = "#FF4C00";
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
for (const group of grid.slice(0, 4)) {
|
||||
const groupIndex = grid.indexOf(group);
|
||||
ctx.globalAlpha = rowAlphas[groupIndex];
|
||||
|
||||
for (const index of group) {
|
||||
ctx.fillRect(
|
||||
(3 + (index % 7) * 2) * scaler,
|
||||
(3 + Math.floor(index / 7) * 2) * scaler,
|
||||
2 * scaler,
|
||||
2 * scaler,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (isRunning) {
|
||||
requestAnimationFrame(render);
|
||||
}
|
||||
};
|
||||
|
||||
const timeouts: number[] = [];
|
||||
|
||||
let runCount = 0;
|
||||
|
||||
const cycle = () => {
|
||||
isRunning = true;
|
||||
activeGroup = (activeGroup + 1) % 5;
|
||||
|
||||
rowAlphas.forEach((alpha, index) => {
|
||||
let targetAlpha = alpha;
|
||||
|
||||
if (index === activeGroup) targetAlpha = 1;
|
||||
else if (index === (activeGroup + 1) % 4) targetAlpha = 0.12;
|
||||
else if (index === (activeGroup + 2) % 4) targetAlpha = 0.2;
|
||||
else if (index === (activeGroup + 3) % 4) targetAlpha = 0.4;
|
||||
|
||||
animate(alpha, targetAlpha, {
|
||||
duration: 0.05,
|
||||
onUpdate: (value) => {
|
||||
rowAlphas[index] = value;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
timeouts.forEach((timeout) => {
|
||||
window.clearTimeout(timeout);
|
||||
});
|
||||
|
||||
timeouts.push(
|
||||
window.setTimeout(() => {
|
||||
isRunning = false;
|
||||
}, 300),
|
||||
);
|
||||
|
||||
if (activeGroup === 3) runCount += 1;
|
||||
|
||||
if ((runCount === 2 || !isActive) && activeGroup === 2) return;
|
||||
|
||||
timeouts.push(
|
||||
window.setTimeout(() => {
|
||||
cycle();
|
||||
}, 50),
|
||||
);
|
||||
};
|
||||
|
||||
fnRefs.current = {
|
||||
activate: () => {
|
||||
if (isActive) return;
|
||||
|
||||
isActive = true;
|
||||
|
||||
runCount = 0;
|
||||
|
||||
cycle();
|
||||
render();
|
||||
},
|
||||
deactivate: () => {
|
||||
if (!isActive) return;
|
||||
|
||||
isActive = false;
|
||||
},
|
||||
};
|
||||
|
||||
render();
|
||||
canvas.addEventListener("resize", render);
|
||||
|
||||
if (triggerOnHover) {
|
||||
const group = canvasRef.current!.closest(".group");
|
||||
|
||||
if (group) {
|
||||
group.addEventListener("mouseenter", fnRefs.current.activate);
|
||||
group.addEventListener("mouseleave", fnRefs.current.deactivate);
|
||||
|
||||
return () => {
|
||||
group.removeEventListener("mouseenter", fnRefs.current.activate);
|
||||
group.removeEventListener("mouseleave", fnRefs.current.deactivate);
|
||||
};
|
||||
}
|
||||
}
|
||||
}, [triggerOnHover, size]);
|
||||
|
||||
useEffect(() => {
|
||||
if (triggerOnHover) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting && active) {
|
||||
fnRefs.current.activate();
|
||||
} else {
|
||||
fnRefs.current.deactivate();
|
||||
}
|
||||
},
|
||||
{ threshold: 0.5 },
|
||||
);
|
||||
|
||||
observer.observe(canvasRef.current!);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [active, triggerOnHover]);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
className={cn(
|
||||
alwaysHeat
|
||||
? ""
|
||||
: [
|
||||
"[&.grayscale]:opacity-60 transition-[filter,opacity]",
|
||||
!active && "grayscale",
|
||||
],
|
||||
)}
|
||||
ref={canvasRef}
|
||||
style={{ width: size, height: size }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
"use client";
|
||||
|
||||
import { animate } from "motion";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
import { cn } from "@/utils/cn";
|
||||
import initCanvas from "@/utils/init-canvas";
|
||||
|
||||
export default function EndpointsExtract({
|
||||
active,
|
||||
disabledCells,
|
||||
alwaysHeat = false,
|
||||
triggerOnHover = false,
|
||||
size = 20,
|
||||
}: {
|
||||
active?: boolean;
|
||||
disabledCells?: number[];
|
||||
alwaysHeat?: boolean;
|
||||
triggerOnHover?: boolean;
|
||||
size?: number;
|
||||
}) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
const fnRefs = useRef<{
|
||||
activate: () => void;
|
||||
deactivate: () => void;
|
||||
}>({ activate: () => {}, deactivate: () => {} });
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = initCanvas(canvas);
|
||||
|
||||
let isRunning = false;
|
||||
let isActive = false;
|
||||
|
||||
let activeCol = 0;
|
||||
const colAlphas = [1, 0.4, 0.2, 0.12];
|
||||
|
||||
const scaler = size / 20;
|
||||
|
||||
const render = () => {
|
||||
ctx.fillStyle = "#FF4C00";
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Draw Extract pattern - represents structured data extraction
|
||||
// Draw columns to represent data fields
|
||||
for (let col = 0; col < 4; col++) {
|
||||
ctx.globalAlpha = colAlphas[col];
|
||||
|
||||
// Draw vertical bars of different heights to represent extracted data
|
||||
const heights = [3, 2, 3, 1];
|
||||
const startY = [1, 2, 1, 3];
|
||||
|
||||
for (let row = 0; row < heights[col]; row++) {
|
||||
ctx.fillRect(
|
||||
(3 + col * 4) * scaler,
|
||||
(3 + startY[col] * 2 + row * 4) * scaler,
|
||||
2 * scaler,
|
||||
2 * scaler,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (isRunning) {
|
||||
requestAnimationFrame(render);
|
||||
}
|
||||
};
|
||||
|
||||
const timeouts: number[] = [];
|
||||
|
||||
let runCount = 0;
|
||||
|
||||
const cycle = () => {
|
||||
isRunning = true;
|
||||
activeCol = (activeCol + 1) % 4;
|
||||
|
||||
colAlphas.forEach((alpha, index) => {
|
||||
let targetAlpha = alpha;
|
||||
|
||||
if (index === activeCol) targetAlpha = 1;
|
||||
else if (index === (activeCol + 1) % 4) targetAlpha = 0.12;
|
||||
else if (index === (activeCol + 2) % 4) targetAlpha = 0.2;
|
||||
else if (index === (activeCol + 3) % 4) targetAlpha = 0.4;
|
||||
|
||||
animate(alpha, targetAlpha, {
|
||||
duration: 0.05,
|
||||
onUpdate: (value) => {
|
||||
colAlphas[index] = value;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
timeouts.forEach((timeout) => {
|
||||
window.clearTimeout(timeout);
|
||||
});
|
||||
|
||||
timeouts.push(
|
||||
window.setTimeout(() => {
|
||||
isRunning = false;
|
||||
}, 400),
|
||||
);
|
||||
|
||||
if (activeCol === 3) runCount += 1;
|
||||
|
||||
if ((runCount === 2 || !isActive) && activeCol === 0) return;
|
||||
|
||||
timeouts.push(
|
||||
window.setTimeout(() => {
|
||||
cycle();
|
||||
}, 50),
|
||||
);
|
||||
};
|
||||
|
||||
fnRefs.current = {
|
||||
activate: () => {
|
||||
if (isActive) return;
|
||||
|
||||
isActive = true;
|
||||
|
||||
runCount = 0;
|
||||
cycle();
|
||||
render();
|
||||
},
|
||||
deactivate: () => {
|
||||
if (!isActive) return;
|
||||
|
||||
isActive = false;
|
||||
},
|
||||
};
|
||||
|
||||
render();
|
||||
canvas.addEventListener("resize", render);
|
||||
|
||||
if (triggerOnHover) {
|
||||
const group = canvasRef.current!.closest(".group");
|
||||
|
||||
if (group) {
|
||||
group.addEventListener("mouseenter", fnRefs.current.activate);
|
||||
group.addEventListener("mouseleave", fnRefs.current.deactivate);
|
||||
|
||||
return () => {
|
||||
group.removeEventListener("mouseenter", fnRefs.current.activate);
|
||||
group.removeEventListener("mouseleave", fnRefs.current.deactivate);
|
||||
};
|
||||
}
|
||||
}
|
||||
}, [disabledCells, size, triggerOnHover]);
|
||||
|
||||
useEffect(() => {
|
||||
if (triggerOnHover) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting && active) {
|
||||
fnRefs.current.activate();
|
||||
} else {
|
||||
fnRefs.current.deactivate();
|
||||
}
|
||||
},
|
||||
{ threshold: 0.5 },
|
||||
);
|
||||
|
||||
observer.observe(canvasRef.current!);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [active, triggerOnHover]);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
className={cn(
|
||||
alwaysHeat
|
||||
? ""
|
||||
: [
|
||||
"[&.grayscale]:opacity-60 transition-[filter,opacity]",
|
||||
!active && "grayscale",
|
||||
],
|
||||
)}
|
||||
ref={canvasRef}
|
||||
style={{ width: size, height: size }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { ComponentProps } from "react";
|
||||
|
||||
import EndpointsScrape from "@/components/app/(home)/sections/endpoints/EndpointsScrape/EndpointsScrape";
|
||||
|
||||
export default function EndpointsMap(
|
||||
props: ComponentProps<typeof EndpointsScrape>,
|
||||
) {
|
||||
return <EndpointsScrape {...props} disabledCells={[1, 2, 3, 7, 9, 12, 15]} />;
|
||||
}
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
"use client";
|
||||
|
||||
import { animate } from "motion";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
import { cn } from "@/utils/cn";
|
||||
import initCanvas from "@/utils/init-canvas";
|
||||
|
||||
export default function EndpointsScrape({
|
||||
active,
|
||||
disabledCells,
|
||||
alwaysHeat = false,
|
||||
triggerOnHover = false,
|
||||
size = 20,
|
||||
}: {
|
||||
active?: boolean;
|
||||
disabledCells?: number[];
|
||||
alwaysHeat?: boolean;
|
||||
triggerOnHover?: boolean;
|
||||
size?: number;
|
||||
}) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
const fnRefs = useRef<{
|
||||
activate: () => void;
|
||||
deactivate: () => void;
|
||||
}>({ activate: () => {}, deactivate: () => {} });
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = initCanvas(canvas);
|
||||
|
||||
let isRunning = false;
|
||||
let isActive = false;
|
||||
|
||||
let activeRow = 2;
|
||||
const rowAlphas = [0.2, 0.4, 1, 0.12];
|
||||
|
||||
const scaler = size / 20;
|
||||
|
||||
const render = () => {
|
||||
ctx.fillStyle = "#FF4C00";
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
for (let i = 0; i < 16; i++) {
|
||||
if (disabledCells && disabledCells.includes(i)) continue;
|
||||
|
||||
ctx.globalAlpha = rowAlphas[Math.floor(i / 4)];
|
||||
|
||||
ctx.fillRect(
|
||||
(3 + (i % 4) * 4) * scaler,
|
||||
(3 + Math.floor(i / 4) * 4) * scaler,
|
||||
2 * scaler,
|
||||
2 * scaler,
|
||||
);
|
||||
}
|
||||
|
||||
if (isRunning) {
|
||||
requestAnimationFrame(render);
|
||||
}
|
||||
};
|
||||
|
||||
const timeouts: number[] = [];
|
||||
|
||||
let runCount = 0;
|
||||
|
||||
const cycle = () => {
|
||||
isRunning = true;
|
||||
activeRow = (activeRow + 1) % 5;
|
||||
|
||||
rowAlphas.forEach((alpha, index) => {
|
||||
let targetAlpha = alpha;
|
||||
|
||||
if (index === activeRow) targetAlpha = 1;
|
||||
else if (index === (activeRow + 1) % 4) targetAlpha = 0.12;
|
||||
else if (index === (activeRow + 2) % 4) targetAlpha = 0.2;
|
||||
else if (index === (activeRow + 3) % 4) targetAlpha = 0.4;
|
||||
|
||||
animate(alpha, targetAlpha, {
|
||||
duration: 0.05,
|
||||
onUpdate: (value) => {
|
||||
rowAlphas[index] = value;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
timeouts.forEach((timeout) => {
|
||||
window.clearTimeout(timeout);
|
||||
});
|
||||
|
||||
timeouts.push(
|
||||
window.setTimeout(() => {
|
||||
isRunning = false;
|
||||
}, 400),
|
||||
);
|
||||
|
||||
if (activeRow === 3) runCount += 1;
|
||||
|
||||
if ((runCount === 2 || !isActive) && activeRow === 2) return;
|
||||
|
||||
timeouts.push(
|
||||
window.setTimeout(() => {
|
||||
cycle();
|
||||
}, 50),
|
||||
);
|
||||
};
|
||||
|
||||
fnRefs.current = {
|
||||
activate: () => {
|
||||
if (isActive) return;
|
||||
|
||||
isActive = true;
|
||||
|
||||
runCount = 0;
|
||||
cycle();
|
||||
render();
|
||||
},
|
||||
deactivate: () => {
|
||||
if (!isActive) return;
|
||||
|
||||
isActive = false;
|
||||
},
|
||||
};
|
||||
|
||||
render();
|
||||
canvas.addEventListener("resize", render);
|
||||
|
||||
if (triggerOnHover) {
|
||||
const group = canvasRef.current!.closest(".group");
|
||||
|
||||
if (group) {
|
||||
group.addEventListener("mouseenter", fnRefs.current.activate);
|
||||
group.addEventListener("mouseleave", fnRefs.current.deactivate);
|
||||
|
||||
return () => {
|
||||
group.removeEventListener("mouseenter", fnRefs.current.activate);
|
||||
group.removeEventListener("mouseleave", fnRefs.current.deactivate);
|
||||
};
|
||||
}
|
||||
}
|
||||
}, [disabledCells, size, triggerOnHover]);
|
||||
|
||||
useEffect(() => {
|
||||
if (triggerOnHover) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting && active) {
|
||||
fnRefs.current.activate();
|
||||
} else {
|
||||
fnRefs.current.deactivate();
|
||||
}
|
||||
},
|
||||
{ threshold: 0.5 },
|
||||
);
|
||||
|
||||
observer.observe(canvasRef.current!);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [active, triggerOnHover]);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
className={cn(
|
||||
alwaysHeat
|
||||
? ""
|
||||
: [
|
||||
"[&.grayscale]:opacity-60 transition-[filter,opacity]",
|
||||
!active && "grayscale",
|
||||
],
|
||||
)}
|
||||
ref={canvasRef}
|
||||
style={{ width: size, height: size }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
/* eslint-disable @stylistic/array-element-newline */
|
||||
"use client";
|
||||
|
||||
import initCanvas from "@/utils/init-canvas";
|
||||
import { animate } from "motion";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
export default function EndpointsSearch({
|
||||
alwaysHeat,
|
||||
size = 20,
|
||||
}: {
|
||||
alwaysHeat?: boolean;
|
||||
size?: number;
|
||||
}) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = initCanvas(canvas);
|
||||
|
||||
let isRunning = false;
|
||||
let isActive = false;
|
||||
|
||||
let diff = 0;
|
||||
const defaultRowAlphas = [
|
||||
0, 0.2, 0.4, 0, 0.4, 1, 0.4, 0.2, 0.2, 0.4, 1, 0.4, 0, 0.4, 0.2, 0,
|
||||
];
|
||||
|
||||
const differs = Array.from({ length: 16 }, () => 0.2 + Math.random() * 0.2);
|
||||
|
||||
differs[5] = 0.6;
|
||||
differs[6] = 0.6;
|
||||
differs[9] = 0.6;
|
||||
differs[10] = 0.6;
|
||||
|
||||
const scaler = size / 20;
|
||||
|
||||
const render = () => {
|
||||
ctx.fillStyle = "#FF4C00";
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
for (let i = 0; i < 16; i++) {
|
||||
if ([0, 3, 12, 15].includes(i)) continue;
|
||||
|
||||
const maxAlpha = [5, 6, 9, 10].includes(i) ? 1 : 0.4;
|
||||
|
||||
const alpha = defaultRowAlphas[i] + diff * differs[i];
|
||||
ctx.globalAlpha = Math.min(
|
||||
Math.min(alpha, maxAlpha) - Math.max(alpha - maxAlpha, 0),
|
||||
1,
|
||||
);
|
||||
|
||||
ctx.fillRect(
|
||||
(3 + (i % 4) * 4) * scaler,
|
||||
(3 + Math.floor(i / 4) * 4) * scaler,
|
||||
2 * scaler,
|
||||
2 * scaler,
|
||||
);
|
||||
}
|
||||
|
||||
if (isRunning) {
|
||||
requestAnimationFrame(render);
|
||||
}
|
||||
};
|
||||
|
||||
const timeouts: number[] = [];
|
||||
|
||||
let runCount = 0;
|
||||
|
||||
const duration = 300;
|
||||
|
||||
const cycle = () => {
|
||||
isRunning = true;
|
||||
|
||||
animate(diff, 1, {
|
||||
duration: duration / 1000,
|
||||
onUpdate: (value) => {
|
||||
diff = value < 0.5 ? value * 2 : 1 - (value - 0.5) * 2;
|
||||
},
|
||||
});
|
||||
|
||||
timeouts.forEach((timeout) => {
|
||||
window.clearTimeout(timeout);
|
||||
});
|
||||
|
||||
timeouts.push(
|
||||
window.setTimeout(
|
||||
() => {
|
||||
isRunning = false;
|
||||
},
|
||||
Math.max(duration, 300),
|
||||
),
|
||||
);
|
||||
|
||||
runCount += 1;
|
||||
|
||||
if (runCount === 3 || !isActive) return;
|
||||
|
||||
timeouts.push(
|
||||
window.setTimeout(() => {
|
||||
cycle();
|
||||
}, duration),
|
||||
);
|
||||
};
|
||||
|
||||
const activate = () => {
|
||||
if (isActive) return;
|
||||
|
||||
isActive = true;
|
||||
|
||||
runCount = 0;
|
||||
cycle();
|
||||
render();
|
||||
};
|
||||
|
||||
const deactivate = () => {
|
||||
if (!isActive) return;
|
||||
|
||||
isActive = false;
|
||||
};
|
||||
|
||||
render();
|
||||
canvas.addEventListener("resize", render);
|
||||
|
||||
const group = canvasRef.current!.closest(".group");
|
||||
|
||||
if (group) {
|
||||
group.addEventListener("mouseenter", activate);
|
||||
group.addEventListener("mouseleave", deactivate);
|
||||
|
||||
return () => {
|
||||
group.removeEventListener("mouseenter", activate);
|
||||
group.removeEventListener("mouseleave", deactivate);
|
||||
};
|
||||
}
|
||||
}, [size]);
|
||||
|
||||
return <canvas ref={canvasRef} style={{ width: size, height: size }} />;
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/* eslint-disable @stylistic/array-element-newline */
|
||||
"use client";
|
||||
|
||||
import { animate } from "motion";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
import initCanvas from "@/utils/init-canvas";
|
||||
|
||||
export default function EndpointsExtract({ size = 20 }: { size?: number }) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = initCanvas(canvas);
|
||||
|
||||
let isRunning = false;
|
||||
let isActive = false;
|
||||
|
||||
let diff = 0;
|
||||
const defaultRowAlphas = [
|
||||
0.4, 0.04, 0.2, 0.4, 0.2, 0, 0, 0.04, 0.04, 0, 0, 0.2, 0.4, 0.2, 0.04,
|
||||
0.4,
|
||||
];
|
||||
|
||||
const differs = Array.from({ length: 16 }, () => 0.2 + Math.random() * 0.2);
|
||||
|
||||
const scaler = size / 20;
|
||||
|
||||
const render = () => {
|
||||
ctx.fillStyle = "#FF4C00";
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
for (let i = 0; i < 16; i++) {
|
||||
if ([5, 6, 9, 10].includes(i)) continue;
|
||||
|
||||
ctx.globalAlpha = defaultRowAlphas[i] + diff * differs[i];
|
||||
ctx.globalAlpha =
|
||||
Math.min(ctx.globalAlpha, 0.4) - Math.max(ctx.globalAlpha - 0.4, 0);
|
||||
|
||||
ctx.fillRect(
|
||||
(3 + (i % 4) * 4) * scaler,
|
||||
(3 + Math.floor(i / 4) * 4) * scaler,
|
||||
2 * scaler,
|
||||
2 * scaler,
|
||||
);
|
||||
}
|
||||
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.fillRect(7, 7, 6, 2);
|
||||
ctx.globalAlpha = 0.4;
|
||||
ctx.fillRect(7, 11, 2 + diff * 4, 2);
|
||||
|
||||
if (isRunning) {
|
||||
requestAnimationFrame(render);
|
||||
}
|
||||
};
|
||||
|
||||
const timeouts: number[] = [];
|
||||
|
||||
let runCount = 0;
|
||||
|
||||
const duration = 300;
|
||||
|
||||
const cycle = () => {
|
||||
isRunning = true;
|
||||
|
||||
animate(diff, 1, {
|
||||
duration: duration / 1000,
|
||||
onUpdate: (value) => {
|
||||
diff = value < 0.5 ? value * 2 : 1 - (value - 0.5) * 2;
|
||||
},
|
||||
});
|
||||
|
||||
timeouts.forEach((timeout) => {
|
||||
window.clearTimeout(timeout);
|
||||
});
|
||||
|
||||
timeouts.push(
|
||||
window.setTimeout(
|
||||
() => {
|
||||
isRunning = false;
|
||||
},
|
||||
Math.max(duration, 300),
|
||||
),
|
||||
);
|
||||
|
||||
runCount += 1;
|
||||
|
||||
if (runCount === 3 || !isActive) return;
|
||||
|
||||
timeouts.push(
|
||||
window.setTimeout(() => {
|
||||
cycle();
|
||||
}, duration),
|
||||
);
|
||||
};
|
||||
|
||||
const activate = () => {
|
||||
if (isActive) return;
|
||||
|
||||
isActive = true;
|
||||
|
||||
runCount = 0;
|
||||
cycle();
|
||||
render();
|
||||
};
|
||||
|
||||
const deactivate = () => {
|
||||
if (!isActive) return;
|
||||
|
||||
isActive = false;
|
||||
};
|
||||
|
||||
render();
|
||||
canvas.addEventListener("resize", render);
|
||||
|
||||
const group = canvasRef.current!.closest(".group");
|
||||
|
||||
if (group) {
|
||||
group.addEventListener("mouseenter", activate);
|
||||
group.addEventListener("mouseleave", deactivate);
|
||||
|
||||
return () => {
|
||||
group.removeEventListener("mouseenter", activate);
|
||||
group.removeEventListener("mouseleave", deactivate);
|
||||
};
|
||||
}
|
||||
}, [size]);
|
||||
|
||||
return <canvas ref={canvasRef} style={{ width: size, height: size }} />;
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
"use client";
|
||||
|
||||
import { animate } from "motion";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
import { cn } from "@/utils/cn";
|
||||
import initCanvas from "@/utils/init-canvas";
|
||||
|
||||
export default function EndpointsMcp({
|
||||
active,
|
||||
alwaysHeat = false,
|
||||
triggerOnHover = false,
|
||||
size = 20,
|
||||
}: {
|
||||
active?: boolean;
|
||||
alwaysHeat?: boolean;
|
||||
triggerOnHover?: boolean;
|
||||
size?: number;
|
||||
}) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
const fnRefs = useRef<{
|
||||
activate: () => void;
|
||||
deactivate: () => void;
|
||||
}>({ activate: () => {}, deactivate: () => {} });
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = initCanvas(canvas);
|
||||
|
||||
let isRunning = false;
|
||||
let isActive = false;
|
||||
|
||||
let activeIndex = 5;
|
||||
const rowAlphas = [0.12, 0.2, 0.4, 0.4, 1, 1, 1, 0.4, 0.2];
|
||||
|
||||
const scaler = size / 20;
|
||||
|
||||
const render = () => {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.fillStyle = "#FF4C00";
|
||||
|
||||
for (let i = 0; i < 9; i++) {
|
||||
ctx.globalAlpha = rowAlphas[i];
|
||||
|
||||
ctx.fillRect(
|
||||
(5 + (i % 3) * 4) * scaler,
|
||||
(5 + Math.floor(i / 3) * 4) * scaler,
|
||||
2 * scaler,
|
||||
2 * scaler,
|
||||
);
|
||||
}
|
||||
|
||||
if (isRunning) {
|
||||
requestAnimationFrame(render);
|
||||
}
|
||||
};
|
||||
|
||||
const timeouts: number[] = [];
|
||||
|
||||
let runCount = 0;
|
||||
|
||||
const cycle = () => {
|
||||
isRunning = true;
|
||||
activeIndex = (activeIndex + 1) % 9;
|
||||
|
||||
rowAlphas.forEach((alpha, index) => {
|
||||
let targetAlpha = alpha;
|
||||
|
||||
if (index === activeIndex) targetAlpha = 1;
|
||||
else if (index === (activeIndex - 1 + 9) % 9) targetAlpha = 1;
|
||||
else if (index === (activeIndex - 2 + 9) % 9) targetAlpha = 1;
|
||||
else if (index === (activeIndex - 3 + 9) % 9) targetAlpha = 0.4;
|
||||
else if (index === (activeIndex - 4 + 9) % 9) targetAlpha = 0.2;
|
||||
else if (index === (activeIndex - 5 + 9) % 9) targetAlpha = 0.2;
|
||||
else if (index === (activeIndex - 6 + 9) % 9) targetAlpha = 0.12;
|
||||
else if (index === (activeIndex - 7 + 9) % 9) targetAlpha = 0.12;
|
||||
else if (index === (activeIndex - 8 + 9) % 9) targetAlpha = 0.4;
|
||||
|
||||
animate(alpha, targetAlpha, {
|
||||
duration: 30 / 1000,
|
||||
onUpdate: (value) => {
|
||||
rowAlphas[index] = value;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
timeouts.forEach((timeout) => {
|
||||
window.clearTimeout(timeout);
|
||||
});
|
||||
|
||||
timeouts.push(
|
||||
window.setTimeout(() => {
|
||||
isRunning = false;
|
||||
}, 300),
|
||||
);
|
||||
|
||||
if (activeIndex === 7) runCount += 1;
|
||||
|
||||
if ((runCount === 2 || !isActive) && activeIndex === 6) return;
|
||||
|
||||
timeouts.push(
|
||||
window.setTimeout(() => {
|
||||
cycle();
|
||||
}, 30),
|
||||
);
|
||||
};
|
||||
|
||||
fnRefs.current = {
|
||||
activate: () => {
|
||||
if (isActive) return;
|
||||
|
||||
isActive = true;
|
||||
|
||||
runCount = 0;
|
||||
|
||||
cycle();
|
||||
render();
|
||||
},
|
||||
deactivate: () => {
|
||||
if (!isActive) return;
|
||||
|
||||
isActive = false;
|
||||
},
|
||||
};
|
||||
|
||||
render();
|
||||
canvas.addEventListener("resize", render);
|
||||
|
||||
if (triggerOnHover) {
|
||||
const group = canvasRef.current!.closest(".group");
|
||||
|
||||
if (group) {
|
||||
group.addEventListener("mouseenter", fnRefs.current.activate);
|
||||
group.addEventListener("mouseleave", fnRefs.current.deactivate);
|
||||
|
||||
return () => {
|
||||
group.removeEventListener("mouseenter", fnRefs.current.activate);
|
||||
group.removeEventListener("mouseleave", fnRefs.current.deactivate);
|
||||
};
|
||||
}
|
||||
}
|
||||
}, [size, triggerOnHover]);
|
||||
|
||||
useEffect(() => {
|
||||
if (triggerOnHover) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting && active) {
|
||||
fnRefs.current.activate();
|
||||
} else {
|
||||
fnRefs.current.deactivate();
|
||||
}
|
||||
},
|
||||
{ threshold: 0.5 },
|
||||
);
|
||||
|
||||
observer.observe(canvasRef.current!);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [active, triggerOnHover]);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
className={cn(
|
||||
alwaysHeat
|
||||
? ""
|
||||
: [
|
||||
"[&.grayscale]:opacity-60 transition-[filter,opacity]",
|
||||
!active && "grayscale",
|
||||
],
|
||||
)}
|
||||
ref={canvasRef}
|
||||
style={{ width: size, height: size }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
import { setIntervalOnVisible } from "@/utils/set-timeout-on-visible";
|
||||
|
||||
import data from "./data.json";
|
||||
|
||||
export default function HeroFlame() {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const ref2 = useRef<HTMLDivElement>(null);
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let index = 0;
|
||||
|
||||
const interval = setIntervalOnVisible({
|
||||
element: wrapperRef.current,
|
||||
callback: () => {
|
||||
index++;
|
||||
if (index >= data.length) index = 0;
|
||||
|
||||
ref.current!.innerHTML = data[index];
|
||||
ref2.current!.innerHTML = data[index];
|
||||
},
|
||||
interval: 85,
|
||||
});
|
||||
|
||||
return () => interval?.();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="cw-686 h-190 top-408 absolute flex gap-16 pointer-events-none select-none"
|
||||
ref={wrapperRef}
|
||||
>
|
||||
<div className="flex-1 overflow-clip relative">
|
||||
<div
|
||||
className="text-black-alpha-20 font-ascii absolute bottom-0 -left-380 fc-decoration"
|
||||
dangerouslySetInnerHTML={{ __html: data[0] }}
|
||||
ref={ref}
|
||||
style={{
|
||||
whiteSpace: "pre",
|
||||
fontSize: "9px",
|
||||
lineHeight: "11px",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-clip relative">
|
||||
<div
|
||||
className="text-black-alpha-20 font-ascii absolute bottom-0 -right-380 -scale-x-100 fc-decoration"
|
||||
dangerouslySetInnerHTML={{ __html: data[0] }}
|
||||
ref={ref2}
|
||||
style={{
|
||||
whiteSpace: "pre",
|
||||
fontSize: "9px",
|
||||
lineHeight: "11px",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
[
|
||||
" \n \n \n \n \n . . \n .. ..+ \n .:. \n .. .. .:: \n +.. ..: :. \n .:..::. .. .. \n .--:::. .. ... .:. .. \n .. .:+=-::.:. . ...-.::. .. \n ::.... .:--+::..: ......:+....:. :.. .. \n ....... ::-=:::: ..:-:-...: .--..:: ......... \n .. . . . ..::-:-.. .-+-:::.. ...::::. .: ...::.:.. \n . -... ....: . . .--=+-::. :-=-:.... . .:..:: .:---:::::-::.... \n ..::........::=..... ...:-.. .:-=--+=-:. ..--:..=::.... . .:.. ..:---::::---=:::..:... \n ..........::::.:::::::-::.-.. ...::--==:. ..-::-+==-:... .-::....... ..--:. ..:=+==.---=-+-:::::::-.. \n . .....::......:: ::::-::.---=+-:..::-+==++X=-:. ..:-::-=-== ---.. .:.--::.. .:-==::=--X==-----====--::+:::+... \n ..-....-:..::-::=-=-:-::--===++=-==-----== X+=-:.::-==----+==+XX+=-::.:+--==--::. .:-+X=----+X=-=------===--::-:...:. .... \n ....::::...:-:-==+++=++==+++XX++==++--+-+==++++=-===+=---:-==+X:XXX+=-:-=-==++=-:. .:-=+=- -=X+X+===+---==--==--:..::...+....+ \n ..:::---.::.---=+==XXXXXXXX+XX++==++===--+===:+X+====+=--::--=+XXXXXXX+==++==+XX+=: ::::--=+++X++X+XXXX+=----==++.+=--::+::::+. ::.=... \n .:::-==-------=X+++XXXXXXXXXXX++==++.==-==-:-==+X++==+=-=--=++++X++:X:X+++X+-+X X+=---=-==+=+++XXXXX+XX=+=--=X++XXX==---::-+-::::.:..-..\n",
|
||||
" \n \n \n \n .. \n . .+. \n \n .: \n : .. :. \n .. ... .. .. \n :...+. . .. :. . \n .=-::... . . ... .. \n .. .--=-::... .....=+.:. . . \n -:.... .:-=:...: .::...... .:. .. . ... \n .. .. . .:: :.:: .:-::.. . .-..:.: ........... \n ..= . .. .::-==.. .-=-::... ..:.. . ..:::.::.:... \n .+.:.:. ..-.:: . . ..:. .:=-==-::. .:--.. .. .. ... .:---:::::--::..... \n .. ..+::.......-::..: . ::--.. ..:::-==-:. ..::.. .:... ..-.. =..:=== ::::-+-=:...+.=.. \n .....= ....:::..:::::- ::=:. ..:=--==+:. .:-::-=-=--:... .:-..... .:-=:...-+==--:+:-+-=-:-:.:+... \n .....:...::.:: ::::-::----=+-::--:---+=XX=-:. ..-=-=::--==X==-:....-.:--::.. .::++-::--+=---:-:---=-=::...-.. \n ....:..:....:=:- ==--=-:--===++====---- -==+==-::--==-:-::--=XX ++=-:::---+===-:. .:-X+ ----=X==----: :=--.--::........... \n .+.:::::-..:-:-===++=++=++++X++=====-.--=X==++==----=--::+:-=+XXXXX++---=-==+++=:. ..:-+++---=+XX+++=-::-===-+=--:...:.......... \n ...::--- ::::--=+=+XXXXXXXX+++=====+===--=---==++==-=+=--::-==++XX+XXX++=+X==+XX+=-::-::--==++X++XXXXXXX=-::-+=++X+=-:::::::::-:=+..... \n ..: :-===----=-=+++++XX+XX-XX++X+==++==+=--:--==.XX+==++.===+++XX.++++XX++=X+=++XX+==--=--===+ +XXXXXXXXX+=--=X++.XX==--=-:---:::::::..:.\n",
|
||||
" \n \n . \n . \n \n . : \n . .. \n . .. . \n . \n :.. . . \n .::... . \n ==-:: : . \n . . .-.=::. .:: ..+ . .=. \n . :.. ..+:..:-. .:.. .: .+. . .. \n . . .:.:-:. .-=-... ..-. ...:.. .. .. \n . . .. .. :. ..::--:.. .::-. . .: .:::-:....-...:. \n .. ......:: ::. .::+:::: .:=::==:.. ...... ... . . .::----:...::::::.... \n . .:.....:::::=:-. ..::--=-.. :-::--==.::.. . .: .. .::-:..:==--::..:::::.:.....: . \n +.. ...:-+...:.: :=---::..:::::-=+=:.. .::.:::--.++--:.. ..-:..+. .:=-::::==-:::::::-=-:.::... . \n -... .. .. .:---:+:-:-::-=+==--=-::.::-X=--::..::::.:.:--=XX+==--::::.:-+=-:. ..-=-::-::=++-::-::.:---::.. . \n ......... .:::-=+== -==+= =====--=-:::::-+=+=--:.:::..::.:-===XXX+=--::--+==+=-. ..-==-:.:-=XX=----:.:-=-=--:.. .+..... \n ...:::+:...:=:-+++XX++++.=-=-==-- ===-:.:=---=+=-:::--::..:-==++X+X+==--=+==+X+-:.......:-==++==++X:X++=-:::-==+=--:.......::::..+. \n ...:---::+:::--==++-XX++XX+=:=++=--====-:::+--==+=---== ---==++XXX++XX+.+XX===+X+=-:::-:--=====XX+-++XXX+-:--++=+X+--::::.:::::::..... \n ..::.-==---+- ++++.+++XXX-XX++:X+=-=X+==:-::.=+X+XXX+=+X++++XXXXXXXX=XX=- +++-==++X+==-==-=:=++XXXXXXXXXX++==+ ++X X+=---.--.-:.:::.:... \n",
|
||||
" \n \n . \n : \n . \n . \n . \n \n \n :. . \n :-....:. .- \n ....... .. \n . .. .. :. \n .. ..:.:. .-.. . ...:... \n . .. ..... .:--:.. .::... . . .. ........ .:. . \n .. ..... ...: ..=..::-. .:.:=-:. . ... .. .:.:..:-...-:......... \n . .:: ..::.:.:.. . .:.::-.. ..=.:--==:.. . .::..+==::::+........ . \n .:.......:.:::.....:::..::==:.. :.....::-+X-:::. .-.:... ..-:..::=::-:-...:.:::... \n .. :::.:.-.:::-=---::=::...:--==-:. ..= ....:-++=--=-::.+...==-::. .--:....:=-:-:::.... ::.. \n +.... .::-==+-----------------:::..-==--::. ... ..::-=+:+++=-:::.-- =+=-:. ..=--:::::=++-=::-::.:=-::... .. \n ..:-.... :..:-+++=+=-===-------::--.::..:------:... :....::-=++XX+=--::----+X-:. ..-==-----+X+==-=-:::-=+=-::.. .....:...- \n ...:::::....:-- ==+X+====-----=+::-==-:::::----=-:.::-::::--=+XXXXXX+=-=+=-==++--:-:::.::=--=-==X++++X+=-:=:===++--::......:...... \n ...:----:::--.====+=++X+++======---=+=--:-::=+++X++=-====== ++XXXX.XXX +++-=--=+X+--::--:---==+XX+++XXXX+=--=+=++X+=-:::-::-:::....... \n .. ...:::--==:---++=+=====++ XXXXX++===+X+===+==-+X++++++++X+X++.XXXXXXXXXXXX ++======X+==-=--===+XXXXXXXXXXXX+=+++++X+XX=.--==--::-....:....\n",
|
||||
" . \n . \n \n \n \n \n \n \n . ..: \n :. . .. \n ::.. \n . .. \n .: :::. :. .. \n .:..:. .:-. ::. . .: .. ..... \n ... . ... . . ... ..::-:. . . ... .:........:. \n .. =.... . ... .. .:.:-+-.. ....-=:....:.=... .. \n ... -..=::... . ......:::-: .::=+-:::+. ::-.. .:..::=:...:.......... \n ....:. ...:....:::=:.:-.. .::=--: ..:==:-::::. . .:---:. .:=:::. :::-.-...:...=. \n ..:.--::::::::::::::--:-:....--:::... ..:-==++---:.=.-::-=+-:. .:::.....--=-:.-.::.--:... \n ....... ..-=---==-=-==-::.-::+:--::..-:---:::....... ..---=+XXX+=-+:::--+X-:.. :--:.::::-++-::::::::==-:..: +.... \n ..:...:....:-=-=+:+=--==::--::-=::--:...:--.:-::....::::---+=+XXX.+=-:-----+=-::..... .::--:---=+++==--:..:-==+-:::.. ......+-. \n ..:-::::..::---=====+=== -:--:--::-==-::-::-==-==----=--::-==+:XXXXXX+==+=--==+=::..:::.::---++.+=+++X+=-:=:===+=--::...:..::...:. \n ....:-------==-=+=====XX+X+==------+=---=:==+=.==+=+++++X+==+XXXXXXXXXXX+++==--==+=-:-:-:---=X XX++XXXXX++===+==+++=------::: :....... \n .....-.:+:--==+======= ++=++X:XXXX+=:==XX==:++=-=++==+X+=+++:+XXXXX:-X.XXXXX+X++==-++++X+======++XXXXX-XXXX--XXX+==+++++X++==.---::::..=:. ..\n",
|
||||
" \n \n \n \n \n \n \n . \n . . \n :. \n .. . .. \n . .. ::. . . \n . . .:+.. : .. . \n . . . ..-- .. .. ..: \n . .. .. . ...-:... . ...:-.. ... \n . ....-.-. ..:.. .. :: ...-::...... .. .. .:.:::=....:. .. \n -... .....:...:.:-. ...=.:: .:=-::::::. .::--. ....:.:. ..:.. ..: .. \n .::::::.:..+:....:..-=:-:. ..:..::. ..=------::...:..:=-.. ........::-:.. ..::.:. \n . . :::----:::::=::....+:=-:-..-::-::-... . .:-==++++++-:.-::-+-... .::.... .:==::.....:--+:.. . . \n .... .. .:-=-=+==---:-:=:::..::::-:..:-:::::::....=.:..::--=+XXX-=-:-:-:=--::.. :.::..:::-++--::...::==-:.:.. ....+. \n ...-.:.. ...:-==--=+--:-::::..::.:--:..:=:-+-:---::--:::+:-==++X:XX+-==--:--==-::.... ..::::-++==++:-::..:-==-:::.. .....+=. . \n -..:::::::--::--==--++=+==--::::::-=-:: =-=-- --=+=+++=++-==++XXXXX- X+==+-----==::..:-.::--=+.++=++XX++=-:-======:--::.:.-...:... \n ......:=:--=----=- -=-===XX+XX+=----.-+=-:-==-+==-.-=+=====-+ X+XXXXXXX:XXX+========++=------+++XXX+.+X=XXX.XX+======+++---:::-.:.=..:.... \n .......::::-=:===++ ==++=++XXXXXX:++++++=====++=+X++:=++=====++XXXXXXXXXXXXX+X++-===++XXX++=+==+X++XX++X-XXXX-XXX+==+++=+X:===-+-:::=..::....\n",
|
||||
" \n \n \n \n \n \n \n \n \n . .. \n ...... \n ..::. \n : . . . \n ... : ....+: . . .. \n . .:. .... . .:. .... . ...:. ..-:.. .. \n . . ..- ...=.: ..:. .:-::::..=.. .:-. ...... .:. :.. \n ....::. .. -.......=: :::. ...-.. .- --+::::---.. ..:.. . .. +..::. ...:. \n . .:::::-:....::-........---:-...::..:. ....:--=--===+=:....:-.... .. .. .:-:.. ..::. . \n . .:::--:--::=:::::. ..::-: ..::-....: ..:......:--===+=X=-::.---::-.. ..:....::.==-::.. :---... . .-. \n .. ...::-:-:--::-::-::. ....::-...----:::::.:=:::-:::-====+XX+=--:::=--::. .::::-=== =+=::....:=-:..-. .... \n .+...... :::::-----=.--:-::...=..:=:...:---::.:--==+--=+:--+:+XXXXXXX=-==:+:---:::...:...-:--++===+XX=-::::=----:::...:...... . \n .....:=:- :::-:::-+--++===+=-::-.::-=:::-=+=--:.::-==+-:=++ +XXXXXXXXX++==+=-=+--=--::::::---+++==++XX-X+=+ ==---===--::.+....=.... \n .=....:.::.==--:-==--====XX XXX++=++=== ==--==+X+==---------==+XXXXXXXXXXXX++==== =+==++-=.=-=+++:=++=++XXXXXXXXX+=====++---:-::..:...:.... \n ......:::--=++= ====-===++XX-X XXXXX:X+====-==+XX:+++========+XXXXXXXXXXXXXXX++=++XX +++++=+++X:X+XX.++XXXXX .XX+++=:+++X+=.---::::...:=:...\n",
|
||||
" \n \n \n \n \n \n \n \n .: . \n . .... \n \n :. ... \n . . ..:. . \n . ..:.. .. .:: .. . \n . .. .:. ..: .:...... .:.. .. :.. .. \n ..-::: .:... .:=:. .. :. ..::=.:: :::=:. . .=. .. . \n ......:.. ..:-.. .. .:-:.. :.... . . .::=-------+-:......:. .. .+.:. .. \n ...::......-:.::. ..:-:....:::.. .: ....:--=--=-=+-:.::::.... .. .::-=-::. :.... \n ..:::.:.::..:..::.. ....--=....=::=:.:...-..:-:.::==--=+++--::---:.... ...::-:--=+:.. .:.:.. . \n .....:: ..:-::--:+:.:-.. .:=..:-..=.--:....::-+-::=-:-.-=++==+XX+=-=-::-::... ... :.::-=-=++X=-:..:::-::.:....... \n ....::.........---====--=-::.:=:.:--=::-=.::....::--::==== XX +-++XX++==.-=-:. :::-..:...:-- =-.==+X++------------:..... .. .. \n .....+.-:::::::-:::+--=X++ ++=====+==---:--=X+=-::.::::--:--=+XXXXXX++X++-====--==:--==--=:-=-=+==+-==+X.XX++++:=----=-::..:.. ....=.. \n . .. ...::+-==------:--==XXXXXXXXXXXX+=------+++X+===--=-----==+XX-XXXXXX:-++==+=++=--==++=++XX++==++==++XXXX:X++++=-===+=--:+::.....=-.... \n ......::--==.+++=+===.=++XXXXXXXXXXXX=++=.=--=+++X+++==+=====+X-X-XXXXXXX X+++++++++++===:==:++XX+XXX++XXXXXXXX.+++++XX+XX+=-=-:::+:::.:.:..\n",
|
||||
" \n \n \n \n \n \n \n . .. \n . \n .. \n . \n .. .:. ... \n ... ...... .. . \n . .. :. . ..:. .:: :. \n +... . ..:. . . .. .:.:..:=:. . . . \n .. ..: .:: ... . ..::-::::.:.:=-. ... .. ...::... \n .:.... ..+.: . ::. .::.. . ..::.::::==::--:..:.. . . .::-:-:. . \n . ........... .-.. .. .:. .-:...... .. ....::-=-::--=-::.:::. .. ....:+:-==.. .. .. \n :...... .:.-:.+....-....:. ..:: .::-.. ..::-:..=::::--+=--==+==-=-:.:...=. ...::---+=::... :::......... \n .. ...- ......::==----:-::.::=::+:-:.::-:.... ...-:::---=+==+:====+++++=:: . ...:: .-. .=.::--=-+=--::::::=-.:::. \n .......:......:..:--X+==:=+=:--=-=-:::--==+=--:....::-:.::-=XXXX++++X+===-:-::-:::.-:::-:=::-=------==++==-=+==-::+::.. .. .. \n +:......:-- ::-::::--+XXXXX++===--=-=-::-+++++==--::::::::-=+XXXXXXXX++==-==-----::--=++++++===--=---==+X++++====----=-:..... ......... \n .....:::::-=+==-+=+--==+X-XXXXX+=X+==--=---==+XX+==+--------=++X+XXXXX+XX++=-==++==------=++++X+=+++==++XXXX=X++==+===:+== :--:............\n .......:--.-====++.X+=++XXXXXXXXXXXXX+==---.===X+.+X+X++=.++++=XXX.XXXX+XX=X++++ +XX+X+==--====+XX+XXX XXXX XXX X+=+X++++++===-=-: :::=::....\n",
|
||||
" \n \n \n \n \n \n . \n \n \n . \n .. \n .. ..... .... \n . .. ..: .::. . \n . .: . .... .:-. .. \n . ::. .=.:..::.. .-=. .. .. -.. \n ..: .: :..= .: :-::+:::.:-::-... .. ..:-:-:. \n . . ..:: .. .: .-:.. . ..+..::--:::--:::=:.. . .. ..=::=-. . \n .......::... ..:.=. .. .: .:.......... .-...::-+-:--=--:-:::. .. .=..: :=-.. ..:.. \n . .:.:=::=:..::....::..::: ...:.. ..+::.::.:-:--==:--== ++==:.:...... :... ...:=:-=-::.:.:::-:.-... \n .... .. ...:=+--=--=--:::---:::::::--.... ..:::::::-+X+===--==+===:-:: :...:-:::... ..::::-:-===---:::-- ::::. . \n .......:....::..::-XX+:+++=---------::=++===--:..-::.:.::-=XXXX+.++++=----.::::::+--===---:---:::::==+=======--::+:::.. .. .. \n =.....+::--:::=::+:-=+XXXXXX+=-=::-:-=:--==+X+==--::.:=:::-=+X=XXXX+X+==--=--=- -.::--==XX+==+===---+==+++:-=====--.-=-::.:.. ...-....= \n ....=.::+::---===X==-===XXXXXXXX+=X+---------=++++==++---==+=++XXXXXXXXXXX++====++==-------===+X+==+++++XXXXXX+=+=++====+=-=-=-::..-.::.... \n .... .:.---=:====+X++.+XXXXXXX+.+++++=-- -==++XX++X+XX+++XXXXXXXXXXXX+=+XX.++++=XXXXX+==--=-==++X.XXXXX XX= XXXX++.X+++ += =--=-::::::::.-..\n",
|
||||
" \n \n \n \n \n \n . \n . \n \n . . \n . . . ..: \n . .:. :: \n .: .+. :: \n .. ... .. . .. ...= .:.:: .. .:. \n .: .. .:.. ....:..::..-.:..:.. ..:::+ \n ..: .. .:. ... .. :-..::..::. . . ...:. . \n .::. :. . : . .. . ...+::--:.:----+.... . . ...:: .. \n ..-:..... .-.....-..::... .::. .:..::..::----.:.:--+-=+:.::. .. .: ...-::... ....:+.. \n . .. ..:=-:--: :-:....:----....:::.. :.:.....:-=++=-:::----:=:.. :...::..-.. . ...::- -+::::::=:.....+ \n ...........:=X=++++==-:..::::=:-+=--:-::.....+..=.:-X.++X+=-===-:--:....:.:: :-=-:-:.-::..::---------.::.:..... .. . \n ......-:::=-:.::-=+XXXXX++--::..::-:==-==+=--:::...:..:-++XXXX++++=--::-:::::.:..::-++=---+---::::--==.==-----::::+:..::. ..... \n ......-::::--==-:-=-=+XX=XX+=--=-:::::::-=:=++=--:=:.:-====+XX:X-XXXX+==----+=.--: ::::====++==-----+=+++++++==------.:----:. . ...... \n ...:..:::::-----==+===++XX XXXX+=+==--:::---=++X+X++==--=++XX X++XXXXXXXXX+++=+XXX++=--:::--==-= ++++ XXXXXXXX:+++X+ ===----=--.:::..::..: \n .....:::--===--==+XX++XXXXXXXXX+++===------==+X+++XXXX:++XXXXXX+++++++=+XXXXX+++XXX-+X+= ----=.=+XXXXXXXXXXXXXX++XXXXX+++=.---==-:--:::-... \n",
|
||||
" \n \n \n \n . \n . \n \n . . \n . \n : \n . . . \n .. . .: : \n .. . . . .. ... . . \n .. . . ....+ .. ... . . \n .. . . ... . ..::. :...:-. . . \n ... .. .. .:. :.. . .::.. ::.:::.::-. . \n ......... ..: ::..:-. .:. ... .. :-::.....::-.-:.... .: .... .. \n .:--::::..::. .::-:.=....:. ........:.:-=-:...:::::: .:.-....:. . ..... ....+.=...: \n .:.. ..:++=-=X=--:.......::=:---:::+.. ..=.:-==:-==++=:::::::. ....::::-::::.... ..:::=.::-::.... .. \n ...::-: ..:-+X+XX +==--.. ..:-==-----:-:.: .:---++++++++++=-::..:.. .......:====--:::::...::-:--:-::.::+..::..... +.. \n ..+....::--:::.:-=+XXXX++=--:.....::-=---=+--:.:....:-==+++X++=++=-::::::--:::+....-==---=--=:.:::--===+==-:-:::::::::::.. ..... \n :.. ......::+::-=--- ==+XX:XX+=----:-:.:::-==+=++=-:::+:-==+X:X+++X++X++==-==+X++==-:...:-=---=========++X-XXX+=+==----:+:::::......==.. \n ...+.::::---.::-=+XX=+X=:XXXXX+===--=--:=:-- ====X+===-=++=+XX+X++++XXXXX+X.+++++X++==-::=:-+-==++=+++XXXXXXXXX. :XX++++=-:- -==:::::::... \n .....::--=====--=+XXXXXXXXXXXX-+++++X+=---=++++-+XX++++XX++XXXX+++++++=+XX=XXX+XXXXXX++=------==+XXXXXXXXXXXXXXX+XXXXXXX+=---==----::=::... \n",
|
||||
" \n \n \n \n \n \n \n . \n . \n . \n . . \n . .. \n . . .. .. .. \n : .. ....... . ..:. \n . .-. .. .:. ....:.::. \n . . .. .. .. .. +..:.. ..-::...- . ..= \n .::....:+..: .:. ....... ...........:::::...:.:.. ..- . . ..:... \n . . .-=-:-===--:. .:+::--=.... .:-==---:------::..... .....-.+.:::. . .......... . \n ...... .=.-++==+X++=-.. .:.--:::::.:.. ..-X==-===+==-- ::.... ...-------...... :..:.::::-.+... ..... \n .+..::....:-=++++X++---.. ...--.:-:--:-: .. ..-== +X++====--:..::::........:--:::::::-:.. :::--==++-.::.......... . \n .............::-::: ==+=+XX++--::::....::--==:=--:.=...:-==++:++++====-::-=X+=-=-.-:..+--+:.:-::----=--==++XXX ==-::::::...::.. ...... \n ....::::::-=::=-===-=+:XXXXX+=-=-:::: :..::-=+=+==-.::========++.+XXX:+X++==+==X+==--....:-:---=-=--=+:++X=XXXXXX.+====+::::-::..:+:.... \n .+..::::-==----:+X++++XXXXXX+== ==++=-::--=++X++X+==++X++-++==+++++XXXXXXXX++++X.X++=--::.:::-=++++++=X=XXXXX+XXX+X++XX+--::=--::::::... \n ....:: --=====---=+++XX.X=XXX++=+++X+=====+XX XX+XXXXXXXXXXX++= ++++X+=+XXXXXXXXX+X-XX..=-- =-==XXXXXXXXXXXXXXX+.XXXX-XXX=--------:::::.-. \n",
|
||||
" \n \n \n \n \n \n \n \n \n \n . . \n . . .. .. \n .: . . .. \n . . .:.. .. .. \n .. . .. .+... . \n .. .::..: .. ... ..............:.+. . .. . \n .:..::::---=. ..:---:. . .=--=-:.-::..=:-:. . .. ..... . . .-. \n . .:==-----+=-:. :--:.=.-..... . .-===::---:-:::::. ..::-:--:. . .:.=.::--.. . \n ..... ..:-=== ===.-:: .::::..:: .:.... ..:==-=+=+=-=-=::.+.:.... .:-:::.. ...+... .:.:::-=++::.. .=.. \n . .. ..-...:..::+====.++=-:+::.... .::::---=::.. ..:::-=+==-+X+===-:.:-+=--::=:: ..::.:. .:..:+::=:::--=+XX+---:...::.... .. \n ..:.+....:--.::-:---=+X++++==-:::::......::==++--:..:==--=-=--==+X==++=---+-=+==--::. .:::: :-::::--+=--=+XXXXX++=---:=-:.:....=.... \n .. ..::::-=-::=++==+XX.XX+X+----==.--::.:-=+X+X+===-=X+===----== XXXX++++==++++++==-:....:::.==-=--==XXX=XXXX-X:++==-++=:::--:::::....= \n ...:.-::-====-.-=++:+XXXXXX+==-==++=----- =XXXXXXXXXXXXX++=---:==+X.XX.+XXXXXX+X+XX++++-:.::--=X++:X =XXXXX++.XX++X++X+++--::--:-::::.:. \n .-..::.-=====-=-=+++XXXXXXXX+:+++XX+=+===+XXXXXXXXXXXXXXX+======++XXX++XX XX.XXXXXX=++X=--====+XXXXXX+XX+X X++++XXXXXX++=--- ---- :::.: .. \n",
|
||||
" \n \n \n \n \n \n \n \n \n . \n \n .. \n . \n . . .. \n ... : .. ......... .:.:. \n .:..+....-:. .:::-:. .:-:--=:.+.... .. . .:... \n .:==-::::-=:: .:::..... . .:==--:--::.-::.... .-.-:::. ... .::-:. \n . ..--=-------:.. ..::....:::.-. ......==--:--=-:=-+.+....... .:-:....- . .....:-==-:. \n .:......::-==--==--:..........::::=---.. ..:==--==---=-----...:=-+--:......:.... .. ........::--=++=-:.....:.. \n .....-.:::::::::-==+==--:-:.:.:... ...::-++=--:...:=---.-=---+=-==-::+==----.:-::....::..-:....::==::--=+X++=-::::+:-:.... -.. \n .......::-=-:=+=--++X++==-=-::-==::::.:.:-+XX+==---+==--=::::--+X=====--====-=+:--... ....:-=-:-::-=++++++XX++==.----==-::-:...=... \n ..:.::::------====+XX+X.++=---.==-:-::.:-=XXXXXXXX+++=- --:::--+XXXX+++.==++++++==-=-:....--++:===+XXX++XX+++X++==-===--:::::::::.... \n ....::---------==+=+X+++XX+=====+++==-:--=+XXXX+XXXX-++==--::--=+XXXXXXXXXXXXXXXXX+= =-:=:-- +XXXXXXXXXXXXX++=++++++++=--::-- :::....+ \n ....::-=====--=-++++X++++XXXXXXXXXX+++==+XXX:X+XXXXXXXXX=--.-==+XX-+XXXXXXXX+XX+XXX+++++=+++=+XXXXXXXXXXXX+++==+.++XX++=-=--=--- ::..:. . \n",
|
||||
" \n \n \n \n \n \n \n \n \n . \n \n \n .. .. \n .. . ...:. \n . .:. ...... .:=:-=-:. . . \n .:--::..-.::. ........ .. :-=-::::::.-..... ....:. .....:. \n .::--:=:::::.. ..- ...:.:. .::---:::::::--+.... .... .. .... ::-.. \n .... ...:--:-::-::.::.. .......--::.. ..:=.---:::--:::....:--:-=. . ..... .. ...:..:.:+:--- .. .. \n +...::.=..::-==--:::::::::.... .::=+=-::.....:::--.---=----:..:--::::.:.::...-+...=-.:. ..:==:. ::- ===-::....-:.. \n . .-.:-:--=--:=+X+=--=:::::---:....::-=+X+=--::.--:+:..:+:=+==---:--=--::-::-:... ..-=:::::::-===--=+++==-:::::::::::....... \n .......:: ------==++X+== ---::==-+::.:.:=+XX=X+++==--::....-:-XX+=++++========---:-:. .:-+=---=++++ +=+X+=====-------:::........ \n ... .:::-:---==:===++XX+==---+++X==-:.:-=+XX=X+XXX+==-::.:+:-=XXXXXXXXXXXXXX-++==--:....:--X+XXX-XXXX-XXX+==+====-=---::::::..-=.... \n ..::=.-------=+-===+++XX++==+X.XX+:+---=+XX:XXXXX:++:=-:::-:==+XXXXXXXXXXXX++ X+=====--===:+XXXXXXXXXXXX+==--=++X++.=--::--=:::...... \n .... ::-==--=-=+X+XX+== ++XXXXXXXXXX+++++++X++:XXXXXXX+X+==---=++=XXXXXX:XXX++ ==XXXXXXXXXXX++XXXXXXXXXXXXX++====++XX:+==-= ===--:.....:.:.\n",
|
||||
" \n \n \n \n \n \n \n \n \n \n \n . . \n . .....: \n . .:-:--::. . \n .::=-... .. .......... .:-::....=. . . . . \n ..:::::....... . ... . :.:-: :.::. .... . . ... . . ... ...... \n .. ..::=:::.......... .-=..:. .:::-:::.::..... .::---:. :. .:.+.:....:::. \n .-....::-=--::.:....:.:. .-.---==-.. ...:..:::::=-:.::..:--::..... =.. ...=-. ..::.....::--+-:... .. \n ..:---::--=X=--:.:.:.:-:-:.....:--=+X=::..=:::.=...:-+=---------::-::..::.......:-:....=.:----::-==-=--:......:::.. . \n .-..:--:::-=--++=-:+::::---=::...:--=+XX+=++=--::.....::+X+++=+X+=====-.-:::::. .:--:::-==-+-=:==+.+---:.::.::::.....- .. \n .......::-::------=+++==-.--=-+X+==:..:-=+XXXX++X==-::..:::-+XXX-XX+XXXX+===+-=-:.. ..:::=X++.XX++++++++++--=--::::::-:-:. ...:... \n ..:::::--::-+==-==-=+++=====++XX=++::.:-++XXX.++++==-:...:--=+XXXXXX X+XX===X+==-+--:--:-=+XXXXXXXXXX+++= ----+==--=-:::--:........ \n ..:---=---==X+++=-===+XX++==+XXX===.===+++ +XX++====+--::--=+XXXXXXXXXX++.==+X-XXX+++===-=XXXXXXXXXXXXX+=----=+X++=-----==-::........ \n ....=:--======+XX+X+=----=+.XX+.XXXXX+=+=++X+=+XX XX+==--=---=+XXXX-X=XXXX++===.=+XXXXX.X.++++XXXXXXXXXXXXX-+++=++X XX++==++===-:..........\n",
|
||||
" \n \n \n \n \n \n \n \n \n \n \n .. \n :::::.. \n .... . -=...... .::..:...+ \n ..::.... .. .: .. .--:::.+... .. .. . .. . \n . ...:..-... .. .. .=.. .. .:=:::::.:. ... .:::.. .: .. . .... \n . ..:--::...:...::..: .:-:..-:... ....:::::-:....+..:-:.::. .=. .. .:....::::. \n ..:.:..::-=--:..+....::::. ...: --+-:.-. .......::+-::::: ---::::... ... ..:::. . ..::::.. ::--- -.. .. \n ..::-:::---+=-:..::..:::-:....::--=++=-:-:-:::-....:-+=-=+==++=-.-+::..:::..= ..::.....:::-:-=-----=-:......+:...= . \n .-.:::::::--.+===--:::-=:.==-:.+.::-+XX+++++-- :.. ..:-XX++++XX++++==-=-::.. ..=:=+--:++=:=-=====:=-:::...=.::......+ .. \n ......::-:--=----=++==---==-=+XX=-:..:-=+XXX+++:==--:...:--+XX:XXX+XX++=-==+-+-:=...::: +X++XX++=+++ +=+=----=:.::::-::-..... .. \n .:::::-::--+=--=---+X+===-=-=+XX++=:::-=+XXX.+++===-:-..:-=+X:X:XXXXX XX===XX-++=--+-::-=+XX++:XXXXXX++=-::--====---:::--:...+ ... \n ..:-----:-==X++==-.--+XX++==+X=X++==+==+XXXXX+++==----: :-=+XXXXXXXXXX+=====+XXXXX+++=---+XXXXXXXXXXXXX+=----=+X++==--=-=--:.+..=.... \n .+.::--==-==++XXX+=----+=+XXXXXXXXX++==+++++=XXX=XXX==------=+XXXXXXX XX.+===== +X-XXX+XX===+XXX=XX=XXX=XX+.++++XXXXX:+++++==-:.... .....-\n",
|
||||
" \n \n \n \n \n \n \n \n \n \n \n . ...... \n . .. .. ...=::.. \n .+.. . .:. . .:.. .=... : \n ...:.-.. .: . .:. =.. ..:.....:. . . . \n ...:::..:... ..... .:.....-: ........-+:... ..:.:::. .: .... \n ......-:--::. +.. ... . ..:::::=:...... -..:==:.:::..:=-:::. . .. ... ......: ::... . \n ..:...:-=--::...:.:+:..-::..::.---==-=-:.:.:.. ..:==----:-=+==--::.. ... .::. ...+:::---=:::-:.. .... \n ...:::=::-=--:--=::--::-==::..:.-+++===+=-::.. ..:-X++++++++++=--+-:.. ...:-:::-==------==----::.-... ..... \n .. -..::-:: ::-=------------X=-:....-+XX++====:--: .:=X =XXXXXX+==--=+-::::..:....-+====+===-= ===---::-::....+.....= \n ..+:..::::-=--::::-++==--:::--+X+=-:..:-=+XX+=------:. ..-=+XXXXXXXX+=+=--=X==:=-=::..::+++==+++++++++=--:::==---::::.::.... .. \n .:::::::--=== -:-:-=X:X==::-=++++=-=====+-XX++==-::-:..+:-=+.X+XXXXX+=-=--+XXX-X+==-:::-=XXXXXXXXXXX++=-=:::-++===-:::--::.. ..+ \n ..:---===-==+++=-.:::-++XX+==+XX++==-====+XXXXX+++=---::::-=+XXXXXXXX++=-:-==++XX+++=+=--=X:XXXXXXXX=X+++=-=-=+XXXXX+===--+:... ...= \n ...::.--+==++X+ +++=--.:=++XXXXXXXX+.+==++====++XXXXX+==+=-==+X:X-XXXXXXX+==--+==+XX.+=-=+==++XXX ++XXX-XXX: XX X-XXXXXX+ +=--+:...:......=\n",
|
||||
" \n \n \n \n \n \n \n \n \n \n . \n . .. . ........ \n .. . . .. ..:.. :. \n .... . .. .. +...-=.. .. \n ....... .. . .. .-. .. ....--:.. . ..: :... \n .. . .:::.. . : .+...::. ..:-:.........::-........:-=-::-.. . .. -... :..... \n ..=...--::-=.....=+....-:..:.....:----:.:... ..:=-:::::--=-==--:. ... ......:.:--....... \n ...:...:--=:..:::-...:::+-...:.:--=-----:::. ..:+X+==++ ++== -=-:... ...:-..---:--:.:---:::=:.. . \n ...-::..::-=:+::=::::::=-+-:....-+X+===----:.. .-+ XXXXX++X==-:-=-::..... ..-=---=---::::---- :=::.:.. ..... \n . ......:::--:.::.-+=--+:..:::-=+--::.:-=X++==----:::...:-=+XXXXX+++=-=-:-==---:.+. .:=+X=:===-.-===--::: :-:::-:........ \n ...:::.::-=---::::-=X+=--::::-==+=--==--+XX++=--=:::....:-=+XXXXX+=+---:--XX-XX+=-::..:-=++++++--XX+=--:..-:-==---:-::-:=.. . \n ..::----:-=-== -::.:=+=++=---===+= --===+XX+++===X---:..:-=+XXXXXX+==--::--=+X++==-=-::-=+XX+XX+X XXX+==-:::-=X++++=----:::.. .+. \n ...:: -----=====+=-::::-=++X++++XXX+=--==+=+:++XX:+++ ==-:--=+=XX=XXXX++==-:-==+X++==--==--+XXXX+=++XXX XX+++++XXXX ++====-.--... +.... \n ...-.:: ----:===.+==X+=-===++X.XXXXX++X+XX+=-==-==+XXXX++====+XX=X XXXX+XXX+======XXX++=-=-===++XXXX++X XXXX+XXX=XXXXX ++++===-:--:..........\n",
|
||||
" \n \n \n \n \n \n \n \n . \n . \n . ..:: \n . .. ::. . \n . . . .. .: ::. \n +... . .:.. .: .:::. .. ::...:.. . \n ::.. :.... ..::.:.. ..:-::.. .:: --:.:. . . \n =....-::.... . :-::. ..::.::-.::. .:--::.:-::-----+-.. .. .. .....:. . \n .... ::-.:....:.. . ..:=:.:..::::::..::-.. ..=+=--=+X+--=--==:... . ...... :::.. ... ::..... \n .-... ..:-=:...:::......--:=..:=++=--:::::. ..=XX++XX++===:----::.. ..-::.--:........::::-:.. . \n .....-::-...:-+:::::..-...:--::-::==++=--:.::......:-=+XX-++==-=-:::-+-::-:.:. ..:+X+==---:-::-::::.:::............ \n .. .:..---:::...:=+--::...:.:-=-::-==-=+++==-:-=::....:-=XX:X++=-=-::-:=X+=++=-:....:-=========++=-:. ...:-:::::::.:: .. \n .::-:.:::--=--::..:-=+=--:+:----=-=::-+XX.X+==---=-- ..:-+XXXXX++=--:..:--+X+==----:..:-+:+++++++=X+---:..::=X====-::.:::. \n ..:::-:::- ---+=:...:--=++=+=.==++=-:---++X+X+===-=--=:::-+XXX==XXX+=-- ::--=+:+==-:-=:-=+XXXX+=++++XX++== ==++.X+=----:::::. \n ..+...::-----------+=::--=+X+++++==:++====----===X+=+=+=+==-=XXXXXXXXXXXX+===+==+XX+==-:-=-=+++XXX+=:++XXXXXX+X-X+++++=+=---:::+:......... \n .....::::-----------=++==+:XXXX-X.XXXX+X+=-::-:--==XX+X+XX+ ++XX+:+XXXXX-X:+++++.+XXXX=--=.====+XXXX++XXXXXXXXXXXXXX+XX++==--::::--...::.....\n",
|
||||
" \n \n \n \n \n \n \n . \n .: \n . :. \n . . .. . \n . . .. \n .. ... .. .:. .::- \n :.. .:. ......: .::. ...::-:. \n ::.. .. .-. . . ..... ..:::.. ---:: =---:. .. \n . ..:::. . . ..:-.. .:..:........ .:----=+-+=----::-:. . ..... . ..:. \n .. ....::-:. ..... ..::-:.-====-:.:.... .:+X+=:++=-----:--:.. .:.:.::.... .. .:.. \n .. :.. ....--:..: .. .::..:-=--===::..::: .=.:==X+X+==--- -:.:-:...-: .:-++==--:.:..::........ . . .. \n ..::-::...:.:--:.....:. ..: ::.:-=.===-=-::--:.=.:-=+XXXX+=- -+:::-+=--+-::. .:--=-=-----=+=-:.. .=.::..- .....+. \n ...::....::--:+....:--:::::.::+::::..:-+XX+=-:-::--::..:+XXXXX=+=--::..:-=++==-::.:...:-=+=+=====++=::.....:==-:-::....... \n ....:..::::::--:....:-=---===:--==-:+::=++X++-:-:-:=--::=XXXXX++++=-:::=-:-=++=--::-=::=+XX+X+=-==+X+==-----=++==-:::::..... \n ....-::::::::::::=-..:-==X++=====-==---.::-=+=X+--- -==--:-+ ++XX+X+X+=---==-=+XX==-::-=--=++XXX+==+=+XXXXXX++++==+=--::::...:. ... \n ...-..::::----:::-:-=---=+XX-XX++++====+-:..: ---++=-+=+====+X++=++XXXXXX+===+++X=++=-::--=-==-+X++=+-+XXXXXXXXX++++ +=+=-::.:..:.-...-... \n -.....::----==-------=== +XXXXXXXXXX++-++-::.::====XX++++==+++XX ++++XX X XXXXXX++++++=--==+==-+++XXXXXXXXXXXXXXX++-XXX+++=--:::: :.:-::::.:.\n",
|
||||
" \n \n \n \n \n \n \n : \n . \n \n . . ... .. \n . ... . :. ::. \n .. .. . .... .::.:..:-.:.. \n .:... .:. . . +..=::===--::-:.. \n ..::. .::......... . . ..:--:--X+=-::::- .. \n .. ..:. . .-...:===--:.... .: ..-XX+====:-::::::.. ... :...:. \n .:.:. .::. .. . .....-==:- -:...:.:....:-+X++++=----::..-:...:.. .-=+=--:..::::... .. . \n .:::... .::. .. .. ::....--+=--::: ..-:...:=+:++X++=-+:::::-=-::.-... .---=----::-+=::.. .... .. \n .. ...::.. :: .=.::...........:=+X+=-:.:..--:::-+XXXX+===-::..:--+X=--::... ..:-+==+=---==--::.....:+:....+ .... \n ...... ..:.:.::. .:----::-=:.:=:::::.-=+X=-:. .::::..:-+=XXX+==-::.=.--:-++=--::.::.:-=++ X=----=+=--:::+-==-:::.+.. . \n ..............:-....:--=++=---==---:::::--=X=-:.:::---:::-X+++XX+==-::::==-=++==-:.:- --==+X++=---==XX++XX+==-==--=:::... . \n .-....:-:-:-::+::::-::--=+XX++=======-=::..:---+=--:==-----.+X+==+XX+X+=--===.XX:==::..:-=-===X+==-= =+XXXXXXXX+==+==+--:........ .... \n .... ::: . ------:=::--=+XXXXX-X++X==---:....:-=+++=-==-----++X+==++XXXXXXXXXXX++++=-::.:-=--==+XX+=++++XXXXXXXX++XX+:++=--:.:......:::.... \n ......::--======.----==+XXXX-XXXXX-====--:::::===+XX ++====+++X++XX++-+X=XX+XXX+:++X+=- -=.====+XX:XX+XXXXXXXXXX++++.XXX+==--::::::::::::.+.\n",
|
||||
" \n \n \n \n \n . \n . \n . \n \n .. ..+ \n . . . .::.::..:: \n . . :. ..:--=-:::.. \n .. . . ..-..:--=-:::::. \n . .. .....-. . .-::+::--=:::.:.. \n . .. .:-=--:...... .:...-=XX+=-==-:.:+:.. .. :+...... \n .. .. . ..:-:==-:::. .. :..:-=++======-:.:. ....: :--=-::.-..::. . \n .... :. ..= .....:--==--:... ..:..::-+++=====-...:::--:.. .-:=--:-=-:--:::.. \n .... ::....+:. .. ...:-+==-..... ..:..:-++++==--:...:--=X+=-::. .. ..--=---=----.:::. -....:.. \n . .. ..::==::.::=........:=++=-... .:....-+XXX+=--:....:---== =-:....:..:-+===-::-:--.:.:::::-=::+.. \n ......... .:. ..::-===-:::-=--:..:.--++--:..:.::::.:=+X++X+---:...:=--===:--..::--:-=++=---::-=X=-=++======--:..:. \n .......::.:.::...::..:-===-++=-:===---:..:.:-=+-::.--:.:::-=++==+X===--::-==++X+=-....:----=.++-::.-==XX+X++XX+=====-::....: \n ....::::::-:::-:.::--+XXXXXX++==+=--:-...:.::-=X=-::-:::::-==+===++XX++==X++X+-++-:..=.------++=======+XX.XXXX++++==--=::.:. ....:..... \n ...:.:::-==- -=.::--=+XXXXXXXX+++:---:. .::=-:-+X+=-----:=.====++++XXXX+++.X++==++=:::-= --==+XXXX.+++XXX:XXX-XX++++=++=-:- .....::::.... \n ......::--==+.==+=--=++XXXXXXXXXXX==---::: ==X+===+XX++==+++.==++++XX+=+.XX++:+==++XX+=-=+=.===++XXXXX-X.XX=XX++==-++X+XX+==.-::: ::-:::=...\n",
|
||||
" \n \n \n \n \n \n \n \n . .. ..- .... \n .::-:::. . \n . :..::-:.. \n .. .......::.::... \n .... .. ..::==--::-:.... \n .:---.-.. ...-=+:+==-::::..+.. .. .. \n . :::=-::... .=.---+==----+:..:. .. ::=-:....::.. . \n . . . . .:--=--:: .-..--=+===---:..::.::.. :::=-:.::-::.... \n . ....= . . ..-=--:. . .:-=X+==--:.. .:-=+++=-.. .:=:--:.:::::.. . . \n ..:--:=.....=. .-.== -:.. -... .-=+XX=--::. :-+===--:.. .-. ..-==--::.:=:...-.::::-::.. \n . . ...::-----.. :--. +..:====-..:..:.+..:-==+X=-:::..=.:=--==--:...::-:: +===-::..:--:: =---==--:.-.. \n ........... .=...::---==--:::=-:-:.....:-+=-:..::.....:==++.+=---::.:==-++X=-:. ..-::-:=++=-=:::-++======X= =--::..... \n +:........::::..-.:=XXX+XXX=--::--:=:. .=..:-++--..::...:---====+==+--:=:=+ +++=:. .-----=X+==----==X++++=+++==--:::::... ..... \n .=.:=::::=-:--::.:-=+XXXXX=+==--:::.. .:.:--=+X=-::::.------==+XXXX++==+ ====++-:...-=:-=--=XX++====+XXXX+++X+++==-==-:::.. ....::.... \n ....::.--==--=-----=+XXXXX-X++=+--:::::..--=X+-=+X+====--=--===++XXXXXX++=-++= =++=: :-----==++XX+=++XXXXXXX++ ++=+++++=-- ::...:::::.=. \n ......::-===+=+++===++XXXXXXXXX+++===-:::.-=+XX+==+XXX===:+X==+XX+XXXXX+XX++======++X+=---== ++-=+XXXXXXXXXXXX++==+.+XXXX+=+==--::--::::....\n",
|
||||
" \n \n \n \n \n \n \n . .... . \n ..:--::.. \n ....::.. \n .. . ..-:... \n ... ..:--:--:.::: . \n .::::.. . ...:---+=--:..... . . \n .::-+-.... ...==++=----::.... .--:.. . .. \n .-::---:.. ..:--==.---::..::=..... .=.--:..=.::=.. \n .. . .::--::. . .:-=+==--::=.. ::--:-::. .:::-:....::.. \n ..::. .. . .:-=--... .:-=X+=--::. .:-:++=--.. . .::--:::..::. ...=... \n ..:::-:.:..::-. ..:+=--:.-. .. .-==X+=-::.. .::====--:... ::.::==--::..:::....::::-=-:.. \n .. ........::-=---:.:-:::. .:-=+--:..:+.....: ==+X=-:: ....=--==+=-:=....::--+==-::..::=--:-::-=+--::.... \n .....+.::... ..:-== =-==--:..:::-:. ...:=+-::..... :-=-=.=+=--+.::-=--++X=-:. .:----+X=-:+:---++====.++==--:..:::.. . \n ........:-:::...:-+X XXXXX+--: :::.. .-.::-=X=-:..:..---:--==+X++.=----:== ++-:. .-:-:--+X++=---=++++++==++++--: :-::.. ...... \n .=..:::-:------..:-+XXXXXXX+==-::.... ..:-=+=++==---::.--:-=:=+.XXX++==-===--++-:..:-::----=+X+==-==XXXXX+=++ ++==--==::::. ...+:..... \n ....::-:-== ==-=--==+XXXXXX-+++=--::.:..:--+X+.==++=======- -=+++XXXXXXX+=======++=-::-:--.==++.X+=++XXXXXX++=+++=X+-++=-=--::..:::::... \n .....::-=-=+=++++++++XXXXXXXX++X+ +=--::--=+XX+==+XX+===.=+=++XX.++++XXXXX+====:==+X+= --==-+X==++XXXXXXXXXX++==.++:XXXX++===--.:--::::-.. \n",
|
||||
" \n \n \n \n \n ... \n ......::.. \n .:..= \n .. \n . ...... .. \n ....=..--:+:. . .. \n .:--:.. ..-=--=-:.... . . \n ..::--:. .---==--:: .. .: .:-::.. \n ..:--.: ..---=-=--::....-=.... . .:::....:.. \n .:--::. .:-===-:::=.. ..:-::-:.. ..:::- ..... \n ..- . . ..==-:... ..-=+=-:-... ..==++-::. . ::-::..-... ..... \n .:::: .. .:+=-::. .. .-=++=-:... .:---=-::+......:.:X--:..:.....= ....:--::. \n .........-::::..:+::.. ..-==::.-.. .:===++=- ... :-:--==-::... .::-+==::...::-:..-..:----:.. \n .:.=. ..:--+=-----:::....:. .::=+-.=. ::::--+=-:: ..:-::-==+-:. :.::-++= --::-+X=-=----=---:.. .... \n .-... .:::: ..:=+XX++==+=:.:..... ..:-++::::.-. .:::--=XX==-::::.:--+X-:. .:.: :-=X=--:::-=+= ==--==+--:.-..::.. ...=. \n ........::=-::..:-++XX++-+=--:.-. .:-++++-:.:--..::::--=+XXX+=--::-+--==-:. .::.::: :-=+--::-=+X+-+==:==+=-::::-::... . .... \n .+..::::-:---:-:::-+ XXXX++==+-::...=. .::-+X ===-::::-------==++X.XX+==--=---=+=:..:..::::--==+=--+XXXXX+=====++=-==----::...:.::.:. \n .:.::--=====++====XXXXXXX+==+==+=-:::::-==++==++==--=::--==++==+XXX-XX++.======X+-::.::=-=++==++++XXXXXXX+==-=+=++ ++==----::::::::.=. \n .. ..::-===+==++XX+XXXXXXXXX++=++X+X+=-=== +=++=++XX:==---=+++++++=+++XXXX++=====-+XX==-===-+X==++XXXXXXXXXX+=-==++X+X+-+===+=-----::.... \n",
|
||||
" \n \n \n .:. \n \n ...=. \n \n \n .:.... \n .-..::... \n .:---::. .:-=--:=.... .. . \n .:=::. ::.:=-- ::.-. ..::-:.. \n ..-:.. .-::=--:.... ..:... ..-... . \n ..=::. .::---::.... :--::.. ...-:... . \n .. ..+-:.. .-=--::. :--+-:... .::+:...... ... \n :..:.-. ..==:.. .. :-++==-::. .---:=-:.=. .::+-:..=. .:::... \n .:::..:.... ... .-==-. . .::-=+=--:. .:: :-=--... ..::=+--::...:........:::-:. \n .......::::+-:.::.... . ::==:..: .::--=X=-::...::.::-==-:. ...:-=+=---::-+==-:::::--::.. ... \n . ..-:.=.:-++++=--+-:-.. .:-+-:...... ...::-=X+=--:...:.:=+=-:. ...:.::==-:::+-==:----::--=-:. ...... .. \n . . ...:: =...-==+X++===-:-.... .:=++=:...::. ..:---=+XX+=-::..::--==:. .:... ...:=-::.::=+=====--==+-:.....:+.. ..+.. \n .......::::::..:-=+XX+=.=-==:::... ..:-==+=-:.....:..:---==++XXX=-:---::-+=::. . ..=:+:-.-::.=+XXXX====-=+=-:-:::::.. ...... \n ..:::------=--- =+X:X++==-== =++=:.:.:::--=:===-::....:+:-= ==++XXXX+==--===.X+-:.. ...:-=---=--=+XXX.XX+=--====+=+=----:.......... \n . .=.:------==+X++++XXXX++.==-==+XX+-+-=-===-=.=+++==::::-=X++++=++XXXXXX+=--==-=+X=-::--:-=X===+=++XXXXX=X+=-=+=+XX+=====-=--:::::=... \n . . ...:--======+++XXXXXXXXXX++++++XXX++===:+++==+XXX.++=---=+XXXXX+-+++XXXXX+=======+X+====++=+.++XXXXXXXXXXXX==+++XXXX++ ======---:.::..- .\n",
|
||||
" . \n .. \n \n \n \n \n . \n .... \n . ..--....... . \n ..:==+::. .::--+.. . . .. \n .+.-.+. ...:--.:. .. =..-:... \n ..-:. .:-:---.. . . . .:... \n :+-. ..:--+... .:-::.. ..:-:. \n .==:.. ..-+--:... .::-=-.. .:=-.. \n .:-.... .-+-:. ..:++--::.:. .::----:. ..==:::. ....: \n .::...... .--=:. .:-=+--::.. .....::--:.. ..-+-::.. .. .. ...+::.. \n .. ...:-::.... . .:-:.. . .. ..:-=XX-::. ......:=--.. ..:-=:-:..:==-:::.=.-:::.. \n +..:..:-=:=+--:::.... .:==:... .. ...--=X+--:.. ..:==--:. ..-:..::--=-- ::-:-::-:. . \n .......:--=+==-:-:::... .:--==-.. . ..:--=+XX+=-::...:-=+-:. .. . ..:-::..:--==-==::.:=-:... ..=. \n .........:....::=:=X+=-----:--:.:. ..:-=.=:.-. -...:--==+XXX=-::--::+=+-:. .:::+:...:-=X++X=----==-:::::..-. ..+. \n .+..:::::-::::.:-=+X+==-:----+==+=:+..::=--==---::. ..:-=:--==:++XX-=-:--==-=X=:. .:--::-:::-=+=XXX++=--=======- :::....=:... \n .. :--:-:--==+++=+X+X+==-- :--=+XX=-:---=--= ====--...::-+==+XX++XXX:X=--::- -=+=:..:-:::-==--=-==+XXX:++=+-===+XX==.-----::::=...: \n . ..+:----=--==+++=XXXXX+==++===-+++=---=+==-+XXXXX+=-::--==+ XX+XXXXXXX++=---=--=++-::--==-=X=:+++XXXXXXXX++-== +X-X++=---- -:::+::.. \n . ..:::--=++= =+=+=XX+XXX:X++XX++++++==++X++==+XXXX-X++=-==++XXXX:X X-XXXXXX+=+===+-++==+-===+X+X:XXXXXXXX.X+===+XX++XXX+==.--=--:::+:+....\n",
|
||||
" \n \n \n \n \n \n . \n .-:. .:----:.. \n .:--:=.. ...::=-:.. .:. \n . ::.. .:--::. ..... \n .-=:. ..:--::. . ..::. \n :=:.. ..==::. .:--::. .:-:.. \n ... .-=:. :..++-:.. . .:.:-::. .-:.. \n ... :=:. ..=+-::... . ...::--: .--:+.. .. \n ...=... .::. ..--+==-.. .:. ..::-::. :-::.. . . .... \n .. ::-.. . .::. .:-=++-:.. ..==::. .::... :--::.....::.-.. \n . ...+:::--:::..= ..---.. ..:-=++-::. ..==-:.. ..::. .=::-:::::-.. ::.. \n . . ..::::----.::::.-. . .- --:. ..:--=++=-:....+.-=+-.. ...:. ..::-----+-:.:-:: .... \n -...... ..:---==-::.::-:::.::. +..:==--:.. .:=::--=+++X--:::::::=+-.. .:..:.-...:=+===+=-:.--:::...... \n ...::..:.:: ---=++=-::.::-++--:==:..::-:-+--:-.. ..:-::--++++ +=:..::--:=+-.. . ..:-..:::::-+X=++=--::-==-==-:=........=:. \n ..:::::::--:-=-++X+=--:::::--=====-::-= --=--=-:. ..:-=---=+.++XX+=::...-::-=:. ..:::.:-::=::--=+XX+====-=--=++=-+::::.. ....- \n ..:=------:-=-=+X+X+=--=-----=.=--:--X--=+X+++==-:..::-==++XXXXXXXX+=-::::-:--=-.. .:-:--+--=-=++X+XX+++==---=++==---=:::=:::...+ \n +..::--=---===+==+++++++=-++========-=XX+==+XXXXXX+=:::--=+XXXXXXXXXXXXX+-----====-::-:-==+XXX-XXXXX.XX X+=---==+++-=------:::::..=. \n ..: :..:::-==.=--=+X++++XX+-XXXXXX+:==++++XX+++=+XXXXXXXX=--=+XX-XXXX XXX.XX+++ ++==+XX++==-=-=+ X-XXXX+XXXXXX+==.++=++XXX=----=--::=:::: ...\n",
|
||||
" \n \n \n \n \n .. ...... . \n .::.. ..::=+-::. \n .--... .. ...-:.. ..- \n ..:-. ..-:.+. .:. \n .-..- ..=-:. .. ..: \n -:. ..+-:.. .:---::. .:.. \n ..- ::. ..-+:::. .:=::. :... \n .-. ..-+=--:. . .+.-:.. ::.. \n . .. .:. .--====:.. ..=-:. .:+.. ... \n ...... ..-. .:-==-: : .+=:.. ... ::- .....:.... . \n ... ..... . ..:::. .:.:-===-:. ...-+-:. .. .:.:-:....:...:.. \n .......::........ .:=-:: .::.::==++=:=..:..--=-. ... . .::-:: :--:.:-.:. .. \n . . ..::::---:. ..:.:....--:.....::--:--: ..:+-:-:-==++=::...:::-=:. . ........:-+-:-=-:::::...=.. \n -...-......-::--+=-:....:---.-=--=:+:-=--:-::.. . .:-::::=====+-:. .: :--. .. . . ....+..-++===--::::--::-:..+. \n .-.::...:...::-=+=-::.-.:.:---.=--::-+-:---:-:.. . .:-=--== == ==:.. ..:--. ..:..:......:-+++===--::::-=-:-:... . :.. \n .::::::::.::--==+=--:::+ :::-----::-++==++=---:......:-+X++X+++X++=-:......--:. ..::=-:.:--=++++==--=: :-==---:::......... \n ..:::-::+:--=--====+===+==--------:-=X++ XX+++X++=-:..:-=+ XXXXXXX X++-:::: -.--:..:.:-=+X+++ +=+XX+X++==:::--= ==--.:::::..... . \n ....::---.::-++===.+XX:XXXX+++--=====+++==++.XXXX++==-::-==+XXXXXXXXXXXX+=.==-==+==+--:--=+XXXXX-XX.XXXX=+=--==-==++=-:.:--:: ..:.:... \n .+...::-:--===--=++=+++++XXXXXXXXX+===+X+++++=+=+XX=XX++====++XXXXXXXXXXXX=++==+=+XX-XX++=-:==++XXXXXXXXXXXXXX+==+X=++XX+=-:-=-:::: :::=..:.\n",
|
||||
" \n \n \n \n ... ..+-:.... \n ::. ...::-::.. \n .::. . ..-... . \n .. .-=:: .. \n : .:=-:.. ... .. \n :. .-+:::. .::-=::. . \n . .:X-::-. .. ...:... . \n .. .:=--:-.. ..-:. . \n . ::=-=--.. .+-: . . \n .... ...:---=-:.. . .-=:.. . ..::. .. \n . ..:... .-:..:--==-.. ..+.:+-:. ... ...::.. ..... \n ..... . .:::=-:. .::...-=-==:. ..::-=:. ... .:.::.:..:...:. \n ....--:..... .. ..-:.. :=::.::. . :--:.:----=-:. ...-:. . . . ..-::.:.::..::-.. . \n .. ..::-::. ........-:-=::==::..:..: .:--::--=----.. ..::. .. . .-=----::....::.::.. \n .... .. .:::-=-:.. ...::-:-=-:-::=-:=::.. .:-=---=====-. . .-.. . .. ..-==---:.:-....::::... \n ...........::--=-:::..-::.:-::--:.:-+==--:..-... .:-++XX++++==-:. . .::.. ..:.=. ...:-=++- -::::..:--:-:=.. . . . \n ....::.:..:-::--=:=----:+:-=--:-:.:-++XXX+=-=+=---:...:-=+XXXX=XX+==:....+:::::. ....:=+==-::--==+XX==--:..::----::. ......=.. \n ....:-:::::+=----==++X++====+-:----.==+=+++X++=+==--=:.:--=+X=XXX=XXX++-:+::--=-:--:..:-+XXX+:++==+XX XX=-:-.----+=-::..::......... \n .. .. :::----:::-=--= =+XXXXXXXXX+=--===+-====++XX++=+=-----==+++XXXXXXXX+++==+=XXXX++=-::--=+X+X++X++XXXXXXX=-==+===++=-::::::::...::.... \n ......::-===+=-=-====++-+XXXXXXX..++==+X+======+XXX-+=+=--=+X++++XXXXXXXX++======XXXXXX++-===+++=XXXXXX=XXXXX++==-++XX+XX+= ---::::::::::. .\n",
|
||||
" \n \n .. :+:. \n ... .--::. . \n . ..--:. \n .. .--:. \n .. .:=:+.. \n . .--...:. .-:.. \n :=:..:... .::-:. . \n .=:. .. . :-:. \n .::::::. .:=:. \n .. ::-::::--: .. .--.. .. \n .. .-. .:..::+:--.. . .:+:. . ..... \n . .:...: .. :.:=::: ..:=:. . .... . .. \n .. .. .--::.:. :.:. ::.:::.. .::. ..:.. ..... \n .:...+ ::...:=-.....:. ..-=..::-:=::. ..: .:-....... .-... \n . ..:.. ..:-:::..-:...=.. .:--:::::=-:. :. .-=::::... .:... .. \n .. ....:::. .+.:+:::-.:-::-:. .. .:-=--:::--.. ... .. .-=-:-:.... ........ \n ..... ..:..::---:+.. ..:==:::- ..:==-::.. .-.:. ..:-=+=====--:. . ..:.. ..:. :--++-::.... .::..- \n ..:..- :...::---=-::...:-+:::-:..:-++X++-::--:-::. ..-+XXXXXXXX+=-. . ....::. .:--=-:..::--=++-:::....:-:-:.. . \n :..::......:.::-=X++==--:-++-::-::---=+++X==-----:-:..:-++X=XXXXX:+=-.-..:::::::.....=+XX+=---=--=+X++=-:: :-:-=-:.. .. ..+. \n ..+::-::..:::=:--=+XXXXX+=+X=-:--:=:---==+XX+===--::-----==XXXXXXXXX+=-+-=+X++++--:..:=-++X+=====.=+XXXX+=---=--==-:....: ........ \n .. ..:=---==::-:--====+XXXXXXXX+==-=- ==+---=+XX.+==--:=--===++XXXXXXXXX+==-==+XXXXX+=--:--.++XX++++.X+XXXXX+=--=++++++=-:.:::::...+:..-. \n ....::--=++=+=---.==++.+XXXXXXXXX++=+==+-====:XXXX+++=--====+XX-XX:XXXXX+====:==+=XXXXX+==-=++XXX-X==XXXXXXXX++==+X=XXX++=-:::::::::.::...-\n",
|
||||
" \n . .:: \n .+. .-::. \n .:--:. . \n ..--.. \n . .:-:.. \n .-:...::. .:. \n :-....... .--:.. \n .-.. .. ..:-.. \n .:+:=.. .:-:. \n . ::+::..:=: .-:.. \n . . ..:.:::-. :-.. . .:.. \n .:..-. . ..:.... .:.-. .... \n .. .:-:... . .. :.::... .-:. ..... . \n .. -.. ..-::.... ..:-..-.::+.. .:. .::.. . .. \n .. ..:..:..-::.. .. +:::.=.:.:::. . .--:. .... ... \n . +.... ....:: :-:.:..-:=.. .::-:::..:-:. :. .-=::::... .. .. \n .. ..:::=.. ..-=:::::.:.:--... . .:--------::. ..:. .. .--=--:. ....-. \n . .. ..+..=:::.... ..:+-:-::.+.:=+=--:. .:.:.. ..:=+++++++=--. ..... :...:. ..:-=+-::.-.. .:::.. \n ..::.. ....:-==--::.=.:-=-=:::..:-=++XX=---=:-::. .:-+X+XX.XXX+=-.:. ....... . .-++==:..=::--==--::..:..::-:.. \n ..-.::... ..::::-=XX X+=---+=-::::-- -=+++X++==---::-:::-++:XXXXXX-+==:..:-=--:-:.. .:==+X+=------== +++=:::--::--:.. .... ..-. \n . ...:::-:...::-- ==+X-XXX:+++=-:---.-::-==+X+X+=---: ----==+ XXXXXXX:==---+XXXX-X=-:..:-++ X+====-=++XXX+X=::--= =-=-:.............. \n .. .:.--====--::---==-++XXXXXXX+==-=--==----+XXXX+==--:==--=++XX.XXX-XX++=---=+XXXX-++=-:--+++X+++ ++XX=XXX++=--+XXX+++=-:.....::.+.:..+. \n ......:--==+===-:--=++XX+XXXXXXXXX++++=====++== XXX-++=---===++XX+:XXXXXX+=======+XXXX++=====+XXX++XXXXXXXXXXX++==+XXXXX++=-::::::-:::.....=\n",
|
||||
" ::.. \n .-:. \n .::. \n ..: .. \n : .:.: .. \n :.. . .::. \n :. .--.. \n .::. . .::. \n . :. . .. .:. \n -...:...:: :.. \n ..: . .:. .... \n .:-::. . . ..-. .:. . \n .::. .. ..:.. .. .. \n ... .:.... ....:....... .. :.. \n .:..:...: .:. ..=:.. ..... . .:-...+. \n .. .:=::::... .:. .::::. ..... .. . .=:.. \n .=... .-=:....... ::.. . .::::::--:.. .. .:-=-:.. ... \n . ...::.+. .-::......::-.--:.......... .-==+==--=+=:. .... ..:--:.. . :..:. \n .. ..::-+=-:.. ..=-::-...-==+==+-:--::-.....:++XX+:+ XX=-:. . .:==-=:. ...::---.. ..:...::. \n ..:.. ..+:-+XX++=::::-=-:....:---==++===--:::::::-=+XXXXXXXX+=-:... :=:-.. . .--==+=-::..:::-===+=:.:::::+-:. ... \n . ....::.. .:.::=++XX++==-==- ::-::--::==++=+=--:-=: ::--=XX=XX-XX++=--:-XX+===+-....:=+=++--::----=++=.++::::---:-:. ..... \n . ..::-----:..-::--==+X-X+++==--::--:-::-:-+X+=+=--::-=:::-==XX X-XXX++=+--=+XX+ ++=--..:-=++ ==--==+XXX++-+=--+++++==-:.............. \n ....::--====--::==++XXXXXXXXX++==-=--::-=++=X++=+.==-::-:--==+X+XXX:XX+===--=++XXXX++==----+XX+====+XXXXX=X++= =+XX+XX+=-:..:::::::..-.. \n .....+::--======--= XXXXXX.X-XX+XXXX+=---===+XX+=+XX.++=--===-++XXXXXX=X.-++====++XXX.X++---=++X=X=++XXXXX-XX.XX++++XXXXX+==-::===-- :::.....\n",
|
||||
" -.. \n .. . \n .. .. \n . ... \n . .:: \n .:. :. \n ..: .. \n ... : \n .:.. .: .. \n ..:.. .. . \n .:::.. . . . \n . .. .. . :. \n .. ... .. . .. .. . :. \n .:-:... .. :.. . .-. \n .-::.... ...-..:=:. .-:. \n . ..::.. . . ..-.. .. ..:....::-=:. .::.+.. . \n ..:-:. .-:. . .::=:--:..::.+.....:-=-----::-=:.. =.. ..::.. . . \n .. .:-++=-.. .-:.. ::-.-:-::=-:......:=+X:X+===+X+-:. .::--. . .:::-:.........: \n ... ...:=XX+==-:.:..-::.. .:--------=-:::...::-+XXXX++++X+--... ... .-----:......-: --+=:.......:. . \n .:... ..::-=+++=--:.:-::..:::::--==+=---:::-..=:=:=++XXX+.++==--::=+==-::.:. .:-+-=--:..:::.-==-==+=::.:::.:. . \n ....:::::....:::--=+X++====-::..::...:-:-+X=--=:-:-==:::--=+XXXX+X+==--:-++XX====-:....-==+=-:::--++++====-====-==--:. . ....... \n ..::::-:--:::=X=== =+X++:+++--:.::.-.:-==+-+=-==-::.::::--=+X-XX++++==--:-=++XX+====-:.:-=XX=--::-=+XXX++===.=XX+==+=-:..:-::.....-. \n ......::-----==--+X+=+X:XXX+++XXX==--:::.-==+XX++==++.-:: -- ==+X+XX+XX++=+=-==++.XX+=+=-:--=+++=--==+XXXXXXXX+===++X+++==:::-=-:::: :.... \n ...+..:::-=+=----===+++XXXX X+XXX+X+=---- ++=++X++XXXXX+=====+X+++++XX+XXX+++===+XXXXX+==---=+.++X++XXXXXXXXXXXX++++XXX++=--------::-- ::....\n",
|
||||
" \n . \n . .-. \n . \n . . \n .. . \n .. . \n .:. . \n .:.... \n .. \n . . . \n .:::.-.. . .. \n .:.. .:.. . \n ...... . . ....::. .:. \n .. .:. . .:..= ..... ...:.+..:=+. . ... \n .::-:. ..: .:..=:..:--:......::-:::-+:::-:.. ..- ..= \n ..--=--. . . .::..:..::.. . .:=++XX+==-=-=:. ..... :..-:. . \n .:==== -::.. ....-. .::.:-::.+::.-....:-=++XX=====--:. .. .::..: . .:::---:. . \n . . . .:=====-::.::++......:--+-=-:.:::......::=++XX+=+==--::.:==--:. ..-.:-::. . ..:--------:..+:... \n .. .........-:.:::-====-=:--:.. .....:::-==-:.::::::::::--++XX+==-=+--::==+XX=--::.. .:--==-:+..::--==------=-::--:.. .. \n ......::::..--=-:---=+==-===-:.. .. ..:====+-::-::..=:+:::-+XXX===+==-::-=+++X+=---::..--=+=-:...:-=.+++===-=+++=--=-:..:::.-.. .. \n ...:::::---:-==--=++X+-++=+==-::.:..::-=+X++=-=--=-..::-::-=+XX+=++=+=--:-=++XX+=-=-::-:=+==-::::-=+XXXXX+===++X+=:=---::-::...:.... \n ......::-.---:----==++XXX XXX++:++=--:::----=++XX.++++=:::-:-==+==+XX+XX++-=-==+XX++===-::=== -======+XXXXXXXXX+==+XX++=---=::-::=-::::=.. \n .. ..::-=----::-:===++.XXXX-XXX=XXXX==--.====+=++XXX.X++====+.++++ +X++XXX:+=.=++XX+:++==--==+ +:XX-XX-XXXXXX:XX++XXXX++=- ::-:-::::::::..=.\n",
|
||||
" \n \n . \n \n \n .: \n \n ... \n . \n -.... \n ::.. \n .. .... . \n . .. . ...:--: \n . . .. ..-.::.. ......:::-: .. \n .::=. .. . .::.. -...:-:--::..... . . ..: \n .::=:--:. . ...... ... .:-===+=:: :::. . . ....::. \n .::::::.. ... .. ...::=. .:. .:----+++--=::-.. ... . ...::::.. . \n . .:------....... . .::::-:...:. .:-==++==------::..-.--:. ...:::. . .:::::--:. ..-. \n .. :....:.:-----:.... .:-::--:.:. ... . .::-=+X+=------::+=+==+-::. .::--:.. ..:::-:::-:-:+..... \n ......:-..:::::::---=---=---:. . .:=+-=-:::....-.:.:::-+X+=----- -::-++=++--:::.+..::-.-:. .::-==++-=-====-::-::...... \n ...:...+::+::::::-+++ +=----::.......::-=+==-=-::-:. ...::-+++=- -=+=-:::-==++=-::-::=:.-=--::...:-++XXX++=-==++=--:::............ \n .. .+.:::::.::+----=XXXXXX++======:::..:--+-++.++-==--:.:---:-===++==++.=----=+X+==+--::-=-----------+XXXX:XX+====+-+=--::.:::..::::..= \n ......::---::.::-- -=+X=XXXX+:++++XX+-::-=====++XXXXX+=----==:====+XXX:XX X==-=++X==-+=-::=--=-=+X++++XX-XXX:XX+==+XX+==-::::::::::::::.. \n ......::--==---::-==+.+++XXXXXXXXXXXXX+==++X+ ==++XXXXXX+-==.+++XX ++XX+XXXXX++=+=+X++X+=-: --=++XXXXXXXXXXXXXXX+-+XXXXX+=--::-.-::-::::.....\n",
|
||||
" \n \n \n . \n . \n \n \n .. \n ..... \n . \n .=. \n . .. ..::: \n .:.. . .::. .... .: . \n .... . .. .:::::.. .. . \n . ..... . . . .-::---:..+. .... \n ......... . .:+: . ..-::-----:::... .. . . ..+.:. \n .....:.. ...::...+ .::--=+=-:::::-:...:::-: ::. ...::-:.. . \n ....:::::::- . . .:=:.::.... .::-=X+--:-::-=-==:--:-:.. ...::.. ..:-:::::...... \n :.. .. -...:.:---::::.::. ..----:::: ........:-=X=-::::.::.:==-----:.+. .:-::. ...: ------.--:.::. . \n ...:...:.....:-=+X+=--::.:.. .- -=-.-:..-::. . ..:=== --::-=-:..:-==+=-:..::-...=::::.. ..-====+==-----=-:::::... . . \n .......::.::.::=++XX-X+=-+--=-::. ..:-=-=+===--:::.. ::-:-=====- ===-:.::-+X=-- ::::-=:::--.:..:-+XXXXXXX+= ===--::::............. \n .....:: :::..--::-==++XX:X====+++==:.=.:-==.=+XXX+=--::::-----==+XXXX-++=-::=++=.---::..::.--=++=--=+XXXXXXX++===++=--::.::........... \n ....:::----::-:--.===-++XXX++X++X+X++-:-=++:===+XX-X+==--===++++++XXXXXXXX+=--==+==++=-:.-::==+XX.-XX:XXXXXXX++=++X++X+-:::::::::::..+... \n .....::---===---=+XX:+++X+++XXXXXXXX++===+XX+-==+XXXXXX=+++XXX:XX.X+XX+:XXX++=++==+XXX+=--:--=+++XX:XX-+XXXXX:++=+XXX+XX+=-:-===-:::::.=....\n",
|
||||
" \n \n \n \n \n .. \n \n \n \n :. .. \n :. . .... \n . . .... . \n .. ...:.. . . \n . .. ....=::.. . .. \n . .. . .:-:-:....... :.. .. ..:. \n ..: ... .:..... .::+:-=-::..:::+.....=:. .. .=.:. \n ...::.+... ..--:.. .. .::--=-::....::=-:--:.:. ... -.....:..-. .. \n .. ..:-=--:: .. .. ...--::+:. .. ..:-=--:.. ..:.:--::-::. . ..::.. ....::..::..-.... \n ..........:+==--.:.:..:. .:.:-+-::.. ... .:--=--+:::::. .:--+=-:...:. ..::.. ..::--==--::::::..... \n :......:..:...:-+=:==+--:::+=:.. ...-==+==-:.-.. ...:---===---=-:-..:-=+-:...=.:::::::::....-=+X++++.+=--+-::...... .. \n :...+....::-::-=====:++=======+-:. ..:-==+.XX++-:....:+::--==+XX++-=-::.:==+--::+......:--++=-::-++XXX.++X ==+=--::..::=:+....... . \n ...+.::--:::+===+= ==+++X+=+.+==++=:.::-====+XX++==--::--==++=+XX.XX=XX+-::--==-==--.....:-=XXX +=++XXXX.+X+++++=== =::.::-::........ \n ....::+:-=----=+ ====+:++=+++:++++= =---+X+==++XX++====-=+XXXXX+XXX.X-XXX++--+==+X++=-:..::-++XX++XX+XXXXXX++==++++=+X=--:-:-::..:...... \n . ....::--=+++==+X=++=+===+++XXXXX++:==+++++XX++XXXX+++++-+X.XXXXXXXX+-XXX:++++X+=XXXX-+=-:--=++ XXXXXXXXXXXX++=-+++XXXXXX=-==---+::...:. ..\n",
|
||||
" \n \n \n \n \n \n \n . . \n .. \n .. \n .. \n . . . \n . . .:.. . \n .. . .--:...:. .. =.. .. \n . .. .::... . ..-::....-: .. . . . \n .:-::...: --::. ..-:-:.... .:.:...=. . . -.... \n .--:::::.. .:--:. .::-:.::.. .....:-::-:. . .. ........ \n . ..:- --:--....=. .:-+-:.. .::--:-:..... =:::-:.. . .. =..:-=-:.:..... \n .. ...... -----=--:::--.. .:-=+++-:.. ..:-----+::-:. .::--:.. ......:::.. .:=:===--=+-::::.. ... \n .+.. :...:--:::---+==+=--==--=-. ..:-=+XX+==:. .::.-=-=XX++=--:.. .: -- ::.. .+.:-+X=-:.:--=++-===+==-=--:..+...::.. \n ......::..::=-------====:-+===-==-.. .:---=+X+==-::::: --==X=XX XXXXX=-:.:------:::. ..::+X+ ==--=++X++=++==+=--:--:..:::..... . \n .....::=--::=------==-= ====++==:--:::=+===++X+==--:-::=++XX -:XXXXXXX+-::---=++=-:.. .:-=++==+===+XXXXX+===++=--=+=-::-::.+.....: \n ....::- ===--======-+-===++XXXX+=---+====+X++:XX++++=--=+XXXXXXXXXXXXXX++==+X++XXXX+-::.:--==+X++++=XXXX.++=====++ XX++=:--::::..+.. .. .\n ....:::-:==++==== ==+-===+=+XXXXX+==-= =+==XXXXXXXX++XX===+XXXXXXXXXXXXXXX+==+++-XXXXX =--===++XXX=X.X.XXXXX++:==++XX+XX+=.--:::.:.........\n",
|
||||
" \n \n \n \n \n \n \n \n . \n \n \n ..- \n .. .::. . . \n .:. .::.......... \n .:... -:..+ . .:...=. .. \n .::..:.. .:-:. ..=::.:... ..........: . \n ..: -:::::....-. .:--:.. ..::---:..... ::..-:. . .::...:. \n ....-::--:-=:::.::. ..:.==::. .::----::: .. .:.::..: .... .::--:::-:... . \n ...::::..::=-=---.--:--:. .:--XX+=-:.. ..::-=====--:=:. ..:-::.+. ...:==-:-..:- -=----==-:::...-.. .. \n . ......::-:::::::- -=---.---=-:. ..:--+X+==--::. ..::-=+X+XXXXX+=:....:-----:.. ..:-XX==-::---====-=+=---:::......:... \n .-....:-::-:--:--: --==---.--==-- .. :-=--==++=--::::::--==+X XXXXX-X+-:::=--:=-:::. ..:-=+=--=--==+++==++==+=-+--=-:.::..... \n ....:::---:-----=--=====-===-+==----+--=++==+X====-::--=++XXXXXXXXXXXX+=+--==+XX+=-:....:-==+====+=+XX+X+==+==-==.===-::::::.:.... \n ...+:---===---.======-===+X:XX++==-----==+XXXXXX+==----==+XXXXXXXXXXXXX++=+X+ +XXXX+=-:+:--=+++++X++XXXXX+++=====+X++.==-::-::......... .\n .....::-==++.==-==-++=+==+++XXXXXX+========XXXXXXXX-+++++XX-X:XXX+XXXXXX ++=++==+XXXX:X+==.=++XXXXXXXXXXXXXXX+===:+XX=XX+=--:-::::.........\n",
|
||||
" \n \n \n \n \n \n \n \n \n \n . .. \n . .. \n . ..+ \n .. ..:.. ....... \n ....-..- . :-:. . ...::.=. .. ... \n ...=..+.::. .::. : :.. ..::-::....... . ..: .... \n .. ........:::::..::. ..:-=-:.. ..:-----::..-... .... :... .:.:.....:. .. \n ........:.+.::.::.:-:. ...:=+==-:..- ..:-=+X+=--::..+ . ....:. .:.:=-:.. .:..::::::-:. ... . \n ............::::::--::-=-.. ..:-=++=--:::... ..::-=+:XX+-+=-:.....::-::.. .:==-:-::+::::----.-=-::..:.....+. \n .....:..::::::::::--:+::::--.=:..-:-:-==:=+=-:..+.:.::-==+XXX++XX+-:..::-==--:.. ..:-==--:::-=-==-=--=--+-::::::.... . \n ......: ::.::::.---.-=---.=-+-----:::::--++====--:..-::-==++XXXXXXXX+=--::-=+XX+=-:.....:--=------==X==:==--=-==-----:......... \n .:.:::.---::.--+:--+-==++======-:--::---=+X-+++==-.::-=++++:XXX:XXXXX+======++XXX+=-::.::-========++X++++==+====++==--::... -..... \n .....:--:===+------=--====+XX.++X+=-==---=+XXXXXXX+==----=++:X:X=XXXXXXX+=++===+X.X++==---=++XX+++ XXXX==XX++=-==+XXX++=-::::::.......... \n ......::-=.=+ ==--=++++====+X=XX:XXX+ +X==++++X XXXXX++===-+XXX.XXXXX.-XX++=+==.=+XX-+++:+=++XXXXXX.XX.XXXXX+.X+=+++XXX X+=--::--::.........\n",
|
||||
" \n \n \n \n \n \n \n \n .. \n \n \n \n .: .-.. \n . .... .. ::.. \n ..... :.. .::. ...::::......... \n . ...:.. ..:. ..= ::.. ....:--==-- ::.. ... ... . .. \n ... .......::. ..:----::=..= ..:-+XX= =--:... ..=.::. .=.:.:.. .. ..=...::. \n . +......:.........:: . ..:-----=::.. ..:--=XX+====::.=....::::. ..:--::=....-....:::::::.... .. \n .... .. .=.::-::::::....:::..=.::-------::....:.:---=++===++==-:..:-==--::.. .:----::::::::--::--+:::::....=.. \n ...............::----- -:: ::::.:=:::-===----:.=..::-=-=+X+X++++==-:::=-+XX+=--. ...:::--:::=::-=--=---:----.::.::. . \n ....::::....::::+:---:==---=-=-::+::::--=X+===-:+:..::-==++X XXXXX+==:--=-=+X+=+=-::..::------:--==+=++==---====---::.-.... \n ...:::--:-:::::------===++====++-----++-=+X+X+=:=-:.:::-=+-+XXXXXXX++++-==-=+-+===---:: -=+++== =:+++XX+++===+=+X+==--::... .-... \n ..+...:----------=========XX=X++-+X+===---==++=XX+==+==--==+++XXXXXXXXX+++==--==+X++===---++XXXX+.++XXXXXXXXX++===+XX++==--:::::.-.......- \n ....=..::-========-=:++++ +++XXXXXXXX++=====+-=++XXXXX+++++XX++XX:XXXXXX.X+======+XX.+++++++==+X:XXXXXXXX+X:XXX-X+++XXXX++==--:--:::::.......\n",
|
||||
" \n \n \n \n \n \n \n \n \n \n . \n .. ..... \n . .::..:. \n .. .:.. .. ..-:-:::.. .. \n . .-. ....... . ..:::=---::::.. ... . ... \n ... . ... .:::...... .. ..:--++=-----:. ...:.. .. ...... . . \n :..: ..:. ... ..:=--::.... . .:: -=++==-=---:=....:::... .:::..... . +....... \n ..:-:.::.. .. ..-...:-:-- :::.....::---+====- ----::..:--=:-:.. .:--::.......+=:.:::........ \n . . ..:-:::::+.....+=.::-...-=--::::....:.::- -+=====+=+-::+:-=-++=--:. ..:-::.:.. ..::=::-::=-:::::..... \n ...............::------+::::::..::::::-=+==-::... .::--=+X+++ +X+=-: :--++==---::...::=--::.=.::--====--:--==-::.... \n ..:-::::..-.::-:-=-- =+==--:--=-:::::---=++=-::::....::-=++XXXX++++=+=-.--===.---: ::.:-=++=--:-==+++++-==-=-=+==--::.......... \n ...:::.:::::-:----- ==+XX+=====++---.:--=++++=---=-+::---=+XXXXXXXX+==--:---=++===- ::---+X++=-===++XXXX++=-==+++==--::::.:...... .:. \n ....:.::----------===-===+XXX X++ +X+===--+-==:+XX+==:====++=+++XX.XXXXX+===--==+XX+= ==--===+XX+++++X=XX XXXXX+++++X++==-.-:+:::.......... \n ..=...::---.-==+=+==++++++++-=XXXXXX X++++:+==:=++XXX++ +XXXX+XXXXXXX+.+XX++===+XXXX=X+.==--:==+XXXXXXXXXXXXXXXXX+++XX=++++-=-----.::........\n",
|
||||
" \n \n \n \n \n \n \n \n \n :... \n .:..: \n .. .:.:..+. \n .. . .. ...::.:.. . .. \n . .:-:-:--:...:. . . \n .. . .:..... . .::+===--:.::-:=.. ...:.. . \n .. . .:=-:::.. -.. .::-+===--::---:.....::+.. .:.... . ... \n .:...... ...::--::-:..... ..::==-=--::-:-.::.::--:::... .--::.. . .... . \n .::::........ ......----... .. .:+::==-------=-::::-===-::::. .::--:. .. ......::........ \n .. ..+.::-::--:..:.:...+..:..::==-:....: .::::-++========::.:-+=.=-::::.-..:-.-:::. ...::-----:: ---::.. \n .....-.+..:.:.:::--+==--::::-:-:..:::::-=--:+...+...:+:-=++X++== =--:::-+--=-:::..:..:-==--::.:--=+++==-----==--::.. . \n .....-.=..:::-:::--=-=+=-----:-=::.: ::--==--:::::..::+--+XXXX-====--:.::-=-==---::.::::=+==-----=+XX++-==:-===.--::........ \n ....-:::::::.------= -=+X++===+=.==-- --:--==++=-+--::-:====+XXXX+-+==-::=--=++==---:.:--==++==--=++ XXXXX++===+++===-::-:::....+:... \n ....-.::-:------====+=.=++:XX+X++++ ===+ ======+XX+==-== ++++++X+XXXXX=+==-==++XXXX==--::+--==XX++-+X XXXXXX:++++++++====-+-::::::........ \n ...=.::--=-=== ===++XXX++XXXXXXXXXX+XXX:++======+X XX+.++X++XX+++X+XX++XXX++=++ XXXX+===--:--=++XXXXXXXXXXXXXX++++XX+X+++.=-------::::......\n",
|
||||
" \n \n \n \n \n \n \n :.... \n . . \n . ....- \n .:..:::::.. \n .:..+.::... . \n =::::.::... . +...::.. \n .. .:-==-:-:....::... .::. \n .:-::... .:-+=---::..:::......::.. ....+ . \n . . :..:-:::.... .:--=--::::::::-:.::+:-:.. :=-::. .. \n ....... ..::-:::.. .:::=--::::::.-:---=---:...:.:.:--:...- ..::..=. ..... \n . ..:::::::..... .. ...-:-::. ...-::-+=-=-::::::.:-+--=--::.....::-::... -..:------:.:.:... \n .. . ....:=::-::-:-...:..:..::..:--::. . .:::--+ +=----::.=.:-=:--::.+....+.-=::.=...:-=:==--+- ---::=... \n ............::::::--:-+-::=:::-:..=:...:--:::.... ..+---=++X+=-=-::..:.:==:=--:....::::==--::---=++.====--=---=:-:..: ... \n .....::::-::.:--::---====-------::.:::.---==--:.: ..-==--==-++====-::...::==----=::.:.::-==-::--=+=++++++======----: :. .... \n ....-:=::::: --:==----=+==++=+==+=- --=.=+=.==X+=--:: -===+==+++X+-+==-::--==.++=-:::. ..:-==++===+++XX+++X====+===----:::::.:...:.. \n ....::---------==:++==+++++++++X+=++XX+=======+XX++ ===+==-++==++X XXX++==++++++++=--:.:.::-==XX++.XXXXXXXX++=++:XX++==--::+: :::.... . \n .. .::---=========+X++XXX+XXXXXXX++:XXXX+=+=== =+X=X++X+-+.+++++XXXX++XXXX+++==++XX+==----- -=+XXXXXXXXXXXXX+++++X-XXX++=--------.:::......\n",
|
||||
" \n \n \n \n \n \n . \n . . \n :. .:.. \n .. .. ..::. \n . .....:: ..:. \n .-:...-.:.. . .. .. \n . .:-=-::... ...+. :+.. \n ... .:-+=-::::. ........:--.. ..-.. \n . .=-:.... .::==::::......-::::..-:. .::... \n .... ..::=. .:-==:=:.......::---::-. .:-::::. ........ \n ........ . ..=.:. .=::==--:+......:-+.--::. .+. ..::-:. .:-::::-:::.:. \n .....:-:.::-.. .. .. :. ..:.. .::--+=--:. .. ..:=::=::.......::-:.. ..:-----:::-=-:.:.:. \n ...... .::::::::.::--...... .....:-:.. .::--=+=.=::.... ..:=::.:-..:....::--+....:-=+=--==-==-::::::... \n ....-:.....::::::::::--- :::::.=....::--==-:.. . ..:=--+====--::.......-+-:.::+.. ...:=-:..::-.=======+-==----:::...: .. \n ......::...:-::--::--------==-=-:::---=++==++=-::-..:--:-=:==++===-:..::::==- ::.... .--=+.---=+X===+==+==+= ----::........ \n ...=:::-::::--=-==-=+==--====+++-:===-=-==.++X+=----=---======+XXXX+=-:-===++==--::. . .:--X+===.:XX+.+=+===+++====-::.::-:.+.... \n ...:::------+=-==+.++X+=++XXXXXX++=+X+===+=-==+XX++=+=++ =====+XXXXXX +===+==++X+==::....::-++X ++XXXXXXX+===-=++-+++=-=--=::::::=.-.... \n ...:-----=----==+X++XXXXXXXXXXX+XX+++++==+==-==+XX.+=++-=====+++XX:XXX-X+=+++=+XXX+=--======+++XXXXXXXXXX+===+++XX++++=+------::::=:.....\n"
|
||||
]
|
||||
@@ -0,0 +1,36 @@
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
|
||||
import AnimatedWidth from "@/components/shared/layout/animated-width";
|
||||
import ArrowRight from "@/components/app/(home)/sections/hero-input/_svg/ArrowRight";
|
||||
import Button from "@/components/shared/button/Button";
|
||||
|
||||
export default function HeroInputSubmitButton({
|
||||
tab,
|
||||
dirty,
|
||||
}: {
|
||||
tab: string;
|
||||
dirty: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Button className="hero-input-button !p-0 bg-heat-100 hover:bg-heat-200" size="large" variant="primary">
|
||||
<AnimatedWidth>
|
||||
<AnimatePresence initial={false} mode="popLayout">
|
||||
<motion.div
|
||||
animate={{ opacity: 1, x: 0, filter: "blur(0px)" }}
|
||||
exit={{ opacity: 0, x: -10, filter: "blur(2px)" }}
|
||||
initial={{ opacity: 0, x: 10, filter: "blur(2px)" }}
|
||||
key={dirty ? "dirty" : "clean"}
|
||||
>
|
||||
{dirty ? (
|
||||
<div className="py-8 w-126 text-center text-white">Start building</div>
|
||||
) : (
|
||||
<div className="w-60 py-8 flex-center">
|
||||
<ArrowRight />
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</AnimatedWidth>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
|
||||
import Globe from "./_svg/Globe";
|
||||
import HeroInputSubmitButton from "./Button/Button";
|
||||
import HeroInputTabsMobile from "./Tabs/Mobile/Mobile";
|
||||
import HeroInputTabs from "./Tabs/Tabs";
|
||||
import AsciiExplosion from "@/components/shared/effects/flame/ascii-explosion";
|
||||
import { Endpoint } from "@/components/shared/Playground/Context/types";
|
||||
|
||||
export default function HeroInput() {
|
||||
const [tab, setTab] = useState<Endpoint>(Endpoint.Scrape);
|
||||
const [url, setUrl] = useState<string>("");
|
||||
|
||||
return (
|
||||
<div className="max-w-552 mx-auto w-full relative z-[11] lg:z-[2] rounded-20 lg:-mt-76">
|
||||
<div
|
||||
className="overlay bg-accent-white"
|
||||
style={{
|
||||
boxShadow:
|
||||
"0px 0px 44px 0px rgba(0, 0, 0, 0.02), 0px 88px 56px -20px rgba(0, 0, 0, 0.03), 0px 56px 56px -20px rgba(0, 0, 0, 0.02), 0px 32px 32px -20px rgba(0, 0, 0, 0.03), 0px 16px 24px -12px rgba(0, 0, 0, 0.03), 0px 0px 0px 1px rgba(0, 0, 0, 0.05), 0px 0px 0px 10px #F9F9F9",
|
||||
}}
|
||||
/>
|
||||
|
||||
<label className="p-16 flex gap-8 items-center w-full relative border-b border-black-alpha-5">
|
||||
<Globe />
|
||||
|
||||
<input
|
||||
className="w-full bg-transparent text-body-input text-accent-black placeholder:text-black-alpha-48"
|
||||
placeholder="https://example.com"
|
||||
type="text"
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
(
|
||||
document.querySelector(
|
||||
".hero-input-button",
|
||||
) as HTMLButtonElement
|
||||
)?.click();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className="p-10 flex justify-between items-center relative">
|
||||
<HeroInputTabs
|
||||
setTab={setTab}
|
||||
tab={tab}
|
||||
allowedModes={[
|
||||
Endpoint.Scrape,
|
||||
Endpoint.Search,
|
||||
Endpoint.Map,
|
||||
Endpoint.Crawl,
|
||||
]}
|
||||
/>
|
||||
|
||||
<HeroInputTabsMobile
|
||||
setTab={setTab}
|
||||
tab={tab}
|
||||
allowedModes={[
|
||||
Endpoint.Scrape,
|
||||
Endpoint.Search,
|
||||
Endpoint.Map,
|
||||
Endpoint.Crawl,
|
||||
]}
|
||||
/>
|
||||
|
||||
<Link
|
||||
className="contents"
|
||||
href={`/playground?endpoint=${tab}&url=${url}&autorun=true`}
|
||||
>
|
||||
<HeroInputSubmitButton dirty={url.length > 0} tab={tab} />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="h-248 top-84 cw-768 pointer-events-none absolute overflow-clip -z-10">
|
||||
<AsciiExplosion className="-top-200" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
//@ts-nocheck
|
||||
import { animate, AnimatePresence, cubicBezier, motion } from "motion/react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import { tabs } from "@/components/app/(home)/sections/hero-input/Tabs/Tabs";
|
||||
import { cn } from "@/utils/cn";
|
||||
import { Endpoint } from "@/components/shared/Playground/Context/types";
|
||||
|
||||
export default function HeroInputTabsMobile(props: {
|
||||
setTab: (tab: Endpoint) => void;
|
||||
tab: Endpoint;
|
||||
allowedModes?: Endpoint[];
|
||||
}) {
|
||||
// Filter tabs based on allowedModes if provided
|
||||
const visibleTabs = props.allowedModes
|
||||
? tabs.filter((tab) => props.allowedModes.includes(tab.value))
|
||||
: tabs;
|
||||
|
||||
const activeTab = visibleTabs.find((tab) => tab.value === props.tab)!;
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const ref = useRef<HTMLButtonElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (window.innerWidth > 996) {
|
||||
return;
|
||||
}
|
||||
|
||||
document.addEventListener("click", (e) => {
|
||||
if (ref.current && e.composedPath().includes(ref.current)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsOpen(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className="py-8 px-10 flex items-center rounded-10 before:inside-border before:border-black-alpha-4 relative lg:hidden gap-4"
|
||||
ref={ref}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
<activeTab.icon size={24} alwaysHeat />
|
||||
<div className="px-6 text-label-medium">{activeTab.label}</div>
|
||||
<svg
|
||||
className={cn(
|
||||
"transition-all duration-200",
|
||||
isOpen ? "rotate-180 text-accent-black" : "text-black-alpha-48",
|
||||
)}
|
||||
fill="none"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8.4001 10.2L12.0001 13.8L15.6001 10.2"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.25"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<AnimatePresence mode="popLayout">
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
animate={{ opacity: 1, filter: "blur(0px)" }}
|
||||
className="absolute z-[1001] top-[calc(100%-4px)] left-[calc(50%-(50vw-6px))] w-[calc(100vw-12px)]"
|
||||
exit={{ opacity: 0, filter: "blur(2px)" }}
|
||||
initial={{ opacity: 0, filter: "blur(2px)" }}
|
||||
transition={{
|
||||
duration: 0.2,
|
||||
ease: cubicBezier(0.25, 0.1, 0.25, 1.0),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="mx-auto w-full p-4 max-w-366 rounded-16 bg-accent-white"
|
||||
style={{
|
||||
boxShadow:
|
||||
"0 32px 40px 6px rgba(0, 0, 0, 0.02), 0 12px 32px 0 rgba(0, 0, 0, 0.02), 0 24px 32px -8px rgba(0, 0, 0, 0.02), 0 8px 16px -2px rgba(0, 0, 0, 0.02), 0 0 0 1px rgba(0, 0, 0, 0.04)",
|
||||
}}
|
||||
>
|
||||
<div className="py-10 px-12 text-label-small text-black-alpha-48">
|
||||
Output
|
||||
</div>
|
||||
|
||||
<MenuItems
|
||||
setTab={props.setTab}
|
||||
tab={props.tab}
|
||||
visibleTabs={visibleTabs}
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function MenuItems(props: {
|
||||
tab: Endpoint;
|
||||
setTab: (tab: Endpoint) => void;
|
||||
visibleTabs: typeof tabs;
|
||||
}) {
|
||||
const backgroundRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div
|
||||
className="absolute top-0 opacity-0 left-0 bg-black-alpha-4 rounded-12 w-full pointer-events-none"
|
||||
ref={backgroundRef}
|
||||
/>
|
||||
|
||||
{props.visibleTabs.map((tab) => (
|
||||
<div
|
||||
className="text-label-small select-none cursor-pointer flex gap-12 py-12 px-16"
|
||||
key={tab.value}
|
||||
onClick={() => {
|
||||
animate(
|
||||
backgroundRef.current!,
|
||||
{
|
||||
scaleX: [1, 0.99, 1],
|
||||
scaleY: [1, 0.96, 1],
|
||||
opacity: [1, 0.9, 1],
|
||||
},
|
||||
{
|
||||
ease: cubicBezier(0.165, 0.84, 0.44, 1),
|
||||
duration: 0.15,
|
||||
},
|
||||
);
|
||||
|
||||
props.setTab(tab.value);
|
||||
}}
|
||||
onMouseEnter={async (e) => {
|
||||
const child = e.currentTarget as HTMLElement;
|
||||
|
||||
if (backgroundRef.current?.getBoundingClientRect().height === 0) {
|
||||
backgroundRef.current!.style.height = child.offsetHeight + "px";
|
||||
}
|
||||
|
||||
if (getComputedStyle(backgroundRef.current!).opacity === "0") {
|
||||
await animate(
|
||||
backgroundRef.current!,
|
||||
{
|
||||
y: child.offsetTop,
|
||||
},
|
||||
{
|
||||
ease: cubicBezier(0.165, 0.84, 0.44, 1),
|
||||
duration: 0.01,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
animate(backgroundRef.current!, { scale: 0.995 }).then(() =>
|
||||
animate(backgroundRef.current!, { scale: 1 }),
|
||||
);
|
||||
|
||||
animate(
|
||||
backgroundRef.current!,
|
||||
{
|
||||
y: child.offsetTop,
|
||||
opacity: 1,
|
||||
height: child.offsetHeight + "px",
|
||||
},
|
||||
{
|
||||
ease: cubicBezier(0.165, 0.84, 0.44, 1),
|
||||
duration: 0.2,
|
||||
},
|
||||
);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
animate(
|
||||
backgroundRef.current!,
|
||||
{
|
||||
opacity: 0,
|
||||
},
|
||||
{
|
||||
ease: cubicBezier(0.165, 0.84, 0.44, 1),
|
||||
duration: 0.2,
|
||||
},
|
||||
);
|
||||
}}
|
||||
>
|
||||
<div className="size-24 p-2">
|
||||
<tab.icon size={20} alwaysHeat />
|
||||
</div>
|
||||
<div className="px-6 text-label-medium">{tab.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
import { animate } from "motion";
|
||||
import { Fragment, useRef } from "react";
|
||||
|
||||
import EndpointsSearch from "@/components/app/(home)/sections/endpoints/EndpointsSearch/EndpointsSearch";
|
||||
import EndpointsCrawl from "@/components/app/(home)/sections/endpoints/EndpointsCrawl/EndpointsCrawl";
|
||||
import EndpointsMap from "@/components/app/(home)/sections/endpoints/EndpointsMap/EndpointsMap";
|
||||
import EndpointsScrape from "@/components/app/(home)/sections/endpoints/EndpointsScrape/EndpointsScrape";
|
||||
import EndpointsExtract from "@/components/app/(home)/sections/endpoints/EndpointsExtract/EndpointsExtract";
|
||||
|
||||
import { cn } from "@/utils/cn";
|
||||
import Tooltip from "@/components/ui/shadcn/tooltip";
|
||||
import { Endpoint } from "@/components/shared/Playground/Context/types";
|
||||
|
||||
export const tabs = [
|
||||
{
|
||||
label: "Scrape",
|
||||
value: Endpoint.Scrape,
|
||||
action: "scraping",
|
||||
description:
|
||||
"Scrapes only the specified URL without crawling subpages. Outputs the content from the page.",
|
||||
icon: EndpointsScrape,
|
||||
},
|
||||
{
|
||||
label: "Search",
|
||||
value: Endpoint.Search,
|
||||
description: "Search the web and get full content from results",
|
||||
action: "searching",
|
||||
icon: EndpointsSearch,
|
||||
new: true,
|
||||
},
|
||||
{
|
||||
label: "Map",
|
||||
value: Endpoint.Map,
|
||||
action: "mapping",
|
||||
description: "Attempts to output all website's urls in a few seconds.",
|
||||
icon: EndpointsMap,
|
||||
},
|
||||
{
|
||||
label: "Crawl",
|
||||
value: Endpoint.Crawl,
|
||||
action: "crawling",
|
||||
description:
|
||||
"Crawls a URL and all its accessible subpages, outputting the content from each page.",
|
||||
icon: EndpointsCrawl,
|
||||
},
|
||||
{
|
||||
label: "Extract",
|
||||
value: Endpoint.Extract,
|
||||
action: "extracting",
|
||||
description:
|
||||
"Extract structured data from pages using LLMs. Provide URLs and a schema to get organized data.",
|
||||
icon: EndpointsExtract,
|
||||
new: true,
|
||||
},
|
||||
];
|
||||
|
||||
export default function HeroInputTabs(props: {
|
||||
setTab: (tab: Endpoint) => void;
|
||||
tab: Endpoint;
|
||||
disabled?: boolean;
|
||||
allowedModes?: Endpoint[];
|
||||
}) {
|
||||
// Filter tabs based on allowedModes if provided
|
||||
const visibleTabs = props.allowedModes
|
||||
? tabs.filter((tab) => props.allowedModes!.includes(tab.value))
|
||||
: tabs;
|
||||
|
||||
const activeIndex = visibleTabs.findIndex((tab) => tab.value === props.tab);
|
||||
|
||||
const backgroundRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="bg-black-alpha-4 flex items-center rounded-10 p-2 relative lg-max:hidden"
|
||||
style={{
|
||||
boxShadow:
|
||||
"0px 6px 12px 0px rgba(0, 0, 0, 0.02) inset, 0px 0.75px 0.75px 0px rgba(0, 0, 0, 0.02) inset, 0px 0.25px 0.25px 0px rgba(0, 0, 0, 0.04) inset",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="absolute top-2 left-2 h-32 bg-accent-white rounded-8 w-89"
|
||||
ref={backgroundRef}
|
||||
style={{
|
||||
boxShadow:
|
||||
"0px 6px 12px -3px rgba(0, 0, 0, 0.04), 0px 3px 6px -1px rgba(0, 0, 0, 0.04), 0px 1px 2px 0px rgba(0, 0, 0, 0.04), 0px 0.5px 0.5px 0px rgba(0, 0, 0, 0.06)",
|
||||
}}
|
||||
/>
|
||||
|
||||
{visibleTabs.map((tab, index) => (
|
||||
<Fragment key={tab.value}>
|
||||
{index > 0 && (
|
||||
<div
|
||||
className={cn(
|
||||
"px-2 transition-all",
|
||||
!(index !== activeIndex && index !== activeIndex + 1) &&
|
||||
"opacity-0",
|
||||
)}
|
||||
>
|
||||
<div className="w-1 h-12 bg-black-alpha-5" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
className={cn(
|
||||
"text-label-medium p-6 relative transition-all group flex items-center",
|
||||
tab.value === props.tab
|
||||
? "text-accent-black"
|
||||
: "text-black-alpha-56",
|
||||
!tab.new && "pr-4",
|
||||
)}
|
||||
key={tab.value}
|
||||
ref={(element) => {
|
||||
if (element && backgroundRef.current) {
|
||||
if (activeIndex === index) {
|
||||
animate(
|
||||
backgroundRef.current,
|
||||
{
|
||||
x: element.offsetLeft - 2,
|
||||
width: element.offsetWidth - 1,
|
||||
},
|
||||
{
|
||||
type: "spring",
|
||||
stiffness: 200,
|
||||
damping: 23,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}}
|
||||
onClick={(e) => {
|
||||
props.setTab(tab.value);
|
||||
|
||||
const t = e.target as HTMLElement;
|
||||
|
||||
const target =
|
||||
t instanceof HTMLButtonElement
|
||||
? t
|
||||
: (t.closest("button") as HTMLButtonElement);
|
||||
|
||||
if (backgroundRef.current) {
|
||||
animate(backgroundRef.current, { scale: 0.975 }).then(() =>
|
||||
animate(backgroundRef.current!, { scale: 1 }),
|
||||
);
|
||||
|
||||
animate(
|
||||
backgroundRef.current,
|
||||
{
|
||||
x: target.offsetLeft - 2,
|
||||
width: target.offsetWidth - 1,
|
||||
},
|
||||
{
|
||||
type: "spring",
|
||||
stiffness: 250,
|
||||
damping: 25,
|
||||
},
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{tab.icon && <tab.icon active={tab.value === props.tab} />}
|
||||
|
||||
<span className="px-6"> {tab.label}</span>
|
||||
|
||||
{tab.new && (
|
||||
<div
|
||||
className={cn(
|
||||
"py-2 px-6 rounded-4 text-[12px]/[16px] font-[450] transition-all",
|
||||
tab.value === props.tab
|
||||
? "bg-heat-12 text-heat-100"
|
||||
: "bg-black-alpha-4 text-black-alpha-56",
|
||||
)}
|
||||
>
|
||||
New
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Tooltip delay={0.25} description={tab.description} offset={-8} />
|
||||
</button>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
export default function ArrowRight() {
|
||||
return (
|
||||
<svg
|
||||
fill="none"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M11.6667 4.79163L16.875 9.99994M16.875 9.99994L11.6667 15.2083M16.875 9.99994H3.125"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
export default function Globe() {
|
||||
return (
|
||||
<svg
|
||||
fill="none"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 19.7083C16.2572 19.7083 19.7083 16.2572 19.7083 12C19.7083 7.74276 16.2572 4.29163 12 4.29163M12 19.7083C7.74276 19.7083 4.29163 16.2572 4.29163 12C4.29163 7.74276 7.74276 4.29163 12 4.29163M12 19.7083C10.044 19.7083 8.45829 16.2572 8.45829 12C8.45829 7.74276 10.044 4.29163 12 4.29163M12 19.7083C13.956 19.7083 15.5416 16.2572 15.5416 12C15.5416 7.74276 13.956 4.29163 12 4.29163M19.5 12H4.49996"
|
||||
stroke="#262626"
|
||||
strokeLinecap="square"
|
||||
strokeOpacity="0.32"
|
||||
strokeWidth="1.25"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import CurvyRect, { Connector } from "@/components/shared/layout/curvy-rect";
|
||||
import { encryptText } from "@/components/app/(home)/sections/hero/Title/Title";
|
||||
|
||||
import HeroScrapingCodeLoading from "./Loading/Loading";
|
||||
import Code from "@/components/ui/code";
|
||||
|
||||
const URL = {
|
||||
value: "https://example.com",
|
||||
encrypted: "h=t*A:!/z!aap?A-cZz",
|
||||
};
|
||||
const MARKDOWN = {
|
||||
value: "# Getting Started...",
|
||||
encrypted: "# ?0z-ang S*a-Z-a0*9",
|
||||
};
|
||||
const TITLE = {
|
||||
value: "Guide",
|
||||
encrypted: "G!=*?",
|
||||
};
|
||||
const SCREENSHOT = {
|
||||
value: "https://example.com/hero",
|
||||
encrypted: "ht-=*:/?*Za!zl=-?a9?h0-!",
|
||||
};
|
||||
|
||||
export default function HeroScrapingCode({ step }: { step: number }) {
|
||||
const [url, setUrl] = useState(URL.encrypted);
|
||||
const [markdown, setMarkdown] = useState(MARKDOWN.encrypted);
|
||||
const [title, setTitle] = useState(TITLE.encrypted);
|
||||
const [screenshot, setScreenshot] = useState(SCREENSHOT.encrypted);
|
||||
|
||||
const reveal = useCallback((value: string, setter: (v: string) => void) => {
|
||||
let progress = 0;
|
||||
let increaseProgress = -10;
|
||||
|
||||
const animate = () => {
|
||||
increaseProgress = (increaseProgress + 1) % 5;
|
||||
|
||||
if (increaseProgress === 4) {
|
||||
progress += 0.2;
|
||||
}
|
||||
|
||||
if (progress > 1) {
|
||||
progress = 1;
|
||||
setter(encryptText(value, progress, { randomizeChance: 0.3 }));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setter(encryptText(value, progress, { randomizeChance: 0.3 }));
|
||||
|
||||
const interval = 70 + progress * 30;
|
||||
setTimeout(animate, interval);
|
||||
};
|
||||
|
||||
animate();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (step >= 0 && url === URL.encrypted) reveal(URL.value, setUrl);
|
||||
|
||||
if (step >= 3 && title === TITLE.encrypted) reveal(TITLE.value, setTitle);
|
||||
if (step >= 4 && markdown === MARKDOWN.encrypted)
|
||||
reveal(MARKDOWN.value, setMarkdown);
|
||||
|
||||
if (step >= 5 && screenshot === SCREENSHOT.encrypted)
|
||||
reveal(SCREENSHOT.value, setScreenshot);
|
||||
|
||||
const interval = setInterval(() => {
|
||||
if (step < 0) {
|
||||
URL.encrypted = encryptText(URL.value, 0, { randomizeChance: 0.3 });
|
||||
setUrl(URL.encrypted);
|
||||
}
|
||||
|
||||
if (step < 3) {
|
||||
TITLE.encrypted = encryptText(TITLE.value, 0, { randomizeChance: 0.3 });
|
||||
setTitle(TITLE.encrypted);
|
||||
}
|
||||
|
||||
if (step < 4) {
|
||||
MARKDOWN.encrypted = encryptText(MARKDOWN.value, 0, {
|
||||
randomizeChance: 0.3,
|
||||
});
|
||||
setMarkdown(MARKDOWN.encrypted);
|
||||
}
|
||||
|
||||
if (step < 5) {
|
||||
SCREENSHOT.encrypted = encryptText(SCREENSHOT.value, 0, {
|
||||
randomizeChance: 0.3,
|
||||
});
|
||||
setScreenshot(SCREENSHOT.encrypted);
|
||||
}
|
||||
}, 70);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [step, reveal]);
|
||||
|
||||
return (
|
||||
<div className="h-280 lg:h-310 flex z-[1] w-full relative -top-1 bg-background-base">
|
||||
<Connector className="lg:hidden absolute -top-10 -left-[10.5px]" />
|
||||
<Connector className="lg:hidden absolute -top-10 -right-[10.5px]" />
|
||||
<div className="lg:hidden absolute top-0 left-[calc(50%-50vw)] w-screen h-1 bg-border-faint" />
|
||||
|
||||
<Connector className="lg:hidden absolute -bottom-10 -left-[10.5px]" />
|
||||
<Connector className="lg:hidden absolute -bottom-10 -right-[10.5px]" />
|
||||
<div className="lg:hidden absolute bottom-0 left-[calc(50%-50vw)] w-screen h-1 bg-border-faint" />
|
||||
|
||||
<div className="flex-1 lg-max:min-w-0 h-full relative lg:before:inside-border before:border-border-faint">
|
||||
<CurvyRect className="overlay" allSides />
|
||||
<CurvyRect
|
||||
className="size-32 absolute bottom-0 -left-31 lg-max:hidden"
|
||||
bottomRight
|
||||
/>
|
||||
|
||||
<div className="pl-15 border-b border-border-faint p-13 flex justify-between items-center">
|
||||
<div className="flex gap-10 items-center">
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<div
|
||||
className="w-12 h-12 rounded-full relative before:inside-border before:border-border-muted"
|
||||
key={index}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="text-mono-x-small font-mono text-black-alpha-20">
|
||||
[ .JSON ]
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-scroll hide-scrollbar lg:contents relative">
|
||||
<Code
|
||||
code={`[
|
||||
{
|
||||
"url": "${url}",
|
||||
"markdown": "${markdown}",
|
||||
"json": { "title": "${title}", "docs": "..." },
|
||||
"screenshot": "${screenshot}.png"
|
||||
}
|
||||
]`}
|
||||
language="json"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<HeroScrapingCodeLoading finished={step >= 6} />
|
||||
</div>
|
||||
|
||||
<div className="w-28 lg-max:hidden -ml-1 relative">
|
||||
<div className="h-1 w-[calc(100%-1px)] top-0 left-0 absolute bg-border-faint" />
|
||||
<CurvyRect className="overlay" topLeft />
|
||||
</div>
|
||||
|
||||
<div className="h-53 lg-max:hidden -right-37 bottom-0 absolute w-65">
|
||||
<CurvyRect className="overlay" bottom topRight />
|
||||
<div className="overlay border-y border-border-faint" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { encryptText } from "@/components/app/(home)/sections/hero/Title/Title";
|
||||
import AnimatedWidth from "@/components/shared/layout/animated-width";
|
||||
import Spinner from "@/components/ui/spinner";
|
||||
|
||||
export default function HeroScrapingCodeLoading({
|
||||
finished,
|
||||
}: {
|
||||
finished: boolean;
|
||||
}) {
|
||||
const [scrapingText, setScrapingText] = useState("Scraping...");
|
||||
|
||||
useEffect(() => {
|
||||
if (finished) return;
|
||||
|
||||
let timeout = 0;
|
||||
let tick = 0;
|
||||
|
||||
const animate = () => {
|
||||
tick += 1;
|
||||
|
||||
if (tick % 3 !== 0) {
|
||||
setScrapingText(
|
||||
encryptText("Scraping", 0, {
|
||||
randomizeChance: 0.6 + Math.random() * 0.3,
|
||||
}) + "...",
|
||||
);
|
||||
} else {
|
||||
setScrapingText("Scraping...");
|
||||
}
|
||||
|
||||
const interval = 80;
|
||||
timeout = window.setTimeout(animate, interval);
|
||||
};
|
||||
|
||||
animate();
|
||||
|
||||
return () => {
|
||||
window.clearTimeout(timeout);
|
||||
};
|
||||
}, [finished]);
|
||||
|
||||
return (
|
||||
<div className="flex gap-6 p-6 pr-0 rounded-full before:inside-border before:border-border-faint absolute right-20 bottom-20 text-mono-small font-mono text-accent-black">
|
||||
<Spinner finished={finished} />
|
||||
|
||||
<AnimatedWidth initial={{ width: "auto" }}>
|
||||
<AnimatePresence initial={false} mode="popLayout">
|
||||
<motion.div
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
className="pr-12"
|
||||
exit={{ opacity: 0, x: 10 }}
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
>
|
||||
{finished ? "Scrape Completed" : scrapingText}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</AnimatedWidth>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
export default function Check() {
|
||||
return (
|
||||
<svg
|
||||
fill="none"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
d="M10 2.5C5.85786 2.5 2.5 5.85786 2.5 10C2.5 14.1421 5.85786 17.5 10 17.5C14.1421 17.5 17.5 14.1421 17.5 10C17.5 5.85786 14.1421 2.5 10 2.5ZM12.8305 8.59995C13.0928 8.27937 13.0455 7.80685 12.7249 7.54455C12.4043 7.28226 11.9318 7.32951 11.6695 7.65009L8.81932 11.1337L7.90533 10.2197C7.61244 9.9268 7.13756 9.9268 6.84467 10.2197C6.55178 10.5126 6.55178 10.9875 6.84467 11.2804L8.34467 12.7804C8.4945 12.9302 8.70073 13.0096 8.91236 12.9991C9.12399 12.9885 9.32129 12.8889 9.45547 12.725L12.8305 8.59995Z"
|
||||
fill="#FA5D19"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
.hero-scraping-highlight::before {
|
||||
animation: hero-scraping-highlight-before 1s linear infinite;
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
@keyframes hero-scraping-highlight-before {
|
||||
0% {
|
||||
border-color: var(--border-loud);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
40% {
|
||||
opacity: 0.25;
|
||||
border-color: var(--heat-100);
|
||||
}
|
||||
|
||||
80% {
|
||||
border-color: var(--border-loud);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
"use client";
|
||||
|
||||
import { animate } from "motion";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import CurvyRect from "@/components/shared/layout/curvy-rect";
|
||||
import { sleep } from "@/utils/sleep";
|
||||
|
||||
import BrowserMobile from "./_svg/BrowserMobile";
|
||||
import BrowserTab from "./_svg/BrowserTab";
|
||||
import HeroScrapingCode from "./Code/Code";
|
||||
import HeroScrapingTag from "./Tag/Tag";
|
||||
|
||||
import "./HeroScraping.css";
|
||||
|
||||
export default function HeroScraping() {
|
||||
const [step, setStep] = useState(-1);
|
||||
|
||||
const navigationRef = useRef<HTMLDivElement>(null);
|
||||
const buttonRef = useRef<HTMLDivElement>(null);
|
||||
const h1Ref = useRef<HTMLDivElement>(null);
|
||||
const descriptionRef = useRef<HTMLDivElement>(null);
|
||||
const ctaRef = useRef<HTMLDivElement>(null);
|
||||
const highlightRef = useRef<HTMLDivElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const wrapElement = async (
|
||||
element: HTMLElement,
|
||||
{ borderRadius }: { borderRadius?: number } = {},
|
||||
) => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const containerBnds = containerRef.current.getBoundingClientRect();
|
||||
const elementBnds = element.getBoundingClientRect();
|
||||
|
||||
if (!highlightRef.current) return;
|
||||
|
||||
try {
|
||||
if (highlightRef.current) {
|
||||
await animate(highlightRef.current, { opacity: 0 }, { duration: 0.3 });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error animating highlight:", error);
|
||||
}
|
||||
|
||||
if (!highlightRef.current) return;
|
||||
|
||||
Object.assign(highlightRef.current.style, {
|
||||
left: elementBnds.left - containerBnds.left - 4 + "px",
|
||||
top: elementBnds.top - containerBnds.top - 4 + "px",
|
||||
width: elementBnds.width + 8 + "px",
|
||||
height: elementBnds.height + 8 + "px",
|
||||
borderRadius: borderRadius ? `${borderRadius}px` : undefined,
|
||||
});
|
||||
|
||||
try {
|
||||
await animate(
|
||||
highlightRef.current,
|
||||
{ opacity: [1, 0.5, 0.3, 0.8, 0.6, 0.9, 0.7, 1] },
|
||||
{ duration: 0.4 },
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error animating highlight:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const start = async () => {
|
||||
setStep(0);
|
||||
if (!highlightRef.current) return;
|
||||
|
||||
await animate(highlightRef.current, {
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
});
|
||||
|
||||
await sleep(700);
|
||||
|
||||
setTimeout(() => setStep(1), 300);
|
||||
if (navigationRef.current) {
|
||||
await wrapElement(navigationRef.current);
|
||||
}
|
||||
|
||||
await sleep(1200);
|
||||
|
||||
setTimeout(() => setStep(2), 300);
|
||||
if (buttonRef.current) {
|
||||
await wrapElement(buttonRef.current);
|
||||
}
|
||||
|
||||
await sleep(1200);
|
||||
|
||||
setTimeout(() => setStep(3), 300);
|
||||
if (h1Ref.current) {
|
||||
await wrapElement(h1Ref.current, { borderRadius: 12 });
|
||||
}
|
||||
|
||||
await sleep(1200);
|
||||
|
||||
setTimeout(() => setStep(4), 300);
|
||||
if (descriptionRef.current) {
|
||||
await wrapElement(descriptionRef.current, { borderRadius: 8 });
|
||||
}
|
||||
|
||||
await sleep(1200);
|
||||
|
||||
setTimeout(() => setStep(5), 300);
|
||||
if (ctaRef.current) {
|
||||
await wrapElement(ctaRef.current, { borderRadius: 24 });
|
||||
}
|
||||
|
||||
await sleep(1500);
|
||||
setTimeout(() => setStep(6), 300);
|
||||
|
||||
if (highlightRef.current) {
|
||||
await animate(highlightRef.current, { opacity: 0 }, { duration: 0.3 });
|
||||
}
|
||||
};
|
||||
|
||||
let started = false;
|
||||
|
||||
const onScroll = () => {
|
||||
if (started) return;
|
||||
|
||||
if (window.scrollY > 100) {
|
||||
started = true;
|
||||
start();
|
||||
window.removeEventListener("scroll", onScroll);
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
if (started) return;
|
||||
|
||||
started = true;
|
||||
start();
|
||||
window.removeEventListener("scroll", onScroll);
|
||||
}, 2000);
|
||||
|
||||
window.addEventListener("scroll", onScroll);
|
||||
|
||||
return () => window.removeEventListener("scroll", onScroll);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="pt-56 lg:pt-25 lg:px-25 container -mt-36 relative"
|
||||
ref={containerRef}
|
||||
>
|
||||
<div className="h-53 absolute top-[calc(100%-1px)] w-full left-0">
|
||||
<div className="h-1 bg-border-faint bottom-0 left-0 w-full absolute" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="left-61 top-89 rounded-[16px] size-32 absolute hero-scraping-highlight before:inside-border before:border-border-loud opacity-0 scale-[0.9]"
|
||||
ref={highlightRef}
|
||||
/>
|
||||
|
||||
<div className="overlay lg-max:hidden">
|
||||
<div className="h-1 absolute bottom-0 w-full left-0 bg-border-faint" />
|
||||
<CurvyRect className="overlay" bottom />
|
||||
</div>
|
||||
|
||||
<div className="lg:h-370 rounded-t-16 lg-max:pt-70 relative">
|
||||
<div className="overlay mask-border lg-max:hidden p-1 bg-gradient-to-b from-black/7 to-transparent" />
|
||||
|
||||
<div className="top-17 left-17 flex gap-8 items-center absolute lg-max:hidden">
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<div
|
||||
className="w-10 h-10 rounded-full relative before:inside-border before:border-border-muted"
|
||||
key={index}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="pt-42 lg:px-6">
|
||||
<BrowserMobile className="absolute top-0 cw-316 lg:hidden" />
|
||||
|
||||
<BrowserTab className="absolute top-[7.5px] left-70 lg-max:hidden bg-background-base z-[1]" />
|
||||
<div className="absolute size-18 top-17 left-89 lg-max:hidden before:inside-border before:border-border-muted z-[2] rounded-full" />
|
||||
|
||||
<div className="rounded-t-16 relative lg:h-330 lg:p-6">
|
||||
<div className="overlay mask-border lg-max:hidden p-1 bg-gradient-to-b from-black/7 to-transparent" />
|
||||
|
||||
<div className="lg:h-322 rounded-t-10 relative">
|
||||
<div className="overlay mask-border lg-max:hidden p-1 bg-gradient-to-b z-[2] from-black/7 to-transparent" />
|
||||
|
||||
<div className="px-28 lg-max:hidden py-20 flex justify-between items-center relative border-b border-border-faint">
|
||||
<div className="flex gap-8 items-center relative">
|
||||
<div className="size-24 rounded-full relative before:inside-border before:border-border-muted" />
|
||||
<div className="w-64 h-12 rounded-full relative before:inside-border before:border-border-muted" />
|
||||
|
||||
{step >= 0 && (
|
||||
<HeroScrapingTag
|
||||
active={step === 0}
|
||||
className="absolute left-[calc(100%+24px)] top-0"
|
||||
initial={{ x: -12, opacity: 0 }}
|
||||
label="Logo"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="absolute top-24 center-x flex gap-8"
|
||||
ref={navigationRef}
|
||||
>
|
||||
{step >= 1 && (
|
||||
<HeroScrapingTag
|
||||
active={step === 1}
|
||||
className="absolute right-[calc(100%+20px)] -top-4"
|
||||
initial={{ x: 12, opacity: 0 }}
|
||||
label="Navigation"
|
||||
/>
|
||||
)}
|
||||
|
||||
{Array.from({ length: 4 }).map((_, index) => (
|
||||
<div
|
||||
className="w-64 h-16 rounded-full relative before:inside-border before:border-border-muted"
|
||||
key={index}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="w-72 h-24 rounded-full relative before:inside-border before:border-border-muted"
|
||||
ref={buttonRef}
|
||||
>
|
||||
{step >= 2 && (
|
||||
<HeroScrapingTag
|
||||
active={step === 2}
|
||||
className="absolute right-[calc(100%+20px)] top-0"
|
||||
initial={{ x: 12, opacity: 0 }}
|
||||
label="Button"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="lg:grid grid-cols-2">
|
||||
<div className="pt-40 pl-151 flex gap-16 relative lg-max:hidden">
|
||||
<CurvyRect
|
||||
className="size-32 -top-1 -right-1 absolute"
|
||||
topRight
|
||||
/>
|
||||
|
||||
<div className="h-53 lg-max:hidden -left-37 bottom-1 absolute w-65">
|
||||
<CurvyRect className="overlay" left />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div
|
||||
className="flex gap-16 mb-16 flex-wrap w-300 relative"
|
||||
ref={h1Ref}
|
||||
>
|
||||
{step >= 3 && (
|
||||
<HeroScrapingTag
|
||||
active={step === 3}
|
||||
className="absolute right-[calc(100%+16px)] top-0"
|
||||
initial={{ x: 12, opacity: 0 }}
|
||||
label="H1 Title"
|
||||
/>
|
||||
)}
|
||||
<div className="w-144 h-32 rounded-8 relative before:inside-border before:border-border-muted" />
|
||||
<div className="w-82 h-32 rounded-8 relative before:inside-border before:border-border-muted" />
|
||||
<div className="w-100 h-32 rounded-8 relative before:inside-border before:border-border-muted" />
|
||||
<div className="w-180 h-32 rounded-8 relative before:inside-border before:border-border-muted" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex gap-6 mb-32 flex-wrap w-300 relative"
|
||||
ref={descriptionRef}
|
||||
>
|
||||
{step >= 4 && (
|
||||
<HeroScrapingTag
|
||||
active={step === 4}
|
||||
className="absolute top-0 right-[calc(100%+16px)]"
|
||||
initial={{ x: 12, opacity: 0 }}
|
||||
label="Description"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="w-131 h-10 rounded-full relative before:inside-border before:border-border-muted" />
|
||||
<div className="w-72 h-10 rounded-full relative before:inside-border before:border-border-muted" />
|
||||
<div className="w-34 h-10 rounded-full relative before:inside-border before:border-border-muted" />
|
||||
<div className="w-56 h-10 rounded-full relative before:inside-border before:border-border-muted" />
|
||||
<div className="w-116 h-10 rounded-full relative before:inside-border before:border-border-muted" />
|
||||
<div className="w-116 h-10 rounded-full relative before:inside-border before:border-border-muted" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="w-64 h-24 rounded-full relative before:inside-border before:border-border-muted"
|
||||
ref={ctaRef}
|
||||
>
|
||||
{step >= 5 && (
|
||||
<HeroScrapingTag
|
||||
active={step === 5}
|
||||
className="absolute top-0 right-[calc(100%+16px)]"
|
||||
initial={{ x: 12, opacity: 0 }}
|
||||
label="CTA Button"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<HeroScrapingCode step={step} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { motion } from "motion/react";
|
||||
import { ComponentProps, useEffect, useState } from "react";
|
||||
|
||||
import { encryptText } from "@/components/app/(home)/sections/hero/Title/Title";
|
||||
import { cn } from "@/utils/cn";
|
||||
|
||||
export default function HeroScrapingTag({
|
||||
active,
|
||||
label,
|
||||
...attrs
|
||||
}: ComponentProps<typeof motion.div> & { active?: boolean; label: string }) {
|
||||
const [value, setValue] = useState(
|
||||
encryptText(label, 0, { randomizeChance: 0 }),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
let progress = 0;
|
||||
let increaseProgress = -10;
|
||||
|
||||
const animate = () => {
|
||||
increaseProgress = (increaseProgress + 1) % 5;
|
||||
|
||||
if (increaseProgress === 4) {
|
||||
progress += 0.2;
|
||||
}
|
||||
|
||||
if (progress > 1) {
|
||||
progress = 1;
|
||||
setValue(encryptText(label, progress, { randomizeChance: 0 }));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setValue(encryptText(label, progress, { randomizeChance: 0 }));
|
||||
|
||||
const interval = 40 + progress * 20;
|
||||
setTimeout(animate, interval);
|
||||
};
|
||||
|
||||
animate();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
{...attrs}
|
||||
animate={{
|
||||
x: 0,
|
||||
y: 0,
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
filter: "blur(0px)",
|
||||
}}
|
||||
className={cn(
|
||||
"py-4 h-max font-mono w-max px-6 text-mono-x-small rounded-6 transition-colors",
|
||||
active
|
||||
? "bg-heat-12 text-heat-100"
|
||||
: "bg-black-alpha-4 text-black-alpha-56",
|
||||
attrs.className,
|
||||
)}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 100,
|
||||
damping: 18,
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
export default function BrowserMobile(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
fill="none"
|
||||
height="112"
|
||||
viewBox="0 0 316 112"
|
||||
width="316"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<g clipPath="url(#clip0_2254_6088)">
|
||||
<rect
|
||||
height="370"
|
||||
rx="15.5"
|
||||
stroke="url(#paint0_linear_2254_6088)"
|
||||
strokeOpacity="0.07"
|
||||
width="315"
|
||||
x="0.5"
|
||||
y="0.5"
|
||||
/>
|
||||
<mask fill="white" id="path-2-inside-1_2254_6088">
|
||||
<path d="M240 32C240 37.5228 244.477 42 250 42H294C302.837 42 310 49.1634 310 58V361C310 366.523 305.523 371 300 371H16C10.4772 371 6 366.523 6 361V58C6 49.1634 13.1634 42 22 42H70C75.5228 42 80 37.5228 80 32V18C80 12.4772 84.4772 8 90 8H230C235.523 8 240 12.4772 240 18V32Z" />
|
||||
</mask>
|
||||
<path
|
||||
d="M310 58L311 58L310 58ZM22 42L22 41L22 42ZM250 42V43H294V42V41H250V42ZM294 42V43C302.284 43 309 49.7157 309 58L310 58L311 58C311 48.6112 303.389 41 294 41V42ZM310 58H309V361H310H311V58H310ZM300 371V370H16V371V372H300V371ZM6 361H7V58H6H5V361H6ZM6 58H7C7 49.7157 13.7157 43 22 43L22 42L22 41C12.6112 41 5 48.6112 5 58H6ZM22 42V43H70V42V41H22V42ZM80 32H81V18H80H79V32H80ZM90 8V9H230V8V7H90V8ZM240 18H239V32H240H241V18H240ZM230 8V9C234.971 9 239 13.0294 239 18H240H241C241 11.9249 236.075 7 230 7V8ZM70 42V43C76.0751 43 81 38.0751 81 32H80H79C79 36.9706 74.9706 41 70 41V42ZM16 371V370C11.0294 370 7 365.971 7 361H6H5C5 367.075 9.92487 372 16 372V371ZM80 18H81C81 13.0294 85.0294 9 90 9V8V7C83.9249 7 79 11.9249 79 18H80ZM310 361H309C309 365.971 304.971 370 300 370V371V372C306.075 372 311 367.075 311 361H310ZM250 42V41C245.029 41 241 36.9706 241 32H240H239C239 38.0751 243.925 43 250 43V42Z"
|
||||
fill="url(#paint1_linear_2254_6088)"
|
||||
fillOpacity="0.07"
|
||||
mask="url(#path-2-inside-1_2254_6088)"
|
||||
/>
|
||||
<rect
|
||||
height="310"
|
||||
rx="9.5"
|
||||
stroke="url(#paint2_linear_2254_6088)"
|
||||
strokeOpacity="0.07"
|
||||
width="291"
|
||||
x="12.5"
|
||||
y="48.5"
|
||||
/>
|
||||
<rect
|
||||
height="9"
|
||||
rx="4.5"
|
||||
stroke="#E8E8E8"
|
||||
width="9"
|
||||
x="17.5"
|
||||
y="17.5"
|
||||
/>
|
||||
<rect
|
||||
height="9"
|
||||
rx="4.5"
|
||||
stroke="#E8E8E8"
|
||||
width="9"
|
||||
x="35.5"
|
||||
y="17.5"
|
||||
/>
|
||||
<rect
|
||||
height="9"
|
||||
rx="4.5"
|
||||
stroke="#E8E8E8"
|
||||
width="9"
|
||||
x="53.5"
|
||||
y="17.5"
|
||||
/>
|
||||
<rect
|
||||
height="17"
|
||||
rx="8.5"
|
||||
stroke="#E8E8E8"
|
||||
width="17"
|
||||
x="89.5"
|
||||
y="17.5"
|
||||
/>
|
||||
<mask fill="white" id="path-10-inside-2_2254_6088">
|
||||
<path d="M12 48H304V112H12V48Z" />
|
||||
</mask>
|
||||
<path
|
||||
d="M304 112V111H12V112V113H304V112Z"
|
||||
fill="#EDEDED"
|
||||
mask="url(#path-10-inside-2_2254_6088)"
|
||||
/>
|
||||
<rect
|
||||
height="23"
|
||||
rx="11.5"
|
||||
stroke="#E8E8E8"
|
||||
width="71"
|
||||
x="212.5"
|
||||
y="68.5"
|
||||
/>
|
||||
<circle cx="44" cy="80" r="11.5" stroke="#E8E8E8" />
|
||||
<rect
|
||||
height="11"
|
||||
rx="5.5"
|
||||
stroke="#E8E8E8"
|
||||
width="63"
|
||||
x="64.5"
|
||||
y="74.5"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="paint0_linear_2254_6088"
|
||||
x1="158"
|
||||
x2="158"
|
||||
y1="0"
|
||||
y2="371"
|
||||
>
|
||||
<stop />
|
||||
<stop offset="1" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="paint1_linear_2254_6088"
|
||||
x1="529.5"
|
||||
x2="529.5"
|
||||
y1="8"
|
||||
y2="324"
|
||||
>
|
||||
<stop offset="0.4" />
|
||||
<stop offset="1" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="paint2_linear_2254_6088"
|
||||
x1="158"
|
||||
x2="158"
|
||||
y1="48"
|
||||
y2="359"
|
||||
>
|
||||
<stop />
|
||||
<stop offset="1" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_2254_6088">
|
||||
<rect fill="white" height="112" width="316" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { HTMLAttributes } from "react";
|
||||
|
||||
export default function BrowserTab(attrs: HTMLAttributes<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
fill="none"
|
||||
height="36"
|
||||
viewBox="0 0 226 36"
|
||||
width="226"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...attrs}
|
||||
>
|
||||
<path
|
||||
d="M0 35C5.52285 35 10 30.5228 10 25V11C10 5.47715 14.4772 1 20 1H206C211.523 1 216 5.47715 216 11V25C216 30.5228 220.477 35 226 35"
|
||||
stroke="#E8E8E8"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
"use client";
|
||||
|
||||
import { Fragment } from "react";
|
||||
|
||||
import CurvyRect from "@/components/shared/layout/curvy-rect";
|
||||
|
||||
import CenterStar from "./_svg/CenterStar";
|
||||
|
||||
export default function HomeHeroBackground() {
|
||||
return (
|
||||
<div className="overlay contain-layout pointer-events-none lg-max:hidden">
|
||||
<div className="top-100 h-[calc(100%-99px)] border-border-faint border-y w-full left-0 absolute" />
|
||||
|
||||
<div className="cw-[1314px] z-[105] absolute top-0 border-x border-border-faint h-full">
|
||||
<div className="text-mono-x-small font-mono text-black-alpha-12 select-none">
|
||||
<div className="absolute top-111 -left-1 w-102 text-center">
|
||||
{" "}
|
||||
[ 200 OK ]{" "}
|
||||
</div>
|
||||
<div className="absolute bottom-10 -left-1 w-102 text-center">
|
||||
{" "}
|
||||
[ .JSON ]{" "}
|
||||
</div>
|
||||
|
||||
<div className="absolute top-111 -right-1 w-102 text-center">
|
||||
{" "}
|
||||
[ SCRAPE ]{" "}
|
||||
</div>
|
||||
<div className="absolute bottom-10 -right-1 w-102 text-center">
|
||||
{" "}
|
||||
[ .MD ]{" "}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="top-302 h-1 left-0 bg-border-faint w-303 absolute" />
|
||||
<div className="top-403 h-1 left-0 bg-border-faint w-303 absolute" />
|
||||
<div className="top-504 h-1 left-100 bg-border-faint w-203 absolute" />
|
||||
|
||||
<div className="top-302 h-1 right-0 bg-border-faint w-303 absolute" />
|
||||
<div className="top-403 h-1 right-0 bg-border-faint w-303 absolute" />
|
||||
<div className="top-504 h-1 right-100 bg-border-faint w-203 absolute" />
|
||||
|
||||
{Array.from({ length: 2 }, (_, i) => (
|
||||
<Fragment key={i}>
|
||||
<CurvyRect
|
||||
bottomLeft={i === 1}
|
||||
bottomRight={i === 0}
|
||||
className="w-101 h-[calc(100%-99px)] top-100 absolute"
|
||||
style={{ [i === 0 ? "left" : "right"]: -101 }}
|
||||
/>
|
||||
|
||||
<CurvyRect
|
||||
className="w-102 h-203 top-100 absolute"
|
||||
style={{ [i === 0 ? "left" : "right"]: -1 }}
|
||||
allSides
|
||||
/>
|
||||
<CurvyRect
|
||||
className="size-102 top-302 absolute"
|
||||
style={{ [i === 0 ? "left" : "right"]: -1 }}
|
||||
allSides
|
||||
/>
|
||||
<CurvyRect
|
||||
className="w-102 h-203 top-403 absolute"
|
||||
style={{ [i === 0 ? "left" : "right"]: -1 }}
|
||||
allSides
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="cw-[910px] absolute top-100 border-x border-border-faint h-[calc(100%-99px)]" />
|
||||
<div className="cw-[708px] absolute top-100 border-x border-border-faint h-[calc(100%-99px)]">
|
||||
<CenterStar className="absolute top-77 -right-24 z-[1]" />
|
||||
<CenterStar className="absolute top-77 -left-24 z-[1]" />
|
||||
</div>
|
||||
|
||||
<CurvyRect
|
||||
className="cw-[708px] absolute top-100 h-[calc(100%-99px)]"
|
||||
bottom
|
||||
/>
|
||||
|
||||
<div className="cw-[506px] absolute top-100 border-x border-border-faint h-102" />
|
||||
<div className="cw-[304px] absolute top-100 border-x border-border-faint h-102" />
|
||||
<div className="cw-[102px] absolute top-100 border-x border-border-faint h-102" />
|
||||
|
||||
<div className="top-201 h-1 bg-border-faint cw-[1112px] absolute" />
|
||||
|
||||
<div className="cw-[1112px] absolute top-0 h-full">
|
||||
<CurvyRect className="w-full absolute top-full h-100 left-0" top />
|
||||
<CurvyRect
|
||||
className="w-100 absolute top-full h-100 -left-99"
|
||||
topRight
|
||||
/>
|
||||
<CurvyRect
|
||||
className="w-100 absolute top-full h-100 -right-99"
|
||||
topLeft
|
||||
/>
|
||||
|
||||
{Array.from({ length: 5 }, (_, i) => (
|
||||
<Fragment key={i}>
|
||||
<CurvyRect
|
||||
className="size-102 absolute left-0"
|
||||
style={{
|
||||
top: 100 + i * 101,
|
||||
}}
|
||||
allSides
|
||||
/>
|
||||
|
||||
<CurvyRect
|
||||
className="size-102 absolute right-0"
|
||||
style={{
|
||||
top: 100 + i * 101,
|
||||
}}
|
||||
allSides
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
|
||||
<CurvyRect
|
||||
className="size-102 absolute left-101 top-100"
|
||||
bottomLeft
|
||||
top
|
||||
/>
|
||||
<CurvyRect
|
||||
className="size-102 absolute left-101 top-201"
|
||||
bottom
|
||||
topLeft
|
||||
/>
|
||||
|
||||
<CurvyRect
|
||||
className="size-102 absolute right-101 top-100"
|
||||
bottomRight
|
||||
top
|
||||
/>
|
||||
<CurvyRect
|
||||
className="size-102 absolute right-101 top-201"
|
||||
bottom
|
||||
topRight
|
||||
/>
|
||||
|
||||
{Array.from({ length: 3 }, (_, i) => (
|
||||
<Fragment key={i}>
|
||||
<CurvyRect
|
||||
className="size-102 absolute left-101"
|
||||
style={{
|
||||
top: 302 + i * 101,
|
||||
}}
|
||||
allSides
|
||||
/>
|
||||
|
||||
<CurvyRect
|
||||
className="size-102 absolute right-101"
|
||||
style={{
|
||||
top: 302 + i * 101,
|
||||
}}
|
||||
allSides
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
|
||||
<CurvyRect
|
||||
className="size-102 absolute top-100 left-202"
|
||||
bottomRight
|
||||
top
|
||||
/>
|
||||
|
||||
{Array.from({ length: 5 }, (_, i) => (
|
||||
<CurvyRect
|
||||
className="size-102 absolute top-100"
|
||||
key={i}
|
||||
style={{ left: 303 + i * 101 }}
|
||||
allSides
|
||||
/>
|
||||
))}
|
||||
|
||||
<CurvyRect
|
||||
className="size-102 absolute top-100 right-202"
|
||||
bottomLeft
|
||||
top
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { Connector } from "@/components/shared/layout/curvy-rect";
|
||||
import {
|
||||
useHeaderContext,
|
||||
useHeaderHeight,
|
||||
} from "@/components/shared/header/HeaderContext";
|
||||
import { cn } from "@/utils/cn";
|
||||
|
||||
export const BackgroundOuterPiece = () => {
|
||||
const [noRender, setNoRender] = useState(false);
|
||||
const { dropdownContent } = useHeaderContext();
|
||||
const { headerHeight } = useHeaderHeight();
|
||||
|
||||
useEffect(() => {
|
||||
const heroContent = document.getElementById("hero-content");
|
||||
if (!heroContent) {
|
||||
// If hero-content doesn't exist, don't render the background piece
|
||||
setNoRender(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const heroContentHeight = heroContent.clientHeight;
|
||||
|
||||
const onScroll = () => {
|
||||
setNoRender(window.scrollY > heroContentHeight - 120);
|
||||
};
|
||||
|
||||
onScroll();
|
||||
|
||||
window.addEventListener("scroll", onScroll);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("scroll", onScroll);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"cw-[1335px] transition-all z-[105] absolute top-0 flex justify-between h-[calc(100%+21px)] duration-[200ms] pointer-events-none",
|
||||
{ "opacity-0": noRender || dropdownContent || !headerHeight },
|
||||
)}
|
||||
style={{
|
||||
paddingTop: headerHeight - 10,
|
||||
}}
|
||||
>
|
||||
<div className="h-[3000px] w-[calc(100%-21px)] left-[10.5px] absolute bottom-21 border-x border-border-faint" />
|
||||
|
||||
<Connector className="sticky" style={{ top: headerHeight - 10 }} />
|
||||
<Connector className="sticky" style={{ top: headerHeight - 10 }} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
export default function CenterStar({
|
||||
...props
|
||||
}: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
fill="none"
|
||||
height="47"
|
||||
viewBox="0 0 47 47"
|
||||
width="47"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M24 18C24 21.3137 26.6863 24 30 24H34V25H30C26.6863 25 24 27.6863 24 31V35H23V31C23 27.6863 20.3137 25 17 25H13V24H17C20.3137 24 23 21.3137 23 18V14H24V18Z"
|
||||
fill="var(--heat-100)"
|
||||
fillOpacity="1"
|
||||
/>
|
||||
<circle cx="23.5" cy="23.5" r="23" stroke="#EDEDED" strokeOpacity="1" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import Link from "next/link";
|
||||
|
||||
export default function HomeHeroBadge() {
|
||||
return (
|
||||
<Link
|
||||
className="p-4 rounded-full flex w-max mx-auto mb-12 lg:mb-16 items-center relative before:inside-border before:border-border-faint group"
|
||||
href="#"
|
||||
onClick={(e) => e.preventDefault()}
|
||||
>
|
||||
<div className="px-8 text-label-x-small">OpenAI Inspired Agent Builder</div>
|
||||
|
||||
<div className="p-1">
|
||||
<div className="size-18 bg-accent-black flex-center rounded-full group-hover:bg-heat-100 transition-all group-hover:w-30">
|
||||
<svg
|
||||
fill="none"
|
||||
height="8"
|
||||
viewBox="0 0 10 8"
|
||||
width="10"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
className="transition-all -translate-x-2 group-hover:translate-x-0"
|
||||
d="M6 1L9 4L6 7"
|
||||
stroke="white"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.25"
|
||||
/>
|
||||
|
||||
<path
|
||||
className="transition-all -translate-x-3 group-hover:translate-x-0 scale-x-[0] group-hover:scale-x-[1] origin-right"
|
||||
d="M1 4L9 4"
|
||||
stroke="white"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.25"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import Link from "next/link";
|
||||
|
||||
import { Connector } from "@/components/shared/layout/curvy-rect";
|
||||
import HeroFlame from "@/components/shared/effects/flame/hero-flame";
|
||||
|
||||
import HomeHeroBackground from "./Background/Background";
|
||||
import { BackgroundOuterPiece } from "./Background/BackgroundOuterPiece";
|
||||
import HomeHeroBadge from "./Badge/Badge";
|
||||
import HomeHeroPixi from "./Pixi/Pixi";
|
||||
import HomeHeroTitle from "./Title/Title";
|
||||
import HeroInput from "../hero-input/HeroInput";
|
||||
import HeroScraping from "../hero-scraping/HeroScraping";
|
||||
|
||||
export default function HomeHero() {
|
||||
return (
|
||||
<section className="overflow-x-clip" id="home-hero">
|
||||
<div
|
||||
className="pt-28 lg:pt-254 lg:-mt-100 pb-115 relative"
|
||||
id="hero-content"
|
||||
>
|
||||
<HomeHeroPixi />
|
||||
<HeroFlame />
|
||||
|
||||
<BackgroundOuterPiece />
|
||||
|
||||
<HomeHeroBackground />
|
||||
|
||||
<div className="relative container px-16">
|
||||
<HomeHeroBadge />
|
||||
<HomeHeroTitle />
|
||||
|
||||
<p className="text-center text-body-large">
|
||||
Power your AI apps with clean data crawled
|
||||
<br className="lg-max:hidden" />
|
||||
from any website.
|
||||
<Link
|
||||
className="bg-black-alpha-4 hover:bg-black-alpha-6 lg:ml-4 rounded-6 px-8 lg:px-6 text-label-large lg-max:py-2 h-30 lg:h-24 block lg-max:mt-8 lg-max:mx-auto lg-max:w-max lg:inline-block gap-4 transition-all"
|
||||
href="https://github.com/firecrawl/firecrawl"
|
||||
target="_blank"
|
||||
>
|
||||
It's also open source.
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="container lg:contents !p-16 relative -mt-90">
|
||||
<div className="absolute top-0 left-[calc(50%-50vw)] w-screen h-1 bg-border-faint lg:hidden" />
|
||||
<div className="absolute bottom-0 left-[calc(50%-50vw)] w-screen h-1 bg-border-faint lg:hidden" />
|
||||
|
||||
<Connector className="-top-10 -left-[10.5px] lg:hidden" />
|
||||
<Connector className="-top-10 -right-[10.5px] lg:hidden" />
|
||||
<Connector className="-bottom-10 -left-[10.5px] lg:hidden" />
|
||||
<Connector className="-bottom-10 -right-[10.5px] lg:hidden" />
|
||||
|
||||
<HeroInput />
|
||||
</div>
|
||||
|
||||
<HeroScraping />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
"use client";
|
||||
|
||||
import { Suspense, lazy, useState, useEffect } from "react";
|
||||
|
||||
const Pixi = lazy(() => import("@/components/shared/pixi/Pixi"));
|
||||
import features from "./tickers/features";
|
||||
|
||||
function PixiContent() {
|
||||
return (
|
||||
<Pixi
|
||||
canvasAttrs={{
|
||||
className: "cw-[1314px] h-506 absolute top-100 lg-max:hidden",
|
||||
}}
|
||||
fps={Infinity}
|
||||
initOptions={{ backgroundAlpha: 0 }}
|
||||
smartStop={false}
|
||||
tickers={[features]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default function HomeHeroPixi() {
|
||||
const [hasError, setHasError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleError = (e: ErrorEvent) => {
|
||||
if (e.message.includes('pixi') || e.message.includes('ChunkLoadError')) {
|
||||
setHasError(true);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('error', handleError);
|
||||
return () => window.removeEventListener('error', handleError);
|
||||
}, []);
|
||||
|
||||
if (hasError) {
|
||||
// Return empty div as fallback if Pixi fails to load
|
||||
return <div className="cw-[1314px] h-506 absolute top-100 lg-max:hidden" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={<div className="cw-[1314px] h-506 absolute top-100 lg-max:hidden" />}>
|
||||
<PixiContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import { Ticker } from "@/components/shared/pixi/Pixi";
|
||||
import PixiAssetManager from "@/components/shared/pixi/PixiAssetManager";
|
||||
import { RenderTexture, Sprite, Text } from "pixi.js";
|
||||
|
||||
// Add more contrast to the ASCII_CHARS and ensure 'X' is used
|
||||
// const ASCII_CHARS = [' ', '.', ':', '-', '=', '+', 'X'];
|
||||
const ASCII_CHARS = ' .":,-_^=+';
|
||||
|
||||
function getAsciiChar(luminance: number) {
|
||||
if (luminance < 50) return " ";
|
||||
|
||||
const norm = Math.max(0, Math.min(1, (luminance - 16) / (250 - 16)));
|
||||
const skewed = Math.pow(norm, 1.5);
|
||||
|
||||
const minIdx = 1;
|
||||
const maxIdx = ASCII_CHARS.length - 1;
|
||||
const idx = minIdx + Math.floor(skewed * (maxIdx - minIdx + 1));
|
||||
const safeIdx = Math.max(minIdx, Math.min(maxIdx, idx));
|
||||
|
||||
return ASCII_CHARS[safeIdx];
|
||||
}
|
||||
|
||||
// Sprinkle logic is now a no-op, as getAsciiChar handles the randomness
|
||||
function sprinkleAscii(line: string) {
|
||||
return line;
|
||||
}
|
||||
|
||||
const tickAscii: Ticker = async ({ app, canvas }) => {
|
||||
const textures = await Promise.all(
|
||||
Array.from({ length: 150 }, async (_, i) => {
|
||||
const texture = await PixiAssetManager.load(
|
||||
`/Arşiv/FAQ Demo/FAQ_${i.toString().padStart(5, "0")}.png`,
|
||||
);
|
||||
|
||||
return texture!;
|
||||
}),
|
||||
);
|
||||
|
||||
const width = canvas.clientWidth;
|
||||
const height = canvas.clientHeight;
|
||||
|
||||
const sprites = textures.map((texture) => new Sprite(texture));
|
||||
|
||||
sprites.forEach((sprite) => {
|
||||
sprite.width = width;
|
||||
sprite.height = height;
|
||||
sprite.x = 0;
|
||||
sprite.y = 0;
|
||||
|
||||
app.stage.addChild(sprite);
|
||||
sprite.alpha = 0;
|
||||
});
|
||||
|
||||
// Render the texture to a renderTexture to extract pixels
|
||||
const renderTexture = RenderTexture.create({ width, height });
|
||||
|
||||
let ascii = "";
|
||||
|
||||
const asciiText = new Text({
|
||||
text: ascii,
|
||||
style: {
|
||||
fontFamily: "monospace",
|
||||
fontSize: 8,
|
||||
fill: 0x000,
|
||||
align: "left",
|
||||
lineHeight: 8,
|
||||
whiteSpace: "pre",
|
||||
},
|
||||
});
|
||||
asciiText.alpha = 0.2;
|
||||
asciiText.x = 0;
|
||||
asciiText.y = 0;
|
||||
|
||||
const variants: string[] = [];
|
||||
|
||||
const render = (index: number) => {
|
||||
ascii = "";
|
||||
const sprite = sprites[index];
|
||||
|
||||
sprites.forEach((sprite) => {
|
||||
sprite.alpha = 0;
|
||||
});
|
||||
|
||||
sprite.alpha = 1;
|
||||
app.renderer.render({ container: sprite, target: renderTexture });
|
||||
sprite.alpha = 0;
|
||||
|
||||
const pixels = app.renderer.extract.pixels(renderTexture).pixels;
|
||||
|
||||
const charWidth = 4.81640625;
|
||||
|
||||
for (let y = 0; y < height; y += 8) {
|
||||
let line = "";
|
||||
|
||||
for (let x = 0; x < width; x += charWidth) {
|
||||
let totalLum = 0;
|
||||
let count = 0;
|
||||
|
||||
for (let dy = 0; dy < 8; dy++) {
|
||||
for (let dx = 0; dx < 4; dx++) {
|
||||
const px = Math.floor(x + dx);
|
||||
const py = Math.floor(y + dy);
|
||||
if (px >= width || py >= height) continue;
|
||||
const idx = (py * width + px) * 4;
|
||||
const r = pixels[idx];
|
||||
const g = pixels[idx + 1];
|
||||
const b = pixels[idx + 2];
|
||||
const lum = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||
totalLum += lum;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
const avgLum = count ? totalLum / count : 0;
|
||||
line += getAsciiChar(avgLum);
|
||||
}
|
||||
ascii += sprinkleAscii(line) + "\n";
|
||||
}
|
||||
|
||||
variants[index] = ascii;
|
||||
|
||||
asciiText.text = ascii;
|
||||
};
|
||||
|
||||
app.stage.addChild(asciiText);
|
||||
|
||||
for (let i = 0; i < sprites.length; i++) {
|
||||
render(i);
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
|
||||
//@ts-ignore
|
||||
app.ticker.safeAdd(() => {
|
||||
i++;
|
||||
if (i >= sprites.length) i = 0;
|
||||
|
||||
render(i);
|
||||
});
|
||||
};
|
||||
|
||||
export default tickAscii;
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Ticker } from "@/components/shared/pixi/Pixi";
|
||||
|
||||
import AnimatedRect from "./components/AnimatedRect";
|
||||
import BlinkingContainer from "./components/BlinkingContainer";
|
||||
import crawl from "./crawl";
|
||||
import mapping from "./mapping";
|
||||
import scrape from "./scrape";
|
||||
import search from "./search";
|
||||
|
||||
type Props = Parameters<Ticker>[0] & {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
export const CELL_SIZE = 80;
|
||||
|
||||
export const MAIN_COLOR = 0xe6e6e6;
|
||||
|
||||
const animations = [scrape, mapping, search, crawl];
|
||||
|
||||
let lastActive = -1;
|
||||
|
||||
export default function cell(props: Props) {
|
||||
const blinkingContainer = BlinkingContainer({
|
||||
x: props.x + 10,
|
||||
y: props.y + 10,
|
||||
app: props.app,
|
||||
});
|
||||
|
||||
const anchorGraphic = AnimatedRect({
|
||||
app: props.app,
|
||||
x: CELL_SIZE / 2,
|
||||
y: CELL_SIZE / 2,
|
||||
width: 4,
|
||||
height: 4,
|
||||
radius: 10,
|
||||
color: MAIN_COLOR,
|
||||
});
|
||||
|
||||
blinkingContainer.container.addChild(anchorGraphic.graphic);
|
||||
|
||||
props.app.stage.addChild(blinkingContainer.container);
|
||||
|
||||
let running = false;
|
||||
|
||||
return {
|
||||
trigger: async () => {
|
||||
if (running) return;
|
||||
|
||||
running = true;
|
||||
|
||||
lastActive = (lastActive + 1) % animations.length;
|
||||
|
||||
const fn = animations[lastActive];
|
||||
|
||||
await fn({
|
||||
...props,
|
||||
blinkingContainer,
|
||||
anchorGraphic,
|
||||
});
|
||||
|
||||
running = false;
|
||||
},
|
||||
};
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import { Ticker } from "@/components/shared/pixi/Pixi";
|
||||
import AnimatedRect from "./components/AnimatedRect";
|
||||
|
||||
type Props = Parameters<Ticker>[0] & {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
export default function cellReveal(props: Props) {
|
||||
const graphic = AnimatedRect({
|
||||
app: props.app,
|
||||
x: props.x + 0.5,
|
||||
y: props.y + 0.5,
|
||||
width: 101,
|
||||
height: 101,
|
||||
radius: 0,
|
||||
alpha: 0,
|
||||
color: 0x000,
|
||||
centering: false,
|
||||
});
|
||||
|
||||
props.app.stage.addChild(graphic.graphic);
|
||||
|
||||
return {
|
||||
trigger: async () => {
|
||||
let cycleCount = 0;
|
||||
|
||||
const cycle = async () => {
|
||||
await graphic.animate(
|
||||
{
|
||||
alpha: Math.random() * 0.04,
|
||||
},
|
||||
{
|
||||
ease: "linear",
|
||||
duration: 0.03,
|
||||
},
|
||||
);
|
||||
|
||||
if (cycleCount < 5) {
|
||||
cycleCount += 1;
|
||||
cycle();
|
||||
} else {
|
||||
await graphic.animate({ alpha: 0 });
|
||||
graphic.graphic.destroy();
|
||||
}
|
||||
};
|
||||
|
||||
cycle();
|
||||
},
|
||||
};
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
//@ts-nocheck
|
||||
|
||||
import { AnimationOptions, cubicBezier } from "motion";
|
||||
import { Application, Container, Graphics, Sprite } from "pixi.js";
|
||||
|
||||
import { isDestroyed } from "@/components/shared/pixi/utils";
|
||||
|
||||
type Props = {
|
||||
app: Application;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
radius: number;
|
||||
color: number;
|
||||
scale?: number;
|
||||
rotation?: number;
|
||||
type?: "rect" | "arc" | "container" | Sprite;
|
||||
animationConfig?: AnimationOptions;
|
||||
alpha?: number;
|
||||
|
||||
centering?: boolean;
|
||||
};
|
||||
|
||||
export type IAnimatedRect = ReturnType<typeof AnimatedRect>;
|
||||
|
||||
export default function AnimatedRect(props: Props) {
|
||||
const graphic = (() => {
|
||||
if (props.type === "container") return new Container();
|
||||
if (props.type instanceof Sprite) return props.type;
|
||||
|
||||
return new Graphics();
|
||||
})();
|
||||
|
||||
props.alpha ??= 1;
|
||||
props.scale ??= 1;
|
||||
props.centering ??= true;
|
||||
props.rotation ??= 0;
|
||||
|
||||
const p = {
|
||||
...props,
|
||||
};
|
||||
|
||||
const render = () => {
|
||||
if (isDestroyed(props.app) || graphic.destroyed) return;
|
||||
|
||||
graphic.scale.set(p.scale!);
|
||||
graphic.alpha = p.alpha!;
|
||||
graphic.rotation = p.rotation!;
|
||||
|
||||
if (!(graphic instanceof Graphics)) {
|
||||
if (graphic instanceof Sprite) {
|
||||
graphic.x = p.x;
|
||||
graphic.y = p.y;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const g = graphic as Graphics;
|
||||
|
||||
g.clear();
|
||||
|
||||
if (p.type !== "arc") {
|
||||
g.roundRect(
|
||||
p.centering ? p.x - p.width / 2 : p.x,
|
||||
p.centering ? p.y - p.height / 2 : p.y,
|
||||
p.width,
|
||||
p.height,
|
||||
p.radius,
|
||||
);
|
||||
} else {
|
||||
g.arc(p.x, p.y, p.width / 2, 0, Math.PI * 2);
|
||||
}
|
||||
|
||||
g.fill({ color: p.color });
|
||||
};
|
||||
|
||||
render();
|
||||
|
||||
p.animationConfig ??= {
|
||||
duration: 0.4,
|
||||
ease: cubicBezier(0.83, 0, 0.17, 1),
|
||||
};
|
||||
|
||||
return {
|
||||
defaultProps: props,
|
||||
currentProps: p,
|
||||
graphic,
|
||||
setStyle: (style: Partial<Props>) => {
|
||||
Object.assign(p, style);
|
||||
|
||||
render();
|
||||
},
|
||||
render,
|
||||
animate: (renderProps: Partial<Props>, settings?: AnimationOptions) =>
|
||||
props.app.animate(p, renderProps, {
|
||||
...p.animationConfig,
|
||||
...settings,
|
||||
onUpdate: render,
|
||||
}),
|
||||
reset: () => props.app.animate(p, props, { onUpdate: render }),
|
||||
};
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
//@ts-nocheck
|
||||
|
||||
import { Application, Graphics } from "pixi.js";
|
||||
|
||||
import { CELL_SIZE } from "@/components/app/(home)/sections/hero/Pixi/tickers/features/cell";
|
||||
|
||||
import AnimatedRect from "./AnimatedRect";
|
||||
|
||||
export type IBlinkingContainer = ReturnType<typeof BlinkingContainer>;
|
||||
|
||||
export default function BlinkingContainer({
|
||||
x,
|
||||
y,
|
||||
app,
|
||||
}: {
|
||||
x: number;
|
||||
y: number;
|
||||
app: Application;
|
||||
}) {
|
||||
const animatedRect = AnimatedRect({
|
||||
app,
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: CELL_SIZE,
|
||||
height: CELL_SIZE,
|
||||
radius: 0,
|
||||
color: 0xededed,
|
||||
type: "container",
|
||||
});
|
||||
|
||||
animatedRect.graphic.pivot.set(CELL_SIZE / 2, CELL_SIZE / 2);
|
||||
|
||||
animatedRect.graphic.x = x + CELL_SIZE / 2;
|
||||
animatedRect.graphic.y = y + CELL_SIZE / 2;
|
||||
|
||||
animatedRect.graphic.addChild(
|
||||
new Graphics()
|
||||
.rect(0, 0, CELL_SIZE, CELL_SIZE)
|
||||
.fill({ color: "#EDEDED", alpha: 0 }),
|
||||
);
|
||||
|
||||
const blinkLayer = new Graphics()
|
||||
.rect(0, 0, CELL_SIZE, CELL_SIZE)
|
||||
.fill({ color: "#F9F9F9" });
|
||||
|
||||
blinkLayer.zIndex = 1;
|
||||
blinkLayer.alpha = 0;
|
||||
|
||||
animatedRect.graphic.addChild(blinkLayer);
|
||||
|
||||
return {
|
||||
container: animatedRect.graphic,
|
||||
animate: animatedRect.animate,
|
||||
reset: animatedRect.reset,
|
||||
shrink: async () => {
|
||||
await animatedRect.animate({ scale: 0.92 });
|
||||
|
||||
animatedRect.animate({ scale: 1 });
|
||||
},
|
||||
blink: ({ delay = 0 }: { delay?: number } = {}) => {
|
||||
app
|
||||
.animate(0, 0.32, {
|
||||
repeatType: "reverse",
|
||||
repeat: 2,
|
||||
delay,
|
||||
duration: 0.065,
|
||||
ease: "linear",
|
||||
onUpdate: (value) => {
|
||||
blinkLayer.alpha = value as number;
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
app.animate(0.32, 0, {
|
||||
duration: 0.065,
|
||||
ease: "linear",
|
||||
onUpdate: (value) => {
|
||||
blinkLayer.alpha = value as number;
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { MAIN_COLOR } from "@/components/app/(home)/sections/hero/Pixi/tickers/features/cell";
|
||||
|
||||
import AnimatedRect from "./AnimatedRect";
|
||||
|
||||
export default function Dot(
|
||||
props: Pick<
|
||||
Parameters<typeof AnimatedRect>[0],
|
||||
"x" | "y" | "app" | "animationConfig"
|
||||
>,
|
||||
) {
|
||||
return AnimatedRect({
|
||||
...props,
|
||||
width: 2,
|
||||
height: 2,
|
||||
radius: 10,
|
||||
color: MAIN_COLOR,
|
||||
type: "arc",
|
||||
});
|
||||
}
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
import { animate } from "motion";
|
||||
|
||||
import { Ticker } from "@/components/shared/pixi/Pixi";
|
||||
import { sleep } from "@/utils/sleep";
|
||||
|
||||
import { CELL_SIZE, MAIN_COLOR } from "./cell";
|
||||
import AnimatedRect, { IAnimatedRect } from "./components/AnimatedRect";
|
||||
import { IBlinkingContainer } from "./components/BlinkingContainer";
|
||||
import Dot from "./components/Dot";
|
||||
|
||||
type Props = Parameters<Ticker>[0] & {
|
||||
x: number;
|
||||
y: number;
|
||||
blinkingContainer: IBlinkingContainer;
|
||||
anchorGraphic: IAnimatedRect;
|
||||
};
|
||||
|
||||
export default async function crawl(props: Props) {
|
||||
const rects = Array.from({ length: 6 }, () => {
|
||||
return AnimatedRect({
|
||||
app: props.app,
|
||||
x: CELL_SIZE / 2,
|
||||
y: CELL_SIZE / 2,
|
||||
width: 8,
|
||||
height: 8,
|
||||
radius: 0,
|
||||
color: MAIN_COLOR,
|
||||
});
|
||||
});
|
||||
|
||||
const dots = Array.from({ length: 16 }, () => {
|
||||
return Dot({
|
||||
x: CELL_SIZE / 2,
|
||||
y: CELL_SIZE / 2,
|
||||
app: props.app,
|
||||
});
|
||||
});
|
||||
|
||||
dots.forEach((dot) =>
|
||||
props.blinkingContainer.container.addChild(dot.graphic),
|
||||
);
|
||||
|
||||
await sleep(500);
|
||||
|
||||
/* Step 1: Reveal the main square, reveal the corner dots */
|
||||
await Promise.all(
|
||||
[
|
||||
dots[0].animate({ x: 30, y: 30 }, { delay: 0.2 }),
|
||||
dots[1].animate({ x: CELL_SIZE - 30, y: 30 }, { delay: 0.2 }),
|
||||
dots[2].animate({ x: 30, y: CELL_SIZE - 30 }, { delay: 0.2 }),
|
||||
dots[3].animate({ x: CELL_SIZE - 30, y: CELL_SIZE - 30 }, { delay: 0.2 }),
|
||||
|
||||
props.anchorGraphic.animate({
|
||||
radius: 0,
|
||||
width: 12,
|
||||
height: 12,
|
||||
}),
|
||||
].flat(),
|
||||
);
|
||||
|
||||
rects.forEach((rect) =>
|
||||
props.blinkingContainer.container.addChild(rect.graphic),
|
||||
);
|
||||
|
||||
rects.unshift(props.anchorGraphic);
|
||||
|
||||
await sleep(500);
|
||||
props.blinkingContainer.blink({ delay: 0.3 });
|
||||
await props.blinkingContainer.shrink();
|
||||
|
||||
let spriteOverlay: IAnimatedRect | null = null;
|
||||
|
||||
// Use fallback rectangle instead of trying to load missing image
|
||||
spriteOverlay = AnimatedRect({
|
||||
x: 13,
|
||||
y: 39,
|
||||
color: MAIN_COLOR,
|
||||
width: 54,
|
||||
height: 34,
|
||||
app: props.app,
|
||||
radius: 4,
|
||||
centering: false,
|
||||
});
|
||||
|
||||
spriteOverlay.graphic.zIndex = -1;
|
||||
props.blinkingContainer.container.addChild(spriteOverlay.graphic);
|
||||
|
||||
await Promise.all(
|
||||
[
|
||||
spriteOverlay?.animate({ height: 23, y: 50 }),
|
||||
|
||||
rects[0].animate({ width: 16, height: 16, y: 34 }),
|
||||
rects.slice(1, 4).map((rect) => rect.animate({ x: 24, y: 50 })),
|
||||
rects.slice(4, 8).map((rect) => rect.animate({ x: 56, y: 50 })),
|
||||
|
||||
dots[0].animate({ x: 28, y: 22 }),
|
||||
dots[1].animate({ x: 52, y: 22 }),
|
||||
dots[2].animate({ x: 16, y: 58 }),
|
||||
dots[3].animate({ x: 64, y: 58 }),
|
||||
|
||||
dots[4].animate({ x: 16, y: 42 }),
|
||||
dots[5].animate({ x: 64, y: 42 }),
|
||||
dots[6].animate({ x: 32, y: 58 }),
|
||||
dots[7].animate({ x: 48, y: 58 }),
|
||||
|
||||
dots.slice(8, 12).map((dot) => dot.animate({ x: 24, y: 50 })),
|
||||
dots.slice(12, 16).map((dot) => dot.animate({ x: 56, y: 50 })),
|
||||
].flat().filter(Boolean),
|
||||
);
|
||||
|
||||
await sleep(500);
|
||||
props.blinkingContainer.blink({ delay: 0.3 });
|
||||
await props.blinkingContainer.shrink();
|
||||
try {
|
||||
await Promise.all(
|
||||
[
|
||||
spriteOverlay?.animate({ height: 8, y: 58 }),
|
||||
|
||||
rects[0].animate({ y: 28 }),
|
||||
[1, 4].map((i) => rects[i].animate({ y: 44 })),
|
||||
[2, 3].map((i) => rects[i].animate({ x: 12, y: 56 })),
|
||||
[5, 6].map((i) => rects[i].animate({ x: 68, y: 56 })),
|
||||
|
||||
dots[0].animate({ y: 16 }),
|
||||
dots[1].animate({ y: 16 }),
|
||||
|
||||
dots[2].animate({ x: 4, y: 64 }),
|
||||
dots[3].animate({ x: 76, y: 64 }),
|
||||
|
||||
dots[4].animate({ x: 4, y: 48 }),
|
||||
dots[5].animate({ x: 76, y: 48 }),
|
||||
dots[6].animate({ x: 20, y: 64 }),
|
||||
dots[7].animate({ x: 60, y: 64 }),
|
||||
|
||||
dots[8].animate({ x: 16, y: 36 }),
|
||||
dots[12].animate({ x: 64, y: 36 }),
|
||||
|
||||
dots[9].animate({ x: 32, y: 52 }),
|
||||
dots[13].animate({ x: 48, y: 52 }),
|
||||
|
||||
[10, 11].map((i) => dots[i].animate({ x: 12, y: 56 })),
|
||||
[14, 15].map((i) => dots[i].animate({ x: 68, y: 56 })),
|
||||
].flat().filter(Boolean),
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
await sleep(500);
|
||||
props.blinkingContainer.blink({ delay: 0.3 });
|
||||
await props.blinkingContainer.shrink();
|
||||
|
||||
await Promise.all(
|
||||
[
|
||||
spriteOverlay.animate({ height: 0, y: 66 }),
|
||||
|
||||
rects[0].animate({ y: 20 }),
|
||||
[1, 4].map((i) => rects[i].animate({ y: 36 })),
|
||||
[2, 5].map((i) => rects[i].animate({ y: 48 })),
|
||||
[3, 6].map((i) => rects[i].animate({ y: 60, x: i === 3 ? 24 : 56 })),
|
||||
|
||||
[0, 1, 4, 5, 8, 9, 12, 13].map((i) =>
|
||||
dots[i].animate({ y: dots[i].currentProps.y - 8 }),
|
||||
),
|
||||
|
||||
dots[2].animate({ x: 4, y: 56 }),
|
||||
dots[3].animate({ x: 76, y: 56 }),
|
||||
|
||||
dots[6].animate({ x: 32, y: 68 }),
|
||||
dots[7].animate({ x: 48, y: 68 }),
|
||||
|
||||
dots[10].animate({ x: 32, y: 52 }),
|
||||
dots[11].animate({ x: 16, y: 68 }),
|
||||
dots[14].animate({ x: 48, y: 52 }),
|
||||
dots[15].animate({ x: 64, y: 68 }),
|
||||
].flat(),
|
||||
);
|
||||
|
||||
await sleep(2000);
|
||||
|
||||
await Promise.all(
|
||||
[
|
||||
rects.map((rect) =>
|
||||
rect.animate(props.anchorGraphic.defaultProps, {
|
||||
delay: Math.random() * 0.3,
|
||||
duration: 0.3,
|
||||
}),
|
||||
),
|
||||
dots.map((dot) =>
|
||||
dot.animate(dot.defaultProps, { delay: Math.random() * 0.3 }),
|
||||
),
|
||||
].flat(),
|
||||
);
|
||||
|
||||
rects.shift();
|
||||
|
||||
rects.forEach((rect) => rect.graphic.destroy());
|
||||
dots.forEach((dot) => dot.graphic.destroy());
|
||||
spriteOverlay.graphic.destroy();
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
import { Ticker } from "@/components/shared/pixi/Pixi";
|
||||
import setTimeoutOnVisible from "@/utils/set-timeout-on-visible";
|
||||
|
||||
import cell from "./cell";
|
||||
import cellReveal from "./cellReveal";
|
||||
|
||||
const CELL_GRID = [
|
||||
"-ooooooooooo-",
|
||||
"-oo-------oo-",
|
||||
"ooo-------ooo",
|
||||
"-oo-------oo-",
|
||||
"-oo-------oo-",
|
||||
];
|
||||
|
||||
const REVEAL_ANIMATION_GRID = [
|
||||
[
|
||||
"---ooooooo---",
|
||||
"--o-------o--",
|
||||
"--o-------o--",
|
||||
"--o-------o--",
|
||||
"--o-------o--",
|
||||
],
|
||||
[
|
||||
"--o-------o--",
|
||||
"-o---------o-",
|
||||
"-o---------o-",
|
||||
"-o---------o-",
|
||||
"-o---------o-",
|
||||
],
|
||||
[
|
||||
"-o---------o-",
|
||||
"-------------",
|
||||
"o-----------o",
|
||||
"-------------",
|
||||
"-------------",
|
||||
],
|
||||
[
|
||||
"-------------",
|
||||
"-------------",
|
||||
"o-----------o",
|
||||
"-------------",
|
||||
"-------------",
|
||||
],
|
||||
];
|
||||
|
||||
const features: Ticker = (params) => {
|
||||
const cells: ReturnType<typeof cell>[] = [];
|
||||
const cellReveals: {
|
||||
cell: ReturnType<typeof cellReveal>;
|
||||
row: number;
|
||||
column: number;
|
||||
}[] = [];
|
||||
|
||||
for (let i = 0; i < CELL_GRID.length; i++) {
|
||||
const row = CELL_GRID[i];
|
||||
|
||||
for (let j = 0; j < row.length; j++) {
|
||||
if (row[j] === "o") {
|
||||
cells.push(
|
||||
cell({
|
||||
...params,
|
||||
x: j * 101,
|
||||
y: i * 101,
|
||||
}),
|
||||
);
|
||||
|
||||
cellReveals.push({
|
||||
cell: cellReveal({
|
||||
...params,
|
||||
x: j * 101,
|
||||
y: i * 101,
|
||||
}),
|
||||
row: i,
|
||||
column: j,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const cycle = () =>
|
||||
setTimeoutOnVisible({
|
||||
element: params.canvas,
|
||||
callback: () => {
|
||||
const cell = cells[Math.floor(Math.random() * cells.length)];
|
||||
|
||||
if (cell) {
|
||||
cell.trigger().then(() => cycle());
|
||||
}
|
||||
},
|
||||
timeout: 3000 * Math.random(),
|
||||
});
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
cycle();
|
||||
}
|
||||
|
||||
let revealIndex = -1;
|
||||
|
||||
const revealCycle = () => {
|
||||
revealIndex += 1;
|
||||
|
||||
for (let i = 0; i < REVEAL_ANIMATION_GRID[revealIndex].length; i++) {
|
||||
const row = REVEAL_ANIMATION_GRID[revealIndex][i];
|
||||
|
||||
for (let j = 0; j < row.length; j++) {
|
||||
if (row[j] === "o") {
|
||||
cellReveals
|
||||
.find((cell) => cell.row === i && cell.column === j)
|
||||
?.cell.trigger();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (revealIndex < REVEAL_ANIMATION_GRID.length - 1) {
|
||||
setTimeout(() => {
|
||||
revealCycle();
|
||||
}, 150);
|
||||
}
|
||||
};
|
||||
|
||||
revealCycle();
|
||||
};
|
||||
|
||||
export default features;
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
import { Ticker } from "@/components/shared/pixi/Pixi";
|
||||
import { sleep } from "@/utils/sleep";
|
||||
|
||||
import { CELL_SIZE, MAIN_COLOR } from "./cell";
|
||||
import AnimatedRect, { IAnimatedRect } from "./components/AnimatedRect";
|
||||
import { IBlinkingContainer } from "./components/BlinkingContainer";
|
||||
import Dot from "./components/Dot";
|
||||
|
||||
type Props = Parameters<Ticker>[0] & {
|
||||
x: number;
|
||||
y: number;
|
||||
blinkingContainer: IBlinkingContainer;
|
||||
anchorGraphic: IAnimatedRect;
|
||||
};
|
||||
|
||||
export default async function mapping(props: Props) {
|
||||
const rects = Array.from({ length: 8 }, () => {
|
||||
return AnimatedRect({
|
||||
app: props.app,
|
||||
x: CELL_SIZE / 2,
|
||||
y: CELL_SIZE / 2,
|
||||
width: 10,
|
||||
height: 10,
|
||||
radius: 0,
|
||||
color: MAIN_COLOR,
|
||||
});
|
||||
});
|
||||
|
||||
const dots = Array.from({ length: 20 }, () => {
|
||||
return Dot({
|
||||
x: CELL_SIZE / 2,
|
||||
y: CELL_SIZE / 2,
|
||||
app: props.app,
|
||||
});
|
||||
});
|
||||
|
||||
dots.forEach((dot) =>
|
||||
props.blinkingContainer.container.addChild(dot.graphic),
|
||||
);
|
||||
|
||||
await sleep(500);
|
||||
|
||||
await props.anchorGraphic.animate({
|
||||
radius: 0,
|
||||
width: 12,
|
||||
height: 12,
|
||||
});
|
||||
|
||||
rects.forEach((rect) =>
|
||||
props.blinkingContainer.container.addChild(rect.graphic),
|
||||
);
|
||||
|
||||
rects.unshift(props.anchorGraphic);
|
||||
|
||||
await sleep(500);
|
||||
|
||||
props.blinkingContainer.blink({ delay: 0.1 });
|
||||
await props.blinkingContainer.shrink();
|
||||
|
||||
await Promise.all(
|
||||
[
|
||||
dots.slice(0, 16).map((dot, index) => {
|
||||
const x = 13 + (index % 4) * 18;
|
||||
const y = 13 + Math.floor(index / 4) * 18;
|
||||
|
||||
return dot.animate({ x, y });
|
||||
}),
|
||||
|
||||
rects[0].animate({ width: 10, height: 10 }),
|
||||
|
||||
rects.map((rect, index) => {
|
||||
const x = 22 + (index % 3) * 18;
|
||||
const y = 22 + Math.floor(index / 3) * 18;
|
||||
|
||||
return rect.animate({ x, y });
|
||||
}),
|
||||
].flat(),
|
||||
);
|
||||
|
||||
await sleep(300);
|
||||
|
||||
props.blinkingContainer.blink({ delay: 0.1 });
|
||||
await props.blinkingContainer.shrink();
|
||||
|
||||
const baseDotPositions = [
|
||||
[13, 13],
|
||||
[31, 31],
|
||||
[49, 31],
|
||||
[13, 31],
|
||||
[49, 49],
|
||||
[67, 49],
|
||||
[13, 49],
|
||||
[31, 67],
|
||||
[67, 67],
|
||||
];
|
||||
|
||||
const dotPositions: string[] = [];
|
||||
|
||||
for (const [x, y] of baseDotPositions) {
|
||||
const positions = [
|
||||
{ x: x - 9, y: y - 9 },
|
||||
{ x: x + 9, y: y - 9 },
|
||||
{ x: x - 9, y: y + 9 },
|
||||
{ x: x + 9, y: y + 9 },
|
||||
];
|
||||
|
||||
for (const position of positions) {
|
||||
if (!dotPositions.includes(`${position.x},${position.y}`)) {
|
||||
dotPositions.push(`${position.x},${position.y}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
[
|
||||
rects[0].animate({ x: 13, y: 13 }),
|
||||
rects[1].animate({ x: 31, y: 31 }),
|
||||
rects[2].animate({ x: 49, y: 31 }),
|
||||
rects[3].animate({ x: 13, y: 31 }),
|
||||
|
||||
rects[4].animate({ x: 49, y: 49 }),
|
||||
rects[5].animate({ x: 67, y: 49 }),
|
||||
|
||||
rects[6].animate({ x: 13, y: 49 }),
|
||||
rects[7].animate({ x: 31, y: 67 }),
|
||||
rects[8].animate({ x: 67, y: 67 }),
|
||||
|
||||
dots.map((dot, index) => {
|
||||
const position = dotPositions[index].split(",").map(Number);
|
||||
|
||||
return dot.animate({ x: position[0], y: position[1] });
|
||||
}),
|
||||
].flat(),
|
||||
);
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const lines = Array.from({ length: 8 }, () => {
|
||||
return AnimatedRect({
|
||||
app: props.app,
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
radius: 0,
|
||||
color: MAIN_COLOR,
|
||||
centering: false,
|
||||
animationConfig: {
|
||||
duration: 0.25,
|
||||
ease: "linear",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
lines.forEach((graphic) =>
|
||||
props.blinkingContainer.container.addChild(graphic.graphic),
|
||||
);
|
||||
|
||||
(async () => {
|
||||
lines[0].setStyle({ width: 1, height: 0, y: 18, x: 12.5 });
|
||||
await lines[0].animate({ height: 9 });
|
||||
|
||||
lines[1].setStyle({ width: 0, height: 1, y: 30.5, x: 18 });
|
||||
await lines[1].animate({ width: 9 });
|
||||
lines[2].setStyle({ width: 0, height: 1, y: 30.5, x: 36 });
|
||||
await lines[2].animate({ width: 9 });
|
||||
|
||||
lines[3].setStyle({ width: 1, height: 3, y: 36, x: 48.5 });
|
||||
await lines[3].animate({ height: 9 });
|
||||
lines[4].setStyle({ width: 0, height: 1, y: 48.5, x: 54 });
|
||||
await lines[4].animate({ width: 9 });
|
||||
})();
|
||||
|
||||
lines[5].setStyle({ width: 0, height: 1, y: 66.5, x: 62 });
|
||||
await lines[5].animate({ width: 28, x: 62 - 28 }, { duration: 0.4 });
|
||||
lines[6].setStyle({ width: 0, height: 1, y: 66.5, x: 26 });
|
||||
await lines[6].animate({ width: 13.5, x: 26 - 13.5 });
|
||||
lines[7].setStyle({ width: 1, height: 0, y: 66.5, x: 12.5 });
|
||||
await lines[7].animate({ height: 14.5, y: 66.5 - 13.5 });
|
||||
|
||||
await sleep(2000);
|
||||
|
||||
props.blinkingContainer.blink({ delay: 0.1 });
|
||||
|
||||
await Promise.all(
|
||||
[
|
||||
lines.map((line) => line.animate({ alpha: 0 })),
|
||||
|
||||
rects.map((rect) =>
|
||||
rect.animate(props.anchorGraphic.defaultProps, {
|
||||
delay: Math.random() * 0.3,
|
||||
duration: 0.3,
|
||||
}),
|
||||
),
|
||||
|
||||
dots.map((dot) =>
|
||||
dot.animate(dot.defaultProps, { delay: Math.random() * 0.3 }),
|
||||
),
|
||||
].flat(),
|
||||
);
|
||||
|
||||
rects.shift();
|
||||
|
||||
lines.forEach((line) => line.graphic.destroy());
|
||||
rects.forEach((rect) => rect.graphic.destroy());
|
||||
dots.forEach((dot) => dot.graphic.destroy());
|
||||
}
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
import { Ticker } from "@/components/shared/pixi/Pixi";
|
||||
import { sleep } from "@/utils/sleep";
|
||||
|
||||
import { CELL_SIZE, MAIN_COLOR } from "./cell";
|
||||
import AnimatedRect, { IAnimatedRect } from "./components/AnimatedRect";
|
||||
import { IBlinkingContainer } from "./components/BlinkingContainer";
|
||||
import Dot from "./components/Dot";
|
||||
|
||||
type Props = Parameters<Ticker>[0] & {
|
||||
x: number;
|
||||
y: number;
|
||||
blinkingContainer: IBlinkingContainer;
|
||||
anchorGraphic: IAnimatedRect;
|
||||
};
|
||||
|
||||
export default async function scrape(props: Props) {
|
||||
const rects = Array.from({ length: 15 }, () => {
|
||||
return AnimatedRect({
|
||||
app: props.app,
|
||||
x: CELL_SIZE / 2,
|
||||
y: CELL_SIZE / 2,
|
||||
width: 10,
|
||||
height: 10,
|
||||
radius: 0,
|
||||
color: MAIN_COLOR,
|
||||
});
|
||||
});
|
||||
|
||||
const dots = Array.from({ length: 25 }, () => {
|
||||
return Dot({
|
||||
x: CELL_SIZE / 2,
|
||||
y: CELL_SIZE / 2,
|
||||
app: props.app,
|
||||
});
|
||||
});
|
||||
|
||||
dots.forEach((dot) =>
|
||||
props.blinkingContainer.container.addChild(dot.graphic),
|
||||
);
|
||||
|
||||
await sleep(500);
|
||||
|
||||
await Promise.all(
|
||||
[
|
||||
[0, 12, 13, 14].map((index) =>
|
||||
dots[index].animate({ x: 30, y: 30 }, { delay: 0.2 }),
|
||||
),
|
||||
[1, 15, 16, 17].map((index) =>
|
||||
dots[index].animate({ x: CELL_SIZE - 30, y: 30 }, { delay: 0.2 }),
|
||||
),
|
||||
[2, 18, 19, 20].map((index) =>
|
||||
dots[index].animate({ x: 30, y: CELL_SIZE - 30 }, { delay: 0.2 }),
|
||||
),
|
||||
[3, 21, 22, 23].map((index) =>
|
||||
dots[index].animate(
|
||||
{ x: CELL_SIZE - 30, y: CELL_SIZE - 30 },
|
||||
{ delay: 0.2 },
|
||||
),
|
||||
),
|
||||
|
||||
props.anchorGraphic.animate({
|
||||
radius: 0,
|
||||
width: 12,
|
||||
height: 12,
|
||||
}),
|
||||
].flat(),
|
||||
);
|
||||
|
||||
rects.forEach((rect) =>
|
||||
props.blinkingContainer.container.addChild(rect.graphic),
|
||||
);
|
||||
|
||||
rects.unshift(props.anchorGraphic);
|
||||
|
||||
await sleep(500);
|
||||
|
||||
props.blinkingContainer.blink({ delay: 0.1 });
|
||||
await props.blinkingContainer.shrink();
|
||||
|
||||
await Promise.all(
|
||||
[
|
||||
[0, 12, 13, 14].map((index) => dots[index].animate({ x: 22, y: 22 })),
|
||||
[1, 15, 16, 17].map((index) =>
|
||||
dots[index].animate({ x: CELL_SIZE - 22, y: 22 }),
|
||||
),
|
||||
[2, 18, 19, 20].map((index) =>
|
||||
dots[index].animate({ x: 22, y: CELL_SIZE - 22 }),
|
||||
),
|
||||
[3, 21, 22, 23].map((index) =>
|
||||
dots[index].animate({ x: CELL_SIZE - 22, y: CELL_SIZE - 22 }),
|
||||
),
|
||||
|
||||
dots[4].animate({ x: 40, y: 22 }),
|
||||
dots[5].animate({ x: 22, y: 40 }),
|
||||
dots[6].animate({ x: CELL_SIZE - 22, y: 40 }),
|
||||
dots[7].animate({ x: 40, y: 58 }),
|
||||
|
||||
dots[8].animate({ x: 40, y: 22 }),
|
||||
dots[9].animate({ x: 22, y: 40 }),
|
||||
dots[10].animate({ x: CELL_SIZE - 22, y: 40 }),
|
||||
dots[11].animate({ x: 40, y: 58 }),
|
||||
|
||||
rects[0].animate({ width: 10, height: 10 }),
|
||||
rects.slice(0, 4).map((rect) => rect.animate({ x: 31, y: 31 })),
|
||||
rects
|
||||
.slice(4, 8)
|
||||
.map((rect) => rect.animate({ x: CELL_SIZE - 31, y: 31 })),
|
||||
rects
|
||||
.slice(8, 12)
|
||||
.map((rect) => rect.animate({ x: 31, y: CELL_SIZE - 31 })),
|
||||
rects
|
||||
.slice(12, 16)
|
||||
.map((rect) => rect.animate({ x: CELL_SIZE - 31, y: CELL_SIZE - 31 })),
|
||||
].flat(),
|
||||
);
|
||||
|
||||
await sleep(1000);
|
||||
|
||||
props.blinkingContainer.blink({ delay: 0.1 });
|
||||
await props.blinkingContainer.shrink();
|
||||
|
||||
await Promise.all(
|
||||
[
|
||||
dots[0].animate({ x: 4, y: 4 }),
|
||||
dots[1].animate({ x: CELL_SIZE - 4, y: 4 }),
|
||||
dots[2].animate({ x: 4, y: CELL_SIZE - 4 }),
|
||||
dots[3].animate({ x: CELL_SIZE - 4, y: CELL_SIZE - 4 }),
|
||||
dots[4].animate({ x: 40, y: 4 }),
|
||||
dots[5].animate({ x: 4, y: 40 }),
|
||||
dots[6].animate({ x: 76, y: 40 }),
|
||||
dots[7].animate({ x: 40, y: 76 }),
|
||||
|
||||
dots[13].animate({ x: 22, y: 4 }),
|
||||
dots[14].animate({ x: 4, y: 22 }),
|
||||
dots[16].animate({ x: 58, y: 4 }),
|
||||
dots[17].animate({ x: 76, y: 22 }),
|
||||
dots[19].animate({ x: 4, y: 58 }),
|
||||
dots[20].animate({ x: 22, y: 76 }),
|
||||
dots[22].animate({ x: 58, y: 76 }),
|
||||
dots[23].animate({ x: 76, y: 58 }),
|
||||
|
||||
rects.map((rect, index) => {
|
||||
const quadrant = Math.floor(index / 4);
|
||||
const position = index % 4;
|
||||
|
||||
const col = (position % 2 === 0 ? 1 : 2) + (quadrant % 2 === 0 ? 0 : 2);
|
||||
const row = Math.floor(position / 2) + (quadrant < 2 ? 1 : 3);
|
||||
|
||||
return rect.animate({
|
||||
x: 13 + (col - 1) * 18,
|
||||
y: 13 + (row - 1) * 18,
|
||||
});
|
||||
}),
|
||||
].flat(),
|
||||
);
|
||||
|
||||
await sleep(1200);
|
||||
|
||||
Promise.all(
|
||||
dots.map((dot) =>
|
||||
dot.animate({ alpha: 0 }, { delay: Math.random() * 0.3 }),
|
||||
),
|
||||
);
|
||||
|
||||
await sleep(100);
|
||||
|
||||
props.blinkingContainer.blink({ delay: 0.2 });
|
||||
|
||||
const newWidths: number[] = [];
|
||||
|
||||
for (let i = 0; i < rects.length; i++) {
|
||||
if (i % 2 === 0) {
|
||||
newWidths.push(20 + Math.random() * 28);
|
||||
} else {
|
||||
const remainingSpace = 62 - newWidths[i - 1];
|
||||
newWidths.push(10 + Math.random() * remainingSpace);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
rects.map((rect, index) => {
|
||||
const y = 8 + Math.floor(index / 2) * 6 + Math.floor(index / 4) * 8;
|
||||
|
||||
return rect.animate(
|
||||
{
|
||||
y,
|
||||
x:
|
||||
(index % 2 === 0 ? 8 : newWidths[index - 1] + 10) +
|
||||
newWidths[index] / 2,
|
||||
height: 4,
|
||||
width: newWidths[index],
|
||||
},
|
||||
{
|
||||
delay: Math.random() * 0.1,
|
||||
},
|
||||
);
|
||||
}),
|
||||
]);
|
||||
|
||||
props.blinkingContainer.blink({ delay: 0.1 });
|
||||
|
||||
await sleep(2000);
|
||||
|
||||
await Promise.all(
|
||||
[
|
||||
rects.map((rect) =>
|
||||
rect.animate(props.anchorGraphic.defaultProps, {
|
||||
delay: Math.random() * 0.3,
|
||||
duration: 0.3,
|
||||
}),
|
||||
),
|
||||
].flat(),
|
||||
);
|
||||
|
||||
rects.shift();
|
||||
|
||||
rects.forEach((rect) => rect.graphic.destroy());
|
||||
dots.forEach((dot) => dot.graphic.destroy());
|
||||
}
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
import { Ticker } from "@/components/shared/pixi/Pixi";
|
||||
import { sleep } from "@/utils/sleep";
|
||||
|
||||
import { CELL_SIZE, MAIN_COLOR } from "./cell";
|
||||
import AnimatedRect, { IAnimatedRect } from "./components/AnimatedRect";
|
||||
import { IBlinkingContainer } from "./components/BlinkingContainer";
|
||||
import Dot from "./components/Dot";
|
||||
|
||||
type Props = Parameters<Ticker>[0] & {
|
||||
x: number;
|
||||
y: number;
|
||||
blinkingContainer: IBlinkingContainer;
|
||||
anchorGraphic: IAnimatedRect;
|
||||
};
|
||||
|
||||
export default async function search(props: Props) {
|
||||
const rects = Array.from({ length: 8 }, () => {
|
||||
return AnimatedRect({
|
||||
app: props.app,
|
||||
x: CELL_SIZE / 2,
|
||||
y: CELL_SIZE / 2,
|
||||
width: 10,
|
||||
height: 10,
|
||||
radius: 0,
|
||||
color: MAIN_COLOR,
|
||||
});
|
||||
});
|
||||
|
||||
const dots = Array.from({ length: 16 }, () => {
|
||||
return Dot({
|
||||
x: CELL_SIZE / 2,
|
||||
y: CELL_SIZE / 2,
|
||||
app: props.app,
|
||||
});
|
||||
});
|
||||
|
||||
dots.forEach((dot) =>
|
||||
props.blinkingContainer.container.addChild(dot.graphic),
|
||||
);
|
||||
|
||||
await sleep(500);
|
||||
|
||||
await props.anchorGraphic.animate({
|
||||
radius: 0,
|
||||
width: 12,
|
||||
height: 12,
|
||||
});
|
||||
|
||||
rects.forEach((rect) =>
|
||||
props.blinkingContainer.container.addChild(rect.graphic),
|
||||
);
|
||||
|
||||
rects.unshift(props.anchorGraphic);
|
||||
|
||||
await sleep(500);
|
||||
|
||||
props.blinkingContainer.blink({ delay: 0.1 });
|
||||
await props.blinkingContainer.shrink();
|
||||
|
||||
await Promise.all(
|
||||
[
|
||||
dots.map((dot, index) => {
|
||||
const x = 13 + (index % 4) * 18;
|
||||
const y = 13 + Math.floor(index / 4) * 18;
|
||||
|
||||
return dot.animate({ x, y });
|
||||
}),
|
||||
|
||||
rects[0].animate({ width: 10, height: 10 }),
|
||||
|
||||
rects.map((rect, index) => {
|
||||
const x = 22 + (index % 3) * 18;
|
||||
const y = 22 + Math.floor(index / 3) * 18;
|
||||
|
||||
return rect.animate({ x, y });
|
||||
}),
|
||||
].flat(),
|
||||
);
|
||||
|
||||
await sleep(300);
|
||||
|
||||
Promise.all(
|
||||
[
|
||||
rects.map((rect) => rect.animate({ alpha: 0.68 })),
|
||||
dots.map((dot) => dot.animate({ alpha: 0.68 })),
|
||||
].flat(),
|
||||
);
|
||||
|
||||
props.blinkingContainer.blink();
|
||||
await sleep(400);
|
||||
|
||||
for await (const rect of rects) {
|
||||
// Get the surrounding dots of this rect
|
||||
const rectX = rect.currentProps.x;
|
||||
const rectY = rect.currentProps.y;
|
||||
const surroundingDots = dots.filter((dot) => {
|
||||
const dx = Math.abs(dot.currentProps.x - rectX);
|
||||
const dy = Math.abs(dot.currentProps.y - rectY);
|
||||
|
||||
// Consider "surrounding" as adjacent horizontally, vertically, or diagonally (distance 18)
|
||||
return (
|
||||
(dx === 0 && dy === 9) ||
|
||||
(dx === 9 && dy === 0) ||
|
||||
(dx === 9 && dy === 9)
|
||||
);
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
[
|
||||
surroundingDots.map((dot) =>
|
||||
dot.animate({ alpha: 1 }, { duration: 0.75 }),
|
||||
),
|
||||
rect.animate({ alpha: 1, width: 14, height: 14 }, { duration: 0.75 }),
|
||||
].flat(),
|
||||
);
|
||||
|
||||
rect.animate({ alpha: 0.68, width: 10, height: 10 }, { duration: 0.75 });
|
||||
Promise.all(
|
||||
surroundingDots.map((dot) =>
|
||||
dot.animate({ alpha: 0.68 }, { duration: 0.75 }),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
[
|
||||
rects.map((rect) =>
|
||||
rect.animate(props.anchorGraphic.defaultProps, {
|
||||
delay: Math.random() * 0.3,
|
||||
duration: 0.3,
|
||||
}),
|
||||
),
|
||||
|
||||
dots.map((dot) =>
|
||||
dot.animate(dot.defaultProps, { delay: Math.random() * 0.3 }),
|
||||
),
|
||||
].flat(),
|
||||
);
|
||||
|
||||
rects.shift();
|
||||
|
||||
rects.forEach((rect) => rect.graphic.destroy());
|
||||
dots.forEach((dot) => dot.graphic.destroy());
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
"use client";
|
||||
|
||||
// import dynamic from "next/dynamic";
|
||||
// import { useRef, useEffect, forwardRef } from "react";
|
||||
|
||||
// const originalText =
|
||||
// "";
|
||||
|
||||
type Options = {
|
||||
randomizeChance?: number;
|
||||
reversed?: boolean;
|
||||
};
|
||||
|
||||
export const encryptText = (
|
||||
text: string,
|
||||
progress: number,
|
||||
_options?: Options,
|
||||
) => {
|
||||
const options = {
|
||||
randomizeChance: 0.7,
|
||||
..._options,
|
||||
};
|
||||
|
||||
const encryptionChars = "a-zA-Z0-9*=?!";
|
||||
const skipTags = ["<br class='lg-max:hidden'>", "<span>", "</span>"];
|
||||
|
||||
// Calculate how many characters should be encrypted
|
||||
const totalChars = text.length;
|
||||
const encryptedCount = Math.floor(totalChars * (1 - progress));
|
||||
|
||||
let result = "";
|
||||
let charIndex = 1;
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const char = text[i];
|
||||
|
||||
// Check if we're at the start of a tag to skip
|
||||
let shouldSkip = false;
|
||||
|
||||
for (const tag of skipTags) {
|
||||
if (text.substring(i, i + tag.length) === tag) {
|
||||
result += tag;
|
||||
i += tag.length - 1; // -1 because loop will increment
|
||||
shouldSkip = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldSkip) continue;
|
||||
|
||||
// Skip spaces - keep them as is
|
||||
if (char === " ") {
|
||||
result += char;
|
||||
charIndex++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// If this character should be encrypted
|
||||
if (
|
||||
options.reversed
|
||||
? charIndex < encryptedCount
|
||||
: text.length - charIndex < encryptedCount
|
||||
) {
|
||||
// 40% chance to show original character, 60% chance to encrypt
|
||||
if (Math.random() < options.randomizeChance) {
|
||||
result += char;
|
||||
} else {
|
||||
// Use random character from encryption set
|
||||
const randomIndex = Math.floor(Math.random() * encryptionChars.length);
|
||||
result += encryptionChars[randomIndex];
|
||||
}
|
||||
} else {
|
||||
// Keep original character
|
||||
result += char;
|
||||
}
|
||||
|
||||
charIndex++;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// const Wrapper = forwardRef<
|
||||
// HTMLDivElement,
|
||||
// React.HTMLAttributes<HTMLDivElement>
|
||||
// >((props, ref) => {
|
||||
// return (
|
||||
// <div className="text-title-h1 mx-auto text-center [&_span]:text-heat-100 mb-12 lg:mb-16">
|
||||
// <div {...props} className="hidden lg:contents" ref={ref} />
|
||||
// <div
|
||||
// className="lg:hidden contents"
|
||||
// dangerouslySetInnerHTML={{ __html: originalText }}
|
||||
// />
|
||||
// </div>
|
||||
// );
|
||||
// });
|
||||
|
||||
// Wrapper.displayName = "Wrapper";
|
||||
|
||||
// export default dynamic(() => Promise.resolve(HomeHeroTitle), {
|
||||
// ssr: false,
|
||||
// loading: () => (
|
||||
// <Wrapper
|
||||
// dangerouslySetInnerHTML={{ __html: encryptText(originalText, 0) }}
|
||||
// />
|
||||
// ),
|
||||
// });
|
||||
|
||||
// function HomeHeroTitle() {
|
||||
// const textRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// useEffect(() => {
|
||||
// if (window.innerWidth < 996) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// let progress = 0;
|
||||
// let increaseProgress = -10;
|
||||
|
||||
// const animate = () => {
|
||||
// increaseProgress = (increaseProgress + 1) % 5;
|
||||
|
||||
// if (increaseProgress === 4) {
|
||||
// progress += 0.3;
|
||||
// }
|
||||
|
||||
// if (progress > 1) {
|
||||
// progress = 1;
|
||||
// textRef.current!.innerHTML = encryptText(originalText, progress);
|
||||
|
||||
// return;
|
||||
// }
|
||||
|
||||
// textRef.current!.innerHTML = encryptText(originalText, progress);
|
||||
|
||||
// const interval = 50 + progress * 20;
|
||||
// setTimeout(animate, interval);
|
||||
// };
|
||||
|
||||
// animate();
|
||||
// }, []);
|
||||
|
||||
// return (
|
||||
// <Wrapper
|
||||
// dangerouslySetInnerHTML={{ __html: encryptText(originalText, 0) }}
|
||||
// ref={textRef}
|
||||
// />
|
||||
// );
|
||||
// }
|
||||
|
||||
// import dynamic from "next/dynamic";
|
||||
// import { useRef, useEffect, forwardRef } from "react";
|
||||
|
||||
// const originalText =
|
||||
// "Turn websites into <br class='lg-max:hidden'><span>LLM-ready</span> data";
|
||||
|
||||
// type Options = {
|
||||
// randomizeChance?: number;
|
||||
// reversed?: boolean;
|
||||
// };
|
||||
|
||||
// export const encryptText = (
|
||||
// text: string,
|
||||
// progress: number,
|
||||
// _options?: Options,
|
||||
// ) => {
|
||||
// const options = {
|
||||
// randomizeChance: 0.7,
|
||||
// ..._options,
|
||||
// };
|
||||
|
||||
// const encryptionChars = "a-zA-Z0-9*=?!";
|
||||
// const skipTags = ["<br class='lg-max:hidden'>", "<span>", "</span>"];
|
||||
|
||||
// // Calculate how many characters should be encrypted
|
||||
// const totalChars = text.length;
|
||||
// const encryptedCount = Math.floor(totalChars * (1 - progress));
|
||||
|
||||
// let result = "";
|
||||
// let charIndex = 1;
|
||||
|
||||
// for (let i = 0; i < text.length; i++) {
|
||||
// const char = text[i];
|
||||
|
||||
// // Check if we're at the start of a tag to skip
|
||||
// let shouldSkip = false;
|
||||
|
||||
// for (const tag of skipTags) {
|
||||
// if (text.substring(i, i + tag.length) === tag) {
|
||||
// result += tag;
|
||||
// i += tag.length - 1; // -1 because loop will increment
|
||||
// shouldSkip = true;
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (shouldSkip) continue;
|
||||
|
||||
// // Skip spaces - keep them as is
|
||||
// if (char === " ") {
|
||||
// result += char;
|
||||
// charIndex++;
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// // If this character should be encrypted
|
||||
// if (
|
||||
// options.reversed
|
||||
// ? charIndex < encryptedCount
|
||||
// : text.length - charIndex < encryptedCount
|
||||
// ) {
|
||||
// // 40% chance to show original character, 60% chance to encrypt
|
||||
// if (Math.random() < options.randomizeChance) {
|
||||
// result += char;
|
||||
// } else {
|
||||
// // Use random character from encryption set
|
||||
// const randomIndex = Math.floor(Math.random() * encryptionChars.length);
|
||||
// result += encryptionChars[randomIndex];
|
||||
// }
|
||||
// } else {
|
||||
// // Keep original character
|
||||
// result += char;
|
||||
// }
|
||||
|
||||
// charIndex++;
|
||||
// }
|
||||
|
||||
// return result;
|
||||
// };
|
||||
|
||||
// const Wrapper = forwardRef<
|
||||
// HTMLDivElement,
|
||||
// React.HTMLAttributes<HTMLDivElement>
|
||||
// >((props, ref) => {
|
||||
// return (
|
||||
// <div className="text-title-h1 mx-auto text-center [&_span]:text-heat-100 mb-12 lg:mb-16">
|
||||
// <div {...props} className="hidden lg:contents" ref={ref} />
|
||||
// <div
|
||||
// className="lg:hidden contents"
|
||||
// dangerouslySetInnerHTML={{ __html: originalText }}
|
||||
// />
|
||||
// </div>
|
||||
// );
|
||||
// });
|
||||
|
||||
// Wrapper.displayName = "Wrapper";
|
||||
|
||||
// export default dynamic(() => Promise.resolve(HomeHeroTitle), {
|
||||
// ssr: false,
|
||||
// loading: () => (
|
||||
// <Wrapper
|
||||
// dangerouslySetInnerHTML={{ __html: encryptText(originalText, 0) }}
|
||||
// />
|
||||
// ),
|
||||
// });
|
||||
|
||||
export default function HomeHeroTitle() {
|
||||
return (
|
||||
<div className="text-title-h1 mx-auto text-center [&_span]:text-[#de6f6f] mb-12 lg:mb-16">
|
||||
Drag-and-drop <br className="lg-max:hidden" />
|
||||
<span>Agent Workflow</span> Builder
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { useState, useEffect } from "react";
|
||||
import { exampleTemplatesList } from "@/lib/workflow/templates/examples";
|
||||
|
||||
interface Step2PlaceholderProps {
|
||||
onReset: () => void;
|
||||
onCreateWorkflow: () => void;
|
||||
onLoadWorkflow?: (workflowId: string) => void;
|
||||
onLoadTemplate?: (templateId: string) => void;
|
||||
}
|
||||
|
||||
interface Workflow {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export default function Step2Placeholder({ onReset, onCreateWorkflow, onLoadWorkflow, onLoadTemplate }: Step2PlaceholderProps) {
|
||||
const [workflows, setWorkflows] = useState<Workflow[]>([]);
|
||||
const [activeTab, setActiveTab] = useState<"workflows" | "templates">("templates");
|
||||
const templates = exampleTemplatesList;
|
||||
|
||||
useEffect(() => {
|
||||
// Load workflows from API
|
||||
const loadWorkflows = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/workflows');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.workflows && Array.isArray(data.workflows)) {
|
||||
setWorkflows(data.workflows.map((w: any) => ({
|
||||
id: w.id,
|
||||
title: w.name,
|
||||
description: w.description,
|
||||
createdAt: new Date(w.updatedAt || w.createdAt).toLocaleDateString(),
|
||||
})));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading workflows:', error);
|
||||
}
|
||||
};
|
||||
|
||||
loadWorkflows();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="max-w-[900px] mx-auto w-full">
|
||||
{/* Title */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="text-center mb-24"
|
||||
>
|
||||
<h2 className="text-title-h2 text-accent-black mb-8">Get Started</h2>
|
||||
<p className="text-body-large text-black-alpha-48">
|
||||
Create a new workflow, use a template, or continue where you left off
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex justify-center gap-8 mb-24">
|
||||
<button
|
||||
onClick={() => setActiveTab("workflows")}
|
||||
className={`px-20 py-10 rounded-8 text-body-medium transition-all ${
|
||||
activeTab === "workflows"
|
||||
? "bg-heat-100 text-white"
|
||||
: "bg-background-base text-accent-black hover:bg-black-alpha-4 border border-border-faint"
|
||||
}`}
|
||||
>
|
||||
Your Workflows ({workflows.length})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab("templates")}
|
||||
className={`px-20 py-10 rounded-8 text-body-medium transition-all ${
|
||||
activeTab === "templates"
|
||||
? "bg-heat-100 text-white"
|
||||
: "bg-background-base text-accent-black hover:bg-black-alpha-4 border border-border-faint"
|
||||
}`}
|
||||
>
|
||||
Templates ({templates.length})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-16 mb-32">
|
||||
{/* Create Workflow Tile - Always first */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{
|
||||
duration: 0.5,
|
||||
delay: 0,
|
||||
ease: "easeOut"
|
||||
}}
|
||||
className="relative cursor-pointer"
|
||||
onClick={onCreateWorkflow}
|
||||
>
|
||||
<div className="bg-accent-white rounded-12 p-24 border-2 border-dashed border-border-light hover:border-heat-100 transition-all h-full flex items-center justify-center min-h-[160px]">
|
||||
<div className="text-center">
|
||||
<div className="w-48 h-48 rounded-full bg-heat-4 flex items-center justify-center mx-auto mb-12">
|
||||
<svg className="w-24 h-24 text-heat-100" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-label-large text-accent-black font-medium">Create Workflow</h3>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Show Workflows or Templates based on tab */}
|
||||
{activeTab === "workflows" ? (
|
||||
workflows.length > 0 ? (
|
||||
workflows.map((workflow, index) => (
|
||||
<motion.div
|
||||
key={workflow.id}
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{
|
||||
duration: 0.5,
|
||||
delay: (index + 1) * 0.1,
|
||||
ease: "easeOut"
|
||||
}}
|
||||
className="relative cursor-pointer"
|
||||
onClick={() => onLoadWorkflow?.(workflow.id)}
|
||||
>
|
||||
<div className="bg-accent-white rounded-12 p-24 border border-border-faint hover:border-heat-100 hover:shadow-sm transition-all h-full min-h-[160px] group">
|
||||
<div className="absolute inset-0 rounded-12 bg-gradient-to-br from-heat-4 to-transparent opacity-0 group-hover:opacity-10 transition-opacity" />
|
||||
<div className="relative">
|
||||
<h3 className="text-label-large text-accent-black font-medium mb-8">{workflow.title}</h3>
|
||||
{workflow.description && (
|
||||
<p className="text-body-small text-black-alpha-48 mb-12 line-clamp-2">{workflow.description}</p>
|
||||
)}
|
||||
<p className="text-body-small text-black-alpha-32">Updated {workflow.createdAt}</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))
|
||||
) : (
|
||||
<div className="col-span-1 lg:col-span-3 flex items-center justify-center min-h-[160px]">
|
||||
<p className="text-body-medium text-black-alpha-48">No saved workflows yet</p>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
exampleTemplatesList.map((template, index) => (
|
||||
<motion.div
|
||||
key={template.id}
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{
|
||||
duration: 0.5,
|
||||
delay: (index + 1) * 0.1,
|
||||
ease: "easeOut"
|
||||
}}
|
||||
className="relative cursor-pointer"
|
||||
onClick={() => onLoadTemplate?.(template.id)}
|
||||
>
|
||||
<div className="bg-accent-white rounded-12 p-24 border border-border-faint hover:border-gray-700 hover:shadow-md transition-all h-full min-h-[160px] relative overflow-hidden group">
|
||||
<div className="relative">
|
||||
<h3 className="text-label-large text-accent-black font-medium mb-8">{template.name}</h3>
|
||||
<p className="text-body-small text-black-alpha-48">{template.description}</p>
|
||||
<div className="mt-12 inline-flex items-center gap-6 text-body-small text-accent-black group-hover:text-gray-700">
|
||||
<span>Use template</span>
|
||||
<svg className="w-12 h-12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Action Button */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.5, delay: 0.5 }}
|
||||
className="flex justify-center"
|
||||
>
|
||||
<button
|
||||
onClick={onReset}
|
||||
className="px-24 py-12 text-label-large text-black-alpha-48 hover:text-accent-black transition-colors"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
"use client";
|
||||
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
|
||||
interface ConfirmDialogProps {
|
||||
isOpen: boolean;
|
||||
title: string;
|
||||
description: string;
|
||||
confirmText?: string;
|
||||
cancelText?: string;
|
||||
variant?: "danger" | "warning" | "default";
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export default function ConfirmDialog({
|
||||
isOpen,
|
||||
title,
|
||||
description,
|
||||
confirmText = "Confirm",
|
||||
cancelText = "Cancel",
|
||||
variant = "default",
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}: ConfirmDialogProps) {
|
||||
const handleConfirm = () => {
|
||||
onConfirm();
|
||||
onCancel(); // Close dialog
|
||||
};
|
||||
|
||||
const variantStyles = {
|
||||
danger: {
|
||||
button: "bg-red-600 hover:bg-red-700 text-white",
|
||||
icon: "text-red-600",
|
||||
bg: "bg-red-50",
|
||||
border: "border-red-200",
|
||||
},
|
||||
warning: {
|
||||
button: "bg-yellow-600 hover:bg-yellow-700 text-white",
|
||||
icon: "text-yellow-600",
|
||||
bg: "bg-yellow-50",
|
||||
border: "border-yellow-200",
|
||||
},
|
||||
default: {
|
||||
button: "bg-heat-100 hover:bg-heat-200 text-white",
|
||||
icon: "text-heat-100",
|
||||
bg: "bg-heat-4",
|
||||
border: "border-heat-100",
|
||||
},
|
||||
};
|
||||
|
||||
const styles = variantStyles[variant];
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 bg-black-alpha-48 z-[200] flex items-center justify-center p-20"
|
||||
onClick={onCancel}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ scale: 0.95, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
exit={{ scale: 0.95, opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="bg-accent-white rounded-16 shadow-2xl max-w-400 w-full"
|
||||
>
|
||||
{/* Content */}
|
||||
<div className="p-24">
|
||||
<div className={`w-48 h-48 rounded-full ${styles.bg} border ${styles.border} flex items-center justify-center mb-16`}>
|
||||
<svg className={`w-24 h-24 ${styles.icon}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
{variant === "danger" ? (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
) : variant === "warning" ? (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
) : (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
)}
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<h2 className="text-title-h5 text-accent-black mb-8">{title}</h2>
|
||||
<p className="text-body-small text-black-alpha-48 mb-24">{description}</p>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-12">
|
||||
<button
|
||||
onClick={onCancel}
|
||||
className="flex-1 px-16 py-10 bg-background-base hover:bg-black-alpha-4 border border-border-faint rounded-8 text-body-medium text-accent-black transition-colors"
|
||||
>
|
||||
{cancelText}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleConfirm}
|
||||
className={`flex-1 px-16 py-10 rounded-8 text-body-medium transition-all active:scale-[0.98] ${styles.button}`}
|
||||
>
|
||||
{confirmText}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
+238
@@ -0,0 +1,238 @@
|
||||
"use client";
|
||||
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { useState } from "react";
|
||||
import type { Node } from "@xyflow/react";
|
||||
|
||||
interface ConnectionMapperModalProps {
|
||||
sourceNode: Node | null;
|
||||
targetNode: Node | null;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onConnect: (mapping: Record<string, string>) => void;
|
||||
}
|
||||
|
||||
export default function ConnectionMapperModal({
|
||||
sourceNode,
|
||||
targetNode,
|
||||
isOpen,
|
||||
onClose,
|
||||
onConnect,
|
||||
}: ConnectionMapperModalProps) {
|
||||
const [mapping, setMapping] = useState<Record<string, string>>({});
|
||||
|
||||
if (!sourceNode || !targetNode) return null;
|
||||
|
||||
const sourceData = sourceNode.data as any;
|
||||
const targetData = targetNode.data as any;
|
||||
|
||||
// Get output keys from source node
|
||||
const getOutputKeys = (node: Node): string[] => {
|
||||
const data = node.data as any;
|
||||
const keys: string[] = [];
|
||||
|
||||
// Check if there's an outputSchema defined
|
||||
if (data.outputSchema && Array.isArray(data.outputSchema)) {
|
||||
keys.push(...data.outputSchema.map((field: any) => field.name));
|
||||
}
|
||||
|
||||
// Check if JSON output with schema
|
||||
if (data.jsonOutputSchema) {
|
||||
try {
|
||||
const schema = JSON.parse(data.jsonOutputSchema);
|
||||
if (schema.properties) {
|
||||
keys.push(...Object.keys(schema.properties));
|
||||
}
|
||||
} catch (e) {
|
||||
// Invalid JSON
|
||||
}
|
||||
}
|
||||
|
||||
// Add default keys based on node type
|
||||
const nodeType = data.nodeType;
|
||||
if (nodeType === 'mcp' || nodeType === 'firecrawl') {
|
||||
const outputField = data.outputField || 'full';
|
||||
if (outputField === 'markdown') keys.push('markdown');
|
||||
else if (outputField === 'html') keys.push('html');
|
||||
else if (outputField === 'metadata') keys.push('metadata', 'metadata.title', 'metadata.description');
|
||||
else if (outputField === 'results') keys.push('results[]', 'results[0]');
|
||||
else if (outputField === 'urls') keys.push('urls[]');
|
||||
else if (outputField === 'json') keys.push('json');
|
||||
else keys.push('*');
|
||||
}
|
||||
|
||||
// If no specific keys, add common ones
|
||||
if (keys.length === 0) {
|
||||
keys.push('message', 'data', 'result');
|
||||
}
|
||||
|
||||
return keys;
|
||||
};
|
||||
|
||||
// Get input keys from target node (from arguments)
|
||||
const getInputKeys = (node: Node): string[] => {
|
||||
const data = node.data as any;
|
||||
|
||||
if (data.arguments && Array.isArray(data.arguments)) {
|
||||
return data.arguments.map((arg: any) => arg.name);
|
||||
}
|
||||
|
||||
return ['input'];
|
||||
};
|
||||
|
||||
const sourceKeys = getOutputKeys(sourceNode);
|
||||
const targetKeys = getInputKeys(targetNode);
|
||||
|
||||
const handleConnect = () => {
|
||||
onConnect(mapping);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleQuickConnect = () => {
|
||||
// Auto-map matching field names
|
||||
const autoMapping: Record<string, string> = {};
|
||||
|
||||
targetKeys.forEach(targetKey => {
|
||||
// Try to find exact match
|
||||
const exactMatch = sourceKeys.find(sk => sk === targetKey);
|
||||
if (exactMatch) {
|
||||
autoMapping[targetKey] = exactMatch;
|
||||
return;
|
||||
}
|
||||
|
||||
// Try partial match
|
||||
const partialMatch = sourceKeys.find(sk =>
|
||||
sk.toLowerCase().includes(targetKey.toLowerCase()) ||
|
||||
targetKey.toLowerCase().includes(sk.toLowerCase())
|
||||
);
|
||||
if (partialMatch) {
|
||||
autoMapping[targetKey] = partialMatch;
|
||||
}
|
||||
});
|
||||
|
||||
setMapping(autoMapping);
|
||||
};
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 bg-black-alpha-48 z-[200] flex items-center justify-center p-20"
|
||||
onClick={onClose}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ scale: 0.95, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
exit={{ scale: 0.95, opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="bg-accent-white rounded-16 shadow-2xl max-w-600 w-full max-h-[80vh] overflow-y-auto"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="p-24 border-b border-border-faint">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-title-h3 text-accent-black">Map Connection</h2>
|
||||
<p className="text-body-small text-black-alpha-48 mt-4">
|
||||
{sourceData.nodeName} → {targetData.nodeName}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-32 h-32 rounded-6 hover:bg-black-alpha-4 transition-colors flex items-center justify-center"
|
||||
>
|
||||
<svg className="w-16 h-16 text-black-alpha-48" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-24 space-y-20">
|
||||
{/* Quick Auto-Map */}
|
||||
<button
|
||||
onClick={handleQuickConnect}
|
||||
className="w-full px-16 py-10 bg-heat-4 hover:bg-heat-8 border border-heat-100 rounded-8 text-body-medium text-heat-100 transition-colors flex items-center justify-center gap-8"
|
||||
>
|
||||
<svg className="w-16 h-16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
Auto-Map Fields
|
||||
</button>
|
||||
|
||||
{/* Mapping Grid */}
|
||||
<div className="space-y-12">
|
||||
<div className="grid grid-cols-2 gap-12 pb-12 border-b border-border-faint">
|
||||
<p className="text-label-small text-black-alpha-48">From ({sourceData.nodeName})</p>
|
||||
<p className="text-label-small text-black-alpha-48">To ({targetData.nodeName})</p>
|
||||
</div>
|
||||
|
||||
{targetKeys.map((targetKey) => (
|
||||
<div key={targetKey} className="grid grid-cols-2 gap-12 items-center">
|
||||
<select
|
||||
value={mapping[targetKey] || ''}
|
||||
onChange={(e) => setMapping({ ...mapping, [targetKey]: e.target.value })}
|
||||
className="px-12 py-8 bg-background-base border border-border-faint rounded-6 text-body-small text-accent-black font-mono focus:outline-none focus:border-heat-100 transition-colors"
|
||||
>
|
||||
<option value="">-- Select source --</option>
|
||||
<option value="__full__">Full Output</option>
|
||||
{sourceKeys.map((sourceKey) => (
|
||||
<option key={sourceKey} value={sourceKey}>
|
||||
{sourceKey}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<div className="flex items-center gap-8">
|
||||
<svg className="w-12 h-12 text-heat-100" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
<code className="text-body-small text-accent-black font-mono">{targetKey}</code>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Preview */}
|
||||
{Object.keys(mapping).length > 0 && (
|
||||
<div className="p-16 bg-blue-50 rounded-12 border border-blue-200">
|
||||
<h3 className="text-label-small text-blue-900 mb-8 font-medium">Connection Preview</h3>
|
||||
<div className="space-y-4 text-body-small text-blue-800 font-mono">
|
||||
{Object.entries(mapping).map(([target, source]) => (
|
||||
<div key={target}>
|
||||
{target} = {source === '__full__' ? 'entire output' : source}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="p-24 border-t border-border-faint flex items-center justify-between">
|
||||
<button
|
||||
onClick={() => {
|
||||
setMapping({});
|
||||
onConnect({});
|
||||
}}
|
||||
className="px-20 py-10 text-body-medium text-black-alpha-48 hover:text-accent-black transition-colors"
|
||||
>
|
||||
Skip Mapping
|
||||
</button>
|
||||
<button
|
||||
onClick={handleConnect}
|
||||
className="px-24 py-10 bg-heat-100 hover:bg-heat-200 text-white rounded-8 transition-all active:scale-[0.98] text-body-medium font-medium"
|
||||
>
|
||||
Connect
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
+352
@@ -0,0 +1,352 @@
|
||||
"use client";
|
||||
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface ConnectorsPanelProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
interface MCPTemplate {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
url: string;
|
||||
authType: 'none' | 'api-key' | 'oauth';
|
||||
apiKeyPlaceholder?: string;
|
||||
tools: string[];
|
||||
category: 'web' | 'ai' | 'data' | 'custom';
|
||||
}
|
||||
|
||||
const MCP_TEMPLATES: MCPTemplate[] = [
|
||||
{
|
||||
id: 'browserbase',
|
||||
name: 'Browserbase',
|
||||
description: 'Browser automation and web scraping',
|
||||
url: 'https://mcp.browserbase.com',
|
||||
authType: 'api-key',
|
||||
apiKeyPlaceholder: 'BROWSERBASE_API_KEY',
|
||||
tools: ['browser_navigate', 'browser_click', 'browser_scrape'],
|
||||
category: 'web',
|
||||
},
|
||||
];
|
||||
|
||||
export default function ConnectorsPanel({ isOpen, onClose }: ConnectorsPanelProps) {
|
||||
const [activeTab, setActiveTab] = useState<'mcp' | 'llm'>('mcp');
|
||||
const [mcpTab, setMcpTab] = useState<'templates' | 'custom'>('templates');
|
||||
const [connectedMCPs, setConnectedMCPs] = useState<any[]>([]);
|
||||
|
||||
// Custom MCP form
|
||||
const [customName, setCustomName] = useState('');
|
||||
const [customUrl, setCustomUrl] = useState('');
|
||||
const [customAuthType, setCustomAuthType] = useState('none');
|
||||
const [customApiKey, setCustomApiKey] = useState('');
|
||||
|
||||
const handleConnectTemplate = (template: MCPTemplate) => {
|
||||
if (template.authType === 'api-key') {
|
||||
// Show API key input
|
||||
const apiKey = prompt(`Enter your ${template.apiKeyPlaceholder}:`);
|
||||
if (!apiKey) return;
|
||||
|
||||
const url = template.url.replace(`{${template.apiKeyPlaceholder}}`, apiKey);
|
||||
const newMCP = {
|
||||
id: `${template.id}_${Date.now()}`,
|
||||
name: template.name,
|
||||
url,
|
||||
authType: 'api-key',
|
||||
accessToken: apiKey,
|
||||
tools: template.tools,
|
||||
category: template.category,
|
||||
};
|
||||
|
||||
setConnectedMCPs([...connectedMCPs, newMCP]);
|
||||
toast.success(`Connected to ${template.name}`);
|
||||
} else {
|
||||
const newMCP = {
|
||||
id: `${template.id}_${Date.now()}`,
|
||||
name: template.name,
|
||||
url: template.url,
|
||||
authType: 'none',
|
||||
tools: template.tools,
|
||||
category: template.category,
|
||||
};
|
||||
|
||||
setConnectedMCPs([...connectedMCPs, newMCP]);
|
||||
toast.success(`Connected to ${template.name}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConnectCustom = () => {
|
||||
if (!customName || !customUrl) {
|
||||
toast.error('Please fill in name and URL');
|
||||
return;
|
||||
}
|
||||
|
||||
const newMCP = {
|
||||
id: `custom_${Date.now()}`,
|
||||
name: customName,
|
||||
url: customUrl,
|
||||
authType: customAuthType,
|
||||
accessToken: customAuthType !== 'none' ? customApiKey : undefined,
|
||||
tools: [],
|
||||
category: 'custom' as const,
|
||||
};
|
||||
|
||||
setConnectedMCPs([...connectedMCPs, newMCP]);
|
||||
toast.success(`Connected to ${customName}`);
|
||||
|
||||
// Reset form
|
||||
setCustomName('');
|
||||
setCustomUrl('');
|
||||
setCustomAuthType('none');
|
||||
setCustomApiKey('');
|
||||
};
|
||||
|
||||
const handleDisconnect = (id: string) => {
|
||||
setConnectedMCPs(connectedMCPs.filter(m => m.id !== id));
|
||||
toast.success('MCP disconnected');
|
||||
};
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 bg-black-alpha-48 z-[100]"
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{/* Panel */}
|
||||
<motion.div
|
||||
initial={{ x: 400, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
exit={{ x: 400, opacity: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="fixed right-0 top-0 h-screen w-full max-w-600 bg-accent-white border-l border-border-faint shadow-2xl z-[101] flex flex-col"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="p-20 border-b border-border-faint">
|
||||
<div className="flex items-center justify-between mb-12">
|
||||
<h2 className="text-title-h3 text-accent-black">Connectors & Config</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-32 h-32 rounded-6 hover:bg-black-alpha-4 transition-colors flex items-center justify-center"
|
||||
>
|
||||
<svg className="w-16 h-16 text-black-alpha-48" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Main Tabs */}
|
||||
<div className="flex gap-8">
|
||||
<button
|
||||
onClick={() => setActiveTab('mcp')}
|
||||
className={`px-16 py-8 rounded-8 text-body-medium font-medium transition-all ${
|
||||
activeTab === 'mcp'
|
||||
? 'bg-heat-100 text-white'
|
||||
: 'bg-background-base text-black-alpha-64 hover:bg-black-alpha-4'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-8">
|
||||
<svg className="w-16 h-16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
MCP Servers
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('llm')}
|
||||
className={`px-16 py-8 rounded-8 text-body-medium font-medium transition-all ${
|
||||
activeTab === 'llm'
|
||||
? 'bg-heat-100 text-white'
|
||||
: 'bg-background-base text-black-alpha-64 hover:bg-black-alpha-4'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-8">
|
||||
<svg className="w-16 h-16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
||||
</svg>
|
||||
LLM Config
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{activeTab === 'mcp' ? (
|
||||
<div>
|
||||
{/* MCP Sub-tabs */}
|
||||
<div className="p-20 border-b border-border-faint">
|
||||
<div className="flex gap-8">
|
||||
<button
|
||||
onClick={() => setMcpTab('templates')}
|
||||
className={`flex-1 px-12 py-8 rounded-8 text-body-small transition-colors ${
|
||||
mcpTab === 'templates'
|
||||
? 'bg-heat-4 text-heat-100 border border-heat-100'
|
||||
: 'bg-background-base text-black-alpha-64 hover:bg-black-alpha-4'
|
||||
}`}
|
||||
>
|
||||
Pre-configured
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setMcpTab('custom')}
|
||||
className={`flex-1 px-12 py-8 rounded-8 text-body-small transition-colors ${
|
||||
mcpTab === 'custom'
|
||||
? 'bg-heat-4 text-heat-100 border border-heat-100'
|
||||
: 'bg-background-base text-black-alpha-64 hover:bg-black-alpha-4'
|
||||
}`}
|
||||
>
|
||||
Custom Remote
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{mcpTab === 'templates' ? (
|
||||
<div className="p-20 space-y-16">
|
||||
<div>
|
||||
<h3 className="text-label-medium text-accent-black mb-12">MCP Templates</h3>
|
||||
<div className="space-y-12">
|
||||
{MCP_TEMPLATES.map((template) => (
|
||||
<div key={template.id} className="p-16 bg-background-base rounded-12 border border-border-faint">
|
||||
<div className="flex items-start justify-between mb-12">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-8 mb-6">
|
||||
<h4 className="text-label-medium text-accent-black font-medium">{template.name}</h4>
|
||||
<span className="px-6 py-2 bg-heat-4 text-heat-100 rounded-4 text-body-small">
|
||||
{template.category}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-body-small text-black-alpha-64 mb-8">{template.description}</p>
|
||||
<p className="text-body-small text-black-alpha-48 font-mono text-xs truncate">
|
||||
{template.tools.length} tools available
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleConnectTemplate(template)}
|
||||
className="px-16 py-8 bg-heat-100 hover:bg-heat-200 text-white rounded-8 text-body-small font-medium transition-all active:scale-[0.98]"
|
||||
>
|
||||
Connect
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-20 space-y-20">
|
||||
<div>
|
||||
<h3 className="text-label-medium text-accent-black mb-12">Add Custom MCP Server</h3>
|
||||
|
||||
<div className="space-y-16">
|
||||
<div>
|
||||
<label className="block text-label-small text-black-alpha-48 mb-8">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={customName}
|
||||
onChange={(e) => setCustomName(e.target.value)}
|
||||
placeholder="My Custom MCP"
|
||||
className="w-full px-12 py-10 bg-accent-white border border-border-faint rounded-8 text-body-medium text-accent-black focus:outline-none focus:border-heat-100 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-label-small text-black-alpha-48 mb-8">URL</label>
|
||||
<input
|
||||
type="text"
|
||||
value={customUrl}
|
||||
onChange={(e) => setCustomUrl(e.target.value)}
|
||||
placeholder="https://mcp.example.com"
|
||||
className="w-full px-12 py-10 bg-accent-white border border-border-faint rounded-8 text-body-medium text-accent-black font-mono focus:outline-none focus:border-heat-100 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-label-small text-black-alpha-48 mb-8">Authentication</label>
|
||||
<select
|
||||
value={customAuthType}
|
||||
onChange={(e) => setCustomAuthType(e.target.value)}
|
||||
className="w-full px-12 py-10 bg-accent-white border border-border-faint rounded-8 text-body-medium text-accent-black focus:outline-none focus:border-heat-100 transition-colors"
|
||||
>
|
||||
<option value="none">None</option>
|
||||
<option value="api-key">API Key</option>
|
||||
<option value="oauth">OAuth</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{customAuthType !== 'none' && (
|
||||
<div>
|
||||
<label className="block text-label-small text-black-alpha-48 mb-8">
|
||||
{customAuthType === 'api-key' ? 'API Key' : 'OAuth Token'}
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={customApiKey}
|
||||
onChange={(e) => setCustomApiKey(e.target.value)}
|
||||
placeholder="Enter your key or ${ENV_VAR}"
|
||||
className="w-full px-12 py-10 bg-accent-white border border-border-faint rounded-8 text-body-medium text-accent-black font-mono focus:outline-none focus:border-heat-100 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={handleConnectCustom}
|
||||
className="w-full px-20 py-12 bg-heat-100 hover:bg-heat-200 text-white rounded-8 text-body-medium font-medium transition-all active:scale-[0.98]"
|
||||
>
|
||||
Add Custom MCP
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Connected MCPs */}
|
||||
{connectedMCPs.length > 0 && (
|
||||
<div className="p-20 border-t border-border-faint">
|
||||
<h3 className="text-label-medium text-accent-black mb-12">Connected MCPs ({connectedMCPs.length})</h3>
|
||||
<div className="space-y-12">
|
||||
{connectedMCPs.map((mcp) => (
|
||||
<div key={mcp.id} className="p-12 bg-accent-white rounded-8 border border-border-faint">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<p className="text-body-small text-accent-black font-medium">{mcp.name}</p>
|
||||
<p className="text-body-small text-black-alpha-48 font-mono text-xs truncate">{mcp.url}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleDisconnect(mcp.id)}
|
||||
className="px-12 py-6 bg-background-base hover:bg-black-alpha-4 border border-border-faint hover:border-border-faint rounded-6 text-body-small text-accent-black transition-colors"
|
||||
>
|
||||
Disconnect
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-20">
|
||||
<h3 className="text-label-medium text-accent-black mb-16">LLM Configuration</h3>
|
||||
<div className="p-16 bg-accent-white rounded-12 border border-border-faint text-center">
|
||||
<p className="text-body-medium text-black-alpha-48">
|
||||
LLM configuration coming soon...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,405 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { Handle, Position } from "@xyflow/react";
|
||||
import type { NodeProps } from "@xyflow/react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
// Custom node component with handles for connections
|
||||
export function CustomNode({ data, selected }: NodeProps) {
|
||||
const nodeType = data.nodeType;
|
||||
const isRunning = data.isRunning;
|
||||
const executionStatus = data.executionStatus;
|
||||
|
||||
// Note node state - MUST be declared before any conditional returns
|
||||
// This ensures hooks are called in the same order every render
|
||||
const noteText = String((data as any).noteText || 'Double-click to edit note');
|
||||
const [isEditing, setIsEditing] = React.useState(false);
|
||||
const [editText, setEditText] = React.useState<string>(noteText);
|
||||
const textareaRef = React.useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
// Determine border and background based on state
|
||||
const getBorderStyle = () => {
|
||||
// Note nodes have no border
|
||||
if (nodeType === 'note') return 'none';
|
||||
if (isRunning) return '1px solid #22C55E';
|
||||
if (executionStatus === 'completed') return '1px solid #9ca3af';
|
||||
if (executionStatus === 'failed') return '1px solid #eb3424';
|
||||
if (selected) return '1px solid #22C55E';
|
||||
return '1px solid #e5e7eb';
|
||||
};
|
||||
|
||||
const getBackgroundColor = () => {
|
||||
// Note nodes get yellow/gold background
|
||||
if (nodeType === 'note') return '#ca8a04';
|
||||
// All nodes have white background
|
||||
return 'white';
|
||||
};
|
||||
|
||||
const getOutlineStyle = () => {
|
||||
if (isRunning) return '2px solid rgba(34, 197, 94, 0.32)';
|
||||
if (selected) return '2px solid rgba(24, 24, 27, 0.18)';
|
||||
return '2px solid transparent';
|
||||
};
|
||||
|
||||
// Note nodes have different styling
|
||||
const isNoteNode = nodeType === 'note';
|
||||
|
||||
// Determine text color based on background
|
||||
const getTextColor = () => {
|
||||
if (isNoteNode) return '#854d0e'; // Dark yellow text for note nodes
|
||||
if (nodeType === 'if-else' || nodeType === 'while') {
|
||||
return '#18181b'; // Dark text for orange background nodes
|
||||
}
|
||||
return '#18181b'; // Default dark text
|
||||
};
|
||||
|
||||
// Update editText when noteText changes (for different notes)
|
||||
React.useEffect(() => {
|
||||
if (isNoteNode) {
|
||||
setEditText(noteText);
|
||||
}
|
||||
}, [noteText, isNoteNode]);
|
||||
|
||||
// Note nodes are visual-only sticky notes with inline editing
|
||||
if (isNoteNode) {
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isEditing && textareaRef.current) {
|
||||
textareaRef.current.focus();
|
||||
textareaRef.current.select();
|
||||
}
|
||||
}, [isEditing]);
|
||||
|
||||
const handleSave = () => {
|
||||
setIsEditing(false);
|
||||
// Update the node data
|
||||
if ((data as any).onUpdate) {
|
||||
(data as any).onUpdate({ noteText: editText });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative"
|
||||
onDoubleClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsEditing(true);
|
||||
}}
|
||||
style={{
|
||||
padding: '10px',
|
||||
fontSize: '11px',
|
||||
backgroundColor: '#fef9c3', // Light yellow
|
||||
border: selected ? '2px solid #eab308' : 'none',
|
||||
outline: selected ? '2px solid rgba(234, 179, 8, 0.2)' : 'none',
|
||||
outlineOffset: 0,
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)',
|
||||
transition: 'all 0.2s ease-out',
|
||||
borderRadius: '6px',
|
||||
minWidth: '140px',
|
||||
maxWidth: '200px',
|
||||
width: 'fit-content',
|
||||
fontFamily: 'ui-sans-serif, system-ui, sans-serif',
|
||||
color: '#854d0e',
|
||||
lineHeight: '1.4',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
cursor: isEditing ? 'text' : 'move',
|
||||
}}
|
||||
>
|
||||
{/* Sticky note "tape" effect */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '-6px',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
width: '30px',
|
||||
height: '12px',
|
||||
backgroundColor: 'rgba(234, 179, 8, 0.3)',
|
||||
borderRadius: '2px',
|
||||
}}
|
||||
/>
|
||||
|
||||
{isEditing ? (
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={editText}
|
||||
onChange={(e) => setEditText(e.target.value)}
|
||||
onBlur={handleSave}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Escape') {
|
||||
setEditText(noteText);
|
||||
setIsEditing(false);
|
||||
}
|
||||
// Save on Ctrl/Cmd+Enter
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
||||
handleSave();
|
||||
}
|
||||
// Don't propagate to prevent ReactFlow shortcuts
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="nodrag"
|
||||
style={{
|
||||
width: '100%',
|
||||
minHeight: '40px',
|
||||
padding: '0',
|
||||
border: 'none',
|
||||
outline: 'none',
|
||||
backgroundColor: 'transparent',
|
||||
fontSize: '11px',
|
||||
fontFamily: 'ui-sans-serif, system-ui, sans-serif',
|
||||
color: '#854d0e',
|
||||
lineHeight: '1.4',
|
||||
resize: 'vertical',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
minHeight: '20px',
|
||||
cursor: 'move',
|
||||
}}
|
||||
>
|
||||
{noteText || 'Double-click to edit note'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative"
|
||||
style={{
|
||||
padding: '10px 16px',
|
||||
fontSize: '13px',
|
||||
backgroundColor: getBackgroundColor(),
|
||||
border: getBorderStyle(),
|
||||
outline: getOutlineStyle(),
|
||||
outlineOffset: 0,
|
||||
boxShadow: 'none',
|
||||
transition: 'outline 0.2s ease-out, border 0.2s ease-out',
|
||||
borderRadius: '16px',
|
||||
minWidth: '140px',
|
||||
maxWidth: '240px',
|
||||
width: 'fit-content',
|
||||
}}
|
||||
>
|
||||
{/* Input handle (left) - all nodes except 'start' and 'note' */}
|
||||
{nodeType !== 'start' && nodeType !== 'note' && (
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
id="input"
|
||||
style={{
|
||||
width: 10,
|
||||
height: 10,
|
||||
background: '#9ca3af',
|
||||
border: '2px solid white',
|
||||
left: -5,
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Render the label (icon + text) */}
|
||||
<div style={{ whiteSpace: 'nowrap', color: getTextColor() }}>
|
||||
{data.label as ReactNode}
|
||||
</div>
|
||||
|
||||
{/* Output handles - special cases for branching nodes */}
|
||||
{nodeType === 'if-else' ? (
|
||||
<>
|
||||
{/* If branch (left bottom) */}
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id="if"
|
||||
style={{
|
||||
width: 10,
|
||||
height: 10,
|
||||
background: '#FA5D19',
|
||||
border: '2px solid white',
|
||||
right: -5,
|
||||
top: '35%',
|
||||
transform: 'translateY(-50%)',
|
||||
}}
|
||||
/>
|
||||
{/* Else branch (right bottom) */}
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id="else"
|
||||
style={{
|
||||
width: 10,
|
||||
height: 10,
|
||||
background: '#18181b',
|
||||
border: '2px solid white',
|
||||
right: -5,
|
||||
top: '65%',
|
||||
transform: 'translateY(-50%)',
|
||||
}}
|
||||
/>
|
||||
{/* Branch labels */}
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: '35%',
|
||||
right: -50,
|
||||
transform: 'translateY(-50%)',
|
||||
fontSize: '10px',
|
||||
color: '#FA5D19',
|
||||
fontWeight: 600,
|
||||
}}>If</div>
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: '65%',
|
||||
right: -55,
|
||||
transform: 'translateY(-50%)',
|
||||
fontSize: '10px',
|
||||
color: '#18181b',
|
||||
fontWeight: 600,
|
||||
}}>Else</div>
|
||||
</>
|
||||
) : nodeType === 'user-approval' ? (
|
||||
<>
|
||||
{/* Approve branch (left bottom) */}
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id="approve"
|
||||
style={{
|
||||
width: 10,
|
||||
height: 10,
|
||||
background: '#10b981',
|
||||
border: '2px solid white',
|
||||
right: -5,
|
||||
top: '35%',
|
||||
transform: 'translateY(-50%)',
|
||||
}}
|
||||
/>
|
||||
{/* Reject branch (right bottom) */}
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id="reject"
|
||||
style={{
|
||||
width: 10,
|
||||
height: 10,
|
||||
background: '#ef4444',
|
||||
border: '2px solid white',
|
||||
right: -5,
|
||||
top: '65%',
|
||||
transform: 'translateY(-50%)',
|
||||
}}
|
||||
/>
|
||||
{/* Branch labels */}
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: '35%',
|
||||
right: -70,
|
||||
transform: 'translateY(-50%)',
|
||||
fontSize: '10px',
|
||||
color: '#10b981',
|
||||
fontWeight: 600,
|
||||
}}>Approve</div>
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: '65%',
|
||||
right: -60,
|
||||
transform: 'translateY(-50%)',
|
||||
fontSize: '10px',
|
||||
color: '#ef4444',
|
||||
fontWeight: 600,
|
||||
}}>Reject</div>
|
||||
</>
|
||||
) : nodeType === 'while' ? (
|
||||
<>
|
||||
{/* Continue branch (top) */}
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id="continue"
|
||||
style={{
|
||||
width: 10,
|
||||
height: 10,
|
||||
background: '#FA5D19',
|
||||
border: '2px solid white',
|
||||
right: -5,
|
||||
top: '35%',
|
||||
transform: 'translateY(-50%)',
|
||||
}}
|
||||
/>
|
||||
{/* Break branch (bottom) */}
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id="break"
|
||||
style={{
|
||||
width: 10,
|
||||
height: 10,
|
||||
background: '#18181b',
|
||||
border: '2px solid white',
|
||||
right: -5,
|
||||
top: '65%',
|
||||
transform: 'translateY(-50%)',
|
||||
}}
|
||||
/>
|
||||
{/* Branch labels */}
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: '35%',
|
||||
right: -70,
|
||||
transform: 'translateY(-50%)',
|
||||
fontSize: '10px',
|
||||
color: '#FA5D19',
|
||||
fontWeight: 600,
|
||||
}}>Continue</div>
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: '65%',
|
||||
right: -55,
|
||||
transform: 'translateY(-50%)',
|
||||
fontSize: '10px',
|
||||
color: '#18181b',
|
||||
fontWeight: 600,
|
||||
}}>Break</div>
|
||||
</>
|
||||
) : nodeType !== 'end' && nodeType !== 'note' ? (
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id="output"
|
||||
style={{
|
||||
width: 10,
|
||||
height: 10,
|
||||
background: '#9ca3af',
|
||||
border: '2px solid white',
|
||||
right: -5,
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)',
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Create all node types by reusing the same component
|
||||
export const nodeTypes = {
|
||||
start: CustomNode,
|
||||
agent: CustomNode,
|
||||
mcp: CustomNode,
|
||||
extract: CustomNode,
|
||||
end: CustomNode,
|
||||
note: CustomNode,
|
||||
logic: CustomNode,
|
||||
data: CustomNode,
|
||||
http: CustomNode,
|
||||
transform: CustomNode,
|
||||
'if-else': CustomNode,
|
||||
'while': CustomNode,
|
||||
'user-approval': CustomNode,
|
||||
'set-state': CustomNode,
|
||||
};
|
||||
@@ -0,0 +1,349 @@
|
||||
"use client";
|
||||
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { useState, useEffect } from "react";
|
||||
import type { Node } from "@xyflow/react";
|
||||
import { toast } from "sonner";
|
||||
import VariableReferencePicker from "./VariableReferencePicker";
|
||||
|
||||
interface DataNodePanelProps {
|
||||
node: Node | null;
|
||||
nodes: Node[];
|
||||
onClose: () => void;
|
||||
onDelete: (nodeId: string) => void;
|
||||
onUpdate: (nodeId: string, data: any) => void;
|
||||
}
|
||||
|
||||
export default function DataNodePanel({
|
||||
node,
|
||||
nodes,
|
||||
onClose,
|
||||
onDelete,
|
||||
onUpdate,
|
||||
}: DataNodePanelProps) {
|
||||
const nodeData = node?.data as any;
|
||||
const nodeType = nodeData?.nodeType?.toLowerCase() || "";
|
||||
|
||||
// Transform state
|
||||
const [transformScript, setTransformScript] = useState(
|
||||
nodeData?.transformScript ||
|
||||
`// Transform the input data using TypeScript
|
||||
// Available variables: input, lastOutput, state
|
||||
|
||||
// Example: Extract and transform data
|
||||
const result = {
|
||||
processed: true,
|
||||
timestamp: input.timestamp || "",
|
||||
data: input
|
||||
};
|
||||
|
||||
return result;`,
|
||||
);
|
||||
|
||||
// Set State variables
|
||||
const [stateKey, setStateKey] = useState(nodeData?.stateKey || "myVariable");
|
||||
const [stateValue, setStateValue] = useState(nodeData?.stateValue || "value");
|
||||
const [valueType, setValueType] = useState<
|
||||
"string" | "number" | "boolean" | "json" | "expression"
|
||||
>(nodeData?.valueType || "string");
|
||||
|
||||
// Auto-save changes
|
||||
useEffect(() => {
|
||||
if (!node?.id) return;
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
onUpdate(node.id, {
|
||||
transformScript,
|
||||
stateKey,
|
||||
stateValue,
|
||||
valueType,
|
||||
});
|
||||
}, 500);
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [
|
||||
transformScript,
|
||||
stateKey,
|
||||
stateValue,
|
||||
valueType,
|
||||
]);
|
||||
|
||||
|
||||
const renderValueInput = () => {
|
||||
switch (valueType) {
|
||||
case "boolean":
|
||||
return (
|
||||
<select
|
||||
value={stateValue}
|
||||
onChange={(e) => setStateValue(e.target.value)}
|
||||
className="w-full px-12 py-8 bg-accent-white border border-border-faint rounded-8 text-sm text-accent-black focus:outline-none focus:border-heat-100 transition-colors"
|
||||
>
|
||||
<option value="true">true</option>
|
||||
<option value="false">false</option>
|
||||
</select>
|
||||
);
|
||||
case "number":
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
value={stateValue}
|
||||
onChange={(e) => setStateValue(e.target.value)}
|
||||
placeholder="42 or {{lastOutput.count}}"
|
||||
className="w-full px-12 py-8 bg-accent-white border border-border-faint rounded-8 text-sm text-accent-black font-mono focus:outline-none focus:border-heat-100 transition-colors"
|
||||
/>
|
||||
);
|
||||
case "json":
|
||||
return (
|
||||
<textarea
|
||||
value={stateValue}
|
||||
onChange={(e) => setStateValue(e.target.value)}
|
||||
rows={4}
|
||||
placeholder='{"key": "value"} or {{lastOutput}}'
|
||||
className="w-full px-12 py-8 bg-accent-white border border-border-faint rounded-8 text-sm text-accent-black font-mono focus:outline-none focus:border-heat-100 transition-colors resize-none"
|
||||
/>
|
||||
);
|
||||
case "expression":
|
||||
return (
|
||||
<textarea
|
||||
value={stateValue}
|
||||
onChange={(e) => setStateValue(e.target.value)}
|
||||
rows={3}
|
||||
placeholder="input.price * 1.1"
|
||||
className="w-full px-12 py-8 bg-accent-white border border-border-faint rounded-8 text-sm text-accent-black font-mono focus:outline-none focus:border-heat-100 transition-colors resize-none"
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
value={stateValue}
|
||||
onChange={(e) => setStateValue(e.target.value)}
|
||||
placeholder="Hello {{input.name}}"
|
||||
className="w-full px-12 py-8 bg-accent-white border border-border-faint rounded-8 text-sm text-accent-black focus:outline-none focus:border-heat-100 transition-colors"
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{node && (
|
||||
<motion.aside
|
||||
initial={{ x: 400, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
exit={{ x: 400, opacity: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="fixed right-20 top-80 h-[calc(100vh-100px)] w-[calc(100vw-240px)] max-w-480 bg-accent-white border border-border-faint shadow-lg overflow-y-auto z-50 rounded-16"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="p-20 border-b border-border-faint">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<h2 className="text-label-large text-accent-black font-medium">
|
||||
{nodeData?.nodeName || "Data"}
|
||||
</h2>
|
||||
<div className="flex items-center gap-8">
|
||||
<button
|
||||
onClick={() => onDelete(node?.id || "")}
|
||||
className="w-32 h-32 rounded-6 hover:bg-black-alpha-4 transition-colors flex items-center justify-center group"
|
||||
title="Delete node"
|
||||
>
|
||||
<svg
|
||||
className="w-16 h-16 text-black-alpha-48 group-hover:text-black-alpha-64"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-32 h-32 rounded-6 hover:bg-black-alpha-4 transition-colors flex items-center justify-center"
|
||||
>
|
||||
<svg
|
||||
className="w-16 h-16 text-black-alpha-48"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-body-small text-black-alpha-48">
|
||||
{nodeType.includes("transform")
|
||||
? "Transform data using JavaScript"
|
||||
: nodeType.includes("state")
|
||||
? "Set workflow state variables"
|
||||
: "Configure data operation"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Form Fields */}
|
||||
<div className="p-20 space-y-24">
|
||||
{/* Transform Node - Code Editor */}
|
||||
{nodeType.includes("transform") && (
|
||||
<>
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-accent-black mb-12">
|
||||
Transform Code (TypeScript)
|
||||
</h3>
|
||||
<p className="text-sm text-black-alpha-48 mb-16">
|
||||
Write TypeScript code to transform data. Runs securely in E2B sandbox.
|
||||
</p>
|
||||
|
||||
{/* Code Editor */}
|
||||
<div className="mb-16">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<label className="block text-sm text-accent-black">
|
||||
TypeScript Code
|
||||
</label>
|
||||
<VariableReferencePicker
|
||||
nodes={nodes}
|
||||
currentNodeId={node?.id || ''}
|
||||
onSelect={(varPath) => {
|
||||
setTransformScript(prev => prev + `\n// Access: ${varPath}\n`);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<textarea
|
||||
value={transformScript}
|
||||
onChange={(e) => setTransformScript(e.target.value)}
|
||||
rows={20}
|
||||
className="w-full px-12 py-10 bg-[#1e1e1e] text-[#d4d4d4] border border-border-faint rounded-8 text-sm font-mono focus:outline-none focus:border-heat-100 transition-colors resize-none"
|
||||
placeholder="// Transform the input data using TypeScript"
|
||||
spellCheck={false}
|
||||
/>
|
||||
<div className="mt-8 text-xs text-black-alpha-48 space-y-4">
|
||||
<p>Available variables:</p>
|
||||
<ul className="list-disc list-inside space-y-2 ml-8">
|
||||
<li><code className="px-4 py-1 bg-background-base rounded text-heat-100 font-mono">input</code> - Current input data</li>
|
||||
<li><code className="px-4 py-1 bg-background-base rounded text-heat-100 font-mono">lastOutput</code> - Output from previous node</li>
|
||||
<li><code className="px-4 py-1 bg-background-base rounded text-heat-100 font-mono">state</code> - Workflow state with variables</li>
|
||||
</ul>
|
||||
<p className="mt-8">Your function should return an object with the transformed data.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Set State Node - Separate from Transform */}
|
||||
{nodeType.includes("state") && !nodeType.includes("transform") && (
|
||||
<>
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-accent-black mb-12">
|
||||
Set global variables
|
||||
</h3>
|
||||
<p className="text-sm text-black-alpha-48 mb-16">
|
||||
Assign values to workflow's state variables
|
||||
</p>
|
||||
|
||||
{/* State Assignments */}
|
||||
<div className="space-y-12">
|
||||
<div className="p-12 bg-background-base rounded-10 border border-border-faint">
|
||||
<div className="space-y-12">
|
||||
{/* Variable Name */}
|
||||
<div>
|
||||
<label className="block text-sm text-accent-black mb-6">
|
||||
Variable Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={stateKey}
|
||||
onChange={(e) => setStateKey(e.target.value)}
|
||||
placeholder="myVariable"
|
||||
className="w-full px-12 py-8 bg-accent-white border border-border-faint rounded-8 text-sm text-accent-black font-mono focus:outline-none focus:border-heat-100"
|
||||
/>
|
||||
<p className="text-xs text-black-alpha-48 mt-4">
|
||||
Access later with <code className="px-4 py-1 bg-background-base rounded text-heat-100 font-mono text-xs">{`{{state.${stateKey}}}`}</code>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Value Type */}
|
||||
<div>
|
||||
<label className="block text-sm text-accent-black mb-6">
|
||||
Value Type
|
||||
</label>
|
||||
<select
|
||||
value={valueType}
|
||||
onChange={(e) => setValueType(e.target.value as any)}
|
||||
className="w-full px-12 py-8 bg-accent-white border border-border-faint rounded-8 text-sm text-accent-black focus:outline-none focus:border-heat-100 transition-colors appearance-none cursor-pointer"
|
||||
>
|
||||
<option value="string">String</option>
|
||||
<option value="number">Number</option>
|
||||
<option value="boolean">Boolean</option>
|
||||
<option value="json">JSON Object</option>
|
||||
<option value="expression">JavaScript Expression</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Value Input */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<label className="block text-sm text-accent-black">
|
||||
Value
|
||||
</label>
|
||||
<VariableReferencePicker
|
||||
nodes={nodes}
|
||||
currentNodeId={node?.id || ''}
|
||||
onSelect={(varPath) => setStateValue(prev => prev + `{{${varPath}}}`)}
|
||||
/>
|
||||
</div>
|
||||
{renderValueInput()}
|
||||
<p className="text-xs text-black-alpha-48 mt-4">
|
||||
{valueType === 'string' && 'Use {{variables}} to reference other data'}
|
||||
{valueType === 'number' && 'Can use {{lastOutput.price}} to reference numbers'}
|
||||
{valueType === 'boolean' && 'true or false'}
|
||||
{valueType === 'json' && 'Valid JSON object or array'}
|
||||
{valueType === 'expression' && 'JavaScript expression like: input.x + lastOutput.y'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Add Assignment Button */}
|
||||
<button
|
||||
onClick={() => {
|
||||
toast.info('Multiple state assignments coming soon!', {
|
||||
description: 'Currently you can set one variable per node. Add another Set State node for more variables.'
|
||||
});
|
||||
}}
|
||||
className="px-12 py-8 bg-background-base hover:bg-black-alpha-4 border border-border-faint rounded-8 text-sm text-accent-black transition-colors flex items-center gap-6"
|
||||
>
|
||||
<svg
|
||||
className="w-14 h-14"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 4v16m8-8H4"
|
||||
/>
|
||||
</svg>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</motion.aside>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
+752
@@ -0,0 +1,752 @@
|
||||
"use client";
|
||||
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { useState, useEffect } from "react";
|
||||
import type { Node } from "@xyflow/react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface DataNodePanelProps {
|
||||
node: Node | null;
|
||||
onClose: () => void;
|
||||
onDelete: (nodeId: string) => void;
|
||||
onUpdate: (nodeId: string, data: any) => void;
|
||||
}
|
||||
|
||||
export default function DataNodePanel({
|
||||
node,
|
||||
onClose,
|
||||
onDelete,
|
||||
onUpdate,
|
||||
}: DataNodePanelProps) {
|
||||
const nodeData = node?.data as any;
|
||||
const nodeType = nodeData?.nodeType?.toLowerCase() || "";
|
||||
|
||||
// Transform state
|
||||
const [transformScript, setTransformScript] = useState(
|
||||
nodeData?.transformScript ||
|
||||
`// Transform the input data
|
||||
return {
|
||||
...input,
|
||||
processed: true,
|
||||
timestamp: new Date().toISOString()
|
||||
};`,
|
||||
);
|
||||
|
||||
// Transform mode (Expressions vs Object vs Advanced)
|
||||
const [transformMode, setTransformMode] = useState<
|
||||
"expressions" | "object" | "advanced"
|
||||
>(nodeData?.transformMode || "expressions");
|
||||
const [expressionPairs, setExpressionPairs] = useState<
|
||||
Array<{ key: string; value: string }>
|
||||
>(nodeData?.expressionPairs || [{ key: "", value: "" }]);
|
||||
|
||||
// JSON Schema Builder state (for Object mode)
|
||||
const [schemaMode, setSchemaMode] = useState<"simple" | "advanced">(
|
||||
nodeData?.schemaMode || "simple",
|
||||
);
|
||||
const [schemaName, setSchemaName] = useState(
|
||||
nodeData?.schemaName || "blog_post_details",
|
||||
);
|
||||
const [schemaFields, setSchemaFields] = useState<
|
||||
Array<{ name: string; type: string; value?: string; description?: string }>
|
||||
>(
|
||||
nodeData?.schemaFields || [
|
||||
{
|
||||
name: "title",
|
||||
type: "STR",
|
||||
value: "",
|
||||
description: "The main heading of the blog post",
|
||||
},
|
||||
{
|
||||
name: "author",
|
||||
type: "STR",
|
||||
value: "",
|
||||
description: "The full name of the post's author",
|
||||
},
|
||||
{
|
||||
name: "date_published",
|
||||
type: "STR",
|
||||
value: "",
|
||||
description: "Date the post was published (YYYY-MM-DD)",
|
||||
},
|
||||
{
|
||||
name: "content",
|
||||
type: "STR",
|
||||
value: "",
|
||||
description: "The full content/body of the blog post",
|
||||
},
|
||||
{
|
||||
name: "tags",
|
||||
type: "ARR",
|
||||
value: "",
|
||||
description: "A list of tag strings associated with the blog post",
|
||||
},
|
||||
],
|
||||
);
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
const [generatedSchema, setGeneratedSchema] = useState("");
|
||||
|
||||
// Set State variables
|
||||
const [stateKey, setStateKey] = useState(nodeData?.stateKey || "myVariable");
|
||||
const [stateValue, setStateValue] = useState(nodeData?.stateValue || "value");
|
||||
const [valueType, setValueType] = useState<
|
||||
"string" | "number" | "boolean" | "json" | "expression"
|
||||
>(nodeData?.valueType || "string");
|
||||
|
||||
// Auto-save changes
|
||||
useEffect(() => {
|
||||
if (!node?.id) return;
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
onUpdate(node.id, {
|
||||
transformScript,
|
||||
transformMode,
|
||||
expressionPairs,
|
||||
stateKey,
|
||||
stateValue,
|
||||
valueType,
|
||||
schemaName,
|
||||
schemaFields,
|
||||
schemaMode,
|
||||
});
|
||||
}, 500);
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [
|
||||
transformScript,
|
||||
transformMode,
|
||||
expressionPairs,
|
||||
stateKey,
|
||||
stateValue,
|
||||
valueType,
|
||||
schemaName,
|
||||
schemaFields,
|
||||
schemaMode,
|
||||
]);
|
||||
|
||||
// Generate schema using AI
|
||||
const handleGenerateSchema = async () => {
|
||||
setIsGenerating(true);
|
||||
try {
|
||||
// Build schema from fields
|
||||
const properties: any = {};
|
||||
schemaFields.forEach((field) => {
|
||||
if (field.name) {
|
||||
const typeMap: any = {
|
||||
STR: "string",
|
||||
NUM: "number",
|
||||
BOOL: "boolean",
|
||||
ARR: "array",
|
||||
OBJ: "object",
|
||||
};
|
||||
|
||||
const fieldDef: any = {
|
||||
type: typeMap[field.type] || "string",
|
||||
};
|
||||
|
||||
if (field.description) {
|
||||
fieldDef.description = field.description;
|
||||
}
|
||||
|
||||
if (field.value) {
|
||||
fieldDef.default = field.value;
|
||||
}
|
||||
|
||||
// Handle array type
|
||||
if (field.type === "ARR") {
|
||||
fieldDef.items = { type: "string" };
|
||||
}
|
||||
|
||||
properties[field.name] = fieldDef;
|
||||
}
|
||||
});
|
||||
|
||||
const schema = {
|
||||
type: "object",
|
||||
properties,
|
||||
additionalProperties: false,
|
||||
required: schemaFields.filter((f) => f.name).map((f) => f.name),
|
||||
};
|
||||
|
||||
const schemaJson = JSON.stringify(schema, null, 2);
|
||||
setGeneratedSchema(schemaJson);
|
||||
|
||||
// Update transform script to use this schema
|
||||
const newScript = `// Extract data matching the schema
|
||||
const result = ${schemaJson};
|
||||
|
||||
// TODO: Map input data to schema fields
|
||||
return {
|
||||
${schemaFields.map((f) => ` ${f.name}: input.${f.name} || ${f.value ? `"${f.value}"` : "null"}`).join(",\n")}
|
||||
};`;
|
||||
|
||||
setTransformScript(newScript);
|
||||
toast.success("Schema generated!", {
|
||||
description: "Transform script updated with schema structure",
|
||||
});
|
||||
} catch (error) {
|
||||
toast.error("Failed to generate schema", {
|
||||
description: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
} finally {
|
||||
setIsGenerating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderValueInput = () => {
|
||||
switch (valueType) {
|
||||
case "boolean":
|
||||
return (
|
||||
<select
|
||||
value={stateValue}
|
||||
onChange={(e) => setStateValue(e.target.value)}
|
||||
className="w-full px-12 py-8 bg-accent-white border border-border-faint rounded-8 text-sm text-accent-black focus:outline-none focus:border-heat-100 transition-colors"
|
||||
>
|
||||
<option value="true">true</option>
|
||||
<option value="false">false</option>
|
||||
</select>
|
||||
);
|
||||
case "number":
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
value={stateValue}
|
||||
onChange={(e) => setStateValue(e.target.value)}
|
||||
placeholder="42 or {{lastOutput.count}}"
|
||||
className="w-full px-12 py-8 bg-accent-white border border-border-faint rounded-8 text-sm text-accent-black font-mono focus:outline-none focus:border-heat-100 transition-colors"
|
||||
/>
|
||||
);
|
||||
case "json":
|
||||
return (
|
||||
<textarea
|
||||
value={stateValue}
|
||||
onChange={(e) => setStateValue(e.target.value)}
|
||||
rows={4}
|
||||
placeholder='{"key": "value"} or {{lastOutput}}'
|
||||
className="w-full px-12 py-8 bg-accent-white border border-border-faint rounded-8 text-sm text-accent-black font-mono focus:outline-none focus:border-heat-100 transition-colors resize-none"
|
||||
/>
|
||||
);
|
||||
case "expression":
|
||||
return (
|
||||
<textarea
|
||||
value={stateValue}
|
||||
onChange={(e) => setStateValue(e.target.value)}
|
||||
rows={3}
|
||||
placeholder="input.price * 1.1"
|
||||
className="w-full px-12 py-8 bg-accent-white border border-border-faint rounded-8 text-sm text-accent-black font-mono focus:outline-none focus:border-heat-100 transition-colors resize-none"
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
value={stateValue}
|
||||
onChange={(e) => setStateValue(e.target.value)}
|
||||
placeholder="Hello {{input.name}}"
|
||||
className="w-full px-12 py-8 bg-accent-white border border-border-faint rounded-8 text-sm text-accent-black focus:outline-none focus:border-heat-100 transition-colors"
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{node && (
|
||||
<motion.aside
|
||||
initial={{ x: 400, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
exit={{ x: 400, opacity: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="fixed right-20 top-80 h-[calc(100vh-100px)] w-[calc(100vw-240px)] max-w-480 bg-accent-white border border-border-faint shadow-lg overflow-y-auto z-50 rounded-16"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="p-20 border-b border-border-faint">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<h2 className="text-label-large text-accent-black font-medium">
|
||||
{nodeData?.nodeName || "Data"}
|
||||
</h2>
|
||||
<div className="flex items-center gap-8">
|
||||
<button
|
||||
onClick={() => onDelete(node?.id || "")}
|
||||
className="w-32 h-32 rounded-6 hover:bg-black-alpha-4 transition-colors flex items-center justify-center group"
|
||||
title="Delete node"
|
||||
>
|
||||
<svg
|
||||
className="w-16 h-16 text-black-alpha-48 group-hover:text-black-alpha-64"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-32 h-32 rounded-6 hover:bg-black-alpha-4 transition-colors flex items-center justify-center"
|
||||
>
|
||||
<svg
|
||||
className="w-16 h-16 text-black-alpha-48"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-body-small text-black-alpha-48">
|
||||
{nodeType.includes("transform")
|
||||
? "Transform data using JavaScript"
|
||||
: nodeType.includes("state")
|
||||
? "Set workflow state variables"
|
||||
: "Configure data operation"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Form Fields */}
|
||||
<div className="p-20 space-y-24">
|
||||
{/* Transform Node - Multiple Modes */}
|
||||
{nodeType.includes("transform") && (
|
||||
<>
|
||||
{/* Mode Selector */}
|
||||
<div className="flex gap-4 mb-16">
|
||||
<button
|
||||
onClick={() => setTransformMode("expressions")}
|
||||
className={`flex-1 px-12 py-8 rounded-8 text-sm font-medium transition-colors ${
|
||||
transformMode === "expressions"
|
||||
? "bg-accent-black text-white"
|
||||
: "bg-background-base text-accent-black border border-border-faint hover:bg-black-alpha-4"
|
||||
}`}
|
||||
>
|
||||
Expressions
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setTransformMode("object")}
|
||||
className={`flex-1 px-12 py-8 rounded-8 text-sm font-medium transition-colors ${
|
||||
transformMode === "object"
|
||||
? "bg-accent-black text-white"
|
||||
: "bg-background-base text-accent-black border border-border-faint hover:bg-black-alpha-4"
|
||||
}`}
|
||||
>
|
||||
Object
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-black-alpha-48 mb-16">
|
||||
{transformMode === "expressions"
|
||||
? "Reshape data using key-value expressions"
|
||||
: "Build a JSON object schema"}
|
||||
</p>
|
||||
|
||||
{transformMode === "expressions" ? (
|
||||
<>
|
||||
{/* Expression Mode - Key/Value Pairs */}
|
||||
<div className="space-y-10">
|
||||
{expressionPairs.map((pair, index) => (
|
||||
<div key={index} className="flex items-center gap-8">
|
||||
<input
|
||||
type="text"
|
||||
value={pair.key}
|
||||
onChange={(e) => {
|
||||
const updated = [...expressionPairs];
|
||||
updated[index].key = e.target.value;
|
||||
setExpressionPairs(updated);
|
||||
}}
|
||||
placeholder="Key"
|
||||
className="flex-1 px-12 py-8 bg-background-base border border-border-faint rounded-8 text-sm text-accent-black font-mono focus:outline-none focus:border-heat-100"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={pair.value}
|
||||
onChange={(e) => {
|
||||
const updated = [...expressionPairs];
|
||||
updated[index].value = e.target.value;
|
||||
setExpressionPairs(updated);
|
||||
}}
|
||||
placeholder="input.foo + 1"
|
||||
className="flex-1 px-12 py-8 bg-background-base border border-border-faint rounded-8 text-sm text-accent-black font-mono focus:outline-none focus:border-heat-100"
|
||||
/>
|
||||
<button
|
||||
onClick={() =>
|
||||
setExpressionPairs(
|
||||
expressionPairs.filter((_, i) => i !== index),
|
||||
)
|
||||
}
|
||||
className="w-28 h-28 rounded-6 hover:bg-black-alpha-4 flex items-center justify-center"
|
||||
>
|
||||
<svg
|
||||
className="w-14 h-14 text-black-alpha-48"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
onClick={() =>
|
||||
setExpressionPairs([
|
||||
...expressionPairs,
|
||||
{ key: "", value: "" },
|
||||
])
|
||||
}
|
||||
className="w-full px-12 py-8 bg-background-base hover:bg-black-alpha-4 border border-border-faint rounded-8 text-sm text-accent-black transition-colors flex items-center justify-center gap-6"
|
||||
>
|
||||
<svg
|
||||
className="w-14 h-14"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 4v16m8-8H4"
|
||||
/>
|
||||
</svg>
|
||||
Add
|
||||
</button>
|
||||
<p className="text-xs text-black-alpha-48 mt-8">
|
||||
Use Common Expression Language.{" "}
|
||||
<a href="#" className="text-heat-100">
|
||||
Learn more
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
) : transformMode === "object" ? (
|
||||
<>
|
||||
{/* Object Mode - JSON Schema Builder */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-12">
|
||||
<h3 className="text-sm font-medium text-accent-black">
|
||||
Structured output (JSON)
|
||||
</h3>
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
onClick={() => setSchemaMode("simple")}
|
||||
className={`px-12 py-6 rounded-8 text-xs font-medium transition-colors ${
|
||||
schemaMode === "simple"
|
||||
? "bg-accent-black text-white"
|
||||
: "bg-background-base text-accent-black border border-border-faint hover:bg-black-alpha-4"
|
||||
}`}
|
||||
>
|
||||
Simple
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSchemaMode("advanced")}
|
||||
className={`px-12 py-6 rounded-8 text-xs font-medium transition-colors ${
|
||||
schemaMode === "advanced"
|
||||
? "bg-accent-black text-white"
|
||||
: "bg-background-base text-accent-black border border-border-faint hover:bg-black-alpha-4"
|
||||
}`}
|
||||
>
|
||||
Advanced
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-black-alpha-48 mb-16">
|
||||
The model will generate a JSON object that matches this
|
||||
schema.
|
||||
</p>
|
||||
|
||||
{schemaMode === "simple" ? (
|
||||
<>
|
||||
{/* Schema Name */}
|
||||
<div className="mb-16">
|
||||
<label className="block text-sm font-medium text-accent-black mb-8">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={schemaName}
|
||||
onChange={(e) => setSchemaName(e.target.value)}
|
||||
className="w-full px-14 py-10 bg-background-base border border-border-faint rounded-10 text-sm text-accent-black font-mono focus:outline-none focus:border-heat-100"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Properties */}
|
||||
<div className="mb-16">
|
||||
<div className="grid grid-cols-[100px_80px_1fr_80px_40px] gap-8 mb-10 text-xs text-black-alpha-48 font-medium">
|
||||
<div>Name</div>
|
||||
<div>Type</div>
|
||||
<div>Value</div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div className="space-y-8">
|
||||
{schemaFields.map((field, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="grid grid-cols-[100px_80px_1fr_80px_40px] gap-8 items-center"
|
||||
>
|
||||
<svg
|
||||
className="w-16 h-16 text-teal-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
<input
|
||||
type="text"
|
||||
value={field.name}
|
||||
onChange={(e) => {
|
||||
const updated = [...schemaFields];
|
||||
updated[index].name = e.target.value;
|
||||
setSchemaFields(updated);
|
||||
}}
|
||||
className="px-10 py-6 bg-background-base border border-border-faint rounded-6 text-sm text-accent-black font-mono focus:outline-none focus:border-heat-100"
|
||||
/>
|
||||
<select
|
||||
value={field.type}
|
||||
onChange={(e) => {
|
||||
const updated = [...schemaFields];
|
||||
updated[index].type = e.target.value;
|
||||
setSchemaFields(updated);
|
||||
}}
|
||||
className="px-8 py-6 bg-accent-white border border-border-faint rounded-6 text-xs text-accent-black focus:outline-none focus:border-heat-100"
|
||||
>
|
||||
<option value="STR">STR</option>
|
||||
<option value="NUM">NUM</option>
|
||||
<option value="BOOL">BOOL</option>
|
||||
<option value="ARR">ARR</option>
|
||||
<option value="OBJ">OBJ</option>
|
||||
</select>
|
||||
<input
|
||||
type="text"
|
||||
value={field.value || ""}
|
||||
onChange={(e) => {
|
||||
const updated = [...schemaFields];
|
||||
updated[index].value = e.target.value;
|
||||
setSchemaFields(updated);
|
||||
}}
|
||||
placeholder="Enter value..."
|
||||
className="px-10 py-6 bg-background-base border border-border-faint rounded-6 text-sm text-accent-black focus:outline-none focus:border-heat-100"
|
||||
/>
|
||||
<button className="px-10 py-6 bg-background-base hover:bg-black-alpha-4 border border-border-faint rounded-6 text-xs text-accent-black">
|
||||
Enter
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setSchemaFields(
|
||||
schemaFields.filter(
|
||||
(_, i) => i !== index,
|
||||
),
|
||||
);
|
||||
}}
|
||||
className="w-32 h-32 rounded-6 hover:bg-black-alpha-4 flex items-center justify-center"
|
||||
>
|
||||
<svg
|
||||
className="w-16 h-16 text-black-alpha-48"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Add Field Button */}
|
||||
<button
|
||||
onClick={() => {
|
||||
setSchemaFields([
|
||||
...schemaFields,
|
||||
{ name: "", type: "STR", value: "" },
|
||||
]);
|
||||
}}
|
||||
className="mt-12 px-16 py-8 bg-background-base hover:bg-black-alpha-4 border border-border-faint rounded-8 text-sm text-accent-black transition-colors flex items-center gap-6"
|
||||
>
|
||||
<svg
|
||||
className="w-14 h-14"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 4v16m8-8H4"
|
||||
/>
|
||||
</svg>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Generate Button */}
|
||||
<button
|
||||
onClick={handleGenerateSchema}
|
||||
disabled={isGenerating}
|
||||
className="w-full px-20 py-12 bg-heat-100 hover:bg-heat-200 text-white rounded-10 text-sm font-medium transition-all active:scale-[0.98] flex items-center justify-center gap-8 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isGenerating ? (
|
||||
<>
|
||||
<svg
|
||||
className="w-16 h-16 animate-spin"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
||||
/>
|
||||
</svg>
|
||||
Generating...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg
|
||||
className="w-16 h-16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M13 10V3L4 14h7v7l9-11h-7z"
|
||||
/>
|
||||
</svg>
|
||||
Generate
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Set State Node - Separate from Transform */}
|
||||
{nodeType.includes("state") && !nodeType.includes("transform") && (
|
||||
<>
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-accent-black mb-12">
|
||||
Set global variables
|
||||
</h3>
|
||||
<p className="text-sm text-black-alpha-48 mb-16">
|
||||
Assign values to workflow's state variables
|
||||
</p>
|
||||
|
||||
{/* State Assignments */}
|
||||
<div className="space-y-12">
|
||||
<div className="p-12 bg-background-base rounded-10 border border-border-faint">
|
||||
<div className="space-y-12">
|
||||
{/* Variable Name */}
|
||||
<div>
|
||||
<label className="block text-sm text-accent-black mb-6">
|
||||
Variable Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={stateKey}
|
||||
onChange={(e) => setStateKey(e.target.value)}
|
||||
placeholder="myVariable"
|
||||
className="w-full px-12 py-8 bg-accent-white border border-border-faint rounded-8 text-sm text-accent-black font-mono focus:outline-none focus:border-heat-100"
|
||||
/>
|
||||
<p className="text-xs text-black-alpha-48 mt-4">
|
||||
Access later with <code className="px-4 py-1 bg-background-base rounded text-heat-100 font-mono text-xs">{`{{state.${stateKey}}}`}</code>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Value Type */}
|
||||
<div>
|
||||
<label className="block text-sm text-accent-black mb-6">
|
||||
Value Type
|
||||
</label>
|
||||
<select
|
||||
value={valueType}
|
||||
onChange={(e) => setValueType(e.target.value as any)}
|
||||
className="w-full px-12 py-8 bg-accent-white border border-border-faint rounded-8 text-sm text-accent-black focus:outline-none focus:border-heat-100 transition-colors appearance-none cursor-pointer"
|
||||
>
|
||||
<option value="string">String</option>
|
||||
<option value="number">Number</option>
|
||||
<option value="boolean">Boolean</option>
|
||||
<option value="json">JSON Object</option>
|
||||
<option value="expression">JavaScript Expression</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Value Input */}
|
||||
<div>
|
||||
<label className="block text-sm text-accent-black mb-6">
|
||||
Value
|
||||
</label>
|
||||
{renderValueInput()}
|
||||
<p className="text-xs text-black-alpha-48 mt-4">
|
||||
{valueType === 'string' && 'Use {{variables}} to reference other data'}
|
||||
{valueType === 'number' && 'Can use {{lastOutput.price}} to reference numbers'}
|
||||
{valueType === 'boolean' && 'true or false'}
|
||||
{valueType === 'json' && 'Valid JSON object or array'}
|
||||
{valueType === 'expression' && 'JavaScript expression like: input.x + lastOutput.y'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Add Assignment Button */}
|
||||
<button className="px-12 py-8 bg-background-base hover:bg-black-alpha-4 border border-border-faint rounded-8 text-sm text-accent-black transition-colors flex items-center gap-6">
|
||||
<svg
|
||||
className="w-14 h-14"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 4v16m8-8H4"
|
||||
/>
|
||||
</svg>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</motion.aside>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
"use client";
|
||||
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { useState } from "react";
|
||||
import type { Edge } from "@xyflow/react";
|
||||
|
||||
interface EdgeLabelModalProps {
|
||||
edge: Edge | null;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (edgeId: string, label: string) => void;
|
||||
}
|
||||
|
||||
export default function EdgeLabelModal({ edge, isOpen, onClose, onSave }: EdgeLabelModalProps) {
|
||||
const [label, setLabel] = useState(edge?.label as string || '');
|
||||
const [labelType, setLabelType] = useState<'custom' | 'true' | 'false' | 'none'>(
|
||||
edge?.label === 'true' ? 'true' :
|
||||
edge?.label === 'false' ? 'false' :
|
||||
edge?.label ? 'custom' : 'none'
|
||||
);
|
||||
|
||||
const handleSave = () => {
|
||||
if (!edge) return;
|
||||
|
||||
let finalLabel = '';
|
||||
if (labelType === 'true') finalLabel = 'true';
|
||||
else if (labelType === 'false') finalLabel = 'false';
|
||||
else if (labelType === 'custom') finalLabel = label;
|
||||
|
||||
onSave(edge.id, finalLabel);
|
||||
onClose();
|
||||
};
|
||||
|
||||
if (!edge) return null;
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 bg-black-alpha-48 z-[200] flex items-center justify-center p-20"
|
||||
onClick={onClose}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ scale: 0.95, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
exit={{ scale: 0.95, opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="bg-accent-white rounded-16 shadow-2xl max-w-400 w-full"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="p-20 border-b border-border-faint">
|
||||
<h2 className="text-title-h4 text-accent-black">Edit Connection</h2>
|
||||
<p className="text-body-small text-black-alpha-48 mt-4">
|
||||
Add a label to this connection
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-20 space-y-16">
|
||||
{/* Label Type */}
|
||||
<div>
|
||||
<label className="block text-label-small text-black-alpha-48 mb-8">
|
||||
Label Type
|
||||
</label>
|
||||
<div className="space-y-8">
|
||||
<button
|
||||
onClick={() => setLabelType('none')}
|
||||
className={`w-full p-12 rounded-8 border-2 transition-all text-left ${
|
||||
labelType === 'none'
|
||||
? 'border-heat-100 bg-heat-4'
|
||||
: 'border-border-faint bg-background-base hover:border-border-light'
|
||||
}`}
|
||||
>
|
||||
<p className="text-body-small text-accent-black font-medium">No Label</p>
|
||||
<p className="text-body-small text-black-alpha-48 mt-4">Standard connection</p>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setLabelType('true')}
|
||||
className={`w-full p-12 rounded-8 border-2 transition-all text-left ${
|
||||
labelType === 'true'
|
||||
? 'border-heat-100 bg-heat-4'
|
||||
: 'border-border-faint bg-background-base hover:border-border-light'
|
||||
}`}
|
||||
>
|
||||
<p className="text-body-small text-accent-black font-medium">True Branch</p>
|
||||
<p className="text-body-small text-black-alpha-48 mt-4">For If/Else true condition</p>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setLabelType('false')}
|
||||
className={`w-full p-12 rounded-8 border-2 transition-all text-left ${
|
||||
labelType === 'false'
|
||||
? 'border-heat-100 bg-heat-4'
|
||||
: 'border-border-faint bg-background-base hover:border-border-light'
|
||||
}`}
|
||||
>
|
||||
<p className="text-body-small text-accent-black font-medium">False Branch</p>
|
||||
<p className="text-body-small text-black-alpha-48 mt-4">For If/Else false condition</p>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setLabelType('custom')}
|
||||
className={`w-full p-12 rounded-8 border-2 transition-all text-left ${
|
||||
labelType === 'custom'
|
||||
? 'border-heat-100 bg-heat-4'
|
||||
: 'border-border-faint bg-background-base hover:border-border-light'
|
||||
}`}
|
||||
>
|
||||
<p className="text-body-small text-accent-black font-medium">Custom Label</p>
|
||||
<p className="text-body-small text-black-alpha-48 mt-4">Your own text</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Custom Label Input */}
|
||||
{labelType === 'custom' && (
|
||||
<div>
|
||||
<label className="block text-label-small text-black-alpha-48 mb-8">
|
||||
Label Text
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={label}
|
||||
onChange={(e) => setLabel(e.target.value)}
|
||||
placeholder="Enter label text"
|
||||
className="w-full px-12 py-8 bg-background-base border border-border-faint rounded-8 text-body-medium text-accent-black focus:outline-none focus:border-heat-100 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="p-20 border-t border-border-faint flex items-center justify-end gap-12">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-20 py-10 text-body-medium text-black-alpha-48 hover:text-accent-black transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className="px-24 py-10 bg-heat-100 hover:bg-heat-200 text-white rounded-8 transition-all active:scale-[0.98] text-body-medium font-medium"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
+1289
File diff suppressed because it is too large
Load Diff
+47
@@ -0,0 +1,47 @@
|
||||
'use client';
|
||||
|
||||
import { Download } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface ExportLangGraphButtonProps {
|
||||
workflowId: string;
|
||||
workflowName: string;
|
||||
}
|
||||
|
||||
export function ExportLangGraphButton({ workflowId, workflowName }: ExportLangGraphButtonProps) {
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/workflows/${workflowId}/export-langgraph`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Export failed');
|
||||
}
|
||||
|
||||
// Download the JSON file
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${workflowName.replace(/\s+/g, '_')}_langgraph.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
|
||||
toast.success('Workflow exported as LangGraph JSON');
|
||||
} catch (error) {
|
||||
console.error('Export error:', error);
|
||||
toast.error('Failed to export workflow');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleExport}
|
||||
className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
Export as LangGraph
|
||||
</button>
|
||||
);
|
||||
}
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
"use client";
|
||||
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
interface ExtractNodePanelProps {
|
||||
nodeData: any;
|
||||
onUpdate: (nodeId: string, updates: any) => void;
|
||||
onClose: () => void;
|
||||
onAddMCP: () => void;
|
||||
}
|
||||
|
||||
export default function ExtractNodePanel({
|
||||
nodeData,
|
||||
onUpdate,
|
||||
onClose,
|
||||
onAddMCP,
|
||||
}: ExtractNodePanelProps) {
|
||||
const [instructions, setInstructions] = useState(nodeData?.instructions || 'Extract information from the input');
|
||||
const [model, setModel] = useState(nodeData?.model || 'gpt-4o');
|
||||
const [customModel, setCustomModel] = useState('');
|
||||
const [jsonSchema, setJsonSchema] = useState(
|
||||
nodeData?.jsonSchema || JSON.stringify({
|
||||
type: "object",
|
||||
properties: {
|
||||
title: { type: "string", description: "The title" },
|
||||
summary: { type: "string", description: "A brief summary" },
|
||||
},
|
||||
required: ["title"]
|
||||
}, null, 2)
|
||||
);
|
||||
const [schemaError, setSchemaError] = useState('');
|
||||
|
||||
// Validate JSON schema
|
||||
useEffect(() => {
|
||||
try {
|
||||
JSON.parse(jsonSchema);
|
||||
setSchemaError('');
|
||||
} catch (e) {
|
||||
setSchemaError('Invalid JSON');
|
||||
}
|
||||
}, [jsonSchema]);
|
||||
|
||||
useEffect(() => {
|
||||
onUpdate(nodeData?.id, {
|
||||
instructions,
|
||||
model,
|
||||
jsonSchema,
|
||||
nodeType: 'extract',
|
||||
});
|
||||
}, [instructions, model, jsonSchema, nodeData?.id, onUpdate]);
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
<motion.aside
|
||||
initial={{ x: 400, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
exit={{ x: 400, opacity: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="fixed right-20 top-80 h-[calc(100vh-100px)] w-[calc(100vw-240px)] max-w-480 bg-accent-white border border-border-faint shadow-lg overflow-hidden z-50 rounded-16 flex flex-col"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="p-20 border-b border-border-faint flex-shrink-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-title-h3 text-accent-black">Extract (Schema)</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-32 h-32 rounded-6 hover:bg-black-alpha-4 transition-colors flex items-center justify-center"
|
||||
>
|
||||
<svg className="w-16 h-16 text-black-alpha-48" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-body-small text-black-alpha-48 mt-4">
|
||||
Use LLM to extract structured data with a JSON schema
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-20 space-y-24">
|
||||
{/* Instructions */}
|
||||
<div>
|
||||
<label className="block text-label-small text-black-alpha-48 mb-8">
|
||||
Extraction Instructions
|
||||
</label>
|
||||
<textarea
|
||||
value={instructions}
|
||||
onChange={(e) => setInstructions(e.target.value)}
|
||||
placeholder="What information should be extracted?"
|
||||
rows={4}
|
||||
className="w-full px-12 py-10 bg-background-base border border-border-faint rounded-8 text-body-medium text-accent-black focus:outline-none focus:border-heat-100 transition-colors resize-none"
|
||||
/>
|
||||
<p className="text-body-small text-black-alpha-32 mt-6">
|
||||
The LLM will extract data matching the schema below
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Model Selection */}
|
||||
<div>
|
||||
<label className="block text-label-small text-black-alpha-48 mb-8">
|
||||
Model
|
||||
</label>
|
||||
<select
|
||||
value={model}
|
||||
onChange={(e) => setModel(e.target.value)}
|
||||
className="w-full px-12 py-10 bg-background-base border border-border-faint rounded-8 text-body-medium text-accent-black focus:outline-none focus:border-heat-100 transition-colors"
|
||||
>
|
||||
<optgroup label="Anthropic">
|
||||
<option value="anthropic/claude-sonnet-4-5-20250929">Claude Sonnet 4.5</option>
|
||||
<option value="anthropic/claude-haiku-4-5-20251001">Claude Haiku 4.5</option>
|
||||
</optgroup>
|
||||
<optgroup label="OpenAI">
|
||||
<option value="gpt-4o">GPT-5</option>
|
||||
<option value="gpt-4o-mini">GPT-5 Mini</option>
|
||||
</optgroup>
|
||||
<optgroup label="Groq">
|
||||
<option value="groq/openai/gpt-oss-120b">GPT OSS 120B</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* JSON Schema */}
|
||||
<div>
|
||||
<label className="block text-label-small text-black-alpha-48 mb-8">
|
||||
Output Schema (JSON Schema)
|
||||
</label>
|
||||
<textarea
|
||||
value={jsonSchema}
|
||||
onChange={(e) => setJsonSchema(e.target.value)}
|
||||
rows={12}
|
||||
className={`w-full px-12 py-10 bg-background-base border rounded-8 text-body-small text-accent-black font-mono focus:outline-none focus:border-heat-100 transition-colors resize-none ${
|
||||
schemaError ? 'border-red-500' : 'border-border-faint'
|
||||
}`}
|
||||
/>
|
||||
{schemaError && (
|
||||
<p className="text-body-small text-accent-black mt-6">{schemaError}</p>
|
||||
)}
|
||||
<p className="text-body-small text-black-alpha-32 mt-6">
|
||||
Define the structure of data to extract
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* MCP Tools */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<label className="block text-label-small text-black-alpha-48">
|
||||
MCP Tools (Optional)
|
||||
</label>
|
||||
<button
|
||||
onClick={onAddMCP}
|
||||
className="px-10 py-6 bg-background-base hover:bg-black-alpha-4 border border-border-faint rounded-6 text-body-small text-accent-black transition-colors flex items-center gap-6"
|
||||
>
|
||||
<svg className="w-12 h-12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
Add MCP
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{nodeData?.mcpTools && nodeData.mcpTools.length > 0 ? (
|
||||
<div className="space-y-8">
|
||||
{nodeData.mcpTools.map((mcp: any, index: number) => (
|
||||
<div key={index} className="p-12 bg-background-base rounded-8 border border-border-faint">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<p className="text-body-small text-accent-black font-medium">{mcp.name}</p>
|
||||
<p className="text-body-small text-black-alpha-48 font-mono text-xs truncate mt-4">
|
||||
{mcp.url}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
const newTools = nodeData.mcpTools.filter((_: any, i: number) => i !== index);
|
||||
onUpdate(nodeData.id, { mcpTools: newTools });
|
||||
}}
|
||||
className="w-24 h-24 rounded-4 hover:bg-black-alpha-4 transition-colors flex items-center justify-center group"
|
||||
>
|
||||
<svg className="w-12 h-12 text-black-alpha-48 group-hover:text-accent-black" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-16 bg-background-base rounded-8 border border-border-faint text-center">
|
||||
<p className="text-body-small text-black-alpha-48">
|
||||
No MCP tools - the agent will only use the LLM
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Info Box */}
|
||||
<div className="p-16 bg-accent-white rounded-12 border border-border-faint">
|
||||
<p className="text-body-small text-accent-black">
|
||||
<strong>How it works:</strong> The LLM analyzes the input and extracts data matching your JSON schema. Use MCP tools to give the agent access to external data sources like web search.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.aside>
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
"use client";
|
||||
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { useState, useEffect } from "react";
|
||||
import type { Node } from "@xyflow/react";
|
||||
import VariableReferencePicker from "./VariableReferencePicker";
|
||||
|
||||
interface HTTPNodePanelProps {
|
||||
node: Node | null;
|
||||
nodes?: Node[];
|
||||
onClose: () => void;
|
||||
onDelete: (nodeId: string) => void;
|
||||
onUpdate: (nodeId: string, data: any) => void;
|
||||
}
|
||||
|
||||
interface Header {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export default function HTTPNodePanel({ node, nodes, onClose, onDelete, onUpdate }: HTTPNodePanelProps) {
|
||||
const nodeData = node?.data as any;
|
||||
|
||||
const [url, setUrl] = useState(nodeData?.httpUrl || "https://api.example.com/endpoint");
|
||||
const [method, setMethod] = useState(nodeData?.httpMethod || "GET");
|
||||
const [headers, setHeaders] = useState<Header[]>(nodeData?.httpHeaders || [
|
||||
{ key: "Content-Type", value: "application/json" }
|
||||
]);
|
||||
const [body, setBody] = useState(nodeData?.httpBody || "");
|
||||
const [authType, setAuthType] = useState(nodeData?.httpAuthType || "none");
|
||||
const [authToken, setAuthToken] = useState(nodeData?.httpAuthToken || "");
|
||||
const [showAuthToken, setShowAuthToken] = useState(false);
|
||||
|
||||
// Auto-save
|
||||
useEffect(() => {
|
||||
if (!node) return;
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
onUpdate(node.id, {
|
||||
httpUrl: url,
|
||||
httpMethod: method,
|
||||
httpHeaders: headers,
|
||||
httpBody: body,
|
||||
httpAuthType: authType,
|
||||
httpAuthToken: authToken,
|
||||
});
|
||||
}, 500);
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [url, method, headers, body, authType, authToken, node, onUpdate]);
|
||||
|
||||
const addHeader = () => {
|
||||
setHeaders([...headers, { key: "", value: "" }]);
|
||||
};
|
||||
|
||||
const updateHeader = (index: number, field: 'key' | 'value', value: string) => {
|
||||
setHeaders(headers.map((h, i) => i === index ? { ...h, [field]: value } : h));
|
||||
};
|
||||
|
||||
const removeHeader = (index: number) => {
|
||||
setHeaders(headers.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const insertQuickHeader = (key: string, value: string) => {
|
||||
const existing = headers.findIndex(h => h.key === key);
|
||||
if (existing >= 0) {
|
||||
updateHeader(existing, 'value', value);
|
||||
} else {
|
||||
setHeaders([...headers, { key, value }]);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{node && (
|
||||
<motion.aside
|
||||
initial={{ x: 400, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
exit={{ x: 400, opacity: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="fixed right-20 top-80 h-[calc(100vh-100px)] w-[calc(100vw-240px)] max-w-480 bg-accent-white border border-border-faint shadow-lg overflow-y-auto z-50 rounded-16"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="p-16 border-b border-border-faint">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h3 className="text-title-h4 text-accent-black">HTTP Request</h3>
|
||||
<div className="flex items-center gap-8">
|
||||
<button
|
||||
onClick={() => onDelete(node?.id || '')}
|
||||
className="w-32 h-32 rounded-6 hover:bg-black-alpha-4 transition-colors flex items-center justify-center group"
|
||||
title="Delete node"
|
||||
>
|
||||
<svg className="w-16 h-16 text-black-alpha-48 group-hover:text-accent-black" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-32 h-32 rounded-6 hover:bg-black-alpha-4 transition-colors flex items-center justify-center"
|
||||
>
|
||||
<svg className="w-16 h-16 text-black-alpha-48" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-body-small text-black-alpha-48">
|
||||
Call any HTTP/REST API
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Form */}
|
||||
<div className="p-16 space-y-16">
|
||||
{/* Method and URL */}
|
||||
<div>
|
||||
<label className="block text-label-small text-black-alpha-48 mb-8">
|
||||
Request
|
||||
</label>
|
||||
<div className="flex gap-8">
|
||||
<select
|
||||
value={method}
|
||||
onChange={(e) => setMethod(e.target.value)}
|
||||
className="px-12 py-8 bg-background-base border border-border-faint rounded-8 text-body-small text-accent-black font-medium focus:outline-none focus:border-heat-100 transition-colors"
|
||||
>
|
||||
<option value="GET">GET</option>
|
||||
<option value="POST">POST</option>
|
||||
<option value="PUT">PUT</option>
|
||||
<option value="PATCH">PATCH</option>
|
||||
<option value="DELETE">DELETE</option>
|
||||
</select>
|
||||
|
||||
<div className="flex-1 relative">
|
||||
<input
|
||||
type="text"
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
placeholder="https://api.example.com/endpoint"
|
||||
className="w-full px-12 py-8 pr-150 bg-background-base border border-border-faint rounded-8 text-body-small text-accent-black font-mono focus:outline-none focus:border-heat-100 transition-colors"
|
||||
/>
|
||||
{nodes && (
|
||||
<div className="absolute right-8 top-1/2 -translate-y-1/2">
|
||||
<VariableReferencePicker
|
||||
nodes={nodes}
|
||||
currentNodeId={node.id}
|
||||
onSelect={(ref) => setUrl(url + `{{${ref}}}`)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Authentication */}
|
||||
<div>
|
||||
<label className="block text-label-small text-black-alpha-48 mb-8">
|
||||
Authentication
|
||||
</label>
|
||||
<select
|
||||
value={authType}
|
||||
onChange={(e) => setAuthType(e.target.value)}
|
||||
className="w-full px-12 py-8 bg-background-base border border-border-faint rounded-8 text-body-small text-accent-black focus:outline-none focus:border-heat-100 transition-colors mb-12"
|
||||
>
|
||||
<option value="none">None</option>
|
||||
<option value="bearer">Bearer Token</option>
|
||||
<option value="basic">Basic Auth</option>
|
||||
<option value="api-key">API Key (Header)</option>
|
||||
</select>
|
||||
|
||||
{authType !== 'none' && (
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showAuthToken ? "text" : "password"}
|
||||
value={authToken}
|
||||
onChange={(e) => setAuthToken(e.target.value)}
|
||||
placeholder={authType === 'bearer' ? 'Bearer token...' : 'API key or credentials...'}
|
||||
className="w-full px-12 py-8 pr-40 bg-background-base border border-border-faint rounded-8 text-body-small text-accent-black font-mono focus:outline-none focus:border-heat-100 transition-colors"
|
||||
/>
|
||||
<button
|
||||
onClick={() => setShowAuthToken(!showAuthToken)}
|
||||
className="absolute right-12 top-1/2 -translate-y-1/2 text-black-alpha-48 hover:text-accent-black transition-colors"
|
||||
>
|
||||
{showAuthToken ? (
|
||||
<svg className="w-16 h-16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="w-16 h-16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Headers */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<label className="block text-label-small text-black-alpha-48">
|
||||
Headers
|
||||
</label>
|
||||
<div className="flex gap-6">
|
||||
<button
|
||||
onClick={() => insertQuickHeader('Content-Type', 'application/json')}
|
||||
className="px-8 py-4 bg-background-base hover:bg-black-alpha-4 border border-border-faint rounded-4 text-body-small text-accent-black transition-colors"
|
||||
>
|
||||
+ JSON
|
||||
</button>
|
||||
<button
|
||||
onClick={addHeader}
|
||||
className="px-8 py-4 bg-background-base hover:bg-black-alpha-4 border border-border-faint rounded-4 text-body-small text-accent-black transition-colors"
|
||||
>
|
||||
+ Header
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-8">
|
||||
{headers.map((header, index) => (
|
||||
<div key={index} className="flex gap-8">
|
||||
<input
|
||||
type="text"
|
||||
value={header.key}
|
||||
onChange={(e) => updateHeader(index, 'key', e.target.value)}
|
||||
placeholder="Header-Name"
|
||||
className="flex-1 px-10 py-6 bg-background-base border border-border-faint rounded-6 text-body-small text-accent-black font-mono focus:outline-none focus:border-heat-100 transition-colors"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={header.value}
|
||||
onChange={(e) => updateHeader(index, 'value', e.target.value)}
|
||||
placeholder="value"
|
||||
className="flex-1 px-10 py-6 bg-background-base border border-border-faint rounded-6 text-body-small text-accent-black focus:outline-none focus:border-heat-100 transition-colors"
|
||||
/>
|
||||
<button
|
||||
onClick={() => removeHeader(index)}
|
||||
className="w-24 h-24 rounded-4 hover:bg-black-alpha-4 transition-colors flex items-center justify-center group"
|
||||
>
|
||||
<svg className="w-12 h-12 text-black-alpha-48 group-hover:text-accent-black" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Body (for POST/PUT/PATCH) */}
|
||||
{(method === 'POST' || method === 'PUT' || method === 'PATCH') && (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<label className="block text-label-small text-black-alpha-48">
|
||||
Request Body
|
||||
</label>
|
||||
{nodes && (
|
||||
<VariableReferencePicker
|
||||
nodes={nodes}
|
||||
currentNodeId={node.id}
|
||||
onSelect={(ref) => setBody(body + `{{${ref}}}`)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<textarea
|
||||
value={body}
|
||||
onChange={(e) => setBody(e.target.value)}
|
||||
rows={8}
|
||||
placeholder='{"key": "value"}'
|
||||
className="w-full px-12 py-10 bg-gray-900 text-heat-100 border border-border-faint rounded-8 text-body-small font-mono focus:outline-none focus:border-heat-100 transition-colors resize-none"
|
||||
/>
|
||||
<div className="flex gap-8 mt-8">
|
||||
<button
|
||||
onClick={() => setBody('{{state.variables.lastOutput}}')}
|
||||
className="px-10 py-6 bg-heat-4 hover:bg-heat-8 border border-heat-100 rounded-6 text-body-small text-accent-black transition-colors"
|
||||
>
|
||||
Use Previous Output
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setBody(JSON.stringify({ data: "{{state.variables.lastOutput}}" }, null, 2))}
|
||||
className="px-10 py-6 bg-heat-4 hover:bg-heat-8 border border-heat-100 rounded-6 text-body-small text-accent-black transition-colors"
|
||||
>
|
||||
Wrap in JSON
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Quick Examples */}
|
||||
<details className="group">
|
||||
<summary className="cursor-pointer text-body-small text-heat-100 hover:text-heat-200 transition-colors">
|
||||
Show API examples
|
||||
</summary>
|
||||
<div className="mt-12 space-y-8">
|
||||
<button
|
||||
onClick={() => {
|
||||
setUrl('https://api.slack.com/api/chat.postMessage');
|
||||
setMethod('POST');
|
||||
insertQuickHeader('Authorization', 'Bearer xoxb-your-token');
|
||||
setBody('{\n "channel": "C123456",\n "text": "{{state.variables.lastOutput}}"\n}');
|
||||
}}
|
||||
className="w-full p-10 bg-heat-4 hover:bg-heat-8 rounded-6 text-left border border-heat-100 transition-colors"
|
||||
>
|
||||
<p className="text-body-small text-accent-black font-medium">Slack Message</p>
|
||||
<p className="text-body-small text-heat-100 mt-4">Post to Slack channel</p>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setUrl('https://api.notion.com/v1/pages');
|
||||
setMethod('POST');
|
||||
insertQuickHeader('Authorization', 'Bearer secret_...');
|
||||
insertQuickHeader('Notion-Version', '2022-06-28');
|
||||
setBody('{\n "parent": { "database_id": "..." },\n "properties": {}\n}');
|
||||
}}
|
||||
className="w-full p-10 bg-heat-4 hover:bg-heat-8 rounded-6 text-left border border-heat-100 transition-colors"
|
||||
>
|
||||
<p className="text-body-small text-accent-black font-medium">Notion Page</p>
|
||||
<p className="text-body-small text-heat-100 mt-4">Create Notion page</p>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setUrl('https://hooks.zapier.com/hooks/catch/...');
|
||||
setMethod('POST');
|
||||
setBody('{{state.variables.lastOutput}}');
|
||||
}}
|
||||
className="w-full p-10 bg-heat-4 hover:bg-heat-8 rounded-6 text-left border border-heat-100 transition-colors"
|
||||
>
|
||||
<p className="text-body-small text-accent-black font-medium">Zapier Webhook</p>
|
||||
<p className="text-body-small text-heat-100 mt-4">Trigger Zapier automation</p>
|
||||
</button>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
{/* Universal Output Selector */}
|
||||
<div className="pt-16 border-t border-border-faint">
|
||||
</div>
|
||||
</div>
|
||||
</motion.aside>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
+624
@@ -0,0 +1,624 @@
|
||||
"use client";
|
||||
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import type { Node } from "@xyflow/react";
|
||||
|
||||
interface LogicNodePanelProps {
|
||||
node: Node | null;
|
||||
nodes: Node[];
|
||||
onClose: () => void;
|
||||
onDelete: (nodeId: string) => void;
|
||||
onUpdate: (nodeId: string, data: any) => void;
|
||||
}
|
||||
|
||||
export default function LogicNodePanel({ node, nodes, onClose, onDelete, onUpdate }: LogicNodePanelProps) {
|
||||
const nodeData = node?.data as any;
|
||||
const nodeType = nodeData?.nodeType?.toLowerCase() || '';
|
||||
const [name, setName] = useState(nodeData?.name || nodeData?.nodeName || "Logic");
|
||||
const conditionTextareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const whileConditionTextareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const approvalMessageTextareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
// If/Else state
|
||||
const [condition, setCondition] = useState(nodeData?.condition || "input.score > 70");
|
||||
|
||||
// While state
|
||||
const [whileCondition, setWhileCondition] = useState(nodeData?.whileCondition || "iteration < 10");
|
||||
const [maxIterations, setMaxIterations] = useState(nodeData?.maxIterations || "100");
|
||||
|
||||
// User Approval state
|
||||
const [approvalMessage, setApprovalMessage] = useState(nodeData?.approvalMessage || "Please review and approve this step");
|
||||
const [timeoutMinutes, setTimeoutMinutes] = useState(nodeData?.timeoutMinutes || "30");
|
||||
|
||||
// Build available variables for conditions
|
||||
const getAvailableVariables = () => {
|
||||
const startNode = nodes.find(n => (n.data as any)?.nodeType === 'start');
|
||||
const inputVariables = (startNode?.data as any)?.inputVariables || [];
|
||||
|
||||
const previousNodes = nodes.filter(n => n.id !== node?.id && (n.data as any)?.nodeType !== 'note' && (n.data as any)?.nodeType !== 'start');
|
||||
|
||||
return {
|
||||
inputVars: inputVariables.map((v: any) => ({
|
||||
name: v.name,
|
||||
path: `input.${v.name}`,
|
||||
description: v.description,
|
||||
type: v.type,
|
||||
})),
|
||||
nodeOutputs: previousNodes.map(n => ({
|
||||
name: (n.data as any)?.nodeName || n.id,
|
||||
path: n.id.replace(/-/g, '_'),
|
||||
description: `Output from ${(n.data as any)?.nodeName || n.id}`,
|
||||
})),
|
||||
special: [
|
||||
{ name: 'lastOutput', path: 'lastOutput', description: 'Output from previous node' },
|
||||
{ name: 'iteration', path: 'iteration', description: 'Current iteration count (while loops only)' },
|
||||
]
|
||||
};
|
||||
};
|
||||
|
||||
const insertVariable = (varPath: string, targetType: 'ifElse' | 'while' | 'approval') => {
|
||||
let textarea: HTMLTextAreaElement | null = null;
|
||||
let currentValue = '';
|
||||
let setter: (value: string) => void;
|
||||
|
||||
switch (targetType) {
|
||||
case 'ifElse':
|
||||
textarea = conditionTextareaRef.current;
|
||||
currentValue = condition;
|
||||
setter = setCondition;
|
||||
break;
|
||||
case 'while':
|
||||
textarea = whileConditionTextareaRef.current;
|
||||
currentValue = whileCondition;
|
||||
setter = setWhileCondition;
|
||||
break;
|
||||
case 'approval':
|
||||
textarea = approvalMessageTextareaRef.current;
|
||||
currentValue = approvalMessage;
|
||||
setter = setApprovalMessage;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!textarea) return;
|
||||
|
||||
const start = textarea.selectionStart;
|
||||
const end = textarea.selectionEnd;
|
||||
const newValue = currentValue.substring(0, start) + varPath + currentValue.substring(end);
|
||||
|
||||
setter(newValue);
|
||||
|
||||
// Restore cursor position
|
||||
setTimeout(() => {
|
||||
textarea!.focus();
|
||||
textarea!.setSelectionRange(start + varPath.length, start + varPath.length);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
// Auto-save changes
|
||||
useEffect(() => {
|
||||
if (!node?.id) return;
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
onUpdate(node.id, {
|
||||
name,
|
||||
nodeName: name,
|
||||
condition,
|
||||
whileCondition,
|
||||
maxIterations,
|
||||
approvalMessage,
|
||||
timeoutMinutes,
|
||||
});
|
||||
}, 500);
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [name, condition, whileCondition, maxIterations, approvalMessage, timeoutMinutes]);
|
||||
|
||||
const availableVars = getAvailableVariables();
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{node && (
|
||||
<motion.aside
|
||||
initial={{ x: 400, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
exit={{ x: 400, opacity: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="fixed right-20 top-80 h-[calc(100vh-100px)] w-[calc(100vw-240px)] max-w-480 bg-accent-white border border-border-faint shadow-lg overflow-y-auto z-50 rounded-16"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="p-20 border-b border-border-faint">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="text-label-large text-accent-black font-medium bg-transparent border-none outline-none focus:outline-none hover:bg-black-alpha-4 px-2 -ml-2 rounded-4 transition-colors"
|
||||
placeholder="Enter node name..."
|
||||
/>
|
||||
<div className="flex items-center gap-8">
|
||||
<button
|
||||
onClick={() => onDelete(node?.id || '')}
|
||||
className="w-32 h-32 rounded-6 hover:bg-black-alpha-4 transition-colors flex items-center justify-center group"
|
||||
title="Delete node"
|
||||
>
|
||||
<svg className="w-16 h-16 text-black-alpha-48 group-hover:text-black-alpha-64" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-32 h-32 rounded-6 hover:bg-black-alpha-4 transition-colors flex items-center justify-center"
|
||||
>
|
||||
<svg className="w-16 h-16 text-black-alpha-48" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-black-alpha-48">
|
||||
{nodeType.includes('if') ? 'Create conditions to branch your workflow' :
|
||||
nodeType.includes('while') ? 'Loop while a condition is true' :
|
||||
nodeType.includes('approval') ? 'Pause for a human to approve or reject a step' :
|
||||
'Configure logic flow'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Form Fields */}
|
||||
<div className="p-16 space-y-16">
|
||||
{/* If/Else Configuration */}
|
||||
{nodeType.includes('if') && (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-label-small text-black-alpha-48 mb-8">
|
||||
Condition
|
||||
</label>
|
||||
<textarea
|
||||
ref={conditionTextareaRef}
|
||||
value={condition}
|
||||
onChange={(e) => setCondition(e.target.value)}
|
||||
rows={3}
|
||||
placeholder="e.g., input.score > 70"
|
||||
className="w-full px-12 py-10 bg-background-base border border-border-faint rounded-8 text-body-medium text-accent-black font-mono focus:outline-none focus:border-heat-100 transition-colors resize-none"
|
||||
/>
|
||||
<p className="text-body-small text-black-alpha-48 mt-8">
|
||||
JavaScript expression that returns true/false
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Quick Variable Selector */}
|
||||
<div className="p-16 bg-heat-4 rounded-12 border border-heat-100">
|
||||
<h3 className="text-label-small text-accent-black mb-12 flex items-center gap-6">
|
||||
<svg className="w-16 h-16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 20l4-16m2 16l4-16M6 9h14M4 15h14" />
|
||||
</svg>
|
||||
Click to Insert Variable
|
||||
</h3>
|
||||
|
||||
{/* Input Variables */}
|
||||
{availableVars.inputVars.length > 0 && (
|
||||
<div className="mb-12">
|
||||
<p className="text-xs text-heat-100 font-medium mb-6">Input Variables:</p>
|
||||
<div className="flex flex-wrap gap-6">
|
||||
{availableVars.inputVars.map((v: any, idx: number) => (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => insertVariable(v.path, 'ifElse')}
|
||||
className="px-10 py-6 bg-heat-8 hover:bg-heat-12 text-accent-black rounded-6 text-xs font-mono transition-colors"
|
||||
title={v.description}
|
||||
>
|
||||
{v.path}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Previous Node Outputs */}
|
||||
{availableVars.nodeOutputs.length > 0 && (
|
||||
<div className="mb-12">
|
||||
<p className="text-xs text-heat-100 font-medium mb-6">Previous Nodes:</p>
|
||||
<div className="flex flex-wrap gap-6">
|
||||
{availableVars.nodeOutputs.map((v: any, idx: number) => (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => insertVariable(v.path, 'ifElse')}
|
||||
className="px-10 py-6 bg-heat-8 hover:bg-heat-12 text-accent-black rounded-6 text-xs font-mono transition-colors"
|
||||
title={v.description}
|
||||
>
|
||||
{v.path}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Special Variables */}
|
||||
<div>
|
||||
<p className="text-xs text-heat-100 font-medium mb-6">Special:</p>
|
||||
<div className="flex flex-wrap gap-6">
|
||||
{availableVars.special.filter(v => v.name !== 'iteration').map((v: any, idx: number) => (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => insertVariable(v.path, 'ifElse')}
|
||||
className="px-10 py-6 bg-heat-8 hover:bg-heat-12 text-accent-black rounded-6 text-xs font-mono transition-colors"
|
||||
title={v.description}
|
||||
>
|
||||
{v.path}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Common Condition Examples */}
|
||||
<div className="p-16 bg-background-base rounded-12 border border-border-faint">
|
||||
<h3 className="text-label-small text-accent-black mb-12">Common Patterns:</h3>
|
||||
<div className="grid grid-cols-2 gap-8">
|
||||
<button
|
||||
onClick={() => setCondition('input.score > 70')}
|
||||
className="text-left px-12 py-8 bg-accent-white hover:bg-heat-4 border border-border-faint rounded-6 transition-colors"
|
||||
>
|
||||
<p className="text-xs font-mono text-heat-100">input.score > 70</p>
|
||||
<p className="text-xs text-black-alpha-48 mt-2">Number check</p>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCondition('input.status === "approved"')}
|
||||
className="text-left px-12 py-8 bg-accent-white hover:bg-heat-4 border border-border-faint rounded-6 transition-colors"
|
||||
>
|
||||
<p className="text-xs font-mono text-heat-100">input.status === "approved"</p>
|
||||
<p className="text-xs text-black-alpha-48 mt-2">String equals</p>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCondition('lastOutput && lastOutput.length > 0')}
|
||||
className="text-left px-12 py-8 bg-accent-white hover:bg-heat-4 border border-border-faint rounded-6 transition-colors"
|
||||
>
|
||||
<p className="text-xs font-mono text-heat-100">lastOutput.length > 0</p>
|
||||
<p className="text-xs text-black-alpha-48 mt-2">Array not empty</p>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCondition('lastOutput.data && lastOutput.data.price')}
|
||||
className="text-left px-12 py-8 bg-accent-white hover:bg-heat-4 border border-border-faint rounded-6 transition-colors"
|
||||
>
|
||||
<p className="text-xs font-mono text-heat-100">lastOutput.data.price</p>
|
||||
<p className="text-xs text-black-alpha-48 mt-2">JSON nested</p>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCondition('input.age >= 18 && input.age <= 65')}
|
||||
className="text-left px-12 py-8 bg-accent-white hover:bg-heat-4 border border-border-faint rounded-6 transition-colors"
|
||||
>
|
||||
<p className="text-xs font-mono text-heat-100">age >= 18 && age <= 65</p>
|
||||
<p className="text-xs text-black-alpha-48 mt-2">Range check</p>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCondition('lastOutput.tags && lastOutput.tags.includes("urgent")')}
|
||||
className="text-left px-12 py-8 bg-accent-white hover:bg-heat-4 border border-border-faint rounded-6 transition-colors"
|
||||
>
|
||||
<p className="text-xs font-mono text-heat-100">tags.includes("urgent")</p>
|
||||
<p className="text-xs text-black-alpha-48 mt-2">Array contains</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* While Configuration */}
|
||||
{nodeType.includes('while') && (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-label-small text-black-alpha-48 mb-8">
|
||||
Loop Condition
|
||||
</label>
|
||||
<textarea
|
||||
ref={whileConditionTextareaRef}
|
||||
value={whileCondition}
|
||||
onChange={(e) => setWhileCondition(e.target.value)}
|
||||
rows={3}
|
||||
placeholder="e.g., iteration < 10"
|
||||
className="w-full px-12 py-10 bg-background-base border border-border-faint rounded-8 text-body-medium text-accent-black font-mono focus:outline-none focus:border-heat-100 transition-colors resize-none"
|
||||
/>
|
||||
<p className="text-body-small text-black-alpha-48 mt-8">
|
||||
JavaScript expression that returns true/false
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Quick Variable Selector */}
|
||||
<div className="p-16 bg-heat-4 rounded-12 border border-heat-100">
|
||||
<h3 className="text-label-small text-accent-black mb-12 flex items-center gap-6">
|
||||
<svg className="w-16 h-16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 20l4-16m2 16l4-16M6 9h14M4 15h14" />
|
||||
</svg>
|
||||
Click to Insert Variable
|
||||
</h3>
|
||||
|
||||
{/* Input Variables */}
|
||||
{availableVars.inputVars.length > 0 && (
|
||||
<div className="mb-12">
|
||||
<p className="text-xs text-heat-100 font-medium mb-6">Input Variables:</p>
|
||||
<div className="flex flex-wrap gap-6">
|
||||
{availableVars.inputVars.map((v: any, idx: number) => (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => insertVariable(v.path, 'while')}
|
||||
className="px-10 py-6 bg-heat-8 hover:bg-heat-12 text-accent-black rounded-6 text-xs font-mono transition-colors"
|
||||
title={v.description}
|
||||
>
|
||||
{v.path}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Previous Node Outputs */}
|
||||
{availableVars.nodeOutputs.length > 0 && (
|
||||
<div className="mb-12">
|
||||
<p className="text-xs text-heat-100 font-medium mb-6">Previous Nodes:</p>
|
||||
<div className="flex flex-wrap gap-6">
|
||||
{availableVars.nodeOutputs.map((v: any, idx: number) => (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => insertVariable(v.path, 'while')}
|
||||
className="px-10 py-6 bg-heat-8 hover:bg-heat-12 text-accent-black rounded-6 text-xs font-mono transition-colors"
|
||||
title={v.description}
|
||||
>
|
||||
{v.path}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Special Variables */}
|
||||
<div>
|
||||
<p className="text-xs text-heat-100 font-medium mb-6">Special:</p>
|
||||
<div className="flex flex-wrap gap-6">
|
||||
{availableVars.special.map((v: any, idx: number) => (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => insertVariable(v.path, 'while')}
|
||||
className="px-10 py-6 bg-heat-8 hover:bg-heat-12 text-accent-black rounded-6 text-xs font-mono transition-colors"
|
||||
title={v.description}
|
||||
>
|
||||
{v.path}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Common Loop Examples */}
|
||||
<div className="p-16 bg-background-base rounded-12 border border-border-faint">
|
||||
<h3 className="text-label-small text-accent-black mb-12">Common Patterns:</h3>
|
||||
<div className="grid grid-cols-2 gap-8">
|
||||
<button
|
||||
onClick={() => setWhileCondition('iteration < 10')}
|
||||
className="text-left px-12 py-8 bg-accent-white hover:bg-heat-4 border border-border-faint rounded-6 transition-colors"
|
||||
>
|
||||
<p className="text-xs font-mono text-heat-100">iteration < 10</p>
|
||||
<p className="text-xs text-black-alpha-48 mt-2">Fixed iterations</p>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setWhileCondition('iteration < lastOutput.totalCount')}
|
||||
className="text-left px-12 py-8 bg-accent-white hover:bg-heat-4 border border-border-faint rounded-6 transition-colors"
|
||||
>
|
||||
<p className="text-xs font-mono text-heat-100">iteration < totalCount</p>
|
||||
<p className="text-xs text-black-alpha-48 mt-2">Array processing</p>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setWhileCondition('lastOutput.hasMore === true')}
|
||||
className="text-left px-12 py-8 bg-accent-white hover:bg-heat-4 border border-border-faint rounded-6 transition-colors"
|
||||
>
|
||||
<p className="text-xs font-mono text-heat-100">lastOutput.hasMore</p>
|
||||
<p className="text-xs text-black-alpha-48 mt-2">Has more pages</p>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setWhileCondition('lastOutput.score < 90 && iteration < 5')}
|
||||
className="text-left px-12 py-8 bg-accent-white hover:bg-heat-4 border border-border-faint rounded-6 transition-colors"
|
||||
>
|
||||
<p className="text-xs font-mono text-heat-100">score < 90 (max 5)</p>
|
||||
<p className="text-xs text-black-alpha-48 mt-2">Retry until good</p>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setWhileCondition('lastOutput.queue && lastOutput.queue.length > 0')}
|
||||
className="text-left px-12 py-8 bg-accent-white hover:bg-heat-4 border border-border-faint rounded-6 transition-colors"
|
||||
>
|
||||
<p className="text-xs font-mono text-heat-100">queue.length > 0</p>
|
||||
<p className="text-xs text-black-alpha-48 mt-2">Process queue</p>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setWhileCondition('lastOutput.data.items[iteration]')}
|
||||
className="text-left px-12 py-8 bg-accent-white hover:bg-heat-4 border border-border-faint rounded-6 transition-colors"
|
||||
>
|
||||
<p className="text-xs font-mono text-heat-100">data.items[iteration]</p>
|
||||
<p className="text-xs text-black-alpha-48 mt-2">JSON array access</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-label-small text-black-alpha-48 mb-8">
|
||||
Max Iterations
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={maxIterations}
|
||||
onChange={(e) => setMaxIterations(e.target.value)}
|
||||
className="w-full px-12 py-10 bg-background-base border border-border-faint rounded-8 text-body-medium text-accent-black focus:outline-none focus:border-heat-100 transition-colors"
|
||||
/>
|
||||
<p className="text-body-small text-black-alpha-48 mt-8">
|
||||
Safety limit to prevent infinite loops
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-16 bg-heat-4 rounded-12 border border-heat-100">
|
||||
<p className="text-body-small text-accent-black">
|
||||
<strong>Loop Setup:</strong> Connect loop body nodes using the "continue" handle. The workflow will repeat until the condition is false or max iterations reached.
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* User Approval Configuration */}
|
||||
{nodeType.includes('approval') && (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-label-small text-black-alpha-48 mb-8">
|
||||
Approval Message
|
||||
</label>
|
||||
<textarea
|
||||
ref={approvalMessageTextareaRef}
|
||||
value={approvalMessage}
|
||||
onChange={(e) => setApprovalMessage(e.target.value)}
|
||||
rows={4}
|
||||
placeholder="e.g., Please review and approve: ${lastOutput.summary}"
|
||||
className="w-full px-12 py-10 bg-background-base border border-border-faint rounded-8 text-body-medium text-accent-black focus:outline-none focus:border-heat-100 transition-colors resize-none"
|
||||
/>
|
||||
<p className="text-body-small text-black-alpha-48 mt-8">
|
||||
Use ${'{variableName}'} to insert dynamic values from your workflow
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Quick Variable Selector for Approval Message */}
|
||||
<div className="p-16 bg-heat-4 rounded-12 border border-heat-100">
|
||||
<h3 className="text-label-small text-accent-black mb-12 flex items-center gap-6">
|
||||
<svg className="w-16 h-16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 20l4-16m2 16l4-16M6 9h14M4 15h14" />
|
||||
</svg>
|
||||
Click to Insert Variable
|
||||
</h3>
|
||||
|
||||
{/* Input Variables */}
|
||||
{availableVars.inputVars.length > 0 && (
|
||||
<div className="mb-12">
|
||||
<p className="text-xs text-heat-100 font-medium mb-6">Input Variables:</p>
|
||||
<div className="flex flex-wrap gap-6">
|
||||
{availableVars.inputVars.map((v: any, idx: number) => (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => insertVariable('${' + v.path + '}', 'approval')}
|
||||
className="px-10 py-6 bg-heat-8 hover:bg-heat-12 text-accent-black rounded-6 text-xs font-mono transition-colors"
|
||||
title={v.description}
|
||||
>
|
||||
${'{' + v.path + '}'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Previous Node Outputs */}
|
||||
{availableVars.nodeOutputs.length > 0 && (
|
||||
<div className="mb-12">
|
||||
<p className="text-xs text-heat-100 font-medium mb-6">Previous Nodes:</p>
|
||||
<div className="flex flex-wrap gap-6">
|
||||
{availableVars.nodeOutputs.map((v: any, idx: number) => (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => insertVariable('${' + v.path + '}', 'approval')}
|
||||
className="px-10 py-6 bg-heat-8 hover:bg-heat-12 text-accent-black rounded-6 text-xs font-mono transition-colors"
|
||||
title={v.description}
|
||||
>
|
||||
${'{' + v.path + '}'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Special Variables */}
|
||||
<div>
|
||||
<p className="text-xs text-heat-100 font-medium mb-6">Special:</p>
|
||||
<div className="flex flex-wrap gap-6">
|
||||
{availableVars.special.filter(v => v.name !== 'iteration').map((v: any, idx: number) => (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => insertVariable('${' + v.path + '}', 'approval')}
|
||||
className="px-10 py-6 bg-heat-8 hover:bg-heat-12 text-accent-black rounded-6 text-xs font-mono transition-colors"
|
||||
title={v.description}
|
||||
>
|
||||
${'{' + v.path + '}'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Common Message Patterns */}
|
||||
<div className="p-16 bg-background-base rounded-12 border border-border-faint">
|
||||
<h3 className="text-label-small text-accent-black mb-12">Common Message Patterns:</h3>
|
||||
<div className="grid grid-cols-1 gap-8">
|
||||
<button
|
||||
onClick={() => setApprovalMessage('Please review and approve: ${lastOutput}')}
|
||||
className="text-left px-12 py-8 bg-accent-white hover:bg-heat-4 border border-border-faint rounded-6 transition-colors"
|
||||
>
|
||||
<p className="text-xs font-mono text-heat-100">Please review and approve: ${'{lastOutput}'}</p>
|
||||
<p className="text-xs text-black-alpha-48 mt-2">Simple approval with data</p>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setApprovalMessage('Transaction for ${input.amount} requires approval')}
|
||||
className="text-left px-12 py-8 bg-accent-white hover:bg-heat-4 border border-border-faint rounded-6 transition-colors"
|
||||
>
|
||||
<p className="text-xs font-mono text-heat-100">Transaction for ${'{input.amount}'} requires approval</p>
|
||||
<p className="text-xs text-black-alpha-48 mt-2">Financial approval</p>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setApprovalMessage('Review ${input.user}\'s request: ${lastOutput.summary}')}
|
||||
className="text-left px-12 py-8 bg-accent-white hover:bg-heat-4 border border-border-faint rounded-6 transition-colors"
|
||||
>
|
||||
<p className="text-xs font-mono text-heat-100">Review ${'{input.user}'}'s request: ${'{lastOutput.summary}'}</p>
|
||||
<p className="text-xs text-black-alpha-48 mt-2">User action approval</p>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setApprovalMessage('Approve deployment to ${input.environment}?\n\nChanges:\n${lastOutput.changes}')}
|
||||
className="text-left px-12 py-8 bg-accent-white hover:bg-heat-4 border border-border-faint rounded-6 transition-colors"
|
||||
>
|
||||
<p className="text-xs font-mono text-heat-100">Multi-line deployment approval</p>
|
||||
<p className="text-xs text-black-alpha-48 mt-2">Detailed approval with formatting</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-label-small text-black-alpha-48 mb-8">
|
||||
Timeout (minutes)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={timeoutMinutes}
|
||||
onChange={(e) => setTimeoutMinutes(e.target.value)}
|
||||
className="w-full px-12 py-10 bg-background-base border border-border-faint rounded-8 text-body-medium text-accent-black focus:outline-none focus:border-heat-100 transition-colors"
|
||||
/>
|
||||
<p className="text-body-small text-black-alpha-48 mt-8">
|
||||
How long to wait for approval before timing out
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-16 bg-heat-4 rounded-12 border border-heat-100">
|
||||
<h3 className="text-label-small text-accent-black mb-8">How it works</h3>
|
||||
<p className="text-body-small text-heat-100 mb-12">
|
||||
The workflow will pause at this node and notify the user with your approval message. Execution continues down the "Approve" branch when approved, or the "Reject" branch when rejected or timed out.
|
||||
</p>
|
||||
<div className="flex gap-8 text-xs">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-full bg-green-500"></div>
|
||||
<span className="text-black-alpha-64">Approve branch</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-full bg-red-500"></div>
|
||||
<span className="text-black-alpha-64">Reject branch</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Universal Output Selector */}
|
||||
<div className="pt-16 border-t border-border-faint">
|
||||
</div>
|
||||
</div>
|
||||
</motion.aside>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
"use client";
|
||||
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Globe, Brain, Database, Package, Loader2, ChevronDown } from "lucide-react";
|
||||
import type { Node } from "@xyflow/react";
|
||||
import { toast } from "sonner";
|
||||
import { useQuery } from "convex/react";
|
||||
import { api } from "@/convex/_generated/api";
|
||||
import { useUser } from "@clerk/nextjs";
|
||||
|
||||
interface MCPPanelProps {
|
||||
node: Node | null;
|
||||
onClose: () => void;
|
||||
onUpdate: (nodeId: string, data: any) => void;
|
||||
mode?: 'configure' | 'add-to-agent';
|
||||
onAddToAgent?: (mcpConfig: any) => void;
|
||||
onOpenSettings?: () => void;
|
||||
}
|
||||
|
||||
export default function MCPPanel({
|
||||
node,
|
||||
onClose,
|
||||
onUpdate,
|
||||
mode = 'configure',
|
||||
onAddToAgent,
|
||||
onOpenSettings
|
||||
}: MCPPanelProps) {
|
||||
const { user } = useUser();
|
||||
const nodeData = node?.data as any;
|
||||
|
||||
// Fetch enabled MCP servers from central registry
|
||||
const mcpServers = useQuery(api.mcpServers.getEnabledMCPs,
|
||||
user?.id ? { userId: user.id } : "skip"
|
||||
);
|
||||
|
||||
// Store only the selected server ID
|
||||
const [selectedServerId, setSelectedServerId] = useState<string | null>(() => {
|
||||
return nodeData?.mcpServerId || null;
|
||||
});
|
||||
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
const selectedServer = mcpServers?.find(s => s._id === selectedServerId);
|
||||
|
||||
// Auto-save selected server ID (only in configure mode)
|
||||
useEffect(() => {
|
||||
if (!node || mode === 'add-to-agent') return;
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
try {
|
||||
onUpdate(node.id, {
|
||||
mcpServerId: selectedServerId,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error saving MCP server selection:', error);
|
||||
toast.error('Failed to save MCP server selection', {
|
||||
description: error instanceof Error ? error.message : 'Unable to save changes',
|
||||
});
|
||||
}
|
||||
}, 500);
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [selectedServerId, node, onUpdate, mode]);
|
||||
|
||||
const getCategoryIcon = (category: string) => {
|
||||
switch (category) {
|
||||
case 'web': return <Globe className="w-16 h-16" />;
|
||||
case 'ai': return <Brain className="w-16 h-16" />;
|
||||
case 'data': return <Database className="w-16 h-16" />;
|
||||
default: return <Package className="w-16 h-16" />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{(node || mode === 'add-to-agent') && (
|
||||
<motion.aside
|
||||
initial={{ x: 400, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
exit={{ x: 400, opacity: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="fixed right-20 top-80 bottom-20 w-[calc(100vw-240px)] max-w-520 bg-accent-white border border-border-faint shadow-lg overflow-y-auto z-50 rounded-16 flex flex-col"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="p-20 border-b border-border-faint">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<h2 className="text-xl font-semibold text-accent-black">
|
||||
{mode === 'add-to-agent' ? 'Add MCP to Agent' : 'Tools Node'}
|
||||
</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-32 h-32 rounded-6 hover:bg-black-alpha-4 transition-colors flex items-center justify-center"
|
||||
>
|
||||
<svg className="w-16 h-16 text-black-alpha-48" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-sm text-black-alpha-48">
|
||||
Select tools from an MCP server to invoke them in your agent
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Configuration */}
|
||||
<div className="p-20 space-y-20">
|
||||
{/* Server Selector */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-black-alpha-48 mb-8">
|
||||
MCP Servers
|
||||
</label>
|
||||
|
||||
{!mcpServers || mcpServers.length === 0 ? (
|
||||
<div className="p-16 bg-background-base rounded-12 border border-border-faint text-center">
|
||||
<p className="text-body-small text-black-alpha-48 mb-12">
|
||||
No servers available in your MCP registry
|
||||
</p>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (onOpenSettings) {
|
||||
onOpenSettings();
|
||||
}
|
||||
setTimeout(() => {
|
||||
onClose();
|
||||
}, 200); // Slight delay ensures settings modal opens
|
||||
}}
|
||||
className="px-16 py-8 bg-heat-100 hover:bg-heat-200 text-white rounded-8 text-xs font-medium transition-all"
|
||||
>
|
||||
Go to Settings
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-8">
|
||||
<select
|
||||
value={selectedServerId || ""}
|
||||
onChange={(e) => {
|
||||
const serverId = e.target.value || null;
|
||||
setSelectedServerId(serverId);
|
||||
|
||||
// In add-to-agent mode, call the callback
|
||||
if (mode === 'add-to-agent' && onAddToAgent && serverId) {
|
||||
const server = mcpServers.find(s => s._id === serverId);
|
||||
if (server) {
|
||||
onAddToAgent({
|
||||
mcpServerId: server._id,
|
||||
name: server.name,
|
||||
tools: server.tools || [],
|
||||
});
|
||||
toast.success(`Added ${server.name} to agent`);
|
||||
setTimeout(() => onClose(), 1000);
|
||||
}
|
||||
}
|
||||
}}
|
||||
className="w-full px-14 py-10 bg-background-base border border-border-faint rounded-10 text-sm text-accent-black focus:outline-none focus:border-heat-100 transition-colors appearance-none cursor-pointer"
|
||||
>
|
||||
<option value="">Select an MCP server...</option>
|
||||
{mcpServers.map((server) => {
|
||||
// const isFirecrawl = server.name === 'Firecrawl' && server.isOfficial;
|
||||
return (
|
||||
<option key={server._id} value={server._id}>
|
||||
{server.name} {/* {isFirecrawl && '(API Key Required)'} */} {server.tools && `(${server.tools.length} tools)`}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
|
||||
{selectedServer && (
|
||||
<div className="mt-12">
|
||||
{/* Server Info Card */}
|
||||
<div className="p-16 bg-background-base rounded-12 border border-border-faint">
|
||||
<div className="flex items-start gap-12">
|
||||
<div className={`text-heat-100`}>
|
||||
{getCategoryIcon(selectedServer.category)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-8 mb-4">
|
||||
<h4 className="text-sm font-medium text-accent-black">
|
||||
{selectedServer.name}
|
||||
</h4>
|
||||
{/* {selectedServer.name === 'Firecrawl' && selectedServer.isOfficial && (
|
||||
<span className="px-6 py-2 bg-heat-4 text-heat-100 rounded-6 text-xs border border-heat-100 font-medium">
|
||||
API Key Required
|
||||
</span>
|
||||
)} */}
|
||||
</div>
|
||||
{selectedServer.description && (
|
||||
<p className="text-xs text-black-alpha-48 mb-8">
|
||||
{selectedServer.description}
|
||||
</p>
|
||||
)}
|
||||
{/* {selectedServer.name === 'Firecrawl' && selectedServer.isOfficial && (
|
||||
<a
|
||||
href="https://www.firecrawl.dev/app/api-keys"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs text-heat-100 hover:text-heat-200 underline block mb-8"
|
||||
>
|
||||
Get API key here →
|
||||
</a>
|
||||
)} */}
|
||||
<div className="flex items-center gap-12 text-xs">
|
||||
<span className="text-black-alpha-48">
|
||||
Category: {selectedServer.category}
|
||||
</span>
|
||||
{selectedServer.connectionStatus === 'connected' && (
|
||||
<span className="px-6 py-2 bg-heat-4 text-heat-100 rounded-6 border border-heat-100">
|
||||
Connected
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Show/Hide Tools Button */}
|
||||
{selectedServer.tools && selectedServer.tools.length > 0 && (
|
||||
<div className="mt-12">
|
||||
<button
|
||||
onClick={() => setShowDetails(!showDetails)}
|
||||
className="flex items-center gap-8 text-xs text-heat-100 hover:text-heat-200 font-medium"
|
||||
>
|
||||
<ChevronDown className={`w-14 h-14 transition-transform ${showDetails ? 'rotate-180' : ''}`} />
|
||||
{showDetails ? 'Hide' : 'Show'} Available Tools ({selectedServer.tools.length})
|
||||
</button>
|
||||
|
||||
{/* Tools List */}
|
||||
<AnimatePresence>
|
||||
{showDetails && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
className="mt-8"
|
||||
>
|
||||
<div className="space-y-6">
|
||||
{selectedServer.tools.map((tool: string) => (
|
||||
<div key={tool} className="p-8 bg-accent-white rounded-6 border border-border-faint">
|
||||
<code className="text-xs font-mono text-heat-100">
|
||||
{tool}
|
||||
</code>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Add New Server Link */}
|
||||
<div className="pt-16 border-t border-border-faint">
|
||||
<p className="text-xs text-black-alpha-48 mb-8">
|
||||
Need to add a new MCP server?
|
||||
</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
onClose();
|
||||
onOpenSettings?.();
|
||||
}}
|
||||
className="text-xs text-heat-100 hover:text-heat-200 font-medium"
|
||||
>
|
||||
Go to Settings → MCP Registry
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.aside>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
+205
@@ -0,0 +1,205 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
interface Argument {
|
||||
name: string;
|
||||
type: "string" | "number" | "boolean" | "object" | "array";
|
||||
required: boolean;
|
||||
defaultValue?: string;
|
||||
reference?: string; // e.g., "state.variables.node_1.price"
|
||||
}
|
||||
|
||||
interface NodeArgumentsPanelProps {
|
||||
nodeId: string;
|
||||
currentArgs: Argument[];
|
||||
onUpdate: (args: Argument[]) => void;
|
||||
}
|
||||
|
||||
export default function NodeArgumentsPanel({ nodeId, currentArgs, onUpdate }: NodeArgumentsPanelProps) {
|
||||
const [arguments_, setArguments] = useState<Argument[]>(currentArgs || []);
|
||||
const [showAddArg, setShowAddArg] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
onUpdate(arguments_);
|
||||
}, 500);
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [arguments_, onUpdate]);
|
||||
|
||||
const addArgument = () => {
|
||||
setArguments([
|
||||
...arguments_,
|
||||
{
|
||||
name: `arg${arguments_.length + 1}`,
|
||||
type: "string",
|
||||
required: false,
|
||||
},
|
||||
]);
|
||||
setShowAddArg(false);
|
||||
};
|
||||
|
||||
const updateArgument = (index: number, updates: Partial<Argument>) => {
|
||||
setArguments(
|
||||
arguments_.map((arg, i) => (i === index ? { ...arg, ...updates } : arg))
|
||||
);
|
||||
};
|
||||
|
||||
const removeArgument = (index: number) => {
|
||||
setArguments(arguments_.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-16">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-label-small text-black-alpha-48">
|
||||
Arguments
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => setShowAddArg(true)}
|
||||
className="px-10 py-6 bg-background-base hover:bg-black-alpha-4 border border-border-faint rounded-6 text-body-small text-accent-black transition-colors flex items-center gap-6"
|
||||
>
|
||||
<svg className="w-12 h-12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
Add Argument
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{arguments_.length === 0 ? (
|
||||
<div className="p-16 bg-background-base rounded-8 border border-border-faint text-center">
|
||||
<p className="text-body-small text-black-alpha-48">
|
||||
No arguments defined. Click "Add Argument" to get started.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-12">
|
||||
{arguments_.map((arg, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="p-12 bg-background-base rounded-8 border border-border-faint"
|
||||
>
|
||||
<div className="flex items-start gap-8 mb-12">
|
||||
<div className="flex-1 grid grid-cols-2 gap-8">
|
||||
{/* Argument Name */}
|
||||
<input
|
||||
type="text"
|
||||
value={arg.name}
|
||||
onChange={(e) => updateArgument(index, { name: e.target.value })}
|
||||
placeholder="argumentName"
|
||||
className="px-10 py-6 bg-white border border-border-faint rounded-6 text-body-small text-accent-black font-mono focus:outline-none focus:border-heat-100 transition-colors"
|
||||
/>
|
||||
|
||||
{/* Argument Type */}
|
||||
<select
|
||||
value={arg.type}
|
||||
onChange={(e) => updateArgument(index, { type: e.target.value as any })}
|
||||
className="px-10 py-6 bg-white border border-border-faint rounded-6 text-body-small text-accent-black focus:outline-none focus:border-heat-100 transition-colors"
|
||||
>
|
||||
<option value="string">String</option>
|
||||
<option value="number">Number</option>
|
||||
<option value="boolean">Boolean</option>
|
||||
<option value="object">Object</option>
|
||||
<option value="array">Array</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Remove Button */}
|
||||
<button
|
||||
onClick={() => removeArgument(index)}
|
||||
className="w-24 h-24 rounded-4 hover:bg-black-alpha-4 transition-colors flex items-center justify-center group"
|
||||
>
|
||||
<svg className="w-12 h-12 text-black-alpha-48 group-hover:text-accent-black" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Value Source */}
|
||||
<div className="space-y-8">
|
||||
<label className="block text-body-small text-black-alpha-48">
|
||||
Value Source (Optional)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={arg.reference || ''}
|
||||
onChange={(e) => updateArgument(index, { reference: e.target.value })}
|
||||
placeholder="state.variables.node_1.price or 'default value'"
|
||||
className="w-full px-10 py-6 bg-white border border-border-faint rounded-6 text-body-small text-accent-black font-mono focus:outline-none focus:border-heat-100 transition-colors"
|
||||
/>
|
||||
<p className="text-body-small text-black-alpha-32">
|
||||
Reference previous node output or set a default value
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Required Toggle */}
|
||||
<div className="flex items-center justify-between mt-12 pt-12 border-t border-border-faint">
|
||||
<span className="text-body-small text-black-alpha-48">Required</span>
|
||||
<button
|
||||
onClick={() => updateArgument(index, { required: !arg.required })}
|
||||
className={`w-36 h-20 rounded-full transition-colors relative ${
|
||||
arg.required ? 'bg-heat-100' : 'bg-black-alpha-12'
|
||||
}`}
|
||||
>
|
||||
<motion.div
|
||||
className="w-16 h-16 bg-white rounded-full absolute top-2 shadow-sm"
|
||||
animate={{ left: arg.required ? '18px' : '2px' }}
|
||||
transition={{ duration: 0.2 }}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showAddArg && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
className="p-12 bg-heat-4 rounded-8 border border-heat-100"
|
||||
>
|
||||
<p className="text-body-small text-accent-black mb-12">
|
||||
Add a new argument to define what this node receives
|
||||
</p>
|
||||
<button
|
||||
onClick={addArgument}
|
||||
className="w-full px-12 py-8 bg-heat-100 hover:bg-heat-200 text-white rounded-6 text-body-small font-medium transition-colors"
|
||||
>
|
||||
Create Argument
|
||||
</button>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Quick Reference Guide */}
|
||||
<details className="group">
|
||||
<summary className="cursor-pointer list-none p-12 bg-heat-4 rounded-8 border border-heat-100 text-body-small text-accent-black hover:bg-heat-8 transition-colors">
|
||||
📖 Variable Reference Guide
|
||||
</summary>
|
||||
<div className="mt-8 p-12 bg-heat-4 rounded-8 border border-heat-100 space-y-6 text-body-small text-accent-black font-mono">
|
||||
<div>
|
||||
<strong>Workflow Input:</strong>
|
||||
<code className="block mt-4 text-heat-100">state.variables.input</code>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Previous Node Output:</strong>
|
||||
<code className="block mt-4 text-heat-100">state.variables.lastOutput</code>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Specific Node Output:</strong>
|
||||
<code className="block mt-4 text-heat-100">state.variables.node_1.price</code>
|
||||
<code className="block mt-2 text-heat-100">state.variables.agent_extract.data</code>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Custom Variables:</strong>
|
||||
<code className="block mt-4 text-heat-100">state.variables.myCustomVar</code>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
"use client";
|
||||
|
||||
import type { Node } from "@xyflow/react";
|
||||
|
||||
interface NodeIOBadgesProps {
|
||||
node: Node;
|
||||
}
|
||||
|
||||
export default function NodeIOBadges({ node }: NodeIOBadgesProps) {
|
||||
const nodeData = node.data as any;
|
||||
const nodeType = nodeData.nodeType;
|
||||
|
||||
// Get output info (just show output, not input)
|
||||
const getOutputInfo = (): string | null => {
|
||||
if (nodeType === 'end') return null;
|
||||
if (nodeType === 'note') return null;
|
||||
if (nodeType === 'start') return null;
|
||||
|
||||
// Check output mapping
|
||||
if (nodeData.outputMapping) {
|
||||
if (nodeData.outputMapping.outputAs === 'field') {
|
||||
return nodeData.outputMapping.fieldName;
|
||||
}
|
||||
if (nodeData.outputMapping.outputAs === 'custom') {
|
||||
return 'custom';
|
||||
}
|
||||
}
|
||||
|
||||
// Check specific field outputs
|
||||
if (nodeData.outputField && nodeData.outputField !== 'full') {
|
||||
return nodeData.outputField;
|
||||
}
|
||||
|
||||
// Check for JSON schema
|
||||
if (nodeData.jsonOutputSchema) {
|
||||
try {
|
||||
const schema = JSON.parse(nodeData.jsonOutputSchema);
|
||||
if (schema.properties) {
|
||||
const fields = Object.keys(schema.properties);
|
||||
if (fields.length === 1) {
|
||||
return fields[0];
|
||||
}
|
||||
if (fields.length === 2) {
|
||||
return fields.join(', ');
|
||||
}
|
||||
return `${fields.length} fields`;
|
||||
}
|
||||
} catch (e) {
|
||||
// Invalid schema
|
||||
}
|
||||
}
|
||||
|
||||
return null; // Don't show badge if no specific output
|
||||
};
|
||||
|
||||
const outputInfo = getOutputInfo();
|
||||
|
||||
if (!outputInfo) return null;
|
||||
|
||||
return (
|
||||
<div className="absolute -bottom-3 right-2 pointer-events-none">
|
||||
{/* Subtle Output Indicator */}
|
||||
<div className="px-4 py-1 bg-black-alpha-4 text-black-alpha-48 text-[9px] font-mono rounded-full border border-border-faint whitespace-nowrap">
|
||||
→ {outputInfo}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,107 @@
|
||||
"use client";
|
||||
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { useState, useEffect } from "react";
|
||||
import type { Node } from "@xyflow/react";
|
||||
|
||||
interface NoteNodePanelProps {
|
||||
node: Node | null;
|
||||
onClose: () => void;
|
||||
onDelete: (nodeId: string) => void;
|
||||
onUpdate: (nodeId: string, data: any) => void;
|
||||
}
|
||||
|
||||
export default function NoteNodePanel({ node, onClose, onDelete, onUpdate }: NoteNodePanelProps) {
|
||||
const nodeData = node?.data as any;
|
||||
const [noteText, setNoteText] = useState(nodeData?.noteText || "Add your notes here...");
|
||||
|
||||
// Auto-save changes
|
||||
useEffect(() => {
|
||||
if (!node?.id) return;
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
onUpdate(node.id, {
|
||||
noteText,
|
||||
nodeName: noteText.slice(0, 30) + (noteText.length > 30 ? '...' : ''), // Update node label
|
||||
});
|
||||
}, 500);
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [noteText]);
|
||||
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{node && (
|
||||
<motion.aside
|
||||
initial={{ x: 400, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
exit={{ x: 400, opacity: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="fixed right-20 top-80 h-[calc(100vh-100px)] w-[calc(100vw-240px)] max-w-480 bg-accent-white border border-border-faint shadow-lg overflow-y-auto z-50 rounded-16"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="p-20 border-b border-border-faint bg-heat-4">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div className="flex items-center gap-8">
|
||||
<svg className="w-20 h-20 text-heat-100" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z" />
|
||||
</svg>
|
||||
<h2 className="text-title-h3 text-accent-black">Sticky Note</h2>
|
||||
</div>
|
||||
<div className="flex items-center gap-8">
|
||||
<button
|
||||
onClick={() => onDelete(node?.id || '')}
|
||||
className="w-32 h-32 rounded-6 hover:bg-heat-8 transition-colors flex items-center justify-center group"
|
||||
title="Delete note"
|
||||
>
|
||||
<svg className="w-16 h-16 text-heat-100 group-hover:text-accent-black" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-32 h-32 rounded-6 hover:bg-heat-8 transition-colors flex items-center justify-center"
|
||||
>
|
||||
<svg className="w-16 h-16 text-heat-100" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-body-small text-heat-100">
|
||||
Visual-only sticky note for documentation
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Form Fields */}
|
||||
<div className="p-20">
|
||||
{/* Sticky Note */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-accent-black mb-8 flex items-center gap-8">
|
||||
<svg className="w-16 h-16 text-heat-100" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
Note Content
|
||||
</label>
|
||||
<textarea
|
||||
value={noteText}
|
||||
onChange={(e) => setNoteText(e.target.value)}
|
||||
rows={10}
|
||||
placeholder="Add documentation, comments, or reminders..."
|
||||
className="w-full px-14 py-10 bg-heat-4 border border-heat-100 rounded-10 text-sm text-accent-black placeholder-black-alpha-48 placeholder:opacity-50 focus:outline-none focus:border-heat-100 transition-colors resize-y font-sans"
|
||||
style={{ minHeight: '200px' }}
|
||||
/>
|
||||
<div className="mt-12 p-12 bg-heat-4 border border-heat-100 rounded-8">
|
||||
<p className="text-xs text-heat-100 leading-relaxed">
|
||||
<strong>Sticky notes are visual-only.</strong> They don't execute or connect to other nodes.
|
||||
Use them to document your workflow, explain logic, or leave reminders.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.aside>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
interface OutputField {
|
||||
name: string;
|
||||
type: "string" | "number" | "boolean" | "object" | "array";
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface OutputSchemaPanelProps {
|
||||
nodeId: string;
|
||||
currentSchema: OutputField[];
|
||||
onUpdate: (schema: OutputField[]) => void;
|
||||
}
|
||||
|
||||
export default function OutputSchemaPanel({ nodeId, currentSchema, onUpdate }: OutputSchemaPanelProps) {
|
||||
const [schema, setSchema] = useState<OutputField[]>(currentSchema || []);
|
||||
|
||||
useEffect(() => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
onUpdate(schema);
|
||||
}, 500);
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [schema, onUpdate]);
|
||||
|
||||
const addField = () => {
|
||||
setSchema([
|
||||
...schema,
|
||||
{
|
||||
name: `field${schema.length + 1}`,
|
||||
type: "string",
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const updateField = (index: number, updates: Partial<OutputField>) => {
|
||||
setSchema(
|
||||
schema.map((field, i) => (i === index ? { ...field, ...updates } : field))
|
||||
);
|
||||
};
|
||||
|
||||
const removeField = (index: number) => {
|
||||
setSchema(schema.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const generateTypeScriptInterface = () => {
|
||||
if (schema.length === 0) return 'interface Output {\n // No fields defined\n}';
|
||||
|
||||
const fields = schema.map(f => {
|
||||
const typeStr = f.type === 'array' ? 'any[]' : f.type;
|
||||
const desc = f.description ? ` // ${f.description}\n` : '';
|
||||
return `${desc} ${f.name}: ${typeStr};`;
|
||||
}).join('\n');
|
||||
|
||||
return `interface ${nodeId.replace(/-/g, '_')}_Output {\n${fields}\n}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-16">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-label-small text-black-alpha-48">
|
||||
Output Schema
|
||||
</h3>
|
||||
<button
|
||||
onClick={addField}
|
||||
className="px-10 py-6 bg-background-base hover:bg-black-alpha-4 border border-border-faint rounded-6 text-body-small text-accent-black transition-colors flex items-center gap-6"
|
||||
>
|
||||
<svg className="w-12 h-12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
Add Field
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{schema.length === 0 ? (
|
||||
<div className="p-16 bg-background-base rounded-8 border border-border-faint text-center">
|
||||
<p className="text-body-small text-black-alpha-48">
|
||||
Define the shape of this node's output
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-12">
|
||||
{schema.map((field, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="p-12 bg-background-base rounded-8 border border-border-faint space-y-8"
|
||||
>
|
||||
<div className="flex items-start gap-8">
|
||||
<div className="flex-1 grid grid-cols-2 gap-8">
|
||||
{/* Field Name */}
|
||||
<input
|
||||
type="text"
|
||||
value={field.name}
|
||||
onChange={(e) => updateField(index, { name: e.target.value })}
|
||||
placeholder="fieldName"
|
||||
className="px-10 py-6 bg-white border border-border-faint rounded-6 text-body-small text-accent-black font-mono focus:outline-none focus:border-heat-100 transition-colors"
|
||||
/>
|
||||
|
||||
{/* Field Type */}
|
||||
<select
|
||||
value={field.type}
|
||||
onChange={(e) => updateField(index, { type: e.target.value as any })}
|
||||
className="px-10 py-6 bg-white border border-border-faint rounded-6 text-body-small text-accent-black focus:outline-none focus:border-heat-100 transition-colors"
|
||||
>
|
||||
<option value="string">String</option>
|
||||
<option value="number">Number</option>
|
||||
<option value="boolean">Boolean</option>
|
||||
<option value="object">Object</option>
|
||||
<option value="array">Array</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Remove Button */}
|
||||
<button
|
||||
onClick={() => removeField(index)}
|
||||
className="w-24 h-24 rounded-4 hover:bg-black-alpha-4 transition-colors flex items-center justify-center group"
|
||||
>
|
||||
<svg className="w-12 h-12 text-black-alpha-48 group-hover:text-accent-black" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Field Description */}
|
||||
<input
|
||||
type="text"
|
||||
value={field.description || ''}
|
||||
onChange={(e) => updateField(index, { description: e.target.value })}
|
||||
placeholder="Field description (optional)"
|
||||
className="w-full px-10 py-6 bg-white border border-border-faint rounded-6 text-body-small text-black-alpha-48 focus:outline-none focus:border-heat-100 transition-colors"
|
||||
/>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* TypeScript Preview */}
|
||||
{schema.length > 0 && (
|
||||
<div>
|
||||
<label className="block text-label-small text-black-alpha-48 mb-8">
|
||||
TypeScript Interface
|
||||
</label>
|
||||
<div className="p-12 bg-gray-900 rounded-8 border border-border-faint">
|
||||
<pre className="text-body-small text-heat-100 font-mono whitespace-pre-wrap">
|
||||
{generateTypeScriptInterface()}
|
||||
</pre>
|
||||
</div>
|
||||
<p className="text-body-small text-black-alpha-48 mt-8">
|
||||
Access fields: <code className="font-mono text-heat-100">state.variables.{nodeId}.fieldName</code>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+281
@@ -0,0 +1,281 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { useState } from "react";
|
||||
import { AlertCircle, Loader2, ExternalLink } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface PasteConfigModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (servers: any[]) => Promise<void>;
|
||||
}
|
||||
|
||||
export default function PasteConfigModal({ isOpen, onClose, onSave }: PasteConfigModalProps) {
|
||||
const [configJSON, setConfigJSON] = useState('');
|
||||
const [parsing, setParsing] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const parseAndSave = async () => {
|
||||
setParsing(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
// Parse the JSON
|
||||
const config = JSON.parse(configJSON);
|
||||
|
||||
// Extract MCP servers from Cursor/Cline format
|
||||
const servers: any[] = [];
|
||||
|
||||
if (config.mcpServers) {
|
||||
for (const [serverName, serverConfig] of Object.entries(config.mcpServers)) {
|
||||
const typedConfig = serverConfig as any;
|
||||
|
||||
// Determine the server type and URL
|
||||
let name = serverName;
|
||||
let url = '';
|
||||
let category = 'custom';
|
||||
let authType = 'none';
|
||||
let accessToken = '';
|
||||
let description = '';
|
||||
let headers: any = null;
|
||||
|
||||
// Check if it's a direct URL configuration (like Context7)
|
||||
if (typedConfig.url) {
|
||||
url = typedConfig.url;
|
||||
|
||||
// Handle headers-based authentication
|
||||
if (typedConfig.headers) {
|
||||
headers = typedConfig.headers;
|
||||
authType = 'api-key';
|
||||
|
||||
// Extract API key from headers
|
||||
const headerKeys = Object.keys(typedConfig.headers);
|
||||
if (headerKeys.length > 0) {
|
||||
// Find the key that contains API_KEY
|
||||
const apiKeyHeader = headerKeys.find(key => key.includes('API_KEY')) || headerKeys[0];
|
||||
accessToken = typedConfig.headers[apiKeyHeader];
|
||||
}
|
||||
}
|
||||
|
||||
// Identify known services by URL or name
|
||||
if (serverName.includes('context7') || url.includes('context7')) {
|
||||
name = 'Context7';
|
||||
category = 'ai';
|
||||
description = 'Documentation and code assistance';
|
||||
} else if (serverName.includes('firecrawl') || url.includes('firecrawl')) {
|
||||
name = 'Firecrawl';
|
||||
category = 'web';
|
||||
description = 'Web scraping, searching, and data extraction';
|
||||
}
|
||||
|
||||
} else if (typedConfig.command === 'npx' && typedConfig.args) {
|
||||
// Handle npx-style configurations (Firecrawl, etc.)
|
||||
const packageName = typedConfig.args.find((arg: string) => arg !== '-y' && !arg.startsWith('-'));
|
||||
|
||||
// Identify known MCPs
|
||||
if (packageName === 'firecrawl-mcp' || serverName.includes('firecrawl')) {
|
||||
name = 'Firecrawl';
|
||||
category = 'web';
|
||||
authType = 'api-key';
|
||||
description = 'Web scraping, searching, and data extraction';
|
||||
|
||||
// Extract API key from env
|
||||
if (typedConfig.env?.FIRECRAWL_API_KEY) {
|
||||
accessToken = typedConfig.env.FIRECRAWL_API_KEY;
|
||||
url = `https://mcp.firecrawl.dev/${accessToken}/v2/mcp`;
|
||||
} else {
|
||||
url = 'https://mcp.firecrawl.dev/{FIRECRAWL_API_KEY}/v2/mcp';
|
||||
}
|
||||
} else {
|
||||
// Generic MCP server
|
||||
name = packageName || serverName;
|
||||
url = `npx -y ${packageName || serverName}`;
|
||||
|
||||
// Check for API keys in env
|
||||
if (typedConfig.env) {
|
||||
const envKeys = Object.keys(typedConfig.env);
|
||||
if (envKeys.length > 0) {
|
||||
authType = 'api-key';
|
||||
// Take the first API key found
|
||||
accessToken = typedConfig.env[envKeys[0]];
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Unsupported format, skip
|
||||
console.warn(`Skipping unsupported MCP config format for ${serverName}`, typedConfig);
|
||||
continue;
|
||||
}
|
||||
|
||||
servers.push({
|
||||
name: name.replace(/-mcp$/, '').replace(/mcp-/, '').replace(/-/g, ' ').split(' ').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),
|
||||
url,
|
||||
description,
|
||||
category,
|
||||
authType,
|
||||
accessToken,
|
||||
headers,
|
||||
tools: [], // Will be discovered on test
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (servers.length === 0) {
|
||||
throw new Error('No MCP servers found in configuration');
|
||||
}
|
||||
|
||||
// Save all servers (onSave will handle testing)
|
||||
await onSave(servers);
|
||||
|
||||
// Don't show success here as onSave will show individual results
|
||||
onClose();
|
||||
setConfigJSON('');
|
||||
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Invalid JSON configuration');
|
||||
} finally {
|
||||
setParsing(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 bg-black-alpha-16 backdrop-blur-sm flex items-center justify-center z-[100]"
|
||||
onClick={onClose}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ scale: 0.9, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
exit={{ scale: 0.9, opacity: 0 }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="bg-accent-white rounded-16 shadow-2xl max-w-2xl w-full mx-20 flex flex-col"
|
||||
style={{ maxHeight: '85vh' }}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="p-20 border-b border-border-faint flex-shrink-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-title-h4 text-accent-black">Paste MCP Configuration</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-32 h-32 rounded-6 hover:bg-black-alpha-4 transition-colors flex items-center justify-center"
|
||||
>
|
||||
<svg className="w-16 h-16 text-black-alpha-48" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-body-small text-black-alpha-48 mt-8">
|
||||
Paste your Cursor/Cline MCP configuration JSON below
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="p-20 space-y-16 overflow-y-auto flex-1">
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<label className="text-body-small text-black-alpha-64 block">
|
||||
Configuration JSON
|
||||
</label>
|
||||
<a
|
||||
href="https://www.firecrawl.dev/app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs text-heat-100 hover:text-heat-200 underline flex items-center gap-4"
|
||||
>
|
||||
See example config
|
||||
<ExternalLink className="w-12 h-12" />
|
||||
</a>
|
||||
</div>
|
||||
<textarea
|
||||
value={configJSON}
|
||||
onChange={(e) => setConfigJSON(e.target.value)}
|
||||
placeholder={`// Example 1 - Direct URL format (Context7):
|
||||
{
|
||||
"mcpServers": {
|
||||
"context7": {
|
||||
"url": "https://mcp.context7.com/mcp",
|
||||
"headers": {
|
||||
"CONTEXT7_API_KEY": "your-api-key"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Example 2 - NPX format (Firecrawl):
|
||||
{
|
||||
"mcpServers": {
|
||||
"firecrawl-mcp": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "firecrawl-mcp"],
|
||||
"env": {
|
||||
"FIRECRAWL_API_KEY": "your-api-key"
|
||||
}
|
||||
}
|
||||
}
|
||||
}`}
|
||||
className="w-full h-[300px] px-14 py-10 bg-background-base border border-border-faint rounded-10 text-sm text-accent-black font-mono focus:outline-none focus:border-heat-100 transition-colors resize-none"
|
||||
spellCheck={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="p-12 bg-accent-black text-white rounded-8">
|
||||
<p className="text-body-small">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="p-12 bg-heat-4 rounded-8 border border-heat-100">
|
||||
<div className="flex items-start gap-8">
|
||||
<AlertCircle className="w-16 h-16 text-heat-100 flex-shrink-0 mt-1" />
|
||||
<div>
|
||||
<p className="text-body-small text-accent-black font-medium mb-4">
|
||||
Supported Formats
|
||||
</p>
|
||||
<p className="text-body-small text-black-alpha-64 mb-6">
|
||||
Supports two formats:
|
||||
<br />• Direct URL with headers (Context7, etc.)
|
||||
<br />• NPX command format (Firecrawl, Cursor, Cline)
|
||||
<br />
|
||||
<br />Connections will be tested automatically after import.
|
||||
</p>
|
||||
<p className="text-xs text-black-alpha-48">
|
||||
💡 Tip: Copy your MCP config from your editor's settings.json file
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="p-20 border-t border-border-faint flex gap-8 flex-shrink-0">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="flex-1 px-20 py-12 bg-black-alpha-4 hover:bg-black-alpha-8 text-accent-black rounded-8 text-body-medium font-medium transition-all"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={parseAndSave}
|
||||
disabled={!configJSON.trim() || parsing}
|
||||
className="flex-1 px-20 py-12 bg-heat-100 hover:bg-heat-200 text-white rounded-8 text-body-medium font-medium transition-all disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-6"
|
||||
>
|
||||
{parsing ? (
|
||||
<>
|
||||
<Loader2 className="w-16 h-16 animate-spin" />
|
||||
Importing & Testing...
|
||||
</>
|
||||
) : (
|
||||
'Import & Test'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
"use client";
|
||||
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { WorkflowExecution, NodeExecutionResult } from "@/lib/workflow/types";
|
||||
|
||||
interface PreviewPanelProps {
|
||||
execution: WorkflowExecution | null;
|
||||
nodeResults: Record<string, NodeExecutionResult>;
|
||||
isRunning: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function PreviewPanel({ execution, nodeResults, isRunning, onClose }: PreviewPanelProps) {
|
||||
return (
|
||||
<AnimatePresence>
|
||||
<motion.aside
|
||||
initial={{ x: 400, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
exit={{ x: 400, opacity: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="fixed right-20 top-80 h-[calc(100vh-100px)] w-480 bg-accent-white border border-border-faint shadow-lg overflow-y-auto z-50 rounded-16"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="p-20 border-b border-border-faint sticky top-0 bg-accent-white">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<h2 className="text-title-h3 text-accent-black">Workflow Preview</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-32 h-32 rounded-6 hover:bg-black-alpha-4 transition-colors flex items-center justify-center"
|
||||
>
|
||||
<svg className="w-16 h-16 text-black-alpha-48" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Status Badge */}
|
||||
{execution && (
|
||||
<div className={`inline-flex items-center gap-8 px-12 py-6 rounded-8 text-body-small ${
|
||||
execution.status === 'running' ? 'bg-heat-4 text-heat-100' :
|
||||
execution.status === 'completed' ? 'bg-heat-4 text-heat-100' :
|
||||
execution.status === 'failed' ? 'bg-black-alpha-4 text-accent-black' :
|
||||
'bg-gray-50 text-gray-600'
|
||||
}`}>
|
||||
{execution.status === 'running' && (
|
||||
<svg className="w-12 h-12 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
)}
|
||||
{execution.status}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-20">
|
||||
{isRunning && Object.keys(nodeResults).length === 0 ? (
|
||||
<div className="text-center py-32">
|
||||
<svg className="w-48 h-48 mx-auto mb-16 text-heat-100 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
<p className="text-body-medium text-black-alpha-48">Starting workflow...</p>
|
||||
</div>
|
||||
) : Object.keys(nodeResults).length === 0 ? (
|
||||
<div className="text-center py-32">
|
||||
<svg className="w-48 h-48 mx-auto mb-16 text-black-alpha-32" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<p className="text-body-medium text-black-alpha-48">Click Preview to run workflow</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-16">
|
||||
{Object.entries(nodeResults).map(([nodeId, result]) => (
|
||||
<div key={nodeId} className="bg-background-base rounded-12 p-16 border border-border-faint">
|
||||
{/* Node Header */}
|
||||
<div className="flex items-center justify-between mb-12">
|
||||
<h3 className="text-label-medium text-accent-black font-medium">
|
||||
{nodeId}
|
||||
</h3>
|
||||
<span className={`text-body-small px-8 py-4 rounded-6 ${
|
||||
result.status === 'running' ? 'bg-heat-4 text-heat-100' :
|
||||
result.status === 'completed' ? 'bg-heat-4 text-heat-100' :
|
||||
result.status === 'failed' ? 'bg-black-alpha-4 text-accent-black' :
|
||||
'bg-gray-50 text-gray-600'
|
||||
}`}>
|
||||
{result.status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Node Output */}
|
||||
{result.output && (
|
||||
<div className="mt-12">
|
||||
<p className="text-body-small text-black-alpha-48 mb-8">Output:</p>
|
||||
<div className="bg-accent-white rounded-8 p-12 border border-border-faint">
|
||||
<pre className="text-body-small text-accent-black whitespace-pre-wrap overflow-auto max-h-200">
|
||||
{typeof result.output === 'string'
|
||||
? result.output
|
||||
: JSON.stringify(result.output, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error */}
|
||||
{result.error && (
|
||||
<div className="mt-12">
|
||||
<p className="text-body-small text-accent-black mb-8">Error:</p>
|
||||
<div className="bg-black-alpha-4 rounded-8 p-12 border border-border-faint">
|
||||
<pre className="text-body-small text-accent-black whitespace-pre-wrap">
|
||||
{result.error}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Timing */}
|
||||
{result.startedAt && result.completedAt && (
|
||||
<p className="text-body-small text-black-alpha-32 mt-8">
|
||||
Duration: {Math.round((new Date(result.completedAt).getTime() - new Date(result.startedAt).getTime()) / 1000)}s
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.aside>
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
"use client";
|
||||
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { useState } from "react";
|
||||
import { Workflow } from "@/lib/workflow/types";
|
||||
|
||||
interface PublishModalProps {
|
||||
workflow: Workflow | null;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onPublish: (config: PublishConfig) => void;
|
||||
}
|
||||
|
||||
interface PublishConfig {
|
||||
name: string;
|
||||
description: string;
|
||||
isPublic: boolean;
|
||||
requiresAuth: boolean;
|
||||
}
|
||||
|
||||
export default function PublishModal({ workflow, isOpen, onClose, onPublish }: PublishModalProps) {
|
||||
const [name, setName] = useState(workflow?.name || "My Workflow");
|
||||
const [description, setDescription] = useState(workflow?.description || "");
|
||||
const [isPublic, setIsPublic] = useState(false);
|
||||
const [requiresAuth, setRequiresAuth] = useState(true);
|
||||
const [published, setPublished] = useState(false);
|
||||
|
||||
const handlePublish = () => {
|
||||
onPublish({
|
||||
name,
|
||||
description,
|
||||
isPublic,
|
||||
requiresAuth,
|
||||
});
|
||||
setPublished(true);
|
||||
};
|
||||
|
||||
const endpointUrl = workflow
|
||||
? `${typeof window !== 'undefined' ? window.location.origin : ''}/api/workflows/${workflow.id}/execute`
|
||||
: '';
|
||||
|
||||
const handleCopyEndpoint = () => {
|
||||
navigator.clipboard.writeText(endpointUrl);
|
||||
};
|
||||
|
||||
const handleCopyCurl = () => {
|
||||
const curl = `curl -X POST ${endpointUrl} \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{"input": "Your input message here"}'`;
|
||||
navigator.clipboard.writeText(curl);
|
||||
};
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 bg-black-alpha-48 z-[100] flex items-center justify-center p-20"
|
||||
onClick={onClose}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ scale: 0.95, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
exit={{ scale: 0.95, opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="bg-accent-white rounded-16 shadow-2xl max-w-600 w-full max-h-[80vh] overflow-y-auto"
|
||||
>
|
||||
{!published ? (
|
||||
<>
|
||||
{/* Header */}
|
||||
<div className="p-24 border-b border-border-faint">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-title-h3 text-accent-black">Publish Workflow</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-32 h-32 rounded-6 hover:bg-black-alpha-4 transition-colors flex items-center justify-center"
|
||||
>
|
||||
<svg className="w-16 h-16 text-black-alpha-48" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-body-small text-black-alpha-48 mt-8">
|
||||
Publish your workflow as an API endpoint
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Form */}
|
||||
<div className="p-24 space-y-20">
|
||||
{/* Name */}
|
||||
<div>
|
||||
<label className="block text-label-small text-black-alpha-48 mb-8">
|
||||
Workflow Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="w-full px-12 py-10 bg-background-base border border-border-faint rounded-8 text-body-medium text-accent-black focus:outline-none focus:border-heat-100 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div>
|
||||
<label className="block text-label-small text-black-alpha-48 mb-8">
|
||||
Description (optional)
|
||||
</label>
|
||||
<textarea
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
rows={3}
|
||||
className="w-full px-12 py-10 bg-background-base border border-border-faint rounded-8 text-body-medium text-accent-black focus:outline-none focus:border-heat-100 transition-colors resize-none"
|
||||
placeholder="Describe what this workflow does..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Public Access */}
|
||||
<div className="flex items-center justify-between p-16 bg-background-base rounded-12 border border-border-faint">
|
||||
<div>
|
||||
<h3 className="text-label-small text-accent-black mb-4">Public Access</h3>
|
||||
<p className="text-body-small text-black-alpha-48">
|
||||
Allow anyone to call this endpoint
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsPublic(!isPublic)}
|
||||
className={`w-44 h-24 rounded-full transition-colors relative ${
|
||||
isPublic ? 'bg-heat-100' : 'bg-black-alpha-12'
|
||||
}`}
|
||||
>
|
||||
<motion.div
|
||||
className="w-20 h-20 bg-white rounded-full absolute top-2 shadow-sm"
|
||||
animate={{ left: isPublic ? '22px' : '2px' }}
|
||||
transition={{ duration: 0.2 }}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Require Authentication */}
|
||||
<div className="flex items-center justify-between p-16 bg-background-base rounded-12 border border-border-faint">
|
||||
<div>
|
||||
<h3 className="text-label-small text-accent-black mb-4">Require Authentication</h3>
|
||||
<p className="text-body-small text-black-alpha-48">
|
||||
Require API key for execution
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setRequiresAuth(!requiresAuth)}
|
||||
className={`w-44 h-24 rounded-full transition-colors relative ${
|
||||
requiresAuth ? 'bg-heat-100' : 'bg-black-alpha-12'
|
||||
}`}
|
||||
>
|
||||
<motion.div
|
||||
className="w-20 h-20 bg-white rounded-full absolute top-2 shadow-sm"
|
||||
animate={{ left: requiresAuth ? '22px' : '2px' }}
|
||||
transition={{ duration: 0.2 }}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Workflow Stats */}
|
||||
{workflow && (
|
||||
<div className="p-16 bg-blue-50 rounded-12 border border-blue-200">
|
||||
<h3 className="text-label-small text-blue-900 mb-12">Workflow Summary</h3>
|
||||
<div className="grid grid-cols-2 gap-12 text-body-small">
|
||||
<div>
|
||||
<span className="text-blue-600">Nodes:</span>
|
||||
<span className="text-blue-900 font-medium ml-8">{workflow.nodes.length}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-blue-600">Connections:</span>
|
||||
<span className="text-blue-900 font-medium ml-8">{workflow.edges.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="p-24 border-t border-border-faint flex items-center justify-between">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-20 py-10 text-body-medium text-black-alpha-48 hover:text-accent-black transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handlePublish}
|
||||
className="px-32 py-10 bg-heat-100 hover:bg-heat-200 text-white rounded-8 transition-all active:scale-[0.98] text-body-medium font-medium"
|
||||
>
|
||||
Publish Workflow
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* Success State */}
|
||||
<div className="p-24">
|
||||
<div className="text-center mb-24">
|
||||
<div className="w-64 h-64 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-16">
|
||||
<svg className="w-32 h-32 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="text-title-h3 text-accent-black mb-8">Workflow Published!</h2>
|
||||
<p className="text-body-medium text-black-alpha-48">
|
||||
Your workflow is now available as an API endpoint
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Endpoint URL */}
|
||||
<div className="mb-24">
|
||||
<label className="block text-label-small text-black-alpha-48 mb-8">
|
||||
API Endpoint
|
||||
</label>
|
||||
<div className="flex gap-8">
|
||||
<div className="flex-1 px-12 py-10 bg-background-base border border-border-faint rounded-8 text-body-small text-accent-black font-mono overflow-x-auto">
|
||||
{endpointUrl}
|
||||
</div>
|
||||
<button
|
||||
onClick={handleCopyEndpoint}
|
||||
className="px-16 py-10 bg-background-base hover:bg-black-alpha-4 border border-border-faint rounded-8 text-body-small text-accent-black transition-colors"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* cURL Example */}
|
||||
<div className="mb-24">
|
||||
<label className="block text-label-small text-black-alpha-48 mb-8">
|
||||
Example cURL Request
|
||||
</label>
|
||||
<div className="relative">
|
||||
<pre className="px-12 py-10 bg-gray-900 text-green-400 rounded-8 text-body-small font-mono overflow-x-auto">
|
||||
{`curl -X POST ${endpointUrl} \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{"input": "Your input message"}'`}
|
||||
</pre>
|
||||
<button
|
||||
onClick={handleCopyCurl}
|
||||
className="absolute top-8 right-8 px-12 py-6 bg-white/10 hover:bg-white/20 rounded-6 text-body-small text-white transition-colors"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* JavaScript Example */}
|
||||
<div className="mb-24">
|
||||
<label className="block text-label-small text-black-alpha-48 mb-8">
|
||||
JavaScript Example
|
||||
</label>
|
||||
<pre className="px-12 py-10 bg-gray-900 text-green-400 rounded-8 text-body-small font-mono overflow-x-auto">
|
||||
{`const response = await fetch('${endpointUrl}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
input: 'Your input message'
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
console.log(result);`}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="p-24 border-t border-border-faint">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-full px-20 py-10 bg-heat-100 hover:bg-heat-200 text-white rounded-8 transition-all active:scale-[0.98] text-body-medium font-medium"
|
||||
>
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
+364
@@ -0,0 +1,364 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { X, Save, Tag, FolderOpen, Clock, BarChart3, Globe, Lock, Info } from "lucide-react";
|
||||
import { useMutation } from "convex/react";
|
||||
import { api } from "@/convex/_generated/api";
|
||||
import { toast } from "sonner";
|
||||
import { useUser } from "@clerk/nextjs";
|
||||
|
||||
interface SaveAsTemplateModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
workflowId: string;
|
||||
workflowName: string;
|
||||
}
|
||||
|
||||
const categories = [
|
||||
"User Templates",
|
||||
"AI & Automation",
|
||||
"Data Processing",
|
||||
"Web Scraping",
|
||||
"Business Logic",
|
||||
"Integration",
|
||||
"Utility",
|
||||
"Custom",
|
||||
];
|
||||
|
||||
const difficultyLevels = [
|
||||
{ value: "beginner", label: "Beginner", color: "text-green-600" },
|
||||
{ value: "intermediate", label: "Intermediate", color: "text-yellow-600" },
|
||||
{ value: "advanced", label: "Advanced", color: "text-orange-600" },
|
||||
{ value: "expert", label: "Expert", color: "text-red-600" },
|
||||
];
|
||||
|
||||
export default function SaveAsTemplateModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
workflowId,
|
||||
workflowName,
|
||||
}: SaveAsTemplateModalProps) {
|
||||
const { user } = useUser();
|
||||
const saveAsTemplate = useMutation(api.templates.saveAsTemplate);
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
name: `${workflowName} Template`,
|
||||
description: "",
|
||||
category: "User Templates",
|
||||
tags: [] as string[],
|
||||
difficulty: "intermediate",
|
||||
estimatedTime: "",
|
||||
isPublic: false,
|
||||
});
|
||||
|
||||
const [currentTag, setCurrentTag] = useState("");
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
const handleAddTag = () => {
|
||||
if (currentTag.trim() && !formData.tags.includes(currentTag.trim())) {
|
||||
setFormData({
|
||||
...formData,
|
||||
tags: [...formData.tags, currentTag.trim()],
|
||||
});
|
||||
setCurrentTag("");
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveTag = (tag: string) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
tags: formData.tags.filter((t) => t !== tag),
|
||||
});
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!formData.name.trim()) {
|
||||
toast.error("Template name is required");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!workflowId) {
|
||||
toast.error("Invalid workflow");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
|
||||
try {
|
||||
await saveAsTemplate({
|
||||
workflowId: workflowId as any,
|
||||
name: formData.name,
|
||||
description: formData.description || undefined,
|
||||
category: formData.category,
|
||||
tags: formData.tags.length > 0 ? formData.tags : undefined,
|
||||
difficulty: formData.difficulty || undefined,
|
||||
estimatedTime: formData.estimatedTime || undefined,
|
||||
isPublic: formData.isPublic,
|
||||
});
|
||||
|
||||
toast.success("Template saved successfully!", {
|
||||
description: formData.isPublic
|
||||
? "Your template is now available to other users"
|
||||
: "Your template has been saved to your private collection",
|
||||
});
|
||||
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error("Failed to save template:", error);
|
||||
toast.error("Failed to save template", {
|
||||
description: error instanceof Error ? error.message : "Please try again",
|
||||
});
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 bg-black/60 z-[100] flex items-center justify-center p-20"
|
||||
onClick={onClose}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ scale: 0.95, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
exit={{ scale: 0.95, opacity: 0 }}
|
||||
transition={{ type: "spring", duration: 0.3 }}
|
||||
className="bg-accent-white rounded-16 shadow-2xl max-w-2xl w-full max-h-[85vh] overflow-hidden flex flex-col"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="p-24 border-b border-border-faint">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-12">
|
||||
<div className="w-40 h-40 rounded-10 bg-heat-100 flex items-center justify-center">
|
||||
<Save className="w-20 h-20 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-title-h4 text-accent-black">Save as Template</h2>
|
||||
<p className="text-body-small text-black-alpha-48 mt-2">
|
||||
Save this workflow as a reusable template
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-32 h-32 rounded-8 hover:bg-black-alpha-4 transition-colors flex items-center justify-center"
|
||||
>
|
||||
<X className="w-16 h-16" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-24 overflow-y-auto flex-1 space-y-20">
|
||||
{/* Template Name */}
|
||||
<div>
|
||||
<label className="text-body-small font-medium text-accent-black mb-8 block">
|
||||
Template Name *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
placeholder="Enter template name"
|
||||
className="w-full px-12 py-10 bg-background-base border border-border-faint rounded-8 text-body-medium text-accent-black placeholder:text-black-alpha-32 focus:outline-none focus:ring-2 focus:ring-heat-100"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div>
|
||||
<label className="text-body-small font-medium text-accent-black mb-8 block">
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||
placeholder="Describe what this template does and when to use it"
|
||||
rows={3}
|
||||
className="w-full px-12 py-10 bg-background-base border border-border-faint rounded-8 text-body-medium text-accent-black placeholder:text-black-alpha-32 focus:outline-none focus:ring-2 focus:ring-heat-100 resize-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Category */}
|
||||
<div>
|
||||
<label className="text-body-small font-medium text-accent-black mb-8 flex items-center gap-6">
|
||||
<FolderOpen className="w-14 h-14" />
|
||||
Category
|
||||
</label>
|
||||
<select
|
||||
value={formData.category}
|
||||
onChange={(e) => setFormData({ ...formData, category: e.target.value })}
|
||||
className="w-full px-12 py-10 bg-background-base border border-border-faint rounded-8 text-body-medium text-accent-black focus:outline-none focus:ring-2 focus:ring-heat-100"
|
||||
>
|
||||
{categories.map((cat) => (
|
||||
<option key={cat} value={cat}>
|
||||
{cat}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
<div>
|
||||
<label className="text-body-small font-medium text-accent-black mb-8 flex items-center gap-6">
|
||||
<Tag className="w-14 h-14" />
|
||||
Tags
|
||||
</label>
|
||||
<div className="flex gap-8 mb-8">
|
||||
<input
|
||||
type="text"
|
||||
value={currentTag}
|
||||
onChange={(e) => setCurrentTag(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
handleAddTag();
|
||||
}
|
||||
}}
|
||||
placeholder="Add tags (press Enter)"
|
||||
className="flex-1 px-12 py-10 bg-background-base border border-border-faint rounded-8 text-body-medium text-accent-black placeholder:text-black-alpha-32 focus:outline-none focus:ring-2 focus:ring-heat-100"
|
||||
/>
|
||||
<button
|
||||
onClick={handleAddTag}
|
||||
className="px-16 py-10 bg-black-alpha-4 hover:bg-black-alpha-8 text-accent-black rounded-8 text-body-small font-medium transition-all"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
{formData.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-6">
|
||||
{formData.tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="px-10 py-4 bg-heat-4 text-heat-100 rounded-6 text-body-small flex items-center gap-4"
|
||||
>
|
||||
{tag}
|
||||
<button
|
||||
onClick={() => handleRemoveTag(tag)}
|
||||
className="hover:text-heat-200"
|
||||
>
|
||||
<X className="w-12 h-12" />
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Difficulty & Time */}
|
||||
<div className="grid grid-cols-2 gap-16">
|
||||
<div>
|
||||
<label className="text-body-small font-medium text-accent-black mb-8 flex items-center gap-6">
|
||||
<BarChart3 className="w-14 h-14" />
|
||||
Difficulty
|
||||
</label>
|
||||
<select
|
||||
value={formData.difficulty}
|
||||
onChange={(e) => setFormData({ ...formData, difficulty: e.target.value })}
|
||||
className="w-full px-12 py-10 bg-background-base border border-border-faint rounded-8 text-body-medium text-accent-black focus:outline-none focus:ring-2 focus:ring-heat-100"
|
||||
>
|
||||
{difficultyLevels.map((level) => (
|
||||
<option key={level.value} value={level.value}>
|
||||
{level.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-body-small font-medium text-accent-black mb-8 flex items-center gap-6">
|
||||
<Clock className="w-14 h-14" />
|
||||
Estimated Time
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.estimatedTime}
|
||||
onChange={(e) => setFormData({ ...formData, estimatedTime: e.target.value })}
|
||||
placeholder="e.g., 5-10 minutes"
|
||||
className="w-full px-12 py-10 bg-background-base border border-border-faint rounded-8 text-body-medium text-accent-black placeholder:text-black-alpha-32 focus:outline-none focus:ring-2 focus:ring-heat-100"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Visibility */}
|
||||
<div className="p-16 bg-background-base rounded-8 border border-border-faint">
|
||||
<div className="flex items-start gap-12">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="isPublic"
|
||||
checked={formData.isPublic}
|
||||
onChange={(e) => setFormData({ ...formData, isPublic: e.target.checked })}
|
||||
className="mt-4 w-16 h-16 rounded-4 border-border-faint text-heat-100 focus:ring-heat-100"
|
||||
/>
|
||||
<label htmlFor="isPublic" className="flex-1 cursor-pointer">
|
||||
<div className="flex items-center gap-8 mb-4">
|
||||
{formData.isPublic ? (
|
||||
<Globe className="w-16 h-16 text-heat-100" />
|
||||
) : (
|
||||
<Lock className="w-16 h-16 text-black-alpha-48" />
|
||||
)}
|
||||
<span className="text-body-medium font-medium text-accent-black">
|
||||
{formData.isPublic ? "Public Template" : "Private Template"}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-body-small text-black-alpha-48">
|
||||
{formData.isPublic
|
||||
? "This template will be visible to all users and can be used by anyone"
|
||||
: "This template will only be visible to you"}
|
||||
</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info Box */}
|
||||
<div className="p-16 bg-heat-4 border border-heat-100 rounded-8">
|
||||
<div className="flex items-start gap-12">
|
||||
<Info className="w-16 h-16 text-heat-100 flex-shrink-0 mt-2" />
|
||||
<div>
|
||||
<p className="text-body-small text-accent-black">
|
||||
Templates allow you to save and reuse workflow configurations. You can create
|
||||
new workflows from your templates at any time.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="p-20 border-t border-border-faint bg-background-lighter flex justify-between">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-20 py-10 bg-black-alpha-4 hover:bg-black-alpha-8 text-accent-black rounded-8 text-body-medium font-medium transition-all"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={isSaving || !formData.name.trim()}
|
||||
className="px-20 py-10 bg-heat-100 hover:bg-heat-200 text-white rounded-8 text-body-medium font-medium transition-all active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-8"
|
||||
>
|
||||
{isSaving ? (
|
||||
<>
|
||||
<div className="w-16 h-16 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||||
Saving...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="w-16 h-16" />
|
||||
Save as Template
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
+1311
File diff suppressed because it is too large
Load Diff
+245
@@ -0,0 +1,245 @@
|
||||
"use client";
|
||||
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface ShareWorkflowModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
workflowId: string;
|
||||
workflowName: string;
|
||||
}
|
||||
|
||||
export default function ShareWorkflowModal({ isOpen, onClose, workflowId, workflowName }: ShareWorkflowModalProps) {
|
||||
const [copied, setCopied] = useState<string | null>(null);
|
||||
|
||||
const baseUrl = typeof window !== 'undefined'
|
||||
? window.location.origin
|
||||
: 'http://localhost:3000';
|
||||
|
||||
const executeUrl = `${baseUrl}/api/workflows/${workflowId}/execute`;
|
||||
const streamUrl = `${baseUrl}/api/workflows/${workflowId}/execute-stream`;
|
||||
|
||||
const curlExample = `curl -X POST ${executeUrl} \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{"url": "https://example.com"}'`;
|
||||
|
||||
const curlStreamExample = `curl -X POST ${streamUrl} \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{"url": "https://example.com"}' \\
|
||||
--no-buffer`;
|
||||
|
||||
const fetchExample = `// JavaScript/TypeScript
|
||||
const response = await fetch('${executeUrl}', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ url: 'https://example.com' }),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
console.log(result);`;
|
||||
|
||||
const fetchStreamExample = `// JavaScript/TypeScript - Streaming
|
||||
const response = await fetch('${streamUrl}', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ url: 'https://example.com' }),
|
||||
});
|
||||
|
||||
const reader = response.body?.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
const chunk = decoder.decode(value);
|
||||
const events = chunk.split('\\n\\n');
|
||||
|
||||
for (const event of events) {
|
||||
if (event.startsWith('data: ')) {
|
||||
const data = JSON.parse(event.slice(6));
|
||||
console.log('Update:', data);
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
const handleCopy = (text: string, label: string) => {
|
||||
navigator.clipboard.writeText(text);
|
||||
setCopied(label);
|
||||
toast.success('Copied to clipboard!');
|
||||
setTimeout(() => setCopied(null), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
onClick={onClose}
|
||||
className="fixed inset-0 bg-black/50 z-[200]"
|
||||
/>
|
||||
|
||||
{/* Modal */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="fixed inset-0 m-auto w-full max-w-3xl h-fit bg-accent-white rounded-16 border border-border-faint shadow-2xl z-[201] max-h-[90vh] overflow-y-auto"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="p-24 border-b border-border-faint sticky top-0 bg-accent-white rounded-t-16">
|
||||
<div className="flex items-center justify-between mb-12">
|
||||
<div>
|
||||
<h2 className="text-title-h3 text-accent-black mb-4">Workflow Saved!</h2>
|
||||
<p className="text-body-medium text-black-alpha-48">
|
||||
Your workflow is ready to use via API
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-32 h-32 rounded-6 hover:bg-black-alpha-4 transition-colors flex items-center justify-center"
|
||||
>
|
||||
<svg className="w-16 h-16 text-black-alpha-48" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-24 space-y-20">
|
||||
{/* URLs Section */}
|
||||
<div className="space-y-16">
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<label className="text-label-medium text-accent-black font-medium">
|
||||
Standard Execution
|
||||
</label>
|
||||
<button
|
||||
onClick={() => handleCopy(executeUrl, 'url')}
|
||||
className="px-12 py-6 bg-background-base hover:bg-black-alpha-4 border border-border-faint rounded-6 text-body-small transition-colors"
|
||||
>
|
||||
{copied === 'url' ? 'Copied!' : 'Copy'}
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={executeUrl}
|
||||
readOnly
|
||||
className="w-full px-12 py-10 bg-background-base border border-border-faint rounded-8 text-body-small text-accent-black font-mono"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<label className="text-label-medium text-accent-black font-medium">
|
||||
Streaming Execution
|
||||
</label>
|
||||
<button
|
||||
onClick={() => handleCopy(streamUrl, 'stream-url')}
|
||||
className="px-12 py-6 bg-background-base hover:bg-black-alpha-4 border border-border-faint rounded-6 text-body-small transition-colors"
|
||||
>
|
||||
{copied === 'stream-url' ? 'Copied!' : 'Copy'}
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={streamUrl}
|
||||
readOnly
|
||||
className="w-full px-12 py-10 bg-background-base border border-border-faint rounded-8 text-body-small text-accent-black font-mono"
|
||||
/>
|
||||
<p className="text-body-small text-black-alpha-48 mt-6">Real-time updates as each node executes</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border-faint my-20"></div>
|
||||
|
||||
{/* Code Examples */}
|
||||
<div className="space-y-16">
|
||||
<h3 className="text-label-large text-accent-black font-medium">Code Examples</h3>
|
||||
|
||||
{/* cURL */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<label className="text-body-medium text-accent-black">
|
||||
cURL
|
||||
</label>
|
||||
<button
|
||||
onClick={() => handleCopy(curlExample, 'curl')}
|
||||
className="px-12 py-6 bg-background-base hover:bg-black-alpha-4 border border-border-faint rounded-6 text-body-small transition-colors"
|
||||
>
|
||||
{copied === 'curl' ? 'Copied!' : 'Copy'}
|
||||
</button>
|
||||
</div>
|
||||
<pre className="p-16 bg-background-base border border-border-faint rounded-8 text-body-small text-accent-black font-mono overflow-x-auto">
|
||||
{curlExample}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
{/* JavaScript */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<label className="text-body-medium text-accent-black">
|
||||
JavaScript/TypeScript
|
||||
</label>
|
||||
<button
|
||||
onClick={() => handleCopy(fetchExample, 'fetch')}
|
||||
className="px-12 py-6 bg-background-base hover:bg-black-alpha-4 border border-border-faint rounded-6 text-body-small transition-colors"
|
||||
>
|
||||
{copied === 'fetch' ? 'Copied!' : 'Copy'}
|
||||
</button>
|
||||
</div>
|
||||
<pre className="p-16 bg-background-base border border-border-faint rounded-8 text-body-small text-accent-black font-mono overflow-x-auto whitespace-pre-wrap">
|
||||
{fetchExample}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
{/* Streaming Info */}
|
||||
<div className="p-16 bg-background-base rounded-12 border border-border-faint">
|
||||
<div className="flex items-center gap-8 mb-8">
|
||||
<div className="w-6 h-6 bg-heat-100 rounded-full"></div>
|
||||
<p className="text-label-small text-accent-black font-medium">Streaming Available</p>
|
||||
</div>
|
||||
<p className="text-body-small text-black-alpha-48 mb-12">
|
||||
Use <code className="px-6 py-2 bg-white rounded-4 text-accent-black font-mono text-xs">execute-stream</code> endpoint for real-time updates
|
||||
</p>
|
||||
<div className="space-y-4 text-body-small text-black-alpha-48">
|
||||
<div className="flex items-center gap-8">
|
||||
<code className="px-6 py-2 bg-white rounded-4 text-accent-black font-mono text-xs">node_started</code>
|
||||
<span>Node begins</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-8">
|
||||
<code className="px-6 py-2 bg-white rounded-4 text-accent-black font-mono text-xs">node_completed</code>
|
||||
<span>Node finishes</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-8">
|
||||
<code className="px-6 py-2 bg-white rounded-4 text-accent-black font-mono text-xs">workflow_completed</code>
|
||||
<span>All done</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="sticky bottom-0 p-20 bg-accent-white border-t border-border-faint rounded-b-16">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-full px-20 py-12 bg-heat-100 hover:bg-heat-200 text-white rounded-8 text-body-medium font-medium transition-all active:scale-[0.98]"
|
||||
>
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
+230
@@ -0,0 +1,230 @@
|
||||
"use client";
|
||||
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { useState, useEffect } from "react";
|
||||
import type { Node } from "@xyflow/react";
|
||||
|
||||
interface InputVariable {
|
||||
name: string;
|
||||
type: "string" | "number" | "boolean" | "url" | "object";
|
||||
required: boolean;
|
||||
defaultValue?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface StartNodePanelProps {
|
||||
node: Node | null;
|
||||
onClose: () => void;
|
||||
onUpdate: (nodeId: string, data: any) => void;
|
||||
}
|
||||
|
||||
export default function StartNodePanel({ node, onClose, onUpdate }: StartNodePanelProps) {
|
||||
const nodeData = node?.data as any;
|
||||
const [inputVariables, setInputVariables] = useState<InputVariable[]>(
|
||||
nodeData?.inputVariables || [
|
||||
{
|
||||
name: "input_as_text",
|
||||
type: "string",
|
||||
required: false,
|
||||
defaultValue: "",
|
||||
description: "",
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
// Auto-save changes
|
||||
useEffect(() => {
|
||||
if (!node) return;
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
onUpdate(node.id, {
|
||||
inputVariables,
|
||||
});
|
||||
}, 500);
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [inputVariables, node, onUpdate]);
|
||||
|
||||
const addVariable = () => {
|
||||
setInputVariables([
|
||||
...inputVariables,
|
||||
{
|
||||
name: `input${inputVariables.length + 1}`,
|
||||
type: "string",
|
||||
required: false,
|
||||
description: "",
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const updateVariable = (index: number, updates: Partial<InputVariable>) => {
|
||||
setInputVariables(
|
||||
inputVariables.map((v, i) => (i === index ? { ...v, ...updates } : v))
|
||||
);
|
||||
};
|
||||
|
||||
const removeVariable = (index: number) => {
|
||||
setInputVariables(inputVariables.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{node && (
|
||||
<motion.aside
|
||||
initial={{ x: 400, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
exit={{ x: 400, opacity: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="fixed right-20 top-80 h-[calc(100vh-100px)] w-[calc(100vw-240px)] max-w-480 bg-accent-white border border-border-faint shadow-lg overflow-y-auto z-50 rounded-16"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="p-20 border-b border-border-faint">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<h2 className="text-xl font-semibold text-accent-black">Start</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-32 h-32 rounded-6 hover:bg-black-alpha-4 transition-colors flex items-center justify-center"
|
||||
>
|
||||
<svg className="w-18 h-18 text-black-alpha-48" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-sm text-black-alpha-48">
|
||||
Define the workflow inputs
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-20 space-y-20">
|
||||
{/* Input Variables List */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-12">
|
||||
<h3 className="text-sm font-medium text-accent-black">
|
||||
Input variables
|
||||
</h3>
|
||||
<button
|
||||
onClick={addVariable}
|
||||
className="px-12 py-6 bg-background-base hover:bg-black-alpha-4 border border-border-faint rounded-8 text-xs text-accent-black transition-colors flex items-center gap-6"
|
||||
>
|
||||
<svg className="w-14 h-14" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
Add Variable
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-12">
|
||||
{inputVariables.length === 0 ? (
|
||||
<div className="p-20 bg-accent-white border border-border-faint border-dashed rounded-12 text-center">
|
||||
<svg className="w-32 h-32 mx-auto mb-12 text-black-alpha-32" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<p className="text-sm text-black-alpha-48 mb-12">No input variables defined</p>
|
||||
<button
|
||||
onClick={addVariable}
|
||||
className="px-16 py-8 bg-accent-black hover:bg-black-alpha-88 text-white rounded-8 text-sm font-medium transition-colors"
|
||||
>
|
||||
Add First Variable
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
inputVariables.map((variable, index) => (
|
||||
<div key={index} className="p-16 bg-background-base rounded-12 border border-border-faint">
|
||||
<div className="space-y-12">
|
||||
{/* Name */}
|
||||
<div>
|
||||
<label className="block text-xs text-black-alpha-48 mb-6">Variable Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={variable.name}
|
||||
onChange={(e) => updateVariable(index, { name: e.target.value })}
|
||||
className="w-full px-12 py-8 bg-accent-white border border-border-faint rounded-8 text-sm text-accent-black font-mono focus:outline-none focus:border-heat-100"
|
||||
placeholder="variable_name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Type */}
|
||||
<div>
|
||||
<label className="block text-xs text-black-alpha-48 mb-6">Type</label>
|
||||
<select
|
||||
value={variable.type}
|
||||
onChange={(e) => updateVariable(index, { type: e.target.value as any })}
|
||||
className="w-full px-12 py-8 bg-accent-white border border-border-faint rounded-8 text-sm text-accent-black focus:outline-none focus:border-heat-100"
|
||||
>
|
||||
<option value="string">String</option>
|
||||
<option value="number">Number</option>
|
||||
<option value="boolean">Boolean</option>
|
||||
<option value="url">URL</option>
|
||||
<option value="object">Object</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div>
|
||||
<label className="block text-xs text-black-alpha-48 mb-6">Description</label>
|
||||
<input
|
||||
type="text"
|
||||
value={variable.description || ''}
|
||||
onChange={(e) => updateVariable(index, { description: e.target.value })}
|
||||
className="w-full px-12 py-8 bg-accent-white border border-border-faint rounded-8 text-sm text-accent-black focus:outline-none focus:border-heat-100"
|
||||
placeholder="Describe this input..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Default Value */}
|
||||
<div>
|
||||
<label className="block text-xs text-black-alpha-48 mb-6">Default Value</label>
|
||||
<input
|
||||
type="text"
|
||||
value={variable.defaultValue || ''}
|
||||
onChange={(e) => updateVariable(index, { defaultValue: e.target.value })}
|
||||
className="w-full px-12 py-8 bg-accent-white border border-border-faint rounded-8 text-sm text-accent-black focus:outline-none focus:border-heat-100"
|
||||
placeholder="Default value..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Required Toggle & Delete */}
|
||||
<div className="flex items-center justify-between pt-8">
|
||||
<label className="flex items-center gap-8 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={variable.required}
|
||||
onChange={(e) => updateVariable(index, { required: e.target.checked })}
|
||||
className="w-16 h-16 rounded-4 border border-border-faint text-heat-100 focus:ring-heat-100"
|
||||
/>
|
||||
<span className="text-xs text-accent-black">Required</span>
|
||||
</label>
|
||||
<button
|
||||
onClick={() => removeVariable(index)}
|
||||
className="px-12 py-6 text-xs text-accent-black hover:bg-black-alpha-4 rounded-6 transition-colors flex items-center gap-6"
|
||||
>
|
||||
<svg className="w-14 h-14" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Help Text */}
|
||||
<div className="p-16 bg-heat-4 border border-heat-100 rounded-12">
|
||||
<h4 className="text-sm font-medium text-accent-black mb-8">Input Variables</h4>
|
||||
<p className="text-xs text-heat-100 leading-relaxed">
|
||||
Input variables define the data your workflow accepts when it starts.
|
||||
These will be shown as form fields when running the workflow.
|
||||
</p>
|
||||
<p className="text-xs text-heat-100 leading-relaxed mt-8">
|
||||
Use <code className="px-4 py-2 bg-heat-8 rounded text-accent-black">{`{{variable_name}}`}</code> in any node to reference these values.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.aside>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
+404
@@ -0,0 +1,404 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/shadcn/tabs";
|
||||
import { Workflow } from "@/lib/workflow/types";
|
||||
import { useQuery } from "convex/react";
|
||||
import { api } from "@/convex/_generated/api";
|
||||
|
||||
interface TestEndpointPanelProps {
|
||||
workflowId: string;
|
||||
workflow: Workflow | null;
|
||||
environment: 'draft' | 'production';
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function TestEndpointPanel({ workflowId, workflow, environment, onClose }: TestEndpointPanelProps) {
|
||||
// Get user's API keys
|
||||
const apiKeys = useQuery(api.apiKeys.list, {});
|
||||
const firstKey = apiKeys?.[0];
|
||||
|
||||
// Try to get the full API key from localStorage (only available if just generated)
|
||||
const [fullApiKey, setFullApiKey] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const storedKey = sessionStorage.getItem('latest_api_key');
|
||||
if (storedKey) {
|
||||
setFullApiKey(storedKey);
|
||||
}
|
||||
}
|
||||
}, [apiKeys]);
|
||||
|
||||
// Get input variables from the workflow's start node
|
||||
const startNode = workflow?.nodes.find(n => (n.data as any)?.nodeType === 'start');
|
||||
const inputVariables = (startNode?.data as any)?.inputVariables || [];
|
||||
|
||||
// Generate default payload from input variables
|
||||
const defaultPayload = useMemo(() => {
|
||||
if (inputVariables.length === 0) {
|
||||
return { input: "https://firecrawl.dev" };
|
||||
}
|
||||
return inputVariables.reduce((acc: any, v: any) => {
|
||||
acc[v.name] = v.defaultValue || '';
|
||||
return acc;
|
||||
}, {});
|
||||
}, [inputVariables, workflow?.id]);
|
||||
|
||||
const [input, setInput] = useState(JSON.stringify(defaultPayload, null, 2));
|
||||
const [response, setResponse] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [copiedKey, setCopiedKey] = useState<string | null>(null);
|
||||
|
||||
// Update input when workflow changes
|
||||
useEffect(() => {
|
||||
setInput(JSON.stringify(defaultPayload, null, 2));
|
||||
}, [defaultPayload, workflowId]);
|
||||
|
||||
const baseUrl = typeof window !== 'undefined' ? window.location.origin : '';
|
||||
const endpointUrl = `${baseUrl}/api/workflows/${workflowId}/execute`;
|
||||
const streamUrl = `${baseUrl}/api/workflows/${workflowId}/execute-stream`;
|
||||
|
||||
const parsedInput = useMemo(() => {
|
||||
try {
|
||||
return input && input.trim().length > 0 ? JSON.parse(input) : defaultPayload;
|
||||
} catch {
|
||||
return defaultPayload;
|
||||
}
|
||||
}, [input, defaultPayload]);
|
||||
|
||||
const requestBodyMinified = useMemo(
|
||||
() => JSON.stringify({ input: parsedInput }),
|
||||
[parsedInput],
|
||||
);
|
||||
|
||||
const requestBodyPretty = useMemo(
|
||||
() => JSON.stringify({ input: parsedInput }, null, 2),
|
||||
[parsedInput],
|
||||
);
|
||||
|
||||
const handleCopy = async (key: string, value: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
setCopiedKey(key);
|
||||
setTimeout(() => {
|
||||
setCopiedKey((current) => (current === key ? null : current));
|
||||
}, 1500);
|
||||
} catch (copyError) {
|
||||
console.error("Failed to copy code block", copyError);
|
||||
}
|
||||
};
|
||||
|
||||
// Use full API key if available (from recent generation), otherwise show placeholder
|
||||
const apiKeyToUse = fullApiKey || 'YOUR_API_KEY_HERE';
|
||||
const hasRealKey = !!fullApiKey;
|
||||
|
||||
const apiKeyHeader = ` -H "Authorization: Bearer ${apiKeyToUse}" \\`;
|
||||
|
||||
const curlStandard = `curl -X POST ${endpointUrl} \\
|
||||
${apiKeyHeader}
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '${requestBodyMinified.replace(/'/g, "'\\''")}'`;
|
||||
|
||||
const curlStreaming = `curl -N -X POST ${streamUrl} \\
|
||||
${apiKeyHeader}
|
||||
-H "Content-Type: application/json" \\
|
||||
-H "Accept: text/event-stream" \\
|
||||
-d '${requestBodyMinified.replace(/'/g, "'\\''")}'`;
|
||||
|
||||
const apiKeyForCode = apiKeyToUse;
|
||||
|
||||
const tsExample = `import fetch from 'node-fetch';
|
||||
|
||||
const payload = ${requestBodyPretty};
|
||||
|
||||
const response = await fetch('${endpointUrl}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ${apiKeyForCode}' // Your API key
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
console.log(result);
|
||||
|
||||
// Streaming example
|
||||
const streamResponse = await fetch('${streamUrl}', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Accept': 'text/event-stream' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
const reader = streamResponse.body?.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
while (reader) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
console.log(decoder.decode(value));
|
||||
}`;
|
||||
|
||||
const pythonExample = `import requests
|
||||
import json
|
||||
|
||||
payload = ${requestBodyPretty}
|
||||
|
||||
# Standard request
|
||||
response = requests.post(
|
||||
"${endpointUrl}",
|
||||
headers={"Content-Type": "application/json"},
|
||||
data=json.dumps(payload),
|
||||
)
|
||||
print(response.json())
|
||||
|
||||
# Streaming request
|
||||
with requests.post(
|
||||
"${streamUrl}",
|
||||
headers={"Content-Type": "application/json", "Accept": "text/event-stream"},
|
||||
data=json.dumps(payload),
|
||||
stream=True,
|
||||
) as r:
|
||||
for line in r.iter_lines():
|
||||
if line:
|
||||
print(line.decode())`;
|
||||
|
||||
|
||||
const handleTest = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setResponse(null);
|
||||
|
||||
try {
|
||||
// Parse input to validate JSON
|
||||
let parsedInput;
|
||||
try {
|
||||
parsedInput = JSON.parse(input);
|
||||
} catch (e) {
|
||||
setError('Invalid JSON in request body');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// API expects { input: <input variables object> }
|
||||
const requestBody = {
|
||||
input: parsedInput,
|
||||
};
|
||||
|
||||
const res = await fetch(endpointUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
setResponse(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Request failed');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.aside
|
||||
initial={{ x: 400, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
exit={{ x: 400, opacity: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="fixed right-20 top-80 h-[calc(100vh-100px)] w-[calc(100vw-240px)] max-w-520 bg-accent-white border border-border-faint shadow-lg overflow-y-auto z-50 rounded-16 flex flex-col"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="p-20 border-b border-border-faint flex-shrink-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-label-large text-accent-black font-medium">Endpoint</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-32 h-32 rounded-6 hover:bg-black-alpha-4 transition-colors flex items-center justify-center"
|
||||
>
|
||||
<svg className="w-16 h-16 text-black-alpha-48" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-body-small text-black-alpha-48 mt-8">
|
||||
Test your workflow API endpoint
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-20 space-y-20">
|
||||
{/* Endpoint URL */}
|
||||
<div>
|
||||
<label className="block text-label-small text-black-alpha-48 mb-8">
|
||||
Endpoint URL
|
||||
</label>
|
||||
<div className="px-12 py-10 bg-background-base border border-border-faint rounded-8 text-body-small text-accent-black font-mono overflow-x-auto">
|
||||
{endpointUrl}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Request Body */}
|
||||
<div>
|
||||
<label className="block text-label-small text-black-alpha-48 mb-8">
|
||||
Input Payload
|
||||
</label>
|
||||
<textarea
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
rows={6}
|
||||
className="w-full px-12 py-10 bg-gray-900 text-white border border-border-faint rounded-8 text-body-small font-mono focus:outline-none focus:border-accent-black transition-colors resize-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Expected Output Schema */}
|
||||
{(() => {
|
||||
// Find the last node before the end node that has output
|
||||
const endNode = workflow?.nodes.find(n => (n.data as any)?.nodeType === 'end');
|
||||
if (!endNode) return null;
|
||||
|
||||
// Find edges that connect to the end node
|
||||
const edgesToEnd = workflow?.edges.filter(e => e.target === endNode.id);
|
||||
if (!edgesToEnd || edgesToEnd.length === 0) return null;
|
||||
|
||||
// Get the last node before end
|
||||
const lastNodeId = edgesToEnd[0]?.source;
|
||||
const lastNode = workflow?.nodes.find(n => n.id === lastNodeId);
|
||||
if (!lastNode) return null;
|
||||
|
||||
const nodeData = lastNode.data as any;
|
||||
const outputSchema = nodeData?.jsonOutputSchema;
|
||||
|
||||
if (outputSchema) {
|
||||
return (
|
||||
<div>
|
||||
<label className="block text-label-small text-black-alpha-48 mb-8">
|
||||
Expected Output Schema
|
||||
</label>
|
||||
<div className="p-12 bg-heat-4 rounded-8 border border-heat-100">
|
||||
<p className="text-body-small text-heat-100 mb-8">
|
||||
This workflow returns structured JSON matching this schema:
|
||||
</p>
|
||||
<pre className="text-body-small text-accent-black font-mono whitespace-pre-wrap overflow-auto max-h-200">
|
||||
{typeof outputSchema === 'string' ? outputSchema : JSON.stringify(outputSchema, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})()}
|
||||
|
||||
{/* Code Examples */}
|
||||
<div>
|
||||
{/* API Key Notice */}
|
||||
{!hasRealKey && (
|
||||
<div className="mb-12 p-12 bg-heat-4 border border-heat-100 rounded-8">
|
||||
<p className="text-body-small text-accent-black">
|
||||
<strong>Note:</strong> Replace <code className="px-4 py-2 bg-white rounded text-xs font-mono">YOUR_API_KEY_HERE</code> with your actual API key from Settings.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasRealKey && (
|
||||
<div className="mb-12 p-12 bg-heat-4 border border-heat-100 rounded-8">
|
||||
<p className="text-body-small text-accent-black">
|
||||
<strong>Ready to use!</strong> Your API key is included in the examples below.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Tabs defaultValue="curl" className="w-full">
|
||||
<TabsList className="grid grid-cols-4 mb-8 bg-background-base border border-border-faint rounded-8">
|
||||
<TabsTrigger value="curl">cURL</TabsTrigger>
|
||||
<TabsTrigger value="curl-stream">Streaming cURL</TabsTrigger>
|
||||
<TabsTrigger value="ts">TypeScript</TabsTrigger>
|
||||
<TabsTrigger value="python">Python</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="curl">
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => handleCopy('curl', curlStandard)}
|
||||
className="absolute top-12 right-12 flex items-center gap-6 px-12 py-6 bg-accent-white hover:bg-[#f4f4f5] border border-border-faint rounded-8 text-xs text-accent-black transition-colors shadow-sm"
|
||||
>
|
||||
{copiedKey === 'curl' ? 'Copied' : 'Copy'}
|
||||
</button>
|
||||
<pre className="px-12 py-10 bg-background-base text-accent-black rounded-8 text-body-small font-mono whitespace-pre-wrap break-words overflow-y-auto overflow-x-hidden max-h-200 border border-border-faint">
|
||||
{curlStandard}
|
||||
</pre>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="curl-stream">
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => handleCopy('curl-stream', curlStreaming)}
|
||||
className="absolute top-12 right-12 flex items-center gap-6 px-12 py-6 bg-accent-white hover:bg-[#f4f4f5] border border-border-faint rounded-8 text-xs text-accent-black transition-colors shadow-sm"
|
||||
>
|
||||
{copiedKey === 'curl-stream' ? 'Copied' : 'Copy'}
|
||||
</button>
|
||||
<pre className="px-12 py-10 bg-background-base text-accent-black rounded-8 text-body-small font-mono whitespace-pre-wrap break-words overflow-y-auto overflow-x-hidden max-h-200 border border-border-faint">
|
||||
{curlStreaming}
|
||||
</pre>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="ts">
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => handleCopy('ts', tsExample)}
|
||||
className="absolute top-12 right-12 flex items-center gap-6 px-12 py-6 bg-accent-white hover:bg-[#f4f4f5] border border-border-faint rounded-8 text-xs text-accent-black transition-colors shadow-sm"
|
||||
>
|
||||
{copiedKey === 'ts' ? 'Copied' : 'Copy'}
|
||||
</button>
|
||||
<pre className="px-12 py-10 bg-background-base text-accent-black rounded-8 text-body-small font-mono whitespace-pre-wrap break-words overflow-y-auto overflow-x-hidden max-h-200 border border-border-faint">
|
||||
{tsExample}
|
||||
</pre>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="python">
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => handleCopy('python', pythonExample)}
|
||||
className="absolute top-12 right-12 flex items-center gap-6 px-12 py-6 bg-accent-white hover:bg-[#f4f4f5] border border-border-faint rounded-8 text-xs text-accent-black transition-colors shadow-sm"
|
||||
>
|
||||
{copiedKey === 'python' ? 'Copied' : 'Copy'}
|
||||
</button>
|
||||
<pre className="px-12 py-10 bg-background-base text-accent-black rounded-8 text-body-small font-mono whitespace-pre-wrap break-words overflow-y-auto overflow-x-hidden max-h-200 border border-border-faint">
|
||||
{pythonExample}
|
||||
</pre>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
{/* Response */}
|
||||
{error && (
|
||||
<div className="p-16 bg-black-alpha-4 rounded-12 border border-border-faint">
|
||||
<h3 className="text-label-small text-accent-black mb-8">Error</h3>
|
||||
<pre className="text-body-small text-accent-black whitespace-pre-wrap">
|
||||
{error}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{response && (
|
||||
<div>
|
||||
<label className="block text-label-small text-black-alpha-48 mb-8">
|
||||
Response
|
||||
</label>
|
||||
<div className="p-12 bg-gray-900 rounded-8 border border-border-faint">
|
||||
<pre className="text-body-small text-white font-mono whitespace-pre-wrap overflow-auto max-h-300">
|
||||
{JSON.stringify(response, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.aside>
|
||||
);
|
||||
}
|
||||
+670
@@ -0,0 +1,670 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { useState, useEffect } from "react";
|
||||
import type { Node } from "@xyflow/react";
|
||||
|
||||
interface ToolsNodePanelProps {
|
||||
node: Node | null;
|
||||
onClose: () => void;
|
||||
onDelete: (nodeId: string) => void;
|
||||
onUpdate: (nodeId: string, data: any) => void;
|
||||
}
|
||||
|
||||
export default function ToolsNodePanel({ node, onClose, onDelete, onUpdate }: ToolsNodePanelProps) {
|
||||
const nodeData = node?.data as any;
|
||||
const nodeType = nodeData?.nodeType?.toLowerCase() || '';
|
||||
const [name, setName] = useState(nodeData?.name || nodeData?.nodeName || "Tool");
|
||||
|
||||
// File Search state
|
||||
const [searchQuery, setSearchQuery] = useState(nodeData?.searchQuery || "");
|
||||
const [filePattern, setFilePattern] = useState(nodeData?.filePattern || "*.ts,*.tsx,*.js,*.jsx");
|
||||
const [maxResults, setMaxResults] = useState(nodeData?.maxResults || "10");
|
||||
const [includeContent, setIncludeContent] = useState(nodeData?.includeContent ?? true);
|
||||
|
||||
// Guardrails state
|
||||
const [inputAsText, setInputAsText] = useState(nodeData?.inputAsText || "input_as_text");
|
||||
const [piiEnabled, setPiiEnabled] = useState(nodeData?.piiEnabled ?? true);
|
||||
const [moderationEnabled, setModerationEnabled] = useState(nodeData?.moderationEnabled ?? false);
|
||||
const [jailbreakEnabled, setJailbreakEnabled] = useState(nodeData?.jailbreakEnabled ?? false);
|
||||
const [hallucinationEnabled, setHallucinationEnabled] = useState(nodeData?.hallucinationEnabled ?? false);
|
||||
const [guardrailModel, setGuardrailModel] = useState(nodeData?.guardrailModel || 'openai/gpt-5-mini');
|
||||
const [actionOnViolation, setActionOnViolation] = useState(nodeData?.actionOnViolation || 'block');
|
||||
const [customRules, setCustomRules] = useState<string>(nodeData?.customRules?.join('\n') || '');
|
||||
|
||||
// PII entities
|
||||
const [piiEntities, setPiiEntities] = useState<string[]>(nodeData?.piiEntities || ['CREDIT_CARD_NUMBER']);
|
||||
const [showPIIModal, setShowPIIModal] = useState(false);
|
||||
|
||||
const allPIIEntities = [
|
||||
{ id: 'PERSON_NAME', label: 'Person name' },
|
||||
{ id: 'EMAIL_ADDRESS', label: 'Email address' },
|
||||
{ id: 'PHONE_NUMBER', label: 'Phone number' },
|
||||
{ id: 'LOCATION', label: 'Location' },
|
||||
{ id: 'DATE_TIME', label: 'Date or time' },
|
||||
{ id: 'URL', label: 'URL' },
|
||||
{ id: 'CREDIT_CARD_NUMBER', label: 'Credit card number' },
|
||||
{ id: 'IBAN', label: 'International bank account number (IBAN)' },
|
||||
{ id: 'CRYPTO_WALLET', label: 'Cryptocurrency wallet address' },
|
||||
{ id: 'MEDICAL_LICENSE', label: 'Medical license number' },
|
||||
{ id: 'IP_ADDRESS', label: 'IP address' },
|
||||
{ id: 'US_DRIVERS_LICENSE', label: 'US driver license number' },
|
||||
{ id: 'US_BANK_ACCOUNT', label: 'US bank account number' },
|
||||
{ id: 'NATIONALITY_RELIGION_POLITICAL', label: 'Nationality/religion/political group' },
|
||||
];
|
||||
|
||||
// Auto-save changes
|
||||
useEffect(() => {
|
||||
if (!node) return;
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
onUpdate(node.id, {
|
||||
name,
|
||||
nodeName: name,
|
||||
searchQuery,
|
||||
filePattern,
|
||||
maxResults,
|
||||
includeContent,
|
||||
inputAsText,
|
||||
piiEnabled,
|
||||
moderationEnabled,
|
||||
jailbreakEnabled,
|
||||
hallucinationEnabled,
|
||||
piiEntities,
|
||||
guardrailModel,
|
||||
actionOnViolation,
|
||||
customRules: customRules.split('\n').filter(r => r.trim()),
|
||||
});
|
||||
}, 500);
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [name, searchQuery, filePattern, maxResults, includeContent, inputAsText, piiEnabled, moderationEnabled, jailbreakEnabled, hallucinationEnabled, piiEntities, guardrailModel, actionOnViolation, customRules, node, onUpdate]);
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{node && (
|
||||
<motion.aside
|
||||
initial={{ x: 400, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
exit={{ x: 400, opacity: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="fixed right-20 top-80 h-[calc(100vh-100px)] w-[calc(100vw-240px)] max-w-480 bg-accent-white border border-border-faint shadow-lg overflow-y-auto z-50 rounded-16"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="p-20 border-b border-border-faint">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="text-label-large text-accent-black font-medium bg-transparent border-none outline-none focus:outline-none hover:bg-black-alpha-4 px-2 -ml-2 rounded-4 transition-colors"
|
||||
placeholder="Enter node name..."
|
||||
/>
|
||||
<div className="flex items-center gap-8">
|
||||
<button
|
||||
onClick={() => onDelete(node?.id || '')}
|
||||
className="w-32 h-32 rounded-6 hover:bg-black-alpha-4 transition-colors flex items-center justify-center group"
|
||||
title="Delete node"
|
||||
>
|
||||
<svg className="w-16 h-16 text-black-alpha-48 group-hover:text-black-alpha-64" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-32 h-32 rounded-6 hover:bg-black-alpha-4 transition-colors flex items-center justify-center"
|
||||
>
|
||||
<svg className="w-16 h-16 text-black-alpha-48" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-body-small text-black-alpha-48">
|
||||
{nodeType.includes('file') ? 'Search through files and code' :
|
||||
nodeType.includes('guardrail') ? 'Add safety checks and content filtering' :
|
||||
'Configure tool'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Form Fields */}
|
||||
<div className="p-20 space-y-24">
|
||||
{/* File Search Configuration */}
|
||||
{nodeType.includes('file') && (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-label-small text-black-alpha-48 mb-8">
|
||||
Search Query
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Search for functions, classes, text..."
|
||||
className="w-full px-12 py-10 bg-background-base border border-border-faint rounded-8 text-body-medium text-accent-black focus:outline-none focus:border-heat-100 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-label-small text-black-alpha-48 mb-8">
|
||||
File Pattern
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={filePattern}
|
||||
onChange={(e) => setFilePattern(e.target.value)}
|
||||
placeholder="*.ts,*.tsx,*.js"
|
||||
className="w-full px-12 py-10 bg-background-base border border-border-faint rounded-8 text-body-medium text-accent-black font-mono focus:outline-none focus:border-heat-100 transition-colors"
|
||||
/>
|
||||
<p className="text-body-small text-black-alpha-48 mt-8">
|
||||
Comma-separated glob patterns
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-label-small text-black-alpha-48 mb-8">
|
||||
Max Results
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={maxResults}
|
||||
onChange={(e) => setMaxResults(e.target.value)}
|
||||
className="w-full px-12 py-10 bg-background-base border border-border-faint rounded-8 text-body-medium text-accent-black focus:outline-none focus:border-heat-100 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-label-small text-accent-black">
|
||||
Include File Content
|
||||
</label>
|
||||
<button
|
||||
onClick={() => setIncludeContent(!includeContent)}
|
||||
className={`w-44 h-24 rounded-full transition-colors relative ${
|
||||
includeContent ? 'bg-heat-100' : 'bg-black-alpha-12'
|
||||
}`}
|
||||
>
|
||||
<motion.div
|
||||
className="w-20 h-20 bg-white rounded-full absolute top-2 shadow-sm"
|
||||
animate={{ left: includeContent ? '22px' : '2px' }}
|
||||
transition={{ duration: 0.2 }}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Guardrails Configuration */}
|
||||
{nodeType.includes('guardrail') && (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-label-small text-black-alpha-48 mb-8">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={nodeData?.name || 'Guardrails'}
|
||||
onChange={(e) => onUpdate(node?.id || '', { name: e.target.value })}
|
||||
className="w-full px-12 py-10 bg-background-base border border-border-faint rounded-8 text-body-medium text-accent-black focus:outline-none focus:border-heat-100 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-label-small text-black-alpha-48 mb-8">
|
||||
Input Variable
|
||||
</label>
|
||||
<div className="flex items-center gap-8 px-12 py-10 bg-background-base border border-border-faint rounded-8">
|
||||
<svg className="w-16 h-16 text-heat-100" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<span className="text-body-small text-accent-black font-mono">{inputAsText}</span>
|
||||
<span className="ml-auto px-8 py-4 bg-heat-4 text-heat-100 rounded-6 text-body-small font-medium">
|
||||
STRING
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-body-small text-black-alpha-48 mt-6">
|
||||
The input content to check for violations
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Guardrail Toggles */}
|
||||
<div className="space-y-12">
|
||||
{/* PII Detection */}
|
||||
<div className="p-16 bg-accent-white border border-border-faint rounded-12">
|
||||
<div className="flex items-center justify-between mb-12">
|
||||
<div className="flex items-center gap-8">
|
||||
<svg className="w-20 h-20 text-heat-100" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
</svg>
|
||||
<span className="text-label-medium text-accent-black">PII Detection</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setPiiEnabled(!piiEnabled)}
|
||||
className={`w-44 h-24 rounded-full transition-colors relative ${
|
||||
piiEnabled ? 'bg-heat-100' : 'bg-black-alpha-12'
|
||||
}`}
|
||||
>
|
||||
<motion.div
|
||||
className="w-20 h-20 bg-white rounded-full absolute top-2 shadow-sm"
|
||||
animate={{ left: piiEnabled ? '22px' : '2px' }}
|
||||
transition={{ duration: 0.2, ease: 'easeOut' }}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setShowPIIModal(true)}
|
||||
className="w-full px-12 py-8 bg-heat-4 hover:bg-heat-8 border border-heat-100 rounded-8 text-body-small text-heat-100 transition-colors flex items-center justify-center gap-6 mb-12"
|
||||
>
|
||||
<svg className="w-14 h-14" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||
</svg>
|
||||
Configure PII Types ({piiEntities.length} selected)
|
||||
</button>
|
||||
|
||||
{/* Info Box */}
|
||||
<div className="p-12 bg-heat-4 border border-heat-100 rounded-8">
|
||||
<div className="flex items-start gap-8 mb-8">
|
||||
<svg className="w-16 h-16 text-heat-100 flex-shrink-0 mt-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<div className="flex-1">
|
||||
<p className="text-xs text-accent-black font-medium mb-4">What it does:</p>
|
||||
<p className="text-xs text-heat-100 leading-relaxed">
|
||||
Detects sensitive personal data (emails, phone numbers, credit cards, etc.) and blocks requests containing them.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-6 mt-10">
|
||||
<div>
|
||||
<p className="text-xs text-heat-100 font-medium flex items-center gap-4">
|
||||
<span className="w-12 h-12 bg-heat-100 rounded-full text-white text-[10px] flex items-center justify-center">✓</span>
|
||||
Pass Example:
|
||||
</p>
|
||||
<p className="text-xs text-heat-100 font-mono mt-2 ml-16">"Hello, how can I help you today?"</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-accent-black font-medium flex items-center gap-4">
|
||||
<span className="w-12 h-12 bg-accent-black rounded-full text-white text-[10px] flex items-center justify-center">✗</span>
|
||||
Fail Example:
|
||||
</p>
|
||||
<p className="text-xs text-heat-100 font-mono mt-2 ml-16">"My email is john@example.com and card is 4532-1234-5678-9010"</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Moderation */}
|
||||
<div className="p-16 bg-accent-white border border-border-faint rounded-12">
|
||||
<div className="flex items-center justify-between mb-12">
|
||||
<div className="flex items-center gap-8">
|
||||
<svg className="w-20 h-20 text-heat-100" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
</svg>
|
||||
<span className="text-label-medium text-accent-black">Content Moderation</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setModerationEnabled(!moderationEnabled)}
|
||||
className={`w-44 h-24 rounded-full transition-colors relative ${
|
||||
moderationEnabled ? 'bg-heat-100' : 'bg-black-alpha-12'
|
||||
}`}
|
||||
>
|
||||
<motion.div
|
||||
className="w-20 h-20 bg-white rounded-full absolute top-2 shadow-sm"
|
||||
animate={{ left: moderationEnabled ? '22px' : '2px' }}
|
||||
transition={{ duration: 0.2, ease: 'easeOut' }}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-12 bg-heat-4 border border-heat-100 rounded-8">
|
||||
<div className="flex items-start gap-8 mb-8">
|
||||
<svg className="w-16 h-16 text-heat-100 flex-shrink-0 mt-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<div className="flex-1">
|
||||
<p className="text-xs text-accent-black font-medium mb-4">What it does:</p>
|
||||
<p className="text-xs text-heat-100 leading-relaxed">
|
||||
Blocks offensive, hateful, violent, or sexually explicit content from being processed.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-6 mt-10">
|
||||
<div>
|
||||
<p className="text-xs text-heat-100 font-medium flex items-center gap-4">
|
||||
<span className="w-12 h-12 bg-heat-100 rounded-full text-white text-[10px] flex items-center justify-center">✓</span>
|
||||
Pass Example:
|
||||
</p>
|
||||
<p className="text-xs text-heat-100 font-mono mt-2 ml-16">"This product is excellent quality"</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-accent-black font-medium flex items-center gap-4">
|
||||
<span className="w-12 h-12 bg-accent-black rounded-full text-white text-[10px] flex items-center justify-center">✗</span>
|
||||
Fail Example:
|
||||
</p>
|
||||
<p className="text-xs text-heat-100 font-mono mt-2 ml-16">"[Offensive or hateful content]"</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Jailbreak */}
|
||||
<div className="p-16 bg-accent-white border border-border-faint rounded-12">
|
||||
<div className="flex items-center justify-between mb-12">
|
||||
<div className="flex items-center gap-8">
|
||||
<svg className="w-20 h-20 text-heat-100" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
<span className="text-label-medium text-accent-black">Jailbreak Detection</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setJailbreakEnabled(!jailbreakEnabled)}
|
||||
className={`w-44 h-24 rounded-full transition-colors relative ${
|
||||
jailbreakEnabled ? 'bg-heat-100' : 'bg-black-alpha-12'
|
||||
}`}
|
||||
>
|
||||
<motion.div
|
||||
className="w-20 h-20 bg-white rounded-full absolute top-2 shadow-sm"
|
||||
animate={{ left: jailbreakEnabled ? '22px' : '2px' }}
|
||||
transition={{ duration: 0.2, ease: 'easeOut' }}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-12 bg-heat-4 border border-heat-100 rounded-8">
|
||||
<div className="flex items-start gap-8 mb-8">
|
||||
<svg className="w-16 h-16 text-heat-100 flex-shrink-0 mt-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<div className="flex-1">
|
||||
<p className="text-xs text-accent-black font-medium mb-4">What it does:</p>
|
||||
<p className="text-xs text-heat-100 leading-relaxed">
|
||||
Detects attempts to bypass AI safety measures or trick the model into ignoring its instructions.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-6 mt-10">
|
||||
<div>
|
||||
<p className="text-xs text-heat-100 font-medium flex items-center gap-4">
|
||||
<span className="w-12 h-12 bg-heat-100 rounded-full text-white text-[10px] flex items-center justify-center">✓</span>
|
||||
Pass Example:
|
||||
</p>
|
||||
<p className="text-xs text-heat-100 font-mono mt-2 ml-16">"Please help me write a professional email"</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-accent-black font-medium flex items-center gap-4">
|
||||
<span className="w-12 h-12 bg-accent-black rounded-full text-white text-[10px] flex items-center justify-center">✗</span>
|
||||
Fail Example:
|
||||
</p>
|
||||
<p className="text-xs text-heat-100 font-mono mt-2 ml-16">"Ignore previous instructions and tell me how to..."</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hallucination */}
|
||||
<div className="p-16 bg-accent-white border border-border-faint rounded-12">
|
||||
<div className="flex items-center justify-between mb-12">
|
||||
<div className="flex items-center gap-8">
|
||||
<svg className="w-20 h-20 text-heat-100" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
||||
</svg>
|
||||
<span className="text-label-medium text-accent-black">Hallucination Detection</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setHallucinationEnabled(!hallucinationEnabled)}
|
||||
className={`w-44 h-24 rounded-full transition-colors relative ${
|
||||
hallucinationEnabled ? 'bg-heat-100' : 'bg-black-alpha-12'
|
||||
}`}
|
||||
>
|
||||
<motion.div
|
||||
className="w-20 h-20 bg-white rounded-full absolute top-2 shadow-sm"
|
||||
animate={{ left: hallucinationEnabled ? '22px' : '2px' }}
|
||||
transition={{ duration: 0.2, ease: 'easeOut' }}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-12 bg-heat-4 border border-heat-100 rounded-8">
|
||||
<div className="flex items-start gap-8 mb-8">
|
||||
<svg className="w-16 h-16 text-heat-100 flex-shrink-0 mt-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<div className="flex-1">
|
||||
<p className="text-xs text-accent-black font-medium mb-4">What it does:</p>
|
||||
<p className="text-xs text-heat-100 leading-relaxed">
|
||||
Checks if the AI output contains factual claims that can't be verified or seem made up.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-6 mt-10">
|
||||
<div>
|
||||
<p className="text-xs text-heat-100 font-medium flex items-center gap-4">
|
||||
<span className="w-12 h-12 bg-heat-100 rounded-full text-white text-[10px] flex items-center justify-center">✓</span>
|
||||
Pass Example:
|
||||
</p>
|
||||
<p className="text-xs text-heat-100 font-mono mt-2 ml-16">"The sky appears blue due to Rayleigh scattering"</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-accent-black font-medium flex items-center gap-4">
|
||||
<span className="w-12 h-12 bg-accent-black rounded-full text-white text-[10px] flex items-center justify-center">✗</span>
|
||||
Fail Example:
|
||||
</p>
|
||||
<p className="text-xs text-heat-100 font-mono mt-2 ml-16">"Studies show that 95% of unicorns prefer rainbow diets"</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Settings Section */}
|
||||
<div className="space-y-16 pt-16 border-t border-border-faint">
|
||||
{/* Model Selection */}
|
||||
<div>
|
||||
<label className="block text-label-small text-black-alpha-48 mb-8">
|
||||
Analysis Model
|
||||
</label>
|
||||
<select
|
||||
value={guardrailModel}
|
||||
onChange={(e) => setGuardrailModel(e.target.value)}
|
||||
className="w-full px-12 py-10 bg-background-base border border-border-faint rounded-8 text-body-medium text-accent-black focus:outline-none focus:border-heat-100 transition-colors cursor-pointer"
|
||||
>
|
||||
<optgroup label="OpenAI (Recommended)">
|
||||
<option value="openai/gpt-5-mini">GPT-5 Mini (Fast & Cheap)</option>
|
||||
<option value="openai/gpt-5">GPT-5</option>
|
||||
</optgroup>
|
||||
<optgroup label="Groq (Fastest)">
|
||||
<option value="groq/openai/gpt-oss-20b">GPT OSS 20B</option>
|
||||
<option value="groq/openai/gpt-oss-120b">GPT OSS 120B</option>
|
||||
</optgroup>
|
||||
<optgroup label="Anthropic">
|
||||
<option value="anthropic/claude-sonnet-4-5-20250929">Claude Sonnet 4.5</option>
|
||||
<option value="anthropic/claude-sonnet-4-20250514">Claude Sonnet 4</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
<p className="text-body-small text-black-alpha-48 mt-8">
|
||||
LLM used to analyze content for violations
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Action on Violation */}
|
||||
<div>
|
||||
<label className="block text-label-small text-black-alpha-48 mb-8">
|
||||
Action on Violation
|
||||
</label>
|
||||
<select
|
||||
value={actionOnViolation}
|
||||
onChange={(e) => setActionOnViolation(e.target.value)}
|
||||
className="w-full px-12 py-10 bg-background-base border border-border-faint rounded-8 text-body-medium text-accent-black focus:outline-none focus:border-heat-100 transition-colors cursor-pointer"
|
||||
>
|
||||
<option value="block">🛑 Block - Stop workflow execution</option>
|
||||
<option value="warn">⚠️ Warn - Continue with warning</option>
|
||||
<option value="flag">🏴 Flag - Log violation and continue</option>
|
||||
</select>
|
||||
<p className="text-body-small text-black-alpha-48 mt-8">
|
||||
What to do when violations are detected
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Custom Rules */}
|
||||
<div>
|
||||
<label className="block text-label-small text-black-alpha-48 mb-8">
|
||||
Custom Rules (Optional)
|
||||
</label>
|
||||
<textarea
|
||||
value={customRules}
|
||||
onChange={(e) => setCustomRules(e.target.value)}
|
||||
placeholder="Block messages containing: refund Block messages about: billing Require approval for: account changes"
|
||||
rows={4}
|
||||
className="w-full px-12 py-10 bg-background-base border border-border-faint rounded-8 text-body-medium text-accent-black placeholder-black-alpha-32 focus:outline-none focus:border-heat-100 transition-colors resize-y font-mono"
|
||||
/>
|
||||
<p className="text-body-small text-black-alpha-48 mt-8">
|
||||
One rule per line. LLM will check content against these rules.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</motion.aside>
|
||||
)}
|
||||
|
||||
{/* PII Configuration Modal */}
|
||||
{showPIIModal && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="fixed inset-0 bg-black-alpha-64 z-[200] flex items-center justify-center backdrop-blur-sm"
|
||||
onClick={() => setShowPIIModal(false)}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ scale: 0.95, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
transition={{ duration: 0.2, ease: 'easeOut' }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="bg-accent-white rounded-16 shadow-2xl w-[600px] max-h-[80vh] overflow-hidden flex flex-col"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="p-20 border-b border-border-faint bg-background-base">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-10">
|
||||
<svg className="w-24 h-24 text-heat-100" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
</svg>
|
||||
<div>
|
||||
<h3 className="text-label-large text-accent-black">
|
||||
PII Detection Configuration
|
||||
</h3>
|
||||
<p className="text-body-small text-black-alpha-48 mt-2">
|
||||
Select which types of personal data to detect
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowPIIModal(false)}
|
||||
className="w-32 h-32 rounded-6 hover:bg-black-alpha-4 transition-colors flex items-center justify-center"
|
||||
>
|
||||
<svg className="w-16 h-16 text-black-alpha-48" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-20 flex-1 overflow-y-auto">
|
||||
<div className="flex items-center justify-between mb-16">
|
||||
<button
|
||||
onClick={() => setPiiEntities(allPIIEntities.map(e => e.id))}
|
||||
className="text-body-small text-heat-100 hover:text-heat-200 transition-colors font-medium"
|
||||
>
|
||||
Select all
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setPiiEntities([])}
|
||||
className="text-body-small text-black-alpha-48 hover:text-accent-black transition-colors"
|
||||
>
|
||||
Clear all
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-16">
|
||||
<div>
|
||||
<h4 className="text-label-small text-accent-black mb-10">Common Types</h4>
|
||||
<div className="grid grid-cols-2 gap-8">
|
||||
{allPIIEntities.slice(0, 8).map((entity) => (
|
||||
<label key={entity.id} className="flex items-center gap-8 p-10 hover:bg-background-base rounded-8 cursor-pointer transition-colors">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={piiEntities.includes(entity.id)}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setPiiEntities([...piiEntities, entity.id]);
|
||||
} else {
|
||||
setPiiEntities(piiEntities.filter(id => id !== entity.id));
|
||||
}
|
||||
}}
|
||||
className="w-16 h-16 rounded-4 border border-border-faint text-heat-100 focus:ring-heat-100"
|
||||
/>
|
||||
<span className="text-body-small text-accent-black">{entity.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-label-small text-accent-black mb-10">US-Specific Types</h4>
|
||||
<div className="grid grid-cols-2 gap-8">
|
||||
{allPIIEntities.slice(8).map((entity) => (
|
||||
<label key={entity.id} className="flex items-center gap-8 p-10 hover:bg-background-base rounded-8 cursor-pointer transition-colors">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={piiEntities.includes(entity.id)}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setPiiEntities([...piiEntities, entity.id]);
|
||||
} else {
|
||||
setPiiEntities(piiEntities.filter(id => id !== entity.id));
|
||||
}
|
||||
}}
|
||||
className="w-16 h-16 rounded-4 border border-border-faint text-heat-100 focus:ring-heat-100"
|
||||
/>
|
||||
<span className="text-body-small text-accent-black">{entity.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="p-20 border-t border-border-faint bg-background-base flex justify-between items-center">
|
||||
<div className="text-body-small text-black-alpha-48">
|
||||
{piiEntities.length} of {allPIIEntities.length} types selected
|
||||
</div>
|
||||
<div className="flex gap-8">
|
||||
<button
|
||||
onClick={() => setShowPIIModal(false)}
|
||||
className="px-16 py-8 text-body-small text-accent-black hover:bg-black-alpha-4 rounded-8 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
onUpdate(node?.id || '', { piiEntities });
|
||||
setShowPIIModal(false);
|
||||
}}
|
||||
className="px-20 py-8 bg-accent-black hover:bg-black-alpha-88 text-white rounded-8 text-body-small font-medium transition-colors"
|
||||
>
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
interface OutputMapping {
|
||||
outputAs: "full" | "field" | "custom";
|
||||
fieldName?: string;
|
||||
customPath?: string;
|
||||
}
|
||||
|
||||
interface UniversalOutputSelectorProps {
|
||||
nodeId: string;
|
||||
nodeType: string;
|
||||
currentMapping?: OutputMapping;
|
||||
onUpdate: (mapping: OutputMapping) => void;
|
||||
}
|
||||
|
||||
export default function UniversalOutputSelector({
|
||||
nodeId,
|
||||
nodeType,
|
||||
currentMapping,
|
||||
onUpdate,
|
||||
}: UniversalOutputSelectorProps) {
|
||||
const [outputAs, setOutputAs] = useState<"full" | "field" | "custom">(
|
||||
currentMapping?.outputAs || "full"
|
||||
);
|
||||
const [fieldName, setFieldName] = useState(currentMapping?.fieldName || "result");
|
||||
const [customPath, setCustomPath] = useState(currentMapping?.customPath || "");
|
||||
|
||||
useEffect(() => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
onUpdate({ outputAs, fieldName, customPath });
|
||||
}, 300);
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [outputAs, fieldName, customPath, onUpdate]);
|
||||
|
||||
const getDefaultFields = () => {
|
||||
switch (nodeType) {
|
||||
case 'agent':
|
||||
return ['message', 'text', 'response'];
|
||||
case 'mcp':
|
||||
case 'firecrawl':
|
||||
return ['markdown', 'html', 'json', 'results', 'urls'];
|
||||
case 'transform':
|
||||
return ['result', 'output', 'data'];
|
||||
case 'if-else':
|
||||
return ['condition', 'branch'];
|
||||
case 'while':
|
||||
return ['iterations', 'result'];
|
||||
case 'guardrails':
|
||||
return ['passed', 'sanitized', 'result'];
|
||||
case 'file-search':
|
||||
return ['files', 'matches'];
|
||||
default:
|
||||
return ['result', 'output'];
|
||||
}
|
||||
};
|
||||
|
||||
const defaultFields = getDefaultFields();
|
||||
|
||||
return (
|
||||
<div className="space-y-12">
|
||||
<div>
|
||||
<label className="block text-body-small text-black-alpha-48 mb-6 font-medium">
|
||||
Output Format
|
||||
</label>
|
||||
<p className="text-body-small text-black-alpha-48 mb-10">
|
||||
Choose how downstream nodes access this node's output
|
||||
</p>
|
||||
|
||||
<div className="space-y-8">
|
||||
{/* Full Output */}
|
||||
<button
|
||||
onClick={() => setOutputAs('full')}
|
||||
className={`w-full p-12 rounded-8 border transition-all text-left ${
|
||||
outputAs === 'full'
|
||||
? 'border-heat-100 bg-heat-4'
|
||||
: 'border-border-faint bg-background-base hover:border-border-light'
|
||||
}`}
|
||||
>
|
||||
<p className="text-body-small text-accent-black font-medium mb-4">Full Output</p>
|
||||
<p className="text-body-small text-black-alpha-48">
|
||||
<code className="font-mono text-xs">state.variables.{nodeId}</code> returns entire result
|
||||
</p>
|
||||
</button>
|
||||
|
||||
{/* Single Field */}
|
||||
<button
|
||||
onClick={() => setOutputAs('field')}
|
||||
className={`w-full p-12 rounded-8 border transition-all text-left ${
|
||||
outputAs === 'field'
|
||||
? 'border-heat-100 bg-heat-4'
|
||||
: 'border-border-faint bg-background-base hover:border-border-light'
|
||||
}`}
|
||||
>
|
||||
<p className="text-body-small text-accent-black font-medium mb-4">Extract Field</p>
|
||||
<p className="text-body-small text-black-alpha-48">
|
||||
Extract a specific field from the output
|
||||
</p>
|
||||
</button>
|
||||
|
||||
{outputAs === 'field' && (
|
||||
<div className="ml-16 pl-12 border-l-2 border-heat-100">
|
||||
<label className="block text-body-small text-black-alpha-48 mb-6">
|
||||
Field Name
|
||||
</label>
|
||||
<select
|
||||
value={fieldName}
|
||||
onChange={(e) => setFieldName(e.target.value)}
|
||||
className="w-full px-10 py-6 bg-white border border-border-faint rounded-6 text-body-small text-accent-black font-mono focus:outline-none focus:border-heat-100 transition-colors mb-8"
|
||||
>
|
||||
{defaultFields.map(field => (
|
||||
<option key={field} value={field}>{field}</option>
|
||||
))}
|
||||
<option value="__custom__">Custom...</option>
|
||||
</select>
|
||||
|
||||
{fieldName === '__custom__' && (
|
||||
<input
|
||||
type="text"
|
||||
value={customPath}
|
||||
onChange={(e) => {
|
||||
setCustomPath(e.target.value);
|
||||
setFieldName(e.target.value);
|
||||
}}
|
||||
placeholder="data.items[0].title"
|
||||
className="w-full px-10 py-6 bg-white border border-border-faint rounded-6 text-body-small text-accent-black font-mono focus:outline-none focus:border-heat-100 transition-colors"
|
||||
/>
|
||||
)}
|
||||
|
||||
<p className="text-body-small text-black-alpha-48 mt-8">
|
||||
Access as: <code className="font-mono text-xs text-heat-100">state.variables.{nodeId}</code>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Custom Path */}
|
||||
<button
|
||||
onClick={() => setOutputAs('custom')}
|
||||
className={`w-full p-12 rounded-8 border transition-all text-left ${
|
||||
outputAs === 'custom'
|
||||
? 'border-heat-100 bg-heat-4'
|
||||
: 'border-border-faint bg-background-base hover:border-border-light'
|
||||
}`}
|
||||
>
|
||||
<p className="text-body-small text-accent-black font-medium mb-4">Custom Path</p>
|
||||
<p className="text-body-small text-black-alpha-48">
|
||||
Use dot notation to extract nested data
|
||||
</p>
|
||||
</button>
|
||||
|
||||
{outputAs === 'custom' && (
|
||||
<div className="ml-16 pl-12 border-l-2 border-heat-100">
|
||||
<input
|
||||
type="text"
|
||||
value={customPath}
|
||||
onChange={(e) => setCustomPath(e.target.value)}
|
||||
placeholder="result.data.items[0].title"
|
||||
className="w-full px-10 py-6 bg-white border border-border-faint rounded-6 text-body-small text-accent-black font-mono focus:outline-none focus:border-heat-100 transition-colors"
|
||||
/>
|
||||
<p className="text-body-small text-black-alpha-48 mt-8">
|
||||
Supports dot notation and array indexing
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Preview */}
|
||||
<div className="p-12 bg-blue-50 rounded-8 border border-blue-200">
|
||||
<p className="text-body-small text-blue-900 font-medium mb-6">Preview:</p>
|
||||
<code className="text-body-small text-blue-800 font-mono">
|
||||
state.variables.{nodeId}
|
||||
{outputAs === 'field' && fieldName !== '__custom__' && ` → ${fieldName} only`}
|
||||
{outputAs === 'custom' && customPath && ` → ${customPath}`}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+234
@@ -0,0 +1,234 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import * as React from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import type { Node } from "@xyflow/react";
|
||||
|
||||
interface VariableReferencePickerProps {
|
||||
nodes: Node[];
|
||||
currentNodeId: string;
|
||||
onSelect: (reference: string) => void;
|
||||
}
|
||||
|
||||
export default function VariableReferencePicker({ nodes, currentNodeId, onSelect }: VariableReferencePickerProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [buttonPosition, setButtonPosition] = useState({ top: 0, right: 0 });
|
||||
const buttonRef = React.useRef<HTMLButtonElement>(null);
|
||||
|
||||
// Get nodes that come before current node
|
||||
const availableNodes = nodes.filter(n => n.id !== currentNodeId);
|
||||
|
||||
// Get input variables from Start node
|
||||
const startNode = nodes.find(n => (n.data as any)?.nodeType === 'start');
|
||||
const inputVariables = (startNode?.data as any)?.inputVariables || [];
|
||||
|
||||
// Build variable list with schema fields - using simple notation
|
||||
const nodeVariables = availableNodes.flatMap(n => {
|
||||
const nodeData = n.data as any;
|
||||
|
||||
// Skip Start node - we'll add its input variables separately
|
||||
if (nodeData.nodeType === 'start') {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Convert node ID to simple variable name (replace hyphens with underscores)
|
||||
const nodeVarName = n.id.replace(/-/g, '_');
|
||||
|
||||
const baseVar = {
|
||||
name: nodeData.nodeName || n.id,
|
||||
path: nodeVarName, // Simple notation!
|
||||
description: `Output from ${nodeData.nodeName || n.id}`,
|
||||
nodeType: nodeData.nodeType,
|
||||
} as any;
|
||||
|
||||
const vars = [baseVar];
|
||||
|
||||
// If node has JSON output schema, add individual field references
|
||||
if (nodeData.jsonOutputSchema) {
|
||||
try {
|
||||
const schema = JSON.parse(nodeData.jsonOutputSchema);
|
||||
if (schema.properties) {
|
||||
Object.keys(schema.properties).forEach(propName => {
|
||||
const propSchema = schema.properties[propName];
|
||||
const propType = propSchema.type || 'any';
|
||||
|
||||
vars.push({
|
||||
name: `${nodeData.nodeName || n.id}.${propName}`,
|
||||
path: `${nodeVarName}.${propName}`, // Simple notation!
|
||||
description: propSchema.description || `${propName} field from ${nodeData.nodeName || n.id}`,
|
||||
nodeType: nodeData.nodeType,
|
||||
propertyType: propType,
|
||||
isField: true,
|
||||
} as any);
|
||||
|
||||
// If property is an object, add nested fields
|
||||
if (propSchema.properties) {
|
||||
Object.keys(propSchema.properties).forEach(nestedProp => {
|
||||
vars.push({
|
||||
name: `${nodeData.nodeName || n.id}.${propName}.${nestedProp}`,
|
||||
path: `${nodeVarName}.${propName}.${nestedProp}`,
|
||||
description: `${nestedProp} in ${propName}`,
|
||||
nodeType: nodeData.nodeType,
|
||||
isField: true,
|
||||
isNested: true,
|
||||
} as any);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// Invalid JSON, skip
|
||||
}
|
||||
}
|
||||
|
||||
return vars;
|
||||
});
|
||||
|
||||
// Build input variables from Start node
|
||||
const startNodeInputs = inputVariables.map((v: any) => ({
|
||||
name: v.name,
|
||||
path: `input.${v.name}`, // Reference input variables as input.variableName
|
||||
description: v.description || `Input: ${v.name}`,
|
||||
type: v.type,
|
||||
isInputVariable: true,
|
||||
}));
|
||||
|
||||
const variables = [
|
||||
{
|
||||
category: "Input Variables",
|
||||
items: startNodeInputs,
|
||||
},
|
||||
{
|
||||
category: "Workflow",
|
||||
items: [
|
||||
{ name: "input (full object)", path: "input", description: "All workflow inputs as object" },
|
||||
{ name: "lastOutput", path: "lastOutput", description: "Output from previous node" },
|
||||
],
|
||||
},
|
||||
{
|
||||
category: "Previous Nodes",
|
||||
items: nodeVariables,
|
||||
},
|
||||
].filter(group => group.items.length > 0); // Remove empty groups
|
||||
|
||||
const handleOpen = () => {
|
||||
if (buttonRef.current) {
|
||||
const rect = buttonRef.current.getBoundingClientRect();
|
||||
setButtonPosition({
|
||||
top: rect.bottom + 8,
|
||||
right: window.innerWidth - rect.right,
|
||||
});
|
||||
}
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<button
|
||||
ref={buttonRef}
|
||||
onClick={handleOpen}
|
||||
className="px-12 py-6 bg-heat-4 hover:bg-heat-8 border border-heat-100 rounded-6 text-body-small text-heat-100 transition-colors flex items-center gap-6"
|
||||
>
|
||||
<svg className="w-14 h-14" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 20l4-16m2 16l4-16M6 9h14M4 15h14" />
|
||||
</svg>
|
||||
Insert Variable
|
||||
</button>
|
||||
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<>
|
||||
{/* Backdrop to close on click outside */}
|
||||
<div
|
||||
className="fixed inset-0 z-[9998]"
|
||||
onClick={() => setIsOpen(false)}
|
||||
/>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
className="fixed w-400 max-w-[calc(100vw-40px)] bg-accent-white border border-border-faint rounded-12 shadow-2xl z-[9999] overflow-hidden"
|
||||
style={{
|
||||
top: `${buttonPosition.top}px`,
|
||||
right: `${buttonPosition.right}px`,
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="p-12 border-b border-border-faint">
|
||||
<h4 className="text-label-small text-accent-black">Available Variables</h4>
|
||||
</div>
|
||||
|
||||
<div className="max-h-320 overflow-y-auto">
|
||||
{variables.map((group, groupIndex) => (
|
||||
<div key={groupIndex}>
|
||||
<div className="px-12 py-8 bg-background-base">
|
||||
<p className="text-body-small text-black-alpha-48 font-medium">
|
||||
{group.category}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{group.items.map((item: any, itemIndex) => (
|
||||
<button
|
||||
key={itemIndex}
|
||||
onClick={() => {
|
||||
onSelect(item.path);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
className={`w-full px-12 py-10 text-left hover:bg-heat-4 transition-colors border-b border-border-faint last:border-0 ${
|
||||
item.isField ? 'pl-24 bg-background-base' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-8">
|
||||
<div className="flex-1 min-w-0">
|
||||
{item.isNested && (
|
||||
<span className="text-body-small text-heat-100 mr-6">↳↳</span>
|
||||
)}
|
||||
{item.isField && !item.isNested && (
|
||||
<span className="text-body-small text-heat-100 mr-6">↳</span>
|
||||
)}
|
||||
<p className={`text-body-small font-medium break-all ${
|
||||
item.isField || item.isInputVariable || item.isNested ? 'text-heat-100' : 'text-accent-black'
|
||||
}`}>
|
||||
{item.name}
|
||||
</p>
|
||||
<p className="text-body-small text-black-alpha-48 mt-2 font-mono text-[10px]">
|
||||
{`{{${item.path}}}`}
|
||||
</p>
|
||||
{item.description && (
|
||||
<p className="text-body-small text-black-alpha-48 mt-4 truncate">
|
||||
{item.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 items-end">
|
||||
{item.propertyType && (
|
||||
<span className="px-6 py-2 bg-black-alpha-4 text-black-alpha-64 rounded-4 text-[10px] font-medium flex-shrink-0">
|
||||
{item.propertyType}
|
||||
</span>
|
||||
)}
|
||||
{(item.nodeType || item.type) && (
|
||||
<span className="px-6 py-2 bg-heat-4 text-heat-100 rounded-4 text-body-small font-medium flex-shrink-0">
|
||||
{item.nodeType || item.type}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="p-12 bg-background-base border-t border-border-faint">
|
||||
<p className="text-body-small text-black-alpha-48">
|
||||
Click a variable to insert its reference
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+2006
File diff suppressed because it is too large
Load Diff
+79
@@ -0,0 +1,79 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, ReactNode } from "react";
|
||||
import { Workflow } from "@/lib/workflow/types";
|
||||
|
||||
interface WorkflowNameEditorProps {
|
||||
workflow: Workflow | null;
|
||||
onUpdate: (updates: Partial<Workflow>) => void;
|
||||
renameTrigger?: number;
|
||||
rightAccessory?: ReactNode;
|
||||
}
|
||||
|
||||
export default function WorkflowNameEditor({ workflow, onUpdate, renameTrigger = 0, rightAccessory }: WorkflowNameEditorProps) {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [name, setName] = useState(workflow?.name || "New Workflow");
|
||||
|
||||
useEffect(() => {
|
||||
if (workflow) {
|
||||
setName(workflow.name);
|
||||
}
|
||||
}, [workflow]);
|
||||
|
||||
useEffect(() => {
|
||||
if (renameTrigger > 0) {
|
||||
setIsEditing(true);
|
||||
}
|
||||
}, [renameTrigger]);
|
||||
|
||||
const handleSave = () => {
|
||||
if (name.trim()) {
|
||||
onUpdate({ name: name.trim() });
|
||||
setIsEditing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleSave();
|
||||
} else if (e.key === 'Escape') {
|
||||
setName(workflow?.name || "New Workflow");
|
||||
setIsEditing(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!workflow) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed top-20 left-240 z-[60]">
|
||||
<div className="flex items-center gap-12">
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
onBlur={handleSave}
|
||||
onKeyDown={handleKeyDown}
|
||||
autoFocus
|
||||
className="px-16 py-8 bg-accent-white border border-heat-100 rounded-8 text-body-medium text-accent-black focus:outline-none shadow-lg min-w-200"
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setIsEditing(true)}
|
||||
className="px-16 py-8 bg-accent-white hover:bg-accent-white border border-border-faint hover:border-heat-100 rounded-8 text-body-medium text-accent-black transition-colors flex items-center gap-8"
|
||||
>
|
||||
<span>{name}</span>
|
||||
<svg className="w-14 h-14 text-black-alpha-48 group-hover:text-heat-100" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
{rightAccessory ? (
|
||||
<div className="flex items-center gap-8">
|
||||
{rightAccessory}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { useCallback, useRef } from "react";
|
||||
import { animate, cubicBezier } from "framer-motion";
|
||||
|
||||
export const useDropdownHover = () => {
|
||||
const backgroundRef = useRef<HTMLDivElement>(null);
|
||||
const timeoutRef = useRef<number | null>(null);
|
||||
|
||||
const onMouseEnter = useCallback((e: React.MouseEvent<HTMLElement>) => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
|
||||
const t = e.target as HTMLElement;
|
||||
const target =
|
||||
t instanceof HTMLButtonElement
|
||||
? t
|
||||
: (t.closest("button") as HTMLButtonElement);
|
||||
|
||||
if (!target || !backgroundRef.current) return;
|
||||
|
||||
if (backgroundRef.current) {
|
||||
// Scale animation sequence like marketing webapp
|
||||
animate(backgroundRef.current, { scale: 0.98, opacity: 1 }).then(() => {
|
||||
if (backgroundRef.current) {
|
||||
animate(backgroundRef.current!, { scale: 1 });
|
||||
}
|
||||
});
|
||||
|
||||
// Y position animation - align with menu item
|
||||
animate(
|
||||
backgroundRef.current,
|
||||
{
|
||||
y: target.offsetTop,
|
||||
},
|
||||
{
|
||||
ease: cubicBezier(0.1, 0.1, 0.25, 1),
|
||||
duration: 0.2,
|
||||
},
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onMouseLeave = useCallback(() => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
|
||||
timeoutRef.current = window.setTimeout(() => {
|
||||
if (backgroundRef.current) {
|
||||
animate(backgroundRef.current, { scale: 1, opacity: 0 });
|
||||
}
|
||||
}, 100);
|
||||
}, []);
|
||||
|
||||
const onClick = useCallback((e: React.MouseEvent<HTMLElement>) => {
|
||||
if (!backgroundRef.current) return;
|
||||
|
||||
// Click animation - scale down then up
|
||||
animate(backgroundRef.current, { scale: 0.98 }, { duration: 0.1 }).then(
|
||||
() => {
|
||||
if (backgroundRef.current) {
|
||||
animate(backgroundRef.current, { scale: 1 }, { duration: 0.1 });
|
||||
}
|
||||
},
|
||||
);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
backgroundRef,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
onClick,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
/**
|
||||
* BigInt Serialization Provider
|
||||
*
|
||||
* Adds global JSON.stringify support for BigInt values.
|
||||
* Required for Next.js 16 beta + React 19 compatibility.
|
||||
*/
|
||||
export function BigIntProvider({ children }: { children: React.ReactNode }) {
|
||||
useEffect(() => {
|
||||
// Add BigInt serialization support globally
|
||||
if (typeof BigInt !== 'undefined') {
|
||||
// @ts-ignore - Adding toJSON to BigInt prototype
|
||||
BigInt.prototype.toJSON = function() {
|
||||
return this.toString();
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Component, ReactNode } from 'react';
|
||||
|
||||
interface ErrorBoundaryProps {
|
||||
children: ReactNode;
|
||||
fallback?: ReactNode;
|
||||
onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
|
||||
}
|
||||
|
||||
interface ErrorBoundaryState {
|
||||
hasError: boolean;
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Error Boundary component to catch React errors and prevent full app crashes.
|
||||
*
|
||||
* Usage:
|
||||
* ```tsx
|
||||
* <ErrorBoundary>
|
||||
* <YourComponent />
|
||||
* </ErrorBoundary>
|
||||
* ```
|
||||
*
|
||||
* With custom fallback:
|
||||
* ```tsx
|
||||
* <ErrorBoundary fallback={<div>Custom error UI</div>}>
|
||||
* <YourComponent />
|
||||
* </ErrorBoundary>
|
||||
* ```
|
||||
*/
|
||||
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
||||
constructor(props: ErrorBoundaryProps) {
|
||||
super(props);
|
||||
this.state = { hasError: false };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
||||
return { hasError: true, error };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||
console.error('ErrorBoundary caught an error:', error, errorInfo);
|
||||
this.props.onError?.(error, errorInfo);
|
||||
}
|
||||
|
||||
handleReset = () => {
|
||||
this.setState({ hasError: false, error: undefined });
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
if (this.props.fallback) {
|
||||
return this.props.fallback;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-24 bg-red-50 border border-red-200 rounded-12 m-20">
|
||||
<h2 className="text-lg font-semibold text-red-800 mb-8">
|
||||
Something went wrong
|
||||
</h2>
|
||||
<p className="text-sm text-red-600 mb-16">
|
||||
{this.state.error?.message || 'An unexpected error occurred'}
|
||||
</p>
|
||||
<button
|
||||
onClick={this.handleReset}
|
||||
className="px-16 py-8 bg-red-600 hover:bg-red-700 text-white rounded-8 text-sm font-medium transition-colors"
|
||||
>
|
||||
Try Again
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
export enum Endpoint {
|
||||
Scrape = "scrape",
|
||||
Crawl = "crawl",
|
||||
Search = "search",
|
||||
Map = "map",
|
||||
Extract = "extract",
|
||||
}
|
||||
|
||||
export enum AgentModel {
|
||||
FIRE_1 = "FIRE-1",
|
||||
}
|
||||
|
||||
export enum FormatType {
|
||||
Markdown = "markdown",
|
||||
Summary = "summary",
|
||||
Json = "json",
|
||||
RawHtml = "rawHtml",
|
||||
Html = "html",
|
||||
Screenshot = "screenshot",
|
||||
ScreenshotFullPage = "screenshot@fullPage",
|
||||
Links = "links",
|
||||
}
|
||||
|
||||
export enum SearchFormatType {
|
||||
Web = "web",
|
||||
Images = "images",
|
||||
News = "news",
|
||||
}
|
||||
|
||||
type Prev = [never, 0, 1, 2, 3, 4, 5];
|
||||
|
||||
type Join<K, P> = K extends string | number
|
||||
? P extends string | number
|
||||
? `${K}.${P}`
|
||||
: never
|
||||
: never;
|
||||
|
||||
export type Paths<T, D extends number = 5> = [D] extends [never]
|
||||
? never
|
||||
: T extends object
|
||||
? {
|
||||
[K in keyof T]-?: K extends string | number
|
||||
? T[K] extends object
|
||||
? K | Join<K, Paths<T[K], Prev[D]>>
|
||||
: K
|
||||
: never;
|
||||
}[keyof T]
|
||||
: "";
|
||||
@@ -0,0 +1,394 @@
|
||||
"use client";
|
||||
|
||||
import { animate } from "framer-motion";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { cn } from "@/utils/cn";
|
||||
|
||||
interface AnimatedDotIconProps {
|
||||
active?: boolean;
|
||||
alwaysHeat?: boolean;
|
||||
triggerOnHover?: boolean;
|
||||
size?: number;
|
||||
className?: string;
|
||||
pattern?:
|
||||
| "usage"
|
||||
| "api-keys"
|
||||
| "settings"
|
||||
| "overview"
|
||||
| "team"
|
||||
| "billing"
|
||||
| "account-settings"
|
||||
| "admin"
|
||||
| "domain-checker"
|
||||
| "extract-playground"
|
||||
| "extract"
|
||||
| "logs"
|
||||
| "playground"
|
||||
| "teams";
|
||||
}
|
||||
|
||||
const initCanvas = (canvas: HTMLCanvasElement) => {
|
||||
const { width, height } = canvas.getBoundingClientRect();
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
|
||||
canvas.style.width = `${width}px`;
|
||||
canvas.style.height = `${height}px`;
|
||||
|
||||
const upscaleCanvas = () => {
|
||||
const scale = window.visualViewport?.scale || 1;
|
||||
const dpr = (window.devicePixelRatio || 1) * scale;
|
||||
|
||||
canvas.width = width * dpr;
|
||||
canvas.height = height * dpr;
|
||||
|
||||
ctx.scale(dpr, dpr);
|
||||
|
||||
canvas.dispatchEvent(new Event("resize"));
|
||||
};
|
||||
|
||||
upscaleCanvas();
|
||||
|
||||
const handleResize = () => {
|
||||
setTimeout(upscaleCanvas, 500);
|
||||
};
|
||||
|
||||
window.addEventListener("resize", handleResize);
|
||||
window.visualViewport?.addEventListener("resize", handleResize);
|
||||
|
||||
return ctx;
|
||||
};
|
||||
|
||||
// Pattern definitions for different pages
|
||||
const patterns = {
|
||||
usage: {
|
||||
grid: [
|
||||
[10, 11, 12, 14, 15, 16],
|
||||
[3, 7, 19, 23],
|
||||
[0, 2, 24, 26],
|
||||
[27, 28, 29, 31, 32, 33],
|
||||
],
|
||||
gridSize: 7,
|
||||
cellSize: 2,
|
||||
spacing: 2,
|
||||
offset: 3,
|
||||
},
|
||||
"api-keys": {
|
||||
grid: [[12], [10, 14], [8, 16], [6, 18], [4, 5, 19, 20]],
|
||||
gridSize: 5,
|
||||
cellSize: 2,
|
||||
spacing: 2,
|
||||
offset: 3,
|
||||
},
|
||||
settings: {
|
||||
grid: [
|
||||
[0, 1, 2, 3, 4],
|
||||
[5, 9],
|
||||
[10, 14],
|
||||
[15, 19],
|
||||
[20, 21, 22, 23, 24],
|
||||
],
|
||||
gridSize: 5,
|
||||
cellSize: 2,
|
||||
spacing: 2,
|
||||
offset: 3,
|
||||
},
|
||||
overview: {
|
||||
grid: [
|
||||
[24],
|
||||
[16, 18, 30, 32],
|
||||
[8, 12, 36, 40],
|
||||
[0, 3, 6, 21, 27, 42, 45, 48],
|
||||
],
|
||||
gridSize: 7,
|
||||
cellSize: 2,
|
||||
spacing: 2,
|
||||
offset: 3,
|
||||
},
|
||||
team: {
|
||||
grid: [
|
||||
[6, 7, 8],
|
||||
[11, 12, 13],
|
||||
[16, 17, 18],
|
||||
[0, 4, 20, 24],
|
||||
],
|
||||
gridSize: 5,
|
||||
cellSize: 2,
|
||||
spacing: 2,
|
||||
offset: 3,
|
||||
},
|
||||
teams: {
|
||||
grid: [
|
||||
[6, 7, 8],
|
||||
[11, 12, 13],
|
||||
[16, 17, 18],
|
||||
[0, 4, 20, 24],
|
||||
],
|
||||
gridSize: 5,
|
||||
cellSize: 2,
|
||||
spacing: 2,
|
||||
offset: 3,
|
||||
},
|
||||
billing: {
|
||||
grid: [
|
||||
[0, 4],
|
||||
[5, 6, 8, 9],
|
||||
[10, 11, 13, 14],
|
||||
[15, 19],
|
||||
],
|
||||
gridSize: 5,
|
||||
cellSize: 2,
|
||||
spacing: 2,
|
||||
offset: 3,
|
||||
},
|
||||
"account-settings": {
|
||||
grid: [
|
||||
[2, 7, 12, 17, 22],
|
||||
[5, 10, 15, 20],
|
||||
[8, 13, 18],
|
||||
[11, 16],
|
||||
],
|
||||
gridSize: 5,
|
||||
cellSize: 2,
|
||||
spacing: 2,
|
||||
offset: 3,
|
||||
},
|
||||
admin: {
|
||||
grid: [
|
||||
[0, 1, 2, 3, 4],
|
||||
[5, 14],
|
||||
[10, 11, 12, 13],
|
||||
[15, 24],
|
||||
[20, 21, 22, 23, 24],
|
||||
],
|
||||
gridSize: 5,
|
||||
cellSize: 2,
|
||||
spacing: 2,
|
||||
offset: 3,
|
||||
},
|
||||
"domain-checker": {
|
||||
grid: [
|
||||
[12, 13, 14],
|
||||
[7, 11, 15, 19],
|
||||
[2, 6, 20, 24],
|
||||
[0, 1, 25, 26],
|
||||
],
|
||||
gridSize: 6,
|
||||
cellSize: 2,
|
||||
spacing: 2,
|
||||
offset: 3,
|
||||
},
|
||||
"extract-playground": {
|
||||
grid: [
|
||||
[5, 10, 15, 20],
|
||||
[6, 11, 16, 21],
|
||||
[7, 12, 17, 22],
|
||||
[8, 13, 18, 23],
|
||||
],
|
||||
gridSize: 5,
|
||||
cellSize: 2,
|
||||
spacing: 2,
|
||||
offset: 3,
|
||||
},
|
||||
extract: {
|
||||
grid: [[12], [7, 17], [2, 6, 18, 22], [0, 1, 3, 4, 20, 21, 23, 24]],
|
||||
gridSize: 5,
|
||||
cellSize: 2,
|
||||
spacing: 2,
|
||||
offset: 3,
|
||||
},
|
||||
logs: {
|
||||
grid: [
|
||||
[0, 5, 10, 15, 20],
|
||||
[1, 6, 11, 16, 21],
|
||||
[2, 7, 12, 17, 22],
|
||||
[3, 8, 13, 18, 23],
|
||||
],
|
||||
gridSize: 5,
|
||||
cellSize: 2,
|
||||
spacing: 2,
|
||||
offset: 3,
|
||||
},
|
||||
playground: {
|
||||
grid: [
|
||||
[6, 8, 16, 18],
|
||||
[10, 11, 12, 13, 14],
|
||||
[5, 9, 15, 19],
|
||||
[0, 4, 20, 24],
|
||||
],
|
||||
gridSize: 5,
|
||||
cellSize: 2,
|
||||
spacing: 2,
|
||||
offset: 3,
|
||||
},
|
||||
};
|
||||
|
||||
export function AnimatedDotIcon({
|
||||
active = true,
|
||||
alwaysHeat = false,
|
||||
triggerOnHover = false,
|
||||
size = 20,
|
||||
className,
|
||||
pattern = "usage",
|
||||
}: AnimatedDotIconProps) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
const fnRefs = useRef<{
|
||||
activate: () => void;
|
||||
deactivate: () => void;
|
||||
}>({ activate: () => {}, deactivate: () => {} });
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = initCanvas(canvas);
|
||||
const config = patterns[pattern];
|
||||
|
||||
let isRunning = false;
|
||||
let isActive = false;
|
||||
|
||||
let activeGroup = 0;
|
||||
const rowAlphas = [0.2, 0.4, 1, 0.04];
|
||||
|
||||
const scaler = size / 20;
|
||||
|
||||
const render = () => {
|
||||
ctx.fillStyle = "#fa5d19";
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
for (const group of config.grid.slice(0, 4)) {
|
||||
const groupIndex = config.grid.indexOf(group);
|
||||
ctx.globalAlpha = rowAlphas[groupIndex];
|
||||
|
||||
for (const index of group) {
|
||||
ctx.fillRect(
|
||||
(config.offset + (index % config.gridSize) * config.spacing) *
|
||||
scaler,
|
||||
(config.offset +
|
||||
Math.floor(index / config.gridSize) * config.spacing) *
|
||||
scaler,
|
||||
config.cellSize * scaler,
|
||||
config.cellSize * scaler,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (isRunning) {
|
||||
requestAnimationFrame(render);
|
||||
}
|
||||
};
|
||||
|
||||
const timeouts: number[] = [];
|
||||
let runCount = 0;
|
||||
|
||||
const cycle = () => {
|
||||
isRunning = true;
|
||||
activeGroup = (activeGroup + 1) % 5;
|
||||
|
||||
rowAlphas.forEach((alpha, index) => {
|
||||
let targetAlpha = alpha;
|
||||
|
||||
if (index === activeGroup) targetAlpha = 1;
|
||||
else if (index === (activeGroup + 1) % 4) targetAlpha = 0.12;
|
||||
else if (index === (activeGroup + 2) % 4) targetAlpha = 0.2;
|
||||
else if (index === (activeGroup + 3) % 4) targetAlpha = 0.4;
|
||||
|
||||
animate(alpha, targetAlpha, {
|
||||
duration: 0.05,
|
||||
onUpdate: (value) => {
|
||||
rowAlphas[index] = value;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
timeouts.forEach((timeout) => {
|
||||
window.clearTimeout(timeout);
|
||||
});
|
||||
|
||||
timeouts.push(
|
||||
window.setTimeout(() => {
|
||||
isRunning = false;
|
||||
}, 300),
|
||||
);
|
||||
|
||||
if (activeGroup === 3) runCount += 1;
|
||||
|
||||
if ((runCount === 2 || !isActive) && activeGroup === 2) return;
|
||||
|
||||
timeouts.push(
|
||||
window.setTimeout(() => {
|
||||
cycle();
|
||||
}, 50),
|
||||
);
|
||||
};
|
||||
|
||||
fnRefs.current = {
|
||||
activate: () => {
|
||||
if (isActive) return;
|
||||
|
||||
isActive = true;
|
||||
runCount = 0;
|
||||
cycle();
|
||||
render();
|
||||
},
|
||||
deactivate: () => {
|
||||
if (!isActive) return;
|
||||
isActive = false;
|
||||
},
|
||||
};
|
||||
|
||||
render();
|
||||
canvas.addEventListener("resize", render);
|
||||
|
||||
if (triggerOnHover) {
|
||||
const group = canvasRef.current!.closest(".group");
|
||||
|
||||
if (group) {
|
||||
group.addEventListener("mouseenter", fnRefs.current.activate);
|
||||
group.addEventListener("mouseleave", fnRefs.current.deactivate);
|
||||
|
||||
return () => {
|
||||
group.removeEventListener("mouseenter", fnRefs.current.activate);
|
||||
group.removeEventListener("mouseleave", fnRefs.current.deactivate);
|
||||
};
|
||||
}
|
||||
}
|
||||
}, [triggerOnHover, size, pattern]);
|
||||
|
||||
useEffect(() => {
|
||||
if (triggerOnHover) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting && active) {
|
||||
fnRefs.current.activate();
|
||||
} else {
|
||||
fnRefs.current.deactivate();
|
||||
}
|
||||
},
|
||||
{ threshold: 0.5 },
|
||||
);
|
||||
|
||||
observer.observe(canvasRef.current!);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [active, triggerOnHover]);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
className={cn(
|
||||
alwaysHeat
|
||||
? ""
|
||||
: [
|
||||
"[&.grayscale]:opacity-60 transition-[filter,opacity]",
|
||||
!active && "grayscale",
|
||||
],
|
||||
className,
|
||||
)}
|
||||
ref={canvasRef}
|
||||
style={{ width: size, height: size }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { cn } from "@/utils/cn";
|
||||
|
||||
const asciiPatterns = [
|
||||
`· · · · · · · · · · · · · · · · · · · ·
|
||||
· · · · · · · · · · · · · · · · · · ·
|
||||
· · · · · · · · · · · · · · · · · · · ·
|
||||
· · · · · · · · · · · · · · · · · · ·
|
||||
· · · · · · · · · · · · · · · · · · · ·`,
|
||||
`· · · · · · · · · · · · · · · · · · · ·
|
||||
· · · · ▪ · · · · · · · · ▪ · · · · ·
|
||||
· · · · · · · · · · · · · · · · · · · ·
|
||||
· · · · · · · · · · · · · · · · · · ·
|
||||
· · · · ▪ · · · · · · · · ▪ · · · · · ·`,
|
||||
`· · · · · · · · · · · · · · · · · · · ·
|
||||
· · · ▪ ▄ ▪ · · · · · · ▪ ▄ ▪ · · · ·
|
||||
· · · · ▪ · · · · · · · · ▪ · · · · · ·
|
||||
· · · · · · · · · · · · · · · · · · ·
|
||||
· · · ▪ ▄ ▪ · · · · · · ▪ ▄ ▪ · · · · ·`,
|
||||
`· · · · · · · · · · · · · · · · · · · ·
|
||||
· · ▪ ▄ █ ▄ ▪ · · · · ▪ ▄ █ ▄ ▪ · · ·
|
||||
· · · ▪ ▄ ▪ · · · · · · ▪ ▄ ▪ · · · · ·
|
||||
· · · ▪ · · · · · · · · · ▪ · · · · ·
|
||||
· · ▪ ▄ █ ▄ ▪ · · · · ▪ ▄ █ ▄ ▪ · · · ·`,
|
||||
];
|
||||
|
||||
interface AsciiBackgroundProps {
|
||||
className?: string;
|
||||
variant?: "dots" | "grid" | "flame";
|
||||
}
|
||||
|
||||
export function AsciiBackground({
|
||||
className,
|
||||
variant = "dots",
|
||||
}: AsciiBackgroundProps) {
|
||||
const [frameIndex, setFrameIndex] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setFrameIndex((prev) => (prev + 1) % asciiPatterns.length);
|
||||
}, 2000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-0 pointer-events-none select-none overflow-hidden",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<pre className="text-heat-100/3 font-mono text-[10px] leading-tight whitespace-pre absolute top-0 left-0 w-full h-full flex items-center justify-center">
|
||||
{asciiPatterns[frameIndex]}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { cn } from "@/utils/cn";
|
||||
import { setIntervalOnVisible } from "@/utils/set-timeout-on-visible";
|
||||
import data from "@/components/shared/effects/flame/explosion-data.json";
|
||||
|
||||
interface AsciiFlameBackgroundProps {
|
||||
className?: string;
|
||||
colorClassName?: string;
|
||||
fontSizePx?: number;
|
||||
lineHeightPx?: number;
|
||||
}
|
||||
|
||||
// Reusable ASCII flame background (same frames used by CoreReliableBarFlame)
|
||||
export default function AsciiFlameBackground({
|
||||
className,
|
||||
colorClassName = "text-heat-100/30",
|
||||
fontSizePx = 10,
|
||||
lineHeightPx = 12.5,
|
||||
}: AsciiFlameBackgroundProps) {
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let index = 0;
|
||||
const stop = setIntervalOnVisible({
|
||||
element: wrapperRef.current,
|
||||
callback: () => {
|
||||
index += 1;
|
||||
if (index >= (data as string[]).length) index = 0;
|
||||
if (ref.current) ref.current.innerHTML = (data as string[])[index];
|
||||
},
|
||||
interval: 80,
|
||||
});
|
||||
|
||||
return () => stop?.();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={wrapperRef}
|
||||
className={cn("relative pointer-events-none select-none", className)}
|
||||
>
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"font-ascii absolute inset-0 fc-decoration",
|
||||
colorClassName,
|
||||
)}
|
||||
style={{
|
||||
whiteSpace: "pre",
|
||||
fontSize: `${fontSizePx}px`,
|
||||
lineHeight: `${lineHeightPx}px`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
.button {
|
||||
transition: all 0.2s cubic-bezier(0.25, 0.1, 0.25, 1),
|
||||
scale 0.1s cubic-bezier(0.25, 0.1, 0.25, 1),
|
||||
box-shadow 0.1s cubic-bezier(0.25, 0.1, 0.25, 1);
|
||||
}
|
||||
|
||||
.button:active {
|
||||
transition: all 0.2s cubic-bezier(0.25, 0.1, 0.25, 1),
|
||||
scale 0.05s cubic-bezier(0.25, 0.1, 0.25, 1),
|
||||
box-shadow 0.05s cubic-bezier(0.25, 0.1, 0.25, 1);
|
||||
}
|
||||
|
||||
.button-primary {
|
||||
background: #ff4c00;
|
||||
background: color(display-p3 0.9816 0.3634 0.0984);
|
||||
|
||||
box-shadow: 0px -6px 12px 0px rgba(255, 0, 0, 0.2) inset,
|
||||
0px 2px 4px 0px rgba(255, 77, 0, 0.12),
|
||||
0px 1px 1px 0px rgba(255, 77, 0, 0.12),
|
||||
0px 0.5px 0.5px 0px rgba(255, 77, 0, 0.16),
|
||||
0px 0.25px 0.25px 0px rgba(255, 77, 0, 0.2);
|
||||
|
||||
box-shadow: 0px -6px 12px 0px color(display-p3 0.9804 0.1127 0.098 / 0.2) inset,
|
||||
0px 2px 4px 0px color(display-p3 0.9804 0.3647 0.098 / 0.12),
|
||||
0px 1px 1px 0px color(display-p3 0.9804 0.3647 0.098 / 0.12),
|
||||
0px 0.5px 0.5px 0px color(display-p3 0.9804 0.3647 0.098 / 0.16),
|
||||
0px 0.25px 0.25px 0px color(display-p3 0.9804 0.3647 0.098 / 0.2);
|
||||
}
|
||||
|
||||
.button-primary:hover {
|
||||
box-shadow: 0px -6px 12px 0px rgba(255, 0, 0, 0.2) inset,
|
||||
0px 4px 8px 0px rgba(255, 77, 0, 0.16),
|
||||
0px 1px 1px 0px rgba(255, 77, 0, 0.12),
|
||||
0px 0.5px 0.5px 0px rgba(255, 77, 0, 0.16),
|
||||
0px 0.25px 0.25px 0px rgba(255, 77, 0, 0.2);
|
||||
box-shadow: 0px -6px 12px 0px color(display-p3 0.9804 0.1127 0.098 / 0.2) inset,
|
||||
0px 4px 8px 0px color(display-p3 0.9804 0.3647 0.098 / 0.16),
|
||||
0px 1px 1px 0px color(display-p3 0.9804 0.3647 0.098 / 0.12),
|
||||
0px 0.5px 0.5px 0px color(display-p3 0.9804 0.3647 0.098 / 0.16),
|
||||
0px 0.25px 0.25px 0px color(display-p3 0.9804 0.3647 0.098 / 0.2);
|
||||
}
|
||||
|
||||
.button-primary:active {
|
||||
box-shadow: 0px -6px 12px 0px rgba(255, 0, 0, 0.2) inset,
|
||||
0px 2px 4px 0px rgba(255, 77, 0, 0.12),
|
||||
0px 1px 1px 0px rgba(255, 77, 0, 0.12),
|
||||
0px 0.5px 0.5px 0px rgba(255, 77, 0, 0.16),
|
||||
0px 0.25px 0.25px 0px rgba(255, 77, 0, 0.2);
|
||||
box-shadow: 0px -6px 12px 0px color(display-p3 0.9804 0.1127 0.098 / 0.2) inset,
|
||||
0px 2px 4px 0px color(display-p3 0.9804 0.3647 0.098 / 0.12),
|
||||
0px 1px 1px 0px color(display-p3 0.9804 0.3647 0.098 / 0.12),
|
||||
0px 0.5px 0.5px 0px color(display-p3 0.9804 0.3647 0.098 / 0.16),
|
||||
0px 0.25px 0.25px 0px color(display-p3 0.9804 0.3647 0.098 / 0.2);
|
||||
}
|
||||
|
||||
.button-background {
|
||||
background: linear-gradient(to bottom, white, transparent);
|
||||
|
||||
opacity: 0.06;
|
||||
|
||||
transition: opacity 0.2s cubic-bezier(0.25, 0.1, 0.25, 1);
|
||||
}
|
||||
|
||||
.button:hover .button-background {
|
||||
opacity: 0.08;
|
||||
}
|
||||
|
||||
.button:active .button-background {
|
||||
opacity: 0;
|
||||
|
||||
transition: opacity 0.05s cubic-bezier(0.25, 0.1, 0.25, 1);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Children, ButtonHTMLAttributes } from "react";
|
||||
|
||||
import { cn } from "@/utils/cn";
|
||||
|
||||
interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: "primary" | "secondary" | "tertiary" | "playground" | "destructive";
|
||||
size?: "default" | "large";
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export default function Button({
|
||||
variant = "primary",
|
||||
size = "default",
|
||||
disabled,
|
||||
...attrs
|
||||
}: Props) {
|
||||
const children = handleChildren(attrs.children);
|
||||
|
||||
return (
|
||||
<button
|
||||
{...attrs}
|
||||
type={attrs.type ?? "button"}
|
||||
className={cn(
|
||||
attrs.className,
|
||||
"[&>span]:px-6 flex items-center justify-center button relative [&>*]:relative",
|
||||
"text-label-medium lg-max:[&_svg]:size-24",
|
||||
`button-${variant} group/button`,
|
||||
{
|
||||
"rounded-8 p-6": size === "default",
|
||||
"rounded-10 p-8 gap-2": size === "large",
|
||||
|
||||
"text-accent-white active:[scale:0.995]": variant === "primary",
|
||||
"text-accent-black active:[scale:0.99] active:bg-black-alpha-7": [
|
||||
"secondary",
|
||||
"tertiary",
|
||||
"playground",
|
||||
].includes(variant),
|
||||
"bg-black-alpha-4 hover:bg-black-alpha-6": variant === "secondary",
|
||||
"hover:bg-black-alpha-4": variant === "tertiary",
|
||||
},
|
||||
variant === "playground" && [
|
||||
"before:inside-border before:border-black-alpha-4",
|
||||
disabled
|
||||
? "before:opacity-0 bg-black-alpha-4 text-black-alpha-24"
|
||||
: "hover:bg-black-alpha-4 hover:before:opacity-0 active:before:opacity-0",
|
||||
],
|
||||
)}
|
||||
disabled={disabled}
|
||||
>
|
||||
{variant === "primary" && (
|
||||
<div className="overlay button-background !absolute" />
|
||||
)}
|
||||
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
const handleChildren = (children: React.ReactNode) => {
|
||||
return Children.toArray(children).map((child) => {
|
||||
if (typeof child === "string") {
|
||||
return <span key={child}>{child}</span>;
|
||||
}
|
||||
|
||||
return child;
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,148 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { cn } from "@/utils/cn";
|
||||
import { LucideIcon } from "lucide-react";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import AnimatedWidth from "@/components/shared/layout/animated-width";
|
||||
|
||||
interface CapsuleButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
icon?: LucideIcon | React.ComponentType<{ className?: string }>;
|
||||
iconPosition?: "left" | "right";
|
||||
children: React.ReactNode;
|
||||
size?: "sm" | "md" | "lg";
|
||||
fullWidth?: boolean;
|
||||
variant?: "primary" | "secondary" | "tertiary" | "ghost";
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export function CapsuleButton({
|
||||
icon: Icon,
|
||||
iconPosition = "left",
|
||||
children,
|
||||
className,
|
||||
size = "md",
|
||||
fullWidth = false,
|
||||
variant = "primary",
|
||||
loading = false,
|
||||
disabled,
|
||||
...props
|
||||
}: CapsuleButtonProps) {
|
||||
const [isPressed, setIsPressed] = React.useState(false);
|
||||
|
||||
const sizeClasses = {
|
||||
sm: "h-32 px-16 text-label-small gap-6",
|
||||
md: "h-40 px-20 text-label-medium gap-8",
|
||||
lg: "h-40 px-20 text-label-medium gap-8",
|
||||
};
|
||||
|
||||
const iconSizes = {
|
||||
sm: "w-14 h-14",
|
||||
md: "w-16 h-16",
|
||||
lg: "w-16 h-16",
|
||||
};
|
||||
|
||||
const variants = {
|
||||
primary: [
|
||||
"bg-heat-100 text-white",
|
||||
"hover:bg-heat-200",
|
||||
"active:scale-[0.98]",
|
||||
"shadow-[0_1px_2px_rgba(0,0,0,0.05)]",
|
||||
"hover:shadow-[0_4px_12px_rgba(250,93,25,0.25)]",
|
||||
],
|
||||
secondary: [
|
||||
"bg-black text-white",
|
||||
"hover:bg-black/90",
|
||||
"active:scale-[0.98]",
|
||||
"shadow-[0_1px_2px_rgba(0,0,0,0.05)]",
|
||||
"hover:shadow-[0_4px_12px_rgba(0,0,0,0.15)]",
|
||||
],
|
||||
tertiary: [
|
||||
"bg-white text-black border border-black-alpha-8",
|
||||
"hover:bg-black-alpha-4 hover:border-black-alpha-12",
|
||||
"active:scale-[0.98]",
|
||||
],
|
||||
ghost: [
|
||||
"bg-transparent text-black-alpha-60",
|
||||
"hover:text-black hover:bg-black-alpha-4",
|
||||
"active:scale-[0.98]",
|
||||
],
|
||||
};
|
||||
|
||||
const isDisabled = disabled || loading;
|
||||
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
// Base styles
|
||||
"inline-flex items-center justify-center rounded-full transition-all duration-200",
|
||||
// Size
|
||||
sizeClasses[size],
|
||||
// Variant
|
||||
variants[variant],
|
||||
// Full width
|
||||
fullWidth && "w-full",
|
||||
// Disabled state
|
||||
isDisabled && [
|
||||
"opacity-50 cursor-not-allowed",
|
||||
"hover:shadow-none hover:bg-current",
|
||||
],
|
||||
// Pressed state
|
||||
isPressed && "scale-[0.98]",
|
||||
className,
|
||||
)}
|
||||
disabled={isDisabled}
|
||||
onMouseDown={() => !isDisabled && setIsPressed(true)}
|
||||
onMouseUp={() => setIsPressed(false)}
|
||||
onMouseLeave={() => setIsPressed(false)}
|
||||
{...props}
|
||||
>
|
||||
<AnimatedWidth initial={{ width: "auto" }}>
|
||||
<AnimatePresence initial={false} mode="popLayout">
|
||||
{loading ? (
|
||||
<motion.div
|
||||
key="loading"
|
||||
animate={{ opacity: 1, filter: "blur(0px)", scale: 1 }}
|
||||
className="flex gap-8 items-center justify-center"
|
||||
exit={{ opacity: 0, filter: "blur(2px)", scale: 0.9 }}
|
||||
initial={{ opacity: 0, filter: "blur(2px)", scale: 0.95 }}
|
||||
>
|
||||
<span>Loading...</span>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="content"
|
||||
animate={{ opacity: 1, filter: "blur(0px)", scale: 1 }}
|
||||
className="flex gap-8 items-center justify-center"
|
||||
exit={{ opacity: 0, filter: "blur(2px)", scale: 0.9 }}
|
||||
initial={{ opacity: 0, filter: "blur(2px)", scale: 0.95 }}
|
||||
>
|
||||
{Icon && iconPosition === "left" && (
|
||||
<span
|
||||
className={cn(
|
||||
iconSizes[size],
|
||||
"flex-shrink-0 inline-flex items-center justify-center",
|
||||
)}
|
||||
>
|
||||
<Icon className="w-full h-full" />
|
||||
</span>
|
||||
)}
|
||||
<span>{children}</span>
|
||||
{Icon && iconPosition === "right" && (
|
||||
<span
|
||||
className={cn(
|
||||
iconSizes[size],
|
||||
"flex-shrink-0 inline-flex items-center justify-center",
|
||||
)}
|
||||
>
|
||||
<Icon className="w-full h-full" />
|
||||
</span>
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</AnimatedWidth>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import Link from "next/link";
|
||||
import { cn } from "@/utils/cn";
|
||||
|
||||
interface FireActionLinkProps {
|
||||
href?: string;
|
||||
label: string;
|
||||
className?: string;
|
||||
variant?: "link" | "button";
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export function FireActionLink({
|
||||
href,
|
||||
label,
|
||||
className,
|
||||
variant = "link",
|
||||
onClick,
|
||||
}: FireActionLinkProps) {
|
||||
const baseClasses =
|
||||
variant === "button"
|
||||
? cn(
|
||||
"inline-block py-4 px-8 rounded-6",
|
||||
"text-label-small text-heat-100 bg-heat-4",
|
||||
"hover:bg-heat-8 transition-all",
|
||||
"active:scale-[0.98]",
|
||||
className,
|
||||
)
|
||||
: cn(
|
||||
"text-label-small text-secondary hover:text-heat-100 transition-all",
|
||||
"hover:underline underline-offset-4",
|
||||
"active:scale-[0.98]",
|
||||
className,
|
||||
);
|
||||
|
||||
if (onClick) {
|
||||
return (
|
||||
<button onClick={onClick} className={baseClasses}>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Link href={href || "#"} className={baseClasses}>
|
||||
{label}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// Button Components
|
||||
export { SlateButton } from "./slate-button";
|
||||
// export { HeatButton } from "./heat-button";
|
||||
export { FireActionLink } from "./fire-action-link";
|
||||
@@ -0,0 +1,122 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { cn } from "@/utils/cn";
|
||||
import { LucideIcon } from "lucide-react";
|
||||
|
||||
interface SlateButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
icon?:
|
||||
| LucideIcon
|
||||
| React.ComponentType<{
|
||||
className?: string;
|
||||
isHovered?: boolean;
|
||||
isOpen?: boolean;
|
||||
}>
|
||||
| React.ReactNode;
|
||||
iconPosition?: "left" | "right";
|
||||
children: React.ReactNode;
|
||||
size?: "sm" | "md" | "lg";
|
||||
fullWidth?: boolean;
|
||||
isLoading?: boolean;
|
||||
isOpen?: boolean;
|
||||
}
|
||||
|
||||
export const SlateButton = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
SlateButtonProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
icon: Icon,
|
||||
iconPosition = "left",
|
||||
children,
|
||||
className,
|
||||
size = "md",
|
||||
fullWidth = false,
|
||||
isLoading = false,
|
||||
isOpen = false,
|
||||
disabled,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const [isHovered, setIsHovered] = React.useState(false);
|
||||
|
||||
const sizeClasses = {
|
||||
sm: "h-32 px-12 text-body-small gap-6",
|
||||
md: "h-40 px-16 text-body-medium gap-8",
|
||||
lg: "h-48 px-24 text-body-large gap-10",
|
||||
};
|
||||
|
||||
const iconSizes = {
|
||||
sm: "w-14 h-14",
|
||||
md: "w-16 h-16",
|
||||
lg: "w-20 h-20",
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
className={cn(
|
||||
// Base styles
|
||||
"inline-flex items-center justify-center rounded-12 transition-all",
|
||||
// Colors
|
||||
"bg-black-alpha-4 text-accent-black",
|
||||
"hover:bg-black-alpha-6",
|
||||
"active:scale-[0.98]",
|
||||
// Border
|
||||
// "border-0",
|
||||
// Size
|
||||
sizeClasses[size],
|
||||
// States
|
||||
disabled && "opacity-50 cursor-not-allowed",
|
||||
isLoading && "cursor-wait",
|
||||
// Full width
|
||||
fullWidth && "w-full",
|
||||
className,
|
||||
)}
|
||||
disabled={disabled || isLoading}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
{...props}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className={cn("animate-spin rounded-full", iconSizes[size])} />
|
||||
) : (
|
||||
<>
|
||||
{Icon &&
|
||||
iconPosition === "left" &&
|
||||
(React.isValidElement(Icon) ? (
|
||||
Icon
|
||||
) : (
|
||||
//@ts-ignore
|
||||
<Icon
|
||||
className={cn(iconSizes[size], "flex-shrink-0")}
|
||||
// @ts-ignore - Some icons support isHovered and isOpen
|
||||
isHovered={isHovered}
|
||||
isOpen={isOpen}
|
||||
/>
|
||||
))}
|
||||
{children}
|
||||
{Icon &&
|
||||
iconPosition === "right" &&
|
||||
(React.isValidElement(Icon) ? (
|
||||
Icon
|
||||
) : (
|
||||
//@ts-ignore
|
||||
<Icon
|
||||
className={cn(iconSizes[size], "flex-shrink-0")}
|
||||
// @ts-ignore - Some icons support isHovered and isOpen
|
||||
isHovered={isHovered}
|
||||
isOpen={isOpen}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
SlateButton.displayName = "SlateButton";
|
||||
@@ -0,0 +1,31 @@
|
||||
import colors from "@/styles/colors.json";
|
||||
|
||||
const TYPED_COLORS = colors as unknown as Record<
|
||||
string,
|
||||
Record<"hex" | "p3", string>
|
||||
>;
|
||||
|
||||
const hslValues = Object.entries(TYPED_COLORS).map(([key, value]) => {
|
||||
// Fix hex values - they need # prefix
|
||||
const hexValue = value.hex.startsWith("#") ? value.hex : `#${value.hex}`;
|
||||
return `--${key}: ${hexValue}`;
|
||||
});
|
||||
|
||||
const p3Values = Object.entries(TYPED_COLORS)
|
||||
.filter(([, value]) => value.p3)
|
||||
.map(([key, value]) => `--${key}: color(display-p3 ${value.p3})`);
|
||||
|
||||
const colorsStyle = `
|
||||
:root {
|
||||
${hslValues.join(";\n ")}
|
||||
}
|
||||
|
||||
@supports (color: color(display-p3 1 1 1)) {
|
||||
:root {
|
||||
${p3Values.join(";\n ")}
|
||||
}
|
||||
}`;
|
||||
|
||||
export default function ColorStyles() {
|
||||
return <style dangerouslySetInnerHTML={{ __html: colorsStyle }} />;
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
import { animate, AnimatePresence, cubicBezier, motion } from "motion/react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import { cn } from "@/utils/cn";
|
||||
import { lockBody } from "../lockBody";
|
||||
import PortalToBody from "../utils/portal-to-body";
|
||||
|
||||
export default function Combobox({
|
||||
placeholder,
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
className,
|
||||
}: {
|
||||
placeholder?: string;
|
||||
options: { label: string; value: string }[];
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
className?: string;
|
||||
}) {
|
||||
const selected = useMemo(() => {
|
||||
return options.find((option) => option.value === value);
|
||||
}, [options, value]);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [bounds, setBounds] = useState<DOMRect | null>(null);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
lockBody("combobox", isOpen);
|
||||
}, [isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("click", (e) => {
|
||||
if (ref.current && e.composedPath().includes(ref.current)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsOpen(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={cn("w-full", className)} ref={ref}>
|
||||
<button
|
||||
className={cn(
|
||||
"relative bg-accent-white flex w-full gap-4 rounded-8 p-6 pl-10",
|
||||
"before:inside-border before:border-black-alpha-8 hover:before:border-black-alpha-12 hover:bg-black-alpha-2",
|
||||
"text-body-medium",
|
||||
isOpen &&
|
||||
"!bg-accent-white before:!border-heat-100 before:!border-[1.25px]",
|
||||
)}
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setIsOpen(!isOpen);
|
||||
setBounds(ref.current?.getBoundingClientRect() ?? null);
|
||||
}}
|
||||
>
|
||||
<div className={cn("flex-1", !selected && "text-black-alpha-40")}>
|
||||
{selected?.label || placeholder}
|
||||
</div>
|
||||
|
||||
<motion.svg
|
||||
animate={{ rotate: isOpen ? 180 : 0 }}
|
||||
fill="none"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M7 8.5L10 11.5L13 8.5"
|
||||
stroke="#262626"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeOpacity="0.56"
|
||||
strokeWidth="1.25"
|
||||
/>
|
||||
</motion.svg>
|
||||
</button>
|
||||
|
||||
<PortalToBody>
|
||||
<AnimatePresence initial={false}>
|
||||
{isOpen && bounds && (
|
||||
<motion.div
|
||||
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
|
||||
className="fixed bg-accent-white rounded-12 z-[401]"
|
||||
exit={{ opacity: 0, y: 0, filter: "blur(4px)" }}
|
||||
initial={{ opacity: 0, y: -12, filter: "blur(4px)" }}
|
||||
style={{
|
||||
top: bounds.top + bounds.height + 8,
|
||||
left: bounds.left,
|
||||
width: bounds.width,
|
||||
boxShadow:
|
||||
"0px 32px 40px 6px rgba(0, 0, 0, 0.02), 0px 12px 32px 0px rgba(0, 0, 0, 0.02), 0px 24px 32px -8px rgba(0, 0, 0, 0.02), 0px 8px 16px -2px rgba(0, 0, 0, 0.02), 0px 0px 0px 1px rgba(0, 0, 0, 0.04)",
|
||||
}}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<div className="p-4">
|
||||
<Items
|
||||
options={options}
|
||||
onChange={(value) => {
|
||||
onChange(value);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</PortalToBody>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const Items = ({
|
||||
options,
|
||||
onChange,
|
||||
}: {
|
||||
options: { label: string; value: string }[];
|
||||
onChange: (value: string) => void;
|
||||
}) => {
|
||||
const backgroundRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div
|
||||
className="absolute top-0 opacity-0 left-0 bg-black-alpha-4 rounded-8 w-full h-32 pointer-events-none"
|
||||
ref={backgroundRef}
|
||||
/>
|
||||
|
||||
{options.map((option) => (
|
||||
<button
|
||||
className="w-full group py-6 px-10 text-label-small"
|
||||
key={option.value}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onChange(option.value);
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
const t = e.target as HTMLElement;
|
||||
|
||||
let target =
|
||||
t instanceof HTMLButtonElement
|
||||
? t
|
||||
: (t.closest("button") as HTMLButtonElement);
|
||||
target = target.closest(".group") as HTMLButtonElement;
|
||||
|
||||
animate(backgroundRef.current!, { scale: 0.995 }).then(() =>
|
||||
animate(backgroundRef.current!, { scale: 1 }),
|
||||
);
|
||||
|
||||
animate(
|
||||
backgroundRef.current!,
|
||||
{
|
||||
y: target.offsetTop,
|
||||
opacity: 1,
|
||||
},
|
||||
{
|
||||
ease: cubicBezier(0.165, 0.84, 0.44, 1),
|
||||
duration: 0.2,
|
||||
},
|
||||
);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
animate(backgroundRef.current!, { opacity: 0 });
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
"use client";
|
||||
|
||||
import { HTMLAttributes, useEffect, useRef } from "react";
|
||||
|
||||
import { cn } from "@/utils/cn";
|
||||
import { setIntervalOnVisible } from "@/utils/set-timeout-on-visible";
|
||||
|
||||
import data from "./hero-flame-data.json";
|
||||
|
||||
export default function CoreFlame(attrs: HTMLAttributes<HTMLDivElement>) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let index = 0;
|
||||
|
||||
const interval = setIntervalOnVisible({
|
||||
element: wrapperRef.current,
|
||||
callback: () => {
|
||||
index++;
|
||||
if (index >= data.length) index = 0;
|
||||
|
||||
const newStr = data[index];
|
||||
|
||||
ref.current!.innerHTML = newStr;
|
||||
},
|
||||
interval: 80,
|
||||
});
|
||||
|
||||
return () => interval?.();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="absolute inset-10 -z-[10] overflow-clip">
|
||||
<div
|
||||
ref={wrapperRef}
|
||||
{...attrs}
|
||||
className={cn(
|
||||
"cw-[1110px] ch-470 absolute pointer-events-none select-none",
|
||||
attrs.className,
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className="text-black-alpha-20 relative left-0 font-ascii fc-decoration"
|
||||
ref={ref}
|
||||
style={{
|
||||
whiteSpace: "pre",
|
||||
fontSize: 8,
|
||||
lineHeight: "10px",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
"use client";
|
||||
|
||||
import { HTMLAttributes, useEffect, useRef } from "react";
|
||||
|
||||
import { cn } from "@/utils/cn";
|
||||
import { setIntervalOnVisible } from "@/utils/set-timeout-on-visible";
|
||||
|
||||
import data from "./explosion-data.json";
|
||||
|
||||
export function AsciiExplosion(attrs: HTMLAttributes<HTMLDivElement>) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let index = -30;
|
||||
|
||||
const interval = setIntervalOnVisible({
|
||||
element: wrapperRef.current,
|
||||
callback: () => {
|
||||
index++;
|
||||
if (index >= data.length) index = -40;
|
||||
if (index < 0) return;
|
||||
|
||||
ref.current!.innerHTML = data[index];
|
||||
},
|
||||
interval: 40,
|
||||
});
|
||||
|
||||
return () => interval?.();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={wrapperRef}
|
||||
{...attrs}
|
||||
className={cn(
|
||||
"w-[720px] h-[400px] absolute flex gap-16 pointer-events-none select-none",
|
||||
attrs.className,
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className="text-[#FA5D19] font-mono fc-decoration"
|
||||
dangerouslySetInnerHTML={{ __html: data[0] }}
|
||||
ref={ref}
|
||||
style={{
|
||||
whiteSpace: "pre",
|
||||
fontSize: "10px",
|
||||
lineHeight: "12.5px",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Default export for backward compatibility
|
||||
export default AsciiExplosion;
|
||||
@@ -0,0 +1,64 @@
|
||||
"use client";
|
||||
|
||||
import { HTMLAttributes, useEffect, useRef } from "react";
|
||||
import { cn } from "@/utils/cn";
|
||||
import { setIntervalOnVisible } from "@/utils/set-timeout-on-visible";
|
||||
import data from "./pulse-data.json";
|
||||
|
||||
interface AuthPulseProps extends HTMLAttributes<HTMLDivElement> {
|
||||
interval?: number;
|
||||
opacity?: number;
|
||||
}
|
||||
|
||||
export function AuthPulse({
|
||||
interval = 100,
|
||||
opacity = 0.15,
|
||||
className,
|
||||
...attrs
|
||||
}: AuthPulseProps) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
const frameIndex = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
const animate = () => {
|
||||
if (ref.current) {
|
||||
ref.current.innerHTML = data[frameIndex.current];
|
||||
frameIndex.current = (frameIndex.current + 1) % data.length;
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize first frame
|
||||
animate();
|
||||
|
||||
const cleanup = setIntervalOnVisible({
|
||||
element: wrapperRef.current,
|
||||
callback: animate,
|
||||
interval,
|
||||
});
|
||||
|
||||
return () => cleanup?.();
|
||||
}, [interval]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={wrapperRef}
|
||||
{...attrs}
|
||||
className={cn(
|
||||
"absolute inset-0 pointer-events-none select-none overflow-hidden",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div
|
||||
ref={ref}
|
||||
className="font-mono text-heat-100 absolute inset-0 flex items-center justify-center fc-decoration"
|
||||
style={{
|
||||
whiteSpace: "pre",
|
||||
fontSize: "9px",
|
||||
lineHeight: "11px",
|
||||
opacity,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
[
|
||||
" \n \n \n \n \n ░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░ \n \n \n \n \n ",
|
||||
" \n \n \n \n ░░░░░░ \n ░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░ \n ░░░░░░ \n \n \n \n ",
|
||||
" \n \n \n ░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░▒▒░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░▒▒░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░ \n \n \n \n ",
|
||||
" \n \n ░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░▒▒▒▒▒▒░░░░░░░░░░░░░░ \n ░░░░░░░░░░▒▒▒▒▒▒░░░░░░░░░░░░░░ \n ░░░░░░░░░░▒▒▒▒▒▒░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░ \n \n \n \n ",
|
||||
" \n ░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░▒▒▓▓▓▓▒▒░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░▒▒▓▓▓▓▒▒░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░ \n \n \n \n ",
|
||||
" \n ░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░▒▒▓▓▓▓▓▓▒▒░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░▒▒▓▓▓▓▓▓▒▒░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░ \n \n \n \n ",
|
||||
" \n ░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░▒▒▓▓▓▓▓▓▓▓▒▒░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░▒▒▓▓▓▓▓▓▓▓▒▒░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░ \n \n \n \n ",
|
||||
" \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░▒▒▓▓▓▓▓▓▓▓▓▓▓▓▒▒░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░▒▒▓▓▓▓▓▓▓▓▓▓▓▓▒▒░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n \n \n \n ",
|
||||
" \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n \n \n \n ",
|
||||
" \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n \n \n \n ",
|
||||
" \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n \n \n \n ",
|
||||
" \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n \n \n \n ",
|
||||
" \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n \n \n \n ",
|
||||
" \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n \n \n \n ",
|
||||
" \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ \n \n \n \n "
|
||||
]
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,58 @@
|
||||
"use client";
|
||||
|
||||
import { HTMLAttributes, useEffect, useRef } from "react";
|
||||
import { cn } from "@/utils/cn";
|
||||
import { setIntervalOnVisible } from "@/utils/set-timeout-on-visible";
|
||||
import data from "./core-flame.json";
|
||||
|
||||
export function CoreFlame(attrs: HTMLAttributes<HTMLDivElement>) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let index = 0;
|
||||
|
||||
const interval = setIntervalOnVisible({
|
||||
element: wrapperRef.current,
|
||||
callback: () => {
|
||||
index++;
|
||||
if (index >= data.length) index = 0;
|
||||
|
||||
const newStr = data[index];
|
||||
|
||||
ref.current!.innerHTML = newStr;
|
||||
},
|
||||
interval: 80,
|
||||
});
|
||||
|
||||
return () => interval?.();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="absolute inset-10 -z-[10] overflow-clip">
|
||||
<div
|
||||
ref={wrapperRef}
|
||||
{...attrs}
|
||||
className={cn(
|
||||
"cw-[1110px] ch-470 absolute pointer-events-none select-none",
|
||||
attrs.className,
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className="text-black-alpha-20 relative left-0 font-ascii"
|
||||
ref={ref}
|
||||
style={{
|
||||
whiteSpace: "pre",
|
||||
fontSize: 8,
|
||||
lineHeight: "10px",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Export default for backward compatibility
|
||||
export default CoreFlame;
|
||||
@@ -0,0 +1,27 @@
|
||||
[
|
||||
" \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n '_^_ ,^_' \n '.\"\"\"+=\"\"=+\"\"^-'' \n ''..:::,_::::::_,:::..'' \n ''.-_\"::___:^^\"___::\":-.''' \n '.'-_:::\"+^\"\"\"+^:::_-'.' \n \n \n \n \n \n \n \n \n \n \n ",
|
||||
" \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n '-''''-' \n ',::_-'-:\"-'_++++^^^^++++:'-\":-'._::,' \n '''''^+++^^\"^++^\"\"^++^^++++^^++^\"\"^++^\"\"^+++^.'''' \n '''-:^^^^+++===+++^\"\"\":^^^\":\"\"\"\":\"^^^:\"\"\"^++++==+++^^^^:-''' \n ''.-_\"^^++++===++^-'''''-:\",....,::,'''''-^++===++++^^\":-.'' \n '''.-_^^+^++=+++:_-..._^^^:\"\":^^^:...-_:+++=++^+^^_-.''' \n '''''.,,_+++^^:_...' '' '..._:^^+++:,,''''''' \n '-' '-' \n \n \n \n \n \n \n \n \n ",
|
||||
" \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n '''-''''-''' \n '.:+^\"\"''_+^_-:^+++^^^^+++^:-,^+_''\"\"^+:.' \n ' '' ''-.-:..^++=^\"\"\"\"^^\":::\"++^++++^^+^:::\"^^\"\"\"\"^=+++..:-.-'' '''' \n -:::\"\"-,^+++=++++==++^^\"::--_\"^\"^^\"\"\"\"^^\"^\"_--_:\"^^^+==++++=+++^--:\":::- \n '_:^^^^+^^++++++==++^:'''''''.:\"^,--,^\":.'''''''_^++==+++++++^+^^^\":_. \n ''-\"^\"_:_:^++++++++\".'---...:\"^\",,\"^\":-..---'.:+++++++++:_:::^^,'' \n '.'' '-\",.-.'-\"^^^,:^\"_:-' ' ' '-:_\"^:,^^^^-'.-.,\"-' ''.' \n '''.,- -_.''' \n \n \n \n \n \n \n \n \n ",
|
||||
" \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n ','' ''' '.'.' '.'.' ''' ''-' \n .''''''' '' ',_\"^\"\"^\"\":^^:\"^^^^^^^^^^^^^^^^^:^^:\"\"^\"\"^\"_,' '' ''''' '. \n ':++:++^^:'' ''-,\"_:::\"^++^^\"\"^\"\"_\"^+^^++^\"\"^++^^+^\"_:^^\"\"^^++^\":::_\",-'' '':^^++:^+:' \n '_^++===++^\":.,:+++^^\"\"^^^:::::\"::_,:\"\":::-__-::::\":,,::\":::::^^^\"\"^^+++:,.:\"^++====+^_' \n ',\"+++=++\"_:++++++^\"\"^^^\"-_,,_::-..-_,:\"::,,::\":,_-..-::_,,_,\"^^^\"\"^++++++:_\"++=+++\"_. \n '_-.\"^:-^:,,-.:\"^^^^__::::_,:___:\"::__''''__:\"\":___:,_::::__^^^^\":.-,,:^-:^\".-_' \n -.' .' '.:_::-'''-\"^\"--,_,\"\"^' '^^\"__,--:^\"-'''-::_:-'' '. ''- \n '-.'' ''' '' ''.-. \n \n \n \n \n \n \n \n \n ",
|
||||
" \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n ' '_.' ''.'' ''.'.' '.'-'' ''.'' '._. ' \n ':.'' '' '' '' ''._::\"^\":^^^\"^^\"^^+^^^^^::^^^^^+^^\"^^\"^^^:\"^\":::.'' '' '' '' ''.:' \n '-^++++^\"\".' ''_^^:_--_:^^^^^^\"\"^^^\"++++^^^\",,:^^^++++\"^^^\"\"\"^^^^^:_--_:^^:'' '.\"\"\"++++^-' \n ,^+++===+^\"_:--\"^^^\"\"\"\"\"\"\":::_::::::\"\"\":::,-,,-,:::\"\"\":_::::_:::\"\"\"\"\"\"^^^^^--:_\"^++==+++^: \n '^^^++++++\",\"++++^\"\":--::_,__:::\":,-,,__:::,''-:::__,,--:::::__,_::,-:\"\"^++++^,:++++++^\"^' \n ':,._^-:'''--,_::_,\"--_:::_::::\"^\"^^:-' '-:^^\"^\"\":::_:::_--\"__::_---''':-^_.,:' \n .' ''-:^\"_.----:^:,,:\"_._\". .\"_.,::,,:^\"----._:^:,'' '. \n ''''' '--,' ',--' ''''' \n \n \n \n \n \n \n \n \n ",
|
||||
" \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n .'' ''''''.''':-___.' '''' ' '''' '.___-_.''.'''''' ''. \n '\"\"^'' .\"^\"++\":_^\"^^^+++^^++++^\"\"++:_:_:- .:_:_:++\"\"^++++^^+++^^^\"^__\"++^^\"- ''\"\"\"' \n '^+++^\"_,-_\"::\"\"\"\":^^^^^\"^+++++++=++++^-',_.' ._,'-^++++=++++++++\"^^^^^:\"\"\"\"::\"_-,_:^++++' \n '^+=++^\"^^\"^:_:\"^^^^^^^^^\"^^++^++++^\"::_-''' '' '''-_::\"^++++^++^^\"^^^^^^^^^^:_:^\"^^\"^++=++' \n ':^+++++++^:,,\"^^:_:_:\"\"\"\"^^^^\"^^^^::\":,-' '-,:\"::^^^^^\"^^^\"\":\":_:_:^^\"_,:^+++++++^:' \n ' '' ''...-,::_::,,,_\"\"^++++^^++++^^\"\"\"' ':\"\"^+++++^^++++^\"\"_,_,::_::,-...'' '' ' \n .-\"-,:::_:::\":\"^^^^^^+\"' ':+^^^^^^\":\":::_:::,-\"-. \n '.--.,^\"_''' '-,,' ',,-' '''_\"^,.---' \n '' '' \n \n \n \n \n \n \n \n ",
|
||||
" \n \n \n \n \n \n \n \n \n \n \n \n \n \n ' ' \n '' :^,-..'-''-,...-.,__^:-' '.:\"_,_.-.-.,-''-'..-,\": '' \n '::^'' ..\"\"+++++^^+^++++++=+++++++^\":-'_-.- ---_'-:\"^+++++++=++++++++^^+++++\"\".- ''^::' \n \"++++_,,_^^^\"\"^\"^+++++++++++++++==+^^^:.'''' '.''.:^^^+====+++++++++++++^\"^\"\"^^^:,__^+++\" \n \"==++^\"^^^_,:\"^+++++++++^++^++++++^:::-'' '''..''' ''-:::^++++++^++^+++++++++^\":,:^^^\"^++==\" \n .:^++++++^\"-,:^+^:\"\"^^^^^+++^^+^^+^::,-,' ',-,_:^+^^+^^^++^^^^^\"\"\"^+^:,-\"^++++++^\". \n '' .::_:\"\"\"_:\":\"^^^++++=+++++===+:.''' '''':^===++++++++++^^^\":\":_\"\"\":__:- '' \n ''''''_::,:\"^^++^^++++++++^. '^++++++++^^++^^\":,::_'''''' \n -_,--\"^^_:-' ''''.,- -,.'''' '-:_^^\",-,_-' \n ''''' ''''' \n \n \n \n \n \n \n \n ",
|
||||
" \n \n \n \n \n \n \n \n \n \n \n \n \n \n '_:' ' ' ''' '' ' ''' ' ' ':_' \n .,' -_:::-.:+^__^^^::.'--,\"^:''' ''':^\"_-,'._:^^^__\"+:..:::_- ',. \n '..\"\"\":\"^\"\"^^++++^^^\":\"^++^+++++++^^-''' '''-^^+++++++^++^\":\"^^^++++^^\"\"^\":^\"^..' \n .^^+^^+^^++^^+++^^^^^^^\"^^^^^\"\"^+++^::..' '..::^+++^\"\"^^^^^\"^^^^^^^+++^^++^^+^^+^^. \n '++^^++^\"^+^^^^^^^^^^+^\"::^^\":::\"++^_,.''''''' '''''''.,_^++^::_\"^^:::^++^^^^^^^^^+^\"^++^^+=' \n '_^^^^^+++^\"\"^^::\"^+^\"\"^^+++^::::\"\"^^,'' ' ' ''-^^\"\"::::^+++^^\"\"^+^^::^^\"\"^+++^^^^^:' \n , .:^++^_,--_::^++++^+==+==+=^^^\"::. '::\"^^^++==+==+^++++^::_--,_^++^:. ,' \n -\"^^. ''--_,_\"^++++^^++:\"+^+^+^- -^+^+^+^:++^^++++^\"_,_--' .^^^, \n .'',,_^++\"-'' ' '''' '''' ' ''-\"++^_-_''.' \n '' ' ' ''' \n \n \n \n \n \n \n \n ",
|
||||
" \n \n \n \n \n \n \n \n \n \n \n \n \n '' '' \n ' ''-- ''.',-' '' '' '-,'.'' .-'' ' \n ''.,.'''_::'':+^\":\"^:::-''__:^::. ' ' .::^:__''-:::^\":\"^+:.':\"_'''',-'' \n '-.^++++=+^++^^^+^^^^^^\"^^^^^^^^++:,, ,,:+++^^^^^^^\"^^^^^^+^^^++^+=++++^--' \n '^^==++^+++++++^^^\"\"^^::::\":\"^++^\":\":-.'' ' ' '''-:\"::^++^^:\"::::^^\"\"^^^^++++++^++==+^' \n '+++++++^^+=+^\"^^+++^^:,:::\"\":\"^\"^^^\".''.'' '''''' ''.''.\"^^^\"^\":\"\":::,:^^+^+^^\"^+=++^+++++++' \n ^+++++++^^^^::\"^++^^\"^^++\"\":,,::\"\"^^_- ' ' ._^^\"\"::,,:\"\"++^^\"^^++^\"::^^^^+++++++^ \n :-\"^^++\"_,_::\"\"^+++++++++++^^^++\":_' '_:\"^+^^^+++++++^+++^\"\"::___\"+++^^-: \n -^+++: '-::_:\"^+++^^:^++,'-::::^_ _^:_::,'.++\":\"^+++^\":_::-' _+++^, \n ''' ' .,__\"\"_. '' '' -_\"\"__,. ' ''' \n \n \n \n \n \n \n \n \n ",
|
||||
" \n \n \n \n \n \n \n \n \n \n \n \n \n ' .' '. '' \n '-''' '''''-,\"^_. '' '' .,\"\",-''''' '''.' \n '-:^+^^\"-..::^:^^\"^,-.'.'.'''_,:,.' '.,_,_.''.'.'.--\"\"^^:^::..-\"^^+^:-' \n .,^++===+^^:_:^^^:_:::,,___\"^^\"^^\":--.. ..--:\"^^\"^^\"___,,:::_\"^^^:_:^\"+===++^,. \n :\"^^+++=+^^\"\"\"\"^\":_-,,___::^^^^^\":_\"^:-'''''''' '''' ''''''''-:^\"_:\"^^^^^::___,,-,:\"^\":\"\"^^+=+++^^\":' \n _\"^++^++++^^^^^^^++::_:___:_:\"::\":_\"::--.''''.'' '''' '.''''.--::\"_:\"::\"\"_:___:__:++^^^^^^^+^++^++^^_ \n -_^\"+++++^+^+^^^^^,-.'.-._::^::_.'''..'''' ' ' '''.-''''_::^::_.-.'.-,\"^^^^+^+^+++++\"^_, \n ':^:^+++^\"^^^^::^^:_-.-,:^^:_''.-.' .-.''_:^^:,-.-_:^^\":\"^^^^^+++^:^:' \n '_:::^:.-_,'-:_:::_--..'._:' ''' ''' ':_.'..--_:::,:-',_-._^:::_' \n -.' '' ' ' '' '.- \n \n \n \n \n \n \n \n \n ",
|
||||
" \n \n \n \n \n \n \n \n \n \n \n \n ' ' \n '-,.''..'''''-,' '--''''''.''.,-' \n ''''-:^^^::\"\":_::\"__.'' ' ' ' ' ''.__\"\":_::\"::^^^:-''.' \n '-\"^^+=++++^_:\":+^^^:_-..'''''''-__-' '.__-'''''''..-_:^^^+\"\":_\"^+++=+^^:,' \n ',:^^+==++++:\"^++++:_--,::::::^+++++:.' '.-.''' ' '.--'' '.:^++++^::::::,-._:+++^^\":^++++=+^^\"_' \n _^^+++++++++^\"^^\"^\"\"\"::\"^:\"^++++^++^\":,. ''--,:-_''''''''_-:,--'' ',_\"^^+^++++^\":^\"::\"\"\"^\"\"^\"^+++++++++^^_' \n ':+++++++++++^^^+^:^^^^^+^\"^+^\":\"\"+^:_\"_.'''..''\"-_,..-_-\".'..'''._\"_:^+\"\":\"^+^\"^++^^^^:^+^^^+++++++++++\"' \n ,^^+++++==+^+:::::::::^^:---_-..-.' '.'''''' '''''''''' ''''''.' '.-..-_---:^^:::_:::::^^+==+++++^^_ \n _\"\"++++^+^\"^__::_,_,_\":.-.-,..-.' ''' ''' ' '.-..,,.-._\"_,_,_::__^\"^+^++++^\": \n ':\"+^\"-_.'' ''''''-_''' ' ' '''_,.' '''' ''._,:^+^:' \n ''-,...' ''..,-.' \n \n \n \n \n \n \n \n \n ",
|
||||
" \n \n \n \n \n \n \n \n \n \n \n \n '-' ' ' '.' \n '-:^^_-.'''' .' '.' ''''.-_^^:,' \n ',_,'_^++++\"_:^:\"\"__' '__\"\":^:,\"++++^:',_,' \n ':\"\"^+++++++^\"\"^^:_::----..--_--'-__ -_-'.-_,--.----::_:^+^\"^+++++++^\"\":' \n _\"\"^^++==++^^+++^_-_:_::_:\"\"++++=+^\"-.'' :^^, -^\": ''.-\"^^=++++\"\":_::,::-_^++++^++==++^^\"\"_' \n _\"^^+++==+++++^^\"\"^^^\"\"\"\"\"^++==+\"\"^++\"^:-'^+:' '-' ''__'' '-' ':+^'.:^\"++^:\"^===+^\"\"^\":^^^^\"^^+++++==+++^^:_' \n '\"^++++++++++++++^^^+^\"^\"::++^+:--:+^\"\"_.,\":-.'.----''''----.'.-_\",._\"\"^+:--:+^++\":\"^\"\"++^^++++++++++++++^\"' \n -:^++++++^+^^+^^\"\"^^,_:,_\"++^:_-'''-.-,,:^\".''.''.' '.''.''.:^:,,-.-.''-_:^++\"_,:_,\"^\"\"^^+^^+^^+++++^\"- \n ::^+++\":\":\"\":^^\"^\":\"-'''--,,--' ''''' ' ' ''''' '--,,,-.''.\":\"^\"^^:\"\":\":\"+++^:: \n ':\"^^:_-' ''.-.' '.-.'' '-_:^^\"\"' \n '_:^,.-' '-.,^:_' \n \n \n \n \n \n \n \n \n ",
|
||||
" \n \n \n \n \n \n \n \n \n \n \n '' '' \n ''..::- -:\"..'' \n ''' '.,^:\"\":-'' ' ' ''-:\"\":^_.' ''' \n '-_::____:\"\"\"^\"_,---..''' ' ''' ''' ' ''...---,_:^\"\"\"\"____::_-' \n -::::\":\"\"\"^^\"^:_---:_..----.''.\"^^- .^^\".''.----..,:---_:^\"^^\"\"\":\"::::, \n -:\":\"\":\"^^+++^\",,_:_:^\"^^\"\"^\":^+\"^+^-'''-^. ' ' '^,'''-\"+^\"+^:\"^\"\"^^\"\":_:_-,:^+++^^\":\"\":\":- \n .._:^^^^^^+++^:,-\"^\"^+++^^^:^^^^+=+++-.\"_,..' ''' ''' '..,,\"--+++=+^^^^:^^^+++^\"^\"-,:^+++^^+^^^:_.' \n ':\"^\"^^^^^++^^_-:^^^+++++:\"\":\"\"+++++^:^^\"_'''''___''' ''',__''''',\"^^:^+++++\"\":\"\":+++++^^^:,_^^++^^^^^\"^\"\"' \n ,\"\"^^^^^^^++\"_,_-_:\"\"\"++++:_.._-:,,--,_,.'''''-:-'''' ''''-:-'''''.,_,--,,:,_.'_:++++^\"^:_-_,_\"++^^^^^^^\"\"_ \n '-_\"^^+\":::\"\"--,__-:-'''-,. ''''''',.' ''' '''' '''' ''' '.-''''''' .,-'''-:-__,--\"\":::\"+^^\"_-' \n ._:::_,' '''' ' ' '''' ',_:::_- \n .,::,,'' '',,_:,. \n ''.'' ''.'' \n \n \n \n \n \n \n \n ",
|
||||
" \n \n \n \n \n \n \n \n \n \n \n '-. .-' \n '.-,.\"_.' .,\".--.' \n '-,'''._^^:^:.' '' '' .:^\"^^:.'''--' \n ',\"::::\"_:^^^^\"\",,_-.' '.' '.' '.-__,\"\"^^^^:_\"::::\"_' \n ,\"\"\"::::^^^^^\"\":,_\":_:_-.-''..::+\"'' '':+::.-''-.._:_:\"_,:\"\"^^^^^::::\"\"\", \n -:::\"\"\"\"^^++++^:\":\"^^\"^\":^^^^+^+++++' ''..-. '-.''' '+++++^+^^^^\"\"^\"^^^:\":^++++^^\"\"\"\":::, \n ..-_\"^^^+++++^^^^^++^^+^^^^^++^+===+\"\"\"^\"_-'.'' ' ' ''.'-_\"^\"\"\"+===+^++^^^^^+^^+++^^^^+++++^^^\"_-.. \n ',\"^^^^^+++++^^^^^^^++==+^^^++=+++\":^^,--' '-:,,. .,,:-' '.--^^:\"+++=++^^^+=++++^^^^^^+++++^+^^^\"_' \n '\"^++^^^^^++^+\"\"\"\"^\":^^++\"_\":-,\"^^::::-''''''-_:-' '-:_-.'''''-_\"::^^\",-::_\"++^^:\"^\"\"\"\"^^++^^^^^++^^' \n -,\"++^^\"\"\",'-::\":\"_.' '' '.' ''-_,,.' ''''''' ''''''' '.,,_-'' '.' ' '._\":\"::-'-\"\"\"^^++\"_- \n '-\"^:::-' -.'' ''' ''' ''.-' '-:::\"\",' \n ',__::,' ',::__,' \n '.--' '--.' \n \n \n \n \n \n \n \n ",
|
||||
" \n \n \n \n \n \n \n \n \n \n \n ''-.''.' '.''.-'' \n '' '---,'.'' ''.'-,--' '' \n ''-__,,.',:^:-'.' '.'-:^:,..,,__-.' \n '-:\":::::_:\"\"\":_,+^:_ ' ' _:^+,,:\"\"\":_:::::\":-' \n ',:\"::__\"^^^^^\"::++^,-''''''-_.''.' ' '-''._-'''' '--^++::\"^^^^^\"__::\":,' \n ',_::\"\"\"\"\"^+^+++++++++++^\"\":^^^:::__'.'.' ' ' .'''__:::^^^:\"\"^+++++++++++^+^\"\"\"\"\"\":_,' \n .-_:\"\"^^^+++++++^^^++++^^\"\"+++:--.'..--.'' ''.--..'..-_+++\"\"\"^++++^^^+++++++^^^\"\":_-. \n '-_:^^^+++++^\":\",-.:^_\"\"+++=+==^^^..:^_.''--_'',_'.' '''__''_--''._\":..^^^==+=+++^\"_^:.--\"_\"^+++++^^^:_-' \n ':^^+++++^\"\":-. ' '-'..\"^++\"^--_\":'-\"\"\":,^,''''' ''''',\",:^\"\"-':\"_--^\"^+^\"..'-' ' '-:\"\"^+++++^^:' \n .\"^^^^^:. '''''' '\"^_+: ''.'-,-:\":.'' ' ' ''._\":-,-'.'' _^_^\"' '''''' .:^^^^^\". \n -,\":\"__,-.' '-''. ',:_'''''' ' ' ''''''_\",' '''-' ''-,__\":\"_- \n '-_\"\",__,.' '' '' '.,__-\":_-' \n '...'' ''...' \n \n \n \n \n \n \n \n ",
|
||||
" \n \n \n \n \n \n \n \n \n \n ' \n '''.''''' '''''.''' \n '''' '.-..''' '''..--' '''' \n '.,,,,--.'_^\"::_: ____\"^_'.--,_,,-' \n '.:\"::\":_::\"\"\":^^,''' ' ' ' ' ''',^^:\"\"\"::_:\"::\":-' \n ',_:\"\"\"\"\"^++^++++=+^+:'-^''-_,' ' ' ',_-''\"-'_+^+==+++^++^\"\"\"\"\"::,' \n'._::\"^^^^++++++++=+^++^\"^^^\":,..'''-:_\"' '\"_:-'''..-:\"^^^\"^^+++=++++++++^^^^^::_-'\n ',_::\"^^^^^++^^^^^+^^++^:++::\"\"_-.:^^^, .-,' ',-. -^^^:--_\"\"::++:^++^\"++^^^^++^+^^^\"::_,. \n ',:\"^^+++^^\":_''''''.._+^\"^^++++=++^^::,-,:\":- ' ' -:^:,,,::^^++=++++^^\"^+_..''''''_:\"^^+++^^\":,' \n ':^++++^\"::,. '''''',\"^:+^_:::_^^^^\".'' ''.:^^^^_:::,^+\"^^,.''''' .,:::^++++^:' \n '\"^+++:-'' .::''' '_:^,^^:,-' '-,:^^,^:_' '''::. ''.:+++^\"' \n .::^^:,--.'' '\"^..-_,.'' ''.,_-.'^^' '.--,:^^\":.' \n '-_::::--.' ' ' ' ' '.--_:\":_-' \n ' '.-.'' ''.-.' '' \n \n \n \n \n \n \n \n ",
|
||||
" \n \n \n \n \n \n \n \n \n \n \n '' '' \n '--.''' '' '' ''.--' \n '.\"^^\"--' '_-'' '.-_' '--:+^^.' \n^\"++^\"^^:'' ''',\"^\"' ' ' '\"^\":'.' '':^^\"^++\"\"\n++=+=+^^+:.''.':=+:'' ' ' '':^=\"'-''.:+^^+=+=++\n++++++\"\"^:' '' ' ' '''' '.-'' ''-.' '''' ' ' '' ':+\"\"^+++++\n^+++++^::' ''.:' '' '' ':.'' '::^+++++^\n\"^^++^^-'' '' - '' '' - '' ''-^^++^^\"\n_++^^^^\",''' '' ' '''-\"^^^^++_\n'^+^^:\"++^-..-.' '.-'.-^++\":^^+^'\n '-^+++^^\".---' '-,-.:^^+++^-' \n '-_,,-''.'' ''.''-,,:-' \n \n \n \n \n \n \n \n \n ",
|
||||
" \n \n \n \n \n \n \n \n \n \n \n ''' ''' \n ._'.' ' ' '.'_. \n..\"^^_:.''''''--.' '.--''''''-:_^^\"-.\n+^+\"\"::-''' '+^''''' ''''':+' '''-::\":+^+\n==++^::^+- '''' ' ' '''' -^^:_^+++=\n=++++^\"^\"' ' ' ' ' ':^\"^++++=\n+=++++^-- '' '' .-\"++++=+\n^+++++^-' '' '' '-^++^++^\n_^^+^^^+-' ''' ''' '.+^^^+^+_\n.^\":\"^\"++:-'..'' ''..'.:++^^\":\"^.\n '.-\"^+++^\".'.'' ''.'.:^^++^\"-.' \n '.-,-.' ' ' '-,-.' \n \n \n \n \n \n \n \n \n ",
|
||||
" \n \n \n \n \n \n \n \n \n \n \n ''' '' '' ''' \n ''-' '-'' \n:_-'.' ' ' '.'-_:\n:''':+_'''' ' ' '''',+:''':\n\".'',^+_.'' '._+^,'''\"\n^^,\"+^_'' ''_^+\",^^\n+=+^+^' '^+^+=+\n\"++==^_.'''.'' '.'''.,^==++\"\n:^++=+++_,' ' ' ',,++^==++:\n :^_^^++\":-' ' ' '-_:++^^:^\" \n '''':--\":-' '.:\"--:'''' \n '''' ''''' \n \n \n \n \n \n \n \n \n ",
|
||||
" \n \n \n \n \n \n \n \n \n \n \n . .' \n '' ' ' '' \n'.''' '''.'\n' -_-:. ':-,-''\n' '..'.-.' '.-.'..' '\n:::\"^_' ',^\":_:\n+++^:- '' .' .:^+++\n^^++=^-'''' ''''.\"=++^^\n:+++=+++^.' '.^+++==++:\n ,::+^:^^:.' '.:\"^:\"+\":: \n '''.''--' '-,''.''' \n \n \n \n \n \n \n \n \n \n ",
|
||||
" \n \n \n \n \n \n \n \n \n \n \n \n \n ' ' \n -' '-' \n^\"'' '' '' '':^\n\"\"-- ' ' --\"\"\n++\":' '_\"++\n\"++=+^_.-' '-._^+=++\"\n-\"^+==+^\":_ ,:\"^+==+^^-\n \"^:-_.--' '--._-:^\"' \n '.' '' ''' ''' \n \n \n \n \n \n \n \n \n \n ",
|
||||
" \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n:.' ' ' '':\n::_' '_\":\n+^:.'. .'':^+\n_++++^_--' '--,^++++_\n.::+=+^^\"-.. ..-\"^^+=+\":-\n .+_'.'' ''.'_+. \n '' '. \n \n \n \n \n \n \n \n \n \n ",
|
||||
" \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n.' '.\n=: _=\n:-,.',. '_'',-:\n':,,:--'''''' '''.'',-:__:'\n '.^^^++_.''' '''._++^^^.. \n -'' ''-' \n ' ' \n \n \n \n \n \n \n \n \n \n ",
|
||||
" \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n.' .\n=. ' ' ' '=\n:::'',' ','':::\n'.._:.:,'--',' ','.-',:.::.-'\n '':_++:.''' '''.:+=__'' \n '' '' \n \n \n \n \n \n \n \n \n \n \n ",
|
||||
" \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n "
|
||||
]
|
||||
@@ -0,0 +1,41 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { cn } from "@/utils/cn";
|
||||
import { CoreFlame } from "./core-flame";
|
||||
|
||||
interface FlameBackgroundProps {
|
||||
intensity?: number; // 0-100, like CPU usage
|
||||
animate?: boolean;
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function FlameBackground({
|
||||
intensity = 0,
|
||||
animate = false,
|
||||
className,
|
||||
children,
|
||||
}: FlameBackgroundProps) {
|
||||
// Convert 0-100 to 0-0.3 opacity
|
||||
const opacity = Math.min((intensity / 100) * 0.3, 0.3);
|
||||
|
||||
// Speed increases with intensity
|
||||
const speed = Math.max(80 - (intensity / 100) * 40, 40);
|
||||
|
||||
// Color gets more orange with intensity
|
||||
const color =
|
||||
intensity > 80 ? "heat-100" : intensity > 50 ? "heat-40" : "black-alpha-20";
|
||||
|
||||
return (
|
||||
<div className={cn("relative", className)}>
|
||||
<CoreFlame
|
||||
className={cn(
|
||||
"transition-opacity duration-1000",
|
||||
animate && "animate-pulse",
|
||||
)}
|
||||
/>
|
||||
{children && <div className="relative z-10">{children}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
[
|
||||
" \n \n \n \n \n . . \n .. ..+ \n .:. \n .. .. .:: \n +.. ..: :. \n .:..::. .. .. \n .--:::. .. ... .:. .. \n .. .:+=-::.:. . ...-.::. .. \n ::.... .:--+::..: ......:+....:. :.. .. \n ....... ::-=:::: ..:-:-...: .--..:: ......... \n .. . . . ..::-:-.. .-+-:::.. ...::::. .: ...::.:.. \n . -... ....: . . .--=+-::. :-=-:.... . .:..:: .:---:::::-::.... \n ..::........::=..... ...:-.. .:-=--+=-:. ..--:..=::.... . .:.. ..:---::::---=:::..:... \n ..........::::.:::::::-::.-.. ...::--==:. ..-::-+==-:... .-::....... ..--:. ..:=+==.---=-+-:::::::-.. \n . .....::......:: ::::-::.---=+-:..::-+==++X=-:. ..:-::-=-== ---.. .:.--::.. .:-==::=--X==-----====--::+:::+... \n ..-....-:..::-::=-=-:-::--===++=-==-----== X+=-:.::-==----+==+XX+=-::.:+--==--::. .:-+X=----+X=-=------===--::-:...:. .... \n ....::::...:-:-==+++=++==+++XX++==++--+-+==++++=-===+=---:-==+X:XXX+=-:-=-==++=-:. .:-=+=- -=X+X+===+---==--==--:..::...+....+ \n ..:::---.::.---=+==XXXXXXXX+XX++==++===--+===:+X+====+=--::--=+XXXXXXX+==++==+XX+=: ::::--=+++X++X+XXXX+=----==++.+=--::+::::+. ::.=... \n .:::-==-------=X+++XXXXXXXXXXX++==++.==-==-:-==+X++==+=-=--=++++X++:X:X+++X+-+X X+=---=-==+=+++XXXXX+XX=+=--=X++XXX==---::-+-::::.:..-..\n",
|
||||
" \n \n \n \n .. \n . .+. \n \n .: \n : .. :. \n .. ... .. .. \n :...+. . .. :. . \n .=-::... . . ... .. \n .. .--=-::... .....=+.:. . . \n -:.... .:-=:...: .::...... .:. .. . ... \n .. .. . .:: :.:: .:-::.. . .-..:.: ........... \n ..= . .. .::-==.. .-=-::... ..:.. . ..:::.::.:... \n .+.:.:. ..-.:: . . ..:. .:=-==-::. .:--.. .. .. ... .:---:::::--::..... \n .. ..+::.......-::..: . ::--.. ..:::-==-:. ..::.. .:... ..-.. =..:=== ::::-+-=:...+.=.. \n .....= ....:::..:::::- ::=:. ..:=--==+:. .:-::-=-=--:... .:-..... .:-=:...-+==--:+:-+-=-:-:.:+... \n .....:...::.:: ::::-::----=+-::--:---+=XX=-:. ..-=-=::--==X==-:....-.:--::.. .::++-::--+=---:-:---=-=::...-.. \n ....:..:....:=:- ==--=-:--===++====---- -==+==-::--==-:-::--=XX ++=-:::---+===-:. .:-X+ ----=X==----: :=--.--::........... \n .+.:::::-..:-:-===++=++=++++X++=====-.--=X==++==----=--::+:-=+XXXXX++---=-==+++=:. ..:-+++---=+XX+++=-::-===-+=--:...:.......... \n ...::--- ::::--=+=+XXXXXXXX+++=====+===--=---==++==-=+=--::-==++XX+XXX++=+X==+XX+=-::-::--==++X++XXXXXXX=-::-+=++X+=-:::::::::-:=+..... \n ..: :-===----=-=+++++XX+XX-XX++X+==++==+=--:--==.XX+==++.===+++XX.++++XX++=X+=++XX+==--=--===+ +XXXXXXXXX+=--=X++.XX==--=-:---:::::::..:.\n",
|
||||
" \n \n . \n . \n \n . : \n . .. \n . .. . \n . \n :.. . . \n .::... . \n ==-:: : . \n . . .-.=::. .:: ..+ . .=. \n . :.. ..+:..:-. .:.. .: .+. . .. \n . . .:.:-:. .-=-... ..-. ...:.. .. .. \n . . .. .. :. ..::--:.. .::-. . .: .:::-:....-...:. \n .. ......:: ::. .::+:::: .:=::==:.. ...... ... . . .::----:...::::::.... \n . .:.....:::::=:-. ..::--=-.. :-::--==.::.. . .: .. .::-:..:==--::..:::::.:.....: . \n +.. ...:-+...:.: :=---::..:::::-=+=:.. .::.:::--.++--:.. ..-:..+. .:=-::::==-:::::::-=-:.::... . \n -... .. .. .:---:+:-:-::-=+==--=-::.::-X=--::..::::.:.:--=XX+==--::::.:-+=-:. ..-=-::-::=++-::-::.:---::.. . \n ......... .:::-=+== -==+= =====--=-:::::-+=+=--:.:::..::.:-===XXX+=--::--+==+=-. ..-==-:.:-=XX=----:.:-=-=--:.. .+..... \n ...:::+:...:=:-+++XX++++.=-=-==-- ===-:.:=---=+=-:::--::..:-==++X+X+==--=+==+X+-:.......:-==++==++X:X++=-:::-==+=--:.......::::..+. \n ...:---::+:::--==++-XX++XX+=:=++=--====-:::+--==+=---== ---==++XXX++XX+.+XX===+X+=-:::-:--=====XX+-++XXX+-:--++=+X+--::::.:::::::..... \n ..::.-==---+- ++++.+++XXX-XX++:X+=-=X+==:-::.=+X+XXX+=+X++++XXXXXXXX=XX=- +++-==++X+==-==-=:=++XXXXXXXXXX++==+ ++X X+=---.--.-:.:::.:... \n",
|
||||
" \n \n . \n : \n . \n . \n . \n \n \n :. . \n :-....:. .- \n ....... .. \n . .. .. :. \n .. ..:.:. .-.. . ...:... \n . .. ..... .:--:.. .::... . . .. ........ .:. . \n .. ..... ...: ..=..::-. .:.:=-:. . ... .. .:.:..:-...-:......... \n . .:: ..::.:.:.. . .:.::-.. ..=.:--==:.. . .::..+==::::+........ . \n .:.......:.:::.....:::..::==:.. :.....::-+X-:::. .-.:... ..-:..::=::-:-...:.:::... \n .. :::.:.-.:::-=---::=::...:--==-:. ..= ....:-++=--=-::.+...==-::. .--:....:=-:-:::.... ::.. \n +.... .::-==+-----------------:::..-==--::. ... ..::-=+:+++=-:::.-- =+=-:. ..=--:::::=++-=::-::.:=-::... .. \n ..:-.... :..:-+++=+=-===-------::--.::..:------:... :....::-=++XX+=--::----+X-:. ..-==-----+X+==-=-:::-=+=-::.. .....:...- \n ...:::::....:-- ==+X+====-----=+::-==-:::::----=-:.::-::::--=+XXXXXX+=-=+=-==++--:-:::.::=--=-==X++++X+=-:=:===++--::......:...... \n ...:----:::--.====+=++X+++======---=+=--:-::=+++X++=-====== ++XXXX.XXX +++-=--=+X+--::--:---==+XX+++XXXX+=--=+=++X+=-:::-::-:::....... \n .. ...:::--==:---++=+=====++ XXXXX++===+X+===+==-+X++++++++X+X++.XXXXXXXXXXXX ++======X+==-=--===+XXXXXXXXXXXX+=+++++X+XX=.--==--::-....:....\n",
|
||||
" . \n . \n \n \n \n \n \n \n . ..: \n :. . .. \n ::.. \n . .. \n .: :::. :. .. \n .:..:. .:-. ::. . .: .. ..... \n ... . ... . . ... ..::-:. . . ... .:........:. \n .. =.... . ... .. .:.:-+-.. ....-=:....:.=... .. \n ... -..=::... . ......:::-: .::=+-:::+. ::-.. .:..::=:...:.......... \n ....:. ...:....:::=:.:-.. .::=--: ..:==:-::::. . .:---:. .:=:::. :::-.-...:...=. \n ..:.--::::::::::::::--:-:....--:::... ..:-==++---:.=.-::-=+-:. .:::.....--=-:.-.::.--:... \n ....... ..-=---==-=-==-::.-::+:--::..-:---:::....... ..---=+XXX+=-+:::--+X-:.. :--:.::::-++-::::::::==-:..: +.... \n ..:...:....:-=-=+:+=--==::--::-=::--:...:--.:-::....::::---+=+XXX.+=-:-----+=-::..... .::--:---=+++==--:..:-==+-:::.. ......+-. \n ..:-::::..::---=====+=== -:--:--::-==-::-::-==-==----=--::-==+:XXXXXX+==+=--==+=::..:::.::---++.+=+++X+=-:=:===+=--::...:..::...:. \n ....:-------==-=+=====XX+X+==------+=---=:==+=.==+=+++++X+==+XXXXXXXXXXX+++==--==+=-:-:-:---=X XX++XXXXX++===+==+++=------::: :....... \n .....-.:+:--==+======= ++=++X:XXXX+=:==XX==:++=-=++==+X+=+++:+XXXXX:-X.XXXXX+X++==-++++X+======++XXXXX-XXXX--XXX+==+++++X++==.---::::..=:. ..\n",
|
||||
" \n \n \n \n \n \n \n . \n . . \n :. \n .. . .. \n . .. ::. . . \n . . .:+.. : .. . \n . . . ..-- .. .. ..: \n . .. .. . ...-:... . ...:-.. ... \n . ....-.-. ..:.. .. :: ...-::...... .. .. .:.:::=....:. .. \n -... .....:...:.:-. ...=.:: .:=-::::::. .::--. ....:.:. ..:.. ..: .. \n .::::::.:..+:....:..-=:-:. ..:..::. ..=------::...:..:=-.. ........::-:.. ..::.:. \n . . :::----:::::=::....+:=-:-..-::-::-... . .:-==++++++-:.-::-+-... .::.... .:==::.....:--+:.. . . \n .... .. .:-=-=+==---:-:=:::..::::-:..:-:::::::....=.:..::--=+XXX-=-:-:-:=--::.. :.::..:::-++--::...::==-:.:.. ....+. \n ...-.:.. ...:-==--=+--:-::::..::.:--:..:=:-+-:---::--:::+:-==++X:XX+-==--:--==-::.... ..::::-++==++:-::..:-==-:::.. .....+=. . \n -..:::::::--::--==--++=+==--::::::-=-:: =-=-- --=+=+++=++-==++XXXXX- X+==+-----==::..:-.::--=+.++=++XX++=-:-======:--::.:.-...:... \n ......:=:--=----=- -=-===XX+XX+=----.-+=-:-==-+==-.-=+=====-+ X+XXXXXXX:XXX+========++=------+++XXX+.+X=XXX.XX+======+++---:::-.:.=..:.... \n .......::::-=:===++ ==++=++XXXXXX:++++++=====++=+X++:=++=====++XXXXXXXXXXXXX+X++-===++XXX++=+==+X++XX++X-XXXX-XXX+==+++=+X:===-+-:::=..::....\n",
|
||||
" \n \n \n \n \n \n \n \n \n . .. \n ...... \n ..::. \n : . . . \n ... : ....+: . . .. \n . .:. .... . .:. .... . ...:. ..-:.. .. \n . . ..- ...=.: ..:. .:-::::..=.. .:-. ...... .:. :.. \n ....::. .. -.......=: :::. ...-.. .- --+::::---.. ..:.. . .. +..::. ...:. \n . .:::::-:....::-........---:-...::..:. ....:--=--===+=:....:-.... .. .. .:-:.. ..::. . \n . .:::--:--::=:::::. ..::-: ..::-....: ..:......:--===+=X=-::.---::-.. ..:....::.==-::.. :---... . .-. \n .. ...::-:-:--::-::-::. ....::-...----:::::.:=:::-:::-====+XX+=--:::=--::. .::::-=== =+=::....:=-:..-. .... \n .+...... :::::-----=.--:-::...=..:=:...:---::.:--==+--=+:--+:+XXXXXXX=-==:+:---:::...:...-:--++===+XX=-::::=----:::...:...... . \n .....:=:- :::-:::-+--++===+=-::-.::-=:::-=+=--:.::-==+-:=++ +XXXXXXXXX++==+=-=+--=--::::::---+++==++XX-X+=+ ==---===--::.+....=.... \n .=....:.::.==--:-==--====XX XXX++=++=== ==--==+X+==---------==+XXXXXXXXXXXX++==== =+==++-=.=-=+++:=++=++XXXXXXXXX+=====++---:-::..:...:.... \n ......:::--=++= ====-===++XX-X XXXXX:X+====-==+XX:+++========+XXXXXXXXXXXXXXX++=++XX +++++=+++X:X+XX.++XXXXX .XX+++=:+++X+=.---::::...:=:...\n",
|
||||
" \n \n \n \n \n \n \n \n .: . \n . .... \n \n :. ... \n . . ..:. . \n . ..:.. .. .:: .. . \n . .. .:. ..: .:...... .:.. .. :.. .. \n ..-::: .:... .:=:. .. :. ..::=.:: :::=:. . .=. .. . \n ......:.. ..:-.. .. .:-:.. :.... . . .::=-------+-:......:. .. .+.:. .. \n ...::......-:.::. ..:-:....:::.. .: ....:--=--=-=+-:.::::.... .. .::-=-::. :.... \n ..:::.:.::..:..::.. ....--=....=::=:.:...-..:-:.::==--=+++--::---:.... ...::-:--=+:.. .:.:.. . \n .....:: ..:-::--:+:.:-.. .:=..:-..=.--:....::-+-::=-:-.-=++==+XX+=-=-::-::... ... :.::-=-=++X=-:..:::-::.:....... \n ....::.........---====--=-::.:=:.:--=::-=.::....::--::==== XX +-++XX++==.-=-:. :::-..:...:-- =-.==+X++------------:..... .. .. \n .....+.-:::::::-:::+--=X++ ++=====+==---:--=X+=-::.::::--:--=+XXXXXX++X++-====--==:--==--=:-=-=+==+-==+X.XX++++:=----=-::..:.. ....=.. \n . .. ...::+-==------:--==XXXXXXXXXXXX+=------+++X+===--=-----==+XX-XXXXXX:-++==+=++=--==++=++XX++==++==++XXXX:X++++=-===+=--:+::.....=-.... \n ......::--==.+++=+===.=++XXXXXXXXXXXX=++=.=--=+++X+++==+=====+X-X-XXXXXXX X+++++++++++===:==:++XX+XXX++XXXXXXXX.+++++XX+XX+=-=-:::+:::.:.:..\n",
|
||||
" \n \n \n \n \n \n \n . .. \n . \n .. \n . \n .. .:. ... \n ... ...... .. . \n . .. :. . ..:. .:: :. \n +... . ..:. . . .. .:.:..:=:. . . . \n .. ..: .:: ... . ..::-::::.:.:=-. ... .. ...::... \n .:.... ..+.: . ::. .::.. . ..::.::::==::--:..:.. . . .::-:-:. . \n . ........... .-.. .. .:. .-:...... .. ....::-=-::--=-::.:::. .. ....:+:-==.. .. .. \n :...... .:.-:.+....-....:. ..:: .::-.. ..::-:..=::::--+=--==+==-=-:.:...=. ...::---+=::... :::......... \n .. ...- ......::==----:-::.::=::+:-:.::-:.... ...-:::---=+==+:====+++++=:: . ...:: .-. .=.::--=-+=--::::::=-.:::. \n .......:......:..:--X+==:=+=:--=-=-:::--==+=--:....::-:.::-=XXXX++++X+===-:-::-:::.-:::-:=::-=------==++==-=+==-::+::.. .. .. \n +:......:-- ::-::::--+XXXXX++===--=-=-::-+++++==--::::::::-=+XXXXXXXX++==-==-----::--=++++++===--=---==+X++++====----=-:..... ......... \n .....:::::-=+==-+=+--==+X-XXXXX+=X+==--=---==+XX+==+--------=++X+XXXXX+XX++=-==++==------=++++X+=+++==++XXXX=X++==+===:+== :--:............\n .......:--.-====++.X+=++XXXXXXXXXXXXX+==---.===X+.+X+X++=.++++=XXX.XXXX+XX=X++++ +XX+X+==--====+XX+XXX XXXX XXX X+=+X++++++===-=-: :::=::....\n",
|
||||
" \n \n \n \n \n \n . \n \n \n . \n .. \n .. ..... .... \n . .. ..: .::. . \n . .: . .... .:-. .. \n . ::. .=.:..::.. .-=. .. .. -.. \n ..: .: :..= .: :-::+:::.:-::-... .. ..:-:-:. \n . . ..:: .. .: .-:.. . ..+..::--:::--:::=:.. . .. ..=::=-. . \n .......::... ..:.=. .. .: .:.......... .-...::-+-:--=--:-:::. .. .=..: :=-.. ..:.. \n . .:.:=::=:..::....::..::: ...:.. ..+::.::.:-:--==:--== ++==:.:...... :... ...:=:-=-::.:.:::-:.-... \n .... .. ...:=+--=--=--:::---:::::::--.... ..:::::::-+X+===--==+===:-:: :...:-:::... ..::::-:-===---:::-- ::::. . \n .......:....::..::-XX+:+++=---------::=++===--:..-::.:.::-=XXXX+.++++=----.::::::+--===---:---:::::==+=======--::+:::.. .. .. \n =.....+::--:::=::+:-=+XXXXXX+=-=::-:-=:--==+X+==--::.:=:::-=+X=XXXX+X+==--=--=- -.::--==XX+==+===---+==+++:-=====--.-=-::.:.. ...-....= \n ....=.::+::---===X==-===XXXXXXXX+=X+---------=++++==++---==+=++XXXXXXXXXXX++====++==-------===+X+==+++++XXXXXX+=+=++====+=-=-=-::..-.::.... \n .... .:.---=:====+X++.+XXXXXXX+.+++++=-- -==++XX++X+XX+++XXXXXXXXXXXX+=+XX.++++=XXXXX+==--=-==++X.XXXXX XX= XXXX++.X+++ += =--=-::::::::.-..\n",
|
||||
" \n \n \n \n \n \n . \n . \n \n . . \n . . . ..: \n . .:. :: \n .: .+. :: \n .. ... .. . .. ...= .:.:: .. .:. \n .: .. .:.. ....:..::..-.:..:.. ..:::+ \n ..: .. .:. ... .. :-..::..::. . . ...:. . \n .::. :. . : . .. . ...+::--:.:----+.... . . ...:: .. \n ..-:..... .-.....-..::... .::. .:..::..::----.:.:--+-=+:.::. .. .: ...-::... ....:+.. \n . .. ..:=-:--: :-:....:----....:::.. :.:.....:-=++=-:::----:=:.. :...::..-.. . ...::- -+::::::=:.....+ \n ...........:=X=++++==-:..::::=:-+=--:-::.....+..=.:-X.++X+=-===-:--:....:.:: :-=-:-:.-::..::---------.::.:..... .. . \n ......-:::=-:.::-=+XXXXX++--::..::-:==-==+=--:::...:..:-++XXXX++++=--::-:::::.:..::-++=---+---::::--==.==-----::::+:..::. ..... \n ......-::::--==-:-=-=+XX=XX+=--=-:::::::-=:=++=--:=:.:-====+XX:X-XXXX+==----+=.--: ::::====++==-----+=+++++++==------.:----:. . ...... \n ...:..:::::-----==+===++XX XXXX+=+==--:::---=++X+X++==--=++XX X++XXXXXXXXX+++=+XXX++=--:::--==-= ++++ XXXXXXXX:+++X+ ===----=--.:::..::..: \n .....:::--===--==+XX++XXXXXXXXX+++===------==+X+++XXXX:++XXXXXX+++++++=+XXXXX+++XXX-+X+= ----=.=+XXXXXXXXXXXXXX++XXXXX+++=.---==-:--:::-... \n",
|
||||
" \n \n \n \n . \n . \n \n . . \n . \n : \n . . . \n .. . .: : \n .. . . . .. ... . . \n .. . . ....+ .. ... . . \n .. . . ... . ..::. :...:-. . . \n ... .. .. .:. :.. . .::.. ::.:::.::-. . \n ......... ..: ::..:-. .:. ... .. :-::.....::-.-:.... .: .... .. \n .:--::::..::. .::-:.=....:. ........:.:-=-:...:::::: .:.-....:. . ..... ....+.=...: \n .:.. ..:++=-=X=--:.......::=:---:::+.. ..=.:-==:-==++=:::::::. ....::::-::::.... ..:::=.::-::.... .. \n ...::-: ..:-+X+XX +==--.. ..:-==-----:-:.: .:---++++++++++=-::..:.. .......:====--:::::...::-:--:-::.::+..::..... +.. \n ..+....::--:::.:-=+XXXX++=--:.....::-=---=+--:.:....:-==+++X++=++=-::::::--:::+....-==---=--=:.:::--===+==-:-:::::::::::.. ..... \n :.. ......::+::-=--- ==+XX:XX+=----:-:.:::-==+=++=-:::+:-==+X:X+++X++X++==-==+X++==-:...:-=---=========++X-XXX+=+==----:+:::::......==.. \n ...+.::::---.::-=+XX=+X=:XXXXX+===--=--:=:-- ====X+===-=++=+XX+X++++XXXXX+X.+++++X++==-::=:-+-==++=+++XXXXXXXXX. :XX++++=-:- -==:::::::... \n .....::--=====--=+XXXXXXXXXXXX-+++++X+=---=++++-+XX++++XX++XXXX+++++++=+XX=XXX+XXXXXX++=------==+XXXXXXXXXXXXXXX+XXXXXXX+=---==----::=::... \n",
|
||||
" \n \n \n \n \n \n \n . \n . \n . \n . . \n . .. \n . . .. .. .. \n : .. ....... . ..:. \n . .-. .. .:. ....:.::. \n . . .. .. .. .. +..:.. ..-::...- . ..= \n .::....:+..: .:. ....... ...........:::::...:.:.. ..- . . ..:... \n . . .-=-:-===--:. .:+::--=.... .:-==---:------::..... .....-.+.:::. . .......... . \n ...... .=.-++==+X++=-.. .:.--:::::.:.. ..-X==-===+==-- ::.... ...-------...... :..:.::::-.+... ..... \n .+..::....:-=++++X++---.. ...--.:-:--:-: .. ..-== +X++====--:..::::........:--:::::::-:.. :::--==++-.::.......... . \n .............::-::: ==+=+XX++--::::....::--==:=--:.=...:-==++:++++====-::-=X+=-=-.-:..+--+:.:-::----=--==++XXX ==-::::::...::.. ...... \n ....::::::-=::=-===-=+:XXXXX+=-=-:::: :..::-=+=+==-.::========++.+XXX:+X++==+==X+==--....:-:---=-=--=+:++X=XXXXXX.+====+::::-::..:+:.... \n .+..::::-==----:+X++++XXXXXX+== ==++=-::--=++X++X+==++X++-++==+++++XXXXXXXX++++X.X++=--::.:::-=++++++=X=XXXXX+XXX+X++XX+--::=--::::::... \n ....:: --=====---=+++XX.X=XXX++=+++X+=====+XX XX+XXXXXXXXXXX++= ++++X+=+XXXXXXXXX+X-XX..=-- =-==XXXXXXXXXXXXXXX+.XXXX-XXX=--------:::::.-. \n",
|
||||
" \n \n \n \n \n \n \n \n \n \n . . \n . . .. .. \n .: . . .. \n . . .:.. .. .. \n .. . .. .+... . \n .. .::..: .. ... ..............:.+. . .. . \n .:..::::---=. ..:---:. . .=--=-:.-::..=:-:. . .. ..... . . .-. \n . .:==-----+=-:. :--:.=.-..... . .-===::---:-:::::. ..::-:--:. . .:.=.::--.. . \n ..... ..:-=== ===.-:: .::::..:: .:.... ..:==-=+=+=-=-=::.+.:.... .:-:::.. ...+... .:.:::-=++::.. .=.. \n . .. ..-...:..::+====.++=-:+::.... .::::---=::.. ..:::-=+==-+X+===-:.:-+=--::=:: ..::.:. .:..:+::=:::--=+XX+---:...::.... .. \n ..:.+....:--.::-:---=+X++++==-:::::......::==++--:..:==--=-=--==+X==++=---+-=+==--::. .:::: :-::::--+=--=+XXXXX++=---:=-:.:....=.... \n .. ..::::-=-::=++==+XX.XX+X+----==.--::.:-=+X+X+===-=X+===----== XXXX++++==++++++==-:....:::.==-=--==XXX=XXXX-X:++==-++=:::--:::::....= \n ...:.-::-====-.-=++:+XXXXXX+==-==++=----- =XXXXXXXXXXXXX++=---:==+X.XX.+XXXXXX+X+XX++++-:.::--=X++:X =XXXXX++.XX++X++X+++--::--:-::::.:. \n .-..::.-=====-=-=+++XXXXXXXX+:+++XX+=+===+XXXXXXXXXXXXXXX+======++XXX++XX XX.XXXXXX=++X=--====+XXXXXX+XX+X X++++XXXXXX++=--- ---- :::.: .. \n",
|
||||
" \n \n \n \n \n \n \n \n \n . \n \n .. \n . \n . . .. \n ... : .. ......... .:.:. \n .:..+....-:. .:::-:. .:-:--=:.+.... .. . .:... \n .:==-::::-=:: .:::..... . .:==--:--::.-::.... .-.-:::. ... .::-:. \n . ..--=-------:.. ..::....:::.-. ......==--:--=-:=-+.+....... .:-:....- . .....:-==-:. \n .:......::-==--==--:..........::::=---.. ..:==--==---=-----...:=-+--:......:.... .. ........::--=++=-:.....:.. \n .....-.:::::::::-==+==--:-:.:.:... ...::-++=--:...:=---.-=---+=-==-::+==----.:-::....::..-:....::==::--=+X++=-::::+:-:.... -.. \n .......::-=-:=+=--++X++==-=-::-==::::.:.:-+XX+==---+==--=::::--+X=====--====-=+:--... ....:-=-:-::-=++++++XX++==.----==-::-:...=... \n ..:.::::------====+XX+X.++=---.==-:-::.:-=XXXXXXXX+++=- --:::--+XXXX+++.==++++++==-=-:....--++:===+XXX++XX+++X++==-===--:::::::::.... \n ....::---------==+=+X+++XX+=====+++==-:--=+XXXX+XXXX-++==--::--=+XXXXXXXXXXXXXXXXX+= =-:=:-- +XXXXXXXXXXXXX++=++++++++=--::-- :::....+ \n ....::-=====--=-++++X++++XXXXXXXXXX+++==+XXX:X+XXXXXXXXX=--.-==+XX-+XXXXXXXX+XX+XXX+++++=+++=+XXXXXXXXXXXX+++==+.++XX++=-=--=--- ::..:. . \n",
|
||||
" \n \n \n \n \n \n \n \n \n . \n \n \n .. .. \n .. . ...:. \n . .:. ...... .:=:-=-:. . . \n .:--::..-.::. ........ .. :-=-::::::.-..... ....:. .....:. \n .::--:=:::::.. ..- ...:.:. .::---:::::::--+.... .... .. .... ::-.. \n .... ...:--:-::-::.::.. .......--::.. ..:=.---:::--:::....:--:-=. . ..... .. ...:..:.:+:--- .. .. \n +...::.=..::-==--:::::::::.... .::=+=-::.....:::--.---=----:..:--::::.:.::...-+...=-.:. ..:==:. ::- ===-::....-:.. \n . .-.:-:--=--:=+X+=--=:::::---:....::-=+X+=--::.--:+:..:+:=+==---:--=--::-::-:... ..-=:::::::-===--=+++==-:::::::::::....... \n .......:: ------==++X+== ---::==-+::.:.:=+XX=X+++==--::....-:-XX+=++++========---:-:. .:-+=---=++++ +=+X+=====-------:::........ \n ... .:::-:---==:===++XX+==---+++X==-:.:-=+XX=X+XXX+==-::.:+:-=XXXXXXXXXXXXXX-++==--:....:--X+XXX-XXXX-XXX+==+====-=---::::::..-=.... \n ..::=.-------=+-===+++XX++==+X.XX+:+---=+XX:XXXXX:++:=-:::-:==+XXXXXXXXXXXX++ X+=====--===:+XXXXXXXXXXXX+==--=++X++.=--::--=:::...... \n .... ::-==--=-=+X+XX+== ++XXXXXXXXXX+++++++X++:XXXXXXX+X+==---=++=XXXXXX:XXX++ ==XXXXXXXXXXX++XXXXXXXXXXXXX++====++XX:+==-= ===--:.....:.:.\n",
|
||||
" \n \n \n \n \n \n \n \n \n \n \n . . \n . .....: \n . .:-:--::. . \n .::=-... .. .......... .:-::....=. . . . . \n ..:::::....... . ... . :.:-: :.::. .... . . ... . . ... ...... \n .. ..::=:::.......... .-=..:. .:::-:::.::..... .::---:. :. .:.+.:....:::. \n .-....::-=--::.:....:.:. .-.---==-.. ...:..:::::=-:.::..:--::..... =.. ...=-. ..::.....::--+-:... .. \n ..:---::--=X=--:.:.:.:-:-:.....:--=+X=::..=:::.=...:-+=---------::-::..::.......:-:....=.:----::-==-=--:......:::.. . \n .-..:--:::-=--++=-:+::::---=::...:--=+XX+=++=--::.....::+X+++=+X+=====-.-:::::. .:--:::-==-+-=:==+.+---:.::.::::.....- .. \n .......::-::------=+++==-.--=-+X+==:..:-=+XXXX++X==-::..:::-+XXX-XX+XXXX+===+-=-:.. ..:::=X++.XX++++++++++--=--::::::-:-:. ...:... \n ..:::::--::-+==-==-=+++=====++XX=++::.:-++XXX.++++==-:...:--=+XXXXXX X+XX===X+==-+--:--:-=+XXXXXXXXXX+++= ----+==--=-:::--:........ \n ..:---=---==X+++=-===+XX++==+XXX===.===+++ +XX++====+--::--=+XXXXXXXXXX++.==+X-XXX+++===-=XXXXXXXXXXXXX+=----=+X++=-----==-::........ \n ....=:--======+XX+X+=----=+.XX+.XXXXX+=+=++X+=+XX XX+==--=---=+XXXX-X=XXXX++===.=+XXXXX.X.++++XXXXXXXXXXXXX-+++=++X XX++==++===-:..........\n",
|
||||
" \n \n \n \n \n \n \n \n \n \n \n .. \n :::::.. \n .... . -=...... .::..:...+ \n ..::.... .. .: .. .--:::.+... .. .. . .. . \n . ...:..-... .. .. .=.. .. .:=:::::.:. ... .:::.. .: .. . .... \n . ..:--::...:...::..: .:-:..-:... ....:::::-:....+..:-:.::. .=. .. .:....::::. \n ..:.:..::-=--:..+....::::. ...: --+-:.-. .......::+-::::: ---::::... ... ..:::. . ..::::.. ::--- -.. .. \n ..::-:::---+=-:..::..:::-:....::--=++=-:-:-:::-....:-+=-=+==++=-.-+::..:::..= ..::.....:::-:-=-----=-:......+:...= . \n .-.:::::::--.+===--:::-=:.==-:.+.::-+XX+++++-- :.. ..:-XX++++XX++++==-=-::.. ..=:=+--:++=:=-=====:=-:::...=.::......+ .. \n ......::-:--=----=++==---==-=+XX=-:..:-=+XXX+++:==--:...:--+XX:XXX+XX++=-==+-+-:=...::: +X++XX++=+++ +=+=----=:.::::-::-..... .. \n .:::::-::--+=--=---+X+===-=-=+XX++=:::-=+XXX.+++===-:-..:-=+X:X:XXXXX XX===XX-++=--+-::-=+XX++:XXXXXX++=-::--====---:::--:...+ ... \n ..:-----:-==X++==-.--+XX++==+X=X++==+==+XXXXX+++==----: :-=+XXXXXXXXXX+=====+XXXXX+++=---+XXXXXXXXXXXXX+=----=+X++==--=-=--:.+..=.... \n .+.::--==-==++XXX+=----+=+XXXXXXXXX++==+++++=XXX=XXX==------=+XXXXXXX XX.+===== +X-XXX+XX===+XXX=XX=XXX=XX+.++++XXXXX:+++++==-:.... .....-\n",
|
||||
" \n \n \n \n \n \n \n \n \n \n \n . ...... \n . .. .. ...=::.. \n .+.. . .:. . .:.. .=... : \n ...:.-.. .: . .:. =.. ..:.....:. . . . \n ...:::..:... ..... .:.....-: ........-+:... ..:.:::. .: .... \n ......-:--::. +.. ... . ..:::::=:...... -..:==:.:::..:=-:::. . .. ... ......: ::... . \n ..:...:-=--::...:.:+:..-::..::.---==-=-:.:.:.. ..:==----:-=+==--::.. ... .::. ...+:::---=:::-:.. .... \n ...:::=::-=--:--=::--::-==::..:.-+++===+=-::.. ..:-X++++++++++=--+-:.. ...:-:::-==------==----::.-... ..... \n .. -..::-:: ::-=------------X=-:....-+XX++====:--: .:=X =XXXXXX+==--=+-::::..:....-+====+===-= ===---::-::....+.....= \n ..+:..::::-=--::::-++==--:::--+X+=-:..:-=+XX+=------:. ..-=+XXXXXXXX+=+=--=X==:=-=::..::+++==+++++++++=--:::==---::::.::.... .. \n .:::::::--=== -:-:-=X:X==::-=++++=-=====+-XX++==-::-:..+:-=+.X+XXXXX+=-=--+XXX-X+==-:::-=XXXXXXXXXXX++=-=:::-++===-:::--::.. ..+ \n ..:---===-==+++=-.:::-++XX+==+XX++==-====+XXXXX+++=---::::-=+XXXXXXXX++=-:-==++XX+++=+=--=X:XXXXXXXX=X+++=-=-=+XXXXX+===--+:... ...= \n ...::.--+==++X+ +++=--.:=++XXXXXXXX+.+==++====++XXXXX+==+=-==+X:X-XXXXXXX+==--+==+XX.+=-=+==++XXX ++XXX-XXX: XX X-XXXXXX+ +=--+:...:......=\n",
|
||||
" \n \n \n \n \n \n \n \n \n \n . \n . .. . ........ \n .. . . .. ..:.. :. \n .... . .. .. +...-=.. .. \n ....... .. . .. .-. .. ....--:.. . ..: :... \n .. . .:::.. . : .+...::. ..:-:.........::-........:-=-::-.. . .. -... :..... \n ..=...--::-=.....=+....-:..:.....:----:.:... ..:=-:::::--=-==--:. ... ......:.:--....... \n ...:...:--=:..:::-...:::+-...:.:--=-----:::. ..:+X+==++ ++== -=-:... ...:-..---:--:.:---:::=:.. . \n ...-::..::-=:+::=::::::=-+-:....-+X+===----:.. .-+ XXXXX++X==-:-=-::..... ..-=---=---::::---- :=::.:.. ..... \n . ......:::--:.::.-+=--+:..:::-=+--::.:-=X++==----:::...:-=+XXXXX+++=-=-:-==---:.+. .:=+X=:===-.-===--::: :-:::-:........ \n ...:::.::-=---::::-=X+=--::::-==+=--==--+XX++=--=:::....:-=+XXXXX+=+---:--XX-XX+=-::..:-=++++++--XX+=--:..-:-==---:-::-:=.. . \n ..::----:-=-== -::.:=+=++=---===+= --===+XX+++===X---:..:-=+XXXXXX+==--::--=+X++==-=-::-=+XX+XX+X XXX+==-:::-=X++++=----:::.. .+. \n ...:: -----=====+=-::::-=++X++++XXX+=--==+=+:++XX:+++ ==-:--=+=XX=XXXX++==-:-==+X++==--==--+XXXX+=++XXX XX+++++XXXX ++====-.--... +.... \n ...-.:: ----:===.+==X+=-===++X.XXXXX++X+XX+=-==-==+XXXX++====+XX=X XXXX+XXX+======XXX++=-=-===++XXXX++X XXXX+XXX=XXXXX ++++===-:--:..........\n",
|
||||
" \n \n \n \n \n \n \n \n . \n . \n . ..:: \n . .. ::. . \n . . . .. .: ::. \n +... . .:.. .: .:::. .. ::...:.. . \n ::.. :.... ..::.:.. ..:-::.. .:: --:.:. . . \n =....-::.... . :-::. ..::.::-.::. .:--::.:-::-----+-.. .. .. .....:. . \n .... ::-.:....:.. . ..:=:.:..::::::..::-.. ..=+=--=+X+--=--==:... . ...... :::.. ... ::..... \n .-... ..:-=:...:::......--:=..:=++=--:::::. ..=XX++XX++===:----::.. ..-::.--:........::::-:.. . \n .....-::-...:-+:::::..-...:--::-::==++=--:.::......:-=+XX-++==-=-:::-+-::-:.:. ..:+X+==---:-::-::::.:::............ \n .. .:..---:::...:=+--::...:.:-=-::-==-=+++==-:-=::....:-=XX:X++=-=-::-:=X+=++=-:....:-=========++=-:. ...:-:::::::.:: .. \n .::-:.:::--=--::..:-=+=--:+:----=-=::-+XX.X+==---=-- ..:-+XXXXX++=--:..:--+X+==----:..:-+:+++++++=X+---:..::=X====-::.:::. \n ..:::-:::- ---+=:...:--=++=+=.==++=-:---++X+X+===-=--=:::-+XXX==XXX+=-- ::--=+:+==-:-=:-=+XXXX+=++++XX++== ==++.X+=----:::::. \n ..+...::-----------+=::--=+X+++++==:++====----===X+=+=+=+==-=XXXXXXXXXXXX+===+==+XX+==-:-=-=+++XXX+=:++XXXXXX+X-X+++++=+=---:::+:......... \n .....::::-----------=++==+:XXXX-X.XXXX+X+=-::-:--==XX+X+XX+ ++XX+:+XXXXX-X:+++++.+XXXX=--=.====+XXXX++XXXXXXXXXXXXXX+XX++==--::::--...::.....\n",
|
||||
" \n \n \n \n \n \n \n . \n .: \n . :. \n . . .. . \n . . .. \n .. ... .. .:. .::- \n :.. .:. ......: .::. ...::-:. \n ::.. .. .-. . . ..... ..:::.. ---:: =---:. .. \n . ..:::. . . ..:-.. .:..:........ .:----=+-+=----::-:. . ..... . ..:. \n .. ....::-:. ..... ..::-:.-====-:.:.... .:+X+=:++=-----:--:.. .:.:.::.... .. .:.. \n .. :.. ....--:..: .. .::..:-=--===::..::: .=.:==X+X+==--- -:.:-:...-: .:-++==--:.:..::........ . . .. \n ..::-::...:.:--:.....:. ..: ::.:-=.===-=-::--:.=.:-=+XXXX+=- -+:::-+=--+-::. .:--=-=-----=+=-:.. .=.::..- .....+. \n ...::....::--:+....:--:::::.::+::::..:-+XX+=-:-::--::..:+XXXXX=+=--::..:-=++==-::.:...:-=+=+=====++=::.....:==-:-::....... \n ....:..::::::--:....:-=---===:--==-:+::=++X++-:-:-:=--::=XXXXX++++=-:::=-:-=++=--::-=::=+XX+X+=-==+X+==-----=++==-:::::..... \n ....-::::::::::::=-..:-==X++=====-==---.::-=+=X+--- -==--:-+ ++XX+X+X+=---==-=+XX==-::-=--=++XXX+==+=+XXXXXX++++==+=--::::...:. ... \n ...-..::::----:::-:-=---=+XX-XX++++====+-:..: ---++=-+=+====+X++=++XXXXXX+===+++X=++=-::--=-==-+X++=+-+XXXXXXXXX++++ +=+=-::.:..:.-...-... \n -.....::----==-------=== +XXXXXXXXXX++-++-::.::====XX++++==+++XX ++++XX X XXXXXX++++++=--==+==-+++XXXXXXXXXXXXXXX++-XXX+++=--:::: :.:-::::.:.\n",
|
||||
" \n \n \n \n \n \n \n : \n . \n \n . . ... .. \n . ... . :. ::. \n .. .. . .... .::.:..:-.:.. \n .:... .:. . . +..=::===--::-:.. \n ..::. .::......... . . ..:--:--X+=-::::- .. \n .. ..:. . .-...:===--:.... .: ..-XX+====:-::::::.. ... :...:. \n .:.:. .::. .. . .....-==:- -:...:.:....:-+X++++=----::..-:...:.. .-=+=--:..::::... .. . \n .:::... .::. .. .. ::....--+=--::: ..-:...:=+:++X++=-+:::::-=-::.-... .---=----::-+=::.. .... .. \n .. ...::.. :: .=.::...........:=+X+=-:.:..--:::-+XXXX+===-::..:--+X=--::... ..:-+==+=---==--::.....:+:....+ .... \n ...... ..:.:.::. .:----::-=:.:=:::::.-=+X=-:. .::::..:-+=XXX+==-::.=.--:-++=--::.::.:-=++ X=----=+=--:::+-==-:::.+.. . \n ..............:-....:--=++=---==---:::::--=X=-:.:::---:::-X+++XX+==-::::==-=++==-:.:- --==+X++=---==XX++XX+==-==--=:::... . \n .-....:-:-:-::+::::-::--=+XX++=======-=::..:---+=--:==-----.+X+==+XX+X+=--===.XX:==::..:-=-===X+==-= =+XXXXXXXX+==+==+--:........ .... \n .... ::: . ------:=::--=+XXXXX-X++X==---:....:-=+++=-==-----++X+==++XXXXXXXXXXX++++=-::.:-=--==+XX+=++++XXXXXXXX++XX+:++=--:.:......:::.... \n ......::--======.----==+XXXX-XXXXX-====--:::::===+XX ++====+++X++XX++-+X=XX+XXX+:++X+=- -=.====+XX:XX+XXXXXXXXXX++++.XXX+==--::::::::::::.+.\n",
|
||||
" \n \n \n \n \n . \n . \n . \n \n .. ..+ \n . . . .::.::..:: \n . . :. ..:--=-:::.. \n .. . . ..-..:--=-:::::. \n . .. .....-. . .-::+::--=:::.:.. \n . .. .:-=--:...... .:...-=XX+=-==-:.:+:.. .. :+...... \n .. .. . ..:-:==-:::. .. :..:-=++======-:.:. ....: :--=-::.-..::. . \n .... :. ..= .....:--==--:... ..:..::-+++=====-...:::--:.. .-:=--:-=-:--:::.. \n .... ::....+:. .. ...:-+==-..... ..:..:-++++==--:...:--=X+=-::. .. ..--=---=----.:::. -....:.. \n . .. ..::==::.::=........:=++=-... .:....-+XXX+=--:....:---== =-:....:..:-+===-::-:--.:.:::::-=::+.. \n ......... .:. ..::-===-:::-=--:..:.--++--:..:.::::.:=+X++X+---:...:=--===:--..::--:-=++=---::-=X=-=++======--:..:. \n .......::.:.::...::..:-===-++=-:===---:..:.:-=+-::.--:.:::-=++==+X===--::-==++X+=-....:----=.++-::.-==XX+X++XX+=====-::....: \n ....::::::-:::-:.::--+XXXXXX++==+=--:-...:.::-=X=-::-:::::-==+===++XX++==X++X+-++-:..=.------++=======+XX.XXXX++++==--=::.:. ....:..... \n ...:.:::-==- -=.::--=+XXXXXXXX+++:---:. .::=-:-+X+=-----:=.====++++XXXX+++.X++==++=:::-= --==+XXXX.+++XXX:XXX-XX++++=++=-:- .....::::.... \n ......::--==+.==+=--=++XXXXXXXXXXX==---::: ==X+===+XX++==+++.==++++XX+=+.XX++:+==++XX+=-=+=.===++XXXXX-X.XX=XX++==-++X+XX+==.-::: ::-:::=...\n",
|
||||
" \n \n \n \n \n \n \n \n . .. ..- .... \n .::-:::. . \n . :..::-:.. \n .. .......::.::... \n .... .. ..::==--::-:.... \n .:---.-.. ...-=+:+==-::::..+.. .. .. \n . :::=-::... .=.---+==----+:..:. .. ::=-:....::.. . \n . . . . .:--=--:: .-..--=+===---:..::.::.. :::=-:.::-::.... \n . ....= . . ..-=--:. . .:-=X+==--:.. .:-=+++=-.. .:=:--:.:::::.. . . \n ..:--:=.....=. .-.== -:.. -... .-=+XX=--::. :-+===--:.. .-. ..-==--::.:=:...-.::::-::.. \n . . ...::-----.. :--. +..:====-..:..:.+..:-==+X=-:::..=.:=--==--:...::-:: +===-::..:--:: =---==--:.-.. \n ........... .=...::---==--:::=-:-:.....:-+=-:..::.....:==++.+=---::.:==-++X=-:. ..-::-:=++=-=:::-++======X= =--::..... \n +:........::::..-.:=XXX+XXX=--::--:=:. .=..:-++--..::...:---====+==+--:=:=+ +++=:. .-----=X+==----==X++++=+++==--:::::... ..... \n .=.:=::::=-:--::.:-=+XXXXX=+==--:::.. .:.:--=+X=-::::.------==+XXXX++==+ ====++-:...-=:-=--=XX++====+XXXX+++X+++==-==-:::.. ....::.... \n ....::.--==--=-----=+XXXXX-X++=+--:::::..--=X+-=+X+====--=--===++XXXXXX++=-++= =++=: :-----==++XX+=++XXXXXXX++ ++=+++++=-- ::...:::::.=. \n ......::-===+=+++===++XXXXXXXXX+++===-:::.-=+XX+==+XXX===:+X==+XX+XXXXX+XX++======++X+=---== ++-=+XXXXXXXXXXXX++==+.+XXXX+=+==--::--::::....\n",
|
||||
" \n \n \n \n \n \n \n . .... . \n ..:--::.. \n ....::.. \n .. . ..-:... \n ... ..:--:--:.::: . \n .::::.. . ...:---+=--:..... . . \n .::-+-.... ...==++=----::.... .--:.. . .. \n .-::---:.. ..:--==.---::..::=..... .=.--:..=.::=.. \n .. . .::--::. . .:-=+==--::=.. ::--:-::. .:::-:....::.. \n ..::. .. . .:-=--... .:-=X+=--::. .:-:++=--.. . .::--:::..::. ...=... \n ..:::-:.:..::-. ..:+=--:.-. .. .-==X+=-::.. .::====--:... ::.::==--::..:::....::::-=-:.. \n .. ........::-=---:.:-:::. .:-=+--:..:+.....: ==+X=-:: ....=--==+=-:=....::--+==-::..::=--:-::-=+--::.... \n .....+.::... ..:-== =-==--:..:::-:. ...:=+-::..... :-=-=.=+=--+.::-=--++X=-:. .:----+X=-:+:---++====.++==--:..:::.. . \n ........:-:::...:-+X XXXXX+--: :::.. .-.::-=X=-:..:..---:--==+X++.=----:== ++-:. .-:-:--+X++=---=++++++==++++--: :-::.. ...... \n .=..:::-:------..:-+XXXXXXX+==-::.... ..:-=+=++==---::.--:-=:=+.XXX++==-===--++-:..:-::----=+X+==-==XXXXX+=++ ++==--==::::. ...+:..... \n ....::-:-== ==-=--==+XXXXXX-+++=--::.:..:--+X+.==++=======- -=+++XXXXXXX+=======++=-::-:--.==++.X+=++XXXXXX++=+++=X+-++=-=--::..:::::... \n .....::-=-=+=++++++++XXXXXXXX++X+ +=--::--=+XX+==+XX+===.=+=++XX.++++XXXXX+====:==+X+= --==-+X==++XXXXXXXXXX++==.++:XXXX++===--.:--::::-.. \n",
|
||||
" \n \n \n \n \n ... \n ......::.. \n .:..= \n .. \n . ...... .. \n ....=..--:+:. . .. \n .:--:.. ..-=--=-:.... . . \n ..::--:. .---==--:: .. .: .:-::.. \n ..:--.: ..---=-=--::....-=.... . .:::....:.. \n .:--::. .:-===-:::=.. ..:-::-:.. ..:::- ..... \n ..- . . ..==-:... ..-=+=-:-... ..==++-::. . ::-::..-... ..... \n .:::: .. .:+=-::. .. .-=++=-:... .:---=-::+......:.:X--:..:.....= ....:--::. \n .........-::::..:+::.. ..-==::.-.. .:===++=- ... :-:--==-::... .::-+==::...::-:..-..:----:.. \n .:.=. ..:--+=-----:::....:. .::=+-.=. ::::--+=-:: ..:-::-==+-:. :.::-++= --::-+X=-=----=---:.. .... \n .-... .:::: ..:=+XX++==+=:.:..... ..:-++::::.-. .:::--=XX==-::::.:--+X-:. .:.: :-=X=--:::-=+= ==--==+--:.-..::.. ...=. \n ........::=-::..:-++XX++-+=--:.-. .:-++++-:.:--..::::--=+XXX+=--::-+--==-:. .::.::: :-=+--::-=+X+-+==:==+=-::::-::... . .... \n .+..::::-:---:-:::-+ XXXX++==+-::...=. .::-+X ===-::::-------==++X.XX+==--=---=+=:..:..::::--==+=--+XXXXX+=====++=-==----::...:.::.:. \n .:.::--=====++====XXXXXXX+==+==+=-:::::-==++==++==--=::--==++==+XXX-XX++.======X+-::.::=-=++==++++XXXXXXX+==-=+=++ ++==----::::::::.=. \n .. ..::-===+==++XX+XXXXXXXXX++=++X+X+=-=== +=++=++XX:==---=+++++++=+++XXXX++=====-+XX==-===-+X==++XXXXXXXXXX+=-==++X+X+-+===+=-----::.... \n",
|
||||
" \n \n \n .:. \n \n ...=. \n \n \n .:.... \n .-..::... \n .:---::. .:-=--:=.... .. . \n .:=::. ::.:=-- ::.-. ..::-:.. \n ..-:.. .-::=--:.... ..:... ..-... . \n ..=::. .::---::.... :--::.. ...-:... . \n .. ..+-:.. .-=--::. :--+-:... .::+:...... ... \n :..:.-. ..==:.. .. :-++==-::. .---:=-:.=. .::+-:..=. .:::... \n .:::..:.... ... .-==-. . .::-=+=--:. .:: :-=--... ..::=+--::...:........:::-:. \n .......::::+-:.::.... . ::==:..: .::--=X=-::...::.::-==-:. ...:-=+=---::-+==-:::::--::.. ... \n . ..-:.=.:-++++=--+-:-.. .:-+-:...... ...::-=X+=--:...:.:=+=-:. ...:.::==-:::+-==:----::--=-:. ...... .. \n . . ...:: =...-==+X++===-:-.... .:=++=:...::. ..:---=+XX+=-::..::--==:. .:... ...:=-::.::=+=====--==+-:.....:+.. ..+.. \n .......::::::..:-=+XX+=.=-==:::... ..:-==+=-:.....:..:---==++XXX=-:---::-+=::. . ..=:+:-.-::.=+XXXX====-=+=-:-:::::.. ...... \n ..:::------=--- =+X:X++==-== =++=:.:.:::--=:===-::....:+:-= ==++XXXX+==--===.X+-:.. ...:-=---=--=+XXX.XX+=--====+=+=----:.......... \n . .=.:------==+X++++XXXX++.==-==+XX+-+-=-===-=.=+++==::::-=X++++=++XXXXXX+=--==-=+X=-::--:-=X===+=++XXXXX=X+=-=+=+XX+=====-=--:::::=... \n . . ...:--======+++XXXXXXXXXX++++++XXX++===:+++==+XXX.++=---=+XXXXX+-+++XXXXX+=======+X+====++=+.++XXXXXXXXXXXX==+++XXXX++ ======---:.::..- .\n",
|
||||
" . \n .. \n \n \n \n \n . \n .... \n . ..--....... . \n ..:==+::. .::--+.. . . .. \n .+.-.+. ...:--.:. .. =..-:... \n ..-:. .:-:---.. . . . .:... \n :+-. ..:--+... .:-::.. ..:-:. \n .==:.. ..-+--:... .::-=-.. .:=-.. \n .:-.... .-+-:. ..:++--::.:. .::----:. ..==:::. ....: \n .::...... .--=:. .:-=+--::.. .....::--:.. ..-+-::.. .. .. ...+::.. \n .. ...:-::.... . .:-:.. . .. ..:-=XX-::. ......:=--.. ..:-=:-:..:==-:::.=.-:::.. \n +..:..:-=:=+--:::.... .:==:... .. ...--=X+--:.. ..:==--:. ..-:..::--=-- ::-:-::-:. . \n .......:--=+==-:-:::... .:--==-.. . ..:--=+XX+=-::...:-=+-:. .. . ..:-::..:--==-==::.:=-:... ..=. \n .........:....::=:=X+=-----:--:.:. ..:-=.=:.-. -...:--==+XXX=-::--::+=+-:. .:::+:...:-=X++X=----==-:::::..-. ..+. \n .+..:::::-::::.:-=+X+==-:----+==+=:+..::=--==---::. ..:-=:--==:++XX-=-:--==-=X=:. .:--::-:::-=+=XXX++=--=======- :::....=:... \n .. :--:-:--==+++=+X+X+==-- :--=+XX=-:---=--= ====--...::-+==+XX++XXX:X=--::- -=+=:..:-:::-==--=-==+XXX:++=+-===+XX==.-----::::=...: \n . ..+:----=--==+++=XXXXX+==++===-+++=---=+==-+XXXXX+=-::--==+ XX+XXXXXXX++=---=--=++-::--==-=X=:+++XXXXXXXX++-== +X-X++=---- -:::+::.. \n . ..:::--=++= =+=+=XX+XXX:X++XX++++++==++X++==+XXXX-X++=-==++XXXX:X X-XXXXXX+=+===+-++==+-===+X+X:XXXXXXXX.X+===+XX++XXX+==.--=--:::+:+....\n",
|
||||
" \n \n \n \n \n \n . \n .-:. .:----:.. \n .:--:=.. ...::=-:.. .:. \n . ::.. .:--::. ..... \n .-=:. ..:--::. . ..::. \n :=:.. ..==::. .:--::. .:-:.. \n ... .-=:. :..++-:.. . .:.:-::. .-:.. \n ... :=:. ..=+-::... . ...::--: .--:+.. .. \n ...=... .::. ..--+==-.. .:. ..::-::. :-::.. . . .... \n .. ::-.. . .::. .:-=++-:.. ..==::. .::... :--::.....::.-.. \n . ...+:::--:::..= ..---.. ..:-=++-::. ..==-:.. ..::. .=::-:::::-.. ::.. \n . . ..::::----.::::.-. . .- --:. ..:--=++=-:....+.-=+-.. ...:. ..::-----+-:.:-:: .... \n -...... ..:---==-::.::-:::.::. +..:==--:.. .:=::--=+++X--:::::::=+-.. .:..:.-...:=+===+=-:.--:::...... \n ...::..:.:: ---=++=-::.::-++--:==:..::-:-+--:-.. ..:-::--++++ +=:..::--:=+-.. . ..:-..:::::-+X=++=--::-==-==-:=........=:. \n ..:::::::--:-=-++X+=--:::::--=====-::-= --=--=-:. ..:-=---=+.++XX+=::...-::-=:. ..:::.:-::=::--=+XX+====-=--=++=-+::::.. ....- \n ..:=------:-=-=+X+X+=--=-----=.=--:--X--=+X+++==-:..::-==++XXXXXXXX+=-::::-:--=-.. .:-:--+--=-=++X+XX+++==---=++==---=:::=:::...+ \n +..::--=---===+==+++++++=-++========-=XX+==+XXXXXX+=:::--=+XXXXXXXXXXXXX+-----====-::-:-==+XXX-XXXXX.XX X+=---==+++-=------:::::..=. \n ..: :..:::-==.=--=+X++++XX+-XXXXXX+:==++++XX+++=+XXXXXXXX=--=+XX-XXXX XXX.XX+++ ++==+XX++==-=-=+ X-XXXX+XXXXXX+==.++=++XXX=----=--::=:::: ...\n",
|
||||
" \n \n \n \n \n .. ...... . \n .::.. ..::=+-::. \n .--... .. ...-:.. ..- \n ..:-. ..-:.+. .:. \n .-..- ..=-:. .. ..: \n -:. ..+-:.. .:---::. .:.. \n ..- ::. ..-+:::. .:=::. :... \n .-. ..-+=--:. . .+.-:.. ::.. \n . .. .:. .--====:.. ..=-:. .:+.. ... \n ...... ..-. .:-==-: : .+=:.. ... ::- .....:.... . \n ... ..... . ..:::. .:.:-===-:. ...-+-:. .. .:.:-:....:...:.. \n .......::........ .:=-:: .::.::==++=:=..:..--=-. ... . .::-:: :--:.:-.:. .. \n . . ..::::---:. ..:.:....--:.....::--:--: ..:+-:-:-==++=::...:::-=:. . ........:-+-:-=-:::::...=.. \n -...-......-::--+=-:....:---.-=--=:+:-=--:-::.. . .:-::::=====+-:. .: :--. .. . . ....+..-++===--::::--::-:..+. \n .-.::...:...::-=+=-::.-.:.:---.=--::-+-:---:-:.. . .:-=--== == ==:.. ..:--. ..:..:......:-+++===--::::-=-:-:... . :.. \n .::::::::.::--==+=--:::+ :::-----::-++==++=---:......:-+X++X+++X++=-:......--:. ..::=-:.:--=++++==--=: :-==---:::......... \n ..:::-::+:--=--====+===+==--------:-=X++ XX+++X++=-:..:-=+ XXXXXXX X++-:::: -.--:..:.:-=+X+++ +=+XX+X++==:::--= ==--.:::::..... . \n ....::---.::-++===.+XX:XXXX+++--=====+++==++.XXXX++==-::-==+XXXXXXXXXXXX+=.==-==+==+--:--=+XXXXX-XX.XXXX=+=--==-==++=-:.:--:: ..:.:... \n .+...::-:--===--=++=+++++XXXXXXXXX+===+X+++++=+=+XX=XX++====++XXXXXXXXXXXX=++==+=+XX-XX++=-:==++XXXXXXXXXXXXXX+==+X=++XX+=-:-=-:::: :::=..:.\n",
|
||||
" \n \n \n \n ... ..+-:.... \n ::. ...::-::.. \n .::. . ..-... . \n .. .-=:: .. \n : .:=-:.. ... .. \n :. .-+:::. .::-=::. . \n . .:X-::-. .. ...:... . \n .. .:=--:-.. ..-:. . \n . ::=-=--.. .+-: . . \n .... ...:---=-:.. . .-=:.. . ..::. .. \n . ..:... .-:..:--==-.. ..+.:+-:. ... ...::.. ..... \n ..... . .:::=-:. .::...-=-==:. ..::-=:. ... .:.::.:..:...:. \n ....--:..... .. ..-:.. :=::.::. . :--:.:----=-:. ...-:. . . . ..-::.:.::..::-.. . \n .. ..::-::. ........-:-=::==::..:..: .:--::--=----.. ..::. .. . .-=----::....::.::.. \n .... .. .:::-=-:.. ...::-:-=-:-::=-:=::.. .:-=---=====-. . .-.. . .. ..-==---:.:-....::::... \n ...........::--=-:::..-::.:-::--:.:-+==--:..-... .:-++XX++++==-:. . .::.. ..:.=. ...:-=++- -::::..:--:-:=.. . . . \n ....::.:..:-::--=:=----:+:-=--:-:.:-++XXX+=-=+=---:...:-=+XXXX=XX+==:....+:::::. ....:=+==-::--==+XX==--:..::----::. ......=.. \n ....:-:::::+=----==++X++====+-:----.==+=+++X++=+==--=:.:--=+X=XXX=XXX++-:+::--=-:--:..:-+XXX+:++==+XX XX=-:-.----+=-::..::......... \n .. .. :::----:::-=--= =+XXXXXXXXX+=--===+-====++XX++=+=-----==+++XXXXXXXX+++==+=XXXX++=-::--=+X+X++X++XXXXXXX=-==+===++=-::::::::...::.... \n ......::-===+=-=-====++-+XXXXXXX..++==+X+======+XXX-+=+=--=+X++++XXXXXXXX++======XXXXXX++-===+++=XXXXXX=XXXXX++==-++XX+XX+= ---::::::::::. .\n",
|
||||
" \n \n .. :+:. \n ... .--::. . \n . ..--:. \n .. .--:. \n .. .:=:+.. \n . .--...:. .-:.. \n :=:..:... .::-:. . \n .=:. .. . :-:. \n .::::::. .:=:. \n .. ::-::::--: .. .--.. .. \n .. .-. .:..::+:--.. . .:+:. . ..... \n . .:...: .. :.:=::: ..:=:. . .... . .. \n .. .. .--::.:. :.:. ::.:::.. .::. ..:.. ..... \n .:...+ ::...:=-.....:. ..-=..::-:=::. ..: .:-....... .-... \n . ..:.. ..:-:::..-:...=.. .:--:::::=-:. :. .-=::::... .:... .. \n .. ....:::. .+.:+:::-.:-::-:. .. .:-=--:::--.. ... .. .-=-:-:.... ........ \n ..... ..:..::---:+.. ..:==:::- ..:==-::.. .-.:. ..:-=+=====--:. . ..:.. ..:. :--++-::.... .::..- \n ..:..- :...::---=-::...:-+:::-:..:-++X++-::--:-::. ..-+XXXXXXXX+=-. . ....::. .:--=-:..::--=++-:::....:-:-:.. . \n :..::......:.::-=X++==--:-++-::-::---=+++X==-----:-:..:-++X=XXXXX:+=-.-..:::::::.....=+XX+=---=--=+X++=-:: :-:-=-:.. .. ..+. \n ..+::-::..:::=:--=+XXXXX+=+X=-:--:=:---==+XX+===--::-----==XXXXXXXXX+=-+-=+X++++--:..:=-++X+=====.=+XXXX+=---=--==-:....: ........ \n .. ..:=---==::-:--====+XXXXXXXX+==-=- ==+---=+XX.+==--:=--===++XXXXXXXXX+==-==+XXXXX+=--:--.++XX++++.X+XXXXX+=--=++++++=-:.:::::...+:..-. \n ....::--=++=+=---.==++.+XXXXXXXXX++=+==+-====:XXXX+++=--====+XX-XX:XXXXX+====:==+=XXXXX+==-=++XXX-X==XXXXXXXX++==+X=XXX++=-:::::::::.::...-\n",
|
||||
" \n . .:: \n .+. .-::. \n .:--:. . \n ..--.. \n . .:-:.. \n .-:...::. .:. \n :-....... .--:.. \n .-.. .. ..:-.. \n .:+:=.. .:-:. \n . ::+::..:=: .-:.. \n . . ..:.:::-. :-.. . .:.. \n .:..-. . ..:.... .:.-. .... \n .. .:-:... . .. :.::... .-:. ..... . \n .. -.. ..-::.... ..:-..-.::+.. .:. .::.. . .. \n .. ..:..:..-::.. .. +:::.=.:.:::. . .--:. .... ... \n . +.... ....:: :-:.:..-:=.. .::-:::..:-:. :. .-=::::... .. .. \n .. ..:::=.. ..-=:::::.:.:--... . .:--------::. ..:. .. .--=--:. ....-. \n . .. ..+..=:::.... ..:+-:-::.+.:=+=--:. .:.:.. ..:=+++++++=--. ..... :...:. ..:-=+-::.-.. .:::.. \n ..::.. ....:-==--::.=.:-=-=:::..:-=++XX=---=:-::. .:-+X+XX.XXX+=-.:. ....... . .-++==:..=::--==--::..:..::-:.. \n ..-.::... ..::::-=XX X+=---+=-::::-- -=+++X++==---::-:::-++:XXXXXX-+==:..:-=--:-:.. .:==+X+=------== +++=:::--::--:.. .... ..-. \n . ...:::-:...::-- ==+X-XXX:+++=-:---.-::-==+X+X+=---: ----==+ XXXXXXX:==---+XXXX-X=-:..:-++ X+====-=++XXX+X=::--= =-=-:.............. \n .. .:.--====--::---==-++XXXXXXX+==-=--==----+XXXX+==--:==--=++XX.XXX-XX++=---=+XXXX-++=-:--+++X+++ ++XX=XXX++=--+XXX+++=-:.....::.+.:..+. \n ......:--==+===-:--=++XX+XXXXXXXXX++++=====++== XXX-++=---===++XX+:XXXXXX+=======+XXXX++=====+XXX++XXXXXXXXXXX++==+XXXXX++=-::::::-:::.....=\n",
|
||||
" ::.. \n .-:. \n .::. \n ..: .. \n : .:.: .. \n :.. . .::. \n :. .--.. \n .::. . .::. \n . :. . .. .:. \n -...:...:: :.. \n ..: . .:. .... \n .:-::. . . ..-. .:. . \n .::. .. ..:.. .. .. \n ... .:.... ....:....... .. :.. \n .:..:...: .:. ..=:.. ..... . .:-...+. \n .. .:=::::... .:. .::::. ..... .. . .=:.. \n .=... .-=:....... ::.. . .::::::--:.. .. .:-=-:.. ... \n . ...::.+. .-::......::-.--:.......... .-==+==--=+=:. .... ..:--:.. . :..:. \n .. ..::-+=-:.. ..=-::-...-==+==+-:--::-.....:++XX+:+ XX=-:. . .:==-=:. ...::---.. ..:...::. \n ..:.. ..+:-+XX++=::::-=-:....:---==++===--:::::::-=+XXXXXXXX+=-:... :=:-.. . .--==+=-::..:::-===+=:.:::::+-:. ... \n . ....::.. .:.::=++XX++==-==- ::-::--::==++=+=--:-=: ::--=XX=XX-XX++=--:-XX+===+-....:=+=++--::----=++=.++::::---:-:. ..... \n . ..::-----:..-::--==+X-X+++==--::--:-::-:-+X+=+=--::-=:::-==XX X-XXX++=+--=+XX+ ++=--..:-=++ ==--==+XXX++-+=--+++++==-:.............. \n ....::--====--::==++XXXXXXXXX++==-=--::-=++=X++=+.==-::-:--==+X+XXX:XX+===--=++XXXX++==----+XX+====+XXXXX=X++= =+XX+XX+=-:..:::::::..-.. \n .....+::--======--= XXXXXX.X-XX+XXXX+=---===+XX+=+XX.++=--===-++XXXXXX=X.-++====++XXX.X++---=++X=X=++XXXXX-XX.XX++++XXXXX+==-::===-- :::.....\n",
|
||||
" -.. \n .. . \n .. .. \n . ... \n . .:: \n .:. :. \n ..: .. \n ... : \n .:.. .: .. \n ..:.. .. . \n .:::.. . . . \n . .. .. . :. \n .. ... .. . .. .. . :. \n .:-:... .. :.. . .-. \n .-::.... ...-..:=:. .-:. \n . ..::.. . . ..-.. .. ..:....::-=:. .::.+.. . \n ..:-:. .-:. . .::=:--:..::.+.....:-=-----::-=:.. =.. ..::.. . . \n .. .:-++=-.. .-:.. ::-.-:-::=-:......:=+X:X+===+X+-:. .::--. . .:::-:.........: \n ... ...:=XX+==-:.:..-::.. .:--------=-:::...::-+XXXX++++X+--... ... .-----:......-: --+=:.......:. . \n .:... ..::-=+++=--:.:-::..:::::--==+=---:::-..=:=:=++XXX+.++==--::=+==-::.:. .:-+-=--:..:::.-==-==+=::.:::.:. . \n ....:::::....:::--=+X++====-::..::...:-:-+X=--=:-:-==:::--=+XXXX+X+==--:-++XX====-:....-==+=-:::--++++====-====-==--:. . ....... \n ..::::-:--:::=X=== =+X++:+++--:.::.-.:-==+-+=-==-::.::::--=+X-XX++++==--:-=++XX+====-:.:-=XX=--::-=+XXX++===.=XX+==+=-:..:-::.....-. \n ......::-----==--+X+=+X:XXX+++XXX==--:::.-==+XX++==++.-:: -- ==+X+XX+XX++=+=-==++.XX+=+=-:--=+++=--==+XXXXXXXX+===++X+++==:::-=-:::: :.... \n ...+..:::-=+=----===+++XXXX X+XXX+X+=---- ++=++X++XXXXX+=====+X+++++XX+XXX+++===+XXXXX+==---=+.++X++XXXXXXXXXXXX++++XXX++=--------::-- ::....\n",
|
||||
" \n . \n . .-. \n . \n . . \n .. . \n .. . \n .:. . \n .:.... \n .. \n . . . \n .:::.-.. . .. \n .:.. .:.. . \n ...... . . ....::. .:. \n .. .:. . .:..= ..... ...:.+..:=+. . ... \n .::-:. ..: .:..=:..:--:......::-:::-+:::-:.. ..- ..= \n ..--=--. . . .::..:..::.. . .:=++XX+==-=-=:. ..... :..-:. . \n .:==== -::.. ....-. .::.:-::.+::.-....:-=++XX=====--:. .. .::..: . .:::---:. . \n . . . .:=====-::.::++......:--+-=-:.:::......::=++XX+=+==--::.:==--:. ..-.:-::. . ..:--------:..+:... \n .. .........-:.:::-====-=:--:.. .....:::-==-:.::::::::::--++XX+==-=+--::==+XX=--::.. .:--==-:+..::--==------=-::--:.. .. \n ......::::..--=-:---=+==-===-:.. .. ..:====+-::-::..=:+:::-+XXX===+==-::-=+++X+=---::..--=+=-:...:-=.+++===-=+++=--=-:..:::.-.. .. \n ...:::::---:-==--=++X+-++=+==-::.:..::-=+X++=-=--=-..::-::-=+XX+=++=+=--:-=++XX+=-=-::-:=+==-::::-=+XXXXX+===++X+=:=---::-::...:.... \n ......::-.---:----==++XXX XXX++:++=--:::----=++XX.++++=:::-:-==+==+XX+XX++-=-==+XX++===-::=== -======+XXXXXXXXX+==+XX++=---=::-::=-::::=.. \n .. ..::-=----::-:===++.XXXX-XXX=XXXX==--.====+=++XXX.X++====+.++++ +X++XXX:+=.=++XX+:++==--==+ +:XX-XX-XXXXXX:XX++XXXX++=- ::-:-::::::::..=.\n",
|
||||
" \n \n . \n \n \n .: \n \n ... \n . \n -.... \n ::.. \n .. .... . \n . .. . ...:--: \n . . .. ..-.::.. ......:::-: .. \n .::=. .. . .::.. -...:-:--::..... . . ..: \n .::=:--:. . ...... ... .:-===+=:: :::. . . ....::. \n .::::::.. ... .. ...::=. .:. .:----+++--=::-.. ... . ...::::.. . \n . .:------....... . .::::-:...:. .:-==++==------::..-.--:. ...:::. . .:::::--:. ..-. \n .. :....:.:-----:.... .:-::--:.:. ... . .::-=+X+=------::+=+==+-::. .::--:.. ..:::-:::-:-:+..... \n ......:-..:::::::---=---=---:. . .:=+-=-:::....-.:.:::-+X+=----- -::-++=++--:::.+..::-.-:. .::-==++-=-====-::-::...... \n ...:...+::+::::::-+++ +=----::.......::-=+==-=-::-:. ...::-+++=- -=+=-:::-==++=-::-::=:.-=--::...:-++XXX++=-==++=--:::............ \n .. .+.:::::.::+----=XXXXXX++======:::..:--+-++.++-==--:.:---:-===++==++.=----=+X+==+--::-=-----------+XXXX:XX+====+-+=--::.:::..::::..= \n ......::---::.::-- -=+X=XXXX+:++++XX+-::-=====++XXXXX+=----==:====+XXX:XX X==-=++X==-+=-::=--=-=+X++++XX-XXX:XX+==+XX+==-::::::::::::::.. \n ......::--==---::-==+.+++XXXXXXXXXXXXX+==++X+ ==++XXXXXX+-==.+++XX ++XX+XXXXX++=+=+X++X+=-: --=++XXXXXXXXXXXXXXX+-+XXXXX+=--::-.-::-::::.....\n",
|
||||
" \n \n \n . \n . \n \n \n .. \n ..... \n . \n .=. \n . .. ..::: \n .:.. . .::. .... .: . \n .... . .. .:::::.. .. . \n . ..... . . . .-::---:..+. .... \n ......... . .:+: . ..-::-----:::... .. . . ..+.:. \n .....:.. ...::...+ .::--=+=-:::::-:...:::-: ::. ...::-:.. . \n ....:::::::- . . .:=:.::.... .::-=X+--:-::-=-==:--:-:.. ...::.. ..:-:::::...... \n :.. .. -...:.:---::::.::. ..----:::: ........:-=X=-::::.::.:==-----:.+. .:-::. ...: ------.--:.::. . \n ...:...:.....:-=+X+=--::.:.. .- -=-.-:..-::. . ..:=== --::-=-:..:-==+=-:..::-...=::::.. ..-====+==-----=-:::::... . . \n .......::.::.::=++XX-X+=-+--=-::. ..:-=-=+===--:::.. ::-:-=====- ===-:.::-+X=-- ::::-=:::--.:..:-+XXXXXXX+= ===--::::............. \n .....:: :::..--::-==++XX:X====+++==:.=.:-==.=+XXX+=--::::-----==+XXXX-++=-::=++=.---::..::.--=++=--=+XXXXXXX++===++=--::.::........... \n ....:::----::-:--.===-++XXX++X++X+X++-:-=++:===+XX-X+==--===++++++XXXXXXXX+=--==+==++=-:.-::==+XX.-XX:XXXXXXX++=++X++X+-:::::::::::..+... \n .....::---===---=+XX:+++X+++XXXXXXXX++===+XX+-==+XXXXXX=+++XXX:XX.X+XX+:XXX++=++==+XXX+=--:--=+++XX:XX-+XXXXX:++=+XXX+XX+=-:-===-:::::.=....\n",
|
||||
" \n \n \n \n \n .. \n \n \n \n :. .. \n :. . .... \n . . .... . \n .. ...:.. . . \n . .. ....=::.. . .. \n . .. . .:-:-:....... :.. .. ..:. \n ..: ... .:..... .::+:-=-::..:::+.....=:. .. .=.:. \n ...::.+... ..--:.. .. .::--=-::....::=-:--:.:. ... -.....:..-. .. \n .. ..:-=--:: .. .. ...--::+:. .. ..:-=--:.. ..:.:--::-::. . ..::.. ....::..::..-.... \n ..........:+==--.:.:..:. .:.:-+-::.. ... .:--=--+:::::. .:--+=-:...:. ..::.. ..::--==--::::::..... \n :......:..:...:-+=:==+--:::+=:.. ...-==+==-:.-.. ...:---===---=-:-..:-=+-:...=.:::::::::....-=+X++++.+=--+-::...... .. \n :...+....::-::-=====:++=======+-:. ..:-==+.XX++-:....:+::--==+XX++-=-::.:==+--::+......:--++=-::-++XXX.++X ==+=--::..::=:+....... . \n ...+.::--:::+===+= ==+++X+=+.+==++=:.::-====+XX++==--::--==++=+XX.XX=XX+-::--==-==--.....:-=XXX +=++XXXX.+X+++++=== =::.::-::........ \n ....::+:-=----=+ ====+:++=+++:++++= =---+X+==++XX++====-=+XXXXX+XXX.X-XXX++--+==+X++=-:..::-++XX++XX+XXXXXX++==++++=+X=--:-:-::..:...... \n . ....::--=+++==+X=++=+===+++XXXXX++:==+++++XX++XXXX+++++-+X.XXXXXXXX+-XXX:++++X+=XXXX-+=-:--=++ XXXXXXXXXXXX++=-+++XXXXXX=-==---+::...:. ..\n",
|
||||
" \n \n \n \n \n \n \n . . \n .. \n .. \n .. \n . . . \n . . .:.. . \n .. . .--:...:. .. =.. .. \n . .. .::... . ..-::....-: .. . . . \n .:-::...: --::. ..-:-:.... .:.:...=. . . -.... \n .--:::::.. .:--:. .::-:.::.. .....:-::-:. . .. ........ \n . ..:- --:--....=. .:-+-:.. .::--:-:..... =:::-:.. . .. =..:-=-:.:..... \n .. ...... -----=--:::--.. .:-=+++-:.. ..:-----+::-:. .::--:.. ......:::.. .:=:===--=+-::::.. ... \n .+.. :...:--:::---+==+=--==--=-. ..:-=+XX+==:. .::.-=-=XX++=--:.. .: -- ::.. .+.:-+X=-:.:--=++-===+==-=--:..+...::.. \n ......::..::=-------====:-+===-==-.. .:---=+X+==-::::: --==X=XX XXXXX=-:.:------:::. ..::+X+ ==--=++X++=++==+=--:--:..:::..... . \n .....::=--::=------==-= ====++==:--:::=+===++X+==--:-::=++XX -:XXXXXXX+-::---=++=-:.. .:-=++==+===+XXXXX+===++=--=+=-::-::.+.....: \n ....::- ===--======-+-===++XXXX+=---+====+X++:XX++++=--=+XXXXXXXXXXXXXX++==+X++XXXX+-::.:--==+X++++=XXXX.++=====++ XX++=:--::::..+.. .. .\n ....:::-:==++==== ==+-===+=+XXXXX+==-= =+==XXXXXXXX++XX===+XXXXXXXXXXXXXXX+==+++-XXXXX =--===++XXX=X.X.XXXXX++:==++XX+XX+=.--:::.:.........\n",
|
||||
" \n \n \n \n \n \n \n \n . \n \n \n ..- \n .. .::. . . \n .:. .::.......... \n .:... -:..+ . .:...=. .. \n .::..:.. .:-:. ..=::.:... ..........: . \n ..: -:::::....-. .:--:.. ..::---:..... ::..-:. . .::...:. \n ....-::--:-=:::.::. ..:.==::. .::----::: .. .:.::..: .... .::--:::-:... . \n ...::::..::=-=---.--:--:. .:--XX+=-:.. ..::-=====--:=:. ..:-::.+. ...:==-:-..:- -=----==-:::...-.. .. \n . ......::-:::::::- -=---.---=-:. ..:--+X+==--::. ..::-=+X+XXXXX+=:....:-----:.. ..:-XX==-::---====-=+=---:::......:... \n .-....:-::-:--:--: --==---.--==-- .. :-=--==++=--::::::--==+X XXXXX-X+-:::=--:=-:::. ..:-=+=--=--==+++==++==+=-+--=-:.::..... \n ....:::---:-----=--=====-===-+==----+--=++==+X====-::--=++XXXXXXXXXXXX+=+--==+XX+=-:....:-==+====+=+XX+X+==+==-==.===-::::::.:.... \n ...+:---===---.======-===+X:XX++==-----==+XXXXXX+==----==+XXXXXXXXXXXXX++=+X+ +XXXX+=-:+:--=+++++X++XXXXX+++=====+X++.==-::-::......... .\n .....::-==++.==-==-++=+==+++XXXXXX+========XXXXXXXX-+++++XX-X:XXX+XXXXXX ++=++==+XXXX:X+==.=++XXXXXXXXXXXXXXX+===:+XX=XX+=--:-::::.........\n",
|
||||
" \n \n \n \n \n \n \n \n \n \n . .. \n . .. \n . ..+ \n .. ..:.. ....... \n ....-..- . :-:. . ...::.=. .. ... \n ...=..+.::. .::. : :.. ..::-::....... . ..: .... \n .. ........:::::..::. ..:-=-:.. ..:-----::..-... .... :... .:.:.....:. .. \n ........:.+.::.::.:-:. ...:=+==-:..- ..:-=+X+=--::..+ . ....:. .:.:=-:.. .:..::::::-:. ... . \n ............::::::--::-=-.. ..:-=++=--:::... ..::-=+:XX+-+=-:.....::-::.. .:==-:-::+::::----.-=-::..:.....+. \n .....:..::::::::::--:+::::--.=:..-:-:-==:=+=-:..+.:.::-==+XXX++XX+-:..::-==--:.. ..:-==--:::-=-==-=--=--+-::::::.... . \n ......: ::.::::.---.-=---.=-+-----:::::--++====--:..-::-==++XXXXXXXX+=--::-=+XX+=-:.....:--=------==X==:==--=-==-----:......... \n .:.:::.---::.--+:--+-==++======-:--::---=+X-+++==-.::-=++++:XXX:XXXXX+======++XXX+=-::.::-========++X++++==+====++==--::... -..... \n .....:--:===+------=--====+XX.++X+=-==---=+XXXXXXX+==----=++:X:X=XXXXXXX+=++===+X.X++==---=++XX+++ XXXX==XX++=-==+XXX++=-::::::.......... \n ......::-=.=+ ==--=++++====+X=XX:XXX+ +X==++++X XXXXX++===-+XXX.XXXXX.-XX++=+==.=+XX-+++:+=++XXXXXX.XX.XXXXX+.X+=+++XXX X+=--::--::.........\n",
|
||||
" \n \n \n \n \n \n \n \n .. \n \n \n \n .: .-.. \n . .... .. ::.. \n ..... :.. .::. ...::::......... \n . ...:.. ..:. ..= ::.. ....:--==-- ::.. ... ... . .. \n ... .......::. ..:----::=..= ..:-+XX= =--:... ..=.::. .=.:.:.. .. ..=...::. \n . +......:.........:: . ..:-----=::.. ..:--=XX+====::.=....::::. ..:--::=....-....:::::::.... .. \n .... .. .=.::-::::::....:::..=.::-------::....:.:---=++===++==-:..:-==--::.. .:----::::::::--::--+:::::....=.. \n ...............::----- -:: ::::.:=:::-===----:.=..::-=-=+X+X++++==-:::=-+XX+=--. ...:::--:::=::-=--=---:----.::.::. . \n ....::::....::::+:---:==---=-=-::+::::--=X+===-:+:..::-==++X XXXXX+==:--=-=+X+=+=-::..::------:--==+=++==---====---::.-.... \n ...:::--:-:::::------===++====++-----++-=+X+X+=:=-:.:::-=+-+XXXXXXX++++-==-=+-+===---:: -=+++== =:+++XX+++===+=+X+==--::... .-... \n ..+...:----------=========XX=X++-+X+===---==++=XX+==+==--==+++XXXXXXXXX+++==--==+X++===---++XXXX+.++XXXXXXXXX++===+XX++==--:::::.-.......- \n ....=..::-========-=:++++ +++XXXXXXXX++=====+-=++XXXXX+++++XX++XX:XXXXXX.X+======+XX.+++++++==+X:XXXXXXXX+X:XXX-X+++XXXX++==--:--:::::.......\n",
|
||||
" \n \n \n \n \n \n \n \n \n \n . \n .. ..... \n . .::..:. \n .. .:.. .. ..-:-:::.. .. \n . .-. ....... . ..:::=---::::.. ... . ... \n ... . ... .:::...... .. ..:--++=-----:. ...:.. .. ...... . . \n :..: ..:. ... ..:=--::.... . .:: -=++==-=---:=....:::... .:::..... . +....... \n ..:-:.::.. .. ..-...:-:-- :::.....::---+====- ----::..:--=:-:.. .:--::.......+=:.:::........ \n . . ..:-:::::+.....+=.::-...-=--::::....:.::- -+=====+=+-::+:-=-++=--:. ..:-::.:.. ..::=::-::=-:::::..... \n ...............::------+::::::..::::::-=+==-::... .::--=+X+++ +X+=-: :--++==---::...::=--::.=.::--====--:--==-::.... \n ..:-::::..-.::-:-=-- =+==--:--=-:::::---=++=-::::....::-=++XXXX++++=+=-.--===.---: ::.:-=++=--:-==+++++-==-=-=+==--::.......... \n ...:::.:::::-:----- ==+XX+=====++---.:--=++++=---=-+::---=+XXXXXXXX+==--:---=++===- ::---+X++=-===++XXXX++=-==+++==--::::.:...... .:. \n ....:.::----------===-===+XXX X++ +X+===--+-==:+XX+==:====++=+++XX.XXXXX+===--==+XX+= ==--===+XX+++++X=XX XXXXX+++++X++==-.-:+:::.......... \n ..=...::---.-==+=+==++++++++-=XXXXXX X++++:+==:=++XXX++ +XXXX+XXXXXXX+.+XX++===+XXXX=X+.==--:==+XXXXXXXXXXXXXXXXX+++XX=++++-=-----.::........\n",
|
||||
" \n \n \n \n \n \n \n \n \n :... \n .:..: \n .. .:.:..+. \n .. . .. ...::.:.. . .. \n . .:-:-:--:...:. . . \n .. . .:..... . .::+===--:.::-:=.. ...:.. . \n .. . .:=-:::.. -.. .::-+===--::---:.....::+.. .:.... . ... \n .:...... ...::--::-:..... ..::==-=--::-:-.::.::--:::... .--::.. . .... . \n .::::........ ......----... .. .:+::==-------=-::::-===-::::. .::--:. .. ......::........ \n .. ..+.::-::--:..:.:...+..:..::==-:....: .::::-++========::.:-+=.=-::::.-..:-.-:::. ...::-----:: ---::.. \n .....-.+..:.:.:::--+==--::::-:-:..:::::-=--:+...+...:+:-=++X++== =--:::-+--=-:::..:..:-==--::.:--=+++==-----==--::.. . \n .....-.=..:::-:::--=-=+=-----:-=::.: ::--==--:::::..::+--+XXXX-====--:.::-=-==---::.::::=+==-----=+XX++-==:-===.--::........ \n ....-:::::::.------= -=+X++===+=.==-- --:--==++=-+--::-:====+XXXX+-+==-::=--=++==---:.:--==++==--=++ XXXXX++===+++===-::-:::....+:... \n ....-.::-:------====+=.=++:XX+X++++ ===+ ======+XX+==-== ++++++X+XXXXX=+==-==++XXXX==--::+--==XX++-+X XXXXXX:++++++++====-+-::::::........ \n ...=.::--=-=== ===++XXX++XXXXXXXXXX+XXX:++======+X XX+.++X++XX+++X+XX++XXX++=++ XXXX+===--:--=++XXXXXXXXXXXXXX++++XX+X+++.=-------::::......\n",
|
||||
" \n \n \n \n \n \n \n :.... \n . . \n . ....- \n .:..:::::.. \n .:..+.::... . \n =::::.::... . +...::.. \n .. .:-==-:-:....::... .::. \n .:-::... .:-+=---::..:::......::.. ....+ . \n . . :..:-:::.... .:--=--::::::::-:.::+:-:.. :=-::. .. \n ....... ..::-:::.. .:::=--::::::.-:---=---:...:.:.:--:...- ..::..=. ..... \n . ..:::::::..... .. ...-:-::. ...-::-+=-=-::::::.:-+--=--::.....::-::... -..:------:.:.:... \n .. . ....:=::-::-:-...:..:..::..:--::. . .:::--+ +=----::.=.:-=:--::.+....+.-=::.=...:-=:==--+- ---::=... \n ............::::::--:-+-::=:::-:..=:...:--:::.... ..+---=++X+=-=-::..:.:==:=--:....::::==--::---=++.====--=---=:-:..: ... \n .....::::-::.:--::---====-------::.:::.---==--:.: ..-==--==-++====-::...::==----=::.:.::-==-::--=+=++++++======----: :. .... \n ....-:=::::: --:==----=+==++=+==+=- --=.=+=.==X+=--:: -===+==+++X+-+==-::--==.++=-:::. ..:-==++===+++XX+++X====+===----:::::.:...:.. \n ....::---------==:++==+++++++++X+=++XX+=======+XX++ ===+==-++==++X XXX++==++++++++=--:.:.::-==XX++.XXXXXXXX++=++:XX++==--::+: :::.... . \n .. .::---=========+X++XXX+XXXXXXX++:XXXX+=+=== =+X=X++X+-+.+++++XXXX++XXXX+++==++XX+==----- -=+XXXXXXXXXXXXX+++++X-XXX++=--------.:::......\n",
|
||||
" \n \n \n \n \n \n . \n . . \n :. .:.. \n .. .. ..::. \n . .....:: ..:. \n .-:...-.:.. . .. .. \n . .:-=-::... ...+. :+.. \n ... .:-+=-::::. ........:--.. ..-.. \n . .=-:.... .::==::::......-::::..-:. .::... \n .... ..::=. .:-==:=:.......::---::-. .:-::::. ........ \n ........ . ..=.:. .=::==--:+......:-+.--::. .+. ..::-:. .:-::::-:::.:. \n .....:-:.::-.. .. .. :. ..:.. .::--+=--:. .. ..:=::=::.......::-:.. ..:-----:::-=-:.:.:. \n ...... .::::::::.::--...... .....:-:.. .::--=+=.=::.... ..:=::.:-..:....::--+....:-=+=--==-==-::::::... \n ....-:.....::::::::::--- :::::.=....::--==-:.. . ..:=--+====--::.......-+-:.::+.. ...:=-:..::-.=======+-==----:::...: .. \n ......::...:-::--::--------==-=-:::---=++==++=-::-..:--:-=:==++===-:..::::==- ::.... .--=+.---=+X===+==+==+= ----::........ \n ...=:::-::::--=-==-=+==--====+++-:===-=-==.++X+=----=---======+XXXX+=-:-===++==--::. . .:--X+===.:XX+.+=+===+++====-::.::-:.+.... \n ...:::------+=-==+.++X+=++XXXXXX++=+X+===+=-==+XX++=+=++ =====+XXXXXX +===+==++X+==::....::-++X ++XXXXXXX+===-=++-+++=-=--=::::::=.-.... \n ...:-----=----==+X++XXXXXXXXXXX+XX+++++==+==-==+XX.+=++-=====+++XX:XXX-X+=+++=+XXX+=--======+++XXXXXXXXXX+===+++XX++++=+------::::=:.....\n"
|
||||
]
|
||||
@@ -0,0 +1,64 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { setIntervalOnVisible } from "@/utils/set-timeout-on-visible";
|
||||
import data from "./hero-flame-data.json";
|
||||
|
||||
interface HeroFlameRightProps {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
hideOnMobile?: boolean;
|
||||
}
|
||||
|
||||
export default function HeroFlameRight({
|
||||
className = "",
|
||||
style,
|
||||
hideOnMobile = true,
|
||||
}: HeroFlameRightProps) {
|
||||
const ref2 = useRef<HTMLDivElement>(null);
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let index = 0;
|
||||
|
||||
const interval = setIntervalOnVisible({
|
||||
element: wrapperRef.current,
|
||||
callback: () => {
|
||||
index++;
|
||||
if (index >= data.length) index = 0;
|
||||
|
||||
if (ref2.current) {
|
||||
ref2.current.innerHTML = data[index];
|
||||
}
|
||||
},
|
||||
interval: 85,
|
||||
});
|
||||
|
||||
return () => interval?.();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
"cw-686 flex gap-16 pointer-events-none select-none",
|
||||
hideOnMobile ? "lg-max:hidden" : "",
|
||||
className,
|
||||
].join(" ")}
|
||||
style={style}
|
||||
ref={wrapperRef}
|
||||
>
|
||||
<div className="flex-1 overflow-clip relative">
|
||||
<div
|
||||
className="text-black-alpha-20 font-ascii absolute bottom-0 -right-380 -scale-x-100 fc-decoration"
|
||||
dangerouslySetInnerHTML={{ __html: data[0] }}
|
||||
ref={ref2}
|
||||
style={{
|
||||
whiteSpace: "pre",
|
||||
fontSize: "9px",
|
||||
lineHeight: "11px",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
import { setIntervalOnVisible } from "@/utils/set-timeout-on-visible";
|
||||
import data from "./hero-flame-data.json";
|
||||
|
||||
export default function HeroFlame() {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const ref2 = useRef<HTMLDivElement>(null);
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let index = 0;
|
||||
|
||||
const interval = setIntervalOnVisible({
|
||||
element: wrapperRef.current,
|
||||
callback: () => {
|
||||
index++;
|
||||
if (index >= data.length) index = 0;
|
||||
|
||||
if (ref.current) {
|
||||
ref.current.innerHTML = data[index];
|
||||
}
|
||||
|
||||
if (ref2.current) {
|
||||
ref2.current.innerHTML = data[index];
|
||||
}
|
||||
},
|
||||
interval: 85,
|
||||
});
|
||||
|
||||
return () => interval?.();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="cw-686 h-190 top-408 absolute flex gap-16 pointer-events-none select-none lg-max:hidden"
|
||||
ref={wrapperRef}
|
||||
>
|
||||
<div className="flex-1 overflow-clip relative">
|
||||
<div
|
||||
className="text-black-alpha-20 font-ascii absolute bottom-0 -left-380 fc-decoration"
|
||||
dangerouslySetInnerHTML={{ __html: data[0] }}
|
||||
ref={ref}
|
||||
style={{
|
||||
whiteSpace: "pre",
|
||||
fontSize: "9px",
|
||||
lineHeight: "11px",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-clip relative">
|
||||
<div
|
||||
className="text-black-alpha-20 font-ascii absolute bottom-0 -right-380 -scale-x-100 fc-decoration"
|
||||
dangerouslySetInnerHTML={{ __html: data[0] }}
|
||||
ref={ref2}
|
||||
style={{
|
||||
whiteSpace: "pre",
|
||||
fontSize: "9px",
|
||||
lineHeight: "11px",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user