diff --git a/.windsurf/workflows/refresh-chat-history.md b/.windsurf/workflows/refresh-chat-history.md index 28156ebf..26ad93dc 100644 --- a/.windsurf/workflows/refresh-chat-history.md +++ b/.windsurf/workflows/refresh-chat-history.md @@ -7,7 +7,7 @@ description: Refresh the Windsurf chat history archive (re-scan all repos, updat Refreshes the centralized Windsurf chat history archive at `__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/`. Auto-discovers new repos, updates symlinks, and re-copies docs + workflows. -## Covered Repos (All 7 workspaces) +## Covered Repos (All 8 workspaces) | Repo | Product | Workflows | Docs | |------|---------|-----------|------| @@ -18,6 +18,7 @@ Auto-discovers new repos, updates symlinks, and re-copies docs + workflows. | `learning_ai_fastgap` | NomGap | ✅ | — | | `learning_ai_jarvis_jr` | JarvisJr | ✅ | — | | `learning_ai_common_plat` | Common Platform | ✅ | — | +| `learning_agent_monitoring_fx` | Agent Monitoring | ✅ | — | ## Steps diff --git a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/.last-refresh.log b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/.last-refresh.log index de57d24b..4c8c7e3d 100644 --- a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/.last-refresh.log +++ b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/.last-refresh.log @@ -1,9 +1,9 @@ -Last refresh: 2026-03-03T19:50:04Z (2026-03-03 11:50:04 PST) +Last refresh: 2026-03-03T19:50:43Z (2026-03-03 11:50:43 PST) Cascade conversations: 50 (297M) Memories: 65 Implicit context: 20 -Code tracker dirs: 188 +Code tracker dirs: 190 File edit history: 2343 entries Workspace storage: 28 workspaces Repo docs: 7 files across 2 repos -Repo workflows: 35 files across 6 repos +Repo workflows: 37 files across 8 repos diff --git a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_ai_fastgap/README.md b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_ai_fastgap/README.md new file mode 100644 index 00000000..a78354ef --- /dev/null +++ b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_ai_fastgap/README.md @@ -0,0 +1,5 @@ +# NomGap Workflows + +Workflows for NomGap fasting app. + +See `/refresh-chat-history` for archive refresh. diff --git a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_ai_jarvis_jr/README.md b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_ai_jarvis_jr/README.md new file mode 100644 index 00000000..ef8acd85 --- /dev/null +++ b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_ai_jarvis_jr/README.md @@ -0,0 +1,5 @@ +# JarvisJr Workflows + +Workflows for JarvisJr voice coaching app. + +See `/refresh-chat-history` for archive refresh. diff --git a/dashboards/admin-web/src/app/(dashboard)/experiments/[id]/page.tsx b/dashboards/admin-web/src/app/(dashboard)/experiments/[id]/page.tsx new file mode 100644 index 00000000..c65f0cc0 --- /dev/null +++ b/dashboards/admin-web/src/app/(dashboard)/experiments/[id]/page.tsx @@ -0,0 +1,479 @@ +/** + * Experiment Detail Page — Live Dashboard with Bayesian Statistics + * Shows real-time metrics, variant comparisons, and statistical results. + */ + +'use client'; + +import { useState, useEffect } from 'react'; +import { useParams } from 'next/navigation'; +import Link from 'next/link'; +import { + ArrowLeft, + Play, + Pause, + Square, + Trophy, + AlertTriangle, + Users, + BarChart3, + TrendingUp, + Clock, + Sparkles, + Download, +} from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Progress } from '@/components/ui/progress'; +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; +import type { ExperimentDoc, VariantDoc, ExperimentResult } from '@/lib/experiments-types'; + +interface ExperimentData { + experiment: ExperimentDoc; + variants: VariantDoc[]; + results?: ExperimentResult; +} + +export default function ExperimentDetailPage() { + const params = useParams(); + const experimentId = params.id as string; + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + fetchExperimentData(); + const interval = setInterval(fetchExperimentData, 30000); // Auto-refresh every 30s + return () => clearInterval(interval); + }, [experimentId]); + + async function fetchExperimentData() { + try { + const [expResponse, resultsResponse] = await Promise.all([ + fetch(`/api/experiments/${experimentId}`), + fetch(`/api/experiments/${experimentId}/results`), + ]); + + if (!expResponse.ok) throw new Error('Failed to fetch experiment'); + + const experiment = await expResponse.json(); + const results = resultsResponse.ok ? await resultsResponse.json() : null; + + setData({ experiment, variants: experiment.variants, results }); + } catch (err) { + setError(err instanceof Error ? err.message : 'Unknown error'); + } finally { + setLoading(false); + } + } + + async function updateStatus(status: string) { + try { + const response = await fetch(`/api/experiments/${experimentId}/${status}`, { + method: 'POST', + }); + if (!response.ok) throw new Error('Failed to update status'); + fetchExperimentData(); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to update'); + } + } + + if (loading) { + return ( +
+
+
+
+
+ ); + } + + if (!data) { + return ( +
+ + + Error + {error || 'Experiment not found'} + +
+ ); + } + + const { experiment, variants, results } = data; + const daysRunning = experiment.startedAt + ? Math.floor((Date.now() - new Date(experiment.startedAt).getTime()) / (1000 * 60 * 60 * 24)) + : 0; + + return ( +
+ {/* Header */} +
+
+ +
+

{experiment.name}

+

{experiment.hypothesis}

+
+
+
+ {experiment.status === 'draft' && ( + + )} + {experiment.status === 'running' && ( + <> + + + + )} + {experiment.status === 'paused' && ( + + )} +
+
+ + {/* Status Banner */} + {results?.earlyStopped && ( + + + Early Stopping Triggered + {results.stopReason} + + )} + + {results?.winnerVariantId && ( + + + Winner Found! + + Variant has {((results.winnerProbability || 0) * 100).toFixed(1)}% probability of being best. + Recommended action: {results.statisticalSummary.recommendedAction}. + + + )} + + {/* Stats Overview */} +
+ + } + /> + } + /> + } + /> +
+ + {/* Main Content */} + + + Variant Performance + Statistical Results + AI Insights + Settings + + + + {variants.map(variant => ( + vr.variantId === variant.id)} + /> + ))} + + + + + + Statistical Summary + + + {results ? ( + <> +
+
+
+ Probability Any Beats Control +
+
+ {(results.statisticalSummary.probabilityAnyBeatsControl * 100).toFixed(1)}% +
+
+
+
+ Expected Loss If Shipped +
+
+ {(results.statisticalSummary.expectedLossIfShipped * 100).toFixed(2)}% +
+
+
+
+ Recommended Action +
+
+ {results.statisticalSummary.recommendedAction} +
+
+
+ +
+

Variant Comparison

+
+ {results.variantResults.map(vr => ( +
+
{vr.variantName}
+
+
+ + P(beats control): {(vr.probabilityBeatsControl * 100).toFixed(1)}% + +
+ +
+
+
0 ? 'text-green-600' : 'text-red-600'}`}> + {vr.expectedLiftPercent > 0 ? '+' : ''}{vr.expectedLiftPercent.toFixed(1)}% +
+
+
+ ))} +
+
+ + ) : ( +

No statistical results available yet.

+ )} +
+
+
+ + + + + + + AI-Generated Insights + + + +
+

Summary

+

+ {results?.winnerVariantId + ? `The ${results.variantResults.find(v => v.variantId === results.winnerVariantId)?.variantName || 'winning variant'} significantly outperformed control with ${((results.winnerProbability || 0) * 100).toFixed(1)}% probability of superiority.` + : 'Results are still inconclusive. Consider running the experiment longer or testing more distinct variations.'} +

+
+ +
+

Recommended Follow-up Experiments

+
    +
  • + + Test the winning variant against additional success metrics +
  • +
  • + + Run a validation experiment with a new user cohort +
  • +
+
+
+
+
+ + + + + Experiment Configuration + + +
+
+ +

{experiment.allocationStrategy}

+
+
+ +

{experiment.targetPercent}%

+
+
+ +

{experiment.primaryMetric?.name}

+
+
+ +

{experiment.guardrails?.minSampleSizePerVariant} per variant

+
+
+ +

{experiment.guardrails?.maxDurationDays} days

+
+
+ +

{experiment.guardrails?.autoStopEnabled ? 'Enabled' : 'Disabled'}

+
+
+
+
+
+
+
+ ); +} + +function StatCard({ + title, + value, + badge, + icon, +}: { + title: string; + value: string; + badge?: React.ReactNode; + icon?: React.ReactNode; +}) { + return ( + + +
+ {title} + {icon} +
+
+ {value} + {badge} +
+
+
+ ); +} + +function VariantCard({ + variant, + isControl, + experiment, + result, +}: { + variant: VariantDoc; + isControl: boolean; + experiment: ExperimentDoc; + result?: { probabilityBeatsControl: number; expectedLiftPercent: number; credibleInterval: { lower: number; mean: number; upper: number } }; +}) { + const conversionRate = variant.stats?.conversionRate || 0; + const participants = variant.stats?.participants || 0; + const events = variant.stats?.events || 0; + + return ( + + +
+
+
+

{variant.name}

+ {isControl && ( + + Control + + )} + {variant.bayesianResults?.probabilityBeatsControl && variant.bayesianResults.probabilityBeatsControl > 0.95 && ( + + + Winner + + )} +
+

{variant.description}

+
+
+
{(conversionRate * 100).toFixed(1)}%
+
conversion rate
+
+
+ +
+
+
{participants.toLocaleString()}
+
participants
+
+
+
{events.toLocaleString()}
+
events
+
+
+
+ {result ? `${(result.probabilityBeatsControl * 100).toFixed(0)}%` : '-'} +
+
P(beats control)
+
+
+ + {result && result.credibleInterval && ( +
+
95% Credible Interval
+
+ {(result.credibleInterval.lower * 100).toFixed(1)}% +
+
+
+ {(result.credibleInterval.upper * 100).toFixed(1)}% +
+
+ )} + + + ); +} + +function getStatusBadge(status: string) { + const colors: Record = { + draft: 'bg-gray-500', + running: 'bg-green-500', + paused: 'bg-yellow-500', + stopped: 'bg-red-500', + completed: 'bg-blue-500', + }; + return {status}; +} diff --git a/dashboards/admin-web/src/app/(dashboard)/experiments/new/page.tsx b/dashboards/admin-web/src/app/(dashboard)/experiments/new/page.tsx new file mode 100644 index 00000000..c9a9b45a --- /dev/null +++ b/dashboards/admin-web/src/app/(dashboard)/experiments/new/page.tsx @@ -0,0 +1,735 @@ +/** + * New Experiment Wizard — Admin Dashboard + * Multi-step wizard for creating experiments with AI suggestions. + */ + +'use client'; + +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import Link from 'next/link'; +import { + ArrowLeft, + ArrowRight, + Sparkles, + FlaskConical, + Target, + Sliders, + Check, + Lightbulb, +} from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Badge } from '@/components/ui/badge'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { Progress } from '@/components/ui/progress'; +import type { CreateExperimentInput, ExperimentSuggestion } from '@/lib/experiments-types'; + +const steps = [ + { id: 'hypothesis', title: 'Hypothesis', icon: Lightbulb }, + { id: 'variants', title: 'Variants', icon: FlaskConical }, + { id: 'metrics', title: 'Metrics', icon: Target }, + { id: 'targeting', title: 'Targeting', icon: Sliders }, + { id: 'review', title: 'Review', icon: Check }, +]; + +export default function NewExperimentPage() { + const router = useRouter(); + const [currentStep, setCurrentStep] = useState(0); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [aiSuggestions, setAiSuggestions] = useState([]); + + const [formData, setFormData] = useState>({ + name: '', + description: '', + hypothesis: '', + variants: [ + { key: 'control', name: 'Control', description: 'Current implementation', isControl: true, flagConfig: {} }, + { key: 'variant_a', name: 'Variant A', description: '', isControl: false, flagConfig: {} }, + ], + allocationStrategy: 'random', + targetPercent: 100, + targeting: {}, + primaryMetric: { + name: 'conversion', + type: 'conversion', + eventName: 'purchase', + aggregation: 'count', + direction: 'increase', + minimumDetectableEffect: 5, + }, + guardrails: { + minSampleSizePerVariant: 100, + maxDurationDays: 30, + autoStopEnabled: true, + winnerThreshold: 95, + requireApprovalFor: 'none', + }, + }); + + async function loadAiSuggestions() { + try { + const response = await fetch('/api/ab-testing/suggestions'); + if (response.ok) { + const data = await response.json(); + setAiSuggestions(data.slice(0, 3)); + } + } catch { + // Silently fail - AI suggestions are optional + } + } + + async function handleSubmit() { + setLoading(true); + setError(null); + + try { + const response = await fetch('/api/ab-testing/experiments', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(formData), + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.message || 'Failed to create experiment'); + } + + const experiment = await response.json(); + router.push(`/experiments/${experiment.id}`); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to create experiment'); + setLoading(false); + } + } + + function applyAiSuggestion(suggestion: ExperimentSuggestion) { + setFormData(prev => ({ + ...prev, + name: prev.name || `Experiment: ${suggestion.hypothesis.primary.slice(0, 50)}...`, + hypothesis: suggestion.hypothesis.primary, + description: suggestion.hypothesis.alternatives[0] || '', + primaryMetric: suggestion.suggestedMetrics[0] || prev.primaryMetric, + })); + } + + const progress = ((currentStep + 1) / steps.length) * 100; + + return ( +
+ {/* Header */} +
+ +
+

Create New Experiment

+

+ Step {currentStep + 1} of {steps.length}: {steps[currentStep].title} +

+
+
+ + {/* Progress */} + + + {/* Step Indicators */} +
+ {steps.map((step, index) => { + const Icon = step.icon; + const isActive = index === currentStep; + const isCompleted = index < currentStep; + + return ( +
+
+ +
+ {step.title} +
+ ); + })} +
+ + {/* Error */} + {error && ( + + {error} + + )} + + {/* Step Content */} + + + {currentStep === 0 && ( + + )} + {currentStep === 1 && ( + + )} + {currentStep === 2 && ( + + )} + {currentStep === 3 && ( + + )} + {currentStep === 4 && ( + + )} + + + + {/* Navigation */} +
+ + + {currentStep < steps.length - 1 ? ( + + ) : ( + + )} +
+
+ ); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Step Components +// ───────────────────────────────────────────────────────────────────────────── + +function HypothesisStep({ + formData, + setFormData, + aiSuggestions, + onLoadSuggestions, + onApplySuggestion, +}: { + formData: Partial; + setFormData: (data: Partial) => void; + aiSuggestions: ExperimentSuggestion[]; + onLoadSuggestions: () => void; + onApplySuggestion: (s: ExperimentSuggestion) => void; +}) { + return ( +
+
+ + setFormData({ ...formData, name: e.target.value })} + placeholder="e.g., Homepage Button Color Test" + className="mt-1" + /> +
+ +
+ +