Skip to content

Commit 7da50f5

Browse files
Merge pull request #26 from databricks-solutions/vivian_dev
CSV upload improvements: separate Discovery/MLflow paths, remember MLflow config
2 parents bf65c13 + 573c413 commit 7da50f5

5 files changed

Lines changed: 475 additions & 124 deletions

File tree

client/src/components/AnnotationStartPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ export const AnnotationStartPage: React.FC<AnnotationStartPageProps> = ({ onStar
243243
</div>
244244
)}
245245
</div>
246-
246+
247247
<div className="mt-4 p-3 bg-purple-100 rounded-lg">
248248
<p className="text-sm text-purple-800">
249249
<strong>Selected:</strong> {

client/src/pages/IntakePage.tsx

Lines changed: 170 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)