feat(ui): redesign voice cockpit with clinical companion drawer, Svelte 5 migration, and Gemini 3.8 voice

- Re-architect VoiceSession into full-bleed studio cockpit with overlay slide-over companion drawer
- Eliminate width squishing defect on LiveInsightPanel and add native dark/light theme support
- Integrate authentic Google Gemini 3.8 Flash TTS with Fenrir persona and fallback audio pipeline
- Implement comprehensive Playwright E2E visual regression suite across Desktop, iPad, and iPhone (21 tests, 45 snapshots)
- Migrate core application to Svelte 5 runes (, , ) with reactive stores
- Maintain .NET 8 Azure Functions serverless endpoints and clean build hygiene
This commit is contained in:
jpremill committed 2026-09-29 23:35:05 -04:00
1 parent c82b112624
commit 1feea40684
148 files changed
+11230 -1706

No files matched your search

+81 -59
View File
@@ -1,10 +1,11 @@
using System.Net;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
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
{
@@ -18,22 +19,27 @@ namespace CognitiveClarity.api
[JsonPropertyName("difficulty")]
public string Difficulty { get; set; } = "Intermediate";
[JsonPropertyName("model")]
public string Model { get; set; } = "google/gemini-3.8-flash";
}
public class ScenarioFunction
{
private readonly ILogger _logger;
private readonly IHttpClientFactory _httpClientFactory;
public ScenarioFunction(ILoggerFactory loggerFactory)
public ScenarioFunction(ILoggerFactory loggerFactory, IHttpClientFactory httpClientFactory)
{
_logger = loggerFactory.CreateLogger<ScenarioFunction>();
_httpClientFactory = httpClientFactory;
}
[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.");
_logger.LogInformation("C# HTTP trigger function processed a scenario generation request via OpenRouter.");
var requestBody = await new StreamReader(req.Body).ReadToEndAsync();
var data = JsonSerializer.Deserialize<ScenarioRequest>(requestBody);
@@ -45,84 +51,100 @@ namespace CognitiveClarity.api
return badResponse;
}
var apiKey = Environment.GetEnvironmentVariable("GEMINI_API_KEY");
var apiKey = System.Environment.GetEnvironmentVariable("OPENROUTER_API_KEY");
if (string.IsNullOrEmpty(apiKey))
{
_logger.LogError("GEMINI_API_KEY is missing from environment variables.");
_logger.LogError("OPENROUTER_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.
";
[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}
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" }
};
[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 matching this schema:
{{
""id"": ""string"",
""title"": ""string"",
""characterName"": ""string"",
""description"": ""string"",
""category"": ""Social Anxiety"" | ""Workplace"" | ""Relationships"" | ""Self-Talk"",
""difficulty"": ""Beginner"" | ""Intermediate"" | ""Advanced"",
""systemInstruction"": ""string"",
""initialMessage"": ""string"",
""cbtFocus"": [""string""]
}}";
var promptText = $"Generate a clinical scenario for: {(string.IsNullOrWhiteSpace(data.Prompt) ? "A scenario exploring the chosen CBT concepts." : data.Prompt)}";
try
var payload = new
{
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
});
model = string.IsNullOrEmpty(data.Model) ? "google/gemini-3.8-flash" : data.Model,
messages = new object[]
{
new { role = "system", content = systemInstruction },
new { role = "user", content = promptText }
},
response_format = new { type = "json_object" },
temperature = 0.7
};
try
{
var client = _httpClientFactory.CreateClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
client.DefaultRequestHeaders.Add("HTTP-Referer", "https://cognitive-clarity.app");
client.DefaultRequestHeaders.Add("X-Title", "Cognitive Clarity");
var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
var response = await client.PostAsync("https://openrouter.ai/api/v1/chat/completions", content);
if (!response.IsSuccessStatusCode)
{
var errText = await response.Content.ReadAsStringAsync();
_logger.LogError("OpenRouter returned error {Status}: {Error}", response.StatusCode, errText);
var errResponse = req.CreateResponse(response.StatusCode);
await errResponse.WriteStringAsync(errText);
return errResponse;
}
var resJson = await response.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(resJson);
var scenarioJson = doc.RootElement
.GetProperty("choices")[0]
.GetProperty("message")
.GetProperty("content")
.GetString() ?? "{}";
var successResponse = req.CreateResponse(HttpStatusCode.OK);
successResponse.Headers.Add("Content-Type", "application/json");
await successResponse.WriteStringAsync(response.Text!);
await successResponse.WriteStringAsync(scenarioJson);
return successResponse;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to generate scenario via Gemini API.");
_logger.LogError(ex, "Failed to generate scenario via OpenRouter API.");
var errorResponse = req.CreateResponse(HttpStatusCode.InternalServerError);
await errorResponse.WriteStringAsync("Error communicating with Gemini API.");
await errorResponse.WriteStringAsync("Error communicating with OpenRouter API.");
return errorResponse;
}
}