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
+6497 -200

No files matched your search

+1
View File
@@ -0,0 +1 @@
VITE_GEMINI_API_KEY=""
+10 -1
View File
@@ -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/
+6
View File
@@ -0,0 +1,6 @@
{
"recommendations": [
"ms-azuretools.vscode-azurefunctions",
"ms-dotnettools.csharp"
]
}
+28 -23
View File
@@ -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',
@@ -175,21 +176,24 @@ const App: React.FC = () => {
<div className="flex items-center gap-2">
<Tooltip content="Adjust voice settings for audio sessions.">
<button
<Button
variant="outline"
size="icon"
className="rounded-xl text-slate-400 hover:text-slate-800"
onClick={() => setShowSettings(true)}
className="p-2.5 rounded-xl bg-slate-50 border border-slate-200 text-slate-400 hover:text-slate-800 transition-all"
>
<Settings size={20} />
</button>
</Button>
</Tooltip>
<Tooltip content="Explore the glossary of CBT distortions, techniques, and concepts.">
<button
<Button
variant="outline"
className="rounded-xl text-slate-600 hover:text-teal-700 hover:bg-teal-50"
onClick={() => setShowReference(true)}
className="flex items-center justify-center gap-2 bg-slate-50 text-slate-600 hover:text-teal-700 hover:bg-teal-50 px-5 py-2.5 rounded-xl border border-slate-200 transition-all font-bold text-sm"
>
<BookOpen size={18} />
<span>Reference Guide</span>
</button>
<BookOpen size={18} className="mr-2" />
Reference Guide
</Button>
</Tooltip>
</div>
</div>
@@ -227,29 +231,29 @@ const App: React.FC = () => {
</div>
<div className="flex gap-1">
{difficulties.map((diff) => (
<button
<Button
key={diff}
variant={selectedDifficulty === diff ? "secondary" : "ghost"}
size="sm"
onClick={() => setSelectedDifficulty(diff)}
className={`px-3 py-1.5 text-xs rounded-md transition-all whitespace-nowrap ${
selectedDifficulty === diff
? 'bg-teal-50 text-teal-700 font-semibold border border-teal-100'
: 'text-slate-500 hover:bg-slate-50 border border-transparent'
}`}
className={`h-7 px-3 text-xs ${selectedDifficulty === diff ? 'bg-teal-50 text-teal-700 font-semibold hover:bg-teal-100 hover:text-teal-800' : 'text-slate-500 hover:text-slate-700'}`}
>
{diff}
</button>
</Button>
))}
</div>
</div>
<Tooltip content="Generate a unique roleplay situation based on your specific needs using AI." position="bottom">
<button
<div>
<Button
onClick={() => setShowGenerator(true)}
className="flex items-center justify-center gap-2 bg-teal-600 text-white px-5 py-2.5 rounded-lg hover:bg-teal-700 transition-all font-bold text-sm shadow-md shadow-teal-100"
className="bg-teal-600 hover:bg-teal-700 text-white shadow-md shadow-teal-100 font-bold"
>
<Plus size={18} />
<span>Create Custom</span>
</button>
<Plus size={18} className="mr-2" />
Create Custom
</Button>
</div>
</Tooltip>
</div>
</div>
@@ -263,16 +267,17 @@ const App: React.FC = () => {
onDelete={scenario.id.startsWith('custom-') ? handleDeleteScenario : undefined}
/>
))}
<button
<Button
variant="outline"
onClick={() => setShowGenerator(true)}
className="flex flex-col items-center justify-center gap-4 p-6 rounded-xl border-2 border-dashed border-slate-200 text-slate-400 hover:border-teal-300 hover:text-teal-600 hover:bg-teal-50/50 transition-all h-full min-h-[250px] group"
className="flex flex-col items-center justify-center gap-4 p-6 rounded-xl border-2 border-dashed border-slate-200 text-slate-400 hover:border-teal-300 hover:text-teal-600 hover:bg-teal-50/50 transition-all h-full min-h-[250px] group whitespace-normal"
>
<div className="bg-slate-50 p-4 rounded-full group-hover:bg-white transition-colors"><Plus size={32} /></div>
<div className="text-center">
<span className="block font-bold text-sm uppercase tracking-widest mb-1">Custom Scenario</span>
<span className="text-xs font-light">Generate a specific roleplay session</span>
</div>
</button>
</Button>
</div>
</main>
+264
View File
@@ -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
+5
View File
@@ -0,0 +1,5 @@
{
"recommendations": [
"ms-azuretools.vscode-azurefunctions"
]
}
+116
View File
@@ -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<string> CBTFocus { get; set; } = new();
}
public class AnalysisFunction
{
private readonly ILogger _logger;
public AnalysisFunction(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger<AnalysisFunction>();
}
[Function("ClinicalAnalysis")]
public async Task<HttpResponseData> 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<AnalysisRequest>(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<string, Schema>
{
{ "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<string> { "Constructive", "Positive", "Neutral" } } }
},
Required = new List<string> { "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<Part> { 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;
}
}
}
}
+124
View File
@@ -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<ChatMessage> 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<ChatFunction>();
}
[Function("ChatProgression")]
public async Task<HttpResponseData> 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<ChatRequest>(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<Content>();
foreach (var msg in data.History)
{
contents.Add(new Content
{
Role = msg.Role,
Parts = new List<Part> { 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<Part> { 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;
}
}
}
}
+14
View File
@@ -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();
+9
View File
@@ -0,0 +1,9 @@
{
"profiles": {
"api": {
"commandName": "Project",
"commandLineArgs": "--port 7145",
"launchBrowser": false
}
}
}
+130
View File
@@ -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<string> 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<ScenarioFunction>();
}
[Function("GenerateScenario")]
public async Task<HttpResponseData> 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<ScenarioRequest>(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<string, Schema>
{
{ "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<string> { "Social Anxiety", "Workplace", "Relationships", "Self-Talk" } } },
{ "difficulty", new Schema { Type = TypeEnum.String, Enum = new List<string> { "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<string> { "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<Part> { 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;
}
}
}
}
+21
View File
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Google.GenAI" Version="1.3.0" />
<PackageReference Include="Microsoft.ApplicationInsights.WorkerService" Version="2.23.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker" Version="2.50.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.ApplicationInsights" Version="2.50.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" Version="2.1.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="2.0.6" />
</ItemGroup>
</Project>
+12
View File
@@ -0,0 +1,12 @@
{
"version": "2.0",
"logging": {
"applicationInsights": {
"samplingSettings": {
"isEnabled": true,
"excludedTypes": "Request"
},
"enableLiveMetricsFilters": true
}
}
}
+21
View File
@@ -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"
}
+29 -26
View File
@@ -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;
@@ -32,7 +37,7 @@ const CBTReference: React.FC<CBTReferenceProps> = ({ onClose }) => {
<div className="fixed inset-0 z-[60] flex items-center justify-center p-4 bg-slate-900/60 backdrop-blur-sm">
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-4xl h-[85vh] overflow-hidden flex flex-col animate-in fade-in zoom-in duration-300">
{/* Header */}
<div className="p-6 bg-slate-50 border-b border-slate-200 flex justify-between items-center">
<div className="p-6 bg-slate-50 border-b border-slate-200 flex justify-between items-center shrink-0">
<div className="flex items-center gap-3">
<div className="bg-teal-600 p-2 rounded-lg text-white">
<BookOpen size={24} />
@@ -42,56 +47,53 @@ const CBTReference: React.FC<CBTReferenceProps> = ({ onClose }) => {
<p className="text-xs text-slate-500 font-medium uppercase tracking-widest">Evidence-Based Concepts & Frameworks</p>
</div>
</div>
<button onClick={onClose} className="p-2 hover:bg-slate-200 rounded-xl transition-colors text-slate-400">
<Button variant="ghost" size="icon" onClick={onClose} className="text-slate-400 hover:bg-slate-200 rounded-xl">
<X size={20} />
</button>
</Button>
</div>
{/* Filters */}
<div className="p-6 space-y-4 border-b border-slate-100 bg-white">
<div className="p-6 space-y-4 border-b border-slate-100 bg-white shrink-0">
<div className="relative">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-400" size={18} />
<input
<Input
type="text"
placeholder="Search concepts, distortions, or definitions..."
value={search}
onChange={(e) => 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"
/>
</div>
<div className="flex flex-wrap gap-2">
{categories.map(cat => (
<button
<Button
key={cat}
variant={activeCategory === cat ? "default" : "outline"}
onClick={() => setActiveCategory(cat)}
className={`px-4 py-1.5 rounded-full text-xs font-bold transition-all border ${
activeCategory === cat
? 'bg-teal-600 text-white border-teal-600 shadow-md shadow-teal-100'
: 'bg-white text-slate-500 border-slate-200 hover:border-teal-200 hover:text-teal-600'
}`}
className={`rounded-full text-xs font-bold ${activeCategory === cat ? 'bg-teal-600 hover:bg-teal-700 shadow-md shadow-teal-100' : 'text-slate-500 hover:text-teal-600 hover:border-teal-200'}`}
>
{cat}
</button>
</Button>
))}
</div>
</div>
{/* Content */}
<div className="flex-grow overflow-y-auto p-6 scrollbar-hide bg-slate-50/30">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<ScrollArea className="flex-grow bg-slate-50/30 p-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pb-6">
{filtered.map((entry, idx) => (
<div
key={idx}
className="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm hover:border-teal-200 transition-all group"
>
<div className="flex justify-between items-start mb-3">
<h3 className="font-bold text-slate-800 group-hover:text-teal-700 transition-colors">{entry.term}</h3>
<div className="flex items-center gap-1.5 px-2 py-0.5 bg-slate-50 border border-slate-100 rounded text-[9px] font-black uppercase tracking-tighter text-slate-500">
<Card key={idx} className="hover:border-teal-200 transition-all group shadow-sm border-slate-200">
<CardHeader className="pb-2">
<div className="flex justify-between items-start mb-2">
<CardTitle className="text-base group-hover:text-teal-700 transition-colors">{entry.term}</CardTitle>
<Badge variant="outline" className="flex items-center gap-1.5 px-2 py-0.5 bg-slate-50 border-slate-100 rounded text-[9px] font-black uppercase tracking-tighter text-slate-500">
{getCategoryIcon(entry.category)}
{entry.category}
</Badge>
</div>
</div>
</CardHeader>
<CardContent>
<p className="text-sm text-slate-600 leading-relaxed mb-4 font-light">
{entry.definition}
</p>
@@ -101,7 +103,8 @@ const CBTReference: React.FC<CBTReferenceProps> = ({ onClose }) => {
<p className="text-xs text-teal-800 italic leading-relaxed">"{entry.example}"</p>
</div>
)}
</div>
</CardContent>
</Card>
))}
{filtered.length === 0 && (
@@ -113,10 +116,10 @@ const CBTReference: React.FC<CBTReferenceProps> = ({ onClose }) => {
</div>
)}
</div>
</div>
</ScrollArea>
{/* Footer */}
<div className="p-4 bg-white border-t border-slate-100 text-center">
<div className="p-4 bg-white border-t border-slate-100 text-center shrink-0">
<p className="text-[10px] text-slate-400 font-medium uppercase tracking-widest">
References compiled from clinical CBT standard frameworks.
</p>
+18 -8
View File
@@ -46,12 +46,22 @@ const ChatSession: React.FC<ChatSessionProps> = ({ scenario, onExit }) => {
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({
@@ -130,7 +140,7 @@ CBT Focus Areas: ${scenario.cbtFocus.join(', ')}`;
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 {
} catch (e) { console.error("Live analysis parse error", e); } finally {
setIsAnalyzingTurn(false);
}
});
@@ -226,7 +236,7 @@ SCENARIO CONTEXT: ${scenario.title} - ${scenario.description}`;
<div className="flex-grow flex flex-col bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
<div className="flex-grow overflow-y-auto p-6 space-y-6 scrollbar-hide">
{messages.map((msg) => (
<ChatMessage key={msg.id} message={msg} isLoading={isLoading && msg.id === messages[messages.length-1].id} />
<ChatMessage key={msg.id} message={msg} isLoading={isLoading && msg.id === messages[messages.length - 1].id} />
))}
<div ref={messagesEndRef} />
</div>
+48 -32
View File
@@ -1,8 +1,10 @@
import React from 'react';
import { Scenario } from '../types';
import { Mic, MessageSquare, Trash2 } from 'lucide-react';
import Tooltip from './Tooltip';
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './ui/card';
import { Button } from './ui/button';
import { Badge } from './ui/badge';
interface ScenarioCardProps {
scenario: Scenario;
@@ -11,12 +13,12 @@ interface ScenarioCardProps {
}
const ScenarioCard: React.FC<ScenarioCardProps> = ({ scenario, onSelect, onDelete }) => {
const getDifficultyColor = (diff: string) => {
const getDifficultyVariant = (diff: string): "default" | "secondary" | "destructive" | "outline" => {
switch (diff) {
case 'Beginner': return 'bg-teal-50 text-teal-800 border-teal-100';
case 'Intermediate': return 'bg-amber-50 text-amber-800 border-amber-100';
case 'Advanced': return 'bg-rose-50 text-rose-800 border-rose-100';
default: return 'bg-slate-50 text-slate-800 border-slate-100';
case 'Beginner': return 'default';
case 'Intermediate': return 'secondary';
case 'Advanced': return 'destructive';
default: return 'outline';
}
};
@@ -27,25 +29,30 @@ const ScenarioCard: React.FC<ScenarioCardProps> = ({ scenario, onSelect, onDelet
};
return (
<div className="bg-white rounded-xl p-6 shadow-sm border border-slate-200 hover:border-teal-200 hover:shadow-md transition-all flex flex-col h-full group relative">
<Card className="hover:border-teal-200 hover:shadow-md transition-all flex flex-col h-full group relative overflow-hidden">
{onDelete && (
<button
<Button
variant="ghost"
size="icon"
onClick={(e) => {
e.stopPropagation();
onDelete(scenario.id);
}}
className="absolute top-4 right-4 p-2 text-slate-300 hover:text-rose-500 hover:bg-rose-50 rounded-lg transition-all opacity-0 group-hover:opacity-100"
className="absolute top-2 right-2 text-slate-300 hover:text-rose-500 hover:bg-rose-50 opacity-0 group-hover:opacity-100 transition-all z-10"
title="Delete custom scenario"
>
<Trash2 size={16} />
</button>
</Button>
)}
<div className="flex justify-between items-start mb-4">
<Tooltip content={difficultyExplanations[scenario.difficulty]}>
<span className={`px-2.5 py-0.5 rounded-full text-[10px] font-bold uppercase tracking-wider border cursor-help ${getDifficultyColor(scenario.difficulty)}`}>
<CardHeader className="pb-2">
<div className="flex justify-between items-start mb-2">
<Tooltip content={difficultyExplanations[scenario.difficulty] || ""}>
<div className="cursor-help">
<Badge variant={getDifficultyVariant(scenario.difficulty)} className="text-[10px] uppercase tracking-wider">
{scenario.difficulty}
</span>
</Badge>
</div>
</Tooltip>
{!onDelete && (
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">
@@ -53,43 +60,52 @@ const ScenarioCard: React.FC<ScenarioCardProps> = ({ scenario, onSelect, onDelet
</span>
)}
</div>
<CardTitle className="text-lg group-hover:text-teal-700 transition-colors pr-6">
{scenario.title}
</CardTitle>
<CardDescription className="text-sm flex-grow leading-relaxed font-light">
{scenario.description}
</CardDescription>
</CardHeader>
<h3 className="text-lg font-bold text-slate-800 mb-2 group-hover:text-teal-700 transition-colors pr-6">{scenario.title}</h3>
<p className="text-slate-600 text-sm mb-5 flex-grow leading-relaxed font-light">{scenario.description}</p>
<div className="mb-6">
<div className="flex flex-wrap gap-1.5">
<CardContent className="flex-grow">
<div className="flex flex-wrap gap-1.5 mt-2">
{scenario.cbtFocus.map(focus => (
<Tooltip key={focus} content={`Specific CBT protocol targeting: ${focus}`}>
<span className="text-[10px] font-medium bg-slate-50 text-slate-500 px-2 py-0.5 rounded border border-slate-100 cursor-help flex items-center gap-1">
<Badge variant="outline" className="text-[10px] font-medium bg-slate-50 text-slate-500 cursor-help">
{focus}
</span>
</Badge>
</Tooltip>
))}
</div>
</div>
</CardContent>
<div className="grid grid-cols-2 gap-3 mt-auto">
<CardFooter className="pt-0">
<div className="grid grid-cols-2 gap-3 w-full">
<Tooltip content="Engage in a structured text-based roleplay with real-time feedback." position="bottom">
<button
<Button
variant="outline"
size="sm"
onClick={() => onSelect(scenario, 'text')}
className="w-full flex items-center justify-center gap-2 px-4 py-2 rounded-lg bg-slate-50 text-slate-600 hover:bg-slate-100 transition-all font-semibold text-xs border border-slate-200"
className="w-full text-slate-600 font-semibold text-xs"
>
<MessageSquare size={14} />
<MessageSquare size={14} className="mr-2" />
Chat
</button>
</Button>
</Tooltip>
<Tooltip content="Practice verbal emotional regulation and vocal expression in real-time." position="bottom">
<button
<Button
size="sm"
onClick={() => onSelect(scenario, 'voice')}
className="w-full flex items-center justify-center gap-2 px-4 py-2 rounded-lg bg-teal-600 text-white hover:bg-teal-700 transition-all font-semibold text-xs shadow-sm shadow-teal-100"
className="w-full bg-teal-600 hover:bg-teal-700 text-white font-semibold text-xs shadow-sm shadow-teal-100"
>
<Mic size={14} />
<Mic size={14} className="mr-2" />
Voice
</button>
</Button>
</Tooltip>
</div>
</div>
</CardFooter>
</Card>
);
};
+67 -48
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;
@@ -30,7 +34,7 @@ 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,
@@ -67,23 +71,25 @@ 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:
[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/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.).
- '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: [
@@ -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,16 +144,22 @@ 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]">
<ScrollArea className="flex-grow p-8 bg-[#FCFDFF]">
<div className="space-y-6 pb-4">
{/* 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">
<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>
@@ -166,7 +183,7 @@ const ScenarioGenerator: React.FC<ScenarioGeneratorProps> = ({ onGenerate, onCan
<p className="text-[10px] text-slate-500 leading-tight">Target specific distortions.</p>
</div>
</div>
</div>
</Card>
{/* Difficulty Selection */}
<div className="space-y-3">
@@ -175,18 +192,18 @@ const ScenarioGenerator: React.FC<ScenarioGeneratorProps> = ({ onGenerate, onCan
</label>
<div className="grid grid-cols-3 gap-2">
{(['Beginner', 'Intermediate', 'Advanced'] as const).map((level) => (
<button
<Button
key={level}
variant={selectedDifficulty === level ? "default" : "outline"}
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'
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'
}`}
>
<BarChart size={14} className={selectedDifficulty === level ? 'text-teal-600' : 'text-slate-300'} />
<BarChart size={14} className={selectedDifficulty === level ? 'text-teal-600' : 'text-slate-400'} />
{level}
</button>
</Button>
))}
</div>
</div>
@@ -196,18 +213,19 @@ const ScenarioGenerator: React.FC<ScenarioGeneratorProps> = ({ onGenerate, onCan
<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">
<div className="flex flex-wrap gap-2">
{CBT_GLOSSARY.map(concept => {
const isSelected = selectedConcepts.includes(concept.term);
return (
<button
<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 ${getConceptColor(concept.category, isSelected)}`}
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}
</button>
</Badge>
);
})}
</div>
@@ -217,11 +235,11 @@ const ScenarioGenerator: React.FC<ScenarioGeneratorProps> = ({ onGenerate, onCan
<label className="block text-[10px] font-black text-slate-400 uppercase tracking-widest mb-3">
Simulation Context / Custom Instructions
</label>
<textarea
<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"
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>
@@ -232,12 +250,14 @@ const ScenarioGenerator: React.FC<ScenarioGeneratorProps> = ({ onGenerate, onCan
{error}
</div>
)}
</div>
</ScrollArea>
<div className="flex justify-end pt-2">
<button
<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 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"
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 ? (
<>
@@ -250,8 +270,7 @@ const ScenarioGenerator: React.FC<ScenarioGeneratorProps> = ({ onGenerate, onCan
Initiate Build
</>
)}
</button>
</div>
</Button>
</div>
</div>
</div>
+23 -22
View File
@@ -1,9 +1,12 @@
import React from 'react';
import { UserSettings } from '../types';
import { X, Volume2, Check, Info, Mic2, Settings2 } from 'lucide-react';
import Tooltip from './Tooltip';
import AudioDeviceSelector from './AudioDeviceSelector';
import { Button } from './ui/button';
import { Card } from './ui/card';
import { Badge } from './ui/badge';
import { ScrollArea } from './ui/scroll-area';
interface SettingsModalProps {
settings: UserSettings;
@@ -41,13 +44,13 @@ const SettingsModal: React.FC<SettingsModalProps> = ({ settings, onUpdate, onClo
<p className="text-[10px] text-slate-400 font-bold uppercase tracking-widest">Configure your AI clinical partner</p>
</div>
</div>
<button onClick={onClose} className="p-2 hover:bg-slate-200 rounded-xl transition-colors text-slate-400">
<Button variant="ghost" size="icon" onClick={onClose} className="text-slate-400 hover:bg-slate-200 rounded-xl">
<X size={20} />
</button>
</Button>
</div>
<div className="p-8 space-y-8 overflow-y-auto">
<section className="space-y-4">
<ScrollArea className="p-8 space-y-8 flex-grow">
<section className="space-y-4 pb-6">
<div className="flex items-center justify-between">
<h3 className="text-xs font-black text-slate-400 uppercase tracking-widest flex items-center gap-2">
<Mic2 size={14} className="text-teal-600" />
@@ -60,42 +63,40 @@ const SettingsModal: React.FC<SettingsModalProps> = ({ settings, onUpdate, onClo
<div className="grid grid-cols-1 gap-3">
{VOICE_OPTIONS.map((voice) => (
<button
<Card
key={voice.id}
onClick={() => onUpdate({ ...settings, voiceName: voice.id, genderPreference: voice.gender, vocalStyle: voice.style })}
className={`flex items-center justify-between p-4 rounded-2xl border-2 transition-all text-left ${
settings.voiceName === voice.id
className={`flex flex-row items-center justify-between p-4 cursor-pointer transition-all ${settings.voiceName === voice.id
? 'bg-teal-50 border-teal-500 shadow-md shadow-teal-100'
: 'bg-white border-slate-100 hover:border-slate-200'
: 'bg-white border-slate-200 hover:border-teal-200'
}`}
>
<div className="flex items-center gap-4">
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${
settings.voiceName === voice.id ? 'bg-teal-600 text-white' : 'bg-slate-100 text-slate-400'
<div className={`w-10 h-10 rounded-full flex items-center shrink-0 justify-center ${settings.voiceName === voice.id ? 'bg-teal-600 text-white' : 'bg-slate-100 text-slate-400'
}`}>
<Volume2 size={18} />
</div>
<div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-2 mb-1">
<span className="font-bold text-slate-800">{voice.label}</span>
<span className="text-[9px] font-black uppercase text-slate-400 bg-slate-100 px-1.5 py-0.5 rounded">
<Badge variant="secondary" className="text-[9px] font-black uppercase text-slate-500 rounded">
{voice.gender} • {voice.style}
</span>
</Badge>
</div>
<p className="text-xs text-slate-500 font-light mt-0.5">{voice.description}</p>
</div>
</div>
{settings.voiceName === voice.id && (
<div className="bg-teal-600 text-white p-1 rounded-full">
<div className="bg-teal-600 text-white p-1 rounded-full shrink-0 ml-2">
<Check size={14} />
</div>
)}
</button>
</Card>
))}
</div>
</section>
<section className="space-y-4">
<section className="space-y-4 pb-6">
<div className="flex items-center justify-between">
<h3 className="text-xs font-black text-slate-400 uppercase tracking-widest flex items-center gap-2">
<Settings2 size={14} className="text-teal-600" />
@@ -109,7 +110,7 @@ const SettingsModal: React.FC<SettingsModalProps> = ({ settings, onUpdate, onClo
/>
</section>
<div className="bg-amber-50 border border-amber-100 rounded-2xl p-4 flex gap-3">
<div className="bg-amber-50 border border-amber-100 rounded-2xl p-4 flex gap-3 mb-6">
<div className="text-amber-600 mt-0.5">
<Info size={18} />
</div>
@@ -117,15 +118,15 @@ const SettingsModal: React.FC<SettingsModalProps> = ({ settings, onUpdate, onClo
These settings apply to <strong>Voice Mode</strong> interactions only. Text-based roleplay personas are dynamically generated based on each specific scenario.
</p>
</div>
</div>
</ScrollArea>
<div className="p-6 bg-slate-50 border-t border-slate-200 flex justify-end flex-shrink-0">
<button
<Button
onClick={onClose}
className="bg-slate-800 text-white px-8 py-3 rounded-2xl font-bold hover:bg-slate-900 shadow-lg shadow-slate-200 transition-all uppercase tracking-widest text-[10px]"
className="bg-slate-800 text-white px-8 h-12 rounded-2xl font-bold hover:bg-slate-900 shadow-lg shadow-slate-200 transition-all uppercase tracking-widest text-[10px]"
>
Save Preferences
</button>
</Button>
</div>
</div>
</div>
+19 -13
View File
@@ -5,6 +5,7 @@ import { GoogleGenAI } from '@google/genai';
import { SYSTEM_SUMMARY_ANALYSIS_PROMPT, CBT_ANALYSIS_SCHEMA } from '../constants';
import { useLiveSession } from '../hooks/useLiveSession';
import { AlertCircle, Settings2, X, BrainCircuit, Zap, Info } from 'lucide-react';
import { Button } from './ui/button';
// Sub-components
import AudioVisualizer from './AudioVisualizer';
@@ -132,13 +133,13 @@ SCENARIO CONTEXT: ${scenario.title} - ${scenario.description}`;
</p>
</div>
<button
<Button
onClick={handleStart}
className="bg-teal-600 hover:bg-teal-500 text-white px-12 py-4 rounded-2xl font-bold transition-all shadow-xl shadow-teal-900/20 uppercase tracking-widest text-sm flex items-center gap-3"
className="bg-teal-600 hover:bg-teal-500 text-white px-12 py-7 rounded-2xl font-bold transition-all shadow-xl shadow-teal-900/20 uppercase tracking-widest text-sm"
>
<Zap size={18} fill="currentColor" />
<Zap size={18} fill="currentColor" className="mr-3" />
Start Session
</button>
</Button>
</div>
) : (
<>
@@ -151,12 +152,14 @@ SCENARIO CONTEXT: ${scenario.title} - ${scenario.description}`;
<div className="flex-grow">
<h4 className="text-sm font-bold">No Audio Detected</h4>
<p className="text-xs text-white/80 mt-1 leading-relaxed">We can't hear you. Please check your microphone or select a different input device.</p>
<button
<Button
variant="secondary"
size="sm"
onClick={() => setShowDeviceSelector(true)}
className="mt-3 bg-white text-amber-600 px-4 py-1.5 rounded-lg text-[10px] font-black uppercase tracking-widest hover:bg-amber-50 transition-colors"
className="mt-3 bg-white text-amber-600 px-4 py-1.5 rounded-lg text-[10px] font-black uppercase tracking-widest hover:bg-amber-50"
>
Change Input Device
</button>
</Button>
</div>
</div>
</div>
@@ -164,13 +167,11 @@ SCENARIO CONTEXT: ${scenario.title} - ${scenario.description}`;
{/* Speaker Indicator Badge */}
<div className="mb-8 flex flex-col items-center">
<div className={`px-4 py-1.5 rounded-full border transition-all duration-500 flex items-center gap-2 ${
isAiSpeaking
<div className={`px-4 py-1.5 rounded-full border transition-all duration-500 flex items-center gap-2 ${isAiSpeaking
? 'bg-blue-500/20 border-blue-400/30 text-blue-300'
: (isUserSpeaking ? 'bg-teal-500/20 border-teal-400/30 text-teal-300' : 'bg-white/5 border-white/10 text-white/40')
}`}>
<div className={`w-2 h-2 rounded-full ${
isAiSpeaking ? 'bg-blue-400 animate-pulse' : (isUserSpeaking ? 'bg-teal-400 animate-pulse' : 'bg-white/20')
<div className={`w-2 h-2 rounded-full ${isAiSpeaking ? 'bg-blue-400 animate-pulse' : (isUserSpeaking ? 'bg-teal-400 animate-pulse' : 'bg-white/20')
}`} />
<span className="text-[10px] font-black uppercase tracking-widest">
{isAiSpeaking ? 'AI Speaking' : (isUserSpeaking ? 'You Speaking' : 'Waiting...')}
@@ -201,9 +202,14 @@ SCENARIO CONTEXT: ${scenario.title} - ${scenario.description}`;
</div>
<h3 className="font-bold text-slate-800">Audio Input</h3>
</div>
<button onClick={() => setShowDeviceSelector(false)} className="p-2 hover:bg-slate-100 rounded-xl transition-colors text-slate-400">
<Button
variant="ghost"
size="icon"
onClick={() => setShowDeviceSelector(false)}
className="text-slate-400 hover:bg-slate-100 rounded-xl"
>
<X size={20} />
</button>
</Button>
</div>
<AudioDeviceSelector
+13 -9
View File
@@ -1,6 +1,6 @@
import React from 'react';
import { Mic, MicOff, MessageSquare } from 'lucide-react';
import { Button } from '../ui/button';
interface VoiceControlsProps {
isMuted: boolean;
@@ -16,18 +16,22 @@ const VoiceControls: React.FC<VoiceControlsProps> = ({
onToggleTranscript
}) => (
<div className="p-8 flex justify-center gap-6 relative z-10 bg-black/20 backdrop-blur-md">
<button
<Button
variant={isMuted ? "destructive" : "secondary"}
size="icon"
className={`h-16 w-16 rounded-full ${!isMuted && 'bg-white/10 text-white hover:bg-white/20'}`}
onClick={onToggleMute}
className={`p-5 rounded-full transition-all ${isMuted ? 'bg-rose-600 text-white' : 'bg-white/10 text-white hover:bg-white/20'}`}
>
{isMuted ? <MicOff size={24} /> : <Mic size={24} />}
</button>
<button
{isMuted ? <MicOff className="!w-6 !h-6" /> : <Mic className="!w-6 !h-6" />}
</Button>
<Button
variant="secondary"
size="icon"
className={`h-16 w-16 rounded-full ${showTranscript ? 'bg-teal-600 text-white hover:bg-teal-700' : 'bg-white/10 text-white hover:bg-white/20'}`}
onClick={onToggleTranscript}
className={`p-5 rounded-full transition-all ${showTranscript ? 'bg-teal-600 text-white' : 'bg-white/10 text-white hover:bg-white/20'}`}
>
<MessageSquare size={24} />
</button>
<MessageSquare className="!w-6 !h-6" />
</Button>
</div>
);
+13 -5
View File
@@ -1,6 +1,6 @@
import React from 'react';
import { PhoneOff, Flag } from 'lucide-react';
import { Button } from '../ui/button';
interface VoiceHeaderProps {
title: string;
@@ -11,17 +11,25 @@ interface VoiceHeaderProps {
const VoiceHeader: React.FC<VoiceHeaderProps> = ({ title, onExit, onFinish }) => (
<div className="p-8 flex justify-between items-center relative z-10">
<div className="flex items-center gap-4">
<button onClick={onExit} className="p-3 bg-white/5 hover:bg-white/10 rounded-2xl text-white/50 hover:text-white transition-all">
<Button
variant="ghost"
size="icon"
onClick={onExit}
className="h-12 w-12 bg-white/5 hover:bg-white/10 rounded-2xl text-white/50 hover:text-white transition-all"
>
<PhoneOff size={20} />
</button>
</Button>
<div>
<h2 className="text-white font-bold text-lg">{title}</h2>
<p className="text-teal-400 text-[10px] font-black uppercase tracking-widest">Active Roleplay Session</p>
</div>
</div>
<button onClick={onFinish} className="bg-teal-600 hover:bg-teal-700 text-white px-6 py-2.5 rounded-xl text-[10px] font-black uppercase tracking-widest transition-all shadow-lg shadow-teal-900/40 flex items-center gap-2">
<Button
onClick={onFinish}
className="bg-teal-600 hover:bg-teal-700 text-white px-6 py-5 rounded-xl text-[10px] font-black uppercase tracking-widest transition-all shadow-lg shadow-teal-900/40 flex items-center gap-2"
>
<Flag size={14} /> Finish
</button>
</Button>
</div>
);
+36
View File
@@ -0,0 +1,36 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }
+57
View File
@@ -0,0 +1,57 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }
+76
View File
@@ -0,0 +1,76 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-xl border bg-card text-card-foreground shadow",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
+120
View File
@@ -0,0 +1,120 @@
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogClose,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}
+22
View File
@@ -0,0 +1,22 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }
+26
View File
@@ -0,0 +1,26 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }
+46
View File
@@ -0,0 +1,46 @@
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
export { ScrollArea, ScrollBar }
+157
View File
@@ -0,0 +1,157 @@
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-select-content-transform-origin]",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}
+31
View File
@@ -0,0 +1,31 @@
"use client"
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
{...props}
/>
)
)
Separator.displayName = SeparatorPrimitive.Root.displayName
export { Separator }
+27
View File
@@ -0,0 +1,27 @@
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
export { Switch }
+55
View File
@@ -0,0 +1,55 @@
"use client"
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }
+22
View File
@@ -0,0 +1,22 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Textarea = React.forwardRef<
HTMLTextAreaElement,
React.ComponentProps<"textarea">
>(({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
{...props}
/>
)
})
Textarea.displayName = "Textarea"
export { Textarea }
+30
View File
@@ -0,0 +1,30 @@
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-tooltip-content-transform-origin]",
className
)}
{...props}
/>
</TooltipPrimitive.Portal>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
+24 -8
View File
@@ -298,19 +298,35 @@ export const CBT_ANALYSIS_SCHEMA = {
};
export const SYSTEM_ANALYSIS_PROMPT = `
You are an expert Clinical CBT Supervisor. Your task is to provide immediate, high-precision cognitive analysis of a patient's latest roleplay interaction.
[CONTEXT]
You are an expert Clinical CBT Supervisor. The user is actively participating in a cognitive behavioral therapy scenario and has just submitted a response to the roleplay partner.
CORE MISSION:
Identify cognitive distortions, evaluate the adaptiveness of the user's response, and provide a single powerful reframing thought.
[TASK]
Provide immediate, high-precision cognitive analysis of the user's latest interaction.
Maintain a neutral, clinical, and encouraging tone. Focus purely on the cognitive and behavioral mechanics of the latest user turn.
[CONSTRAINTS]
- Identify specific cognitive distortions if present.
- Evaluate the adaptiveness of the user's response objectively.
- Maintain a neutral, clinical, and encouraging tone.
- Focus purely on the cognitive and behavioral mechanics.
[OUTPUT REQUIREMENTS]
Return ONLY a valid JSON object matching the CBT_ANALYSIS_SCHEMA structure. Include the identified distortions, an adaptiveness evaluation, evidence for this evaluation, a numerical rating, and suggestions for an alternative thought and behavioral step.
`;
export const SYSTEM_SUMMARY_ANALYSIS_PROMPT = `
You are a Senior Clinical CBT Supervisor performing a post-session synthesis on a full roleplay transcript.
[CONTEXT]
You are a Senior Clinical CBT Supervisor performing a post-session synthesis on a completed roleplay transcript.
CORE MISSION:
Synthesize the entire interaction to identify recurring cognitive patterns, evaluate overall progress, and assign "homework" for behavioral activation.
[TASK]
Synthesize the entire interaction to identify recurring cognitive patterns, evaluate overall progress, and provide structured feedback.
Analyze the transcript for patterns, not just individual messages. Focus on how the user handles the "Internal CBT Logic" of the specific scenario.
[CONSTRAINTS]
- Analyze the transcript holistically for patterns, not just individual messages.
- Focus on how the user handled the specific scenario's "Internal CBT Logic".
- Propose actionable "homework" for behavioral activation.
- Maintain a professional, encouraging, and highly clinical tone.
[OUTPUT REQUIREMENTS]
Return ONLY a valid JSON object matching the CBT_ANALYSIS_SCHEMA structure. Adapt the schema elements to reflect the full session (e.g., summarize distortions across the entire transcript, provide an aggregate rating, and define macro-level homework suggestions).
`;
+69
View File
@@ -0,0 +1,69 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
+6
View File
@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
+212
View File
@@ -0,0 +1,212 @@
Advanced Methodologies for Variable-Effect and Structured Prompting in Generative Image Synthesis
The emergence of generative artificial intelligence as a primary tool in visual production has necessitated a transition from intuitive, experimental interactions to disciplined, architectural frameworks.[1] Within professional domains such as architectural visualization and stylistic digital illustration, the prompt is no longer viewed as a simple textual request but as a deliberate act of design that sets the coordinates for a predictive algorithm.[2] This evolution is underpinned by the realization that large language models (LLMs) and diffusion models operate not through semantic understanding, but through complex statistical predictions where the specificity and structure of input directly determine the contraction of the response space.[2, 3] To achieve nuanced image modifications, such as the localized replacement of architectural materials or the conversion of photographs into specific artistic styles with controlled line weights, practitioners must master the interplay between structured linguistic prompts, variable-based dynamic systems, and pixel-level conditioning adapters.[4, 5, 6]
Theoretical Foundations of Structured Prompting
Effective communication with generative AI models requires a specialized form of linguistic engineering that translates human creative vision into instructions that are statistically coherent for the model’s training data.[3] At its core, an AI image prompt serves as a bridge between imagination and execution, synthesizing patterns from millions of images to generate novel outputs.[3] Because models interpret instructions literally and probabilistically, the absence of structure leads to redundant or off-target results.[2, 3] The foundational structure of a professional prompt is often conceptualized as a formulaic assembly of subject, style, composition, lighting, and technical specifications.[3]
Prompt Component
Structural Function
Strategic Examples
Subject Anchor
Primary focus and semantic core
Modern glass office tower, woman in business suit
Artistic Media
Defines the medium and aesthetic style
Impressionist oil painting, technical blueprint, 3D render
Compositional Framing
Spatial arrangement and camera logic
Bird’s eye view, shallow depth of field, over-the-shoulder
Lighting Conditions
Modulates depth, mood, and realism
Soft natural window light, golden hour, neon studio lighting
Chromatic Palette
Ensures tonal and visual consistency
Muted earth tones, vibrant primary colors, monochromatic blue
Technical Parameters
Signals desired fidelity and polish
8k resolution, sharp focus, professional photography
The subject serves as the anchor of the image, while stylistic directives dramatically influence the model's interpretation of that subject.[3] For instance, specifying "professional corporate photography" for a business portrait creates a different probabilistic trajectory than "pencil sketch" for the same subject.[3] This structured approach defines the perimeter within which the system operates, providing context and identifying the role of the virtual recipient to trigger predictable behavior.[2]
Beyond basic descriptive structure, the field has advanced toward complex prompt engineering techniques designed for precise tasks.[7] Direct instructions are commands that tell the AI exactly what to produce, ideal for straightforward objectives.[7] Task-specific instructions, however, leverage advanced methods like few-shot prompting—where the user provides examples of desired input-output pairs—to guide the model toward specific formats or behaviors.[7, 8, 9] This method is particularly effective when instructing a model to output data in structured formats like JSON or XML for automated workflows.[9, 10]
Analytical Benchmarking of Prompt Architectures
As models like GPT-4o, Claude, and Gemini grow in sophistication, the choice of prompt format significantly impacts accuracy, token consumption, and processing latency.[11, 12] Recent research involving randomized datasets across scenarios such as medical records and commercial receipts has demonstrated clear trade-offs between expressive hierarchical formats and lightweight structures.[11, 12] Hierarchical formats such as JSON and YAML boost accuracy by enabling strict nesting and schema validation, which is critical for complex data where semantic clarity is paramount.[11, 12]
Format Style
Accuracy (Avg)
Token Efficiency
Latency (Seconds)
Optimal Application
JSON
95%
Low
6.5
Complex hierarchical data extraction
YAML
92%
Moderate
5.2
Semi-structured data, user-facing contexts
CSV
88%
High
4.1
Flat transactional data, tabular lists
Simple Prefix
85%
Highest
3.8
Real-time, cost-sensitive applications
These findings suggest that for applications requiring high structural fidelity—such as generating architectural parameters or material specifications—JSON remains the superior default, despite its higher token cost.[11, 13] Conversely, for real-time applications like processing e-commerce receipts, lightweight options like CSV allow for faster processing without significant precision loss.[11, 12] It has also been observed that all current models struggle with narrative-style unstructured data, such as personal stories, where accuracy drops significantly regardless of prompt style, underscoring the need for tailored prompts for specific data types.[13]
Methodologies for Variable-Based Dynamic Prompting
Professional workflows in 2026 rely heavily on dynamic prompts that adapt to context and user input through variables and placeholders.[14] Instead of crafting static, unchanging instructions, practitioners use templates that adjust in real time based on external data or conversation history.[14] This methodology is essential for scaling AI-powered applications, as it reduces repetitive coding and ensures consistency across diverse scenarios.[9, 14]
In the context of image synthesis, dynamic prompting utilizes a specific syntax to automate the generation of multiple variants.[5, 15] The most common syntax involves curly braces {} and the pipe character | to define options (e.g., {option1|option2}).[5, 16] This allows a single prompt to generate a wide range of outputs, such as testing different architectural materials or character traits without manual intervention.[5]
Combinatorial vs. Random Sampling
Dynamic systems offer two primary modes of generation: combinatorial and random.[5, 16, 17] Combinatorial prompts iterate through every possible combination within the prompt template.[5, 16] For example, a prompt such as "I {love|hate} {New York|Chicago}" will exhaustively produce four unique prompts: "I love New York," "I love Chicago," "I hate New York," and "I hate Chicago".[5, 17] This is an invaluable tool for artists searching for interesting combinations of styles and materials.[15]
Random sampling, conversely, selects one option at random for each generation.[5, 16] Advanced implementations allow for probabilistic weighting within the curly braces, such as {@75::wood|25::stone}, which assigns a 75% probability to the "wood" material.[5] This introduces controlled randomness, enabling the discovery of unexpected but statistically likely aesthetic combinations.[5]
Immediate vs. Non-Immediate Variable Evaluation
A critical distinction in professional variable-based prompting is the timing of variable evaluation.[17] In standard dynamic syntax, variables can be evaluated either once for the entire prompt or every time they are referenced.[17]
Immediate Evaluation: By adding an exclamation mark ! before the value (e.g., ${material=!{oak|pine}}), the system forces an immediate selection that remains fixed throughout the generation.[17] This ensures internal consistency, such as ensuring that if a "deck" is specified as "oak" in the beginning of a prompt, all subsequent references to the "surface" or "texture" are also treated as "oak".[17]
Non-Immediate Evaluation: If the exclamation mark is omitted, the variable is re-evaluated at every instance.[17] While this can lead to logical inconsistencies—such as a character wearing "winter shirts" with "summer trousers"—it is useful for generating diverse, randomized elements in background scenery or crowd shots where uniformity is not desired.[17]
Localized Image Modification and Material Transformation
The modification of specific elements within an image, such as changing the material on a wooden deck to stone or metal, requires methodologies that bridge the gap between textual instruction and spatial geometry.[4, 18] Traditional text-to-image models often struggle with precise localized control, frequently altering the entire composition when a change is requested.[19] To address this, current workflows utilize inpainting, ControlNet, and specialized material-swapping models.[6, 18, 20]
Inpainting and Denoising Logic
Inpainting is the fundamental task of filling masked regions of an image with new content while leaving the rest of the image untouched.[20, 21] The methodology involves creating a mask—a black and white overlay where white represents the area to be modified—and providing a prompt that describes the desired new material.[20, 21] The success of inpainting is largely governed by the "Denoising Strength" parameter.[20, 22]
Low Denoising Strength (0.1 - 0.4): The model makes minimal changes, preserving the original texture and structure of the area.[20, 23] This is ideal for subtle corrections or adding minor details.[20]
Moderate Denoising Strength (0.5 - 0.75): The model retains the general shape of the masked object but replaces its texture and color.[20] This is the standard setting for material swaps, such as converting a wooden deck to a marble surface while keeping the perspective and layout of the deck boards.[20, 21]
High Denoising Strength (0.8 - 1.0): The model completely ignores the original content of the masked area, regenerating it from scratch based purely on the prompt.[20] This is used for removing objects or adding entirely new elements that were not present in the original scene.[20]
Exemplar-Based Material Transfer (MatSwap)
For high-fidelity architectural visualization, the industry has moved toward MatSwap, an exemplar-based method that transfers materials to designated surfaces realistically.[18, 24] Unlike traditional inpainting, which relies on textual descriptions of materials, MatSwap directly learns the relationship between an input material image—observed on a flat surface—and its appearance within the 3D geometry and lighting of a specific scene.[18, 24] This removes the need for explicit UV mapping, relying instead on a light- and geometry-aware diffusion model that seamlessly integrates the exemplar material into the photograph.[18, 24] This method is particularly effective when textually describing complex patterns or appearances is non-trivial.[24]
Structural Control and Stylistic Synthesis
The conversion of images into specific artistic styles, such as turning a photograph into a cartoon with controlled line weights, is achieved through structural conditioning tools like ControlNet.[6, 19] ControlNet enhances stable diffusion models by adding extra input conditions beyond the text prompt, such as edge detection, depth maps, or human pose skeletons.[6, 19]
Line Weight Management via ControlNet
To convert an image to a cartoon style with specific line weights, the Line Art preprocessor is the primary methodology.[6] This preprocessor extracts the outlines from an image and feeds them into the diffusion model as a control map.[6]
Line Art Preprocessor
Stylistic Result
Line Weight Characteristic
Line Art Anime
Classic Japanese animation style
Thin, uniform lines with minimal shading
Line Art Realistic
Photographic structural preservation
Variable line thickness based on lighting
Line Art Coarse
Bold illustrative style
Heavy, prominent outlines suitable for graphic novels
Scribble HED
Hand-drawn artistic sketch
Fluid, organic lines with varied pressure
Control over the "weight" of these lines can be further refined through the "Control Weight" parameter within the ControlNet node.[6] Setting the control weight to a lower value (e.g., 0.6) allows the model more freedom to deviate from the original lines, which can result in a more "painterly" or "stylized" cartoon look, whereas a weight of 1.0 strictly adheres to the original geometry.[6] ControlNet++ further enhances this by optimizing cycle consistency, ensuring the final generated image maintains high fidelity to the original input control map.[25]
Style and Character Reference Parameters
In platforms like Midjourney, stylistic modification is governed by specialized parameters that manage "visual vibe" and "identity".[26, 27] The Style Reference (--sref) parameter allows users to use a URL or a numeric code to apply the aesthetic of one image to another without copying specific objects.[26, 27] The intensity of this style transfer is controlled by the Style Weight (--sw) parameter, which ranges from 0 (no influence) to 1000 (maximum influence), with a default of 100.[26, 27]
For character consistency across different styles (e.g., converting a real person into a cartoon), the Character/Omni Reference (--cref) is employed.[26, 28] This parameter instructs the model to recognize facial features and clothing from a reference image and apply them to new scenes.[28] The Character Weight (--cw) parameter acts as a dial for detail; a value of 100 captures everything including hair and clothes, while a value of 0 focuses exclusively on the face, allowing for easy stylistic or outfit modifications while maintaining the character's identity.[28, 29]
Mathematical Foundations of the Diffusion Process
The modification of images through structured prompts is mathematically governed by the denoising process within a latent space.[21] Stable Diffusion models operate as Latent Diffusion Models (LDMs), where high-dimensional images are compressed into a smaller latent space via a Variational Autoencoder (VAE).[21] The core of this process is the Markovian chain of the diffusion forward process, which gradually adds Gaussian noise to the data [21, 25]:
x
t
​
=
α
ˉ
t
​
​
x
0
​
+
1−
α
ˉ
t
​
​
ϵ,ϵ∼N(0,I)
In this equation, x
0
​
represents the original image data, x
t
​
is the noisy latent at timestep t, and ϵ is the noise map.[25] The goal of the model during inference is to learn a denoising process that estimates and subtracts this noise to restore x
0
​
.[21] Structured prompting and ControlNet act as conditioning mechanisms c
t
​
and c
v
​
respectively, steering the U-Net architecture as it predicts the noise at each step.[21, 25] This ensures that as the model "dreams" an image out of random noise, it follows the specific coordinates defined by the prompt's subject, style, and structural constraints.[2, 25]
Advanced Professional Workflows and Implementation
The maturity of prompt engineering as a discipline is marked by the development of rigorous patterns and toolchains.[1, 30] Professional analysts and architects now employ the KERNEL pattern to ensure that prompts are predictable and verifiable.[30]
Easy to Verify: Prompts must include clear success criteria. Instead of asking for an "engaging style," a professional prompt might specify "include 3 distinct code examples" or "use bold lines of 2px width".[30]
Reproducible Results: temporal references like "current trends" are avoided in favor of specific model versions and fixed seeds, ensuring the same prompt produces the same result over time.[15, 30]
Narrow Scope: Complex tasks are split into multiple single-goal prompts, as multi-goal prompts show significantly lower satisfaction rates.[30]
Logical Structure: Every prompt is formatted with context, task, constraints, and specific output requirements.[30]
Architectural Visualization Workflows
In the architectural sector, AI visualization engines like Veras and D5 Render are integrated directly into CAD and BIM software.[4, 31] These tools maintain live data connections that support the evaluation of materials and lighting in real time.[31] A typical workflow for a material swap on an architectural deck involves:
Initial Render: Exporting a massing model or sketch from Revit or Rhino.[4, 32]
AI Refinement: Using Veras to apply a structured prompt defining lighting (e.g., "overcast afternoon") and broad materials.[4, 31]
Localized Editing: Entering an AI editing mode to mask the deck and swap materials (e.g., "weathered cedar wood" to "dark slate tiles") without re-rendering the entire scene.[4, 33]
Parametric Iteration: Using Python scripts or Grasshopper definitions generated by LLMs to adjust structural elements like window sizes or deck heights dynamically.[32]
This methodology allows architects to modulate the level of detail to match each design phase—using abstract massings for early exploration and photorealistic simulations for final client presentations.[31] This shift pushes architects to demonstrate value through authored design and contextual reasoning, while AI handles the redundant translation layers between concept and delivery.[31]
Stylistic Transformation in ComfyUI
For complex stylistic modifications, node-based GUIs like ComfyUI provide the highest level of granular control.[22] By chaining together different blocks (nodes) for prompt encoding, sampling, and upscaling, artists can construct a precise workflow for style transfer.[22]
A cartoonization workflow in ComfyUI typically includes:
Load Image: Uploading the target photograph.[22]
CLIP Text Encode: Entering the structured style prompt (e.g., "80s retro anime style, watercolor medium").[22, 34]
ControlNet Stacker: Applying a Line Art preprocessor to maintain the structure of the original image while the model changes the aesthetic.[6]
IP-Adapter: Using a reference cartoon image to "inject" the specific color palette and line style into the generation.[35, 36]
Prompt Weighting: Using numerical weighting (e.g., (thick lines:1.3)) to emphasize the desired illustrative characteristics.[37]
This modular approach allows for "single image fine-tuning," where a single reference image can steer the entire generation process more effectively than text alone.[38]
The Future of Analytical Partnerships in AI
The next phase of generative AI is expected to move beyond isolated image generation toward embedded analytical partnerships.[31] Instead of functioning as separate tools, AI engines will be integrated within core authoring environments, maintaining live data connections to performance monitoring and compliance throughout a project's lifecycle.[31] In this context, the prompt evolves from a creative instruction to a technical parameter that governs real-world data.[31, 32]
While AI can automate routine setup and establish a baseline look almost immediately, the "human judgment" multiplier remains the defining factor in professional outputs.[1, 31] Without expert oversight, polished AI outputs risk being misaligned, incomplete, or physically impossible.[31, 33] The frameworks emerging in 2026 represent a maturing discipline where systematic approaches to probabilistic systems make the practice of AI image modification more teachable, transferable, and reliable, yet they do not eliminate the need for the skilled human judgment required to navigate inherent uncertainty.[1]
Integration of Structured Data for Prompt Optimization
Professional use cases often involve the extraction and categorization of requirements before a visual prompt is even drafted.[39] In systems architecture, for instance, a Tradeoff Matrix Generator or a System Design Reasoner might be used to define the constraints of a project.[39] When applied to image modification, this ensures that every element of the visual output is grounded in functional and non-functional requirements.[39]
Requirement Category
Visual Prompt Translation
Impact on Image Modification
Functional
"Modern kitchen with marble island"
Subject and material specification
Non-Functional
"99.9% uptime" (translated to visual stability)
Compositional clarity and lack of artifacts
Compliance
"SOC 2 and GDPR" (translated to privacy filters)
Removal of identifying features or brands
Scalability
"Support for 100K users"
High resolution and detailed upscaling
By prioritizing security, scalability, and compliance within the prompt structure, architects and designers can ensure that their visualizations are not only aesthetically pleasing but also build-ready and legally sound.[33, 39] This holistic approach to prompt design ensures that generative AI becomes a reliable component of the professional toolchain, providing repeatable workflows and consistent, high-quality results.[30, 39]
Quantitative Performance Metrics across LLM Models
The effectiveness of these methodologies is further validated through rigorous Charakterization of response times and success rates across different query complexities.[40] Simple queries involving the direct reading of a single value typically exhibit low latency, while complex queries requiring multilevel reasoning and data aggregation from multiple systems (ERP, SCADA, field) show higher variance.[40]
Query Complexity
Success Rate
Avg Latency (s)
Thermal Stabilization
Simple
98%
1.2
Stable (45°C)
Moderate
92%
3.5
Equilibrium (55°C)
Complex
84%
7.8
Throttling Risk (80°C)
For systems operating under sustained cognitive loads—such as generating a batch of 64 images with different variable combinations—maintaining thermal stability and communication stack efficiency is paramount.[40, 41] Professional setups often require monitoring CPU temperatures and utilizing automated latency measurements to ensure that the AI infrastructure remains sustainable during prolonged operational cycles.[40] This technical oversight ensures that the creative potential of variable-based prompts is supported by a robust and reliable hardware environment.[40]
Summary of Best Practices for Image Modification
In summary, the transition from intuitive "prompting" to "structured engineering" is the primary driver of success in modern image modification.[1, 2] To achieve specific goals like material swaps or cartoonization, practitioners should:
Define a Clear Subject Anchor: Be specific about the object being modified to anchor the model’s prediction.[3]
Utilize Structural Conditioning (ControlNet): Preserve the geometry of the original image using line art or depth maps.[6]
Employ Variable Weights for Detail Control: Use in-line numerical weighting to prioritize stylistic elements like line weight.[6, 37]
Leverage Dynamic Syntax for Scaling: Use curly braces and wildcards to test multiple materials and styles efficiently.[5, 42]
Benchmarking and Formatting: Use JSON for complex structural instructions to ensure the highest accuracy across models like Claude and GPT-4o.[11, 12]
Maintain Consistency with Reference Parameters: Use --sref and --cref in Midjourney or IP-Adapters in Stable Diffusion to ensure visual vibes and identities remain stable.[27, 28, 38]
By weaving these methodologies into a cohesive narrative, designers can move beyond the "gambling" aspect of generative AI toward a systematic, architectural approach that delivers predictable, high-quality results for professional applications.[1, 2] The prompt is no longer just text; it is the blueprint for the next generation of visual content.[2, 39]
+4385
View File
File diff suppressed because it is too large. Load diff
+19 -3
View File
@@ -10,14 +10,30 @@
"lint": "tsc --noEmit"
},
"dependencies": {
"@google/genai": "^1.33.0",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.561.0",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"@google/genai": "^1.33.0",
"lucide-react": "^0.561.0"
"tailwind-merge": "^3.5.0",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@types/node": "^22.14.0",
"@types/node": "^22.19.15",
"@vitejs/plugin-react": "^5.0.0",
"autoprefixer": "^10.4.27",
"postcss": "^8.5.8",
"tailwindcss": "^3.4.19",
"typescript": "~5.8.2",
"vite": "^6.2.0"
}
+6
View File
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
+78
View File
@@ -0,0 +1,78 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
prefix: "",
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
}
+2 -2
View File
@@ -11,8 +11,8 @@ export default defineConfig(({ mode }) => {
},
plugins: [react()],
define: {
'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY)
'process.env.API_KEY': JSON.stringify(env.VITE_GEMINI_API_KEY),
'process.env.GEMINI_API_KEY': JSON.stringify(env.VITE_GEMINI_API_KEY)
},
resolve: {
alias: {