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..624000d4880 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,74 @@ 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)) + } + }} + sx={(t) => ({ + cursor: 'pointer', + ...(isSelected + ? { + bgcolor: '#2196f3', + border: '1px solid #2196f3', + '& .MuiChip-label': { color: '#fff' }, + '&:hover': { bgcolor: '#1976d2', borderColor: '#1976d2' } + } + : { + bgcolor: + t.palette.mode === 'light' + ? 'rgba(15,23,42,0.25)' + : 'rgba(255,255,255,0.12)', + border: '1px solid', + 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: + 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)' + } + }) + })} + /> + ) + })} + + + ) + }} /> @@ -839,7 +938,7 @@ const ProcessCsv = ({ = 1 ? 'pointer' : 'default', @@ -920,20 +1019,35 @@ 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', + bgcolor: '#2196f3', + border: '1px solid #2196f3', + '& .MuiChip-label': { color: '#fff' } + }} + /> + ) + })} {watchedValues.sourceColumns.length > 3 && ( ({ + 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 } + })} /> )} @@ -950,7 +1064,7 @@ const ProcessCsv = ({ = 2 ? 'pointer' : 'default', @@ -1162,7 +1276,7 @@ const ProcessCsv = ({ {activeStep === 3 ? ( ) : ( - )} diff --git a/packages-answers/ui/src/CsvTransfomer/ProcessingHistory.tsx b/packages-answers/ui/src/CsvTransfomer/ProcessingHistory.tsx index 0f061f9cac4..10dde23e5ad 100644 --- a/packages-answers/ui/src/CsvTransfomer/ProcessingHistory.tsx +++ b/packages-answers/ui/src/CsvTransfomer/ProcessingHistory.tsx @@ -294,20 +294,35 @@ const ProcessingHistory = ({ user }: { user: User }) => { ) }} 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..6871b045ff1 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: {