Skip to content

Commit bd188f1

Browse files
patconclaude
andcommitted
Add load-file button to PipelineSelector for h5ad mode
Thread onLoadFile callback from story through App and D3Map to PipelineSelector, showing an upload button that lets users load a new h5ad file without refreshing the page. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 744b6f3 commit bd188f1

4 files changed

Lines changed: 52 additions & 11 deletions

File tree

src/components/convo-explorer/App.h5ad.stories.tsx

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ function H5adFileLoader() {
1313
const [preloadedData, setPreloadedData] = React.useState<PreloadedData | null>(null);
1414
const [loading, setLoading] = React.useState(false);
1515
const [error, setError] = React.useState<string | null>(null);
16+
const fileInputRef = React.useRef<HTMLInputElement>(null);
17+
18+
const handleLoadFile = React.useCallback(() => {
19+
fileInputRef.current?.click();
20+
}, []);
1621

1722
const handleFileChange = React.useCallback(async (event: React.ChangeEvent<HTMLInputElement>) => {
1823
const file = event.target.files?.[0];
@@ -35,6 +40,8 @@ function H5adFileLoader() {
3540
setError(err instanceof Error ? err.message : 'Failed to parse h5ad file');
3641
} finally {
3742
setLoading(false);
43+
// Reset so re-selecting the same file triggers onChange again
44+
if (fileInputRef.current) fileInputRef.current.value = '';
3845
}
3946
}, []);
4047

@@ -53,25 +60,39 @@ function H5adFileLoader() {
5360
{error && (
5461
<p className="text-sm text-destructive">{error}</p>
5562
)}
56-
<label
63+
<input
64+
ref={fileInputRef}
65+
type="file"
66+
accept=".h5ad,.h5,.hdf5"
67+
onChange={handleFileChange}
68+
className="sr-only"
69+
/>
70+
<button
5771
className={`inline-flex items-center gap-2 px-4 py-2 rounded-md border cursor-pointer
5872
hover:bg-accent hover:text-accent-foreground transition-colors
5973
${loading ? 'opacity-50 pointer-events-none' : ''}`}
74+
onClick={handleLoadFile}
75+
disabled={loading}
6076
>
6177
{loading ? 'Loading...' : 'Choose .h5ad file'}
62-
<input
63-
type="file"
64-
accept=".h5ad,.h5,.hdf5"
65-
onChange={handleFileChange}
66-
className="sr-only"
67-
/>
68-
</label>
78+
</button>
6979
</div>
7080
</div>
7181
);
7282
}
7383

74-
return <App preloadedData={preloadedData} />;
84+
return (
85+
<>
86+
<input
87+
ref={fileInputRef}
88+
type="file"
89+
accept=".h5ad,.h5,.hdf5"
90+
onChange={handleFileChange}
91+
className="sr-only"
92+
/>
93+
<App preloadedData={preloadedData} onLoadFile={handleLoadFile} />
94+
</>
95+
);
7596
}
7697

7798
const meta: Meta = {

src/components/convo-explorer/App.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,10 @@ type AppProps = {
4444
initialPipelineId?: string;
4545
pipelineFilter?: string;
4646
preloadedData?: PreloadedData;
47+
onLoadFile?: () => void;
4748
};
4849

49-
export const App: React.FC<AppProps> = ({ testAnimation = false, kedroBaseUrl, initialPipelineId, pipelineFilter, preloadedData }) => {
50+
export const App: React.FC<AppProps> = ({ testAnimation = false, kedroBaseUrl, initialPipelineId, pipelineFilter, preloadedData, onLoadFile }) => {
5051
const [dataset, setDataset] = React.useState<[string, [number, number]][]>([]);
5152
const [statements, setStatements] = React.useState<any[]>([]);
5253
const [loading, setLoading] = React.useState(true);
@@ -698,6 +699,7 @@ export const App: React.FC<AppProps> = ({ testAnimation = false, kedroBaseUrl, i
698699
availablePipelines={kedroBaseUrl ? [] : undefined} // Will be populated by D3Map's usePipelineOptions
699700
onPipelineChange={handlePipelineChange}
700701
preloadedPipelineData={preloadedData?.pipelineData}
702+
onLoadFile={onLoadFile}
701703
/>
702704
</div>
703705
</div>

src/components/convo-explorer/D3Map.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ type D3MapProps = {
4444
onPipelineChange?: (pipelineId: string) => void;
4545
/** Preloaded pipeline data (e.g. from h5ad file) — bypasses fetch-based loading */
4646
preloadedPipelineData?: Record<string, [string, [number, number]][] | null>;
47+
/** Callback to trigger loading a new file (shown as button in PipelineSelector) */
48+
onLoadFile?: () => void;
4749
};
4850

4951
const PREFERRED_KEDRO_PIPELINE = 'mean_localmap_bestkmeans';
@@ -67,6 +69,7 @@ export const D3Map: React.FC<D3MapProps> = ({
6769
availablePipelines = [],
6870
onPipelineChange,
6971
preloadedPipelineData,
72+
onLoadFile,
7073
}) => {
7174
const svgRef = React.useRef<SVGSVGElement>(null);
7275
const containerRef = React.useRef<d3.Selection<SVGGElement, unknown, null, undefined> | null>(null);
@@ -775,6 +778,7 @@ export const D3Map: React.FC<D3MapProps> = ({
775778
pipelineLoadingStates={Object.fromEntries(
776779
currentPipelineOptions.map(p => [p.id, !pipelineData[p.id]])
777780
)}
781+
onLoadFile={onLoadFile}
778782
top="1rem"
779783
left="1rem"
780784
/>

src/components/convo-explorer/PipelineSelector.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as React from "react";
22
import { Combobox, type ComboboxOption } from "../ui/combobox";
33
import { Button } from "../ui/button";
4-
import { Repeat1, Repeat } from "lucide-react";
4+
import { Repeat1, Repeat, Upload } from "lucide-react";
55

66
interface PipelineOption {
77
id: string;
@@ -37,6 +37,8 @@ interface PipelineSelectorProps {
3737
right?: number | string;
3838
/** Custom positioning - bottom coordinate */
3939
bottom?: number | string;
40+
/** Callback to trigger loading a new file */
41+
onLoadFile?: () => void;
4042
}
4143

4244
export const PipelineSelector: React.FC<PipelineSelectorProps> = ({
@@ -54,6 +56,7 @@ export const PipelineSelector: React.FC<PipelineSelectorProps> = ({
5456
left = "1rem",
5557
right,
5658
bottom,
59+
onLoadFile,
5760
}) => {
5861
// Measure the longest pipeline label to set a stable combobox width
5962
const longestLabel = React.useMemo(() => {
@@ -115,6 +118,17 @@ export const PipelineSelector: React.FC<PipelineSelectorProps> = ({
115118
>
116119
<Repeat className="h-4 w-4" />
117120
</Button>
121+
{onLoadFile && (
122+
<Button
123+
variant="outline"
124+
size="sm"
125+
onClick={onLoadFile}
126+
disabled={isAnimating}
127+
title="Load a new file"
128+
>
129+
<Upload className="h-4 w-4" />
130+
</Button>
131+
)}
118132
</div>
119133
</div>
120134
</div>

0 commit comments

Comments
 (0)