diff --git a/apps/sim/app/(landing)/components/mothership/components/iso-marks/index.ts b/apps/sim/app/(landing)/components/mothership/components/iso-marks/index.ts index 19316f0e0ec..d08e1cc7e12 100644 --- a/apps/sim/app/(landing)/components/mothership/components/iso-marks/index.ts +++ b/apps/sim/app/(landing)/components/mothership/components/iso-marks/index.ts @@ -2,22 +2,6 @@ export { IsoBuildIllustration, type IsoBuildIllustrationProps, } from '@/app/(landing)/components/mothership/components/iso-marks/iso-build-illustration' -export { - IsoCubeGrid, - type IsoCubeGridProps, -} from '@/app/(landing)/components/mothership/components/iso-marks/iso-cube-grid' -export { - IsoCubeRow, - type IsoCubeRowProps, -} from '@/app/(landing)/components/mothership/components/iso-marks/iso-cube-row' -export { - IsoFourBox, - type IsoFourBoxProps, -} from '@/app/(landing)/components/mothership/components/iso-marks/iso-four-box' -export { - IsoGridPlane, - type IsoGridPlaneProps, -} from '@/app/(landing)/components/mothership/components/iso-marks/iso-grid-plane' export { IsoIngestIllustration, type IsoIngestIllustrationProps, @@ -30,11 +14,3 @@ export { IsoMonitorIllustration, type IsoMonitorIllustrationProps, } from '@/app/(landing)/components/mothership/components/iso-marks/iso-monitor-illustration' -export { - IsoStackedPlanes, - type IsoStackedPlanesProps, -} from '@/app/(landing)/components/mothership/components/iso-marks/iso-stacked-planes' -export { - IsoStar, - type IsoStarProps, -} from '@/app/(landing)/components/mothership/components/iso-marks/iso-star' diff --git a/apps/sim/app/(landing)/components/mothership/components/iso-marks/iso-cube-grid.tsx b/apps/sim/app/(landing)/components/mothership/components/iso-marks/iso-cube-grid.tsx deleted file mode 100644 index b15e732ac9d..00000000000 --- a/apps/sim/app/(landing)/components/mothership/components/iso-marks/iso-cube-grid.tsx +++ /dev/null @@ -1,153 +0,0 @@ -'use client' - -import { cn } from '@sim/emcn' -import { GooDefs } from '@/app/(landing)/components/mothership/components/iso-marks/goo-defs' -import { - type Edge, - gradientForTone, - isoProject, - type MarkState, - type Pt, - TARGET, - useGooMark, - useMarkIds, -} from '@/app/(landing)/components/mothership/components/iso-marks/use-goo-mark' - -/** - * Sim iso goo-mark: CUBE GRID. - * Nine small iso cubes tiled in a 3×3 screen grid. Rest: the nine sit spread - * apart. Hover (gather): they pull in toward the center into a snug grid - still - * clearly nine separate cubes, never merged into a blob. No spin; the motion is - * pure convergence. - */ -interface GridState extends MarkState { - gap: number - tilt: number - tone: number -} - -const REST: GridState = { gap: 3.3, tilt: 0.5, tone: 1 } -const HOVER: GridState = { gap: 2.05, tilt: 0.5, tone: 1 } - -const GRID = 3 -const U = 0.5 -const STROKE = 2.4 -const GOO_FUSION = 0.55 - -/** A unit iso cube, projected to screen space and centered on the origin. */ -function unitCubeEdges(ky: number): Edge[] { - const corner = (sx: number, sy: number, sz: number) => isoProject(sx * U, sy * U, sz * U, ky) - const c = [ - corner(-1, -1, -1), - corner(1, -1, -1), - corner(1, 1, -1), - corner(-1, 1, -1), - corner(-1, -1, 1), - corner(1, -1, 1), - corner(1, 1, 1), - corner(-1, 1, 1), - ] - const ed: [number, number][] = [ - [0, 1], - [1, 2], - [2, 3], - [3, 0], - [4, 5], - [5, 6], - [6, 7], - [7, 4], - [0, 4], - [1, 5], - [2, 6], - [3, 7], - ] - return ed.map(([a, b]) => [c[a], c[b]] as Edge) -} - -function buildEdges(s: GridState): Edge[] { - const base = unitCubeEdges(s.tilt) - const edges: Edge[] = [] - for (let i = 0; i < GRID; i++) { - for (let j = 0; j < GRID; j++) { - const dx = (i - (GRID - 1) / 2) * s.gap - const dy = (j - (GRID - 1) / 2) * s.gap - for (const [A, B] of base) { - edges.push([ - [A[0] + dx, A[1] + dy], - [B[0] + dx, B[1] + dy], - ]) - } - } - } - return edges -} - -function normalizeEdges(edges: Edge[]): Edge[] { - const pts = edges.flat() - let minx = Number.POSITIVE_INFINITY - let maxx = Number.NEGATIVE_INFINITY - let miny = Number.POSITIVE_INFINITY - let maxy = Number.NEGATIVE_INFINITY - for (const [x, y] of pts) { - if (x < minx) minx = x - if (x > maxx) maxx = x - if (y < miny) miny = y - if (y > maxy) maxy = y - } - const w = maxx - minx || 1 - const h = maxy - miny || 1 - const scale = TARGET / Math.max(w, h) - const ox = 50 - ((minx + maxx) / 2) * scale - const oy = 50 - ((miny + maxy) / 2) * scale - const tx = (p: Pt): Pt => [ox + p[0] * scale, oy + p[1] * scale] - return edges.map(([A, B]) => [tx(A), tx(B)] as Edge) -} - -function edgesToD(edges: Edge[]): string { - let d = '' - for (const [A, B] of edges) { - d += `M${A[0].toFixed(2)} ${A[1].toFixed(2)} L${B[0].toFixed(2)} ${B[1].toFixed(2)} ` - } - return d.trim() -} - -export interface IsoCubeGridProps { - size?: number - className?: string - forceHover?: boolean -} - -export function IsoCubeGrid({ size = 110, className, forceHover = false }: IsoCubeGridProps) { - const { current, bind } = useGooMark({ rest: REST, hover: HOVER, forceHover }) - const { gradId, gooId } = useMarkIds() - - const edges = normalizeEdges(buildEdges(current)) - const { from, to } = gradientForTone(current.tone) - - return ( - - - - - - - ) -} diff --git a/apps/sim/app/(landing)/components/mothership/components/iso-marks/iso-cube-row.tsx b/apps/sim/app/(landing)/components/mothership/components/iso-marks/iso-cube-row.tsx deleted file mode 100644 index d077f28afc2..00000000000 --- a/apps/sim/app/(landing)/components/mothership/components/iso-marks/iso-cube-row.tsx +++ /dev/null @@ -1,146 +0,0 @@ -'use client' - -import { cn } from '@sim/emcn' -import { GooDefs } from '@/app/(landing)/components/mothership/components/iso-marks/goo-defs' -import { - type Edge, - gradientForTone, - isoProject, - type MarkState, - type Pt, - TARGET, - useGooMark, - useMarkIds, -} from '@/app/(landing)/components/mothership/components/iso-marks/use-goo-mark' - -/** - * Sim iso goo-mark: CUBE ROW. - * Three cubes set in a level row. Rest: an even row, all one size. Hover (read): - * each cube resizes to a different scale, like a live gauge re-leveling. No - * spin; the motion is pure isometric scale. - */ -interface RowState extends MarkState { - s0: number - s1: number - s2: number - tilt: number - tone: number -} - -const REST: RowState = { s0: 0.62, s1: 0.62, s2: 0.62, tilt: 0.5, tone: 1 } -const HOVER: RowState = { s0: 0.95, s1: 0.55, s2: 0.8, tilt: 0.5, tone: 1 } - -const SLOTS = 3 -const SPREAD = 0.92 -const STROKE = 2.4 -const GOO_FUSION = 1.0 - -function cubeAt(cx: number, cy: number, half: number, ky: number): Edge[] { - const corner = (sx: number, sy: number, sz: number) => - isoProject(cx + sx * half, cy + sy * half, sz * half, ky) - const c = [ - corner(-1, -1, -1), - corner(1, -1, -1), - corner(1, 1, -1), - corner(-1, 1, -1), - corner(-1, -1, 1), - corner(1, -1, 1), - corner(1, 1, 1), - corner(-1, 1, 1), - ] - const ed: [number, number][] = [ - [0, 1], - [1, 2], - [2, 3], - [3, 0], - [4, 5], - [5, 6], - [6, 7], - [7, 4], - [0, 4], - [1, 5], - [2, 6], - [3, 7], - ] - return ed.map(([a, b]) => [c[a], c[b]] as Edge) -} - -function buildEdges(s: RowState): Edge[] { - const sizes = [s.s0, s.s1, s.s2] - const edges: Edge[] = [] - for (let i = 0; i < SLOTS; i++) { - const t = (i - (SLOTS - 1) / 2) * SPREAD - edges.push(...cubeAt(t, -t, sizes[i], s.tilt)) - } - return edges -} - -function normalizeEdges(edges: Edge[]): Edge[] { - const pts = edges.flat() - let minx = Number.POSITIVE_INFINITY - let maxx = Number.NEGATIVE_INFINITY - let miny = Number.POSITIVE_INFINITY - let maxy = Number.NEGATIVE_INFINITY - for (const [x, y] of pts) { - if (x < minx) minx = x - if (x > maxx) maxx = x - if (y < miny) miny = y - if (y > maxy) maxy = y - } - const w = maxx - minx || 1 - const h = maxy - miny || 1 - const scale = TARGET / Math.max(w, h) - const ox = 50 - ((minx + maxx) / 2) * scale - const oy = 50 - ((miny + maxy) / 2) * scale - const tx = (p: Pt): Pt => [ox + p[0] * scale, oy + p[1] * scale] - return edges.map(([A, B]) => [tx(A), tx(B)] as Edge) -} - -function edgesToD(edges: Edge[]): string { - let d = '' - for (const [A, B] of edges) { - d += `M${A[0].toFixed(2)} ${A[1].toFixed(2)} L${B[0].toFixed(2)} ${B[1].toFixed(2)} ` - } - return d.trim() -} - -export interface IsoCubeRowProps { - size?: number - className?: string - forceHover?: boolean -} - -export function IsoCubeRow({ size = 110, className, forceHover = false }: IsoCubeRowProps) { - const { current, bind } = useGooMark({ rest: REST, hover: HOVER, forceHover }) - const { gradId, gooId } = useMarkIds() - - const edges = normalizeEdges(buildEdges(current)) - const { from, to } = gradientForTone(current.tone) - - return ( - - - - - - - ) -} diff --git a/apps/sim/app/(landing)/components/mothership/components/iso-marks/iso-four-box.tsx b/apps/sim/app/(landing)/components/mothership/components/iso-marks/iso-four-box.tsx deleted file mode 100644 index 9b1f52cd4cb..00000000000 --- a/apps/sim/app/(landing)/components/mothership/components/iso-marks/iso-four-box.tsx +++ /dev/null @@ -1,162 +0,0 @@ -'use client' - -import { cn } from '@sim/emcn' -import { GooDefs } from '@/app/(landing)/components/mothership/components/iso-marks/goo-defs' -import { - type Edge, - gradientForTone, - isoProject, - type MarkState, - type Pt, - rotate2, - TARGET, - useGooMark, - useMarkIds, -} from '@/app/(landing)/components/mothership/components/iso-marks/use-goo-mark' - -/** - * Sim iso goo-mark: FOUR-BOX TWIST. - * Four wireframe boxes layered vertically, each rotated at a progressive angular - * offset so the stack twists into a rounded cluster. Rest: open and twisted, - * still. Hover (close + spin): gap collapses, twist unwinds, spins. An optional - * signal-blue accent box is off by default (the landing stays greyscale). - */ -interface FourBoxState extends MarkState { - gap: number - twist: number - spin: number - tilt: number - tone: number -} - -const REST: FourBoxState = { gap: 1, twist: 11, spin: -0.38, tilt: 0.4, tone: 1 } -const HOVER: FourBoxState = { gap: 0, twist: 0, spin: -3.14, tilt: 0.4, tone: 1 } - -const BOXES = 2 -const STROKE = 2.4 -const GOO_FUSION = 1.4 -const BLUE = '#9FC6E8' - -function boxEdges(s: number, ky: number, rot: number, zc: number): Edge[] { - const corner = (sx: number, sy: number, sz: number) => { - const [rx, ry] = rotate2(sx * s, sy * s, rot) - return isoProject(rx, ry, sz * s * 0.4 + zc, ky) - } - const c = [ - corner(-1, -1, -1), - corner(1, -1, -1), - corner(1, 1, -1), - corner(-1, 1, -1), - corner(-1, -1, 1), - corner(1, -1, 1), - corner(1, 1, 1), - corner(-1, 1, 1), - ] - const ed: [number, number][] = [ - [0, 1], - [1, 2], - [2, 3], - [3, 0], - [4, 5], - [5, 6], - [6, 7], - [7, 4], - [0, 4], - [1, 5], - [2, 6], - [3, 7], - ] - return ed.map(([a, b]) => [c[a], c[b]] as Edge) -} - -function buildBoxes(c: FourBoxState): Edge[][] { - const twRad = (c.twist * Math.PI) / 180 - const totalH = (BOXES - 1) * c.gap - const boxes: Edge[][] = [] - for (let i = 0; i < BOXES; i++) { - const zc = i * c.gap - totalH / 2 - const rot = c.spin * i * 0.5 + i * twRad - boxes.push(boxEdges(1.0, c.tilt, rot, zc)) - } - return boxes -} - -function normalizeBoxes(boxes: Edge[][]): Edge[][] { - const pts = boxes.flat().flat() - let minx = Number.POSITIVE_INFINITY - let maxx = Number.NEGATIVE_INFINITY - let miny = Number.POSITIVE_INFINITY - let maxy = Number.NEGATIVE_INFINITY - for (const [x, y] of pts) { - if (x < minx) minx = x - if (x > maxx) maxx = x - if (y < miny) miny = y - if (y > maxy) maxy = y - } - const w = maxx - minx || 1 - const h = maxy - miny || 1 - const scale = TARGET / Math.max(w, h) - const ox = 50 - ((minx + maxx) / 2) * scale - const oy = 50 - ((miny + maxy) / 2) * scale - const tx = (p: Pt): Pt => [ox + p[0] * scale, oy + p[1] * scale] - return boxes.map((bx) => bx.map(([A, B]) => [tx(A), tx(B)] as Edge)) -} - -function edgesToD(edges: Edge[]): string { - let d = '' - for (const [A, B] of edges) { - d += `M${A[0].toFixed(2)} ${A[1].toFixed(2)} L${B[0].toFixed(2)} ${B[1].toFixed(2)} ` - } - return d.trim() -} - -export interface IsoFourBoxProps { - size?: number - className?: string - forceHover?: boolean - /** Render one box (the 2nd from bottom) in the signal-blue accent. */ - blueAccent?: boolean -} - -export function IsoFourBox({ - size = 110, - className, - forceHover = false, - blueAccent = false, -}: IsoFourBoxProps) { - const { current, bind } = useGooMark({ rest: REST, hover: HOVER, forceHover }) - const { gradId, gooId } = useMarkIds() - - const boxes = normalizeBoxes(buildBoxes(current)) - const { from, to } = gradientForTone(current.tone) - const blueIdx = blueAccent ? 1 : -1 - const normal: Edge[] = [] - let blue: Edge[] = [] - boxes.forEach((bx, i) => { - if (i === blueIdx) blue = bx - else normal.push(...bx) - }) - - return ( - - ) -} diff --git a/apps/sim/app/(landing)/components/mothership/components/iso-marks/iso-grid-plane.tsx b/apps/sim/app/(landing)/components/mothership/components/iso-marks/iso-grid-plane.tsx deleted file mode 100644 index 18012300aa3..00000000000 --- a/apps/sim/app/(landing)/components/mothership/components/iso-marks/iso-grid-plane.tsx +++ /dev/null @@ -1,82 +0,0 @@ -'use client' - -import { cn } from '@sim/emcn' -import { GooDefs } from '@/app/(landing)/components/mothership/components/iso-marks/goo-defs' -import { - type Edge, - edgesToPaths, - isoProject, - type MarkState, - rotate2, - useGooMark, - useMarkIds, -} from '@/app/(landing)/components/mothership/components/iso-marks/use-goo-mark' - -/** - * Sim iso goo-mark: GRID PLANE. - * A single flat lattice rotated 45deg and squashed into isometric. - * Rest: tilted, still. Hover (open + spin): tilt flattens, spins. - */ -interface GridState extends MarkState { - tilt: number - spin: number -} - -const REST: GridState = { tilt: 0.5, spin: 0 } -const HOVER: GridState = { tilt: 0.42, spin: 1.2 } - -const DIVISIONS = 4 -const STROKE = 1.5 -const GOO_FUSION = 0.8 - -function buildEdges(c: GridState): Edge[] { - const half = 40 - const proj = (u: number, v: number) => { - const [ru, rv] = rotate2(u, v, c.spin) - return isoProject(ru * half, rv * half, 0, c.tilt) - } - const E: Edge[] = [] - for (let i = 0; i <= DIVISIONS; i++) { - const v = -1 + (2 * i) / DIVISIONS - E.push([proj(-1, v), proj(1, v)]) - } - for (let i = 0; i <= DIVISIONS; i++) { - const u = -1 + (2 * i) / DIVISIONS - E.push([proj(u, -1), proj(u, 1)]) - } - return E -} - -export interface IsoGridPlaneProps { - size?: number - className?: string - forceHover?: boolean -} - -export function IsoGridPlane({ size = 110, className, forceHover = false }: IsoGridPlaneProps) { - const { current, bind } = useGooMark({ rest: REST, hover: HOVER, forceHover }) - const { gradId, gooId } = useMarkIds() - - return ( - - ) -} diff --git a/apps/sim/app/(landing)/components/mothership/components/iso-marks/iso-stacked-planes.tsx b/apps/sim/app/(landing)/components/mothership/components/iso-marks/iso-stacked-planes.tsx deleted file mode 100644 index ef38efe5de4..00000000000 --- a/apps/sim/app/(landing)/components/mothership/components/iso-marks/iso-stacked-planes.tsx +++ /dev/null @@ -1,127 +0,0 @@ -'use client' - -import { cn } from '@sim/emcn' -import { GooDefs } from '@/app/(landing)/components/mothership/components/iso-marks/goo-defs' -import { - type Edge, - edgesToPaths, - gradientForTone, - isoProject, - type MarkState, - rotate2, - useGooMark, - useMarkIds, -} from '@/app/(landing)/components/mothership/components/iso-marks/use-goo-mark' - -/** - * Sim iso goo-mark: STACKED PLANES. - * N lattice sheets layered with a vertical gap. - * Rest: open and spread, slightly tilted, still. Hover (close + spin): gap - * collapses tight, tilt steepens, spins. - */ -interface StackState extends MarkState { - gap: number - tilt: number - spin: number - stroke: number - gradCx: number - gradCy: number - gradR: number - tone: number -} - -const REST: StackState = { - gap: 34.5, - tilt: 0.34, - spin: -2.82, - stroke: 2, - gradCx: 50, - gradCy: 50, - gradR: 44, - tone: 1, -} -const HOVER: StackState = { - gap: 11.5, - tilt: 0.33, - spin: -3.14, - stroke: 2, - gradCx: 50, - gradCy: 50, - gradR: 44, - tone: 1, -} - -const PLANES = 4 -const DIVISIONS = 2 -const GOO_FUSION = 1.1 - -function buildEdges(c: StackState): Edge[] { - const half = 40 - const totalH = (PLANES - 1) * c.gap - const proj = (u: number, v: number, z: number) => { - const [ru, rv] = rotate2(u, v, c.spin) - const p = isoProject(ru * half, rv * half, 0, c.tilt) - return [p[0], p[1] + (z - totalH / 2)] as [number, number] - } - const E: Edge[] = [] - for (let pl = 0; pl < PLANES; pl++) { - const z = pl * c.gap - for (let i = 0; i <= DIVISIONS; i++) { - const v = -1 + (2 * i) / DIVISIONS - E.push([proj(-1, v, z), proj(1, v, z)]) - } - for (let i = 0; i <= DIVISIONS; i++) { - const u = -1 + (2 * i) / DIVISIONS - E.push([proj(u, -1, z), proj(u, 1, z)]) - } - } - return E -} - -export interface IsoStackedPlanesProps { - size?: number - className?: string - forceHover?: boolean -} - -export function IsoStackedPlanes({ - size = 110, - className, - forceHover = false, -}: IsoStackedPlanesProps) { - const { current, bind } = useGooMark({ rest: REST, hover: HOVER, forceHover }) - const { gradId, gooId } = useMarkIds() - const { from, to } = gradientForTone(current.tone) - - return ( - - ) -} diff --git a/apps/sim/app/(landing)/components/mothership/components/iso-marks/iso-star.tsx b/apps/sim/app/(landing)/components/mothership/components/iso-marks/iso-star.tsx deleted file mode 100644 index 93a74def0d5..00000000000 --- a/apps/sim/app/(landing)/components/mothership/components/iso-marks/iso-star.tsx +++ /dev/null @@ -1,122 +0,0 @@ -'use client' - -import { cn } from '@sim/emcn' -import { GooDefs } from '@/app/(landing)/components/mothership/components/iso-marks/goo-defs' -import { - type Edge, - edgesToPaths, - isoProject, - type MarkState, - type Pt, - useGooMark, - useMarkIds, -} from '@/app/(landing)/components/mothership/components/iso-marks/use-goo-mark' - -/** - * Sim iso goo-mark: ISO STAR. - * Three cuboid bars crossing at 0 / +60 / -60 degrees forming a 6-point - * interlocking star. Rest: thin bars, still. Hover (open + spin): bars thicken, - * spins. - */ -interface StarState extends MarkState { - thickness: number - spin: number -} - -const REST: StarState = { thickness: 5, spin: 0 } -const HOVER: StarState = { thickness: 13, spin: 1.2 } - -const BAR_LENGTH = 16 -const STROKE = 1.5 -const GOO_FUSION = 0.8 - -function rotZ(p: [number, number, number], a: number): [number, number, number] { - const c = Math.cos(a) - const s = Math.sin(a) - return [p[0] * c - p[1] * s, p[0] * s + p[1] * c, p[2]] -} - -function barEdges(L: number, T: number): [number, number, number][][] { - const C = (sx: number, sy: number, sz: number): [number, number, number] => [ - sx * L, - sy * T, - sz * T, - ] - const corners: [number, number, number][] = [ - [-1, -1, -1], - [1, -1, -1], - [1, 1, -1], - [-1, 1, -1], - [-1, -1, 1], - [1, -1, 1], - [1, 1, 1], - [-1, 1, 1], - ] - const ci = (s: [number, number, number]) => C(s[0], s[1], s[2]) - const ed: [number, number][] = [ - [0, 1], - [1, 2], - [2, 3], - [3, 0], - [4, 5], - [5, 6], - [6, 7], - [7, 4], - [0, 4], - [1, 5], - [2, 6], - [3, 7], - ] - return ed.map(([a, b]) => [ci(corners[a]), ci(corners[b])]) -} - -function buildEdges(c: StarState): Edge[] { - const L = BAR_LENGTH * 0.5 - const T = c.thickness * 0.5 - const angs = [c.spin, Math.PI / 3 + c.spin, -Math.PI / 3 + c.spin] - const E: Edge[] = [] - for (const a of angs) { - for (const [p, q] of barEdges(L, T)) { - const pr = rotZ(p, a) - const qr = rotZ(q, a) - const A: Pt = isoProject(pr[0], pr[1], 1 + pr[2], 1) - const B: Pt = isoProject(qr[0], qr[1], 1 + qr[2], 1) - E.push([A, B]) - } - } - return E -} - -export interface IsoStarProps { - size?: number - className?: string - forceHover?: boolean -} - -export function IsoStar({ size = 110, className, forceHover = false }: IsoStarProps) { - const { current, bind } = useGooMark({ rest: REST, hover: HOVER, forceHover }) - const { gradId, gooId } = useMarkIds() - - return ( - - ) -} diff --git a/apps/sim/app/api/files/delete/route.test.ts b/apps/sim/app/api/files/delete/route.test.ts index 7dc868d0767..9e2bfee569c 100644 --- a/apps/sim/app/api/files/delete/route.test.ts +++ b/apps/sim/app/api/files/delete/route.test.ts @@ -57,8 +57,6 @@ vi.mock('@/lib/uploads/server/metadata', () => ({ deleteFileMetadata: vi.fn().mockResolvedValue(undefined), })) -vi.mock('@/lib/uploads/setup.server', () => ({})) - vi.mock('fs/promises', () => ({ unlink: vi.fn().mockResolvedValue(undefined), access: vi.fn().mockResolvedValue(undefined), diff --git a/apps/sim/app/api/files/parse/route.test.ts b/apps/sim/app/api/files/parse/route.test.ts index ccf9dd684ab..ca6eac2dbfd 100644 --- a/apps/sim/app/api/files/parse/route.test.ts +++ b/apps/sim/app/api/files/parse/route.test.ts @@ -103,7 +103,6 @@ vi.mock('path', () => ({ extname: actualPath.extname, })) -vi.mock('@/lib/uploads/setup.server', () => ({})) vi.mock('@/lib/uploads/core/setup.server', () => ({ UPLOAD_DIR_SERVER: '/test/uploads', })) diff --git a/apps/sim/app/api/webhooks/trigger/[path]/route.test.ts b/apps/sim/app/api/webhooks/trigger/[path]/route.test.ts index 0aec54129cd..78c8c64d845 100644 --- a/apps/sim/app/api/webhooks/trigger/[path]/route.test.ts +++ b/apps/sim/app/api/webhooks/trigger/[path]/route.test.ts @@ -104,8 +104,6 @@ const testData = { } const { - generateRequestHashMock, - validateSlackSignatureMock, handleWhatsAppVerificationMock, handleSlackChallengeMock, processWhatsAppDeduplicationMock, @@ -121,8 +119,6 @@ const { admissionRejectedResponseMock, tryAdmitMock, } = vi.hoisted(() => ({ - generateRequestHashMock: vi.fn().mockResolvedValue('test-hash-123'), - validateSlackSignatureMock: vi.fn().mockResolvedValue(true), handleWhatsAppVerificationMock: vi.fn().mockResolvedValue(null), handleSlackChallengeMock: vi.fn().mockReturnValue(null), processWhatsAppDeduplicationMock: vi.fn().mockResolvedValue(null), @@ -203,10 +199,6 @@ vi.mock('@/background/webhook-execution', () => ({ }), })) -vi.mock('@/background/logs-webhook-delivery', () => ({ - logsWebhookDelivery: {}, -})) - vi.mock('@/lib/webhooks/utils', () => ({ handleWhatsAppVerification: handleWhatsAppVerificationMock, handleSlackChallenge: handleSlackChallengeMock, @@ -215,11 +207,6 @@ vi.mock('@/lib/webhooks/utils', () => ({ processWebhook: processWebhookMock, })) -vi.mock('@/app/api/webhooks/utils', () => ({ - generateRequestHash: generateRequestHashMock, - validateSlackSignature: validateSlackSignatureMock, -})) - vi.mock('@/executor', () => ({ Executor: vi.fn().mockImplementation(() => ({ execute: executeMock, diff --git a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/schedule-calendar/components/calendar-event-chip/calendar-event-chip.tsx b/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/schedule-calendar/components/calendar-event-chip/calendar-event-chip.tsx deleted file mode 100644 index 785b0bb9d94..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/schedule-calendar/components/calendar-event-chip/calendar-event-chip.tsx +++ /dev/null @@ -1,60 +0,0 @@ -'use client' -import { chipContentGap, chipPrimaryFillTokens, cn } from '@sim/emcn' -import { format } from 'date-fns' -import type { - CalendarEvent, - ScheduledTask, -} from '@/app/workspace/[workspaceId]/scheduled-tasks/utils/schedule-events' - -interface CalendarEventChipProps { - event: CalendarEvent - onSelect: (task: ScheduledTask) => void - /** Right-click — open the task's context menu at the cursor. */ - onContextMenu: (task: ScheduledTask, e: React.MouseEvent) => void - /** Layout/sizing only (`w-full`, `min-w-0 flex-1`); chrome lives here. */ - className?: string -} - -/** - * Compact task pill rendered inside a month day cell or a time-grid slot — the - * one leaf shared by both grids. Every task renders identically regardless of - * status — plaintext start time + title, no icons or status colors; the - * details modal carries the state. The pill is the grid's real ` - ) -} diff --git a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/schedule-calendar/components/calendar-event-chip/index.ts b/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/schedule-calendar/components/calendar-event-chip/index.ts deleted file mode 100644 index 82edbb09366..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/schedule-calendar/components/calendar-event-chip/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { CalendarEventChip } from './calendar-event-chip' diff --git a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/schedule-calendar/components/calendar-toolbar/calendar-toolbar.tsx b/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/schedule-calendar/components/calendar-toolbar/calendar-toolbar.tsx deleted file mode 100644 index b32d2d930ca..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/schedule-calendar/components/calendar-toolbar/calendar-toolbar.tsx +++ /dev/null @@ -1,84 +0,0 @@ -'use client' - -import { - Check, - Chip, - ChipDatePicker, - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from '@sim/emcn' -import { ChevronLeft, ChevronRight } from '@sim/emcn/icons' -import { format, parseISO } from 'date-fns' -import type { CalendarScope } from '@/app/workspace/[workspaceId]/scheduled-tasks/utils/calendar-grid' - -const SCOPE_OPTIONS: { value: CalendarScope; label: string }[] = [ - { value: 'day', label: 'Day' }, - { value: 'week', label: 'Week' }, - { value: 'month', label: 'Month' }, -] - -interface CalendarToolbarProps { - scope: CalendarScope - anchor: Date - label: string - onPrev: () => void - onNext: () => void - onToday: () => void - onSelectDate: (date: Date) => void - onScopeChange: (scope: CalendarScope) => void -} - -/** - * Calendar ribbon: a "Today" jump and the period-label date picker on the left; - * the prev/next chevrons and the scope picker on the right. The controls are - * bare chips — the period label is a ghost `ChipDatePicker` that jumps the view - * to any picked date — and the scope picker is a `DropdownMenu`, matching the - * Filter/Sort menus on the resource options bar. - */ -export function CalendarToolbar({ - scope, - anchor, - label, - onPrev, - onNext, - onToday, - onSelectDate, - onScopeChange, -}: CalendarToolbarProps) { - const scopeLabel = SCOPE_OPTIONS.find((option) => option.value === scope)?.label ?? 'Week' - - return ( -
-
- Today - onSelectDate(parseISO(value))} - /> -
-
- - - - - {scopeLabel} - - - {SCOPE_OPTIONS.map((option) => ( - onScopeChange(option.value)}> - {option.label} - {option.value === scope && ( - - )} - - ))} - - -
-
- ) -} diff --git a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/schedule-calendar/components/calendar-toolbar/index.ts b/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/schedule-calendar/components/calendar-toolbar/index.ts deleted file mode 100644 index aab36b40942..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/schedule-calendar/components/calendar-toolbar/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { CalendarToolbar } from './calendar-toolbar' diff --git a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/schedule-calendar/components/index.ts b/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/schedule-calendar/components/index.ts deleted file mode 100644 index dd145b4ae79..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/schedule-calendar/components/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { CalendarEventChip } from './calendar-event-chip' -export { CalendarToolbar } from './calendar-toolbar' -export { MonthGrid } from './month-grid' -export { TimeGrid } from './time-grid' diff --git a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/schedule-calendar/components/month-grid/index.ts b/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/schedule-calendar/components/month-grid/index.ts deleted file mode 100644 index 40e4f18f3f2..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/schedule-calendar/components/month-grid/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { MonthGrid } from './month-grid' diff --git a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/schedule-calendar/components/month-grid/month-grid.tsx b/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/schedule-calendar/components/month-grid/month-grid.tsx deleted file mode 100644 index 17cde723105..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/schedule-calendar/components/month-grid/month-grid.tsx +++ /dev/null @@ -1,168 +0,0 @@ -'use client' -import { chipPrimaryFillTokens, cn } from '@sim/emcn' -import { format } from 'date-fns' -import { CalendarEventChip } from '@/app/workspace/[workspaceId]/scheduled-tasks/components/schedule-calendar/components/calendar-event-chip' -import { - type CalendarDayCell, - type MonthGrid as MonthGridData, - WEEKDAY_LABELS, -} from '@/app/workspace/[workspaceId]/scheduled-tasks/utils/calendar-grid' -import { - type CalendarEvent, - dayKey, - type ScheduledTask, -} from '@/app/workspace/[workspaceId]/scheduled-tasks/utils/schedule-events' - -/** - * Lines of task content a month day cell shows before collapsing the rest - * behind a "N more" overflow line. Capping at a fixed line count keeps every - * cell's height contribution bounded no matter how many tasks pile onto a day. - */ -const MAX_DAY_EVENT_LINES = 3 - -interface MonthGridProps { - grid: MonthGridData - onSelectDay: (date: Date) => void - onSelectTask: (task: ScheduledTask) => void - /** A task pill was right-clicked — open its context menu at the cursor. */ - onTaskContextMenu: (task: ScheduledTask, e: React.MouseEvent) => void - /** Drill into the day scope, where overflowing tasks have room to render. */ - onShowDay: (date: Date) => void - eventsByDay?: Map -} - -/** - * One day in the month grid. Clicking empty space opens the create modal; the - * cell is a plain clickable `
` so the task pills inside can be real - * ` - )} -
- - ) -} - -/** - * Month scope: a sticky weekday header over a 7-column grid of day cells that - * fills the body height. All seven tracks are equal, so the border-to-border - * column rhythm is even; the edge cells span clear to the panel edges (the page - * gutter stays hoverable and clickable) and inset their own content via - * `pl-6`/`pr-6`. Events flow in via `eventsByDay` — the single injection point - * the container fills. - */ -export function MonthGrid({ - grid, - onSelectDay, - onSelectTask, - onTaskContextMenu, - onShowDay, - eventsByDay, -}: MonthGridProps) { - return ( -
-
- {WEEKDAY_LABELS.map((label, index) => ( -
- {label} -
- ))} -
-
- {grid.weeks.map((week) => - week.map((cell, colIndex) => ( - - )) - )} -
-
- ) -} diff --git a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/schedule-calendar/components/time-grid/index.ts b/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/schedule-calendar/components/time-grid/index.ts deleted file mode 100644 index 9156c99899c..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/schedule-calendar/components/time-grid/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { TimeGrid } from './time-grid' diff --git a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/schedule-calendar/components/time-grid/time-grid.tsx b/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/schedule-calendar/components/time-grid/time-grid.tsx deleted file mode 100644 index 4f76fb5520e..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/schedule-calendar/components/time-grid/time-grid.tsx +++ /dev/null @@ -1,235 +0,0 @@ -'use client' - -import { useEffect, useState } from 'react' -import { chipPrimaryFillTokens, cn } from '@sim/emcn' -import { format } from 'date-fns' -import { zonedClockDate } from '@/lib/core/utils/timezone' -import { CalendarEventChip } from '@/app/workspace/[workspaceId]/scheduled-tasks/components/schedule-calendar/components/calendar-event-chip' -import { - type CalendarDayCell, - EVENT_CHIP_HEIGHT, - formatHourLabel, - formatSlotTime, - layoutColumn, - TIME_SLOT_HEIGHT, - timeToOffset, -} from '@/app/workspace/[workspaceId]/scheduled-tasks/utils/calendar-grid' -import { - type CalendarEvent, - dayKey, - type ScheduledTask, -} from '@/app/workspace/[workspaceId]/scheduled-tasks/utils/schedule-events' - -const GUTTER_WIDTH = 56 - -/** Re-render cadence for the current-time indicator. */ -const TICK_MS = 60_000 - -interface TimeGridProps { - /** One column per day: 7 for week scope, 1 for day scope. */ - days: CalendarDayCell[] - hours: number[] - /** The viewer's effective timezone — positions the now-line. */ - timezone: string - onSelectSlot: (date: Date, time: string) => void - onSelectTask: (task: ScheduledTask) => void - /** A task pill was right-clicked — open its context menu at the cursor. */ - onTaskContextMenu: (task: ScheduledTask, e: React.MouseEvent) => void - eventsByDay?: Map -} - -/** - * Live now-line drawn over today's column — a chip-primary dot at the left edge - * and a hairline across the column, positioned by {@link timeToOffset}. Renders - * nothing until mounted (keeps SSR output stable, avoiding a hydration mismatch - * on the time-dependent offset), then ticks once a minute so the line advances. - * Positioned in `timezone` so it tracks the same zone the day columns render in. - * The parent column is `relative`; this is `absolute`. - */ -function CurrentTimeIndicator({ timezone }: { timezone: string }) { - const [now, setNow] = useState(null) - - useEffect(() => { - setNow(new Date()) - const interval = setInterval(() => setNow(new Date()), TICK_MS) - return () => clearInterval(interval) - }, []) - - if (!now) return null - - return ( -
-
-
-
- ) -} - -/** - * One hour cell in a day column: a click target that opens the create modal - * seeded to this hour, plus the hour's gridlines. Tasks are not rendered here — - * they live in the day's {@link DayEvents} overlay so each sits at its exact - * minute rather than snapping to the top of the hour. - */ -function HourCell({ - date, - hour, - isLastColumn, - onSelect, -}: { - date: Date - hour: number - isLastColumn: boolean - onSelect: (date: Date, time: string) => void -}) { - return ( -
onSelect(date, formatSlotTime(hour))} - style={{ height: TIME_SLOT_HEIGHT }} - className={cn( - 'cursor-pointer border-[var(--border)] border-r border-b transition-colors hover-hover:bg-[var(--surface-active)]', - isLastColumn && 'pr-6' - )} - /> - ) -} - -/** - * A day column's task pills, each absolutely positioned at its exact start time - * via {@link timeToOffset}. The layer is non-interactive so empty space falls - * through to the hour cells beneath (click-to-create); the pills re-enable - * pointer events. The layer clips to the day's bounds so a late-night pill never - * spills past the final hour row. Coincident tasks overlap by design. - */ -function DayEvents({ - events, - isLastColumn, - onSelectTask, - onTaskContextMenu, -}: { - events: CalendarEvent[] - isLastColumn: boolean - onSelectTask: (task: ScheduledTask) => void - onTaskContextMenu: (task: ScheduledTask, e: React.MouseEvent) => void -}) { - const placed = layoutColumn(events, EVENT_CHIP_HEIGHT) - return ( -
- {placed.map(({ item: event, topPx, lane, lanes }) => ( -
- -
- ))} -
- ) -} - -/** - * Shared time-based grid for the week (7 columns) and day (1 column) scopes: a - * sticky day header, a fixed hour gutter, and a stack of hour slots per day. - * Column widths come from a CSS grid template shared by the header and body so - * they stay aligned. The sticky header paints chrome on the day cells only — - * its gutter spacer is transparent and border-free, so the hour labels scroll - * clear to the top of the viewport. Today's column is `relative` and hosts the - * {@link CurrentTimeIndicator}. Events flow in via `eventsByDay` — the single - * injection point the container fills. - */ -export function TimeGrid({ - days, - hours, - timezone, - onSelectSlot, - onSelectTask, - onTaskContextMenu, - eventsByDay, -}: TimeGridProps) { - const columnsStyle = { - gridTemplateColumns: `${GUTTER_WIDTH}px repeat(${days.length}, minmax(0, 1fr))`, - } - - return ( -
-
-
- {days.map((day, dayIndex) => ( -
- {format(day.date, 'EEE')} - - {format(day.date, 'd')} - -
- ))} -
- -
-
- {hours.map((hour) => ( -
- - {formatHourLabel(hour)} - -
- ))} -
- - {days.map((day, dayIndex) => ( -
- {day.isToday && } - {hours.map((hour) => ( - - ))} - -
- ))} -
-
- ) -} diff --git a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/schedule-calendar/index.ts b/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/schedule-calendar/index.ts deleted file mode 100644 index f8405c4a8cf..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/schedule-calendar/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ScheduleCalendar } from './schedule-calendar' diff --git a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/schedule-calendar/schedule-calendar.tsx b/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/schedule-calendar/schedule-calendar.tsx deleted file mode 100644 index 04d26254d1b..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/schedule-calendar/schedule-calendar.tsx +++ /dev/null @@ -1,146 +0,0 @@ -'use client' - -import { useCallback, useEffect, useMemo, useRef, useState } from 'react' -import { zonedClockDate } from '@/lib/core/utils/timezone' -import { - CalendarToolbar, - MonthGrid, - TimeGrid, -} from '@/app/workspace/[workspaceId]/scheduled-tasks/components/schedule-calendar/components' -import { - buildCalendarGrid, - type CalendarScope, - formatScopeLabel, - timeToOffset, -} from '@/app/workspace/[workspaceId]/scheduled-tasks/utils/calendar-grid' -import type { - CalendarEvent, - ScheduledTask, -} from '@/app/workspace/[workspaceId]/scheduled-tasks/utils/schedule-events' - -interface ScheduleCalendarProps { - scope: CalendarScope - anchor: Date - today: Date - /** The viewer's effective timezone — positions the now-line and centering. */ - timezone: string - onScopeChange: (scope: CalendarScope) => void - onPrev: () => void - onNext: () => void - onToday: () => void - onSelectDate: (date: Date) => void - onSelectSlot: (date: Date, time?: string) => void - /** A task pill was clicked — open its details modal. */ - onSelectTask: (task: ScheduledTask) => void - /** A task pill was right-clicked — open its context menu at the cursor. */ - onTaskContextMenu: (task: ScheduledTask, e: React.MouseEvent) => void - /** A month cell's overflow line was clicked — jump to that day's view. */ - onShowDay: (date: Date) => void - /** Day-bucketed events feeding both the month grid and the time grid. */ - eventsByDay?: Map -} - -/** - * Calendar body, retained unmounted for reuse. The scheduled-tasks page that - * hosted it — along with its `useCalendar`/`useScheduledTasks` hooks, modals, - * and sidebar entry — was removed; this component tree and `../../utils` are - * kept deliberately so the calendar can be repurposed on a future surface. It - * has no importer today: that is intentional, NOT dead code to delete. - * - * Owns the scroll region and view dispatch: it renders the toolbar, derives the - * grid from caller-supplied scope/anchor state, and switches between the month - * grid and the shared time grid on the grid discriminant. - * - * Scroll behavior: entering week/day scope, and "Today" presses (signaled via an - * internal `scrollSignal`), center the current time in the viewport; month scope - * resets to the top. Plain prev/next navigation never re-centers. Today presses - * scroll smoothly as an orientation cue; mount and scope switches position - * instantly (animating initial placement would read as a glitch). Centering is - * computed from the time-grid header height plus {@link timeToOffset} rather than - * the now-line element, so it works even on first paint before the line mounts. - * - * Event injection is the single integration point — `eventsByDay` is threaded - * straight into both grids, which forward it to their cells. - */ -export function ScheduleCalendar({ - scope, - anchor, - today, - timezone, - onScopeChange, - onPrev, - onNext, - onToday, - onSelectDate, - onSelectSlot, - onSelectTask, - onTaskContextMenu, - onShowDay, - eventsByDay, -}: ScheduleCalendarProps) { - const scrollRef = useRef(null) - const lastScrollSignalRef = useRef(0) - const [scrollSignal, setScrollSignal] = useState(0) - - const grid = useMemo(() => buildCalendarGrid(scope, anchor, today), [scope, anchor, today]) - const label = useMemo(() => formatScopeLabel(scope, anchor), [scope, anchor]) - - const handleToday = useCallback(() => { - onToday() - setScrollSignal((signal) => signal + 1) - }, [onToday]) - - useEffect(() => { - const region = scrollRef.current - if (!region) return - const behavior: ScrollBehavior = - scrollSignal !== lastScrollSignalRef.current ? 'smooth' : 'auto' - lastScrollSignalRef.current = scrollSignal - if (scope === 'month') { - region.scrollTo({ top: 0, behavior }) - return - } - const header = region.querySelector('[data-time-grid-header]') - const headerHeight = header ? header.getBoundingClientRect().height : 0 - const target = - headerHeight + timeToOffset(zonedClockDate(new Date(), timezone)) - region.clientHeight / 2 - region.scrollTo({ top: Math.max(0, target), behavior }) - }, [scope, scrollSignal, timezone]) - - return ( -
- -
- {grid.kind === 'month' ? ( - onSelectSlot(date)} - onSelectTask={onSelectTask} - onTaskContextMenu={onTaskContextMenu} - onShowDay={onShowDay} - eventsByDay={eventsByDay} - /> - ) : ( - onSelectSlot(date, time)} - onSelectTask={onSelectTask} - onTaskContextMenu={onTaskContextMenu} - eventsByDay={eventsByDay} - /> - )} -
-
- ) -} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/index.ts index 254349f067b..4bf89ce3a0f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/index.ts @@ -1,10 +1,6 @@ -export { useCaretViewport } from './use-caret-viewport' export { useContextManagement } from './use-context-management' export { useFileAttachments } from './use-file-attachments' export { useIntegrationAutoMention } from './use-integration-auto-mention' export { useMentionData } from './use-mention-data' -export { useMentionInsertHandlers } from './use-mention-insert-handlers' -export { useMentionKeyboard } from './use-mention-keyboard' export { useMentionMenu } from './use-mention-menu' export { useMentionTokens } from './use-mention-tokens' -export { useTextareaAutoResize } from './use-textarea-auto-resize' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-caret-viewport.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-caret-viewport.ts deleted file mode 100644 index 51cc9212289..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-caret-viewport.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { useMemo } from 'react' - -interface CaretViewportPosition { - left: number - top: number -} - -interface UseCaretViewportResult { - caretViewport: CaretViewportPosition | null - side: 'top' | 'bottom' -} - -interface UseCaretViewportProps { - textareaRef: React.RefObject - message: string - caretPos: number -} - -/** - * Calculates the viewport position of the caret in a textarea using the mirror div technique. - * This hook memoizes the calculation to prevent unnecessary DOM manipulation on every render. - */ -export function useCaretViewport({ - textareaRef, - message, - caretPos, -}: UseCaretViewportProps): UseCaretViewportResult { - return useMemo(() => { - const textareaEl = textareaRef.current - if (!textareaEl) { - return { caretViewport: null, side: 'bottom' as const } - } - - const textareaRect = textareaEl.getBoundingClientRect() - const style = window.getComputedStyle(textareaEl) - - const mirrorDiv = document.createElement('div') - mirrorDiv.style.position = 'absolute' - mirrorDiv.style.visibility = 'hidden' - mirrorDiv.style.whiteSpace = 'pre-wrap' - mirrorDiv.style.overflowWrap = 'break-word' - mirrorDiv.style.font = style.font - mirrorDiv.style.padding = style.padding - mirrorDiv.style.border = style.border - mirrorDiv.style.width = style.width - mirrorDiv.style.lineHeight = style.lineHeight - mirrorDiv.style.boxSizing = style.boxSizing - mirrorDiv.style.letterSpacing = style.letterSpacing - mirrorDiv.style.textTransform = style.textTransform - mirrorDiv.style.textIndent = style.textIndent - mirrorDiv.style.textAlign = style.textAlign - mirrorDiv.textContent = message.substring(0, caretPos) - - const caretMarker = document.createElement('span') - caretMarker.style.display = 'inline-block' - caretMarker.style.width = '0px' - caretMarker.style.padding = '0' - caretMarker.style.border = '0' - mirrorDiv.appendChild(caretMarker) - - document.body.appendChild(mirrorDiv) - const markerRect = caretMarker.getBoundingClientRect() - const mirrorRect = mirrorDiv.getBoundingClientRect() - document.body.removeChild(mirrorDiv) - - const caretViewport = { - left: textareaRect.left + (markerRect.left - mirrorRect.left) - textareaEl.scrollLeft, - top: textareaRect.top + (markerRect.top - mirrorRect.top) - textareaEl.scrollTop, - } - - const margin = 8 - const spaceBelow = window.innerHeight - caretViewport.top - margin - const side: 'top' | 'bottom' = spaceBelow >= caretViewport.top - margin ? 'bottom' : 'top' - - return { caretViewport, side } - }, [textareaRef, message, caretPos]) -} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-insert-handlers.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-insert-handlers.ts deleted file mode 100644 index 75eb4f7ec50..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-insert-handlers.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { useCallback, useMemo } from 'react' -import { - DOCS_CONFIG, - FOLDER_CONFIGS, - type FolderConfig, -} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/constants' -import type { useMentionMenu } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-menu' -import type { MentionFolderNav } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/types' -import { isContextAlreadySelected } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/utils' -import type { ChatContext } from '@/stores/panel' - -interface UseMentionInsertHandlersProps { - /** Mention menu hook instance */ - mentionMenu: ReturnType - /** Current workflow ID */ - workflowId: string | null - /** Currently selected contexts */ - selectedContexts: ChatContext[] - /** Callback to update selected contexts */ - onContextAdd: (context: ChatContext) => void - /** Folder navigation state exposed from MentionMenu via callback */ - mentionFolderNav?: MentionFolderNav | null -} - -/** - * Custom hook to provide insert handlers for different mention types. - * - * @param props - Configuration object - * @returns Insert handler functions for each mention type - */ -export function useMentionInsertHandlers({ - mentionMenu, - workflowId, - selectedContexts, - onContextAdd, - mentionFolderNav, -}: UseMentionInsertHandlersProps) { - const { - replaceActiveMentionWith, - insertAtCursor, - setShowMentionMenu, - setOpenSubmenuFor, - resetActiveMentionQuery, - } = mentionMenu - - /** - * Closes all menus and resets state - */ - const closeMenus = useCallback(() => { - setShowMentionMenu(false) - if (mentionFolderNav?.isInFolder) { - mentionFolderNav.closeFolder() - } - setOpenSubmenuFor(null) - }, [setShowMentionMenu, setOpenSubmenuFor, mentionFolderNav]) - - const createInsertHandler = useCallback( - (config: FolderConfig) => { - return (item: TItem) => { - const label = config.getLabel(item) - const context = config.buildContext(item, workflowId) - - if (isContextAlreadySelected(context, selectedContexts)) { - resetActiveMentionQuery() - closeMenus() - return - } - - if (config.useInsertFallback) { - if (!replaceActiveMentionWith(label)) { - insertAtCursor(` @${label} `) - } - } else { - replaceActiveMentionWith(label) - } - - onContextAdd(context) - closeMenus() - } - }, - [ - workflowId, - selectedContexts, - replaceActiveMentionWith, - insertAtCursor, - onContextAdd, - resetActiveMentionQuery, - closeMenus, - ] - ) - - /** - * Special handler for Docs (no item parameter, uses DOCS_CONFIG) - */ - const insertDocsMention = useCallback(() => { - const label = DOCS_CONFIG.getLabel() - const context = DOCS_CONFIG.buildContext() - - // Prevent duplicate insertion - if (isContextAlreadySelected(context, selectedContexts)) { - resetActiveMentionQuery() - closeMenus() - return - } - - // Docs uses fallback insertion - if (!replaceActiveMentionWith(label)) { - insertAtCursor(` @${label} `) - } - - onContextAdd(context) - closeMenus() - }, [ - selectedContexts, - replaceActiveMentionWith, - insertAtCursor, - onContextAdd, - resetActiveMentionQuery, - closeMenus, - ]) - - const handlers = useMemo( - () => ({ - insertPastChatMention: createInsertHandler(FOLDER_CONFIGS.chats), - insertWorkflowMention: createInsertHandler(FOLDER_CONFIGS.workflows), - insertKnowledgeMention: createInsertHandler(FOLDER_CONFIGS.knowledge), - insertBlockMention: createInsertHandler(FOLDER_CONFIGS.blocks), - insertWorkflowBlockMention: createInsertHandler(FOLDER_CONFIGS['workflow-blocks']), - insertLogMention: createInsertHandler(FOLDER_CONFIGS.logs), - insertIntegrationMention: createInsertHandler(FOLDER_CONFIGS.integrations), - insertDocsMention, - }), - [createInsertHandler, insertDocsMention] - ) - - return handlers -} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-keyboard.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-keyboard.ts deleted file mode 100644 index 8ab898483ff..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-keyboard.ts +++ /dev/null @@ -1,355 +0,0 @@ -import { type KeyboardEvent, useCallback, useMemo } from 'react' -import { - FOLDER_CONFIGS, - FOLDER_ORDER, - type MentionFolderId, - ROOT_MENU_ITEM_COUNT, -} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/constants' -import type { - useMentionData, - useMentionMenu, -} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks' -import type { MentionFolderNav } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/types' -import { - getFolderData as getFolderDataUtil, - getFolderEnsureLoaded as getFolderEnsureLoadedUtil, -} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/utils' - -interface UseMentionKeyboardProps { - /** Mention menu hook instance */ - mentionMenu: ReturnType - /** Mention data hook instance */ - mentionData: ReturnType - /** Callback to insert specific mention types */ - insertHandlers: { - insertPastChatMention: (chat: any) => void - insertWorkflowMention: (wf: any) => void - insertKnowledgeMention: (kb: any) => void - insertBlockMention: (blk: any) => void - insertWorkflowBlockMention: (blk: any) => void - insertLogMention: (log: any) => void - insertIntegrationMention: (integration: any) => void - insertDocsMention: () => void - } - /** Folder navigation state exposed from MentionMenu via callback */ - mentionFolderNav: MentionFolderNav | null -} - -/** - * Custom hook to handle keyboard navigation in the mention menu. - */ -export function useMentionKeyboard({ - mentionMenu, - mentionData, - insertHandlers, - mentionFolderNav, -}: UseMentionKeyboardProps) { - const { - showMentionMenu, - mentionActiveIndex, - submenuActiveIndex, - setMentionActiveIndex, - setSubmenuActiveIndex, - setSubmenuQueryStart, - getCaretPos, - getActiveMentionQueryAtPosition, - getSubmenuQuery, - resetActiveMentionQuery, - scrollActiveItemIntoView, - } = mentionMenu - - const currentFolder = mentionFolderNav?.currentFolder ?? null - const isInFolder = mentionFolderNav?.isInFolder ?? false - - /** - * Map of folder IDs to insert handlers - */ - const insertHandlerMap = useMemo( - (): Record void> => ({ - chats: insertHandlers.insertPastChatMention, - workflows: insertHandlers.insertWorkflowMention, - knowledge: insertHandlers.insertKnowledgeMention, - blocks: insertHandlers.insertBlockMention, - 'workflow-blocks': insertHandlers.insertWorkflowBlockMention, - logs: insertHandlers.insertLogMention, - integrations: insertHandlers.insertIntegrationMention, - }), - [insertHandlers] - ) - - /** - * Get data array for a folder from mentionData - */ - const getFolderData = useCallback( - (folderId: MentionFolderId) => getFolderDataUtil(mentionData, folderId), - [mentionData] - ) - - /** - * Filter items for a folder based on query using config's filterFn - */ - const filterFolderItems = useCallback( - (folderId: MentionFolderId, query: string): any[] => { - const config = FOLDER_CONFIGS[folderId] - const items = getFolderData(folderId) - if (!query) return items - const q = query.toLowerCase() - return items.filter((item) => config.filterFn(item, q)) - }, - [getFolderData] - ) - - /** - * Ensure data is loaded for a folder - */ - const ensureFolderLoaded = useCallback( - (folderId: MentionFolderId): void => { - const ensureFn = getFolderEnsureLoadedUtil(mentionData, folderId) - if (ensureFn) void ensureFn() - }, - [mentionData] - ) - - /** - * Build aggregated list matching the portal's ordering - */ - const buildAggregatedList = useCallback( - (query: string): Array<{ type: MentionFolderId | 'docs'; value: any }> => { - const q = query.toLowerCase() - const result: Array<{ type: MentionFolderId | 'docs'; value: any }> = [] - - for (const folderId of FOLDER_ORDER) { - const filtered = filterFolderItems(folderId, q) - filtered.forEach((item) => { - result.push({ type: folderId, value: item }) - }) - } - - if ('docs'.includes(q)) { - result.push({ type: 'docs', value: null }) - } - - return result - }, - [filterFolderItems] - ) - - /** - * Generic navigation helper for navigating through items - */ - const navigateItems = useCallback( - ( - direction: 'up' | 'down', - itemCount: number, - setIndex: (fn: (prev: number) => number) => void - ) => { - setIndex((prev) => { - const last = Math.max(0, itemCount - 1) - if (itemCount === 0) return 0 - const next = - direction === 'down' ? (prev >= last ? 0 : prev + 1) : prev <= 0 ? last : prev - 1 - requestAnimationFrame(() => scrollActiveItemIntoView(next)) - return next - }) - }, - [scrollActiveItemIntoView] - ) - - /** - * Handles arrow up/down navigation in mention menu - */ - const handleArrowNavigation = useCallback( - (e: KeyboardEvent) => { - if (!showMentionMenu || !(e.key === 'ArrowDown' || e.key === 'ArrowUp')) return false - - e.preventDefault() - const caretPos = getCaretPos() - const active = getActiveMentionQueryAtPosition(caretPos) - const mainQ = (!isInFolder ? active?.query || '' : '').toLowerCase() - const direction = e.key === 'ArrowDown' ? 'down' : 'up' - - const showAggregatedView = mainQ.length > 0 - if (showAggregatedView && !isInFolder) { - const aggregatedList = buildAggregatedList(mainQ) - navigateItems(direction, aggregatedList.length, setSubmenuActiveIndex) - return true - } - - if (currentFolder && FOLDER_CONFIGS[currentFolder as MentionFolderId]) { - const q = getSubmenuQuery().toLowerCase() - const filtered = filterFolderItems(currentFolder as MentionFolderId, q) - navigateItems(direction, filtered.length, setSubmenuActiveIndex) - return true - } - - navigateItems(direction, ROOT_MENU_ITEM_COUNT, setMentionActiveIndex) - return true - }, - [ - showMentionMenu, - isInFolder, - currentFolder, - buildAggregatedList, - filterFolderItems, - navigateItems, - getCaretPos, - getActiveMentionQueryAtPosition, - getSubmenuQuery, - setMentionActiveIndex, - setSubmenuActiveIndex, - ] - ) - - /** - * Handles arrow right to enter submenus - */ - const handleArrowRight = useCallback( - (e: KeyboardEvent) => { - if (!showMentionMenu || e.key !== 'ArrowRight' || !mentionFolderNav) return false - - const caretPos = getCaretPos() - const active = getActiveMentionQueryAtPosition(caretPos) - const mainQ = (active?.query || '').toLowerCase() - - if (mainQ.length > 0) return false - - e.preventDefault() - - const isDocsSelected = mentionActiveIndex === FOLDER_ORDER.length - if (isDocsSelected) { - resetActiveMentionQuery() - insertHandlers.insertDocsMention() - return true - } - - const selectedFolderId = FOLDER_ORDER[mentionActiveIndex] - if (selectedFolderId) { - const config = FOLDER_CONFIGS[selectedFolderId] - resetActiveMentionQuery() - mentionFolderNav.openFolder(selectedFolderId, config.title) - setSubmenuQueryStart(getCaretPos()) - ensureFolderLoaded(selectedFolderId) - } - - return true - }, - [ - showMentionMenu, - mentionActiveIndex, - mentionFolderNav, - getCaretPos, - getActiveMentionQueryAtPosition, - resetActiveMentionQuery, - setSubmenuQueryStart, - ensureFolderLoaded, - insertHandlers, - ] - ) - - /** - * Handles arrow left to exit submenus - */ - const handleArrowLeft = useCallback( - (e: KeyboardEvent) => { - if (!showMentionMenu || e.key !== 'ArrowLeft') return false - - if (isInFolder && mentionFolderNav) { - e.preventDefault() - mentionFolderNav.closeFolder() - setSubmenuQueryStart(null) - return true - } - - return false - }, - [showMentionMenu, isInFolder, mentionFolderNav, setSubmenuQueryStart] - ) - - /** - * Handles Enter key to select mention - */ - const handleEnterSelection = useCallback( - (e: KeyboardEvent) => { - if (!showMentionMenu || e.key !== 'Enter' || e.shiftKey) return false - - e.preventDefault() - const caretPos = getCaretPos() - const active = getActiveMentionQueryAtPosition(caretPos) - const mainQ = (!isInFolder ? active?.query || '' : '').toLowerCase() - const showAggregatedView = mainQ.length > 0 - - if (showAggregatedView && !isInFolder) { - const aggregated = buildAggregatedList(mainQ) - const idx = Math.max(0, Math.min(submenuActiveIndex, aggregated.length - 1)) - const chosen = aggregated[idx] - if (chosen) { - if (chosen.type === 'docs') { - insertHandlers.insertDocsMention() - } else { - const handler = insertHandlerMap[chosen.type] - handler(chosen.value) - } - } - return true - } - - if (isInFolder && currentFolder && FOLDER_CONFIGS[currentFolder as MentionFolderId]) { - const folderId = currentFolder as MentionFolderId - const q = getSubmenuQuery().toLowerCase() - const filtered = filterFolderItems(folderId, q) - if (filtered.length > 0) { - const chosen = filtered[Math.max(0, Math.min(submenuActiveIndex, filtered.length - 1))] - const handler = insertHandlerMap[folderId] - handler(chosen) - setSubmenuQueryStart(null) - } - return true - } - - const isDocsSelected = mentionActiveIndex === FOLDER_ORDER.length - if (isDocsSelected) { - resetActiveMentionQuery() - insertHandlers.insertDocsMention() - return true - } - - const selectedFolderId = FOLDER_ORDER[mentionActiveIndex] - if (selectedFolderId && mentionFolderNav) { - const config = FOLDER_CONFIGS[selectedFolderId] - resetActiveMentionQuery() - mentionFolderNav.openFolder(selectedFolderId, config.title) - setSubmenuActiveIndex(0) - setSubmenuQueryStart(getCaretPos()) - ensureFolderLoaded(selectedFolderId) - } - - return true - }, - [ - showMentionMenu, - isInFolder, - currentFolder, - mentionActiveIndex, - submenuActiveIndex, - mentionFolderNav, - buildAggregatedList, - filterFolderItems, - insertHandlerMap, - getCaretPos, - getActiveMentionQueryAtPosition, - getSubmenuQuery, - resetActiveMentionQuery, - setSubmenuActiveIndex, - setSubmenuQueryStart, - ensureFolderLoaded, - insertHandlers, - ] - ) - - return { - handleArrowNavigation, - handleArrowRight, - handleArrowLeft, - handleEnterSelection, - } -} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-textarea-auto-resize.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-textarea-auto-resize.ts deleted file mode 100644 index 82ee7107ec7..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-textarea-auto-resize.ts +++ /dev/null @@ -1,232 +0,0 @@ -'use client' - -import { type RefObject, useEffect, useLayoutEffect, useRef } from 'react' - -/** - * Maximum textarea height in pixels - */ -const MAX_TEXTAREA_HEIGHT = 120 - -interface UseTextareaAutoResizeProps { - /** Current message content */ - message: string - /** Width of the panel */ - panelWidth: number - /** Selected mention contexts */ - selectedContexts: any[] - /** External textarea ref to sync with */ - textareaRef: RefObject - /** Container ref for observing layout shifts */ - containerRef: HTMLDivElement | null -} - -/** - * Custom hook to auto-resize textarea and sync with overlay. - * Uses ResizeObserver for accurate, event-driven synchronization without arbitrary timeouts. - * - * @param props - Configuration object - * @returns Overlay ref for highlight rendering - */ -export function useTextareaAutoResize({ - message, - panelWidth, - selectedContexts, - textareaRef, - containerRef, -}: UseTextareaAutoResizeProps) { - const overlayRef = useRef(null) - const containerResizeObserverRef = useRef(null) - const textareaResizeObserverRef = useRef(null) - - /** - * Syncs all styles and dimensions between textarea and overlay. - * Called immediately when DOM changes are detected. - */ - const syncOverlayStyles = useRef(() => { - const textarea = textareaRef.current - const overlay = overlayRef.current - if (!textarea || !overlay || typeof window === 'undefined') return - - const styles = window.getComputedStyle(textarea) - - overlay.style.font = styles.font - overlay.style.fontSize = styles.fontSize - overlay.style.fontFamily = styles.fontFamily - overlay.style.fontWeight = styles.fontWeight - overlay.style.fontStyle = styles.fontStyle - overlay.style.fontVariant = styles.fontVariant - overlay.style.letterSpacing = styles.letterSpacing - overlay.style.lineHeight = styles.lineHeight - overlay.style.fontKerning = (styles as any).fontKerning ?? '' - overlay.style.fontFeatureSettings = (styles as any).fontFeatureSettings ?? '' - overlay.style.textRendering = (styles as any).textRendering ?? '' - ;(overlay.style as any).tabSize = (styles as any).tabSize ?? '' - ;(overlay.style as any).MozTabSize = (styles as any).MozTabSize ?? '' - overlay.style.textTransform = styles.textTransform - overlay.style.textIndent = styles.textIndent - - overlay.style.padding = styles.padding - overlay.style.paddingTop = styles.paddingTop - overlay.style.paddingRight = styles.paddingRight - overlay.style.paddingBottom = styles.paddingBottom - overlay.style.paddingLeft = styles.paddingLeft - overlay.style.margin = styles.margin - overlay.style.marginTop = styles.marginTop - overlay.style.marginRight = styles.marginRight - overlay.style.marginBottom = styles.marginBottom - overlay.style.marginLeft = styles.marginLeft - overlay.style.border = styles.border - overlay.style.borderWidth = styles.borderWidth - - overlay.style.whiteSpace = styles.whiteSpace - overlay.style.wordBreak = styles.wordBreak - overlay.style.wordWrap = styles.wordWrap - overlay.style.overflowWrap = styles.overflowWrap - overlay.style.textAlign = styles.textAlign - overlay.style.boxSizing = styles.boxSizing - overlay.style.borderRadius = styles.borderRadius - overlay.style.direction = styles.direction - overlay.style.hyphens = (styles as any).hyphens ?? '' - - const textareaWidth = textarea.clientWidth - const textareaHeight = textarea.clientHeight - - overlay.style.width = `${textareaWidth}px` - overlay.style.height = `${textareaHeight}px` - - const computedMaxHeight = styles.maxHeight - if (computedMaxHeight && computedMaxHeight !== 'none') { - overlay.style.maxHeight = computedMaxHeight - } - - overlay.scrollTop = textarea.scrollTop - overlay.scrollLeft = textarea.scrollLeft - }) - - /** - * Auto-resize textarea based on content. - * Uses useLayoutEffect to run synchronously AFTER DOM mutations but BEFORE browser paint. - * This ensures we sync after React commits changes to the DOM. - */ - useLayoutEffect(() => { - const textarea = textareaRef.current - const overlay = overlayRef.current - if (!textarea || !overlay) return - - const cursorPos = textarea.selectionStart ?? 0 - const isAtEnd = cursorPos === message.length - const wasScrolledToBottom = - textarea.scrollHeight - textarea.scrollTop - textarea.clientHeight < 5 - - textarea.style.height = 'auto' - overlay.style.height = 'auto' - - void textarea.offsetHeight - void overlay.offsetHeight - - const scrollHeight = textarea.scrollHeight - const nextHeight = Math.min(scrollHeight, MAX_TEXTAREA_HEIGHT) - - const heightString = `${nextHeight}px` - const overflowString = scrollHeight > MAX_TEXTAREA_HEIGHT ? 'auto' : 'hidden' - - textarea.style.height = heightString - textarea.style.overflowY = overflowString - overlay.style.height = heightString - overlay.style.overflowY = overflowString - - void textarea.offsetHeight - void overlay.offsetHeight - - if ((isAtEnd || wasScrolledToBottom) && scrollHeight > nextHeight) { - const scrollValue = scrollHeight - textarea.scrollTop = scrollValue - overlay.scrollTop = scrollValue - } else { - overlay.scrollTop = textarea.scrollTop - overlay.scrollLeft = textarea.scrollLeft - } - - syncOverlayStyles.current() - }, [message, selectedContexts, textareaRef]) - - /** - * Sync scroll position between textarea and overlay - */ - useEffect(() => { - const textarea = textareaRef.current - const overlay = overlayRef.current - - if (!textarea || !overlay) return - - const handleScroll = () => { - overlay.scrollTop = textarea.scrollTop - overlay.scrollLeft = textarea.scrollLeft - } - - textarea.addEventListener('scroll', handleScroll, { passive: true }) - return () => textarea.removeEventListener('scroll', handleScroll) - }, [textareaRef]) - - /** - * Setup ResizeObserver on the CONTAINER to catch layout shifts when pills wrap. - * This is critical because when pills wrap, the textarea moves but doesn't resize. - */ - useLayoutEffect(() => { - const textarea = textareaRef.current - const overlay = overlayRef.current - if (!textarea || !overlay || !containerRef || typeof window === 'undefined') return - - syncOverlayStyles.current() - - if (typeof ResizeObserver !== 'undefined' && !containerResizeObserverRef.current) { - containerResizeObserverRef.current = new ResizeObserver(() => { - syncOverlayStyles.current() - }) - containerResizeObserverRef.current.observe(containerRef) - } - - if (typeof ResizeObserver !== 'undefined' && !textareaResizeObserverRef.current) { - textareaResizeObserverRef.current = new ResizeObserver(() => { - syncOverlayStyles.current() - }) - textareaResizeObserverRef.current.observe(textarea) - } - - const mutationObserver = new MutationObserver(() => { - syncOverlayStyles.current() - }) - mutationObserver.observe(textarea, { - attributes: true, - attributeFilter: ['style', 'class'], - }) - - const handleResize = () => syncOverlayStyles.current() - window.addEventListener('resize', handleResize) - - return () => { - mutationObserver.disconnect() - window.removeEventListener('resize', handleResize) - } - }, [panelWidth, textareaRef, containerRef]) - - /** - * Cleanup ResizeObservers on unmount - */ - useEffect(() => { - return () => { - if (containerResizeObserverRef.current) { - containerResizeObserverRef.current.disconnect() - containerResizeObserverRef.current = null - } - if (textareaResizeObserverRef.current) { - textareaResizeObserverRef.current.disconnect() - textareaResizeObserverRef.current = null - } - } - }, []) - - return { - overlayRef, - } -} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/filter-popover/filter-popover.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/filter-popover/filter-popover.tsx deleted file mode 100644 index e3553338095..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/filter-popover/filter-popover.tsx +++ /dev/null @@ -1,117 +0,0 @@ -'use client' - -import { memo } from 'react' -import { - Button, - Popover, - PopoverContent, - PopoverDivider, - PopoverItem, - PopoverScrollArea, - PopoverSection, - PopoverTrigger, -} from '@sim/emcn' -import { ListFilter } from '@sim/emcn/icons' -import clsx from 'clsx' -import { EntryBlockTile } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/entry-block-tile' -import type { - BlockInfo, - TerminalFilters, -} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/types' - -/** - * Props for the FilterPopover component - */ -export interface FilterPopoverProps { - open: boolean - onOpenChange: (open: boolean) => void - filters: TerminalFilters - toggleStatus: (status: 'error' | 'info') => void - toggleBlock: (blockId: string) => void - uniqueBlocks: BlockInfo[] - hasActiveFilters: boolean -} - -/** - * Filter popover component used in terminal header and output panel - */ -export const FilterPopover = memo(function FilterPopover({ - open, - onOpenChange, - filters, - toggleStatus, - toggleBlock, - uniqueBlocks, - hasActiveFilters, -}: FilterPopoverProps) { - return ( - - - - - e.stopPropagation()} - minWidth={160} - maxWidth={220} - maxHeight={300} - > - Status - toggleStatus('error')} - > -
- Error - - toggleStatus('info')} - > -
- Info - - - {uniqueBlocks.length > 0 && ( - <> - - Blocks - - {uniqueBlocks.map((block) => { - const isSelected = filters.blockIds.has(block.blockId) - - return ( - toggleBlock(block.blockId)} - > - - {block.blockName} - - ) - })} - - - )} - - - ) -}) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/filter-popover/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/filter-popover/index.ts deleted file mode 100644 index 3804f73bb57..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/filter-popover/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { FilterPopover, type FilterPopoverProps } from './filter-popover' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/index.ts index 2fdcd094f91..3041753fc06 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/index.ts @@ -1,5 +1,4 @@ export { EntryBlockTile, type EntryBlockTileProps } from './entry-block-tile' -export { FilterPopover, type FilterPopoverProps } from './filter-popover' export { LogRowContextMenu, type LogRowContextMenuProps } from './log-row-context-menu' export { OutputPanel, type OutputPanelProps } from './output-panel' export { StatusDisplay, type StatusDisplayProps } from './status-display' diff --git a/apps/sim/blocks/blocks/logs.test.ts b/apps/sim/blocks/blocks/logs.test.ts index 58b1734845b..1d0e5a356a6 100644 --- a/apps/sim/blocks/blocks/logs.test.ts +++ b/apps/sim/blocks/blocks/logs.test.ts @@ -1,13 +1,7 @@ /** * @vitest-environment node */ -import { describe, expect, it, vi } from 'vitest' - -vi.mock('@/lib/workflows/subblocks/options', () => ({ - fetchTriggerTypeOptions: vi.fn(), - fetchWorkspaceWorkflowOptions: vi.fn(), -})) - +import { describe, expect, it } from 'vitest' import { LogsV2Block } from '@/blocks/blocks/logs' function buildQueryParams(params: Record) { diff --git a/apps/sim/lib/compare/data/feature-catalog.ts b/apps/sim/lib/compare/data/feature-catalog.ts deleted file mode 100644 index fdaf554dec1..00000000000 --- a/apps/sim/lib/compare/data/feature-catalog.ts +++ /dev/null @@ -1,939 +0,0 @@ -import type { SimFeature } from '@/lib/compare/data/types' - -/** - * Sim's full feature catalog, sourced directly from the codebase (file paths - * as of the compare-pages-data worktree, checked 2026-07-02). This is a - * superset of {@link ComparisonFacts}. A page builder can filter this list - * by {@link SimFeature.category} or tag to assemble the subset relevant to - * a specific "Sim vs X" page, without re-deriving facts each time. - * - * Absence is recorded as honestly as presence: entries tagged "not-found" - * document a capability that was searched for and does not currently exist, - * so a future page builder doesn't have to re-verify it. - */ -export const SIM_FEATURES: SimFeature[] = [ - // ---- deployment-api ---------------------------------------------------- - { - id: 'deploy-versioned-rest-api', - name: 'Deploy a workflow as a versioned REST API', - category: 'deployment-api', - tags: ['api', 'enterprise'], - description: - 'Workflows deploy/undeploy via POST/DELETE on /api/v1/workflows/[id]/deploy. Each deploy creates an immutable, numbered entry in a workflow_deployment_version table (state snapshot, isActive flag); executions, webhooks, and schedules all pin to the exact deployed version that ran, so draft edits never affect live traffic until redeployed. Rollback to a prior version is a first-class action.', - competitiveNote: - 'The draft/deployed split with per-version execution pinning is more explicit than a simple "publish" toggle. Live traffic is isolated from in-progress edits by construction, not by convention.', - sources: [ - { - url: 'https://docs.sim.ai/execution/api', - label: 'Sim Docs: External API', - asOf: '2026-07-02', - }, - { - url: 'https://github.com/simstudioai/sim/blob/main/packages/db/schema.ts', - label: 'Sim codebase: workflow_deployment_version table', - asOf: '2026-07-02', - }, - ], - }, - { - id: 'streaming-api-responses', - name: 'Streaming API responses (SSE)', - category: 'deployment-api', - tags: ['api'], - description: - 'Workflow execution can stream over Server-Sent Events by passing a stream body param or X-Stream-Response header, returning stream:chunk/stream:done events. A separate reconnect/replay endpoint lets a client resume a stream from a given event id, backed by an event buffer, capped at 55 minutes. Agent-block responses stream per-provider through a shared StreamingExecution wrapper.', - competitiveNote: - 'Streaming plus a resumable/replayable event buffer is a level of durability beyond a plain SSE passthrough. A dropped client connection does not lose the run.', - sources: [ - { - url: 'https://docs.sim.ai/execution/api', - label: 'Sim Docs: External API', - asOf: '2026-07-02', - }, - { - url: 'https://github.com/simstudioai/sim/blob/main/apps/sim/app/api/workflows/[id]/executions/[executionId]/stream/route.ts', - label: 'Sim codebase: stream reconnect/replay route', - asOf: '2026-07-02', - }, - ], - }, - { - id: 'chat-deployment-surface', - name: 'Deploy a workflow as an embeddable public chat', - category: 'deployment-api', - tags: ['api'], - description: - 'A chat_trigger block plus /api/chat routes let a workflow be deployed as a public or gated (password/email/SSO) chat endpoint, addressable by a custom subdomain/identifier, independent of the REST API deployment.', - sources: [ - { - url: 'https://docs.sim.ai/workflows/deployment/chat', - label: 'Sim Docs - Chat Deployment', - asOf: '2026-07-02', - }, - { - url: 'https://docs.sim.ai/workflows/deployment/chat', - label: 'Sim Docs - Chat Deployment', - asOf: '2026-07-02', - }, - ], - }, - { - id: 'inbound-webhook-trigger', - name: 'Generic inbound webhook trigger', - category: 'deployment-api', - tags: ['api'], - description: - 'A generic_webhook trigger accepts any HTTP method with optional Bearer/header auth, payload-path-based idempotency (7-day dedup window), and configurable response mode/status/body. Usable without a pre-built app-specific integration.', - sources: [ - { - url: 'https://docs.sim.ai/triggers/webhook', - label: 'Sim Docs: Webhook Trigger', - asOf: '2026-07-02', - }, - ], - }, - { - id: 'api-key-auth-and-rate-limiting', - name: 'API-key auth with plan-based rate limiting', - category: 'deployment-api', - tags: ['api', 'enterprise'], - description: - 'The public v1 API authenticates via an x-api-key header (personal or workspace-scoped keys); workspace-scoped keys are restricted to their workspace, and personal keys can be disabled per-workspace. Rate limits are keyed by subscription plan and per-endpoint, with standard X-RateLimit-* headers and 429/Retry-After on exceed.', - sources: [ - { - url: 'https://docs.sim.ai/api-reference/authentication', - label: 'Sim Docs: API Authentication', - asOf: '2026-07-02', - }, - { - url: 'https://docs.sim.ai/execution/api', - label: 'Sim Docs: External API (rate limits)', - asOf: '2026-07-02', - }, - ], - }, - { - id: 'official-sdk', - name: 'Official client SDK', - category: 'deployment-api', - tags: ['not-found'], - description: - 'No official JS/Python (or other language) client SDK exists in the repo or as a published package. The public API is REST-only, consumed via a plain x-api-key header. This is recorded as an honest gap, not inferred.', - sources: [], - }, - - // ---- human-in-the-loop -------------------------------------------------- - { - id: 'human-in-the-loop-approval-block', - name: 'Human-in-the-loop approval block', - category: 'human-in-the-loop', - tags: ['enterprise'], - description: - 'A dedicated human_in_the_loop block pauses workflow execution and waits for a human to submit a "Resume Form," with configurable display data and notification tool calls (e.g. Slack, email) fired on pause. A separate wait block supports plain time-based pauses (in-process ≤5 min, or a persisted async pause ≤30 days) without requiring human input.', - competitiveNote: - 'This is a first-class, deeply implemented capability, not a workaround built from generic wait/poll nodes.', - sources: [ - { - url: 'https://docs.sim.ai/blocks/human-in-the-loop', - label: 'Sim Docs: Human in the Loop Block', - asOf: '2026-07-02', - }, - ], - }, - { - id: 'durable-pause-resume-execution', - name: 'Durable pause/resume via execution snapshots', - category: 'human-in-the-loop', - tags: ['enterprise'], - description: - 'Paused runs persist their full execution state (ExecutionSnapshot) to the database, independent of any third-party durable-execution service. Resume happens via a public per-execution resume URL (API + UI), supporting sync, streaming, or async job-queue-dispatched resume; an approver opens a link (surfaced via the notification tool call) rather than needing product access.', - sources: [ - { - url: 'https://github.com/simstudioai/sim/blob/main/apps/sim/executor/execution/snapshot.ts', - label: 'Sim codebase: execution snapshot serializer', - asOf: '2026-07-02', - }, - { - url: 'https://docs.sim.ai/blocks/human-in-the-loop', - label: 'Sim Docs - Human-in-the-Loop Block', - asOf: '2026-07-02', - }, - ], - }, - - // ---- enterprise-governance ---------------------------------------------- - { - id: 'sso-saml-oidc', - name: 'SSO (SAML and OIDC)', - category: 'enterprise-governance', - tags: ['enterprise', 'security'], - description: - "SSO is implemented via better-auth's sso plugin, supporting both SAML and OIDC configs per provider. Registration requires an Enterprise-plan org, org owner/admin role, and DNS-validated domain ownership (no cross-org domain squatting). Self-hostable via an SSO_ENABLED flag, independent of the hosted Enterprise-plan gate.", - sources: [ - { - url: 'https://docs.sim.ai/platform/enterprise/sso', - label: 'Sim Docs: Single Sign-On (SSO)', - asOf: '2026-07-02', - }, - { - url: 'https://docs.sim.ai/platform/enterprise/sso', - label: 'Sim Docs - Single Sign-On (SSO)', - asOf: '2026-07-02', - }, - ], - }, - { - id: 'org-session-policies', - name: 'Organization session policies', - category: 'enterprise-governance', - tags: ['enterprise', 'security'], - description: - 'Enterprise organization owners and admins can cap max session lifetime (1 to 8,760 hours from sign-in, regardless of activity) and set an idle timeout (48 to 8,760 hours without activity), applied to every member on every device. Both limits are optional; the default is a 30-day session that extends automatically while a member stays active. A separate "Sign out all members" action revokes every member session in the organization except the acting admin\'s own.', - competitiveNote: - 'Automation platforms commonly ship SSO without any session-lifetime control of their own, leaving re-authentication cadence entirely to the upstream identity provider.', - sources: [ - { - url: 'https://docs.sim.ai/platform/enterprise/session-policies', - label: 'Sim Docs: Session Policies', - asOf: '2026-08-10', - }, - ], - }, - { - id: 'scim-directory-sync', - name: 'SCIM / automated directory sync', - category: 'enterprise-governance', - tags: ['not-found'], - description: - 'No SCIM table, route, or plugin exists. User provisioning is invite-based only. There is no automated push-provisioning from an identity provider (e.g. Okta/Azure AD SCIM).', - sources: [], - }, - { - id: 'org-admin-console', - name: 'Org-level team management console', - category: 'enterprise-governance', - tags: ['enterprise'], - description: - 'Org owner/admins manage seats, invite/remove members, transfer ownership, and view billing in a Team Management settings surface. Roles are binary (admin/member) at the team level. There is no granular custom-role RBAC beyond that.', - sources: [ - { - url: 'https://docs.sim.ai/permissions/roles-and-permissions', - label: 'Sim Docs: Roles and Permissions', - asOf: '2026-07-02', - }, - ], - }, - { - id: 'per-member-usage-limits', - name: 'Org-pooled and per-member usage limits', - category: 'enterprise-governance', - tags: ['enterprise'], - description: - 'Usage governance supports both an org-level pooled cap (organization.orgUsageLimit) and individual per-member overrides (organizationMemberUsageLimit, keyed by org+user, with an auditable setBy field recording which admin set the limit).', - sources: [ - { - url: 'https://github.com/simstudioai/sim/blob/main/packages/db/schema.ts', - label: 'Sim codebase: orgUsageLimit / organizationMemberUsageLimit', - asOf: '2026-07-02', - }, - ], - }, - { - id: 'audit-log-siem-export', - name: 'Audit logging with SIEM/warehouse export', - category: 'enterprise-governance', - tags: ['enterprise', 'security'], - description: - 'A customer-facing audit-logs API (org admin/owner + active Enterprise plan required) supports filtering by action/resource/actor/date range with cursor pagination. Beyond in-product viewing, a generic "data drains" dispatcher can continuously stream audit logs (and workflow logs) to Datadog, S3, GCS, Azure Blob, BigQuery, Snowflake, or a generic webhook, with encryption at rest.', - competitiveNote: - 'Continuous SIEM/warehouse export across six destination types is materially deeper than a downloadable CSV export.', - sources: [ - { - url: 'https://docs.sim.ai/enterprise/audit-logs', - label: 'Sim Docs: Audit Logs', - asOf: '2026-07-02', - }, - { - url: 'https://docs.sim.ai/platform/enterprise/data-drains', - label: 'Sim Docs: Data Drains', - asOf: '2026-07-02', - }, - ], - }, - { - id: 'admin-api-self-hosted-gitops', - name: 'Separate admin API for self-hosted GitOps', - category: 'enterprise-governance', - tags: ['enterprise', 'self-hosted'], - description: - 'A distinct /api/v1/admin/** surface (organizations, users, workspaces, subscriptions, credits, audit logs, workflows, folders, access control) is authenticated by a static ADMIN_API_KEY header rather than a user session, explicitly documented for self-hosted GitOps/scripting rather than interactive use.', - sources: [ - { - url: 'https://docs.sim.ai/platform/enterprise', - label: 'Sim Docs: Enterprise Admin API (x-admin-key)', - asOf: '2026-07-02', - }, - ], - }, - { - id: 'environment-promotion', - name: 'Dev/staging/prod environment promotion', - category: 'enterprise-governance', - tags: ['not-found'], - description: - 'No customer-facing deployment-environment concept (separate staging/prod deploy targets) exists. What does exist is versioned deploy/rollback of a single workflow (see deploy-versioned-rest-api) and per-user/per-workspace encrypted environment variable stores, which are secret stores, not deployment stages.', - sources: [], - }, - - // ---- knowledge-base-search ----------------------------------------------- - { - id: 'kb-connector-live-sync', - name: '51 knowledge-base source connectors with recurring sync', - category: 'knowledge-base-search', - tags: ['integrations'], - description: - 'Knowledge bases can sync documents from 51 external source connectors (including Google Drive, Notion, Confluence, SharePoint, S3, Slack, Salesforce, HubSpot, Jira, GitHub, Zendesk, and more). Sync is interval-based and recurring, not one-time import, via a syncIntervalMinutes/nextSyncAt schedule (default daily) polled by a cron endpoint every 5 minutes, with a per-run sync log (docs added/updated/deleted/failed) and stale-lock recovery. Manual on-demand re-sync is also supported. No push/webhook-driven re-sync (e.g. Drive change notifications) was found. The mechanism is polling-based.', - sources: [ - { - url: 'https://github.com/simstudioai/sim/blob/main/apps/sim/connectors/registry.ts', - label: 'Sim codebase: connector registry (51 connectors)', - asOf: '2026-07-02', - }, - { - url: 'https://docs.sim.ai/knowledgebase/connectors', - label: 'Sim Docs - Knowledge Base Connectors', - asOf: '2026-07-02', - }, - ], - }, - { - id: 'kb-hybrid-search', - name: 'Hybrid semantic + keyword knowledge-base search', - category: 'knowledge-base-search', - tags: [], - description: - 'Knowledge base search combines pgvector embedding similarity with a generated tsvector full-text index; searching across knowledge bases with different embedding models is explicitly blocked to avoid meaningless cross-model comparisons.', - sources: [ - { - url: 'https://docs.sim.ai/tools/knowledge', - label: 'Sim Docs - Knowledge Base Tool', - asOf: '2026-07-02', - }, - ], - }, - { - id: 'global-product-search', - name: 'Unified cross-entity global search', - category: 'knowledge-base-search', - tags: ['not-found'], - description: - 'No single search spans workflows, execution logs, files, and knowledge bases together. What exists is a command-palette-style search modal scoped to blocks/tools/tool-operations/triggers/docs (for building workflows), plus separate page-local filters on Logs and Files.', - sources: [], - }, - - // ---- data-tables --------------------------------------------------------- - { - id: 'tables-builtin-database', - name: 'Tables: a built-in database module', - category: 'data-tables', - tags: ['data'], - description: - 'Tables store rows as flexible JSONB documents against a per-table JSON column schema (not fixed per-user Postgres tables), with fractional ordering keys and a GIN(jsonb_path_ops) index for containment queries. A REST API (internal and public v1) covers rows, columns, CSV import/export, and bulk jobs; a Table workflow block supports query/insert/upsert/update/delete/get-row/get-schema operations, and tables can themselves trigger workflow runs on new rows.', - competitiveNote: - 'Column types are simple (no spreadsheet-style formula/computed-column engine); "enrichment" is LLM-driven per-row/per-column-group enrichment, not formulas.', - sources: [ - { - url: 'https://github.com/simstudioai/sim/blob/main/packages/db/schema.ts', - label: 'Sim codebase: userTableDefinitions/userTableRows', - asOf: '2026-07-02', - }, - { - url: 'https://docs.sim.ai/integrations/table', - label: 'Sim Docs: Table integration', - asOf: '2026-07-02', - }, - ], - }, - { - id: 'tables-llm-enrichment', - name: 'LLM-driven row/column enrichment', - category: 'data-tables', - tags: ['ai'], - description: - 'Tables support per-row enrichment via LLM-backed "column groups". An enrichment run populates cells using an LLM given the row and column context, distinct from static spreadsheet formulas.', - sources: [ - { - url: 'https://github.com/simstudioai/sim/blob/main/apps/sim/lib/copilot/tools/server/enrichment/enrichment-run.ts', - label: 'Sim codebase: table enrichment run', - asOf: '2026-07-02', - }, - ], - }, - - // ---- files ----------------------------------------------------------------- - { - id: 'files-shared-team-store', - name: 'Shared, workspace-scoped file store', - category: 'files', - tags: ['data'], - description: - 'Files are stored in a genuinely shared, workspace-scoped store (not per-user), with nested folders and soft delete. A REST API (internal and public v1) covers upload/serve/manage. A File workflow block reads, writes, appends, fetches, compresses/decompresses, and manages sharing for files as workflow inputs or outputs.', - sources: [ - { - url: 'https://github.com/simstudioai/sim/blob/main/packages/db/schema.ts', - label: 'Sim codebase: workspaceFile/workspaceFileFolder', - asOf: '2026-07-02', - }, - { - url: 'https://docs.sim.ai/tools/file', - label: 'Sim Docs: File Tool', - asOf: '2026-07-02', - }, - ], - }, - { - id: 'files-rich-viewers', - name: 'Rich in-app file viewers and editors', - category: 'files', - tags: [], - description: - 'The Files module renders CSV, XLSX, PDF, DOCX, PPTX (sandboxed), images, Mermaid diagrams, and plain text/code inline, plus a dedicated rich WYSIWYG Markdown editor (not just a preview) for editing Markdown files in place.', - sources: [ - { - url: 'https://docs.sim.ai/files', - label: 'Sim Docs: Files', - asOf: '2026-07-02', - }, - ], - }, - { - id: 'copilot-virtual-filesystem', - name: 'Copilot virtual filesystem (VFS)', - category: 'files', - tags: ['ai'], - description: - "A distinct in-memory virtual filesystem abstraction lets the Copilot agent browse workspace resources (workflows, tables, docs) as file-like paths/tools. It reads from the Files module and table data but is a separate concept from a user's actual file store.", - sources: [ - { - url: 'https://github.com/simstudioai/sim/tree/main/apps/sim/lib/copilot/vfs/', - label: 'Sim codebase: copilot VFS', - asOf: '2026-07-02', - }, - ], - }, - - // ---- environments-enterprise (workspace forking / dev-qa-prod promote) ---- - { - id: 'workspace-fork-promote', - name: 'Fork a workspace and promote changes between environments', - category: 'environments-enterprise', - tags: ['enterprise', 'flagship'], - description: - 'A whole workspace (not a single workflow) can be forked to create a dev/qa/prod-style child environment. Deployed workflows are cloned into the child (left undeployed there), with an optional copy of files, tables, knowledge bases, custom tools, skills, and MCP server configs. Changes can then be synced bidirectionally between the parent and child ("promote": push parent→child or pull child→parent), with a diff preview before applying and a stored snapshot enabling one-level rollback of each promote run.', - competitiveNote: - 'This is a genuine git-like fork/diff/promote/rollback system scoped to an entire workspace, not a single-workflow versioning feature. Most workflow-automation competitors only version individual workflows, not whole environments with cross-environment resource remapping.', - sources: [ - { - url: 'https://docs.sim.ai/platform/enterprise/forks', - label: 'Sim Docs: Workspace Forks', - asOf: '2026-07-08', - }, - ], - }, - { - id: 'fork-credential-remapping', - name: 'Per-environment credential and env-var remapping on promote', - category: 'environments-enterprise', - tags: ['enterprise', 'security', 'flagship'], - description: - "Forking never copies credentials. All credential references are cleared in the child workspace at creation time. Instead, an admin explicitly maps each source OAuth/service-account credential to the target workspace's own credential via a dedicated mapping UI/API before promoting; environment variables remap by name (including rewriting {{ENV_KEY}} references inside copied custom-tool code or MCP headers if renamed, e.g. SLACK_API_KEY → SLACK_API_KEY_TEST). Credential and env-var mappings are required. An unmapped one blocks the promote rather than silently syncing a secret across environments, while optional resources (knowledge bases, tables, files, MCP servers) clear gracefully if unmapped.", - competitiveNote: - 'Treating credentials/env-vars as required-and-blocking on promote (rather than silently copying secrets) is a specific, auditable safety design for enterprise dev→qa→prod pipelines.', - sources: [ - { - url: 'https://docs.sim.ai/platform/enterprise/forks', - label: 'Sim Docs: Workspace Forks (mappings)', - asOf: '2026-07-08', - }, - ], - }, - { - id: 'fork-enterprise-gating', - name: 'Workspace forking gated to Enterprise plan (or self-hosted flag)', - category: 'environments-enterprise', - tags: ['enterprise'], - description: - 'Forking/promotion is gated on the billed account having Enterprise-tier access on hosted Sim, mirroring the same access-gate pattern used for SSO. Self-hosted deployments can enable it independent of billing via a FORKING_ENABLED/NEXT_PUBLIC_FORKING_ENABLED environment flag.', - sources: [ - { - url: 'https://docs.sim.ai/platform/enterprise/forks', - label: 'Sim Docs: Workspace Forks (self-hosted setup)', - asOf: '2026-07-08', - }, - ], - }, - - // ---- version-control ------------------------------------------------------ - { - id: 'copilot-checkpoint-revert', - name: 'Server-persisted checkpoint/revert for AI-driven edits', - category: 'version-control', - tags: ['ai'], - description: - 'Before and after each Copilot AI edit to a workflow, a full canvas-state snapshot is saved server-side (keyed by user/workflow/chat/message) and can be restored via a revert endpoint or browsed via a checkpoint list. This is real server-side point-in-time restore, but scoped to Copilot-driven sessions. Manual drag-and-drop edits are not autosaved server-side.', - sources: [ - { - url: 'https://github.com/simstudioai/sim/blob/main/packages/db/schema.ts', - label: 'Sim codebase: workflowCheckpoints table', - asOf: '2026-07-02', - }, - { - url: 'https://github.com/simstudioai/sim/blob/main/apps/sim/app/api/copilot/checkpoints/revert/route.ts', - label: 'Sim codebase: checkpoint revert API', - asOf: '2026-07-02', - }, - ], - }, - { - id: 'ai-edit-visual-diff', - name: 'Visual diff with accept/reject for AI-proposed changes', - category: 'version-control', - tags: ['ai'], - description: - 'A dedicated diff engine computes added/edited/deleted blocks and edges (plus field-level diffs) between the live workflow and a Copilot-proposed change, rendered with an accept/reject UI before the change is applied. This diff view is scoped to Copilot-proposed edits vs. the current baseline. There is no user-facing tool to diff two arbitrary past deployment versions against each other.', - sources: [ - { - url: 'https://github.com/simstudioai/sim/blob/main/apps/sim/lib/workflows/diff/diff-engine.ts', - label: 'Sim codebase: WorkflowDiffEngine', - asOf: '2026-07-02', - }, - ], - }, - { - id: 'manual-edit-history', - name: 'Persisted history for manual (non-AI) canvas edits', - category: 'version-control', - tags: ['not-found'], - description: - 'Undo/redo for manual drag-and-drop editing is client-side only (localStorage-persisted, capped at 100 ops / 5 stacks per browser). It is not synced across devices or recoverable server-side. There is no autosave history timeline or arbitrary-version diff/compare tool for manual edits, and no per-workflow git-like branch/merge model (only the workspace-level fork/promote system, cataloged separately). Knowledge base documents have no version/history tracking at all.', - sources: [ - { - url: 'https://github.com/simstudioai/sim/blob/main/apps/sim/stores/undo-redo/store.ts', - label: 'Sim codebase: client-side undo/redo store', - asOf: '2026-07-02', - }, - ], - }, - - // ---- durability-observability --------------------------------------------- - { - id: 'otel-telemetry', - name: 'OpenTelemetry-instrumented execution telemetry', - category: 'durability-observability', - tags: [], - description: - 'Sim ships real OpenTelemetry instrumentation (NodeSDK, batched OTLP trace/metric export, sampling) covering generative-AI, copilot, and tool-execution spans. This is aimed at product/ops-level observability (togglable by the user, and disableable via NEXT_TELEMETRY_DISABLED) rather than a customer-facing per-execution trace-waterfall UI inside the product. Block-level execution timing is tracked via start/end timestamps on execution logs, not an exposed span tree.', - sources: [ - { - url: 'https://github.com/simstudioai/sim/blob/main/apps/sim/instrumentation-node.ts', - label: 'Sim codebase: OTel NodeSDK setup', - asOf: '2026-07-02', - }, - ], - }, - { - id: 'execution-stats-dashboard', - name: 'Aggregate execution stats (success rate, avg duration)', - category: 'durability-observability', - tags: [], - description: - 'A stats API buckets execution logs into time segments and returns total/successful/failed execution counts, average duration, and overall success rate per workflow and in aggregate. This covers averages and error-rate only. There is no p50/p95/p99 latency percentile view or a dedicated cost-over-time chart in this endpoint.', - sources: [ - { - url: 'https://docs.sim.ai/execution/logging', - label: 'Sim Docs - Logging', - asOf: '2026-07-02', - }, - ], - }, - { - id: 'workspace-event-trigger-for-alerting', - name: 'Build custom failure/cost-spike alerting via a workspace-event trigger', - category: 'durability-observability', - tags: [], - description: - "There is no turnkey 'email me when a run fails' checkbox. Instead, a sim_workspace_event trigger fires on Sim's own platform events (run success/failure, deployments, cost/latency spikes), which a user can wire to any notification block (Slack, email, generic webhook, SMTP) to build custom alerting. The primitive exists, but it is build-it-yourself, not a pre-built alert rule UI.", - sources: [ - { - url: 'https://docs.sim.ai/workflows/triggers/sim', - label: 'Sim Docs: Sim Workspace Events trigger', - asOf: '2026-07-02', - }, - ], - }, - { - id: 'deliberate-no-auto-retry', - name: 'Deliberate no-automatic-retry execution model', - category: 'durability-observability', - tags: [], - description: - "Background job retries are explicitly disabled at the infrastructure layer (maxAttempts: 1) by design; durability instead comes from app-level bookkeeping. Scheduled executions track consecutive infrastructure-failure counts and auto-disable a schedule after a threshold, distinguishing infra failures from business-logic failures. There is no automatic block-level retry loop, no idempotency-key-based exactly-once block execution, no dead-letter queue for failed runs, and no 'replay a past execution with its original inputs' feature. The only checkpoint/resume path is the human-in-the-loop pause/resume mechanism (cataloged separately).", - sources: [ - { - url: 'https://github.com/simstudioai/sim/blob/main/apps/sim/trigger.config.ts', - label: 'Sim codebase: retries.default.maxAttempts = 1', - asOf: '2026-07-02', - }, - { - url: 'https://docs.sim.ai/triggers/schedule', - label: 'Sim Docs: Schedule Trigger (Automatic Disabling)', - asOf: '2026-07-02', - }, - ], - }, - { - id: 'per-model-call-cost-attribution', - name: 'Cost/token tracking per model-call event', - category: 'durability-observability', - tags: [], - description: - 'Usage log rows carry execution/workflow/workspace IDs plus input/output token counts and tool cost, categorized as model/fixed/tool spend, giving cost attribution per model-call event within an execution. This approximates per-block cost for single-call agent blocks, but attribution below the model-call level (e.g. disambiguating multiple tool calls inside one agent block) is not confirmed as a distinct column.', - sources: [ - { - url: 'https://docs.sim.ai/execution/logging', - label: 'Sim Docs: Logging', - asOf: '2026-07-02', - }, - ], - }, - - // ---- generative-media ------------------------------------------------------- - { - id: 'image-generation-multi-provider', - name: 'Image generation across 4 provider families', - category: 'generative-media', - tags: ['ai'], - description: - "A dedicated Image Generator block supports OpenAI (GPT Image 1.5/1/1 Mini, DALL-E 3), Google Gemini 'Nano Banana' image models, and (via a Fal.ai multi-model proxy) Nano Banana 2/Pro, Seedream 4.5, FLUX 2 Pro, and Grok Imagine Image. Stability AI, Midjourney, Ideogram, and Recraft are not integrated.", - sources: [ - { - url: 'https://github.com/simstudioai/sim/blob/main/apps/sim/blocks/blocks/image_generator.ts', - label: 'Sim codebase: Image Generator V2 block', - asOf: '2026-07-02', - }, - ], - }, - { - id: 'video-generation-multi-provider', - name: 'Video generation across 5+ provider families', - category: 'generative-media', - tags: ['ai'], - description: - 'A dedicated Video Generator block supports Runway Gen-4, Google Veo (3 / 3 Fast / 3.1 / 3.1 Fast), Luma Dream Machine (Ray 2), MiniMax Hailuo (2.3 / 02), and (via a Fal.ai multi-model proxy) Sora 2 / Sora 2 Pro, ByteDance Seedance 2.0, Kling (3.0 Pro/4K, O3 Pro/4K, 2.5/2.1 Turbo Pro), WAN 2.1/2.2, and LTX-family models. HeyGen and Pika are not integrated.', - competitiveNote: - 'Depth here (5+ first-party providers plus a multi-model proxy spanning a dozen more video models) is unusually broad for a workflow-automation platform. This is typically the domain of dedicated media-gen tools, not general automation builders.', - sources: [ - { - url: 'https://github.com/simstudioai/sim/blob/main/apps/sim/blocks/blocks/video_generator.ts', - label: 'Sim codebase: Video Generator V3 block', - asOf: '2026-07-02', - }, - ], - }, - { - id: 'text-to-speech-multi-provider', - name: 'Text-to-speech across 7 provider families', - category: 'generative-media', - tags: ['ai'], - description: - 'A dedicated TTS block supports OpenAI TTS, Deepgram Aura, ElevenLabs, Cartesia Sonic, Google Cloud TTS, Azure TTS, and PlayHT. A separate dedicated ElevenLabs block additionally covers sound effects, speech-to-speech, and audio isolation.', - sources: [ - { - url: 'https://github.com/simstudioai/sim/blob/main/apps/sim/blocks/blocks/tts.ts', - label: 'Sim codebase: TTS block', - asOf: '2026-07-02', - }, - ], - }, - { - id: 'speech-to-text-multi-provider', - name: 'Speech-to-text across 5 provider families', - category: 'generative-media', - tags: ['ai'], - description: - 'A dedicated STT block supports OpenAI Whisper, Deepgram (Nova 3/2/Whisper Large), ElevenLabs Scribe, AssemblyAI, and Google Gemini transcription variants.', - sources: [ - { - url: 'https://github.com/simstudioai/sim/blob/main/apps/sim/blocks/blocks/stt.ts', - label: 'Sim codebase: STT block', - asOf: '2026-07-02', - }, - ], - }, - - // ---- control-flow-execution --------------------------------------------------- - { - id: 'control-flow-primitives', - name: 'Conditional branching, LLM-based routing, loops, and parallel execution', - category: 'control-flow-execution', - tags: [], - description: - 'Beyond simple if/else (Condition block), a Router block lets an LLM semantically pick the next path among candidate downstream blocks rather than evaluating a boolean. Loop and Parallel are canvas subflow containers (not single blocks) supporting for-each/while-style iteration and concurrent fan-out branching, respectively.', - sources: [ - { - url: 'https://docs.sim.ai/workflows/blocks/router', - label: 'Sim Docs: Router block', - asOf: '2026-07-02', - }, - { - url: 'https://docs.sim.ai/blocks/loop', - label: 'Sim Docs: Loop Block', - asOf: '2026-07-02', - }, - ], - }, - { - id: 'nested-sub-workflow-invocation', - name: 'Invoke another workflow as a step (nested sub-workflows)', - category: 'control-flow-execution', - tags: [], - description: - "A Workflow block lets one Sim workflow call another as a single step, passing an input variable exposed as the child's start input, enabling composable, reusable sub-workflows.", - sources: [ - { - url: 'https://docs.sim.ai/workflows/blocks/workflow', - label: 'Sim Docs: Workflow (sub-workflow) block', - asOf: '2026-07-02', - }, - ], - }, - { - id: 'sandboxed-code-execution', - name: 'Sandboxed JavaScript and Python code execution', - category: 'control-flow-execution', - tags: [], - description: - 'A Function block runs arbitrary JavaScript, Python, or Shell. Import-free JavaScript stays in a fast local isolated runtime; JavaScript with imports, all Python, and all Shell run in a remote sandbox provider (E2B or Daytona), defaulting to a dedicated Function base image that ships a data-science stack and generic CLIs such as jq, yq, ripgrep, and sqlite3.', - sources: [ - { - url: 'https://docs.sim.ai/workflows/blocks/function', - label: 'Sim Docs: Function Block', - asOf: '2026-08-10', - }, - ], - }, - { - id: 'configurable-workspace-sandboxes', - name: 'Configurable workspace code sandboxes', - category: 'control-flow-execution', - tags: ['enterprise'], - description: - 'A workspace maintains named sandboxes, each declaring a language, its pip or npm dependencies, optional Debian/APT system packages, and optional managed CLI tools from a catalog grouped by cloud, Kubernetes, infrastructure, deployment, data and storage, and security tools. A Function block selects one and its code can import those dependencies and run those commands. Only workspace admins can create or edit them, and on sim.ai they require an active Max or Enterprise plan. There, each specification is prebuilt into a reusable image so runs pay no install cost, and identical specifications share one build; a self-hosted deployment using Daytona installs the specification at the start of every run instead, since prebuilt images require E2B. Managed CLIs use pinned, integrity-checked vendor artifacts.', - competitiveNote: - 'Most automation platforms run custom code on a fixed image whose dependency set the vendor controls, so an unlisted package or vendor CLI is simply unavailable without leaving the platform.', - sources: [ - { - url: 'https://docs.sim.ai/workflows/blocks/function#sandboxes', - label: 'Sim Docs: Function block - Sandboxes', - asOf: '2026-08-10', - }, - ], - }, - { - id: 'browser-automation-blocks', - name: 'Browser automation and web scraping (multiple engines)', - category: 'control-flow-execution', - tags: [], - description: - 'Dedicated blocks cover natural-language browser automation (Browser Use: navigate + act), structured or agentic web extraction (Stagehand), and full crawl/scrape/map/extract operations (Firecrawl), alongside additional named scraping/search integrations (Apify, Bright Data, Linkup, Jina).', - sources: [ - { - url: 'https://docs.sim.ai/tools/browser_use', - label: 'Sim Docs: Browser Use Integration', - asOf: '2026-07-02', - }, - { - url: 'https://docs.sim.ai/tools/firecrawl', - label: 'Sim Docs: Firecrawl Integration', - asOf: '2026-07-02', - }, - ], - }, - { - id: 'guardrails-and-evaluator-blocks', - name: 'Guardrails and LLM-judge evaluator blocks', - category: 'control-flow-execution', - tags: [], - description: - 'A Guardrails block covers JSON-validity checks, regex validation, RAG/hallucination scoring (0-10 with reasoning), and PII detection/masking. An Evaluator block scores content against user-defined named metrics via an LLM judge. These are per-call scoring/validation primitives. There is no batch golden-dataset eval-suite runner or A/B prompt-testing harness in the block library.', - sources: [ - { - url: 'https://docs.sim.ai/blocks/guardrails', - label: 'Sim Docs: Guardrails Block', - asOf: '2026-07-02', - }, - { - url: 'https://docs.sim.ai/blocks/evaluator', - label: 'Sim Docs: Evaluator Block', - asOf: '2026-07-02', - }, - ], - }, - { - id: 'memory-and-variable-blocks', - name: 'Cross-turn memory and workflow-scoped variables', - category: 'control-flow-execution', - tags: [], - description: - 'A Memory block stores/retrieves records keyed by conversation ID for injecting artificial memory into agent blocks (which also have native memory modes of their own). A Variables block provides a workflow-scoped variable store shared across Variables blocks within a single run (not persisted across separate runs). Third-party Mem0 and Zep memory-service integrations are also available as blocks.', - sources: [ - { - url: 'https://docs.sim.ai/integrations/memory', - label: 'Sim Docs: Memory integration', - asOf: '2026-07-02', - }, - { - url: 'https://docs.sim.ai/workflows/blocks/variables', - label: 'Sim Docs: Variables block', - asOf: '2026-07-02', - }, - ], - }, - { - id: 'agent-to-agent-interop', - name: 'Agent-to-Agent (A2A) protocol client', - category: 'control-flow-execution', - tags: [], - description: - "An A2A block lets a Sim workflow act as a client to any Agent-to-Agent-protocol-compliant external agent: send a message, get/cancel a task, and fetch the remote agent's Agent Card. This is distinct from MCP (tool-calling protocol). A2A is agent-to-agent messaging.", - sources: [ - { - url: 'https://github.com/simstudioai/sim/blob/main/apps/sim/blocks/blocks/a2a.ts', - label: 'Sim codebase: A2A block', - asOf: '2026-07-02', - }, - ], - }, - - // ---- enterprise-governance (permission groups, follow-up findings) -------- - { - id: 'permission-group-model-and-tool-governance', - name: 'Permission groups: per-role model and tool allow/deny lists', - category: 'enterprise-governance', - tags: ['enterprise', 'security'], - description: - 'Beyond workspace-level admin/write/read roles, an Enterprise "permission group" config can allow-list or deny-list specific LLM providers/models a role may use, and separately deny specific tools/integrations (or disable all MCP or custom tools) for that role. E.g. allow Slack but deny Salesforce, or allow OpenAI but deny a specific Ollama model. Enforced server-side at execution time (agent, evaluator, and router blocks), not just in the UI.', - sources: [ - { - url: 'https://docs.sim.ai/permissions/roles-and-permissions', - label: 'Sim Docs: Roles and Permissions', - asOf: '2026-07-02', - }, - { - url: 'https://docs.sim.ai/permissions/roles-and-permissions', - label: 'Sim Docs: Roles and Permissions', - asOf: '2026-07-02', - }, - ], - }, - { - id: 'log-retention-window-and-pii-redaction', - name: 'Configurable log-retention window with PII redaction', - category: 'enterprise-governance', - tags: ['enterprise', 'security'], - description: - 'An Enterprise-gated feature lets an org configure how long execution logs are retained and enable Presidio-based redaction of PII from logged inputs/outputs. This is a log-retention/redaction policy, not a "zero data retention" mode for LLM providers. It does not affect whether a model provider itself retains prompts.', - sources: [ - { - url: 'https://docs.sim.ai/platform/enterprise', - label: 'Sim Docs: Enterprise (Data Retention)', - asOf: '2026-07-02', - }, - ], - }, - { - id: 'zero-data-retention-llm-mode', - name: 'Zero-data-retention (ZDR) mode for LLM calls', - category: 'enterprise-governance', - tags: ['not-found'], - description: - 'No "zero data retention" or "incognito" mode exists for how Sim itself handles LLM requests (i.e. no documented ZDR agreements with model providers, no request-level opt-out of provider-side retention). The only genuine ZDR references in the codebase describe a competitor\'s offering in this same comparison dataset.', - sources: [], - }, - { - id: 'ai-gateway-proxy-routing', - name: 'Governed AI request proxy/gateway', - category: 'enterprise-governance', - tags: ['not-found'], - description: - "Model calls go directly from the execution environment to each provider's API (after a permission-group pre-call gate), not through a dedicated policy-enforcing AI gateway/proxy layer.", - sources: [], - }, - { - id: 'dynamic-agent-tool-discovery', - name: 'Dynamic (browse-and-pick) tool use by agents', - category: 'ai-capabilities', - tags: ['not-found'], - description: - 'An Agent block can only call tools the workflow author explicitly attached to it at build time. It cannot browse and choose from a broader pool (e.g. an MCP server\'s full tool catalog, or "every tool in the workspace") at inference time. Runtime MCP discovery exists but only refreshes the schema of an already-configured tool.', - sources: [ - { - url: 'https://github.com/simstudioai/sim/blob/main/apps/sim/executor/handlers/agent/agent-handler.ts', - label: 'Sim codebase: agent tool resolution (pre-wired only)', - asOf: '2026-07-02', - }, - ], - }, - { - id: 'automatic-model-fallback', - name: 'Automatic LLM model/provider fallback', - category: 'ai-capabilities', - tags: ['not-found'], - description: - 'A failed or rate-limited LLM call is not automatically retried against a different model or provider; the error is thrown rather than retried with a fallback model.', - sources: [ - { - url: 'https://github.com/simstudioai/sim/blob/main/apps/sim/providers/index.ts', - label: 'Sim codebase: executeProviderRequest (no retry/fallback)', - asOf: '2026-07-02', - }, - ], - }, - - // ---- files (artifact generation, follow-up findings) ----------------------- - { - id: 'copilot-document-artifact-generation', - name: 'Copilot-generated document artifacts (decks, docs, spreadsheets)', - category: 'files', - tags: [], - description: - 'Copilot has an internal document-compilation tool that runs Python (python-pptx/python-docx/openpyxl) or Node (pptxgenjs/docx) in a dedicated E2B sandbox to produce real .pptx/.docx/.xlsx binaries, content-addressed and served back to the user.', - competitiveNote: - 'This capability is scoped to Copilot\'s own chat-assistant tool. It is not exposed as a configurable option in the workflow-builder Function block, so a workflow author cannot wire "generate a slide deck" into a reusable automation today, only ask Copilot for one interactively.', - sources: [ - { - url: 'https://github.com/simstudioai/sim/blob/main/apps/sim/lib/copilot/tools/server/files/doc-compile.ts', - label: 'Sim codebase: Copilot doc-compile tool', - asOf: '2026-07-02', - }, - ], - }, - { - id: 'workflow-builder-artifact-generation', - name: 'Workflow-author-facing artifact generation (decks/docs from a workflow step)', - category: 'files', - tags: ['not-found'], - description: - 'The canvas Function/code block does not expose the pptx/docx/xlsx-capable sandbox template, and there is no first-class "artifact" object (with versioning) anywhere in the codebase. Generating a shareable, versioned document from an ordinary workflow step is not currently possible outside asking Copilot directly.', - sources: [], - }, - { - id: 'native-end-user-forms', - name: 'Native end-user input forms (non-chat trigger surface)', - category: 'deployment-api', - tags: ['not-found'], - description: - 'There is no Sim-native "Forms" builder. A simple field-based input form a non-technical person fills out to trigger a workflow, distinct from the chat or API surfaces. Only third-party form integrations (Google Forms, Typeform, JSM forms) exist, which consume external form services rather than hosting a form within Sim.', - sources: [], - }, -] diff --git a/apps/sim/lib/compare/data/index.ts b/apps/sim/lib/compare/data/index.ts index aaf20b4444c..8ad58021459 100644 --- a/apps/sim/lib/compare/data/index.ts +++ b/apps/sim/lib/compare/data/index.ts @@ -18,7 +18,6 @@ export { tinesProfile } from '@/lib/compare/data/competitors/tines' export { vellumProfile } from '@/lib/compare/data/competitors/vellum' export { workatoProfile } from '@/lib/compare/data/competitors/workato' export { zapierProfile } from '@/lib/compare/data/competitors/zapier' -export { SIM_FEATURES } from '@/lib/compare/data/feature-catalog' export { simProfile } from '@/lib/compare/data/sim' export type { ComparisonFacts, @@ -26,7 +25,4 @@ export type { CompetitorProfile, Fact, FactSource, - FeatureCategory, - SimFeature, } from '@/lib/compare/data/types' -export { featuresByCategory, featuresByTag } from '@/lib/compare/data/types' diff --git a/apps/sim/lib/compare/data/types.ts b/apps/sim/lib/compare/data/types.ts index 03725a9a1ee..b974b845ccd 100644 --- a/apps/sim/lib/compare/data/types.ts +++ b/apps/sim/lib/compare/data/types.ts @@ -236,59 +236,3 @@ export interface CompetitorProfile { }> facts: ComparisonFacts } - -/** - * Broad grouping for {@link SimFeature} entries. A single feature catalog - * entry belongs to exactly one category, but can carry additional - * {@link SimFeature.tags} for cross-cutting filtering (e.g. an "enterprise" - * tag on a feature that's primarily categorized as "security-compliance"). - */ -export type FeatureCategory = - | 'deployment-api' - | 'human-in-the-loop' - | 'enterprise-governance' - | 'knowledge-base-search' - | 'data-tables' - | 'files' - | 'ai-capabilities' - | 'collaboration' - | 'observability' - | 'security-compliance' - | 'environments-enterprise' - | 'version-control' - | 'durability-observability' - | 'generative-media' - | 'control-flow-execution' - -/** - * One entry in Sim's full feature catalog. Deliberately more granular than - * {@link ComparisonFacts}, which only covers the small set of rows every - * competitor page needs. The catalog is the superset a page builder can - * filter down from (by category or tag) when a given "Sim vs X" page only - * wants to surface the features relevant to that competitor. - */ -export interface SimFeature { - /** kebab-case identifier, e.g. "streaming-api", "human-in-the-loop-approval". */ - id: string - /** Display name, e.g. "Streaming API responses". */ - name: string - category: FeatureCategory - /** Additional cross-cutting labels for filtering (e.g. "enterprise", "beta"). */ - tags: string[] - /** Neutral, factual description of what the feature does. */ - description: string - /** Optional note on why this is differentiated vs. the competitive landscape. Must stay factual, not promotional. */ - competitiveNote?: string - sources: FactSource[] -} - -export function featuresByCategory( - features: SimFeature[], - category: FeatureCategory -): SimFeature[] { - return features.filter((f) => f.category === category) -} - -export function featuresByTag(features: SimFeature[], tag: string): SimFeature[] { - return features.filter((f) => f.tags.includes(tag)) -} diff --git a/apps/sim/lib/uploads/providers/s3/client.test.ts b/apps/sim/lib/uploads/providers/s3/client.test.ts index 6b1e2e75b8d..5e1ddde0baf 100644 --- a/apps/sim/lib/uploads/providers/s3/client.test.ts +++ b/apps/sim/lib/uploads/providers/s3/client.test.ts @@ -85,13 +85,6 @@ vi.mock('@/lib/core/config/env', () => ({ typeof value === 'string' ? value.toLowerCase() === 'false' || value === '0' : value === false, })) -vi.mock('@/lib/uploads/setup', () => ({ - S3_CONFIG: { - bucket: 'test-bucket', - region: 'test-region', - }, -})) - vi.mock('@/lib/uploads/config', () => ({ S3_CONFIG: mockS3Config, S3_KB_CONFIG: { diff --git a/apps/sim/lib/workflows/application/duplicate-workflow.ts b/apps/sim/lib/workflows/application/duplicate-workflow.ts deleted file mode 100644 index fd6f01cf884..00000000000 --- a/apps/sim/lib/workflows/application/duplicate-workflow.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { AuditAction, AuditResourceType } from '@sim/audit' -import { type Principal, resolvePrincipalAttribution } from '@sim/auth/principal' -import { db } from '@sim/db' -import { generateRequestId } from '@/lib/core/utils/request' -import { notifyWorkflowUpdated } from '@/lib/realtime/notify' -import { defineAuthorizedWorkflowUseCase } from '@/lib/workflows/application/authorized-workflow-use-case' -import { resolveActiveWorkflowApplicationContext } from '@/lib/workflows/application/context' -import { workflowOperations } from '@/lib/workflows/application/operations' -import { assertedWorkflowWorkspaceId } from '@/lib/workflows/application/principal-scope' -import { duplicateWorkflow as duplicateWorkflowRecord } from '@/lib/workflows/persistence/duplicate' - -export interface DuplicateWorkflowInput { - sourceWorkflowId: string - assertedWorkspaceId?: string - folderId: string | null - name: string -} - -export const duplicateWorkflow = defineAuthorizedWorkflowUseCase({ - operation: workflowOperations.duplicate, - resolveContext: ({ principal, input }: { principal: Principal; input: DuplicateWorkflowInput }) => - resolveActiveWorkflowApplicationContext({ - workflowId: input.sourceWorkflowId, - assertedWorkspaceId: assertedWorkflowWorkspaceId(principal, input.assertedWorkspaceId), - }), - async execute({ principal, input, context }) { - const attribution = resolvePrincipalAttribution(principal, { - workspaceBillingOwnerUserId: context.billedAccountUserId, - }) - return db.transaction((tx) => - duplicateWorkflowRecord({ - sourceWorkflowId: context.workflowId, - userId: attribution.attributedUserId, - workspaceId: context.workspaceId, - folderId: input.folderId, - name: input.name, - requestId: generateRequestId(), - tx, - }) - ) - }, - projectAudit: ({ context, result }) => ({ - action: AuditAction.WORKFLOW_DUPLICATED, - resourceType: AuditResourceType.WORKFLOW, - resourceId: result.id, - resourceName: result.name, - description: `Duplicated workflow "${context.workflow.name}" as "${result.name}"`, - metadata: { sourceWorkflowId: context.workflowId, workspaceId: context.workspaceId }, - }), - afterSuccess: ({ result }) => notifyWorkflowUpdated(result.id), -}) diff --git a/apps/sim/lib/workflows/executor/execution-events.ts b/apps/sim/lib/workflows/executor/execution-events.ts index e13003fe1f8..4b77f239b95 100644 --- a/apps/sim/lib/workflows/executor/execution-events.ts +++ b/apps/sim/lib/workflows/executor/execution-events.ts @@ -1,10 +1,5 @@ import type { SecretSafeBlockLog } from '@/lib/logs/execution/display-types' -import type { - BlockCompletionCallbackData, - ChildWorkflowContext, - IterationContext, - ParentIteration, -} from '@/executor/execution/types' +import type { ParentIteration } from '@/executor/execution/types' import type { SubflowType } from '@/stores/workflows/workflow/types' export type ExecutionEventType = @@ -344,202 +339,3 @@ export function formatSSEEvent(event: ExecutionEvent): string { export function encodeSSEEvent(event: ExecutionEvent): Uint8Array { return new TextEncoder().encode(formatSSEEvent(event)) } - -/** - * Creates execution callbacks using a provided event sink. - */ -export function createExecutionCallbacks(options: { - executionId: string - workflowId: string - sendEvent: (event: ExecutionEvent) => void | Promise -}) { - const { executionId, workflowId, sendEvent } = options - - const sendBufferedEvent = async (event: ExecutionEvent) => { - await sendEvent(event) - } - - const onBlockStart = async ( - blockId: string, - blockName: string, - blockType: string, - executionOrder: number, - iterationContext?: IterationContext, - childWorkflowContext?: ChildWorkflowContext - ) => { - await sendBufferedEvent({ - type: 'block:started', - timestamp: new Date().toISOString(), - executionId, - workflowId, - data: { - blockId, - blockName, - blockType, - executionOrder, - ...(iterationContext && { - iterationCurrent: iterationContext.iterationCurrent, - iterationTotal: iterationContext.iterationTotal, - iterationType: iterationContext.iterationType, - iterationContainerId: iterationContext.iterationContainerId, - ...(iterationContext.parentIterations?.length && { - parentIterations: iterationContext.parentIterations, - }), - }), - ...(childWorkflowContext && { - childWorkflowBlockId: childWorkflowContext.parentBlockId, - childWorkflowName: childWorkflowContext.workflowName, - }), - }, - }) - } - - const onBlockComplete = async ( - blockId: string, - blockName: string, - blockType: string, - callbackData: BlockCompletionCallbackData, - iterationContext?: IterationContext, - childWorkflowContext?: ChildWorkflowContext - ) => { - const callbackError = callbackData.output?.error - const iterationData = iterationContext - ? { - iterationCurrent: iterationContext.iterationCurrent, - iterationTotal: iterationContext.iterationTotal, - iterationType: iterationContext.iterationType, - iterationContainerId: iterationContext.iterationContainerId, - ...(iterationContext.parentIterations?.length && { - parentIterations: iterationContext.parentIterations, - }), - } - : {} - const childWorkflowData = childWorkflowContext - ? { - childWorkflowBlockId: childWorkflowContext.parentBlockId, - childWorkflowName: childWorkflowContext.workflowName, - } - : {} - - const instanceData = callbackData.childWorkflowInstanceId - ? { childWorkflowInstanceId: callbackData.childWorkflowInstanceId } - : {} - if (callbackError) { - await sendBufferedEvent({ - type: 'block:error', - timestamp: new Date().toISOString(), - executionId, - workflowId, - data: { - blockId, - blockName, - blockType, - input: callbackData.input, - error: callbackError, - durationMs: callbackData.executionTime || 0, - startedAt: callbackData.startedAt, - executionOrder: callbackData.executionOrder, - endedAt: callbackData.endedAt, - ...iterationData, - ...childWorkflowData, - ...instanceData, - }, - }) - } else { - await sendBufferedEvent({ - type: 'block:completed', - timestamp: new Date().toISOString(), - executionId, - workflowId, - data: { - blockId, - blockName, - blockType, - input: callbackData.input, - output: callbackData.output, - durationMs: callbackData.executionTime || 0, - startedAt: callbackData.startedAt, - executionOrder: callbackData.executionOrder, - endedAt: callbackData.endedAt, - ...iterationData, - ...childWorkflowData, - ...instanceData, - }, - }) - } - } - - const onStream = async (streamingExecution: unknown) => { - const streamingExec = streamingExecution as { stream: ReadableStream; execution: any } - const blockId = streamingExec.execution?.blockId - const reader = streamingExec.stream.getReader() - const decoder = new TextDecoder() - - try { - while (true) { - const { done, value } = await reader.read() - if (done) break - const chunk = decoder.decode(value, { stream: true }) - await sendBufferedEvent({ - type: 'stream:chunk', - timestamp: new Date().toISOString(), - executionId, - workflowId, - data: { blockId, chunk }, - }) - } - await sendBufferedEvent({ - type: 'stream:done', - timestamp: new Date().toISOString(), - executionId, - workflowId, - data: { blockId }, - }) - } finally { - try { - reader.releaseLock() - } catch {} - } - } - - const onChildWorkflowInstanceReady = async ( - blockId: string, - childWorkflowInstanceId: string, - iterationContext?: IterationContext, - executionOrder?: number, - childWorkflowContext?: ChildWorkflowContext - ) => { - await sendBufferedEvent({ - type: 'block:childWorkflowStarted', - timestamp: new Date().toISOString(), - executionId, - workflowId, - data: { - blockId, - childWorkflowInstanceId, - ...(iterationContext && { - iterationCurrent: iterationContext.iterationCurrent, - iterationTotal: iterationContext.iterationTotal, - iterationType: iterationContext.iterationType, - iterationContainerId: iterationContext.iterationContainerId, - ...(iterationContext.parentIterations?.length && { - parentIterations: iterationContext.parentIterations, - }), - }), - ...(childWorkflowContext && { - childWorkflowBlockId: childWorkflowContext.parentBlockId, - childWorkflowName: childWorkflowContext.workflowName, - }), - ...(executionOrder !== undefined && { executionOrder }), - }, - }) - } - - return { - sendEvent: sendBufferedEvent, - onBlockStart, - onBlockComplete, - onStream, - onChildWorkflowInstanceReady, - } -} diff --git a/apps/sim/package.json b/apps/sim/package.json index 1387daea0f5..ce4254ddb02 100644 --- a/apps/sim/package.json +++ b/apps/sim/package.json @@ -237,7 +237,6 @@ "stripe": "18.5.0", "svix": "1.88.0", "tailwindcss-animate": "^1.0.7", - "three": "0.177.0", "tldts": "7.0.30", "twilio": "5.9.0", "typebox": "1.1.38", @@ -271,7 +270,6 @@ "@types/react": "^19", "@types/react-dom": "^19", "@types/ssh2": "^1.15.5", - "@types/three": "0.177.0", "@vitejs/plugin-react": "^6.0.5", "@vitest/coverage-v8": "^4.1.0", "node-gyp": "12.4.0", diff --git a/bun.lock b/bun.lock index 4bdb55470ee..7aa8133c862 100644 --- a/bun.lock +++ b/bun.lock @@ -341,7 +341,6 @@ "stripe": "18.5.0", "svix": "1.88.0", "tailwindcss-animate": "^1.0.7", - "three": "0.177.0", "tldts": "7.0.30", "twilio": "5.9.0", "typebox": "1.1.38", @@ -375,7 +374,6 @@ "@types/react": "^19", "@types/react-dom": "^19", "@types/ssh2": "^1.15.5", - "@types/three": "0.177.0", "@vitejs/plugin-react": "^6.0.5", "@vitest/coverage-v8": "^4.1.0", "node-gyp": "12.4.0", @@ -1139,8 +1137,6 @@ "@daytona/toolbox-api-client": ["@daytona/toolbox-api-client@0.200.0", "", { "dependencies": { "axios": "^1.6.1" } }, "sha512-yj4u7wApHz53ayHRNX408nuazfo4AT+GlFjx35LPyxyC2ffOF4TPrVR0pQxYWQt+lg61GFNI9xr9Ig0oXaKT8w=="], - "@dimforge/rapier3d-compat": ["@dimforge/rapier3d-compat@0.12.0", "", {}, "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow=="], - "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], "@e2b/code-interpreter": ["@e2b/code-interpreter@2.7.0", "", { "dependencies": { "e2b": "^2.28.0" } }, "sha512-XsYMn1FNzci0niW0Zf8PdYYFzGgyfi79sRJIZBtA4NejnDwgDYUPXdM0zBtrJsPwLS+fv8RPN3RPd5THCHmPow=="], @@ -2105,8 +2101,6 @@ "@turbo/windows-arm64": ["@turbo/windows-arm64@2.9.14", "", { "os": "win32", "cpu": "arm64" }, "sha512-fVdCsnmYoKICsycbWuuGp6Jvi51/3G/UluFWuAUCvR8PIW5IJkAk5BM9UF8PSm0Q2IphWHFZjYEgjHsh3B9y/g=="], - "@tweenjs/tween.js": ["@tweenjs/tween.js@23.1.3", "", {}, "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA=="], - "@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], "@types/archiver": ["@types/archiver@8.0.0", "", { "dependencies": { "@types/node": "*", "@types/readdir-glob": "*" } }, "sha512-YpXPbEuv9+eUIPPQWUPahj3cvs9isWRuF+J4z+KbdYVDO3rWorWQFxUVHnwPu2AgKwvgpki5F2VMX0Xx+mX45A=="], @@ -2257,10 +2251,6 @@ "@types/ssh2": ["@types/ssh2@1.15.5", "", { "dependencies": { "@types/node": "^18.11.18" } }, "sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ=="], - "@types/stats.js": ["@types/stats.js@0.17.4", "", {}, "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA=="], - - "@types/three": ["@types/three@0.177.0", "", { "dependencies": { "@dimforge/rapier3d-compat": "~0.12.0", "@tweenjs/tween.js": "~23.1.3", "@types/stats.js": "*", "@types/webxr": "*", "@webgpu/types": "*", "fflate": "~0.8.2", "meshoptimizer": "~0.18.1" } }, "sha512-/ZAkn4OLUijKQySNci47lFO+4JLE1TihEjsGWPUT+4jWqxtwOPPEwJV1C3k5MEx0mcBPCdkFjzRzDOnHEI1R+A=="], - "@types/tough-cookie": ["@types/tough-cookie@4.0.5", "", {}, "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA=="], "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], @@ -2271,8 +2261,6 @@ "@types/webidl-conversions": ["@types/webidl-conversions@7.0.3", "", {}, "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA=="], - "@types/webxr": ["@types/webxr@0.5.24", "", {}, "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg=="], - "@types/whatwg-url": ["@types/whatwg-url@11.0.5", "", { "dependencies": { "@types/webidl-conversions": "*" } }, "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ=="], "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], @@ -2351,8 +2339,6 @@ "@vitest/utils": ["@vitest/utils@4.1.9", "", { "dependencies": { "@vitest/pretty-format": "4.1.9", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA=="], - "@webgpu/types": ["@webgpu/types@0.1.70", "", {}, "sha512-LFiNHHKMvmAEvwVew3JLJmTdShhbdwRFSImUshGhE2mGE8ybQzIo63l5uRp+YKnNx+8Qno8Kf6gN+DKMreIJCA=="], - "@xmldom/is-dom-node": ["@xmldom/is-dom-node@1.0.1", "", {}, "sha512-CJDxIgE5I0FH+ttq/Fxy6nRpxP70+e2O048EPe85J2use3XKdatVM7dDVvFNjQudd9B49NPoZ+8PG49zj4Er8Q=="], "@xmldom/xmldom": ["@xmldom/xmldom@0.8.13", "", {}, "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw=="], @@ -3049,7 +3035,7 @@ "fetch-cookie": ["fetch-cookie@3.2.0", "", { "dependencies": { "set-cookie-parser": "^2.4.8", "tough-cookie": "^6.0.0" } }, "sha512-n61pQIxP25C6DRhcJxn7BDzgHP/+S56Urowb5WFxtcRMpU6drqXD90xjyAsVQYsNSNNVbaCcYY1DuHsdkZLuiA=="], - "fflate": ["fflate@0.8.3", "", {}, "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA=="], + "fflate": ["fflate@0.4.8", "", {}, "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA=="], "file-type": ["file-type@16.5.4", "", { "dependencies": { "readable-web-to-node-stream": "^3.0.0", "strtok3": "^6.2.4", "token-types": "^4.1.1" } }, "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw=="], @@ -3579,8 +3565,6 @@ "mermaid": ["mermaid@11.16.1", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.2", "@iconify/utils": "^3.0.2", "@mermaid-js/parser": "^1.2.0", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", "cytoscape": "^3.33.3", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", "dayjs": "^1.11.20", "dompurify": "^3.3.3", "es-toolkit": "^1.45.1", "katex": "^0.16.45", "khroma": "^2.1.0", "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" } }, "sha512-TQsq6u22fAn3rek5VOubrhKPo1g5hwC3FXUN9hiyupTckcYiGuuKGkNQrKYwGJkXUxZdojwRG46gsSCFZMDp4g=="], - "meshoptimizer": ["meshoptimizer@0.18.1", "", {}, "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw=="], - "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], @@ -4417,8 +4401,6 @@ "thread-stream": ["thread-stream@3.2.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-zLBvqpwr4Esa0kRjcrzGU6zL25lePWaCLMx0RQFrmteozIfeNdaMLpG5U7PeHzvlFkAWaRKA9/KVW4F60iB+qw=="], - "three": ["three@0.177.0", "", {}, "sha512-EiXv5/qWAaGI+Vz2A+JfavwYCMdGjxVsrn3oBwllUoqYeaBO75J63ZfyaQKoiLrqNHoTlUc6PFgMXnS0kI45zg=="], - "tiny-async-pool": ["tiny-async-pool@1.3.0", "", { "dependencies": { "semver": "^5.5.0" } }, "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA=="], "tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="], @@ -5305,8 +5287,6 @@ "posthog-js/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA=="], - "posthog-js/fflate": ["fflate@0.4.8", "", {}, "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA=="], - "postject/commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="], "pptxgenjs/@types/node": ["@types/node@22.19.21", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-VMeFBSCKQKmm2swI2kW51SFusDqekC6q9trBCvJ/JliDchFSuoYYKN7yVNjPthP1HKZcx3U1gI/wTcEBjEFKTA=="],