golem-flask-backend / src /components /mini-hs-qnn-advisor.tsx
mememechez's picture
Deploy final cleaned source code
ca28016
raw
history blame
23.4 kB
"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>
);
};