diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..c8de6b5 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +VITE_GEMINI_API_KEY="" diff --git a/.gitignore b/.gitignore index a547bf3..4fee690 100644 --- a/.gitignore +++ b/.gitignore @@ -7,12 +7,19 @@ yarn-error.log* pnpm-debug.log* lerna-debug.log* +# Node node_modules dist dist-ssr *.local -# Editor directories and files +# Env +.env +.env.* +!.env.example + +# Azure Functions +local.settings.json .vscode/* !.vscode/extensions.json .idea @@ -22,3 +29,5 @@ dist-ssr *.njsproj *.sln *.sw? +bin/ +obj/ diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..bb76300 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "ms-azuretools.vscode-azurefunctions", + "ms-dotnettools.csharp" + ] +} \ No newline at end of file diff --git a/App.tsx b/App.tsx index a8f4284..6952067 100644 --- a/App.tsx +++ b/App.tsx @@ -10,6 +10,7 @@ import CBTReference from './components/CBTReference'; import SettingsModal from './components/SettingsModal'; import Tooltip from './components/Tooltip'; import { Brain, Info, ShieldCheck, Filter, Plus, BookOpen, Activity, Zap, Settings } from 'lucide-react'; +import { Button } from './components/ui/button'; const DEFAULT_SETTINGS: UserSettings = { voiceName: 'Zephyr', @@ -25,7 +26,7 @@ const App: React.FC = () => { const [sessionMode, setSessionMode] = useState('text'); const [selectedDifficulty, setSelectedDifficulty] = useState('All'); const [isPreparing, setIsPreparing] = useState(false); - + // Custom Scenarios State with LocalStorage persistence const [customScenarios, setCustomScenarios] = useState(() => { try { @@ -80,7 +81,7 @@ const App: React.FC = () => { }, []); const allScenarios = [...customScenarios, ...SCENARIOS]; - const filteredScenarios = allScenarios.filter(scenario => + const filteredScenarios = allScenarios.filter(scenario => selectedDifficulty === 'All' || scenario.difficulty === selectedDifficulty ); @@ -99,7 +100,7 @@ const App: React.FC = () => {
- +

Initializing Session

@@ -143,12 +144,12 @@ const App: React.FC = () => { ) : (
- +
)}
@@ -172,32 +173,35 @@ const App: React.FC = () => {

Cognitive Clarity

- +
- + - + + Reference Guide +
- +

An adaptive clinical training environment designed to strengthen cognitive flexibility and navigate social friction.

- +
@@ -222,64 +226,65 @@ const App: React.FC = () => {
- - Difficulty + + Difficulty
{difficulties.map((diff) => ( - + ))}
- +
+ +
{filteredScenarios.map(scenario => ( - ))} - +
- +
- Cognitive Clarity v1.2 + Cognitive Clarity v1.2
diff --git a/api/.gitignore b/api/.gitignore new file mode 100644 index 0000000..ff5b00c --- /dev/null +++ b/api/.gitignore @@ -0,0 +1,264 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# Azure Functions localsettings file +local.settings.json + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +#*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc \ No newline at end of file diff --git a/api/.vscode/extensions.json b/api/.vscode/extensions.json new file mode 100644 index 0000000..dde673d --- /dev/null +++ b/api/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "ms-azuretools.vscode-azurefunctions" + ] +} \ No newline at end of file diff --git a/api/AnalysisFunction.cs b/api/AnalysisFunction.cs new file mode 100644 index 0000000..64ea565 --- /dev/null +++ b/api/AnalysisFunction.cs @@ -0,0 +1,116 @@ +using System.Net; +using System.Text.Json; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; +using Google.GenAI; +using System.Text.Json.Serialization; + +namespace CognitiveClarity.api +{ + public class AnalysisRequest + { + [JsonPropertyName("turnContext")] + public string TurnContext { get; set; } = string.Empty; + + [JsonPropertyName("scenarioTitle")] + public string ScenarioTitle { get; set; } = string.Empty; + + [JsonPropertyName("scenarioDescription")] + public string ScenarioDescription { get; set; } = string.Empty; + + [JsonPropertyName("cbtFocus")] + public List CBTFocus { get; set; } = new(); + } + + public class AnalysisFunction + { + private readonly ILogger _logger; + + public AnalysisFunction(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } + + [Function("ClinicalAnalysis")] + public async Task Run( + [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "analysis")] HttpRequestData req) + { + _logger.LogInformation("C# HTTP trigger function processed a clinical analysis request."); + + var requestBody = await new StreamReader(req.Body).ReadToEndAsync(); + var data = JsonSerializer.Deserialize(requestBody); + + if (data == null || string.IsNullOrWhiteSpace(data.TurnContext)) + { + var badResponse = req.CreateResponse(HttpStatusCode.BadRequest); + await badResponse.WriteStringAsync("Please provide turnContext in the request body"); + return badResponse; + } + + var apiKey = Environment.GetEnvironmentVariable("GEMINI_API_KEY"); + if (string.IsNullOrEmpty(apiKey)) + { + _logger.LogError("GEMINI_API_KEY is missing from environment variables."); + return req.CreateResponse(HttpStatusCode.InternalServerError); + } + + // Initialize the Gemini Client + var ai = new GeminiClient(apiKey: apiKey); + + var systemAnalysisPrompt = @" +You are a master clinical psychologist specializing in Cognitive Behavioral Therapy (CBT). +You are observing a roleplay session between a user and an AI character. +Analyze the user's latest response for: +1. Cognitive distortions present (if any). +2. CBT techniques successfully applied by the user (if any). +3. A brief, encouraging clinical insight or redirection (max 2 sentences). +Be precise, clinical, and constructive. +"; + + var analysisSystemInstruction = $@"{systemAnalysisPrompt} +SCENARIO CONTEXT: +Title: {data.ScenarioTitle} +Description: {data.ScenarioDescription} +CBT Focus Areas: {string.Join(", ", data.CBTFocus)}"; + + var schema = new Schema + { + Type = TypeEnum.Object, + Properties = new Dictionary + { + { "distortionsIdentified", new Schema { Type = TypeEnum.Array, Items = new Schema { Type = TypeEnum.String } } }, + { "techniquesUsed", new Schema { Type = TypeEnum.Array, Items = new Schema { Type = TypeEnum.String } } }, + { "clinicalInsight", new Schema { Type = TypeEnum.String } }, + { "feedbackType", new Schema { Type = TypeEnum.String, Enum = new List { "Constructive", "Positive", "Neutral" } } } + }, + Required = new List { "distortionsIdentified", "techniquesUsed", "clinicalInsight", "feedbackType" } + }; + + try + { + var response = await ai.Models.GenerateContentAsync( + model: "gemini-3.0-flash", + contents: data.TurnContext, + new GenerateContentConfig + { + SystemInstruction = new Content { Parts = new List { new Part { Text = analysisSystemInstruction } } }, + ResponseMimeType = "application/json", + ResponseSchema = schema + }); + + var successResponse = req.CreateResponse(HttpStatusCode.OK); + successResponse.Headers.Add("Content-Type", "application/json"); + await successResponse.WriteStringAsync(response.Text!); + return successResponse; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to generate analysis via Gemini API."); + var errorResponse = req.CreateResponse(HttpStatusCode.InternalServerError); + await errorResponse.WriteStringAsync("Error communicating with Gemini API."); + return errorResponse; + } + } + } +} diff --git a/api/ChatFunction.cs b/api/ChatFunction.cs new file mode 100644 index 0000000..ef4ec91 --- /dev/null +++ b/api/ChatFunction.cs @@ -0,0 +1,124 @@ +using System.Net; +using System.Text.Json; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; +using Google.GenAI; +using System.Text.Json.Serialization; + +namespace CognitiveClarity.api +{ + public class ChatMessage + { + [JsonPropertyName("role")] + public string Role { get; set; } = string.Empty; // "user" or "model" + + [JsonPropertyName("text")] + public string Text { get; set; } = string.Empty; + } + + public class ChatRequest + { + [JsonPropertyName("history")] + public List History { get; set; } = new(); + + [JsonPropertyName("systemInstruction")] + public string SystemInstruction { get; set; } = string.Empty; + } + + public class ChatFunction + { + private readonly ILogger _logger; + + public ChatFunction(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } + + [Function("ChatProgression")] + public async Task Run( + [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "chat")] HttpRequestData req) + { + _logger.LogInformation("C# HTTP trigger function processed a chat progression request."); + + var requestBody = await new StreamReader(req.Body).ReadToEndAsync(); + var data = JsonSerializer.Deserialize(requestBody); + + if (data == null || data.History.Count == 0) + { + var badResponse = req.CreateResponse(HttpStatusCode.BadRequest); + await badResponse.WriteStringAsync("Please provide chat history in the request body"); + return badResponse; + } + + var apiKey = Environment.GetEnvironmentVariable("GEMINI_API_KEY"); + if (string.IsNullOrEmpty(apiKey)) + { + _logger.LogError("GEMINI_API_KEY is missing from environment variables."); + return req.CreateResponse(HttpStatusCode.InternalServerError); + } + + // Initialize the Gemini Client + var ai = new GeminiClient(apiKey: apiKey); + + // Construct contents from history + var contents = new List(); + foreach (var msg in data.History) + { + contents.Add(new Content + { + Role = msg.Role, + Parts = new List { new Part { Text = msg.Text } } + }); + } + + var roleplaySystemInstruction = $@"{data.SystemInstruction} + + [CONTEXT] + You are participating in a clinical CBT roleplay simulation. + The instructions above define your character, the scenario, and your internal CBT logic. + + [TASK] + Respond to the user naturally in character to advance the roleplay scenario. + + [CONSTRAINTS] + - CRITICAL: Stay strictly in character as the persona defined in the scenario. + - CRITICAL: NEVER provide clinical feedback, advice, or meta-commentary about the user's progress. + - CRITICAL: NEVER use phrases like ""Great job holding your boundary"" or ""Feedback:"". + - CRITICAL: Do not congratulate the user on using CBT techniques; simply react as the character would. + - If the user successfully concludes the scenario, end the dialogue naturally. + + [OUTPUT REQUIREMENTS] + Return the character's direct dialogue response. Keep it conversational and proportional to the user's input. + "; + + try + { + var response = await ai.Models.GenerateContentAsync( + model: "gemini-3.0-flash", + contents: contents, + new GenerateContentConfig + { + SystemInstruction = new Content { Parts = new List { new Part { Text = roleplaySystemInstruction } } }, + // Streaming isn't natively supported in standard HTTP Functions easily without Server-Sent Events (SSE). + // We will return a standard block response for now, which simplifies the architecture. + }); + + var successResponse = req.CreateResponse(HttpStatusCode.OK); + successResponse.Headers.Add("Content-Type", "application/json"); + + // Return just the text in a JSON wrapper + var resultObj = new { text = response.Text }; + await successResponse.WriteStringAsync(JsonSerializer.Serialize(resultObj)); + return successResponse; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to generate chat progression via Gemini API."); + var errorResponse = req.CreateResponse(HttpStatusCode.InternalServerError); + await errorResponse.WriteStringAsync("Error communicating with Gemini API."); + return errorResponse; + } + } + } +} diff --git a/api/Program.cs b/api/Program.cs new file mode 100644 index 0000000..704475c --- /dev/null +++ b/api/Program.cs @@ -0,0 +1,14 @@ +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +var builder = FunctionsApplication.CreateBuilder(args); + +builder.ConfigureFunctionsWebApplication(); + +builder.Services + .AddApplicationInsightsTelemetryWorkerService() + .ConfigureFunctionsApplicationInsights(); + +builder.Build().Run(); diff --git a/api/Properties/launchSettings.json b/api/Properties/launchSettings.json new file mode 100644 index 0000000..86d9893 --- /dev/null +++ b/api/Properties/launchSettings.json @@ -0,0 +1,9 @@ +{ + "profiles": { + "api": { + "commandName": "Project", + "commandLineArgs": "--port 7145", + "launchBrowser": false + } + } +} \ No newline at end of file diff --git a/api/ScenarioFunction.cs b/api/ScenarioFunction.cs new file mode 100644 index 0000000..5dabd43 --- /dev/null +++ b/api/ScenarioFunction.cs @@ -0,0 +1,130 @@ +using System.Net; +using System.Text.Json; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; +using Google.GenAI; +using System.Text.Json.Serialization; + +namespace CognitiveClarity.api +{ + public class ScenarioRequest + { + [JsonPropertyName("prompt")] + public string Prompt { get; set; } = string.Empty; + + [JsonPropertyName("selectedConcepts")] + public List SelectedConcepts { get; set; } = new(); + + [JsonPropertyName("difficulty")] + public string Difficulty { get; set; } = "Intermediate"; + } + + public class ScenarioFunction + { + private readonly ILogger _logger; + + public ScenarioFunction(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } + + [Function("GenerateScenario")] + public async Task Run( + [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "scenario/generate")] HttpRequestData req) + { + _logger.LogInformation("C# HTTP trigger function processed a scenario generation request."); + + var requestBody = await new StreamReader(req.Body).ReadToEndAsync(); + var data = JsonSerializer.Deserialize(requestBody); + + if (data == null || (string.IsNullOrWhiteSpace(data.Prompt) && data.SelectedConcepts.Count == 0)) + { + var badResponse = req.CreateResponse(HttpStatusCode.BadRequest); + await badResponse.WriteStringAsync("Please pass a prompt or concepts in the request body"); + return badResponse; + } + + var apiKey = Environment.GetEnvironmentVariable("GEMINI_API_KEY"); + if (string.IsNullOrEmpty(apiKey)) + { + _logger.LogError("GEMINI_API_KEY is missing from environment variables."); + return req.CreateResponse(HttpStatusCode.InternalServerError); + } + + // Initialize the Gemini Client + var ai = new GeminiClient(apiKey: apiKey); + + var selectedContext = data.SelectedConcepts.Count > 0 + ? $"Focus specifically on these CBT concepts: {string.Join(", ", data.SelectedConcepts)}." + : ""; + + var systemInstruction = $@" + [CONTEXT] + You are a Clinical CBT Scenario Architect. + The user requires a structured clinical simulation designed to address specific cognitive distortions and behavioral challenges. + Clinical Focus: {selectedContext} + + [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 '{data.Difficulty}'. + - '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. + "; + + var schema = new Schema + { + Type = TypeEnum.Object, + Properties = new Dictionary + { + { "id", new Schema { Type = TypeEnum.String } }, + { "title", new Schema { Type = TypeEnum.String } }, + { "characterName", new Schema { Type = TypeEnum.String } }, + { "description", new Schema { Type = TypeEnum.String } }, + { "category", new Schema { Type = TypeEnum.String, Enum = new List { "Social Anxiety", "Workplace", "Relationships", "Self-Talk" } } }, + { "difficulty", new Schema { Type = TypeEnum.String, Enum = new List { "Beginner", "Intermediate", "Advanced" } } }, + { "systemInstruction", new Schema { Type = TypeEnum.String } }, + { "initialMessage", new Schema { Type = TypeEnum.String } }, + { "cbtFocus", new Schema { Type = TypeEnum.Array, Items = new Schema { Type = TypeEnum.String } } } + }, + Required = new List { "id", "title", "characterName", "description", "category", "difficulty", "systemInstruction", "initialMessage", "cbtFocus" } + }; + + var promptText = $"Generate a clinical scenario for: {(string.IsNullOrWhiteSpace(data.Prompt) ? "A scenario exploring the chosen CBT concepts." : data.Prompt)}"; + + try + { + var response = await ai.Models.GenerateContentAsync( + model: "gemini-3.0-flash", + contents: promptText, + new GenerateContentConfig + { + SystemInstruction = new Content { Parts = new List { new Part { Text = systemInstruction } } }, + ResponseMimeType = "application/json", + ResponseSchema = schema + }); + + var successResponse = req.CreateResponse(HttpStatusCode.OK); + successResponse.Headers.Add("Content-Type", "application/json"); + await successResponse.WriteStringAsync(response.Text!); + return successResponse; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to generate scenario via Gemini API."); + var errorResponse = req.CreateResponse(HttpStatusCode.InternalServerError); + await errorResponse.WriteStringAsync("Error communicating with Gemini API."); + return errorResponse; + } + } + } +} diff --git a/api/api.csproj b/api/api.csproj new file mode 100644 index 0000000..9022e8b --- /dev/null +++ b/api/api.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + v4 + Exe + enable + enable + + + + + + + + + + + + + diff --git a/api/host.json b/api/host.json new file mode 100644 index 0000000..ee5cf5f --- /dev/null +++ b/api/host.json @@ -0,0 +1,12 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + }, + "enableLiveMetricsFilters": true + } + } +} \ No newline at end of file diff --git a/components.json b/components.json new file mode 100644 index 0000000..1608496 --- /dev/null +++ b/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "index.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/components/CBTReference.tsx b/components/CBTReference.tsx index c35c23c..004084c 100644 --- a/components/CBTReference.tsx +++ b/components/CBTReference.tsx @@ -1,6 +1,11 @@ import React, { useState } from 'react'; import { CBT_GLOSSARY, GlossaryEntry } from '../constants'; import { X, Search, BookOpen, Brain, Activity, HelpCircle } from 'lucide-react'; +import { Button } from './ui/button'; +import { Input } from './ui/input'; +import { Badge } from './ui/badge'; +import { ScrollArea } from './ui/scroll-area'; +import { Card, CardHeader, CardTitle, CardContent } from './ui/card'; interface CBTReferenceProps { onClose: () => void; @@ -11,8 +16,8 @@ const CBTReference: React.FC = ({ onClose }) => { const [activeCategory, setActiveCategory] = useState<'All' | 'Distortion' | 'Technique' | 'Concept'>('All'); const filtered = CBT_GLOSSARY.filter(entry => { - const matchesSearch = entry.term.toLowerCase().includes(search.toLowerCase()) || - entry.definition.toLowerCase().includes(search.toLowerCase()); + const matchesSearch = entry.term.toLowerCase().includes(search.toLowerCase()) || + entry.definition.toLowerCase().includes(search.toLowerCase()); const matchesCategory = activeCategory === 'All' || entry.category === activeCategory; return matchesSearch && matchesCategory; }); @@ -32,7 +37,7 @@ const CBTReference: React.FC = ({ onClose }) => {
{/* Header */} -
+
@@ -42,68 +47,66 @@ const CBTReference: React.FC = ({ onClose }) => {

Evidence-Based Concepts & Frameworks

- +
{/* Filters */} -
+
- setSearch(e.target.value)} - className="w-full pl-12 pr-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-4 focus:ring-teal-500/10 focus:border-teal-500 focus:outline-none transition-all text-sm" + className="w-full pl-12 pr-4 h-12 bg-slate-50 border-slate-200 rounded-xl focus-visible:ring-teal-500/50" />
- +
{categories.map(cat => ( - + ))}
{/* Content */} -
-
+ +
{filtered.map((entry, idx) => ( -
-
-

{entry.term}

-
- {getCategoryIcon(entry.category)} - {entry.category} + + +
+ {entry.term} + + {getCategoryIcon(entry.category)} + {entry.category} +
-
-

- {entry.definition} -

- {entry.example && ( -
- Clinical Example -

"{entry.example}"

-
- )} -
+ + +

+ {entry.definition} +

+ {entry.example && ( +
+ Clinical Example +

"{entry.example}"

+
+ )} +
+ ))} - + {filtered.length === 0 && (
@@ -113,10 +116,10 @@ const CBTReference: React.FC = ({ onClose }) => {
)}
-
+ {/* Footer */} -
+

References compiled from clinical CBT standard frameworks.

diff --git a/components/ChatSession.tsx b/components/ChatSession.tsx index 711fabf..b2e20e1 100644 --- a/components/ChatSession.tsx +++ b/components/ChatSession.tsx @@ -20,14 +20,14 @@ const ChatSession: React.FC = ({ scenario, onExit }) => { const [messages, setMessages] = useState([]); const [isLoading, setIsLoading] = useState(false); const [status, setStatus] = useState<'active' | 'analyzing' | 'finished'>('active'); - + // Clinical Feedback State const [liveInsight, setLiveInsight] = useState(null); const [isAnalyzingTurn, setIsAnalyzingTurn] = useState(false); const [finalReport, setFinalReport] = useState(null); - + const chatRef = useRef(null); - const analysisRef = useRef(null); + const analysisRef = useRef(null); const messagesEndRef = useRef(null); const scrollToBottom = useCallback(() => { @@ -41,17 +41,27 @@ const ChatSession: React.FC = ({ scenario, onExit }) => { // Session Initialization useEffect(() => { const ai = new GoogleGenAI({ apiKey: process.env.API_KEY }); - + // Roleplay Chat Setup const roleplaySystemInstruction = ` ${scenario.systemInstruction} - CRITICAL ROLEPLAY PROTOCOL: - 1. Stay strictly in character as the persona defined in the scenario. - 2. NEVER provide clinical feedback, advice, or meta-commentary about the user's progress. - 3. NEVER use phrases like "Great job holding your boundary" or "Feedback:". - 4. Do not congratulate the user on using CBT techniques; simply react as the character would. - 5. If the user successfully concludes the scenario, end the dialogue naturally. + [CONTEXT] + You are participating in a clinical CBT roleplay simulation. + The instructions above define your character, the scenario, and your internal CBT logic. + + [TASK] + Respond to the user naturally in character to advance the roleplay scenario. + + [CONSTRAINTS] + - CRITICAL: Stay strictly in character as the persona defined in the scenario. + - CRITICAL: NEVER provide clinical feedback, advice, or meta-commentary about the user's progress. + - CRITICAL: NEVER use phrases like "Great job holding your boundary" or "Feedback:". + - CRITICAL: Do not congratulate the user on using CBT techniques; simply react as the character would. + - If the user successfully concludes the scenario, end the dialogue naturally. + + [OUTPUT REQUIREMENTS] + Return the character's direct dialogue response. Keep it conversational and proportional to the user's input. `; chatRef.current = ai.chats.create({ @@ -82,7 +92,7 @@ CBT Focus Areas: ${scenario.cbtFocus.join(', ')}`; text: scenario.initialMessage, timestamp: new Date() }]); - + setLiveInsight(null); }, [scenario]); @@ -116,8 +126,8 @@ CBT Focus Areas: ${scenario.cbtFocus.join(', ')}`; const c = chunk as GenerateContentResponse; const textChunk = c.text || ""; fullResponseText += textChunk; - - setMessages(prev => prev.map(m => + + setMessages(prev => prev.map(m => m.id === botMsgId ? { ...m, text: fullResponseText } : m )); } @@ -126,12 +136,12 @@ CBT Focus Areas: ${scenario.cbtFocus.join(', ')}`; if (analysisRef.current) { setIsAnalyzingTurn(true); const turnContext = `User said: "${userMsg.text}". Roleplay partner said: "${fullResponseText}".`; - + analysisRef.current.sendMessage({ message: turnContext }).then(res => { try { - if (res.text) setLiveInsight(JSON.parse(res.text)); - } catch(e) { console.error("Live analysis parse error", e); } finally { - setIsAnalyzingTurn(false); + if (res.text) setLiveInsight(JSON.parse(res.text)); + } catch (e) { console.error("Live analysis parse error", e); } finally { + setIsAnalyzingTurn(false); } }); } @@ -148,11 +158,11 @@ CBT Focus Areas: ${scenario.cbtFocus.join(', ')}`; if (messages.length < 2) return onExit(); setStatus('analyzing'); - + try { const ai = new GoogleGenAI({ apiKey: process.env.API_KEY }); const transcript = messages.map(m => `[${m.role.toUpperCase()}]: ${m.text}`).join('\n'); - + const summaryPrompt = `${SYSTEM_SUMMARY_ANALYSIS_PROMPT} SCENARIO CONTEXT: ${scenario.title} - ${scenario.description}`; @@ -182,8 +192,8 @@ SCENARIO CONTEXT: ${scenario.title} - ${scenario.description}`; return (
- -
+ +

Compiling Report

Generating evidence-based synthesis of your cognitive and behavioral patterns during this session...

@@ -206,14 +216,14 @@ SCENARIO CONTEXT: ${scenario.title} - ${scenario.description}`;

{scenario.title}

- {scenario.cbtFocus.slice(0, 3).map(f => ( - {f} - ))} + {scenario.cbtFocus.slice(0, 3).map(f => ( + {f} + ))}
- + )} -
- - - {scenario.difficulty} - - - {!onDelete && ( - - {scenario.category} - - )} -
- -

{scenario.title}

-

{scenario.description}

- -
-
+ +
+ +
+ + {scenario.difficulty} + +
+
+ {!onDelete && ( + + {scenario.category} + + )} +
+ + {scenario.title} + + + {scenario.description} + +
+ + +
{scenario.cbtFocus.map(focus => ( - + {focus} - + ))}
-
+ -
- - - - - - -
-
+ +
+ + + + + + +
+
+ ); }; diff --git a/components/ScenarioGenerator.tsx b/components/ScenarioGenerator.tsx index 7888326..3b68334 100644 --- a/components/ScenarioGenerator.tsx +++ b/components/ScenarioGenerator.tsx @@ -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 = ({ onGenerate, onCan const [error, setError] = useState(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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 (
-
+
{/* Header */} -
+

@@ -133,125 +144,133 @@ const ScenarioGenerator: React.FC = ({ onGenerate, onCan Construct an evidence-based clinical environment.

- +
-
- {/* Prompting Guide */} -
-
- -

Clinical Blueprint

-
-
-
-
- Character Role -
-

Define who the AI plays.

+ +
+ {/* Prompting Guide */} + +
+ +

Clinical Blueprint

-
-
- The Challenge +
+
+
+ Character Role +
+

Define who the AI plays.

-

Describe the social friction.

-
-
-
- CBT Focus +
+
+ The Challenge +
+

Describe the social friction.

+
+
+
+ CBT Focus +
+

Target specific distortions.

-

Target specific distortions.

-
-
+ - {/* Difficulty Selection */} -
- -
- {(['Beginner', 'Intermediate', 'Advanced'] as const).map((level) => ( - - ))} -
-
- - {/* Clinical Targets (Selection) */} -
- -
- {CBT_GLOSSARY.map(concept => { - const isSelected = selectedConcepts.includes(concept.term); - return ( - - ); - })} + + {level} + + ))} +
-
-
- -