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 ? (
-