From 3845647422494e4a7306f68bfd396257ecc37677 Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Tue, 5 May 2026 18:07:56 -0400 Subject: [PATCH 1/4] refactor(csv-transformer): remove cronEnabled prop from CsvTransformerClient and related components - Simplified CsvTransformerClient by removing the cronEnabled prop, which is no longer needed. - Updated the Page component to reflect this change, ensuring it no longer passes the cronEnabled prop. - Enhanced user experience by directly managing worker status within the CsvTransformer component, providing real-time feedback on background processing status. This refactor streamlines the component's interface and improves maintainability. --- .../csv-transformer/CsvTransformerClient.tsx | 8 +- .../(main-layout)/csv-transformer/page.tsx | 4 +- .../CsvTransfomer/CsvTransformer.Client.tsx | 66 +++++- .../ui/src/CsvTransfomer/ProcessCsv.tsx | 204 +++++++++++------ .../src/CsvTransfomer/ProcessingHistory.tsx | 23 +- .../ui/src/CsvTransfomer/parseCsv.ts | 34 ++- .../src/GuardrailsSettings/AdvancedMode.tsx | 54 ----- .../src/GuardrailsSettings/MasterConfig.tsx | 90 +------- .../ui/src/GuardrailsSettings/SimpleMode.tsx | 17 +- .../ui/src/theme/components/muiOverrides.ts | 215 +++++++++++++++--- packages/docs/src/pages/jlinc-partnership.tsx | 6 +- packages/docs/src/pages/webinar-thank-you.tsx | 2 +- .../src/controllers/csv-parser/index.ts | 13 ++ .../server/src/routes/csv-parser/index.ts | 1 + packages/server/src/utils/cron.ts | 4 +- .../server/src/utils/isCsvRunCronEnabled.ts | 7 + packages/ui/src/themes/compStyleOverride.js | 71 +++++- 17 files changed, 535 insertions(+), 284 deletions(-) create mode 100644 packages/server/src/utils/isCsvRunCronEnabled.ts diff --git a/apps/web/app/(Main UI)/sidekick-studio/(main-layout)/csv-transformer/CsvTransformerClient.tsx b/apps/web/app/(Main UI)/sidekick-studio/(main-layout)/csv-transformer/CsvTransformerClient.tsx index da2e37f7ac5..08cad0b3223 100644 --- a/apps/web/app/(Main UI)/sidekick-studio/(main-layout)/csv-transformer/CsvTransformerClient.tsx +++ b/apps/web/app/(Main UI)/sidekick-studio/(main-layout)/csv-transformer/CsvTransformerClient.tsx @@ -3,12 +3,8 @@ import dynamic from 'next/dynamic' const View = dynamic(() => import('@ui/CsvTransfomer'), { ssr: false }) -interface Props { - cronEnabled: boolean -} - -const CsvTransformerClient = ({ cronEnabled }: Props) => { - return +const CsvTransformerClient = () => { + return } export default CsvTransformerClient diff --git a/apps/web/app/(Main UI)/sidekick-studio/(main-layout)/csv-transformer/page.tsx b/apps/web/app/(Main UI)/sidekick-studio/(main-layout)/csv-transformer/page.tsx index 9a8b64bb941..2416bb0fa10 100644 --- a/apps/web/app/(Main UI)/sidekick-studio/(main-layout)/csv-transformer/page.tsx +++ b/apps/web/app/(Main UI)/sidekick-studio/(main-layout)/csv-transformer/page.tsx @@ -2,11 +2,9 @@ import React from 'react' import CsvTransformerClient from './CsvTransformerClient' const Page = () => { - const cronEnabled = process.env.ENABLE_CSV_RUN_CRON === 'true' - return ( <> - + ) } diff --git a/packages-answers/ui/src/CsvTransfomer/CsvTransformer.Client.tsx b/packages-answers/ui/src/CsvTransfomer/CsvTransformer.Client.tsx index 597fdbb7b94..0b00975629c 100644 --- a/packages-answers/ui/src/CsvTransfomer/CsvTransformer.Client.tsx +++ b/packages-answers/ui/src/CsvTransfomer/CsvTransformer.Client.tsx @@ -19,9 +19,7 @@ import { Container, Box, Stack, Tabs, Tab, Typography, Alert, AlertTitle } from import ProcessCsv from './ProcessCsv' import ProcessingHistory from './ProcessingHistory' -interface CsvTransformerProps { - cronEnabled?: boolean -} +type WorkerBannerState = 'loading' | 'enabled' | 'disabled' | 'error' function TabPanel(props: any) { const { children, currentValue, value, ...other } = props @@ -38,9 +36,10 @@ function TabPanel(props: any) { ) } -const CsvTransformer = ({ cronEnabled = false }: CsvTransformerProps) => { +const CsvTransformer = () => { const { user, isLoading } = useUser() const [chatflows, setChatflows] = useState([]) + const [workerBanner, setWorkerBanner] = useState('loading') const searchParams = useSearchParams() const router = useRouter() const tab = searchParams.get('tab') ?? 'process' @@ -65,6 +64,41 @@ const CsvTransformer = ({ cronEnabled = false }: CsvTransformerProps) => { fetchChatflows() }, [fetchChatflows]) + useEffect(() => { + if (isLoading) return + if (!user) { + setWorkerBanner('error') + return + } + let cancelled = false + const loadWorkerStatus = async () => { + const baseURL = sessionStorage.getItem('baseURL') || '' + const token = sessionStorage.getItem('access_token') + if (!baseURL || !token) { + if (!cancelled) setWorkerBanner('error') + return + } + try { + const response = await fetch(`${baseURL}/api/v1/csv-parser/worker-status`, { + headers: { + 'x-request-from': 'aai', + Authorization: `Bearer ${token}` + } + }) + if (!response.ok) throw new Error('worker-status failed') + const data = (await response.json()) as { workerEnabled?: boolean } + if (cancelled) return + setWorkerBanner(data.workerEnabled ? 'enabled' : 'disabled') + } catch { + if (!cancelled) setWorkerBanner('error') + } + } + loadWorkerStatus() + return () => { + cancelled = true + } + }, [isLoading, user]) + // Auto-refresh when user returns from marketplace (only if no CSV chatflows currently) useEffect(() => { const handleVisibilityChange = () => { @@ -100,16 +134,30 @@ const CsvTransformer = ({ cronEnabled = false }: CsvTransformerProps) => { AI CSV Transformer - {cronEnabled ? ( + {workerBanner === 'loading' && ( + + Checking worker status + Loading background CSV processing status from the API server… + + )} + {workerBanner === 'enabled' && ( Background processing is enabled - Submitted CSVs will be picked up by the cron worker on this environment. + Submitted CSVs will be picked up by the cron worker on the Flowise/API server (status from API). - ) : ( + )} + {workerBanner === 'disabled' && ( Background processing is disabled - CSV runs can be created but they will not be processed until ENABLE_CSV_RUN_CRON=true is set on the - server (then the worker is restarted). + CSV runs can be created but will not be processed until ENABLE_CSV_RUN_CRON=true is set on the + Flowise/API server and that process is restarted. + + )} + {workerBanner === 'error' && ( + + Could not load worker status + The UI could not read CSV worker status from the API (session, network, or server error). Background processing may + still be enabled on the server. )} diff --git a/packages-answers/ui/src/CsvTransfomer/ProcessCsv.tsx b/packages-answers/ui/src/CsvTransfomer/ProcessCsv.tsx index a496d45a896..d3ab75eafc7 100644 --- a/packages-answers/ui/src/CsvTransfomer/ProcessCsv.tsx +++ b/packages-answers/ui/src/CsvTransfomer/ProcessCsv.tsx @@ -2,9 +2,12 @@ import { useState, useCallback, useMemo, useEffect, useRef } from 'react' import { useSearchParams } from 'next/navigation' import { useDropzone } from 'react-dropzone' import { Controller, useForm } from 'react-hook-form' -import { InputLabel, Select, MenuItem } from '@mui/material' - import { + InputLabel, + Select, + MenuItem, + Divider, + ListItemIcon, Stack, Box, Button, @@ -30,10 +33,19 @@ import { User } from 'types' import DownloadOutlined from '@mui/icons-material/DownloadOutlined' import CloseOutlined from '@mui/icons-material/CloseOutlined' import FilePresentOutlined from '@mui/icons-material/FilePresentOutlined' +import AddIcon from '@mui/icons-material/Add' import CsvNoticeCard from './CsvNoticeCard' import SnackMessage from '../SnackMessage' -import { parseCsvWithHeaders, parseCsvWithoutHeaders } from './parseCsv' +import { parseCsvWithHeaders, parseCsvWithoutHeaders, formatCsvHeaderForUi } from './parseCsv' + +/** Sentinel Select value — opens CSV marketplace; never stored as processorId. */ +const CREATE_NEW_CSV_PROCESSOR_VALUE = '__aai_csv_create_new__' + +function openCsvProcessorMarketplace() { + localStorage.setItem('answerai.csv.install-intent', 'true') + window.open('/sidekick-studio/marketplaces?usecase=CSV', '_blank', 'noopener,noreferrer') +} interface ChatFlow { id: string @@ -143,6 +155,34 @@ const ProcessCsv = ({ onRefreshChatflows?: () => Promise }) => { const theme = useTheme() + + /** Avoid full-screen blurred modal backdrop from global MuiBackdrop — keep a normal dropdown feel. */ + const csvProcessorSelectMenuProps = useMemo( + () => ({ + disableScrollLock: true, + BackdropProps: { + sx: { + backdropFilter: 'none', + WebkitBackdropFilter: 'none', + backgroundColor: 'transparent' + } + }, + PaperProps: { + sx: { + backdropFilter: 'none', + WebkitBackdropFilter: 'none', + backgroundImage: 'none', + bgcolor: 'background.paper', + border: '1px solid', + borderColor: 'divider', + boxShadow: theme.shadows[8], + maxHeight: 360 + } + } + }), + [theme.shadows] + ) + const [headers, setHeaders] = useState([]) const [rows, setRows] = useState([]) const [file, setFile] = useState(null) @@ -385,14 +425,19 @@ const ProcessCsv = ({ () => ({ ...{ ...baseStyle, - backgroundColor: theme.palette.grey[50], - borderColor: theme.palette.primary.main, - color: theme.palette.primary.main, + // Theme-aware tokens so the dropzone reads correctly in both + // light and dark mode. Hardcoding `grey[50]` produced a bright + // white surface in dark mode, and `primary.main` resolves to a + // translucent white in the AnswerAI dark theme — leaving the + // border + text effectively invisible. + backgroundColor: theme.palette.action.hover, + borderColor: theme.palette.divider, + color: theme.palette.text.primary, padding: theme.spacing(4), cursor: 'pointer' }, ...(isFocused ? { borderColor: theme.palette.secondary.main } : {}), - ...(isDragAccept ? { borderColor: theme.palette.primary.main } : {}), + ...(isDragAccept ? { borderColor: theme.palette.success.main } : {}), ...(isDragReject ? { borderColor: theme.palette.error.main } : {}) }), [isFocused, isDragAccept, isDragReject, theme] @@ -529,7 +574,7 @@ const ProcessCsv = ({ ) : ( - {'Drag 'n' drop a CSV file here, or click to select a file'} + Drag and drop a CSV file here, or click to select a file )} @@ -586,26 +631,49 @@ const ProcessCsv = ({ label='AI Processor' required error={!!errors.processorId} - disabled={chatflows.length === 0} fullWidth + displayEmpty + MenuProps={csvProcessorSelectMenuProps} + onChange={(e) => { + const v = e.target.value as string + if (v === CREATE_NEW_CSV_PROCESSOR_VALUE) { + openCsvProcessorMarketplace() + return + } + field.onChange(e) + }} + renderValue={(selected) => { + if (!selected) { + return ( + + {chatflows.length === 0 + ? 'Select or create a CSV processor' + : 'Select an AI processor'} + + ) + } + const cf = chatflows.find((c) => c.id === selected) + return cf?.name ?? selected + }} > - {chatflows.length === 0 ? ( - - No CSV processors available + {chatflows.map((chatflow) => ( + + {chatflow.name} - ) : ( - chatflows.map((chatflow) => ( - - {chatflow.name} - - )) - )} + ))} + {chatflows.length > 0 && } + + + + + Create new processor… + {errors.processorId?.message || (chatflows.length === 0 - ? 'Install a CSV processor from the marketplace below to continue' - : 'Select the AI model to process your CSV')} + ? 'Choose Create new processor or use the card below, then refresh.' + : 'Pick a processor above, or use Create new processor to open CSV templates in a new tab—install one, then refresh.')} )} @@ -736,43 +804,42 @@ const ProcessCsv = ({ ( - - - {headers.map((header) => { - const isSelected = value.includes(header) - return ( - { - if (!isSelected) { - onChange([...value, header]) - } else { - onChange(value.filter((col: string) => col !== header)) - } - }} - color={isSelected ? 'primary' : 'default'} - variant={isSelected ? 'filled' : 'outlined'} - sx={{ - '&:hover': { - backgroundColor: isSelected ? 'primary.main' : 'action.hover' - } - }} - /> - ) - })} - - - )} + render={({ field: { value, onChange } }) => { + const selected = Array.isArray(value) ? value : [] + return ( + + + {headers.map((header, index) => { + const visible = formatCsvHeaderForUi(header, index) + const isSelected = selected.includes(header) + return ( + { + if (!isSelected) { + onChange([...selected, header]) + } else { + onChange(selected.filter((col: string) => col !== header)) + } + }} + /> + ) + })} + + + ) + }} /> @@ -920,15 +987,18 @@ const ProcessCsv = ({ Selected columns: {watchedValues.sourceColumns.length} - {watchedValues.sourceColumns.slice(0, 3).map((col) => ( - 20 ? `${col.substring(0, 20)}...` : col} - title={col} // Full text on hover - size='small' - sx={{ maxWidth: '150px' }} - /> - ))} + {watchedValues.sourceColumns.slice(0, 3).map((col, i) => { + const display = formatCsvHeaderForUi(col, i) + return ( + 20 ? `${display.substring(0, 20)}…` : display} + title={display} + size='small' + sx={{ maxWidth: '150px' }} + /> + ) + })} {watchedValues.sourceColumns.length > 3 && ( { ) }} sx={{ + borderColor: 'divider', '& .super-app-theme--header': { fontWeight: 'bold', fontSize: '0.875rem', - color: 'white' + color: 'text.primary' }, '& .MuiDataGrid-cell': { fontSize: '0.825rem', - color: 'white' + color: 'text.primary' }, '& .MuiDataGrid-columnHeaders': { - fontWeight: 'bold' + fontWeight: 'bold', + color: 'text.primary', + borderBottomColor: 'divider' + }, + '& .MuiDataGrid-columnHeaderTitle': { + fontWeight: 700, + color: 'text.primary' + }, + '& .MuiDataGrid-row': { + '&:hover': { + bgcolor: 'action.hover' + } }, '& .MuiTablePagination-root': { - color: 'white' + color: 'text.primary', + '& .MuiTablePagination-selectLabel, & .MuiTablePagination-displayedRows': { + color: 'text.secondary' + } } }} /> diff --git a/packages-answers/ui/src/CsvTransfomer/parseCsv.ts b/packages-answers/ui/src/CsvTransfomer/parseCsv.ts index 319174314fb..ef02763650f 100644 --- a/packages-answers/ui/src/CsvTransfomer/parseCsv.ts +++ b/packages-answers/ui/src/CsvTransfomer/parseCsv.ts @@ -14,6 +14,23 @@ function generateColumnName(index: number): string { return `Column ${index + 1}` } +/** + * Strip BOM, zero-width, and format chars so headers are not visually blank in the UI. + */ +function sanitizeHeaderLabel(raw: string): string { + return String(raw ?? '') + .replace(/^\uFEFF/, '') + .replace(/[\u200B-\u200D\uFEFF]/g, '') + .trim() +} + +/** Readable label for chips and UI (handles invisible-only header cells). */ +export function formatCsvHeaderForUi(raw: string, columnIndex?: number): string { + const v = sanitizeHeaderLabel(String(raw ?? '')) + if (v) return v + return typeof columnIndex === 'number' ? generateColumnName(columnIndex) : 'Column' +} + /** * Parse CSV content using RFC 4180 compliant parser with headers */ @@ -32,11 +49,22 @@ export function parseCsvWithoutHeaders(input: string): ParsedCsvResult { * Parse CSV with headers */ function parseWithHeaders(input: string): ParsedCsvResult { + const headerCounts = new Map() + const result = Papa.parse>(input.trim(), { header: true, skipEmptyLines: true, comments: '#', - transformHeader: (header) => header.trim() // Clean up header names + transformHeader: (header, index) => { + const colIndex = typeof index === 'number' ? index : 0 + let base = sanitizeHeaderLabel(String(header ?? '')) + if (!base) { + base = generateColumnName(colIndex) + } + const n = (headerCounts.get(base) ?? 0) + 1 + headerCounts.set(base, n) + return n === 1 ? base : `${base} (${n})` + } }) // Be very lenient with errors - Papa Parse can handle most cases @@ -56,8 +84,8 @@ function parseWithHeaders(input: string): ParsedCsvResult { throw new Error('CSV has no header row or headers could not be determined.') } - // Filter out empty header names - const cleanHeaders = headers.filter((header) => header && header.trim() !== '') + // After transformHeader, names should be non-empty; keep only real labels + const cleanHeaders = headers.filter((h) => sanitizeHeaderLabel(h) !== '') if (cleanHeaders.length === 0) { throw new Error('CSV has no valid header names.') } diff --git a/packages-answers/ui/src/GuardrailsSettings/AdvancedMode.tsx b/packages-answers/ui/src/GuardrailsSettings/AdvancedMode.tsx index 511376a2312..a4be722887d 100644 --- a/packages-answers/ui/src/GuardrailsSettings/AdvancedMode.tsx +++ b/packages-answers/ui/src/GuardrailsSettings/AdvancedMode.tsx @@ -100,51 +100,6 @@ const innerTabsSx = { } } -// Shared sx for sliders. Default MUI Slider uses `primary.main` for the rail, -// track and thumb — all invisible in the AnswerAI dark theme. Anchor on -// `text.primary` so the slider track is visible regardless of mode. -const sliderSx = { - color: 'text.primary', - '& .MuiSlider-rail': { - opacity: 0.32 - }, - '& .MuiSlider-track': { - border: 'none' - }, - '& .MuiSlider-thumb': { - boxShadow: 'none', - '&:hover, &.Mui-focusVisible': { - boxShadow: '0 0 0 6px rgba(127, 127, 127, 0.16)' - } - }, - '& .MuiSlider-mark': { - backgroundColor: 'text.secondary', - opacity: 0.6 - }, - '& .MuiSlider-markLabel': { - color: 'text.secondary', - fontSize: 12 - } -} - -// Shared sx for the per-section Enable switches (Safety, PII, Faithfulness). -// The default MUI Switch reads as grey-on-grey in the AnswerAI dark theme -// because the active state resolves to translucent `primary.main`. Anchor -// checked thumb + track on `info.main` (Material Blue, shared across modes) -// so "on" is unmistakable and consistent with the page-level switches. -const sectionSwitchSx = { - '& .MuiSwitch-switchBase.Mui-checked': { - color: 'info.main', - '& + .MuiSwitch-track': { - backgroundColor: 'info.main', - opacity: 0.5 - } - }, - '& .MuiSwitch-switchBase.Mui-checked:hover': { - backgroundColor: 'rgba(33, 150, 243, 0.08)' - } -} - // Shared sx for the AccordionSummary section heads ("Input Validation", // "Output Validation", "Advanced Settings"). Aligns with the subtitle2 + // fontWeight 600 rhythm used elsewhere on the page and gives the summary a @@ -374,7 +329,6 @@ export default function AdvancedMode({ config, onSave, saving, error, success, o setSafetyEnabled(e.target.checked) markChanged() }} - sx={sectionSwitchSx} /> } label='Enable' @@ -413,7 +367,6 @@ export default function AdvancedMode({ config, onSave, saving, error, success, o { value: 1, label: '1.0' } ]} valueLabelDisplay='auto' - sx={sliderSx} /> @@ -505,7 +458,6 @@ export default function AdvancedMode({ config, onSave, saving, error, success, o step={0.01} valueLabelDisplay='auto' size='small' - sx={sliderSx} /> @@ -557,7 +509,6 @@ export default function AdvancedMode({ config, onSave, saving, error, success, o setPiiEnabled(e.target.checked) markChanged() }} - sx={sectionSwitchSx} /> } label='Enable' @@ -595,7 +546,6 @@ export default function AdvancedMode({ config, onSave, saving, error, success, o { value: 1, label: '1.0' } ]} valueLabelDisplay='auto' - sx={sliderSx} /> @@ -691,7 +641,6 @@ export default function AdvancedMode({ config, onSave, saving, error, success, o step={0.01} valueLabelDisplay='auto' size='small' - sx={sliderSx} /> @@ -769,7 +718,6 @@ export default function AdvancedMode({ config, onSave, saving, error, success, o setFaithfulnessEnabled(e.target.checked) markChanged() }} - sx={sectionSwitchSx} /> } label='Enable' @@ -800,7 +748,6 @@ export default function AdvancedMode({ config, onSave, saving, error, success, o { value: 0.1, label: '0.1' } ]} valueLabelDisplay='auto' - sx={sliderSx} /> @@ -861,7 +808,6 @@ export default function AdvancedMode({ config, onSave, saving, error, success, o step={1} marks valueLabelDisplay='auto' - sx={sliderSx} /> diff --git a/packages-answers/ui/src/GuardrailsSettings/MasterConfig.tsx b/packages-answers/ui/src/GuardrailsSettings/MasterConfig.tsx index c7590d38be9..1f9f4efb788 100644 --- a/packages-answers/ui/src/GuardrailsSettings/MasterConfig.tsx +++ b/packages-answers/ui/src/GuardrailsSettings/MasterConfig.tsx @@ -37,40 +37,6 @@ const ConfirmDialog = dynamic(() => import('flowise-ui/src/ui-component/dialog/C const FIDDLER_CREDENTIAL_NAME = 'fiddlerApi' -// Shared sx for "neutral" outlined action buttons (Edit, Change, Recheck). -// MUI's default outlined Button anchors text + border on `primary.main`, -// which in the AnswerAI dark theme resolves to rgba(255,255,255,0.12) — -// translucent white. The buttons end up reading as if they were disabled -// even when fully enabled. Anchor on `text.primary` + `divider` so they -// have clear contrast in both modes; the actual `disabled` state is left -// to MUI's default treatment so it still reads as inert when applicable. -const outlinedActionSx = { - color: 'text.primary', - borderColor: 'divider', - '&:hover': { - borderColor: 'text.primary', - bgcolor: 'action.hover' - } -} - -// Shared sx for the page switches ("Enable Guardrails", "Observability-only"). -// The default MUI `color='primary'` resolves to translucent white in the -// AnswerAI dark theme, making the on-state nearly invisible. Anchor the -// checked state on `info.main` (Material Blue, shared across light/dark) so -// the on-state reads unambiguously in both modes. -const pageSwitchSx = { - '& .MuiSwitch-switchBase.Mui-checked': { - color: 'info.main', - '& + .MuiSwitch-track': { - backgroundColor: 'info.main', - opacity: 0.5 - } - }, - '& .MuiSwitch-switchBase.Mui-checked:hover': { - backgroundColor: 'rgba(33, 150, 243, 0.08)' - } -} - interface GuardrailConfig { enabled?: boolean credentialId?: string @@ -356,7 +322,7 @@ export default function MasterConfig({ config, onConfigChange, onSave }: MasterC {/* Enable/Disable Toggle */} } + control={} label='Enable Guardrails' sx={{ mb: 2 }} /> @@ -410,12 +376,7 @@ export default function MasterConfig({ config, onConfigChange, onSave }: MasterC {/* Show Change when user owns any credentials they could switch to */} {credentials.length > 0 && ( - )} @@ -480,7 +441,6 @@ export default function MasterConfig({ config, onConfigChange, onSave }: MasterC disabled={!enabled || editLoading} onClick={handleEditCredential} startIcon={editLoading ? : } - sx={outlinedActionSx} > Edit @@ -500,7 +460,6 @@ export default function MasterConfig({ config, onConfigChange, onSave }: MasterC size='small' disabled={!enabled} onClick={() => setShowCredentialDropdown(!showCredentialDropdown)} - sx={outlinedActionSx} > Change @@ -556,7 +515,6 @@ export default function MasterConfig({ config, onConfigChange, onSave }: MasterC onClick={loadCapabilities} disabled={loadingCapabilities} startIcon={loadingCapabilities ? : } - sx={outlinedActionSx} > Recheck @@ -742,43 +700,12 @@ export default function MasterConfig({ config, onConfigChange, onSave }: MasterC disabled={!enabled} aria-label='Failure mode' size='small' - sx={(theme) => { - // The AnswerAI theme defines `primary.{main,light,dark}` as - // translucent whites/slates rather than a vivid color, so anchoring - // the selected state on `primary.*` produced ghosted text in dark - // mode. We use MUI's mode-balanced `action.selected` token + a - // visible border + `text.primary` for guaranteed contrast in both - // modes — same pattern used for menu/table selection across the app. - const isDark = theme.palette.mode === 'dark' - return { - '& .MuiToggleButton-root': { - color: 'text.primary', - borderColor: 'divider', - textTransform: 'none', - fontWeight: 500, - px: 1.5, - transition: 'background-color 120ms ease, border-color 120ms ease' - }, - '& .MuiToggleButton-root:hover': { - bgcolor: 'action.hover' - }, - '& .MuiToggleButton-root.Mui-selected': { - bgcolor: 'action.selected', - color: 'text.primary', - fontWeight: 600, - borderColor: isDark ? 'rgba(255, 255, 255, 0.45)' : 'rgba(15, 23, 42, 0.5)', - '&:hover': { - bgcolor: isDark ? 'rgba(255, 255, 255, 0.22)' : 'rgba(15, 23, 42, 0.12)' - } - } - } - }} > - + Fail open (allow, warn) - + Fail closed (block, 503) @@ -792,14 +719,7 @@ export default function MasterConfig({ config, onConfigChange, onSave }: MasterC {/* Shadow pilot toggle. Violations are recorded but never block. */} - } + control={} label={ Observability-only (shadow mode) diff --git a/packages-answers/ui/src/GuardrailsSettings/SimpleMode.tsx b/packages-answers/ui/src/GuardrailsSettings/SimpleMode.tsx index 826fb73f698..3cf30f63bdc 100644 --- a/packages-answers/ui/src/GuardrailsSettings/SimpleMode.tsx +++ b/packages-answers/ui/src/GuardrailsSettings/SimpleMode.tsx @@ -175,27 +175,12 @@ export default function SimpleMode({ config, onSave, saving, error, success, onC - {/* Save uses a local sx override to force a solid disabled background. - The global theme applies a gradient `background` to all contained - buttons; MUI's default disabled state only resets `backgroundColor`, - which leaves the gradient bleeding through at low contrast in both - modes. We zero out `background` + `boxShadow` on disabled here so - the button reads unmistakably as disabled when no preset is chosen. */} diff --git a/packages-answers/ui/src/theme/components/muiOverrides.ts b/packages-answers/ui/src/theme/components/muiOverrides.ts index 99889a1249e..afac4f0f3da 100644 --- a/packages-answers/ui/src/theme/components/muiOverrides.ts +++ b/packages-answers/ui/src/theme/components/muiOverrides.ts @@ -5,11 +5,15 @@ import { Components, Theme } from '@mui/material/styles' import { glassmorphismTokens } from '../tokens/glassmorphism' -import { colorTokens } from '../tokens/colors' +import { colorTokens, statusColors } from '../tokens/colors' export const muiComponentOverrides = (mode: 'light' | 'dark'): Components> => { const glass = glassmorphismTokens[mode] const colors = colorTokens[mode] + // Heavier neutral border used for "selected" affordances (toggle buttons, + // selected cards) so the chosen state reads unambiguously in both themes. + const selectedBorder = mode === 'dark' ? 'rgba(255, 255, 255, 0.45)' : 'rgba(15, 23, 42, 0.5)' + const selectedBorderHover = mode === 'dark' ? 'rgba(255, 255, 255, 0.6)' : 'rgba(15, 23, 42, 0.7)' return { // Global CSS Baseline @@ -137,16 +141,20 @@ export const muiComponentOverrides = (mode: 'light' | 'dark'): Components - Got it! We'll tailor the workshop follow-ups to your goals. + Got it! We'll tailor the workshop follow-ups to your goals. )} diff --git a/packages/server/src/controllers/csv-parser/index.ts b/packages/server/src/controllers/csv-parser/index.ts index 518ed2fd099..18bcc43eda5 100644 --- a/packages/server/src/controllers/csv-parser/index.ts +++ b/packages/server/src/controllers/csv-parser/index.ts @@ -3,6 +3,7 @@ import csvParserService from '../../services/csv-parser' import { InternalFlowiseError } from '../../errors/internalFlowiseError' import { StatusCodes } from 'http-status-codes' import { CreateCsvParseRunRequest } from '../../types/csvTypes' +import { isCsvRunCronEnabled } from '../../utils/isCsvRunCronEnabled' const getAllCsvParseRuns = async (req: Request, res: Response, next: NextFunction) => { try { @@ -76,6 +77,17 @@ const createCsvParseRun = async (req: Request, res: Response, next: NextFunction } } +const getWorkerStatus = async (req: Request, res: Response, next: NextFunction) => { + try { + if (!req.user) { + throw new InternalFlowiseError(StatusCodes.UNAUTHORIZED, 'Error: csvParserController.getWorkerStatus - Unauthorized') + } + return res.json({ workerEnabled: isCsvRunCronEnabled() }) + } catch (error) { + next(error) + } +} + const getProcessedCsvSignedUrl = async (req: Request, res: Response, next: NextFunction) => { try { if (!req.user) { @@ -96,6 +108,7 @@ const getProcessedCsvSignedUrl = async (req: Request, res: Response, next: NextF export default { getAllCsvParseRuns, + getWorkerStatus, getCsvParseRunById, createCsvParseRun, getProcessedCsvSignedUrl diff --git a/packages/server/src/routes/csv-parser/index.ts b/packages/server/src/routes/csv-parser/index.ts index 93cea695e3b..8e67b75e45d 100644 --- a/packages/server/src/routes/csv-parser/index.ts +++ b/packages/server/src/routes/csv-parser/index.ts @@ -4,6 +4,7 @@ import csvParserController from '../../controllers/csv-parser' const router = express.Router() router.get('/', csvParserController.getAllCsvParseRuns) +router.get('/worker-status', csvParserController.getWorkerStatus) router.get('/:id', csvParserController.getCsvParseRunById) router.post('/', csvParserController.createCsvParseRun) router.get('/:id/signed-url', csvParserController.getProcessedCsvSignedUrl) diff --git a/packages/server/src/utils/cron.ts b/packages/server/src/utils/cron.ts index 58b0d189fe0..fb3046d6f44 100644 --- a/packages/server/src/utils/cron.ts +++ b/packages/server/src/utils/cron.ts @@ -1,6 +1,7 @@ import cron from 'node-cron' import axios from 'axios' import logger from './logger' +import { isCsvRunCronEnabled } from './isCsvRunCronEnabled' import initCsvRun from '../jobs/initCsvRun' import processCsvRows from '../jobs/processCsvRows' import generateCsv from '../jobs/generateCsv' @@ -22,7 +23,6 @@ const API_HOST = process.env.API_HOST || `http://localhost:${process.env.PORT || * Default: true */ const ENABLE_BILLING_SYNC_CRON = process.env.ENABLE_BILLING_SYNC_CRON !== 'false' -const ENABLE_CSV_RUN_CRON = process.env.ENABLE_CSV_RUN_CRON === 'true' /** * Initialize cron jobs @@ -51,7 +51,7 @@ export function initCronJobs() { logger.info('📅 [cron]: Billing usage sync cron job is disabled') } - if (ENABLE_CSV_RUN_CRON) { + if (isCsvRunCronEnabled()) { logger.info('📅 [cron]: Initializing csv run cron job') initCsvRun() processCsvRows() diff --git a/packages/server/src/utils/isCsvRunCronEnabled.ts b/packages/server/src/utils/isCsvRunCronEnabled.ts new file mode 100644 index 00000000000..9bde4b31830 --- /dev/null +++ b/packages/server/src/utils/isCsvRunCronEnabled.ts @@ -0,0 +1,7 @@ +/** + * Single source of truth for ENABLE_CSV_RUN_CRON (Flowise server process env). + * Used by cron registration and the csv-parser worker-status API. + */ +export function isCsvRunCronEnabled(): boolean { + return process.env.ENABLE_CSV_RUN_CRON === 'true' +} diff --git a/packages/ui/src/themes/compStyleOverride.js b/packages/ui/src/themes/compStyleOverride.js index 8f24b0c7cfe..c95fbbe5a77 100644 --- a/packages/ui/src/themes/compStyleOverride.js +++ b/packages/ui/src/themes/compStyleOverride.js @@ -178,22 +178,91 @@ export default function componentStyleOverrides(theme) { } } }, + // SLIDER + // Anchor on `text.primary` so rail/track/thumb stay visible regardless + // of mode. Mirrors the unified theme override for consistency between + // TheAnswer pages and Flowise core canvas/dialogs. MuiSlider: { styleOverrides: { root: { + color: theme.darkTextPrimary, '&.Mui-disabled': { color: theme.colors?.grey300 } }, + rail: { + opacity: 0.32 + }, + track: { + border: 'none' + }, + thumb: { + boxShadow: 'none', + '&:hover, &.Mui-focusVisible': { + boxShadow: '0 0 0 6px rgba(127, 127, 127, 0.16)' + } + }, mark: { - backgroundColor: theme.paper, + backgroundColor: theme.darkTextSecondary, + opacity: 0.6, width: '4px' }, + markLabel: { + color: theme.darkTextSecondary, + fontSize: 12 + }, valueLabel: { color: theme?.colors?.primaryLight } } }, + // SWITCH + // Anchor checked state on Material Blue (`#2196f3`) so the on-state + // reads unambiguously across both Flowise core themes. Matches the + // unified TheAnswer theme — switches in ChatflowGuardrails / ShareChatbot + // / canvas dialogs visually align with switches in the rest of the app. + MuiSwitch: { + styleOverrides: { + switchBase: { + '&.Mui-checked': { + color: '#2196f3', + '& + .MuiSwitch-track': { + backgroundColor: '#2196f3', + opacity: 0.5 + }, + '&:hover': { + backgroundColor: 'rgba(33, 150, 243, 0.08)' + } + } + } + } + }, + // TOGGLE BUTTON + // Selected state on a translucent neutral overlay + heavier border so + // the chosen option is unmistakable in both themes. + MuiToggleButton: { + styleOverrides: { + root: { + color: theme.darkTextPrimary, + borderColor: theme.divider, + textTransform: 'none', + transition: 'background-color 120ms ease, border-color 120ms ease', + '&:hover': { + backgroundColor: theme?.customization?.isDarkMode ? 'rgba(255, 255, 255, 0.08)' : 'rgba(15, 23, 42, 0.04)' + }, + '&.Mui-selected': { + backgroundColor: theme?.customization?.isDarkMode ? 'rgba(255, 255, 255, 0.16)' : 'rgba(15, 23, 42, 0.08)', + color: theme.darkTextPrimary, + fontWeight: 600, + borderColor: theme?.customization?.isDarkMode ? 'rgba(255, 255, 255, 0.45)' : 'rgba(15, 23, 42, 0.5)', + '&:hover': { + backgroundColor: theme?.customization?.isDarkMode ? 'rgba(255, 255, 255, 0.16)' : 'rgba(15, 23, 42, 0.08)', + borderColor: theme?.customization?.isDarkMode ? 'rgba(255, 255, 255, 0.6)' : 'rgba(15, 23, 42, 0.7)' + } + } + } + } + }, MuiDivider: { styleOverrides: { root: { From 465b3977c3843b771d8f05db26f1ac28db00dcec Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Tue, 5 May 2026 18:19:56 -0400 Subject: [PATCH 2/4] refactor(csv-transformer): enhance MuiChip styling for improved UI interaction - Removed redundant color and variant props from the CsvTransformer component to streamline the code. - Introduced custom styling for MuiChip components, ensuring better visual feedback on selection states. - Updated hover and active states for chips to enhance user experience and maintain consistency across light and dark modes. This refactor improves the overall maintainability of the component while enhancing the user interface. --- .../ui/src/CsvTransfomer/ProcessCsv.tsx | 22 ++++++++++- .../ui/src/theme/components/muiOverrides.ts | 39 +++++++++---------- 2 files changed, 38 insertions(+), 23 deletions(-) diff --git a/packages-answers/ui/src/CsvTransfomer/ProcessCsv.tsx b/packages-answers/ui/src/CsvTransfomer/ProcessCsv.tsx index d3ab75eafc7..8a7ad9c3503 100644 --- a/packages-answers/ui/src/CsvTransfomer/ProcessCsv.tsx +++ b/packages-answers/ui/src/CsvTransfomer/ProcessCsv.tsx @@ -824,8 +824,6 @@ const ProcessCsv = ({ key={`csv-col-${index}`} label={visible} size='small' - color={isSelected ? 'primary' : 'default'} - variant={isSelected ? 'filled' : 'outlined'} onClick={() => { if (!isSelected) { onChange([...selected, header]) @@ -833,6 +831,26 @@ const ProcessCsv = ({ onChange(selected.filter((col: string) => col !== header)) } }} + sx={{ + cursor: 'pointer', + ...(isSelected + ? { + bgcolor: '#2196f3', + border: '1px solid #2196f3', + '& .MuiChip-label': { color: '#fff' }, + '&:hover': { bgcolor: '#1976d2', borderColor: '#1976d2' } + } + : { + bgcolor: 'transparent', + border: '1px solid', + borderColor: 'divider', + '& .MuiChip-label': { color: 'text.primary' }, + '&:hover': { + bgcolor: 'action.hover', + borderColor: 'text.secondary' + } + }) + }} /> ) })} diff --git a/packages-answers/ui/src/theme/components/muiOverrides.ts b/packages-answers/ui/src/theme/components/muiOverrides.ts index afac4f0f3da..a7e863c70ad 100644 --- a/packages-answers/ui/src/theme/components/muiOverrides.ts +++ b/packages-answers/ui/src/theme/components/muiOverrides.ts @@ -465,31 +465,28 @@ export const muiComponentOverrides = (mode: 'light' | 'dark'): Components Date: Tue, 5 May 2026 18:49:49 -0400 Subject: [PATCH 3/4] refactor(csv-transformer): enhance styling for improved UI consistency - Updated MuiChip component styles to provide better visual feedback across light and dark modes. - Adjusted background and border colors for various states to ensure consistency and improved user interaction. - Refined typography color settings for better readability based on file presence. This refactor enhances the overall user experience and maintains a cohesive design across the application. --- .../ui/src/CsvTransfomer/ProcessCsv.tsx | 56 ++++++++++++++----- .../ui/src/theme/components/muiOverrides.ts | 27 +++++---- 2 files changed, 58 insertions(+), 25 deletions(-) diff --git a/packages-answers/ui/src/CsvTransfomer/ProcessCsv.tsx b/packages-answers/ui/src/CsvTransfomer/ProcessCsv.tsx index 8a7ad9c3503..624000d4880 100644 --- a/packages-answers/ui/src/CsvTransfomer/ProcessCsv.tsx +++ b/packages-answers/ui/src/CsvTransfomer/ProcessCsv.tsx @@ -831,7 +831,7 @@ const ProcessCsv = ({ onChange(selected.filter((col: string) => col !== header)) } }} - sx={{ + sx={(t) => ({ cursor: 'pointer', ...(isSelected ? { @@ -841,16 +841,30 @@ const ProcessCsv = ({ '&:hover': { bgcolor: '#1976d2', borderColor: '#1976d2' } } : { - bgcolor: 'transparent', + bgcolor: + t.palette.mode === 'light' + ? 'rgba(15,23,42,0.25)' + : 'rgba(255,255,255,0.12)', border: '1px solid', - borderColor: 'divider', - '& .MuiChip-label': { color: 'text.primary' }, + borderColor: + t.palette.mode === 'light' + ? 'rgba(15,23,42,0.25)' + : 'rgba(255,255,255,0.22)', + '& .MuiChip-label': { + color: t.palette.text.primary + }, '&:hover': { - bgcolor: 'action.hover', - borderColor: 'text.secondary' + bgcolor: + t.palette.mode === 'light' + ? 'rgba(15,23,42,0.15)' + : 'rgba(255,255,255,0.2)', + borderColor: + t.palette.mode === 'light' + ? 'rgba(15,23,42,0.45)' + : 'rgba(255,255,255,0.4)' } }) - }} + })} /> ) })} @@ -924,7 +938,7 @@ const ProcessCsv = ({ = 1 ? 'pointer' : 'default', @@ -1013,7 +1027,12 @@ const ProcessCsv = ({ label={display.length > 20 ? `${display.substring(0, 20)}…` : display} title={display} size='small' - sx={{ maxWidth: '150px' }} + sx={{ + maxWidth: '150px', + bgcolor: '#2196f3', + border: '1px solid #2196f3', + '& .MuiChip-label': { color: '#fff' } + }} /> ) })} @@ -1021,7 +1040,14 @@ const ProcessCsv = ({ ({ + bgcolor: + t.palette.mode === 'light' ? 'rgba(15,23,42,0.06)' : 'rgba(255,255,255,0.25)', + border: '1px solid', + borderColor: + t.palette.mode === 'light' ? 'rgba(15,23,42,0.2)' : 'rgba(255,255,255,0.18)', + '& .MuiChip-label': { color: t.palette.text.secondary } + })} /> )} @@ -1038,7 +1064,7 @@ const ProcessCsv = ({ = 2 ? 'pointer' : 'default', @@ -1250,7 +1276,7 @@ const ProcessCsv = ({ {activeStep === 3 ? ( ) : ( - )} diff --git a/packages-answers/ui/src/theme/components/muiOverrides.ts b/packages-answers/ui/src/theme/components/muiOverrides.ts index a7e863c70ad..da915132096 100644 --- a/packages-answers/ui/src/theme/components/muiOverrides.ts +++ b/packages-answers/ui/src/theme/components/muiOverrides.ts @@ -266,14 +266,20 @@ export const muiComponentOverrides = (mode: 'light' | 'dark'): Components Date: Tue, 5 May 2026 18:58:04 -0400 Subject: [PATCH 4/4] refactor(muiOverrides): improve MuiChip label styling for better contrast - Updated styling for filled colored chips to ensure labels remain white for contrast. - Clarified comments to specify that outlined colored chips will use the palette color for text, avoiding unnecessary overrides. This refactor enhances the visual consistency and accessibility of the MuiChip component across different color modes. --- packages-answers/ui/src/theme/components/muiOverrides.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages-answers/ui/src/theme/components/muiOverrides.ts b/packages-answers/ui/src/theme/components/muiOverrides.ts index da915132096..6871b045ff1 100644 --- a/packages-answers/ui/src/theme/components/muiOverrides.ts +++ b/packages-answers/ui/src/theme/components/muiOverrides.ts @@ -490,8 +490,9 @@ export const muiComponentOverrides = (mode: 'light' | 'dark'): Components