173 lines
5.4 KiB
TypeScript
173 lines
5.4 KiB
TypeScript
|
|
/**
|
||
|
|
* StudioParameterList - Display and add discovered parameters as design variables
|
||
|
|
*/
|
||
|
|
|
||
|
|
import React, { useState, useEffect } from 'react';
|
||
|
|
import { Plus, Check, SlidersHorizontal, Loader2 } from 'lucide-react';
|
||
|
|
import { intakeApi } from '../../api/intake';
|
||
|
|
|
||
|
|
interface Expression {
|
||
|
|
name: string;
|
||
|
|
value: number | null;
|
||
|
|
units: string | null;
|
||
|
|
is_candidate: boolean;
|
||
|
|
confidence: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface StudioParameterListProps {
|
||
|
|
draftId: string;
|
||
|
|
onParameterAdded: () => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
export const StudioParameterList: React.FC<StudioParameterListProps> = ({
|
||
|
|
draftId,
|
||
|
|
onParameterAdded,
|
||
|
|
}) => {
|
||
|
|
const [expressions, setExpressions] = useState<Expression[]>([]);
|
||
|
|
const [addedParams, setAddedParams] = useState<Set<string>>(new Set());
|
||
|
|
const [adding, setAdding] = useState<string | null>(null);
|
||
|
|
const [loading, setLoading] = useState(true);
|
||
|
|
|
||
|
|
// Load expressions from spec introspection
|
||
|
|
useEffect(() => {
|
||
|
|
loadExpressions();
|
||
|
|
}, [draftId]);
|
||
|
|
|
||
|
|
const loadExpressions = async () => {
|
||
|
|
setLoading(true);
|
||
|
|
try {
|
||
|
|
const data = await intakeApi.getStudioDraft(draftId);
|
||
|
|
const introspection = (data.spec as any)?.model?.introspection;
|
||
|
|
|
||
|
|
if (introspection?.expressions) {
|
||
|
|
setExpressions(introspection.expressions);
|
||
|
|
|
||
|
|
// Check which are already added as DVs
|
||
|
|
const existingDVs = new Set<string>(
|
||
|
|
((data.spec as any)?.design_variables || []).map((dv: any) => dv.expression_name as string)
|
||
|
|
);
|
||
|
|
setAddedParams(existingDVs);
|
||
|
|
}
|
||
|
|
} catch (err) {
|
||
|
|
console.error('Failed to load expressions:', err);
|
||
|
|
} finally {
|
||
|
|
setLoading(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const addAsDesignVariable = async (expressionName: string) => {
|
||
|
|
setAdding(expressionName);
|
||
|
|
|
||
|
|
try {
|
||
|
|
await intakeApi.createDesignVariables(draftId, [expressionName]);
|
||
|
|
setAddedParams(prev => new Set([...prev, expressionName]));
|
||
|
|
onParameterAdded();
|
||
|
|
} catch (err) {
|
||
|
|
console.error('Failed to add design variable:', err);
|
||
|
|
} finally {
|
||
|
|
setAdding(null);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// Sort: candidates first, then by confidence
|
||
|
|
const sortedExpressions = [...expressions].sort((a, b) => {
|
||
|
|
if (a.is_candidate !== b.is_candidate) {
|
||
|
|
return b.is_candidate ? 1 : -1;
|
||
|
|
}
|
||
|
|
return (b.confidence || 0) - (a.confidence || 0);
|
||
|
|
});
|
||
|
|
|
||
|
|
// Show only candidates by default, with option to show all
|
||
|
|
const [showAll, setShowAll] = useState(false);
|
||
|
|
const displayExpressions = showAll
|
||
|
|
? sortedExpressions
|
||
|
|
: sortedExpressions.filter(e => e.is_candidate);
|
||
|
|
|
||
|
|
if (loading) {
|
||
|
|
return (
|
||
|
|
<div className="flex items-center justify-center py-4">
|
||
|
|
<Loader2 className="w-5 h-5 text-primary-400 animate-spin" />
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (expressions.length === 0) {
|
||
|
|
return (
|
||
|
|
<p className="text-xs text-dark-500 italic py-2">
|
||
|
|
No expressions found. Try running introspection.
|
||
|
|
</p>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
const candidateCount = expressions.filter(e => e.is_candidate).length;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="space-y-2">
|
||
|
|
{/* Header with toggle */}
|
||
|
|
<div className="flex items-center justify-between text-xs text-dark-400">
|
||
|
|
<span>{candidateCount} candidates</span>
|
||
|
|
<button
|
||
|
|
onClick={() => setShowAll(!showAll)}
|
||
|
|
className="hover:text-primary-400 transition-colors"
|
||
|
|
>
|
||
|
|
{showAll ? 'Show candidates only' : `Show all (${expressions.length})`}
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Parameter List */}
|
||
|
|
<div className="space-y-1 max-h-48 overflow-y-auto">
|
||
|
|
{displayExpressions.map((expr) => {
|
||
|
|
const isAdded = addedParams.has(expr.name);
|
||
|
|
const isAdding = adding === expr.name;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div
|
||
|
|
key={expr.name}
|
||
|
|
className={`flex items-center gap-2 px-2 py-1.5 rounded text-sm
|
||
|
|
${isAdded ? 'bg-green-500/10' : 'bg-dark-700/50 hover:bg-dark-700'}
|
||
|
|
transition-colors`}
|
||
|
|
>
|
||
|
|
<SlidersHorizontal className="w-3.5 h-3.5 text-dark-400 flex-shrink-0" />
|
||
|
|
<div className="flex-1 min-w-0">
|
||
|
|
<span className={`block truncate ${isAdded ? 'text-green-400' : 'text-dark-200'}`}>
|
||
|
|
{expr.name}
|
||
|
|
</span>
|
||
|
|
{expr.value !== null && (
|
||
|
|
<span className="text-xs text-dark-500">
|
||
|
|
= {expr.value}{expr.units ? ` ${expr.units}` : ''}
|
||
|
|
</span>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{isAdded ? (
|
||
|
|
<Check className="w-4 h-4 text-green-400 flex-shrink-0" />
|
||
|
|
) : (
|
||
|
|
<button
|
||
|
|
onClick={() => addAsDesignVariable(expr.name)}
|
||
|
|
disabled={isAdding}
|
||
|
|
className="p-1 hover:bg-primary-400/20 rounded text-primary-400 transition-colors disabled:opacity-50"
|
||
|
|
title="Add as design variable"
|
||
|
|
>
|
||
|
|
{isAdding ? (
|
||
|
|
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
||
|
|
) : (
|
||
|
|
<Plus className="w-3.5 h-3.5" />
|
||
|
|
)}
|
||
|
|
</button>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
})}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{displayExpressions.length === 0 && (
|
||
|
|
<p className="text-xs text-dark-500 italic py-2">
|
||
|
|
No candidate parameters found. Click "Show all" to see all expressions.
|
||
|
|
</p>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
export default StudioParameterList;
|