Files
jpremill 1feea40684 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
2026-09-29 23:35:05 -04:00

151 lines
6.1 KiB
C#

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;
namespace CognitiveClarity.api
{
public class ChatMessage
{
[JsonPropertyName("role")]
public string Role { get; set; } = string.Empty; // "user" or "model" or "assistant"
[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;
[JsonPropertyName("model")]
public string Model { get; set; } = "google/gemini-3.8-flash";
}
public class ChatFunction
{
private readonly ILogger _logger;
private readonly IHttpClientFactory _httpClientFactory;
public ChatFunction(ILoggerFactory loggerFactory, IHttpClientFactory httpClientFactory)
{
_logger = loggerFactory.CreateLogger<ChatFunction>();
_httpClientFactory = httpClientFactory;
}
[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 via OpenRouter.");
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 = System.Environment.GetEnvironmentVariable("OPENROUTER_API_KEY");
if (string.IsNullOrEmpty(apiKey))
{
_logger.LogError("OPENROUTER_API_KEY is missing from environment variables.");
return req.CreateResponse(HttpStatusCode.InternalServerError);
}
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.";
var messages = new List<object>
{
new { role = "system", content = roleplaySystemInstruction }
};
foreach (var msg in data.History)
{
messages.Add(new
{
role = msg.Role == "model" ? "assistant" : msg.Role,
content = msg.Text
});
}
var payload = new
{
model = string.IsNullOrEmpty(data.Model) ? "google/gemini-3.8-flash" : data.Model,
messages = messages,
temperature = 0.7,
max_tokens = 1000
};
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 botText = doc.RootElement
.GetProperty("choices")[0]
.GetProperty("message")
.GetProperty("content")
.GetString() ?? string.Empty;
var successResponse = req.CreateResponse(HttpStatusCode.OK);
successResponse.Headers.Add("Content-Type", "application/json");
var resultObj = new { text = botText };
await successResponse.WriteStringAsync(JsonSerializer.Serialize(resultObj));
return successResponse;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to generate chat progression via OpenRouter API.");
var errorResponse = req.CreateResponse(HttpStatusCode.InternalServerError);
await errorResponse.WriteStringAsync("Error communicating with OpenRouter API.");
return errorResponse;
}
}
}
}