"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>; selectedJobId: string; setSelectedJobId: React.Dispatch>; advisorObjective: string; setAdvisorObjective: React.Dispatch>; advisorResult: HSQNNAdvisorOutput | null; setAdvisorResult: React.Dispatch>; selectedJobDetails: TrainingJob | null; setSelectedJobDetails: React.Dispatch>; isLoading: boolean; setIsLoading: React.Dispatch>; error: string | null; setError: React.Dispatch>; isLoadingJobs: boolean; setIsLoadingJobs: React.Dispatch>; } const AdvisorContext = createContext(undefined); export const AdvisorProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [completedJobs, setCompletedJobs] = useState([]); const [selectedJobId, setSelectedJobId] = useState(""); const [advisorObjective, setAdvisorObjective] = useState( "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(null); const [selectedJobDetails, setSelectedJobDetails] = useState(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [isLoadingJobs, setIsLoadingJobs] = useState(false); return ( {children} ); }; 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(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 (
{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; }), }) )}
); } export const MiniHSQNNAdvisor: React.FC = ({ 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 (
{fullMode ? "HS-QNN Advisor" : "Mini HS-QNN Advisor"} {/* Manual Refresh Button */}
{/* Collapsible Objective */}
setShowObjective(v => !v)}>
{showObjective ? : }
{showObjective && (