clean up prompts and improve quality

This commit is contained in:
jpremill committed 2026-03-06 08:46:03 -05:00
1 parent 9290cc18fd
commit 5b2da87f05
44 files changed
+6711 -414

No files matched your search

+161 -142
View File
@@ -1,9 +1,13 @@
import React, { useState } from 'react';
import { GoogleGenAI, Type } from "@google/genai";
import { Scenario } from '../types';
import { CBT_GLOSSARY } from '../constants';
import { Sparkles, X, Loader2, AlertCircle, Info, Target, UserCircle2, BrainCircuit, Check, BarChart } from 'lucide-react';
import { Button } from './ui/button';
import { Textarea } from './ui/textarea';
import { ScrollArea } from './ui/scroll-area';
import { Card } from './ui/card';
import { Badge } from './ui/badge';
interface ScenarioGeneratorProps {
onGenerate: (scenario: Scenario) => void;
@@ -18,7 +22,7 @@ const ScenarioGenerator: React.FC<ScenarioGeneratorProps> = ({ onGenerate, onCan
const [error, setError] = useState<string | null>(null);
const toggleConcept = (term: string) => {
setSelectedConcepts(prev =>
setSelectedConcepts(prev =>
prev.includes(term) ? prev.filter(t => t !== term) : [...prev, term]
);
};
@@ -30,8 +34,8 @@ const ScenarioGenerator: React.FC<ScenarioGeneratorProps> = ({ onGenerate, onCan
setError(null);
try {
const ai = new GoogleGenAI({ apiKey: process.env.API_KEY });
const ai = new GoogleGenAI({ apiKey: import.meta.env.VITE_GEMINI_API_KEY });
const schema = {
type: Type.OBJECT,
properties: {
@@ -39,25 +43,25 @@ const ScenarioGenerator: React.FC<ScenarioGeneratorProps> = ({ onGenerate, onCan
title: { type: Type.STRING },
characterName: { type: Type.STRING },
description: { type: Type.STRING },
category: {
type: Type.STRING,
enum: ['Social Anxiety', 'Workplace', 'Relationships', 'Self-Talk']
category: {
type: Type.STRING,
enum: ['Social Anxiety', 'Workplace', 'Relationships', 'Self-Talk']
},
difficulty: {
type: Type.STRING,
enum: ['Beginner', 'Intermediate', 'Advanced']
difficulty: {
type: Type.STRING,
enum: ['Beginner', 'Intermediate', 'Advanced']
},
systemInstruction: { type: Type.STRING },
initialMessage: { type: Type.STRING },
cbtFocus: {
type: Type.ARRAY,
items: { type: Type.STRING }
cbtFocus: {
type: Type.ARRAY,
items: { type: Type.STRING }
}
},
required: ['id', 'title', 'characterName', 'description', 'category', 'difficulty', 'systemInstruction', 'initialMessage', 'cbtFocus']
};
const selectedContext = selectedConcepts.length > 0
const selectedContext = selectedConcepts.length > 0
? `Focus specifically on these CBT concepts: ${selectedConcepts.join(', ')}.`
: '';
@@ -67,27 +71,29 @@ const ScenarioGenerator: React.FC<ScenarioGeneratorProps> = ({ onGenerate, onCan
responseMimeType: 'application/json',
responseSchema: schema,
systemInstruction: `
[CONTEXT]
You are a Clinical CBT Scenario Architect.
Your goal is to transform a user's prompt into a structured clinical simulation.
The user requires a structured clinical simulation designed to address specific cognitive distortions and behavioral challenges.
Clinical Focus: ${selectedContext}
CORE INSTRUCTIONS:
1. CLINICAL FOCUS: ${selectedContext}
2. CATEGORIZATION: Based on the user's input, you MUST suggest the most appropriate category for the scenario:
- 'Social Anxiety': Situations involving social performance, fear of judgment, or networking.
- 'Workplace': Scenarios involving colleagues, supervisors, career growth, or professional conflict.
- 'Relationships': Dynamics with family, friends, or romantic partners; boundary setting.
- 'Self-Talk': Purely internal cognitive restructuring, rumination, or self-compassion work.
3. DIFFICULTY CALIBRATION: The user has explicitly requested a '${selectedDifficulty}' level.
Generate the scenario content and the AI character's behavior to match this complexity:
- 'Beginner': Low stakes, high support, clear-cut distortions, AI character is cooperative.
- 'Intermediate': Ambiguous stakes, moderate social friction, subtle distortions, AI character has specific needs/points.
- 'Advanced': High emotional triggers, persistent clinical resistance from AI character, complex deep-seated cognitive patterns.
4. INTERNAL LOGIC: The 'systemInstruction' for the character MUST include a detailed "INTERNAL CBT LOGIC" section. This instructs the AI character how to react to the user's progress or cognitive lapses.
5. CHARACTER VOICE: Define a realistic voice profile for the character, including their likely emotional state (frustrated, busy, anxious, etc.).
[TASK]
Transform the user's prompt into a cohesive, structured clinical scenario that sets up a targeted roleplay environment.
[CONSTRAINTS]
- CATEGORIZATION: You MUST categorize the scenario appropriately (Social Anxiety, Workplace, Relationships, or Self-Talk).
- DIFFICULTY CALIBRATION: The requested difficulty is '${selectedDifficulty}'.
- 'Beginner': Low stakes, high support, clear-cut distortions, AI character is cooperative.
- 'Intermediate': Ambiguous stakes, moderate social friction, subtle distortions, AI character has specific needs.
- 'Advanced': High emotional triggers, persistent clinical resistance, complex cognitive patterns.
- INTERNAL LOGIC: Provide a detailed "INTERNAL CBT LOGIC" section instructing the AI character how to react to the user's progress or cognitive lapses during the future roleplay.
- CHARACTER VOICE: Define a realistic, emotionally consistent voice profile.
[OUTPUT REQUIREMENTS]
Return ONLY a valid JSON object conforming exactly to the requested schema. Ensure all arrays and nested objects are populated correctly.
`
},
contents: [
{ role: 'user', parts: [{ text: `Generate a clinical scenario for: ${prompt || 'A scenario exploring the chosen CBT concepts.'}` }] }
{ role: 'user', parts: [{ text: `Generate a clinical scenario for: ${prompt || 'A scenario exploring the chosen CBT concepts.'}` }] }
]
});
@@ -109,21 +115,26 @@ const ScenarioGenerator: React.FC<ScenarioGeneratorProps> = ({ onGenerate, onCan
}
};
const getConceptColor = (category: string, isSelected: boolean) => {
if (!isSelected) return 'bg-white border-slate-200 text-slate-500 hover:border-slate-300';
const getConceptVariant = (category: string, isSelected: boolean) => {
if (!isSelected) return 'outline';
return 'default';
};
const getConceptColorClass = (category: string, isSelected: boolean) => {
if (!isSelected) return 'text-slate-500 border-slate-200 hover:border-slate-300';
switch (category) {
case 'Distortion': return 'bg-rose-600 border-rose-600 text-white shadow-md shadow-rose-100';
case 'Technique': return 'bg-teal-600 border-teal-600 text-white shadow-md shadow-teal-100';
case 'Concept': return 'bg-amber-600 border-amber-600 text-white shadow-md shadow-amber-100';
default: return 'bg-slate-800 border-slate-800 text-white';
case 'Distortion': return 'bg-rose-600 border-rose-600 hover:bg-rose-700 shadow-md shadow-rose-100';
case 'Technique': return 'bg-teal-600 border-teal-600 hover:bg-teal-700 shadow-md shadow-teal-100';
case 'Concept': return 'bg-amber-600 border-amber-600 hover:bg-amber-700 shadow-md shadow-amber-100';
default: return 'bg-slate-800 border-slate-800 hover:bg-slate-900';
}
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-900/60 backdrop-blur-md overflow-y-auto">
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-2xl overflow-hidden animate-in fade-in zoom-in duration-300 border border-slate-100 my-8">
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-2xl overflow-hidden animate-in fade-in zoom-in duration-300 border border-slate-100 my-8 flex flex-col max-h-[90vh]">
{/* Header */}
<div className="p-6 bg-teal-600 text-white flex justify-between items-start relative overflow-hidden">
<div className="p-6 bg-teal-600 text-white flex justify-between items-start relative overflow-hidden shrink-0">
<div className="relative z-10">
<h2 className="text-2xl font-bold flex items-center gap-3">
<Sparkles size={28} className="text-teal-200" />
@@ -133,125 +144,133 @@ const ScenarioGenerator: React.FC<ScenarioGeneratorProps> = ({ onGenerate, onCan
Construct an evidence-based clinical environment.
</p>
</div>
<button onClick={onCancel} className="relative z-10 text-white/70 hover:text-white transition-colors bg-white/10 p-2 rounded-xl border border-white/10">
<Button
variant="ghost"
size="icon"
onClick={onCancel}
className="relative z-10 text-white/70 hover:text-white transition-colors bg-white/10 hover:bg-white/20 border border-white/10"
>
<X size={20} />
</button>
</Button>
<div className="absolute top-0 right-0 w-32 h-32 bg-white/5 rounded-full -mr-10 -mt-10 blur-2xl"></div>
</div>
<div className="p-8 space-y-6 bg-[#FCFDFF]">
{/* Prompting Guide */}
<div className="bg-teal-50/50 border border-teal-100 rounded-xl p-4 space-y-3">
<div className="flex items-center gap-2 text-teal-700">
<Info size={16} />
<h3 className="text-xs font-bold uppercase tracking-wider">Clinical Blueprint</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-1">
<div className="flex items-center gap-1.5 text-[10px] font-bold text-teal-600 uppercase">
<UserCircle2 size={12} /> Character Role
</div>
<p className="text-[10px] text-slate-500 leading-tight">Define who the AI plays.</p>
<ScrollArea className="flex-grow p-8 bg-[#FCFDFF]">
<div className="space-y-6 pb-4">
{/* Prompting Guide */}
<Card className="bg-teal-50/50 border-teal-100 p-4 shadow-none">
<div className="flex items-center gap-2 text-teal-700 mb-3">
<Info size={16} />
<h3 className="text-xs font-bold uppercase tracking-wider">Clinical Blueprint</h3>
</div>
<div className="space-y-1">
<div className="flex items-center gap-1.5 text-[10px] font-bold text-teal-600 uppercase">
<Target size={12} /> The Challenge
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-1">
<div className="flex items-center gap-1.5 text-[10px] font-bold text-teal-600 uppercase">
<UserCircle2 size={12} /> Character Role
</div>
<p className="text-[10px] text-slate-500 leading-tight">Define who the AI plays.</p>
</div>
<p className="text-[10px] text-slate-500 leading-tight">Describe the social friction.</p>
</div>
<div className="space-y-1">
<div className="flex items-center gap-1.5 text-[10px] font-bold text-teal-600 uppercase">
<BrainCircuit size={12} /> CBT Focus
<div className="space-y-1">
<div className="flex items-center gap-1.5 text-[10px] font-bold text-teal-600 uppercase">
<Target size={12} /> The Challenge
</div>
<p className="text-[10px] text-slate-500 leading-tight">Describe the social friction.</p>
</div>
<div className="space-y-1">
<div className="flex items-center gap-1.5 text-[10px] font-bold text-teal-600 uppercase">
<BrainCircuit size={12} /> CBT Focus
</div>
<p className="text-[10px] text-slate-500 leading-tight">Target specific distortions.</p>
</div>
<p className="text-[10px] text-slate-500 leading-tight">Target specific distortions.</p>
</div>
</div>
</div>
</Card>
{/* Difficulty Selection */}
<div className="space-y-3">
<label className="block text-[10px] font-black text-slate-400 uppercase tracking-widest">
Communication Difficulty
</label>
<div className="grid grid-cols-3 gap-2">
{(['Beginner', 'Intermediate', 'Advanced'] as const).map((level) => (
<button
key={level}
onClick={() => setSelectedDifficulty(level)}
className={`flex items-center justify-center gap-2 py-2.5 rounded-xl border-2 transition-all text-[11px] font-bold ${
selectedDifficulty === level
? 'bg-teal-50 border-teal-600 text-teal-700 shadow-sm shadow-teal-50'
: 'bg-white border-slate-100 text-slate-400 hover:border-slate-200'
}`}
>
<BarChart size={14} className={selectedDifficulty === level ? 'text-teal-600' : 'text-slate-300'} />
{level}
</button>
))}
</div>
</div>
{/* Clinical Targets (Selection) */}
<div className="space-y-3">
<label className="block text-[10px] font-black text-slate-400 uppercase tracking-widest">
Clinical Targets (Optional)
</label>
<div className="flex flex-wrap gap-2 max-h-32 overflow-y-auto p-1 scrollbar-hide">
{CBT_GLOSSARY.map(concept => {
const isSelected = selectedConcepts.includes(concept.term);
return (
<button
key={concept.term}
onClick={() => toggleConcept(concept.term)}
className={`px-3 py-1.5 rounded-lg text-[10px] font-bold border transition-all flex items-center gap-1.5 ${getConceptColor(concept.category, isSelected)}`}
{/* Difficulty Selection */}
<div className="space-y-3">
<label className="block text-[10px] font-black text-slate-400 uppercase tracking-widest">
Communication Difficulty
</label>
<div className="grid grid-cols-3 gap-2">
{(['Beginner', 'Intermediate', 'Advanced'] as const).map((level) => (
<Button
key={level}
variant={selectedDifficulty === level ? "default" : "outline"}
onClick={() => setSelectedDifficulty(level)}
className={`h-12 flex items-center justify-center gap-2 rounded-xl border-2 transition-all text-[11px] font-bold ${selectedDifficulty === level
? 'bg-teal-50 border-teal-600 text-teal-700 shadow-sm shadow-teal-50 hover:bg-teal-100 hover:text-teal-800'
: 'bg-white border-slate-200 text-slate-500 hover:border-slate-300'
}`}
>
{isSelected && <Check size={10} />}
{concept.term}
</button>
);
})}
<BarChart size={14} className={selectedDifficulty === level ? 'text-teal-600' : 'text-slate-400'} />
{level}
</Button>
))}
</div>
</div>
</div>
<div>
<label className="block text-[10px] font-black text-slate-400 uppercase tracking-widest mb-3">
Simulation Context / Custom Instructions
</label>
<textarea
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="e.g. Practicing assertiveness with a pushy salesperson who won't take 'no' for an answer."
className="w-full h-28 p-5 border border-slate-200 rounded-2xl focus:ring-4 focus:ring-teal-500/10 focus:border-teal-500 focus:outline-none resize-none text-slate-800 placeholder:text-slate-400 text-sm leading-relaxed shadow-inner bg-white"
disabled={isLoading}
/>
</div>
{error && (
<div className="bg-rose-50 text-rose-600 p-4 rounded-xl text-xs flex items-center gap-3 border border-rose-100 font-medium">
<AlertCircle size={18} />
{error}
{/* Clinical Targets (Selection) */}
<div className="space-y-3">
<label className="block text-[10px] font-black text-slate-400 uppercase tracking-widest">
Clinical Targets (Optional)
</label>
<div className="flex flex-wrap gap-2">
{CBT_GLOSSARY.map(concept => {
const isSelected = selectedConcepts.includes(concept.term);
return (
<Badge
key={concept.term}
variant={getConceptVariant(concept.category, isSelected) as any}
onClick={() => toggleConcept(concept.term)}
className={`px-3 py-1.5 rounded-lg text-[10px] font-bold border transition-all flex items-center gap-1.5 cursor-pointer hover:opacity-90 ${getConceptColorClass(concept.category, isSelected)}`}
>
{isSelected && <Check size={10} />}
{concept.term}
</Badge>
);
})}
</div>
</div>
)}
<div className="flex justify-end pt-2">
<button
onClick={handleGenerate}
disabled={(!prompt.trim() && selectedConcepts.length === 0) || isLoading}
className="bg-teal-600 text-white px-8 py-3.5 rounded-2xl font-bold hover:bg-teal-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-3 transition-all shadow-lg shadow-teal-100 text-sm uppercase tracking-widest"
>
{isLoading ? (
<>
<Loader2 size={20} className="animate-spin" />
Synthesizing...
</>
) : (
<>
<Sparkles size={20} />
Initiate Build
</>
)}
</button>
<div>
<label className="block text-[10px] font-black text-slate-400 uppercase tracking-widest mb-3">
Simulation Context / Custom Instructions
</label>
<Textarea
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="e.g. Practicing assertiveness with a pushy salesperson who won't take 'no' for an answer."
className="w-full h-28 p-5 border-slate-200 rounded-2xl focus-visible:ring-teal-500 resize-none text-slate-800 placeholder:text-slate-400 text-sm leading-relaxed shadow-inner bg-white"
disabled={isLoading}
/>
</div>
{error && (
<div className="bg-rose-50 text-rose-600 p-4 rounded-xl text-xs flex items-center gap-3 border border-rose-100 font-medium">
<AlertCircle size={18} />
{error}
</div>
)}
</div>
</ScrollArea>
<div className="p-6 bg-slate-50 border-t border-slate-100 flex justify-end shrink-0">
<Button
onClick={handleGenerate}
disabled={(!prompt.trim() && selectedConcepts.length === 0) || isLoading}
className="bg-teal-600 text-white h-12 px-8 rounded-2xl font-bold hover:bg-teal-700 flex items-center gap-3 transition-all shadow-lg shadow-teal-100 text-sm uppercase tracking-widest"
>
{isLoading ? (
<>
<Loader2 size={20} className="animate-spin" />
Synthesizing...
</>
) : (
<>
<Sparkles size={20} />
Initiate Build
</>
)}
</Button>
</div>
</div>
</div>