Spaces:
Runtime error
Runtime error
| "use client"; | |
| import React, { useState, useEffect, useCallback, useContext, createContext, useRef } from "react"; | |
| import { Card, CardHeader, CardTitle, CardContent, CardFooter } from "@/components/ui/card"; | |
| import { Button } from "@/components/ui/button"; | |
| import { Label } from "@/components/ui/label"; | |
| import { Textarea } from "@/components/ui/textarea"; | |
| import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; | |
| import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert"; | |
| import { ScrollArea } from "@/components/ui/scroll-area"; | |
| import { Wand2, Save, Loader2, ChevronDown, ChevronUp, RefreshCw, Maximize2, X } from "lucide-react"; | |
| import { adviseHSQNNParameters, type HSQNNAdvisorInput, type HSQNNAdvisorOutput } from "@/ai/flows/hs-qnn-parameter-advisor"; | |
| import type { TrainingParameters, TrainingJob, TrainingJobSummary } from "@/types/training"; | |
| import { toast } from "@/hooks/use-toast"; | |
| import { defaultZPEParams } from "@/lib/constants"; | |
| import { useRouter } from "next/navigation"; | |
| import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"; | |
| interface MiniHSQNNAdvisorProps { | |
| onApplyParameters: (params: TrainingParameters) => void; | |
| onSaveConfig: (params: TrainingParameters) => void; | |
| fullMode?: boolean; | |
| className?: string; | |
| } | |
| // Advisor Context | |
| interface AdvisorContextType { | |
| completedJobs: TrainingJobSummary[]; | |
| setCompletedJobs: React.Dispatch<React.SetStateAction<TrainingJobSummary[]>>; | |
| selectedJobId: string; | |
| setSelectedJobId: React.Dispatch<React.SetStateAction<string>>; | |
| advisorObjective: string; | |
| setAdvisorObjective: React.Dispatch<React.SetStateAction<string>>; | |
| advisorResult: HSQNNAdvisorOutput | null; | |
| setAdvisorResult: React.Dispatch<React.SetStateAction<HSQNNAdvisorOutput | null>>; | |
| selectedJobDetails: TrainingJob | null; | |
| setSelectedJobDetails: React.Dispatch<React.SetStateAction<TrainingJob | null>>; | |
| isLoading: boolean; | |
| setIsLoading: React.Dispatch<React.SetStateAction<boolean>>; | |
| error: string | null; | |
| setError: React.Dispatch<React.SetStateAction<string | null>>; | |
| isLoadingJobs: boolean; | |
| setIsLoadingJobs: React.Dispatch<React.SetStateAction<boolean>>; | |
| } | |
| const AdvisorContext = createContext<AdvisorContextType | undefined>(undefined); | |
| export const AdvisorProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { | |
| const [completedJobs, setCompletedJobs] = useState<TrainingJobSummary[]>([]); | |
| const [selectedJobId, setSelectedJobId] = useState<string>(""); | |
| const [advisorObjective, setAdvisorObjective] = useState<string>( | |
| "Analyze ZPE statistics in general. Determine best parameters to maximize validation accuracy to 100% while maintaining ZPE stability and exploring a slight increase in learning rate if previous accuracy was high." | |
| ); | |
| const [advisorResult, setAdvisorResult] = useState<HSQNNAdvisorOutput | null>(null); | |
| const [selectedJobDetails, setSelectedJobDetails] = useState<TrainingJob | null>(null); | |
| const [isLoading, setIsLoading] = useState<boolean>(false); | |
| const [error, setError] = useState<string | null>(null); | |
| const [isLoadingJobs, setIsLoadingJobs] = useState(false); | |
| return ( | |
| <AdvisorContext.Provider value={{ | |
| completedJobs, setCompletedJobs, | |
| selectedJobId, setSelectedJobId, | |
| advisorObjective, setAdvisorObjective, | |
| advisorResult, setAdvisorResult, | |
| selectedJobDetails, setSelectedJobDetails, | |
| isLoading, setIsLoading, | |
| error, setError, | |
| isLoadingJobs, setIsLoadingJobs | |
| }}> | |
| {children} | |
| </AdvisorContext.Provider> | |
| ); | |
| }; | |
| export function useAdvisorContext() { | |
| const ctx = useContext(AdvisorContext); | |
| if (!ctx) throw new Error("useAdvisorContext must be used within AdvisorProvider"); | |
| return ctx; | |
| } | |
| // Custom DraggableModal wrapper | |
| function DraggableModal({ children }: { children: React.ReactNode }) { | |
| const nodeRef = useRef<HTMLDivElement>(null); | |
| const [pos, setPos] = useState({ x: 0, y: 0 }); | |
| const [dragging, setDragging] = useState(false); | |
| const [offset, setOffset] = useState({ x: 0, y: 0 }); | |
| const onMouseDown = (e: React.MouseEvent) => { | |
| e.stopPropagation(); | |
| setDragging(true); | |
| setOffset({ | |
| x: e.clientX - pos.x, | |
| y: e.clientY - pos.y, | |
| }); | |
| }; | |
| const onMouseMove = (e: MouseEvent) => { | |
| if (dragging) { | |
| setPos({ | |
| x: e.clientX - offset.x, | |
| y: e.clientY - offset.y, | |
| }); | |
| } | |
| }; | |
| const onMouseUp = () => setDragging(false); | |
| React.useEffect(() => { | |
| if (dragging) { | |
| window.addEventListener("mousemove", onMouseMove); | |
| window.addEventListener("mouseup", onMouseUp); | |
| } else { | |
| window.removeEventListener("mousemove", onMouseMove); | |
| window.removeEventListener("mouseup", onMouseUp); | |
| } | |
| return () => { | |
| window.removeEventListener("mousemove", onMouseMove); | |
| window.removeEventListener("mouseup", onMouseUp); | |
| }; | |
| }); | |
| return ( | |
| <div | |
| ref={nodeRef} | |
| style={{ | |
| position: "fixed", | |
| left: `calc(50% + ${pos.x}px)`, | |
| top: `calc(50% + ${pos.y}px)`, | |
| zIndex: 1300, | |
| transform: "translate(-50%, -50%)", | |
| }} | |
| > | |
| {React.Children.map(children, (child: any) => | |
| React.cloneElement(child, { | |
| // Pass down the drag handle to DialogTitle | |
| children: React.Children.map(child.props.children, (modalChild: any) => { | |
| if ( | |
| modalChild && | |
| modalChild.type && | |
| modalChild.type.displayName === "DialogTitle" | |
| ) { | |
| return React.cloneElement(modalChild, { | |
| className: (modalChild.props.className || "") + " cursor-move", | |
| onMouseDown, | |
| }); | |
| } | |
| return modalChild; | |
| }), | |
| }) | |
| )} | |
| </div> | |
| ); | |
| } | |
| export const MiniHSQNNAdvisor: React.FC<MiniHSQNNAdvisorProps> = ({ onApplyParameters, onSaveConfig, fullMode = false, className }) => { | |
| const { completedJobs, setCompletedJobs, selectedJobId, setSelectedJobId, advisorObjective, setAdvisorObjective, advisorResult, setAdvisorResult, selectedJobDetails, setSelectedJobDetails, isLoading, setIsLoading, error, setError, isLoadingJobs, setIsLoadingJobs } = useAdvisorContext(); | |
| const router = useRouter(); | |
| // Collapsible state | |
| const [showJobDetails, setShowJobDetails] = useState(false); | |
| const [showObjective, setShowObjective] = useState(false); | |
| const [showReasoning, setShowReasoning] = useState(false); | |
| const [showSuggestedParams, setShowSuggestedParams] = useState(false); | |
| // Add state for expanded modals | |
| const [expandedObjective, setExpandedObjective] = useState(false); | |
| const [expandedJobDetails, setExpandedJobDetails] = useState(false); | |
| const [expandedReasoning, setExpandedReasoning] = useState(false); | |
| const [expandedSuggestedParams, setExpandedSuggestedParams] = useState(false); | |
| const API_BASE_URL = (process.env.NEXT_PUBLIC_TRAINING_API_BASE || process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:9006") + "/api"; | |
| // --- Suggestion Memory --- | |
| // Key for localStorage | |
| const suggestionKey = selectedJobId ? `hsqnn_advisor_suggestion_${selectedJobId}` : null; | |
| // Load suggestion from localStorage on mount or when job changes | |
| useEffect(() => { | |
| if (!suggestionKey) return; | |
| const stored = localStorage.getItem(suggestionKey); | |
| if (stored) { | |
| try { | |
| setAdvisorResult(JSON.parse(stored)); | |
| } catch {} | |
| } else { | |
| setAdvisorResult(null); | |
| } | |
| }, [suggestionKey]); | |
| // Save suggestion to localStorage when advisorResult changes | |
| useEffect(() => { | |
| if (!suggestionKey) return; | |
| if (advisorResult) { | |
| localStorage.setItem(suggestionKey, JSON.stringify(advisorResult)); | |
| } | |
| }, [advisorResult, suggestionKey]); | |
| // Clear suggestion from localStorage | |
| const clearSuggestion = () => { | |
| if (suggestionKey) localStorage.removeItem(suggestionKey); | |
| setAdvisorResult(null); | |
| }; | |
| // --- End Suggestion Memory --- | |
| const fetchCompletedJobs = useCallback(async () => { | |
| setIsLoadingJobs(true); | |
| try { | |
| const response = await fetch(`${API_BASE_URL}/jobs?limit=50`); | |
| if (!response.ok) throw new Error("Failed to fetch completed jobs list"); | |
| const data = await response.json(); | |
| const completedJobs = (data.jobs || []) | |
| .filter((job: TrainingJobSummary) => job.status === "completed") | |
| .sort((a: TrainingJobSummary, b: TrainingJobSummary) => new Date(b.start_time || 0).getTime() - new Date(a.start_time || 0).getTime()); | |
| setCompletedJobs(completedJobs); | |
| if (completedJobs.length > 0 && !selectedJobId) { | |
| setSelectedJobId(completedJobs[0].job_id); | |
| } | |
| } catch (error: any) { | |
| setError("Error fetching completed jobs: " + error.message); | |
| } finally { | |
| setIsLoadingJobs(false); | |
| } | |
| }, [selectedJobId]); | |
| useEffect(() => { | |
| fetchCompletedJobs(); | |
| }, [fetchCompletedJobs]); | |
| useEffect(() => { | |
| if (selectedJobId) { | |
| const fetchDetails = async () => { | |
| setIsLoading(true); | |
| setAdvisorResult(null); | |
| setError(null); | |
| try { | |
| const response = await fetch(`${API_BASE_URL}/status/${selectedJobId}`); | |
| if (!response.ok) throw new Error(`Failed to fetch details for job ${selectedJobId}`); | |
| const data = await response.json() as TrainingJob; | |
| if (data.status !== 'completed') { | |
| setSelectedJobDetails(null); | |
| throw new Error(`Job ${selectedJobId} is not completed. Current status: ${data.status}`); | |
| } | |
| setSelectedJobDetails(data); | |
| } catch (e: any) { | |
| setSelectedJobDetails(null); | |
| setError("Failed to fetch selected job details: " + e.message); | |
| } finally { | |
| setIsLoading(false); | |
| } | |
| }; | |
| fetchDetails(); | |
| } else { | |
| setSelectedJobDetails(null); | |
| } | |
| }, [selectedJobId]); | |
| function parseLogMessagesToZpeHistory(logMessages: string[] | undefined) { | |
| if (!logMessages) return []; | |
| const zpeHistory: any[] = []; | |
| const zpeRegex = /ZPE: \[(.*?)\]/; | |
| const epochLossAccRegex = /E(\d+) END - TrainL: [\d\.]+, ValAcc: ([\d\.]+)%, ValL: ([\d\.]+)/; | |
| let currentLoss = 0; | |
| let currentAccuracy = 0; | |
| for (const message of logMessages) { | |
| const match = message.match(zpeRegex); | |
| if (match) { | |
| try { | |
| const zpeEffectsString = match[1]; | |
| const zpeEffects = zpeEffectsString.split(',').map((s: string) => parseFloat(s.trim())).filter((n) => !isNaN(n)); | |
| if (zpeHistory.length + 1 > 0 && zpeEffects.length > 0) { | |
| zpeHistory.push({ | |
| epoch: zpeHistory.length + 1, | |
| zpeEffects: zpeEffects, | |
| zpe_effects: zpeEffects, | |
| loss: currentLoss, | |
| accuracy: currentAccuracy, | |
| }); | |
| } | |
| } catch (e) { | |
| console.error("Failed to parse ZPE effects string:", match[1], e); | |
| } | |
| } else { | |
| const epochMatch = message.match(epochLossAccRegex); | |
| if (epochMatch) { | |
| currentLoss = parseFloat(epochMatch[3]); | |
| currentAccuracy = parseFloat(epochMatch[2]); | |
| } | |
| } | |
| } | |
| return zpeHistory.sort((a, b) => a.epoch - b.epoch); | |
| } | |
| const handleGetAdvice = async () => { | |
| if (!selectedJobDetails) { | |
| setError("No previous job selected for advice."); | |
| return; | |
| } | |
| if (selectedJobDetails.status !== 'completed') { | |
| setError("Please select a 'completed' job for advice."); | |
| return; | |
| } | |
| setIsLoading(true); | |
| setError(null); | |
| setAdvisorResult(null); | |
| if (suggestionKey) localStorage.removeItem(suggestionKey); | |
| try { | |
| const zpeHistory = parseLogMessagesToZpeHistory(selectedJobDetails.log_messages || []); | |
| const zpeHistoryString = zpeHistory | |
| .map(entry => `Epoch ${entry.epoch}: ZPE=[${entry.zpe_effects.map((z: number) => z.toFixed(3)).join(', ')}], Loss=${entry.loss.toFixed(4)}, Acc=${entry.accuracy.toFixed(4)}`) | |
| .join('\n') + `\nFinal Accuracy: ${selectedJobDetails.accuracy?.toFixed(4) ?? 'N/A'}%`; | |
| const inputForAI: HSQNNAdvisorInput = { | |
| previousJobId: selectedJobDetails.job_id, | |
| hnnObjective: advisorObjective, | |
| previousJobZpeHistory: zpeHistory, | |
| previousJobZpeHistoryString: zpeHistoryString, | |
| previousTrainingParameters: selectedJobDetails.parameters, | |
| }; | |
| const result = await adviseHSQNNParameters(inputForAI); | |
| setAdvisorResult(result); | |
| toast({ title: "Advice Generated", description: "AI has provided suggestions for the next HNN step." }); | |
| } catch (error: any) { | |
| setError("Failed to get advice: " + (error?.message || error?.toString() || "Unknown error. Is the backend running?")); | |
| } finally { | |
| setIsLoading(false); | |
| } | |
| }; | |
| const handleLoadInTrainer = () => { | |
| if (!advisorResult?.suggestedNextTrainingParameters) { | |
| setError("No advice to apply"); | |
| return; | |
| } | |
| const suggested = advisorResult.suggestedNextTrainingParameters; | |
| const previousParams = selectedJobDetails?.parameters; | |
| let mergedParams: TrainingParameters = { ...defaultZPEParams }; | |
| if (previousParams) { | |
| mergedParams = { ...mergedParams, ...previousParams }; | |
| } | |
| mergedParams = { | |
| ...mergedParams, | |
| ...suggested, | |
| modelName: suggested.modelName || `${previousParams?.modelName || 'ZPE-QuantumWeaver'}_adv_${Date.now().toString().slice(-3)}`, | |
| baseConfigId: selectedJobDetails?.job_id, | |
| }; | |
| // Pass params as query string | |
| const params = encodeURIComponent(JSON.stringify(mergedParams)); | |
| router.push(`/train?advisorParams=${params}`); | |
| }; | |
| const handleSaveConfig = () => { | |
| if (!advisorResult?.suggestedNextTrainingParameters || !selectedJobDetails) { | |
| setError("No suggested parameters to save"); | |
| return; | |
| } | |
| const suggested = advisorResult.suggestedNextTrainingParameters; | |
| const configToSave = { | |
| ...defaultZPEParams, | |
| ...selectedJobDetails.parameters, | |
| ...suggested, | |
| modelName: suggested.modelName || `${selectedJobDetails.parameters.modelName}_advised_${Date.now().toString().slice(-4)}`, | |
| baseConfigId: selectedJobDetails.job_id, | |
| }; | |
| onSaveConfig(configToSave); | |
| }; | |
| return ( | |
| <div className="matrix-panel"> | |
| <CardHeader> | |
| <CardTitle className="flex items-center gap-2 text-lg"> | |
| <Wand2 className="h-5 w-5 text-primary" /> | |
| {fullMode ? "HS-QNN Advisor" : "Mini HS-QNN Advisor"} | |
| {/* Manual Refresh Button */} | |
| <button className="instrument-btn ml-2" onClick={handleGetAdvice} disabled={isLoading} title="Refresh AI Suggestion"> | |
| <RefreshCw className={isLoading ? "animate-spin" : ""} /> | |
| </button> | |
| </CardTitle> | |
| </CardHeader> | |
| <CardContent className="space-y-4"> | |
| <div className="space-y-2"> | |
| <Label htmlFor="selectedJobId">Select Previous Job</Label> | |
| <Select | |
| value={selectedJobId} | |
| onValueChange={setSelectedJobId} | |
| disabled={isLoadingJobs || isLoading} | |
| > | |
| <SelectTrigger className="w-full"> | |
| <SelectValue placeholder="Select a completed job..." /> | |
| </SelectTrigger> | |
| <SelectContent> | |
| {completedJobs.map((job: TrainingJobSummary) => ( | |
| <SelectItem key={job.job_id} value={job.job_id}> | |
| {job.job_id.replace('zpe_job_', '')} ({job.model_name}, Acc: {job.accuracy.toFixed(2)}%) | |
| </SelectItem> | |
| ))} | |
| </SelectContent> | |
| </Select> | |
| </div> | |
| {/* Collapsible Objective */} | |
| <div className="space-y-2"> | |
| <div className="flex items-center justify-between cursor-pointer" onClick={() => setShowObjective(v => !v)}> | |
| <Label htmlFor="advisorObjective">Advisor Objective</Label> | |
| <div className="flex items-center gap-1"> | |
| <button className="instrument-btn p-0 h-6 w-6 text-accent/70 hover:text-accent transition-colors" onClick={e => { e.stopPropagation(); setExpandedObjective(true); }} title="Expand" tabIndex={-1}> | |
| <Maximize2 className="h-3.5 w-3.5" /> | |
| </button> | |
| {showObjective ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />} | |
| </div> | |
| </div> | |
| {showObjective && ( | |
| <Textarea | |
| id="advisorObjective" | |
| value={advisorObjective} | |
| onChange={e => setAdvisorObjective(e.target.value)} | |
| placeholder="e.g., Maximize validation accuracy while maintaining ZPE stability..." | |
| className="min-h-[80px] w-full" | |
| /> | |
| )} | |
| {/* Expanded Modal for Objective */} | |
| <Dialog open={expandedObjective} onOpenChange={setExpandedObjective}> | |
| <DraggableModal> | |
| <DialogContent className="max-w-2xl z-[1200]"> | |
| <DialogTitle className="dialog-title">Advisor Objective</DialogTitle> | |
| <Textarea | |
| value={advisorObjective} | |
| onChange={e => setAdvisorObjective(e.target.value)} | |
| className="min-h-[200px] w-full" | |
| autoFocus | |
| /> | |
| </DialogContent> | |
| </DraggableModal> | |
| </Dialog> | |
| </div> | |
| {/* Collapsible Job Details */} | |
| {selectedJobDetails && ( | |
| <div className="space-y-2"> | |
| <div className="flex items-center justify-between cursor-pointer" onClick={() => setShowJobDetails(v => !v)}> | |
| <Label>Selected Job Details</Label> | |
| <div className="flex items-center gap-1"> | |
| <button className="instrument-btn p-0 h-6 w-6 text-accent/70 hover:text-accent transition-colors" onClick={e => { e.stopPropagation(); setExpandedJobDetails(true); }} title="Expand" tabIndex={-1}> | |
| <Maximize2 className="h-3.5 w-3.5" /> | |
| </button> | |
| {showJobDetails ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />} | |
| </div> | |
| </div> | |
| {showJobDetails && ( | |
| <pre className="p-2 bg-muted rounded-md text-sm overflow-auto max-h-32"> | |
| {JSON.stringify(selectedJobDetails.parameters, null, 2)} | |
| </pre> | |
| )} | |
| {/* Expanded Modal for Job Details */} | |
| <Dialog open={expandedJobDetails} onOpenChange={setExpandedJobDetails}> | |
| <DraggableModal> | |
| <DialogContent className="max-w-2xl z-[1200]"> | |
| <DialogTitle className="dialog-title">Selected Job Details</DialogTitle> | |
| <pre className="p-4 bg-muted rounded-md text-sm overflow-auto max-h-[60vh]"> | |
| {JSON.stringify(selectedJobDetails.parameters, null, 2)} | |
| </pre> | |
| </DialogContent> | |
| </DraggableModal> | |
| </Dialog> | |
| </div> | |
| )} | |
| {error && ( | |
| <Alert variant="destructive"> | |
| <AlertTitle>Error</AlertTitle> | |
| <AlertDescription>{error}</AlertDescription> | |
| </Alert> | |
| )} | |
| {advisorResult && ( | |
| <div className="space-y-2"> | |
| {/* Collapsible Reasoning */} | |
| <div className="flex items-center justify-between cursor-pointer" onClick={() => setShowReasoning(v => !v)}> | |
| <Label>Advisor Reasoning</Label> | |
| <div className="flex items-center gap-1"> | |
| <button className="instrument-btn p-0 h-6 w-6 text-accent/70 hover:text-accent transition-colors" onClick={e => { e.stopPropagation(); setExpandedReasoning(true); }} title="Expand" tabIndex={-1}> | |
| <Maximize2 className="h-3.5 w-3.5" /> | |
| </button> | |
| {showReasoning ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />} | |
| </div> | |
| </div> | |
| {showReasoning && ( | |
| <pre className="p-2 bg-muted rounded-md text-sm whitespace-pre-wrap overflow-auto max-h-32"> | |
| {advisorResult.reasoning || "No reasoning provided."} | |
| </pre> | |
| )} | |
| {/* Expanded Modal for Reasoning */} | |
| <Dialog open={expandedReasoning} onOpenChange={setExpandedReasoning}> | |
| <DraggableModal> | |
| <DialogContent className="max-w-2xl z-[1200]"> | |
| <DialogTitle className="dialog-title">Advisor Reasoning</DialogTitle> | |
| <pre className="p-4 bg-muted rounded-md text-sm whitespace-pre-wrap overflow-auto max-h-[60vh]"> | |
| {advisorResult.reasoning || "No reasoning provided."} | |
| </pre> | |
| </DialogContent> | |
| </DraggableModal> | |
| </Dialog> | |
| {/* Collapsible Suggested Parameters */} | |
| <div className="flex items-center justify-between cursor-pointer" onClick={() => setShowSuggestedParams(v => !v)}> | |
| <Label>Suggested Parameters</Label> | |
| <div className="flex items-center gap-1"> | |
| <button className="instrument-btn p-0 h-6 w-6 text-accent/70 hover:text-accent transition-colors" onClick={e => { e.stopPropagation(); setExpandedSuggestedParams(true); }} title="Expand" tabIndex={-1}> | |
| <Maximize2 className="h-3.5 w-3.5" /> | |
| </button> | |
| {showSuggestedParams ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />} | |
| </div> | |
| </div> | |
| {showSuggestedParams && ( | |
| <pre className="p-2 bg-muted rounded-md text-sm overflow-auto max-h-32"> | |
| {JSON.stringify(advisorResult.suggestedNextTrainingParameters, null, 2)} | |
| </pre> | |
| )} | |
| {/* Expanded Modal for Suggested Parameters */} | |
| <Dialog open={expandedSuggestedParams} onOpenChange={setExpandedSuggestedParams}> | |
| <DraggableModal> | |
| <DialogContent className="max-w-2xl z-[1200]"> | |
| <DialogTitle className="dialog-title">Suggested Parameters</DialogTitle> | |
| <pre className="p-4 bg-muted rounded-md text-sm overflow-auto max-h-[60vh]"> | |
| {JSON.stringify(advisorResult.suggestedNextTrainingParameters, null, 2)} | |
| </pre> | |
| </DialogContent> | |
| </DraggableModal> | |
| </Dialog> | |
| <div className="flex gap-2"> | |
| <button className="instrument-btn" onClick={handleLoadInTrainer} disabled={isLoading}> | |
| <Wand2 className="mr-2 h-4 w-4" /> Load in Trainer | |
| </button> | |
| <button className="instrument-btn" onClick={handleSaveConfig} disabled={isLoading}> | |
| <Save className="mr-2 h-4 w-4" /> Save Config | |
| </button> | |
| </div> | |
| </div> | |
| )} | |
| </CardContent> | |
| <CardFooter> | |
| <button className="instrument-btn" onClick={handleGetAdvice} disabled={isLoading || !selectedJobId}> | |
| {isLoading ? ( | |
| <> | |
| <Loader2 className="mr-2 h-4 w-4 animate-spin" /> Generating Advice... | |
| </> | |
| ) : ( | |
| <> | |
| <Wand2 className="mr-2 h-4 w-4" /> Get HNN Advice | |
| </> | |
| )} | |
| </button> | |
| </CardFooter> | |
| </div> | |
| ); | |
| }; |