@@ -47,20 +47,52 @@ export function IntakePage() {
4747 const { setCurrentPhase } = useWorkflowContext ( ) ;
4848 const queryClient = useQueryClient ( ) ;
4949
50- const [ config , setConfig ] = useState < MLflowConfig > ( {
51- databricks_host : '' ,
52- databricks_token : '' ,
53- experiment_id : '' ,
54- max_traces : 100 ,
55- filter_string : ''
56- } ) ;
50+ // Load MLflow config from localStorage on initial mount
51+ const getInitialConfig = ( ) : MLflowConfig => {
52+ try {
53+ const saved = localStorage . getItem ( 'mlflow_config' ) ;
54+ if ( saved ) {
55+ const parsed = JSON . parse ( saved ) ;
56+ return {
57+ databricks_host : parsed . databricks_host || '' ,
58+ databricks_token : parsed . databricks_token || '' ,
59+ experiment_id : parsed . experiment_id || '' ,
60+ max_traces : parsed . max_traces || 100 ,
61+ filter_string : parsed . filter_string || ''
62+ } ;
63+ }
64+ } catch ( e ) {
65+ // Ignore localStorage errors
66+ }
67+ return {
68+ databricks_host : '' ,
69+ databricks_token : '' ,
70+ experiment_id : '' ,
71+ max_traces : 100 ,
72+ filter_string : ''
73+ } ;
74+ } ;
75+
76+ const [ config , setConfig ] = useState < MLflowConfig > ( getInitialConfig ) ;
5777
5878 const [ status , setStatus ] = useState < MLflowStatus | null > ( null ) ;
5979 const [ isIngesting , setIsIngesting ] = useState ( false ) ;
6080 const [ error , setError ] = useState < string | null > ( null ) ;
6181 const [ csvFile , setCsvFile ] = useState < File | null > ( null ) ;
6282 const [ isUploadingCsv , setIsUploadingCsv ] = useState ( false ) ;
6383 const [ isDeleting , setIsDeleting ] = useState ( false ) ;
84+ const [ csvImportDestination , setCsvImportDestination ] = useState < 'discovery' | 'mlflow' | null > ( null ) ;
85+
86+ // Save config to localStorage whenever it changes
87+ useEffect ( ( ) => {
88+ if ( config . databricks_host || config . databricks_token || config . experiment_id ) {
89+ try {
90+ localStorage . setItem ( 'mlflow_config' , JSON . stringify ( config ) ) ;
91+ } catch ( e ) {
92+ // Ignore localStorage errors
93+ }
94+ }
95+ } , [ config ] ) ;
6496
6597 // Load existing configuration and status
6698 useEffect ( ( ) => {
@@ -79,9 +111,16 @@ export function IntakePage() {
79111 const statusData = await response . json ( ) ;
80112 setStatus ( statusData ) ;
81113
82- // Load existing config if available
114+ // Merge backend config with existing config (prefer backend values if present)
83115 if ( statusData . config ) {
84- setConfig ( statusData . config ) ;
116+ setConfig ( prev => ( {
117+ ...prev ,
118+ databricks_host : statusData . config . databricks_host || prev . databricks_host ,
119+ databricks_token : statusData . config . databricks_token || prev . databricks_token ,
120+ experiment_id : statusData . config . experiment_id || prev . experiment_id ,
121+ max_traces : statusData . config . max_traces || prev . max_traces ,
122+ filter_string : statusData . config . filter_string || prev . filter_string
123+ } ) ) ;
85124 }
86125 }
87126 } catch ( err ) {
@@ -217,6 +256,58 @@ export function IntakePage() {
217256 }
218257 } ;
219258
259+ const uploadCsvToMlflow = async ( ) => {
260+ if ( ! workshopId ) {
261+ setError ( 'No workshop available. Please create a workshop first.' ) ;
262+ return ;
263+ }
264+
265+ if ( ! csvFile ) {
266+ setError ( 'Please select a CSV file to upload.' ) ;
267+ return ;
268+ }
269+
270+ if ( ! config . databricks_host || ! config . databricks_token || ! config . experiment_id ) {
271+ setError ( 'Please configure MLflow settings (Databricks Host, Token, and Experiment ID) before uploading.' ) ;
272+ return ;
273+ }
274+
275+ setIsUploadingCsv ( true ) ;
276+ setError ( null ) ;
277+
278+ try {
279+ const formData = new FormData ( ) ;
280+ formData . append ( 'file' , csvFile ) ;
281+ formData . append ( 'databricks_host' , config . databricks_host ) ;
282+ formData . append ( 'databricks_token' , config . databricks_token ) ;
283+ formData . append ( 'experiment_id' , config . experiment_id ) ;
284+
285+ const response = await fetch ( `/workshops/${ workshopId } /csv-upload-to-mlflow` , {
286+ method : 'POST' ,
287+ body : formData ,
288+ } ) ;
289+
290+ if ( response . ok ) {
291+ const result = await response . json ( ) ;
292+
293+ toast . success ( `Successfully created ${ result . mlflow_traces_created } MLflow traces! Use "Import from MLflow" to bring them into Discovery.` ) ;
294+ setCsvFile ( null ) ;
295+
296+ if ( result . warnings && result . warnings . length > 0 ) {
297+ toast . warning ( `${ result . warnings . length } rows had issues. Check console for details.` ) ;
298+ console . warn ( 'CSV upload warnings:' , result . warnings ) ;
299+ }
300+ } else {
301+ const errorData = await response . json ( ) . catch ( ( ) => ( { } ) ) ;
302+ setError ( errorData . detail || `Failed to upload CSV to MLflow (HTTP ${ response . status } )` ) ;
303+ }
304+ } catch ( err ) {
305+ setError ( 'Network error: Unable to connect to the server. Please check your connection and try again.' ) ;
306+ } finally {
307+ setIsUploadingCsv ( false ) ;
308+ }
309+ } ;
310+
220311 const deleteAllTraces = async ( ) => {
221312 if ( ! workshopId ) {
222313 setError ( 'No workshop available.' ) ;
@@ -437,8 +528,8 @@ export function IntakePage() {
437528 Upload Traces from CSV
438529 </ CardTitle >
439530 < CardDescription >
440- Upload traces from an MLflow trace export CSV for customers without direct MLflow access .
441- CSV must have "request_preview" and "response_preview" columns .
531+ Upload conversational data from a CSV file. CSV must have "request_preview" and "response_preview" columns .
532+ Optionally log the data to MLflow as traces .
442533 </ CardDescription >
443534 </ CardHeader >
444535 < CardContent className = "space-y-4" >
@@ -472,24 +563,87 @@ export function IntakePage() {
472563 </ p >
473564 ) }
474565 < p className = "text-xs text-gray-500" >
475- Expected format: MLflow trace export CSV with "request_preview" and "response_preview" columns
566+ Expected format: CSV with "request_preview" and "response_preview" columns
476567 </ p >
477568 </ div >
478569
570+ { /* Import destination options */ }
571+ < div className = "space-y-2" >
572+ < Label className = "text-sm font-medium text-gray-700" > Import destination:</ Label >
573+ < div className = "space-y-2" >
574+ < div
575+ className = { `flex items-center space-x-3 p-3 rounded-lg border-2 cursor-pointer transition-colors ${
576+ csvImportDestination === 'discovery'
577+ ? 'border-blue-500 bg-blue-50'
578+ : 'border-gray-200 hover:border-gray-300'
579+ } `}
580+ onClick = { ( ) => setCsvImportDestination ( 'discovery' ) }
581+ >
582+ < input
583+ type = "radio"
584+ id = "import-discovery"
585+ name = "csv-import-destination"
586+ checked = { csvImportDestination === 'discovery' }
587+ onChange = { ( ) => setCsvImportDestination ( 'discovery' ) }
588+ className = "h-4 w-4 text-blue-600 focus:ring-blue-500"
589+ />
590+ < div className = "flex-1" >
591+ < label htmlFor = "import-discovery" className = "text-sm font-medium text-gray-700 cursor-pointer" >
592+ Import directly into Discovery
593+ </ label >
594+ < p className = "text-xs text-gray-500" > Add traces to workshop for immediate use</ p >
595+ </ div >
596+ </ div >
597+
598+ < div
599+ className = { `flex items-center space-x-3 p-3 rounded-lg border-2 cursor-pointer transition-colors ${
600+ csvImportDestination === 'mlflow'
601+ ? 'border-purple-500 bg-purple-50'
602+ : 'border-gray-200 hover:border-gray-300'
603+ } `}
604+ onClick = { ( ) => setCsvImportDestination ( 'mlflow' ) }
605+ >
606+ < input
607+ type = "radio"
608+ id = "import-mlflow"
609+ name = "csv-import-destination"
610+ checked = { csvImportDestination === 'mlflow' }
611+ onChange = { ( ) => setCsvImportDestination ( 'mlflow' ) }
612+ className = "h-4 w-4 text-purple-600 focus:ring-purple-500"
613+ />
614+ < div className = "flex-1" >
615+ < label htmlFor = "import-mlflow" className = "text-sm font-medium text-gray-700 cursor-pointer" >
616+ Log to MLflow as traces
617+ </ label >
618+ < p className = "text-xs text-gray-500" > Create MLflow traces only (use "Import from MLflow" to add to Discovery later)</ p >
619+ </ div >
620+ </ div >
621+ </ div >
622+ </ div >
623+
624+ { csvImportDestination === 'mlflow' && ( ! config . databricks_host || ! config . databricks_token || ! config . experiment_id ) && (
625+ < Alert className = "bg-amber-50 border-amber-200" >
626+ < AlertCircle className = "h-4 w-4 text-amber-600" />
627+ < AlertDescription className = "text-amber-700" >
628+ Please configure MLflow settings above to log traces to MLflow.
629+ </ AlertDescription >
630+ </ Alert >
631+ ) }
632+
479633 < Button
480- onClick = { uploadCsvFile }
481- disabled = { isUploadingCsv || ! csvFile }
634+ onClick = { csvImportDestination === 'mlflow' ? uploadCsvToMlflow : uploadCsvFile }
635+ disabled = { isUploadingCsv || ! csvFile || ! csvImportDestination || ( csvImportDestination === 'mlflow' && ( ! config . databricks_host || ! config . databricks_token || ! config . experiment_id ) ) }
482636 className = "w-full"
483637 >
484638 { isUploadingCsv ? (
485639 < >
486640 < Loader2 className = "mr-2 h-4 w-4 animate-spin" />
487- Uploading CSV ...
641+ { csvImportDestination === 'mlflow' ? 'Creating MLflow Traces...' : ' Uploading to Discovery ...' }
488642 </ >
489643 ) : (
490644 < >
491645 < Upload className = "mr-2 h-4 w-4" />
492- Upload CSV Traces
646+ { csvImportDestination === 'mlflow' ? ' Upload & Log to MLflow' : csvImportDestination === 'discovery' ? 'Upload to Discovery' : 'Select destination above' }
493647 </ >
494648 ) }
495649 </ Button >
0 commit comments