mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
improvement(perf): apply react and js performance optimizations across codebase (#3459)
* improvement(perf): apply react and js performance optimizations across codebase - Parallelize independent DB queries with Promise.all in API routes - Defer PostHog and OneDollarStats via dynamic import() to reduce bundle size - Use functional setState in countdown timers to prevent stale closures - Replace O(n*m) .filter().find() with Set-based O(n) lookups in undo-redo - Use .toSorted() instead of .sort() for immutable state operations - Use lazy initializers for useState(new Set()) across 20 components - Remove useMemo wrapping trivially cheap expressions (typeof, ternary, template strings) - Add passive: true to scroll event listener * fix(perf): address PR review feedback - Extract IIFE Set patterns to named consts for readability in use-undo-redo - Hoist Set construction above loops in BATCH_UPDATE_PARENT cases - Add .catch() error handler to PostHog dynamic import - Convert session-provider posthog import to dynamic import() to complete bundle split * fix(analytics): add .catch() to onedollarstats dynamic import
This commit is contained in:
@@ -43,7 +43,7 @@ function VerificationForm({
|
||||
|
||||
useEffect(() => {
|
||||
if (countdown > 0) {
|
||||
const timer = setTimeout(() => setCountdown(countdown - 1), 1000)
|
||||
const timer = setTimeout(() => setCountdown((c) => c - 1), 1000)
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
if (countdown === 0 && isResendDisabled) {
|
||||
|
||||
@@ -1,41 +1,64 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import posthog from 'posthog-js'
|
||||
import { PostHogProvider as PHProvider } from 'posthog-js/react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import type { PostHog } from 'posthog-js'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getEnv, isTruthy } from '@/lib/core/config/env'
|
||||
|
||||
const logger = createLogger('PostHogProvider')
|
||||
|
||||
export function PostHogProvider({ children }: { children: React.ReactNode }) {
|
||||
const [Provider, setProvider] = useState<React.ComponentType<{
|
||||
client: PostHog
|
||||
children: React.ReactNode
|
||||
}> | null>(null)
|
||||
const clientRef = useRef<PostHog | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const posthogEnabled = getEnv('NEXT_PUBLIC_POSTHOG_ENABLED')
|
||||
const posthogKey = getEnv('NEXT_PUBLIC_POSTHOG_KEY')
|
||||
|
||||
if (isTruthy(posthogEnabled) && posthogKey && !posthog.__loaded) {
|
||||
posthog.init(posthogKey, {
|
||||
api_host: '/ingest',
|
||||
ui_host: 'https://us.posthog.com',
|
||||
defaults: '2025-05-24',
|
||||
person_profiles: 'identified_only',
|
||||
autocapture: false,
|
||||
capture_pageview: false,
|
||||
capture_pageleave: false,
|
||||
capture_performance: false,
|
||||
capture_dead_clicks: false,
|
||||
enable_heatmaps: false,
|
||||
session_recording: {
|
||||
maskAllInputs: false,
|
||||
maskInputOptions: {
|
||||
password: true,
|
||||
email: false,
|
||||
},
|
||||
recordCrossOriginIframes: false,
|
||||
recordHeaders: false,
|
||||
recordBody: false,
|
||||
},
|
||||
persistence: 'localStorage+cookie',
|
||||
if (!isTruthy(posthogEnabled) || !posthogKey) return
|
||||
|
||||
Promise.all([import('posthog-js'), import('posthog-js/react')])
|
||||
.then(([posthogModule, { PostHogProvider: PHProvider }]) => {
|
||||
const posthog = posthogModule.default
|
||||
if (!posthog.__loaded) {
|
||||
posthog.init(posthogKey, {
|
||||
api_host: '/ingest',
|
||||
ui_host: 'https://us.posthog.com',
|
||||
defaults: '2025-05-24',
|
||||
person_profiles: 'identified_only',
|
||||
autocapture: false,
|
||||
capture_pageview: false,
|
||||
capture_pageleave: false,
|
||||
capture_performance: false,
|
||||
capture_dead_clicks: false,
|
||||
enable_heatmaps: false,
|
||||
session_recording: {
|
||||
maskAllInputs: false,
|
||||
maskInputOptions: {
|
||||
password: true,
|
||||
email: false,
|
||||
},
|
||||
recordCrossOriginIframes: false,
|
||||
recordHeaders: false,
|
||||
recordBody: false,
|
||||
},
|
||||
persistence: 'localStorage+cookie',
|
||||
})
|
||||
}
|
||||
clientRef.current = posthog
|
||||
setProvider(() => PHProvider)
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error('Failed to load PostHog', { error: err })
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
return <PHProvider client={posthog}>{children}</PHProvider>
|
||||
if (Provider && clientRef.current) {
|
||||
return <Provider client={clientRef.current}>{children}</Provider>
|
||||
}
|
||||
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import type React from 'react'
|
||||
import { createContext, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import posthog from 'posthog-js'
|
||||
import { client } from '@/lib/auth/auth-client'
|
||||
import { extractSessionDataFromAuthClientResult } from '@/lib/auth/session-response'
|
||||
|
||||
@@ -77,22 +76,24 @@ export function SessionProvider({ children }: { children: React.ReactNode }) {
|
||||
}, [loadSession, queryClient])
|
||||
|
||||
useEffect(() => {
|
||||
if (isPending || typeof posthog.identify !== 'function') {
|
||||
return
|
||||
}
|
||||
if (isPending) return
|
||||
|
||||
try {
|
||||
if (data?.user) {
|
||||
posthog.identify(data.user.id, {
|
||||
email: data.user.email,
|
||||
name: data.user.name,
|
||||
email_verified: data.user.emailVerified,
|
||||
created_at: data.user.createdAt,
|
||||
})
|
||||
} else {
|
||||
posthog.reset()
|
||||
}
|
||||
} catch {}
|
||||
import('posthog-js').then(({ default: posthog }) => {
|
||||
try {
|
||||
if (typeof posthog.identify !== 'function') return
|
||||
|
||||
if (data?.user) {
|
||||
posthog.identify(data.user.id, {
|
||||
email: data.user.email,
|
||||
name: data.user.name,
|
||||
email_verified: data.user.emailVerified,
|
||||
created_at: data.user.createdAt,
|
||||
})
|
||||
} else {
|
||||
posthog.reset()
|
||||
}
|
||||
} catch {}
|
||||
}).catch(() => {})
|
||||
}, [data, isPending])
|
||||
|
||||
const value = useMemo<SessionHookResult>(
|
||||
|
||||
@@ -43,27 +43,42 @@ async function getEffectiveBillingStatus(userId: string): Promise<{
|
||||
.from(member)
|
||||
.where(eq(member.userId, userId))
|
||||
|
||||
for (const m of memberships) {
|
||||
const owners = await db
|
||||
.select({ userId: member.userId })
|
||||
.from(member)
|
||||
.where(and(eq(member.organizationId, m.organizationId), eq(member.role, 'owner')))
|
||||
.limit(1)
|
||||
|
||||
if (owners.length > 0 && owners[0].userId !== userId) {
|
||||
const ownerStats = await db
|
||||
.select({
|
||||
blocked: userStats.billingBlocked,
|
||||
blockedReason: userStats.billingBlockedReason,
|
||||
})
|
||||
.from(userStats)
|
||||
.where(eq(userStats.userId, owners[0].userId))
|
||||
// Fetch all org owners in parallel
|
||||
const ownerResults = await Promise.all(
|
||||
memberships.map((m) =>
|
||||
db
|
||||
.select({ userId: member.userId })
|
||||
.from(member)
|
||||
.where(and(eq(member.organizationId, m.organizationId), eq(member.role, 'owner')))
|
||||
.limit(1)
|
||||
)
|
||||
)
|
||||
|
||||
if (ownerStats.length > 0 && ownerStats[0].blocked) {
|
||||
// Collect owner IDs that are not the current user
|
||||
const otherOwnerIds = ownerResults
|
||||
.filter((owners) => owners.length > 0 && owners[0].userId !== userId)
|
||||
.map((owners) => owners[0].userId)
|
||||
|
||||
if (otherOwnerIds.length > 0) {
|
||||
// Fetch all owner stats in parallel
|
||||
const ownerStatsResults = await Promise.all(
|
||||
otherOwnerIds.map((ownerId) =>
|
||||
db
|
||||
.select({
|
||||
blocked: userStats.billingBlocked,
|
||||
blockedReason: userStats.billingBlockedReason,
|
||||
})
|
||||
.from(userStats)
|
||||
.where(eq(userStats.userId, ownerId))
|
||||
.limit(1)
|
||||
)
|
||||
)
|
||||
|
||||
for (const stats of ownerStatsResults) {
|
||||
if (stats.length > 0 && stats[0].blocked) {
|
||||
return {
|
||||
billingBlocked: true,
|
||||
billingBlockedReason: ownerStats[0].blockedReason,
|
||||
billingBlockedReason: stats[0].blockedReason,
|
||||
blockedByOrgOwner: true,
|
||||
}
|
||||
}
|
||||
@@ -114,11 +129,12 @@ export async function GET(request: NextRequest) {
|
||||
let billingData
|
||||
|
||||
if (context === 'user') {
|
||||
// Get user billing (may include organization if they're part of one)
|
||||
billingData = await getSimplifiedBillingSummary(session.user.id, contextId || undefined)
|
||||
|
||||
// Attach effective billing blocked status (includes org owner check)
|
||||
const billingStatus = await getEffectiveBillingStatus(session.user.id)
|
||||
// Get user billing and billing blocked status in parallel
|
||||
const [billingResult, billingStatus] = await Promise.all([
|
||||
getSimplifiedBillingSummary(session.user.id, contextId || undefined),
|
||||
getEffectiveBillingStatus(session.user.id),
|
||||
])
|
||||
billingData = billingResult
|
||||
|
||||
billingData = {
|
||||
...billingData,
|
||||
|
||||
@@ -106,23 +106,16 @@ export async function POST(request: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
// Check if identifier is available
|
||||
const existingIdentifier = await db
|
||||
.select()
|
||||
.from(chat)
|
||||
.where(eq(chat.identifier, identifier))
|
||||
.limit(1)
|
||||
// Check identifier availability and workflow access in parallel
|
||||
const [existingIdentifier, { hasAccess, workflow: workflowRecord }] = await Promise.all([
|
||||
db.select().from(chat).where(eq(chat.identifier, identifier)).limit(1),
|
||||
checkWorkflowAccessForChatCreation(workflowId, session.user.id),
|
||||
])
|
||||
|
||||
if (existingIdentifier.length > 0) {
|
||||
return createErrorResponse('Identifier already in use', 400)
|
||||
}
|
||||
|
||||
// Check if user has permission to create chat for this workflow
|
||||
const { hasAccess, workflow: workflowRecord } = await checkWorkflowAccessForChatCreation(
|
||||
workflowId,
|
||||
session.user.id
|
||||
)
|
||||
|
||||
if (!hasAccess || !workflowRecord) {
|
||||
return createErrorResponse('Workflow not found or access denied', 404)
|
||||
}
|
||||
|
||||
@@ -123,22 +123,24 @@ export async function POST(req: Request) {
|
||||
)
|
||||
}
|
||||
|
||||
const orgExists = await db
|
||||
.select({ id: organization.id })
|
||||
.from(organization)
|
||||
.where(eq(organization.id, organizationId))
|
||||
.limit(1)
|
||||
// Check org existence and name uniqueness in parallel
|
||||
const [orgExists, existingSet] = await Promise.all([
|
||||
db
|
||||
.select({ id: organization.id })
|
||||
.from(organization)
|
||||
.where(eq(organization.id, organizationId))
|
||||
.limit(1),
|
||||
db
|
||||
.select({ id: credentialSet.id })
|
||||
.from(credentialSet)
|
||||
.where(and(eq(credentialSet.organizationId, organizationId), eq(credentialSet.name, name)))
|
||||
.limit(1),
|
||||
])
|
||||
|
||||
if (orgExists.length === 0) {
|
||||
return NextResponse.json({ error: 'Organization not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
const existingSet = await db
|
||||
.select({ id: credentialSet.id })
|
||||
.from(credentialSet)
|
||||
.where(and(eq(credentialSet.organizationId, organizationId), eq(credentialSet.name, name)))
|
||||
.limit(1)
|
||||
|
||||
if (existingSet.length > 0) {
|
||||
return NextResponse.json(
|
||||
{ error: 'A credential set with this name already exists' },
|
||||
|
||||
@@ -118,21 +118,16 @@ export async function POST(request: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
const existingIdentifier = await db
|
||||
.select()
|
||||
.from(form)
|
||||
.where(eq(form.identifier, identifier))
|
||||
.limit(1)
|
||||
// Check identifier availability and workflow access in parallel
|
||||
const [existingIdentifier, { hasAccess, workflow: workflowRecord }] = await Promise.all([
|
||||
db.select().from(form).where(eq(form.identifier, identifier)).limit(1),
|
||||
checkWorkflowAccessForFormCreation(workflowId, session.user.id),
|
||||
])
|
||||
|
||||
if (existingIdentifier.length > 0) {
|
||||
return createErrorResponse('Identifier already in use', 400)
|
||||
}
|
||||
|
||||
const { hasAccess, workflow: workflowRecord } = await checkWorkflowAccessForFormCreation(
|
||||
workflowId,
|
||||
session.user.id
|
||||
)
|
||||
|
||||
if (!hasAccess || !workflowRecord) {
|
||||
return createErrorResponse('Workflow not found or access denied', 404)
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ export default function EmailAuth({ identifier, onAuthSuccess }: EmailAuthProps)
|
||||
|
||||
useEffect(() => {
|
||||
if (countdown > 0) {
|
||||
const timer = setTimeout(() => setCountdown(countdown - 1), 1000)
|
||||
const timer = setTimeout(() => setCountdown((c) => c - 1), 1000)
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
if (countdown === 0 && isResendDisabled) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { memo, useMemo, useState } from 'react'
|
||||
import { memo, useState } from 'react'
|
||||
import { Check, Copy, File as FileIcon, FileText, Image as ImageIcon } from 'lucide-react'
|
||||
import { Tooltip } from '@/components/emcn'
|
||||
import {
|
||||
@@ -46,9 +46,7 @@ export const ClientChatMessage = memo(
|
||||
function ClientChatMessage({ message }: { message: ChatMessage }) {
|
||||
const [isCopied, setIsCopied] = useState(false)
|
||||
|
||||
const isJsonObject = useMemo(() => {
|
||||
return typeof message.content === 'object' && message.content !== null
|
||||
}, [message.content])
|
||||
const isJsonObject = typeof message.content === 'object' && message.content !== null
|
||||
|
||||
// Since tool calls are now handled via SSE events and stored in message.toolCalls,
|
||||
// we can use the content directly without parsing
|
||||
|
||||
@@ -294,7 +294,7 @@ export function Document({
|
||||
|
||||
const searchError = searchQueryError instanceof Error ? searchQueryError.message : null
|
||||
|
||||
const [selectedChunks, setSelectedChunks] = useState<Set<string>>(new Set())
|
||||
const [selectedChunks, setSelectedChunks] = useState<Set<string>>(() => new Set())
|
||||
const [selectedChunk, setSelectedChunk] = useState<ChunkData | null>(null)
|
||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||
|
||||
|
||||
@@ -392,7 +392,7 @@ export function KnowledgeBase({
|
||||
setCurrentPage(1)
|
||||
}, [])
|
||||
|
||||
const [selectedDocuments, setSelectedDocuments] = useState<Set<string>>(new Set())
|
||||
const [selectedDocuments, setSelectedDocuments] = useState<Set<string>>(() => new Set())
|
||||
const [isSelectAllMode, setIsSelectAllMode] = useState(false)
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
|
||||
const [showAddDocumentsModal, setShowAddDocumentsModal] = useState(false)
|
||||
|
||||
@@ -51,7 +51,7 @@ export function AddConnectorModal({ open, onOpenChange, knowledgeBaseId }: AddCo
|
||||
const [sourceConfig, setSourceConfig] = useState<Record<string, string>>({})
|
||||
const [syncInterval, setSyncInterval] = useState(1440)
|
||||
const [selectedCredentialId, setSelectedCredentialId] = useState<string | null>(null)
|
||||
const [disabledTagIds, setDisabledTagIds] = useState<Set<string>>(new Set())
|
||||
const [disabledTagIds, setDisabledTagIds] = useState<Set<string>>(() => new Set())
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [showOAuthModal, setShowOAuthModal] = useState(false)
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ export function AddDocumentsModal({
|
||||
const [fileError, setFileError] = useState<string | null>(null)
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
const [dragCounter, setDragCounter] = useState(0)
|
||||
const [retryingIndexes, setRetryingIndexes] = useState<Set<number>>(new Set())
|
||||
const [retryingIndexes, setRetryingIndexes] = useState<Set<number>>(() => new Set())
|
||||
|
||||
const { isUploading, uploadProgress, uploadFiles, uploadError, clearError } = useKnowledgeUpload({
|
||||
workspaceId,
|
||||
|
||||
@@ -91,7 +91,7 @@ export function CreateBaseModal({ open, onOpenChange }: CreateBaseModalProps) {
|
||||
const [fileError, setFileError] = useState<string | null>(null)
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
const [dragCounter, setDragCounter] = useState(0)
|
||||
const [retryingIndexes, setRetryingIndexes] = useState<Set<number>>(new Set())
|
||||
const [retryingIndexes, setRetryingIndexes] = useState<Set<number>>(() => new Set())
|
||||
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
|
||||
@@ -693,7 +693,7 @@ const TraceSpanNode = memo(function TraceSpanNode({
|
||||
*/
|
||||
export const TraceSpans = memo(function TraceSpans({ traceSpans }: TraceSpansProps) {
|
||||
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(() => new Set())
|
||||
const [expandedSections, setExpandedSections] = useState<Set<string>>(new Set())
|
||||
const [expandedSections, setExpandedSections] = useState<Set<string>>(() => new Set())
|
||||
const toggleSet = useSetToggle()
|
||||
|
||||
const { workflowStartTime, actualTotalDuration, normalizedSpans } = useMemo(() => {
|
||||
|
||||
@@ -289,9 +289,7 @@ export const LogDetails = memo(function LogDetails({
|
||||
)
|
||||
}, [log])
|
||||
|
||||
const hasCostInfo = useMemo(() => {
|
||||
return isWorkflowExecutionLog && log?.cost
|
||||
}, [log, isWorkflowExecutionLog])
|
||||
const hasCostInfo = isWorkflowExecutionLog && log?.cost
|
||||
|
||||
const workflowOutput = useMemo(() => {
|
||||
const executionData = log?.executionData as
|
||||
@@ -329,7 +327,7 @@ export const LogDetails = memo(function LogDetails({
|
||||
[log?.createdAt]
|
||||
)
|
||||
|
||||
const logStatus = useMemo(() => getDisplayStatus(log?.status), [log?.status])
|
||||
const logStatus = getDisplayStatus(log?.status)
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -130,9 +130,9 @@ export function CredentialSets() {
|
||||
const resendInvitation = useResendCredentialSetInvitation()
|
||||
|
||||
const [deletingSet, setDeletingSet] = useState<{ id: string; name: string } | null>(null)
|
||||
const [deletingSetIds, setDeletingSetIds] = useState<Set<string>>(new Set())
|
||||
const [cancellingInvitations, setCancellingInvitations] = useState<Set<string>>(new Set())
|
||||
const [resendingInvitations, setResendingInvitations] = useState<Set<string>>(new Set())
|
||||
const [deletingSetIds, setDeletingSetIds] = useState<Set<string>>(() => new Set())
|
||||
const [cancellingInvitations, setCancellingInvitations] = useState<Set<string>>(() => new Set())
|
||||
const [resendingInvitations, setResendingInvitations] = useState<Set<string>>(() => new Set())
|
||||
const [resendCooldowns, setResendCooldowns] = useState<Record<string, number>>({})
|
||||
|
||||
const addEmail = useCallback(
|
||||
|
||||
@@ -34,7 +34,7 @@ export function CustomTools() {
|
||||
const deleteToolMutation = useDeleteCustomTool()
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [deletingTools, setDeletingTools] = useState<Set<string>>(new Set())
|
||||
const [deletingTools, setDeletingTools] = useState<Set<string>>(() => new Set())
|
||||
const [editingTool, setEditingTool] = useState<string | null>(null)
|
||||
const [showAddForm, setShowAddForm] = useState(false)
|
||||
const [toolToDelete, setToolToDelete] = useState<{ id: string; name: string } | null>(null)
|
||||
|
||||
@@ -440,7 +440,7 @@ export function MCP({ initialServerId }: MCPProps) {
|
||||
const [isAddingServer, setIsAddingServer] = useState(false)
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [deletingServers, setDeletingServers] = useState<Set<string>>(new Set())
|
||||
const [deletingServers, setDeletingServers] = useState<Set<string>>(() => new Set())
|
||||
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
|
||||
const [serverToDelete, setServerToDelete] = useState<{ id: string; name: string } | null>(null)
|
||||
@@ -449,7 +449,7 @@ export function MCP({ initialServerId }: MCPProps) {
|
||||
const [refreshingServers, setRefreshingServers] = useState<
|
||||
Record<string, { status: 'refreshing' | 'refreshed'; workflowsUpdated?: number }>
|
||||
>({})
|
||||
const [expandedTools, setExpandedTools] = useState<Set<string>>(new Set())
|
||||
const [expandedTools, setExpandedTools] = useState<Set<string>>(() => new Set())
|
||||
|
||||
const [showEnvVars, setShowEnvVars] = useState(false)
|
||||
const [envSearchTerm, setEnvSearchTerm] = useState('')
|
||||
|
||||
@@ -35,7 +35,7 @@ export function Skills() {
|
||||
const deleteSkillMutation = useDeleteSkill()
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [deletingSkills, setDeletingSkills] = useState<Set<string>>(new Set())
|
||||
const [deletingSkills, setDeletingSkills] = useState<Set<string>>(() => new Set())
|
||||
const [editingSkill, setEditingSkill] = useState<SkillDefinition | null>(null)
|
||||
const [showAddForm, setShowAddForm] = useState(false)
|
||||
const [skillToDelete, setSkillToDelete] = useState<{ id: string; name: string } | null>(null)
|
||||
|
||||
@@ -49,9 +49,9 @@ export function TeamMembers({
|
||||
isAdminOrOwner,
|
||||
onRemoveMember,
|
||||
}: TeamMembersProps) {
|
||||
const [cancellingInvitations, setCancellingInvitations] = useState<Set<string>>(new Set())
|
||||
const [resendingInvitations, setResendingInvitations] = useState<Set<string>>(new Set())
|
||||
const [resentInvitations, setResentInvitations] = useState<Set<string>>(new Set())
|
||||
const [cancellingInvitations, setCancellingInvitations] = useState<Set<string>>(() => new Set())
|
||||
const [resendingInvitations, setResendingInvitations] = useState<Set<string>>(() => new Set())
|
||||
const [resentInvitations, setResentInvitations] = useState<Set<string>>(() => new Set())
|
||||
const [resendCooldowns, setResendCooldowns] = useState<Record<string, number>>({})
|
||||
|
||||
const { data: memberUsageResponse, isLoading: isLoadingUsage } = useOrganizationMembers(
|
||||
|
||||
@@ -117,9 +117,7 @@ function ServerDetailView({ workspaceId, serverId, onBack }: ServerDetailViewPro
|
||||
}, [toolToView])
|
||||
const [selectedWorkflowId, setSelectedWorkflowId] = useState<string | null>(null)
|
||||
|
||||
const mcpServerUrl = useMemo(() => {
|
||||
return `${getBaseUrl()}/api/mcp/serve/${serverId}`
|
||||
}, [serverId])
|
||||
const mcpServerUrl = `${getBaseUrl()}/api/mcp/serve/${serverId}`
|
||||
|
||||
const handleDeleteTool = async () => {
|
||||
if (!toolToDelete) return
|
||||
@@ -879,7 +877,7 @@ export function WorkflowMcpServers() {
|
||||
const [selectedWorkflowIds, setSelectedWorkflowIds] = useState<string[]>([])
|
||||
const [selectedServerId, setSelectedServerId] = useState<string | null>(null)
|
||||
const [serverToDelete, setServerToDelete] = useState<WorkflowMcpServer | null>(null)
|
||||
const [deletingServers, setDeletingServers] = useState<Set<string>>(new Set())
|
||||
const [deletingServers, setDeletingServers] = useState<Set<string>>(() => new Set())
|
||||
|
||||
const filteredServers = useMemo(() => {
|
||||
if (!searchTerm.trim()) return servers
|
||||
|
||||
@@ -29,7 +29,7 @@ interface FileAttachmentDisplayProps {
|
||||
*/
|
||||
export const FileAttachmentDisplay = memo(({ fileAttachments }: FileAttachmentDisplayProps) => {
|
||||
const [fileUrls, setFileUrls] = useState<Record<string, string>>({})
|
||||
const [failedImages, setFailedImages] = useState<Set<string>>(new Set())
|
||||
const [failedImages, setFailedImages] = useState<Set<string>>(() => new Set())
|
||||
|
||||
/**
|
||||
* Formats file size in bytes to human-readable format
|
||||
|
||||
@@ -236,7 +236,7 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
|
||||
scrollContainer.addEventListener('scroll', checkPosition, { passive: true })
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', checkPosition, true)
|
||||
window.addEventListener('scroll', checkPosition, { capture: true, passive: true })
|
||||
window.addEventListener('resize', checkPosition)
|
||||
|
||||
return () => {
|
||||
|
||||
@@ -148,7 +148,7 @@ export function McpDeploy({
|
||||
return isDefaultDescription ? '' : workflowDescription
|
||||
})
|
||||
const [parameterDescriptions, setParameterDescriptions] = useState<Record<string, string>>({})
|
||||
const [pendingServerChanges, setPendingServerChanges] = useState<Set<string>>(new Set())
|
||||
const [pendingServerChanges, setPendingServerChanges] = useState<Set<string>>(() => new Set())
|
||||
const [saveErrors, setSaveErrors] = useState<string[]>([])
|
||||
|
||||
const parameterSchema = useMemo(
|
||||
|
||||
@@ -135,8 +135,8 @@ function ConnectionItem({
|
||||
* Connection blocks component that displays incoming connections with their schemas
|
||||
*/
|
||||
export function ConnectionBlocks({ connections, currentBlockId }: ConnectionBlocksProps) {
|
||||
const [expandedConnections, setExpandedConnections] = useState<Set<string>>(new Set())
|
||||
const [expandedFieldPaths, setExpandedFieldPaths] = useState<Set<string>>(new Set())
|
||||
const [expandedConnections, setExpandedConnections] = useState<Set<string>>(() => new Set())
|
||||
const [expandedFieldPaths, setExpandedFieldPaths] = useState<Set<string>>(() => new Set())
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
||||
const connectionRefs = useRef<Map<string, HTMLDivElement>>(new Map())
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
@@ -182,10 +181,7 @@ export function LongInput({
|
||||
)
|
||||
|
||||
// During streaming, use local content; otherwise use the controller value
|
||||
const value = useMemo(() => {
|
||||
if (wandHook.isStreaming) return localContent
|
||||
return ctrl.valueString
|
||||
}, [wandHook.isStreaming, localContent, ctrl.valueString])
|
||||
const value = wandHook.isStreaming ? localContent : ctrl.valueString
|
||||
|
||||
// Base value for syncing (not including streaming)
|
||||
const baseValue = isPreview
|
||||
|
||||
@@ -620,7 +620,7 @@ export const Terminal = memo(function Terminal() {
|
||||
const exportConsoleCSV = useTerminalConsoleStore((state) => state.exportConsoleCSV)
|
||||
|
||||
const [selectedEntry, setSelectedEntry] = useState<ConsoleEntry | null>(null)
|
||||
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(new Set())
|
||||
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(() => new Set())
|
||||
const [isToggling, setIsToggling] = useState(false)
|
||||
const [showCopySuccess, setShowCopySuccess] = useState(false)
|
||||
const [showInput, setShowInput] = useState(false)
|
||||
|
||||
@@ -65,12 +65,12 @@ export function TrainingModal() {
|
||||
const [copiedId, setCopiedId] = useState<string | null>(null)
|
||||
const [viewingDataset, setViewingDataset] = useState<string | null>(null)
|
||||
const [expandedDataset, setExpandedDataset] = useState<string | null>(null)
|
||||
const [sendingDatasets, setSendingDatasets] = useState<Set<string>>(new Set())
|
||||
const [sendingDatasets, setSendingDatasets] = useState<Set<string>>(() => new Set())
|
||||
const [sendingAll, setSendingAll] = useState(false)
|
||||
const [selectedDatasets, setSelectedDatasets] = useState<Set<string>>(new Set())
|
||||
const [selectedDatasets, setSelectedDatasets] = useState<Set<string>>(() => new Set())
|
||||
const [sendingSelected, setSendingSelected] = useState(false)
|
||||
const [sentDatasets, setSentDatasets] = useState<Set<string>>(new Set())
|
||||
const [failedDatasets, setFailedDatasets] = useState<Set<string>>(new Set())
|
||||
const [sentDatasets, setSentDatasets] = useState<Set<string>>(() => new Set())
|
||||
const [failedDatasets, setFailedDatasets] = useState<Set<string>>(() => new Set())
|
||||
const [sendingLiveWorkflow, setSendingLiveWorkflow] = useState(false)
|
||||
const [liveWorkflowSent, setLiveWorkflowSent] = useState(false)
|
||||
const [liveWorkflowFailed, setLiveWorkflowFailed] = useState(false)
|
||||
|
||||
@@ -277,7 +277,7 @@ function ConnectionsSection({
|
||||
onResizeMouseDown,
|
||||
onToggleCollapsed,
|
||||
}: ConnectionsSectionProps) {
|
||||
const [expandedBlocks, setExpandedBlocks] = useState<Set<string>>(new Set())
|
||||
const [expandedBlocks, setExpandedBlocks] = useState<Set<string>>(() => new Set())
|
||||
const [expandedVariables, setExpandedVariables] = useState(true)
|
||||
const [expandedEnvVars, setExpandedEnvVars] = useState(true)
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import { configure } from 'onedollarstats'
|
||||
import { env } from '@/lib/core/config/env'
|
||||
|
||||
export function OneDollarStats() {
|
||||
@@ -12,11 +11,15 @@ export function OneDollarStats() {
|
||||
return
|
||||
}
|
||||
|
||||
configure({
|
||||
collectorUrl: 'https://collector.onedollarstats.com/events',
|
||||
autocollect: true,
|
||||
hashRouting: true,
|
||||
})
|
||||
import('onedollarstats')
|
||||
.then(({ configure }) => {
|
||||
configure({
|
||||
collectorUrl: 'https://collector.onedollarstats.com/events',
|
||||
autocollect: true,
|
||||
hashRouting: true,
|
||||
})
|
||||
})
|
||||
.catch(() => {})
|
||||
}, [])
|
||||
|
||||
return null
|
||||
|
||||
@@ -260,7 +260,7 @@ function useJsonCollapse(
|
||||
visibleLineIndices: number[]
|
||||
toggleCollapse: (lineIndex: number) => void
|
||||
} {
|
||||
const [collapsedLines, setCollapsedLines] = useState<Set<number>>(new Set())
|
||||
const [collapsedLines, setCollapsedLines] = useState<Set<number>>(() => new Set())
|
||||
|
||||
const collapsibleRegions = useMemo(() => {
|
||||
if (!showCollapseColumn || language !== 'json') return new Map<number, CollapsibleRegion>()
|
||||
|
||||
@@ -271,7 +271,7 @@ export function AccessControl() {
|
||||
const [newGroupAutoAdd, setNewGroupAutoAdd] = useState(false)
|
||||
const [createError, setCreateError] = useState<string | null>(null)
|
||||
const [deletingGroup, setDeletingGroup] = useState<{ id: string; name: string } | null>(null)
|
||||
const [deletingGroupIds, setDeletingGroupIds] = useState<Set<string>>(new Set())
|
||||
const [deletingGroupIds, setDeletingGroupIds] = useState<Set<string>>(() => new Set())
|
||||
|
||||
const { data: members = [], isPending: membersLoading } = usePermissionGroupMembers(
|
||||
viewingGroup?.id
|
||||
@@ -281,7 +281,7 @@ export function AccessControl() {
|
||||
const [showConfigModal, setShowConfigModal] = useState(false)
|
||||
const [editingConfig, setEditingConfig] = useState<PermissionGroupConfig | null>(null)
|
||||
const [showAddMembersModal, setShowAddMembersModal] = useState(false)
|
||||
const [selectedMemberIds, setSelectedMemberIds] = useState<Set<string>>(new Set())
|
||||
const [selectedMemberIds, setSelectedMemberIds] = useState<Set<string>>(() => new Set())
|
||||
const [providerSearchTerm, setProviderSearchTerm] = useState('')
|
||||
const [integrationSearchTerm, setIntegrationSearchTerm] = useState('')
|
||||
const [platformSearchTerm, setPlatformSearchTerm] = useState('')
|
||||
|
||||
@@ -551,8 +551,9 @@ export function useUndoRedo() {
|
||||
const batchRemoveInverse = entry.inverse as BatchRemoveEdgesOperation
|
||||
const { edgeSnapshots } = batchRemoveInverse.data
|
||||
|
||||
const existingEdgeIds = new Set(useWorkflowStore.getState().edges.map((edge) => edge.id))
|
||||
const edgesToRemove = edgeSnapshots
|
||||
.filter((e) => useWorkflowStore.getState().edges.find((edge) => edge.id === e.id))
|
||||
.filter((e) => existingEdgeIds.has(e.id))
|
||||
.map((e) => e.id)
|
||||
|
||||
if (edgesToRemove.length > 0) {
|
||||
@@ -576,8 +577,9 @@ export function useUndoRedo() {
|
||||
const batchAddInverse = entry.inverse as BatchAddEdgesOperation
|
||||
const { edgeSnapshots } = batchAddInverse.data
|
||||
|
||||
const existingEdgeIds = new Set(useWorkflowStore.getState().edges.map((edge) => edge.id))
|
||||
const edgesToAdd = edgeSnapshots.filter(
|
||||
(e) => !useWorkflowStore.getState().edges.find((edge) => edge.id === e.id)
|
||||
(e) => !existingEdgeIds.has(e.id)
|
||||
)
|
||||
|
||||
if (edgesToAdd.length > 0) {
|
||||
@@ -631,8 +633,9 @@ export function useUndoRedo() {
|
||||
|
||||
if (useWorkflowStore.getState().blocks[blockId]) {
|
||||
if (newParentId && affectedEdges && affectedEdges.length > 0) {
|
||||
const existingEdgeIds = new Set(useWorkflowStore.getState().edges.map((edge) => edge.id))
|
||||
const edgesToAdd = affectedEdges.filter(
|
||||
(e) => !useWorkflowStore.getState().edges.find((edge) => edge.id === e.id)
|
||||
(e) => !existingEdgeIds.has(e.id)
|
||||
)
|
||||
if (edgesToAdd.length > 0) {
|
||||
addToQueue({
|
||||
@@ -695,8 +698,9 @@ export function useUndoRedo() {
|
||||
|
||||
// If we're removing FROM a subflow (undo of add to subflow), remove edges after
|
||||
if (!newParentId && affectedEdges && affectedEdges.length > 0) {
|
||||
const existingEdgeIds = new Set(useWorkflowStore.getState().edges.map((edge) => edge.id))
|
||||
const edgeIdsToRemove = affectedEdges
|
||||
.filter((edge) => useWorkflowStore.getState().edges.find((e) => e.id === edge.id))
|
||||
.filter((edge) => existingEdgeIds.has(edge.id))
|
||||
.map((edge) => edge.id)
|
||||
if (edgeIdsToRemove.length > 0) {
|
||||
useWorkflowStore.getState().batchRemoveEdges(edgeIdsToRemove)
|
||||
@@ -732,6 +736,7 @@ export function useUndoRedo() {
|
||||
// Collect all edge operations first
|
||||
const allEdgesToAdd: Edge[] = []
|
||||
const allEdgeIdsToRemove: string[] = []
|
||||
const existingEdgeIds = new Set(useWorkflowStore.getState().edges.map((edge) => edge.id))
|
||||
|
||||
for (const update of validUpdates) {
|
||||
const { newParentId, affectedEdges } = update
|
||||
@@ -739,7 +744,7 @@ export function useUndoRedo() {
|
||||
// Moving OUT of subflow (undoing insert) → restore edges first
|
||||
if (!newParentId && affectedEdges && affectedEdges.length > 0) {
|
||||
const edgesToAdd = affectedEdges.filter(
|
||||
(e) => !useWorkflowStore.getState().edges.find((edge) => edge.id === e.id)
|
||||
(e) => !existingEdgeIds.has(e.id)
|
||||
)
|
||||
allEdgesToAdd.push(...edgesToAdd)
|
||||
}
|
||||
@@ -747,7 +752,7 @@ export function useUndoRedo() {
|
||||
// Moving INTO subflow (undoing removal) → remove edges first
|
||||
if (newParentId && affectedEdges && affectedEdges.length > 0) {
|
||||
const edgeIds = affectedEdges
|
||||
.filter((edge) => useWorkflowStore.getState().edges.find((e) => e.id === edge.id))
|
||||
.filter((edge) => existingEdgeIds.has(edge.id))
|
||||
.map((edge) => edge.id)
|
||||
allEdgeIdsToRemove.push(...edgeIds)
|
||||
}
|
||||
@@ -1165,8 +1170,9 @@ export function useUndoRedo() {
|
||||
const batchRemoveOp = entry.operation as BatchRemoveEdgesOperation
|
||||
const { edgeSnapshots } = batchRemoveOp.data
|
||||
|
||||
const existingEdgeIds = new Set(useWorkflowStore.getState().edges.map((edge) => edge.id))
|
||||
const edgesToRemove = edgeSnapshots
|
||||
.filter((e) => useWorkflowStore.getState().edges.find((edge) => edge.id === e.id))
|
||||
.filter((e) => existingEdgeIds.has(e.id))
|
||||
.map((e) => e.id)
|
||||
|
||||
if (edgesToRemove.length > 0) {
|
||||
@@ -1191,8 +1197,9 @@ export function useUndoRedo() {
|
||||
const batchAddOp = entry.operation as BatchAddEdgesOperation
|
||||
const { edgeSnapshots } = batchAddOp.data
|
||||
|
||||
const existingEdgeIds = new Set(useWorkflowStore.getState().edges.map((edge) => edge.id))
|
||||
const edgesToAdd = edgeSnapshots.filter(
|
||||
(e) => !useWorkflowStore.getState().edges.find((edge) => edge.id === e.id)
|
||||
(e) => !existingEdgeIds.has(e.id)
|
||||
)
|
||||
|
||||
if (edgesToAdd.length > 0) {
|
||||
@@ -1249,8 +1256,9 @@ export function useUndoRedo() {
|
||||
if (useWorkflowStore.getState().blocks[blockId]) {
|
||||
// If we're removing FROM a subflow, remove edges first
|
||||
if (!newParentId && affectedEdges && affectedEdges.length > 0) {
|
||||
const existingEdgeIds = new Set(useWorkflowStore.getState().edges.map((edge) => edge.id))
|
||||
const edgeIdsToRemove = affectedEdges
|
||||
.filter((edge) => useWorkflowStore.getState().edges.find((e) => e.id === edge.id))
|
||||
.filter((edge) => existingEdgeIds.has(edge.id))
|
||||
.map((edge) => edge.id)
|
||||
if (edgeIdsToRemove.length > 0) {
|
||||
useWorkflowStore.getState().batchRemoveEdges(edgeIdsToRemove)
|
||||
@@ -1316,8 +1324,9 @@ export function useUndoRedo() {
|
||||
|
||||
// If we're adding TO a subflow, restore edges after
|
||||
if (newParentId && affectedEdges && affectedEdges.length > 0) {
|
||||
const existingEdgeIds = new Set(useWorkflowStore.getState().edges.map((edge) => edge.id))
|
||||
const edgesToAdd = affectedEdges.filter(
|
||||
(e) => !useWorkflowStore.getState().edges.find((edge) => edge.id === e.id)
|
||||
(e) => !existingEdgeIds.has(e.id)
|
||||
)
|
||||
if (edgesToAdd.length > 0) {
|
||||
addToQueue({
|
||||
@@ -1351,6 +1360,7 @@ export function useUndoRedo() {
|
||||
// Collect all edge operations first
|
||||
const allEdgesToAdd: Edge[] = []
|
||||
const allEdgeIdsToRemove: string[] = []
|
||||
const existingEdgeIds = new Set(useWorkflowStore.getState().edges.map((edge) => edge.id))
|
||||
|
||||
for (const update of validUpdates) {
|
||||
const { newParentId, affectedEdges } = update
|
||||
@@ -1358,7 +1368,7 @@ export function useUndoRedo() {
|
||||
// Moving INTO subflow (redoing insert) → remove edges first
|
||||
if (newParentId && affectedEdges && affectedEdges.length > 0) {
|
||||
const edgeIds = affectedEdges
|
||||
.filter((edge) => useWorkflowStore.getState().edges.find((e) => e.id === edge.id))
|
||||
.filter((edge) => existingEdgeIds.has(edge.id))
|
||||
.map((edge) => edge.id)
|
||||
allEdgeIdsToRemove.push(...edgeIds)
|
||||
}
|
||||
@@ -1366,7 +1376,7 @@ export function useUndoRedo() {
|
||||
// Moving OUT of subflow (redoing removal) → restore edges after
|
||||
if (!newParentId && affectedEdges && affectedEdges.length > 0) {
|
||||
const edgesToAdd = affectedEdges.filter(
|
||||
(e) => !useWorkflowStore.getState().edges.find((edge) => edge.id === e.id)
|
||||
(e) => !existingEdgeIds.has(e.id)
|
||||
)
|
||||
allEdgesToAdd.push(...edgesToAdd)
|
||||
}
|
||||
|
||||
@@ -956,13 +956,15 @@ export function trackForcedToolUsage(
|
||||
if (forcedToolNames.length > 0 && toolCallsResponse && toolCallsResponse.length > 0) {
|
||||
const toolNames = toolCallsResponse.map((tc) => tc.function?.name || tc.name || tc.id)
|
||||
|
||||
const usedTools = forcedToolNames.filter((toolName) => toolNames.includes(toolName))
|
||||
const toolNameSet = new Set(toolNames)
|
||||
const usedTools = forcedToolNames.filter((toolName) => toolNameSet.has(toolName))
|
||||
|
||||
if (usedTools.length > 0) {
|
||||
hasUsedForcedTool = true
|
||||
updatedUsedForcedTools.push(...usedTools)
|
||||
|
||||
const remainingTools = forcedTools.filter((tool) => !updatedUsedForcedTools.includes(tool))
|
||||
const usedSet = new Set(updatedUsedForcedTools)
|
||||
const remainingTools = forcedTools.filter((tool) => !usedSet.has(tool))
|
||||
|
||||
if (remainingTools.length > 0) {
|
||||
const nextToolToForce = remainingTools[0]
|
||||
|
||||
@@ -127,7 +127,7 @@ export const useChatStore = create<ChatState>()(
|
||||
|
||||
const headers = ['timestamp', 'type', 'content']
|
||||
|
||||
const sortedMessages = messages.sort(
|
||||
const sortedMessages = messages.toSorted(
|
||||
(a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
||||
)
|
||||
|
||||
|
||||
@@ -215,7 +215,7 @@ export const useFolderStore = create<FolderState>()(
|
||||
const buildTree = (parentId: string | null, level = 0): FolderTreeNode[] => {
|
||||
return folders
|
||||
.filter((folder) => folder.parentId === parentId)
|
||||
.sort((a, b) => a.sortOrder - b.sortOrder || a.name.localeCompare(b.name))
|
||||
.toSorted((a, b) => a.sortOrder - b.sortOrder || a.name.localeCompare(b.name))
|
||||
.map((folder) => ({
|
||||
...folder,
|
||||
children: buildTree(folder.id, level + 1),
|
||||
@@ -231,7 +231,7 @@ export const useFolderStore = create<FolderState>()(
|
||||
getChildFolders: (parentId) =>
|
||||
Object.values(get().folders)
|
||||
.filter((folder) => folder.parentId === parentId)
|
||||
.sort((a, b) => a.sortOrder - b.sortOrder || a.name.localeCompare(b.name)),
|
||||
.toSorted((a, b) => a.sortOrder - b.sortOrder || a.name.localeCompare(b.name)),
|
||||
|
||||
getFolderPath: (folderId) => {
|
||||
const folders = get().folders
|
||||
|
||||
@@ -97,7 +97,7 @@ export const useSearchModalStore = create<SearchModalState>()(
|
||||
const filteredTriggers = filterBlocks(allTriggers) as typeof allTriggers
|
||||
const priorityOrder = ['Start', 'Schedule', 'Webhook']
|
||||
|
||||
const sortedTriggers = [...filteredTriggers].sort((a, b) => {
|
||||
const sortedTriggers = filteredTriggers.toSorted((a, b) => {
|
||||
const aIndex = priorityOrder.indexOf(a.name)
|
||||
const bIndex = priorityOrder.indexOf(b.name)
|
||||
const aHasPriority = aIndex !== -1
|
||||
|
||||
@@ -2494,7 +2494,7 @@ export const useCopilotStore = create<CopilotStore>()(
|
||||
|
||||
let masked = value
|
||||
// Sort by length descending to mask longer IDs first
|
||||
const sortedIds = Array.from(sensitiveCredentialIds).sort((a, b) => b.length - a.length)
|
||||
const sortedIds = Array.from(sensitiveCredentialIds).toSorted((a, b) => b.length - a.length)
|
||||
for (const id of sortedIds) {
|
||||
if (id && masked.includes(id)) {
|
||||
masked = masked.split(id).join('••••••••')
|
||||
|
||||
Reference in New Issue
Block a user