diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/components/knowledge-header/knowledge-header.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/components/knowledge-header/knowledge-header.tsx
index 4ae936af73..2fd1203d60 100644
--- a/apps/sim/app/workspace/[workspaceId]/knowledge/components/knowledge-header/knowledge-header.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/knowledge/components/knowledge-header/knowledge-header.tsx
@@ -1,6 +1,6 @@
'use client'
-import { useEffect, useState } from 'react'
+import { useMemo, useState } from 'react'
import { createLogger } from '@sim/logger'
import { AlertTriangle, ChevronDown, LibraryBig, MoreHorizontal } from 'lucide-react'
import Link from 'next/link'
@@ -15,6 +15,7 @@ import {
import { Trash } from '@/components/emcn/icons/trash'
import { filterButtonClass } from '@/app/workspace/[workspaceId]/knowledge/components/constants'
import { useUpdateKnowledgeBase } from '@/hooks/queries/knowledge'
+import { useWorkspacesQuery } from '@/hooks/queries/workspace'
const logger = createLogger('KnowledgeHeader')
@@ -55,43 +56,23 @@ interface Workspace {
export function KnowledgeHeader({ breadcrumbs, options }: KnowledgeHeaderProps) {
const [isActionsPopoverOpen, setIsActionsPopoverOpen] = useState(false)
const [isWorkspacePopoverOpen, setIsWorkspacePopoverOpen] = useState(false)
- const [workspaces, setWorkspaces] = useState
([])
- const [isLoadingWorkspaces, setIsLoadingWorkspaces] = useState(false)
const updateKnowledgeBase = useUpdateKnowledgeBase()
+ const { data: allWorkspaces = [], isLoading: isLoadingWorkspaces } = useWorkspacesQuery(
+ !!options?.knowledgeBaseId
+ )
- useEffect(() => {
- if (!options?.knowledgeBaseId) return
-
- const fetchWorkspaces = async () => {
- try {
- setIsLoadingWorkspaces(true)
-
- const response = await fetch('/api/workspaces')
- if (!response.ok) {
- throw new Error('Failed to fetch workspaces')
- }
-
- const data = await response.json()
-
- const availableWorkspaces = data.workspaces
- .filter((ws: any) => ws.permissions === 'write' || ws.permissions === 'admin')
- .map((ws: any) => ({
- id: ws.id,
- name: ws.name,
- permissions: ws.permissions,
- }))
-
- setWorkspaces(availableWorkspaces)
- } catch (err) {
- logger.error('Error fetching workspaces:', err)
- } finally {
- setIsLoadingWorkspaces(false)
- }
- }
-
- fetchWorkspaces()
- }, [options?.knowledgeBaseId])
+ const workspaces = useMemo(
+ () =>
+ allWorkspaces
+ .filter((ws) => ws.permissions === 'write' || ws.permissions === 'admin')
+ .map((ws) => ({
+ id: ws.id,
+ name: ws.name,
+ permissions: ws.permissions as 'admin' | 'write' | 'read',
+ })),
+ [allWorkspaces]
+ )
const handleWorkspaceChange = async (workspaceId: string | null) => {
if (updateKnowledgeBase.isPending || !options?.knowledgeBaseId) return
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-data.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-data.ts
index 15f5007b6b..121acb7e9d 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-data.ts
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-data.ts
@@ -1,8 +1,11 @@
'use client'
-import { useCallback, useEffect, useState } from 'react'
+import { useCallback, useEffect, useMemo, useState } from 'react'
import { createLogger } from '@sim/logger'
import { useShallow } from 'zustand/react/shallow'
+import { useKnowledgeBasesQuery } from '@/hooks/queries/knowledge'
+import { useRecentLogs } from '@/hooks/queries/logs'
+import { useTemplates } from '@/hooks/queries/templates'
import { usePermissionConfig } from '@/hooks/use-permission-config'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
@@ -107,10 +110,10 @@ export interface MentionDataReturn {
// Ensure loaded functions
ensurePastChatsLoaded: () => Promise
- ensureKnowledgeLoaded: () => Promise
+ ensureKnowledgeLoaded: () => void
ensureBlocksLoaded: () => Promise
- ensureTemplatesLoaded: () => Promise
- ensureLogsLoaded: () => Promise
+ ensureTemplatesLoaded: () => void
+ ensureLogsLoaded: () => void
}
/**
@@ -128,8 +131,20 @@ export function useMentionData(props: UseMentionDataProps): MentionDataReturn {
const [pastChats, setPastChats] = useState([])
const [isLoadingPastChats, setIsLoadingPastChats] = useState(false)
- const [knowledgeBases, setKnowledgeBases] = useState([])
- const [isLoadingKnowledge, setIsLoadingKnowledge] = useState(false)
+ const [shouldLoadKnowledge, setShouldLoadKnowledge] = useState(false)
+ const { data: knowledgeData = [], isLoading: isLoadingKnowledge } = useKnowledgeBasesQuery(
+ workspaceId,
+ { enabled: shouldLoadKnowledge }
+ )
+
+ const knowledgeBases = useMemo(() => {
+ const sorted = [...knowledgeData].sort((a, b) => {
+ const ta = new Date(a.updatedAt || a.createdAt || 0).getTime()
+ const tb = new Date(b.updatedAt || b.createdAt || 0).getTime()
+ return tb - ta
+ })
+ return sorted.map((k) => ({ id: k.id, name: k.name || 'Untitled' }))
+ }, [knowledgeData])
const [blocksList, setBlocksList] = useState([])
const [isLoadingBlocks, setIsLoadingBlocks] = useState(false)
@@ -138,11 +153,39 @@ export function useMentionData(props: UseMentionDataProps): MentionDataReturn {
setBlocksList([])
}, [config.allowedIntegrations])
- const [templatesList, setTemplatesList] = useState([])
- const [isLoadingTemplates, setIsLoadingTemplates] = useState(false)
+ const [shouldLoadTemplates, setShouldLoadTemplates] = useState(false)
+ const { data: templatesData, isLoading: isLoadingTemplates } = useTemplates(
+ { limit: 50, offset: 0 },
+ { enabled: shouldLoadTemplates }
+ )
- const [logsList, setLogsList] = useState([])
- const [isLoadingLogs, setIsLoadingLogs] = useState(false)
+ const templatesList = useMemo(() => {
+ const items = templatesData?.data ?? []
+ return items
+ .map((t) => ({ id: t.id, name: t.name || 'Untitled Template', stars: t.stars || 0 }))
+ .sort((a, b) => b.stars - a.stars)
+ }, [templatesData])
+
+ const [shouldLoadLogs, setShouldLoadLogs] = useState(false)
+ const { data: logsData = [], isLoading: isLoadingLogs } = useRecentLogs(workspaceId, 50, {
+ enabled: shouldLoadLogs,
+ })
+
+ const logsList = useMemo(
+ () =>
+ logsData.map((l) => ({
+ id: l.id,
+ executionId: l.executionId || l.id,
+ level: l.level,
+ trigger: l.trigger || null,
+ createdAt: l.createdAt,
+ workflowName:
+ (l.workflow && (l.workflow.name || l.workflow.title)) ||
+ l.workflowName ||
+ 'Untitled Workflow',
+ })),
+ [logsData]
+ )
const [workflowBlocks, setWorkflowBlocks] = useState([])
const [isLoadingWorkflowBlocks, setIsLoadingWorkflowBlocks] = useState(false)
@@ -191,7 +234,6 @@ export function useMentionData(props: UseMentionDataProps): MentionDataReturn {
}
try {
- // Fetch current blocks from store
const workflowStoreBlocks = useWorkflowStore.getState().blocks
const { registry: blockRegistry } = await import('@/blocks/registry')
@@ -248,25 +290,11 @@ export function useMentionData(props: UseMentionDataProps): MentionDataReturn {
/**
* Ensures knowledge bases are loaded
*/
- const ensureKnowledgeLoaded = useCallback(async () => {
- if (isLoadingKnowledge || knowledgeBases.length > 0) return
- try {
- setIsLoadingKnowledge(true)
- const resp = await fetch(`/api/knowledge?workspaceId=${workspaceId}`)
- if (!resp.ok) throw new Error(`Failed to load knowledge bases: ${resp.status}`)
- const data = await resp.json()
- const items = Array.isArray(data?.data) ? data.data : []
- const sorted = [...items].sort((a: any, b: any) => {
- const ta = new Date(a.updatedAt || a.createdAt || 0).getTime()
- const tb = new Date(b.updatedAt || b.createdAt || 0).getTime()
- return tb - ta
- })
- setKnowledgeBases(sorted.map((k: any) => ({ id: k.id, name: k.name || 'Untitled' })))
- } catch {
- } finally {
- setIsLoadingKnowledge(false)
+ const ensureKnowledgeLoaded = useCallback(() => {
+ if (!shouldLoadKnowledge) {
+ setShouldLoadKnowledge(true)
}
- }, [isLoadingKnowledge, knowledgeBases.length, workspaceId])
+ }, [shouldLoadKnowledge])
/**
* Ensures blocks are loaded
@@ -319,55 +347,22 @@ export function useMentionData(props: UseMentionDataProps): MentionDataReturn {
/**
* Ensures templates are loaded
*/
- const ensureTemplatesLoaded = useCallback(async () => {
- if (isLoadingTemplates || templatesList.length > 0) return
- try {
- setIsLoadingTemplates(true)
- const resp = await fetch('/api/templates?limit=50&offset=0')
- if (!resp.ok) throw new Error(`Failed to load templates: ${resp.status}`)
- const data = await resp.json()
- const items = Array.isArray(data?.data) ? data.data : []
- const mapped = items
- .map((t: any) => ({ id: t.id, name: t.name || 'Untitled Template', stars: t.stars || 0 }))
- .sort((a: any, b: any) => b.stars - a.stars)
- setTemplatesList(mapped)
- } catch {
- } finally {
- setIsLoadingTemplates(false)
+ const ensureTemplatesLoaded = useCallback(() => {
+ if (!shouldLoadTemplates) {
+ setShouldLoadTemplates(true)
}
- }, [isLoadingTemplates, templatesList.length])
+ }, [shouldLoadTemplates])
/**
* Ensures logs are loaded
*/
- const ensureLogsLoaded = useCallback(async () => {
- if (isLoadingLogs || logsList.length > 0) return
- try {
- setIsLoadingLogs(true)
- const resp = await fetch(`/api/logs?workspaceId=${workspaceId}&limit=50&details=full`)
- if (!resp.ok) throw new Error(`Failed to load logs: ${resp.status}`)
- const data = await resp.json()
- const items = Array.isArray(data?.data) ? data.data : []
- const mapped = items.map((l: any) => ({
- id: l.id,
- executionId: l.executionId || l.id,
- level: l.level,
- trigger: l.trigger || null,
- createdAt: l.createdAt,
- workflowName:
- (l.workflow && (l.workflow.name || l.workflow.title)) ||
- l.workflowName ||
- 'Untitled Workflow',
- }))
- setLogsList(mapped)
- } catch {
- } finally {
- setIsLoadingLogs(false)
+ const ensureLogsLoaded = useCallback(() => {
+ if (!shouldLoadLogs) {
+ setShouldLoadLogs(true)
}
- }, [isLoadingLogs, logsList.length, workspaceId])
+ }, [shouldLoadLogs])
return {
- // State
pastChats,
isLoadingPastChats,
workflows,
@@ -382,8 +377,6 @@ export function useMentionData(props: UseMentionDataProps): MentionDataReturn {
isLoadingLogs,
workflowBlocks,
isLoadingWorkflowBlocks,
-
- // Operations
ensurePastChatsLoaded,
ensureKnowledgeLoaded,
ensureBlocksLoaded,
diff --git a/apps/sim/components/emcn/components/checkbox/checkbox.tsx b/apps/sim/components/emcn/components/checkbox/checkbox.tsx
index a6939e6291..c3c1f51340 100644
--- a/apps/sim/components/emcn/components/checkbox/checkbox.tsx
+++ b/apps/sim/components/emcn/components/checkbox/checkbox.tsx
@@ -28,7 +28,7 @@ const checkboxVariants = cva(
'border-[var(--border-1)] bg-transparent',
'focus-visible:outline-none',
'data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50',
- 'data-[state=checked]:border-[var(--text-primary)] data-[state=checked]:bg-[var(--text-primary)]',
+ 'data-[state=checked]:border-[var(--brand-tertiary-2)] data-[state=checked]:bg-[var(--brand-tertiary-2)]',
].join(' '),
{
variants: {
diff --git a/apps/sim/components/emcn/components/combobox/combobox.tsx b/apps/sim/components/emcn/components/combobox/combobox.tsx
index 49f4646402..0535f5f97c 100644
--- a/apps/sim/components/emcn/components/combobox/combobox.tsx
+++ b/apps/sim/components/emcn/components/combobox/combobox.tsx
@@ -20,7 +20,7 @@ import { Input } from '../input/input'
import { Popover, PopoverAnchor, PopoverContent, PopoverScrollArea } from '../popover/popover'
const comboboxVariants = cva(
- 'flex w-full rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-5)] px-[8px] font-sans font-medium text-[var(--text-primary)] placeholder:text-[var(--text-muted)] outline-none focus:outline-none focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50 hover:bg-[var(--surface-7)] dark:hover:border-[var(--surface-7)] dark:hover:bg-[var(--border-1)]',
+ 'flex w-full rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-5)] px-[8px] font-sans font-medium text-[var(--text-primary)] placeholder:text-[var(--text-muted)] outline-none focus:outline-none focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50',
{
variants: {
variant: {
@@ -460,7 +460,7 @@ const Combobox = memo(
void
+ /** When true, shows time picker after date selection and outputs ISO 8601 format */
+ showTime?: boolean
/** Not used in single mode */
startDate?: never
/** Not used in single mode */
@@ -177,14 +179,91 @@ const MONTHS_SHORT = [
/**
* Formats a date for display in the trigger button.
+ * If time is provided, formats as "Jan 30, 2026 at 2:30 PM"
*/
-function formatDateForDisplay(date: Date | null): string {
+function formatDateForDisplay(date: Date | null, time?: string | null): string {
if (!date) return ''
- return date.toLocaleDateString('en-US', {
+ const dateStr = date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
})
+ if (time) {
+ return `${dateStr} at ${formatDisplayTime(time)}`
+ }
+ return dateStr
+}
+
+/**
+ * Converts a 24h time string to 12h display format with AM/PM.
+ */
+function formatDisplayTime(time: string): string {
+ if (!time) return ''
+ const [hours, minutes] = time.split(':')
+ const hour = Number.parseInt(hours, 10)
+ const ampm = hour >= 12 ? 'PM' : 'AM'
+ const displayHour = hour % 12 || 12
+ return `${displayHour}:${minutes} ${ampm}`
+}
+
+/**
+ * Converts 12h time components to 24h format string.
+ */
+function formatStorageTime(hour: number, minute: number, ampm: 'AM' | 'PM'): string {
+ const hours24 = ampm === 'PM' ? (hour === 12 ? 12 : hour + 12) : hour === 12 ? 0 : hour
+ return `${hours24.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`
+}
+
+/**
+ * Parses a 24h time string into 12h components.
+ * Returns default 12:00 PM if no time provided (for UI display only).
+ */
+function parseTimeComponents(time: string | null): {
+ hour: string
+ minute: string
+ ampm: 'AM' | 'PM'
+} {
+ if (!time) return { hour: '12', minute: '00', ampm: 'PM' }
+ const [hours, minutes] = time.split(':')
+ const hour24 = Number.parseInt(hours, 10)
+ const isAM = hour24 < 12
+ return {
+ hour: (hour24 % 12 || 12).toString(),
+ minute: minutes || '00',
+ ampm: isAM ? 'AM' : 'PM',
+ }
+}
+
+/**
+ * Checks if a value contains time information.
+ */
+function valueHasTime(value: string | Date | undefined): boolean {
+ if (!value) return false
+ if (value instanceof Date) {
+ // Check if time is not midnight (default)
+ return value.getHours() !== 0 || value.getMinutes() !== 0
+ }
+ // Check for ISO datetime format: YYYY-MM-DDTHH:mm
+ return /T\d{2}:\d{2}/.test(value)
+}
+
+/**
+ * Extracts time from a datetime string or Date object.
+ * Returns HH:mm format or null if no time present.
+ */
+function extractTimeFromValue(value: string | Date | undefined): string | null {
+ if (!value) return null
+ if (value instanceof Date) {
+ // Only return time if it's not midnight (which could be default)
+ if (value.getHours() === 0 && value.getMinutes() === 0) return null
+ return `${value.getHours().toString().padStart(2, '0')}:${value.getMinutes().toString().padStart(2, '0')}`
+ }
+ // Check for ISO datetime format: YYYY-MM-DDTHH:mm:ss
+ const match = value.match(/T(\d{2}):(\d{2})/)
+ if (match) {
+ return `${match[1]}:${match[2]}`
+ }
+ return null
}
/**
@@ -228,12 +307,16 @@ function isSameDay(date1: Date, date2: Date): boolean {
}
/**
- * Formats a date as YYYY-MM-DD string.
+ * Formats a date as YYYY-MM-DD string, optionally with time as YYYY-MM-DDTHH:mm:ss.
*/
-function formatDateAsString(year: number, month: number, day: number): string {
+function formatDateAsString(year: number, month: number, day: number, time?: string): string {
const m = (month + 1).toString().padStart(2, '0')
const d = day.toString().padStart(2, '0')
- return `${year}-${m}-${d}`
+ const dateStr = `${year}-${m}-${d}`
+ if (time) {
+ return `${dateStr}T${time}:00`
+ }
+ return dateStr
}
/**
@@ -498,6 +581,7 @@ const DatePicker = React.forwardRef((props, ref
const {
value: _value,
onChange: _onChange,
+ showTime: _showTime,
startDate: _startDate,
endDate: _endDate,
onRangeChange: _onRangeChange,
@@ -507,6 +591,7 @@ const DatePicker = React.forwardRef((props, ref
} = rest as any
const isRangeMode = props.mode === 'range'
+ const showTime = !isRangeMode && (props as DatePickerSingleProps).showTime === true
const isControlled = controlledOpen !== undefined
const [internalOpen, setInternalOpen] = React.useState(false)
@@ -524,6 +609,37 @@ const DatePicker = React.forwardRef((props, ref
const selectedDate = !isRangeMode ? parseDate(props.value) : null
+ // Time state for showTime mode
+ // Track whether the incoming value has time
+ const valueTimeInfo = React.useMemo(() => {
+ if (!showTime) return { hasTime: false, time: null }
+ const time = extractTimeFromValue(props.value)
+ return { hasTime: time !== null, time }
+ }, [showTime, props.value])
+
+ const parsedTime = React.useMemo(
+ () => parseTimeComponents(valueTimeInfo.time),
+ [valueTimeInfo.time]
+ )
+ const [hour, setHour] = React.useState(parsedTime.hour)
+ const [minute, setMinute] = React.useState(parsedTime.minute)
+ const [ampm, setAmpm] = React.useState<'AM' | 'PM'>(parsedTime.ampm)
+ // Track whether user has explicitly set time (either from value or interaction)
+ const [timeWasSet, setTimeWasSet] = React.useState(valueTimeInfo.hasTime)
+ const hourInputRef = React.useRef(null)
+
+ // Sync time state when value changes
+ React.useEffect(() => {
+ if (showTime) {
+ const time = extractTimeFromValue(props.value)
+ const newParsed = parseTimeComponents(time)
+ setHour(newParsed.hour)
+ setMinute(newParsed.minute)
+ setAmpm(newParsed.ampm)
+ setTimeWasSet(time !== null)
+ }
+ }, [showTime, props.value])
+
const initialStart = isRangeMode ? parseDate(props.startDate) : null
const initialEnd = isRangeMode ? parseDate(props.endDate) : null
const [rangeStart, setRangeStart] = React.useState(initialStart)
@@ -566,17 +682,186 @@ const DatePicker = React.forwardRef((props, ref
}
}, [isRangeMode, selectedDate])
+ /**
+ * Gets the current time string in 24h format.
+ */
+ const getCurrentTimeString = React.useCallback(() => {
+ const h = Number.parseInt(hour) || 12
+ const m = Number.parseInt(minute) || 0
+ return formatStorageTime(h, m, ampm)
+ }, [hour, minute, ampm])
+
/**
* Handles selection of a specific day in single mode.
*/
const handleSelectDateSingle = React.useCallback(
(day: number) => {
if (!isRangeMode && props.onChange) {
- props.onChange(formatDateAsString(viewYear, viewMonth, day))
+ if (showTime && timeWasSet) {
+ // Only include time if it was explicitly set
+ props.onChange(formatDateAsString(viewYear, viewMonth, day, getCurrentTimeString()))
+ } else {
+ props.onChange(formatDateAsString(viewYear, viewMonth, day))
+ if (!showTime) {
+ setOpen(false)
+ }
+ }
+ }
+ },
+ [
+ isRangeMode,
+ viewYear,
+ viewMonth,
+ props.onChange,
+ setOpen,
+ showTime,
+ getCurrentTimeString,
+ timeWasSet,
+ ]
+ )
+
+ /**
+ * Handles hour input change.
+ */
+ const handleHourChange = React.useCallback((e: React.ChangeEvent) => {
+ const val = e.target.value.replace(/[^0-9]/g, '').slice(0, 2)
+ setHour(val)
+ }, [])
+
+ /**
+ * Handles hour input blur - validates and updates value.
+ */
+ const handleHourBlur = React.useCallback(() => {
+ const numVal = Number.parseInt(hour) || 12
+ const clamped = Math.min(12, Math.max(1, numVal))
+ setHour(clamped.toString())
+ setTimeWasSet(true)
+ if (selectedDate && props.onChange && showTime) {
+ const timeStr = formatStorageTime(clamped, Number.parseInt(minute) || 0, ampm)
+ props.onChange(
+ formatDateAsString(
+ selectedDate.getFullYear(),
+ selectedDate.getMonth(),
+ selectedDate.getDate(),
+ timeStr
+ )
+ )
+ }
+ }, [hour, minute, ampm, selectedDate, props.onChange, showTime])
+
+ /**
+ * Handles minute input change.
+ */
+ const handleMinuteChange = React.useCallback((e: React.ChangeEvent) => {
+ const val = e.target.value.replace(/[^0-9]/g, '').slice(0, 2)
+ setMinute(val)
+ }, [])
+
+ /**
+ * Handles minute input blur - validates and updates value.
+ */
+ const handleMinuteBlur = React.useCallback(() => {
+ const numVal = Number.parseInt(minute) || 0
+ const clamped = Math.min(59, Math.max(0, numVal))
+ setMinute(clamped.toString().padStart(2, '0'))
+ setTimeWasSet(true)
+ if (selectedDate && props.onChange && showTime) {
+ const timeStr = formatStorageTime(Number.parseInt(hour) || 12, clamped, ampm)
+ props.onChange(
+ formatDateAsString(
+ selectedDate.getFullYear(),
+ selectedDate.getMonth(),
+ selectedDate.getDate(),
+ timeStr
+ )
+ )
+ }
+ }, [minute, hour, ampm, selectedDate, props.onChange, showTime])
+
+ /**
+ * Handles AM/PM toggle.
+ */
+ const handleAmpmChange = React.useCallback(
+ (newAmpm: 'AM' | 'PM') => {
+ setAmpm(newAmpm)
+ setTimeWasSet(true)
+ if (selectedDate && props.onChange && showTime) {
+ const timeStr = formatStorageTime(
+ Number.parseInt(hour) || 12,
+ Number.parseInt(minute) || 0,
+ newAmpm
+ )
+ props.onChange(
+ formatDateAsString(
+ selectedDate.getFullYear(),
+ selectedDate.getMonth(),
+ selectedDate.getDate(),
+ timeStr
+ )
+ )
+ }
+ },
+ [hour, minute, selectedDate, props.onChange, showTime]
+ )
+
+ /**
+ * Handles keyboard navigation in hour input (Enter, ArrowUp, ArrowDown).
+ */
+ const handleHourKeyDown = React.useCallback(
+ (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter') {
+ e.preventDefault()
+ e.currentTarget.blur()
+ setOpen(false)
+ } else if (e.key === 'ArrowUp') {
+ e.preventDefault()
+ if (!timeWasSet) setTimeWasSet(true)
+ setHour((prev) => {
+ const num = Number.parseInt(prev, 10) || 12
+ const next = num >= 12 ? 1 : num + 1
+ return next.toString()
+ })
+ } else if (e.key === 'ArrowDown') {
+ e.preventDefault()
+ if (!timeWasSet) setTimeWasSet(true)
+ setHour((prev) => {
+ const num = Number.parseInt(prev, 10) || 12
+ const next = num <= 1 ? 12 : num - 1
+ return next.toString()
+ })
+ }
+ },
+ [setOpen, timeWasSet]
+ )
+
+ /**
+ * Handles keyboard navigation in minute input (Enter, ArrowUp, ArrowDown).
+ */
+ const handleMinuteKeyDown = React.useCallback(
+ (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter') {
+ e.preventDefault()
+ e.currentTarget.blur()
setOpen(false)
+ } else if (e.key === 'ArrowUp') {
+ e.preventDefault()
+ if (!timeWasSet) setTimeWasSet(true)
+ setMinute((prev) => {
+ const num = Number.parseInt(prev, 10) || 0
+ const next = num >= 59 ? 0 : num + 1
+ return next.toString().padStart(2, '0')
+ })
+ } else if (e.key === 'ArrowDown') {
+ e.preventDefault()
+ if (!timeWasSet) setTimeWasSet(true)
+ setMinute((prev) => {
+ const num = Number.parseInt(prev, 10) || 0
+ const next = num <= 0 ? 59 : num - 1
+ return next.toString().padStart(2, '0')
+ })
}
},
- [isRangeMode, viewYear, viewMonth, props.onChange, setOpen]
+ [setOpen, timeWasSet]
)
/**
@@ -640,16 +925,31 @@ const DatePicker = React.forwardRef((props, ref
/**
* Selects today's date (single mode only).
+ * Preserves existing time if set, otherwise outputs date only.
*/
const handleSelectToday = React.useCallback(() => {
if (!isRangeMode && props.onChange) {
const now = new Date()
setViewMonth(now.getMonth())
setViewYear(now.getFullYear())
- props.onChange(formatDateAsString(now.getFullYear(), now.getMonth(), now.getDate()))
- setOpen(false)
+ if (showTime && timeWasSet) {
+ // Only include time if it was explicitly set
+ props.onChange(
+ formatDateAsString(
+ now.getFullYear(),
+ now.getMonth(),
+ now.getDate(),
+ getCurrentTimeString()
+ )
+ )
+ } else {
+ props.onChange(formatDateAsString(now.getFullYear(), now.getMonth(), now.getDate()))
+ if (!showTime) {
+ setOpen(false)
+ }
+ }
}
- }, [isRangeMode, props.onChange, setOpen])
+ }, [isRangeMode, props.onChange, setOpen, showTime, getCurrentTimeString, timeWasSet])
/**
* Applies the selected range (range mode only).
@@ -710,9 +1010,11 @@ const DatePicker = React.forwardRef((props, ref
}
}, [disabled, open, setOpen])
+ // Only show time in display if it was explicitly set
+ const displayTime = showTime && timeWasSet ? getCurrentTimeString() : null
const displayValue = isRangeMode
? formatDateRangeForDisplay(initialStart, initialEnd)
- : formatDateForDisplay(selectedDate)
+ : formatDateForDisplay(selectedDate, displayTime)
const calendarContent = isRangeMode ? (
<>
@@ -783,12 +1085,81 @@ const DatePicker = React.forwardRef((props, ref
onNextMonth={goToNextMonth}
/>
- {/* Today Button */}
-
-
- Today
-
-
+ {/* Time Picker (when showTime is enabled) */}
+ {showTime && (
+
+
Time:
+
{
+ handleHourChange(e)
+ if (!timeWasSet) setTimeWasSet(true)
+ }}
+ onBlur={handleHourBlur}
+ onFocus={(e) => e.target.select()}
+ onKeyDown={handleHourKeyDown}
+ type='text'
+ inputMode='numeric'
+ maxLength={2}
+ autoComplete='off'
+ />
+
:
+
{
+ handleMinuteChange(e)
+ if (!timeWasSet) setTimeWasSet(true)
+ }}
+ onBlur={handleMinuteBlur}
+ onFocus={(e) => e.target.select()}
+ onKeyDown={handleMinuteKeyDown}
+ type='text'
+ inputMode='numeric'
+ maxLength={2}
+ autoComplete='off'
+ />
+
+ {(['AM', 'PM'] as const).map((period) => (
+ handleAmpmChange(period)}
+ className={cn(
+ 'px-[8px] py-[5px] font-medium font-sans text-[12px] transition-colors',
+ timeWasSet && ampm === period
+ ? 'bg-[var(--brand-secondary)] text-[var(--bg)]'
+ : 'bg-[var(--surface-5)] text-[var(--text-secondary)] hover:bg-[var(--surface-7)] hover:text-[var(--text-primary)] dark:hover:bg-[var(--surface-5)]'
+ )}
+ >
+ {period}
+
+ ))}
+
+
+ )}
+
+ {/* Today Button (only shown when time picker is not enabled) */}
+ {!showTime && (
+
+
+ Today
+
+
+ )}
>
)
diff --git a/apps/sim/hooks/queries/knowledge.ts b/apps/sim/hooks/queries/knowledge.ts
index 2d5edb7a7b..a35afd7546 100644
--- a/apps/sim/hooks/queries/knowledge.ts
+++ b/apps/sim/hooks/queries/knowledge.ts
@@ -17,6 +17,8 @@ export const knowledgeKeys = {
[...knowledgeKeys.all, 'detail', knowledgeBaseId ?? ''] as const,
tagDefinitions: (knowledgeBaseId: string) =>
[...knowledgeKeys.detail(knowledgeBaseId), 'tagDefinitions'] as const,
+ tagUsage: (knowledgeBaseId: string) =>
+ [...knowledgeKeys.detail(knowledgeBaseId), 'tagUsage'] as const,
documents: (knowledgeBaseId: string, paramsKey: string) =>
[...knowledgeKeys.detail(knowledgeBaseId), 'documents', paramsKey] as const,
document: (knowledgeBaseId: string, documentId: string) =>
@@ -910,6 +912,38 @@ export function useTagDefinitionsQuery(knowledgeBaseId?: string | null) {
})
}
+export interface TagUsageData {
+ tagName: string
+ tagSlot: string
+ documentCount: number
+ documents: Array<{ id: string; name: string; tagValue: string }>
+}
+
+export async function fetchTagUsage(knowledgeBaseId: string): Promise {
+ const response = await fetch(`/api/knowledge/${knowledgeBaseId}/tag-usage`)
+
+ if (!response.ok) {
+ throw new Error(`Failed to fetch tag usage: ${response.status} ${response.statusText}`)
+ }
+
+ const result = await response.json()
+ if (!result?.success) {
+ throw new Error(result?.error || 'Failed to fetch tag usage')
+ }
+
+ return Array.isArray(result.data) ? result.data : []
+}
+
+export function useTagUsageQuery(knowledgeBaseId?: string | null) {
+ return useQuery({
+ queryKey: knowledgeKeys.tagUsage(knowledgeBaseId ?? ''),
+ queryFn: () => fetchTagUsage(knowledgeBaseId as string),
+ enabled: Boolean(knowledgeBaseId),
+ staleTime: 60 * 1000,
+ placeholderData: keepPreviousData,
+ })
+}
+
export interface CreateTagDefinitionParams {
knowledgeBaseId: string
displayName: string
@@ -968,6 +1002,9 @@ export function useCreateTagDefinition() {
queryClient.invalidateQueries({
queryKey: knowledgeKeys.tagDefinitions(knowledgeBaseId),
})
+ queryClient.invalidateQueries({
+ queryKey: knowledgeKeys.tagUsage(knowledgeBaseId),
+ })
},
})
}
@@ -1006,6 +1043,9 @@ export function useDeleteTagDefinition() {
queryClient.invalidateQueries({
queryKey: knowledgeKeys.tagDefinitions(knowledgeBaseId),
})
+ queryClient.invalidateQueries({
+ queryKey: knowledgeKeys.tagUsage(knowledgeBaseId),
+ })
},
})
}
diff --git a/apps/sim/hooks/queries/logs.ts b/apps/sim/hooks/queries/logs.ts
index 39f0cffe6e..45c0390a80 100644
--- a/apps/sim/hooks/queries/logs.ts
+++ b/apps/sim/hooks/queries/logs.ts
@@ -15,6 +15,8 @@ export const logKeys = {
lists: () => [...logKeys.all, 'list'] as const,
list: (workspaceId: string | undefined, filters: Omit) =>
[...logKeys.lists(), workspaceId ?? '', filters] as const,
+ recent: (workspaceId: string | undefined, limit: number) =>
+ [...logKeys.all, 'recent', workspaceId ?? '', limit] as const,
details: () => [...logKeys.all, 'detail'] as const,
detail: (logId: string | undefined) => [...logKeys.details(), logId ?? ''] as const,
stats: (workspaceId: string | undefined, filters: object) =>
@@ -248,3 +250,57 @@ export function useExecutionSnapshot(executionId: string | undefined) {
staleTime: 5 * 60 * 1000, // 5 minutes - execution snapshots don't change
})
}
+
+/**
+ * Simple recent logs data for lightweight use cases (e.g., mention suggestions)
+ */
+export interface RecentLog {
+ id: string
+ executionId?: string
+ level: string
+ trigger: string | null
+ createdAt: string
+ workflow?: {
+ name?: string
+ title?: string
+ }
+ workflowName?: string
+}
+
+async function fetchRecentLogs(workspaceId: string, limit: number): Promise {
+ const params = new URLSearchParams()
+ params.set('workspaceId', workspaceId)
+ params.set('limit', limit.toString())
+ params.set('details', 'full')
+
+ const response = await fetch(`/api/logs?${params.toString()}`)
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch recent logs')
+ }
+
+ const data = await response.json()
+ return Array.isArray(data?.data) ? data.data : []
+}
+
+interface UseRecentLogsOptions {
+ enabled?: boolean
+}
+
+/**
+ * Hook for fetching recent logs with minimal filtering.
+ * Useful for lightweight use cases like mention suggestions.
+ */
+export function useRecentLogs(
+ workspaceId: string | undefined,
+ limit = 50,
+ options?: UseRecentLogsOptions
+) {
+ return useQuery({
+ queryKey: logKeys.recent(workspaceId, limit),
+ queryFn: () => fetchRecentLogs(workspaceId as string, limit),
+ enabled: Boolean(workspaceId) && (options?.enabled ?? true),
+ staleTime: 30 * 1000,
+ placeholderData: keepPreviousData,
+ })
+}
diff --git a/apps/sim/lib/knowledge/constants.ts b/apps/sim/lib/knowledge/constants.ts
index 3ed4b5e4e4..0d7a0dd271 100644
--- a/apps/sim/lib/knowledge/constants.ts
+++ b/apps/sim/lib/knowledge/constants.ts
@@ -103,7 +103,7 @@ export function getPlaceholderForFieldType(fieldType: string): string {
case 'number':
return 'Enter number'
case 'date':
- return 'YYYY-MM-DD'
+ return 'YYYY-MM-DD or YYYY-MM-DD HH:mm'
default:
return 'Enter value'
}
diff --git a/apps/sim/lib/knowledge/tags/utils.test.ts b/apps/sim/lib/knowledge/tags/utils.test.ts
new file mode 100644
index 0000000000..01707357b6
--- /dev/null
+++ b/apps/sim/lib/knowledge/tags/utils.test.ts
@@ -0,0 +1,187 @@
+/**
+ * Tests for knowledge tag validation utility functions
+ *
+ * @vitest-environment node
+ */
+import { describe, expect, it } from 'vitest'
+import { parseBooleanValue, parseDateValue, parseNumberValue, validateTagValue } from './utils'
+
+describe('Knowledge Tag Utils', () => {
+ describe('validateTagValue', () => {
+ describe('boolean validation', () => {
+ it('should accept "true" as valid boolean', () => {
+ expect(validateTagValue('isActive', 'true', 'boolean')).toBeNull()
+ })
+
+ it('should accept "false" as valid boolean', () => {
+ expect(validateTagValue('isActive', 'false', 'boolean')).toBeNull()
+ })
+
+ it('should accept case-insensitive boolean values', () => {
+ expect(validateTagValue('isActive', 'TRUE', 'boolean')).toBeNull()
+ expect(validateTagValue('isActive', 'FALSE', 'boolean')).toBeNull()
+ expect(validateTagValue('isActive', 'True', 'boolean')).toBeNull()
+ })
+
+ it('should reject invalid boolean values', () => {
+ const result = validateTagValue('isActive', 'yes', 'boolean')
+ expect(result).toContain('expects a boolean value')
+ })
+ })
+
+ describe('number validation', () => {
+ it('should accept valid integers', () => {
+ expect(validateTagValue('count', '42', 'number')).toBeNull()
+ expect(validateTagValue('count', '-10', 'number')).toBeNull()
+ expect(validateTagValue('count', '0', 'number')).toBeNull()
+ })
+
+ it('should accept valid decimals', () => {
+ expect(validateTagValue('price', '19.99', 'number')).toBeNull()
+ expect(validateTagValue('price', '-3.14', 'number')).toBeNull()
+ })
+
+ it('should reject non-numeric values', () => {
+ const result = validateTagValue('count', 'abc', 'number')
+ expect(result).toContain('expects a number value')
+ })
+ })
+
+ describe('date validation', () => {
+ it('should accept valid YYYY-MM-DD format', () => {
+ expect(validateTagValue('createdAt', '2024-01-15', 'date')).toBeNull()
+ expect(validateTagValue('createdAt', '2024-12-31', 'date')).toBeNull()
+ })
+
+ it('should accept valid ISO 8601 timestamp without timezone', () => {
+ expect(validateTagValue('createdAt', '2024-01-15T14:30:00', 'date')).toBeNull()
+ expect(validateTagValue('createdAt', '2024-01-15T00:00:00', 'date')).toBeNull()
+ expect(validateTagValue('createdAt', '2024-01-15T23:59:59', 'date')).toBeNull()
+ })
+
+ it('should accept valid ISO 8601 timestamp with seconds omitted', () => {
+ expect(validateTagValue('createdAt', '2024-01-15T14:30', 'date')).toBeNull()
+ })
+
+ it('should accept valid ISO 8601 timestamp with UTC timezone', () => {
+ expect(validateTagValue('createdAt', '2024-01-15T14:30:00Z', 'date')).toBeNull()
+ })
+
+ it('should accept valid ISO 8601 timestamp with timezone offset', () => {
+ expect(validateTagValue('createdAt', '2024-01-15T14:30:00+05:00', 'date')).toBeNull()
+ expect(validateTagValue('createdAt', '2024-01-15T14:30:00-08:00', 'date')).toBeNull()
+ })
+
+ it('should accept valid ISO 8601 timestamp with milliseconds', () => {
+ expect(validateTagValue('createdAt', '2024-01-15T14:30:00.123Z', 'date')).toBeNull()
+ })
+
+ it('should reject invalid date format', () => {
+ const result = validateTagValue('createdAt', '01/15/2024', 'date')
+ expect(result).toContain('expects a date in YYYY-MM-DD or YYYY-MM-DDTHH:mm:ss format')
+ })
+
+ it('should reject invalid date values like Feb 31', () => {
+ const result = validateTagValue('createdAt', '2024-02-31', 'date')
+ expect(result).toContain('invalid date')
+ })
+
+ it('should reject invalid time values', () => {
+ const result = validateTagValue('createdAt', '2024-01-15T25:00:00', 'date')
+ expect(result).toContain('invalid time')
+ })
+
+ it('should reject invalid minute values', () => {
+ const result = validateTagValue('createdAt', '2024-01-15T12:61:00', 'date')
+ expect(result).toContain('invalid time')
+ })
+ })
+
+ describe('text/default validation', () => {
+ it('should accept any string for text type', () => {
+ expect(validateTagValue('name', 'anything goes', 'text')).toBeNull()
+ expect(validateTagValue('name', '123', 'text')).toBeNull()
+ expect(validateTagValue('name', '', 'text')).toBeNull()
+ })
+ })
+ })
+
+ describe('parseDateValue', () => {
+ it('should parse valid YYYY-MM-DD format', () => {
+ const result = parseDateValue('2024-01-15')
+ expect(result).toBeInstanceOf(Date)
+ expect(result?.getFullYear()).toBe(2024)
+ expect(result?.getMonth()).toBe(0) // January is 0
+ expect(result?.getDate()).toBe(15)
+ })
+
+ it('should parse valid ISO 8601 timestamp', () => {
+ const result = parseDateValue('2024-01-15T14:30:00')
+ expect(result).toBeInstanceOf(Date)
+ expect(result?.getFullYear()).toBe(2024)
+ expect(result?.getMonth()).toBe(0)
+ expect(result?.getDate()).toBe(15)
+ expect(result?.getHours()).toBe(14)
+ expect(result?.getMinutes()).toBe(30)
+ })
+
+ it('should parse valid ISO 8601 timestamp with UTC timezone', () => {
+ const result = parseDateValue('2024-01-15T14:30:00Z')
+ expect(result).toBeInstanceOf(Date)
+ expect(result?.getFullYear()).toBe(2024)
+ })
+
+ it('should return null for invalid format', () => {
+ expect(parseDateValue('01/15/2024')).toBeNull()
+ expect(parseDateValue('invalid')).toBeNull()
+ expect(parseDateValue('')).toBeNull()
+ })
+
+ it('should return null for invalid date values', () => {
+ expect(parseDateValue('2024-02-31')).toBeNull() // Feb 31 doesn't exist
+ expect(parseDateValue('2024-13-01')).toBeNull() // Month 13 doesn't exist
+ })
+ })
+
+ describe('parseNumberValue', () => {
+ it('should parse valid integers', () => {
+ expect(parseNumberValue('42')).toBe(42)
+ expect(parseNumberValue('-10')).toBe(-10)
+ expect(parseNumberValue('0')).toBe(0)
+ })
+
+ it('should parse valid decimals', () => {
+ expect(parseNumberValue('19.99')).toBe(19.99)
+ expect(parseNumberValue('-3.14')).toBeCloseTo(-3.14)
+ })
+
+ it('should return null for non-numeric strings', () => {
+ expect(parseNumberValue('abc')).toBeNull()
+ })
+
+ it('should return 0 for empty string (JavaScript Number behavior)', () => {
+ expect(parseNumberValue('')).toBe(0)
+ })
+ })
+
+ describe('parseBooleanValue', () => {
+ it('should parse "true" to true', () => {
+ expect(parseBooleanValue('true')).toBe(true)
+ expect(parseBooleanValue('TRUE')).toBe(true)
+ expect(parseBooleanValue(' true ')).toBe(true)
+ })
+
+ it('should parse "false" to false', () => {
+ expect(parseBooleanValue('false')).toBe(false)
+ expect(parseBooleanValue('FALSE')).toBe(false)
+ expect(parseBooleanValue(' false ')).toBe(false)
+ })
+
+ it('should return null for invalid values', () => {
+ expect(parseBooleanValue('yes')).toBeNull()
+ expect(parseBooleanValue('no')).toBeNull()
+ expect(parseBooleanValue('1')).toBeNull()
+ expect(parseBooleanValue('')).toBeNull()
+ })
+ })
+})
diff --git a/apps/sim/lib/knowledge/tags/utils.ts b/apps/sim/lib/knowledge/tags/utils.ts
index 713a04cd41..ce0b9982a2 100644
--- a/apps/sim/lib/knowledge/tags/utils.ts
+++ b/apps/sim/lib/knowledge/tags/utils.ts
@@ -1,3 +1,14 @@
+const DATE_ONLY_REGEX = /^\d{4}-\d{2}-\d{2}$/
+const DATETIME_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?$/
+const ISO_WITH_TZ_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?$/
+
+/**
+ * Check if a string is a valid date format (YYYY-MM-DD or ISO 8601 timestamp)
+ */
+function isValidDateFormat(value: string): boolean {
+ return DATE_ONLY_REGEX.test(value) || DATETIME_REGEX.test(value) || ISO_WITH_TZ_REGEX.test(value)
+}
+
/**
* Validate a tag value against its expected field type
* Returns an error message if invalid, or null if valid
@@ -21,16 +32,35 @@ export function validateTagValue(tagName: string, value: string, fieldType: stri
return null
}
case 'date': {
- // Check format first
- if (!/^\d{4}-\d{2}-\d{2}$/.test(stringValue)) {
- return `Tag "${tagName}" expects a date in YYYY-MM-DD format, but received "${value}"`
+ // Check format first - accept YYYY-MM-DD or ISO 8601 datetime
+ if (!isValidDateFormat(stringValue)) {
+ return `Tag "${tagName}" expects a date in YYYY-MM-DD or YYYY-MM-DDTHH:mm:ss format, but received "${value}"`
}
+
+ // Extract date parts for validation
+ const datePart = stringValue.split('T')[0]
+ const [year, month, day] = datePart.split('-').map(Number)
+
// Validate the date is actually valid (e.g., reject 2024-02-31)
- const [year, month, day] = stringValue.split('-').map(Number)
const date = new Date(year, month - 1, day)
if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {
return `Tag "${tagName}" has an invalid date: "${value}"`
}
+
+ // If timestamp is included, validate time components
+ if (stringValue.includes('T')) {
+ const timePart = stringValue.split('T')[1]
+ // Extract hours and minutes, ignoring timezone
+ const timeMatch = timePart.match(/^(\d{2}):(\d{2})/)
+ if (timeMatch) {
+ const hours = Number.parseInt(timeMatch[1], 10)
+ const minutes = Number.parseInt(timeMatch[2], 10)
+ if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) {
+ return `Tag "${tagName}" has an invalid time: "${value}"`
+ }
+ }
+ }
+
return null
}
default:
@@ -56,25 +86,44 @@ export function parseNumberValue(value: string): number | null {
}
/**
- * Parse a string to Date with strict YYYY-MM-DD validation
+ * Parse a string to Date with validation for YYYY-MM-DD or ISO 8601 timestamp
* Returns null if invalid format or invalid date
*/
export function parseDateValue(value: string): Date | null {
const stringValue = String(value).trim()
- // Must be YYYY-MM-DD format
- if (!/^\d{4}-\d{2}-\d{2}$/.test(stringValue)) {
+ // Must be valid date format
+ if (!isValidDateFormat(stringValue)) {
return null
}
+ // Extract date parts
+ const datePart = stringValue.split('T')[0]
+ const [year, month, day] = datePart.split('-').map(Number)
+
// Validate the date is actually valid (e.g., reject 2024-02-31)
- const [year, month, day] = stringValue.split('-').map(Number)
- const date = new Date(year, month - 1, day)
- if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {
+ // First check date-only validity
+ const testDate = new Date(year, month - 1, day)
+ if (
+ testDate.getFullYear() !== year ||
+ testDate.getMonth() !== month - 1 ||
+ testDate.getDate() !== day
+ ) {
return null
}
- return date
+ // If timestamp is included, parse with time
+ if (stringValue.includes('T')) {
+ // Use native Date parsing for ISO strings
+ const date = new Date(stringValue)
+ if (Number.isNaN(date.getTime())) {
+ return null
+ }
+ return date
+ }
+
+ // Date-only: return date at midnight local time
+ return new Date(year, month - 1, day)
}
/**
pFad - Phonifier reborn
Pfad - The Proxy pFad © 2024 Your Company Name. All rights reserved.
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies:
Alternative Proxy
pFad Proxy
pFad v3 Proxy
pFad v4 Proxy