"use client"; import { useState, useEffect } from "react"; import { trpc } from "~/lib/trpc/client.js"; const INPUT_CLASS = "app-input"; const LABEL_CLASS = "app-label"; const PANEL_CLASS = "app-surface p-6 space-y-5"; const PANEL_STRONG_CLASS = "app-surface-strong p-6 space-y-6"; const PRIMARY_BUTTON_CLASS = "rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white transition hover:bg-brand-700 disabled:opacity-50"; const SECONDARY_BUTTON_CLASS = "rounded-xl border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 transition hover:bg-gray-50 disabled:opacity-50 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-800"; const CHECKBOX_ROW_CLASS = "flex items-center gap-2 rounded-xl border border-gray-200 bg-gray-50 px-3 py-2 text-sm text-gray-700 dark:border-gray-700 dark:bg-gray-900/70 dark:text-gray-300"; type Provider = "openai" | "azure"; const ALL_ROLES = ["ADMIN", "MANAGER", "CONTROLLER", "USER", "VIEWER"] as const; type SystemRole = (typeof ALL_ROLES)[number]; interface ScoreWeights { skillDepth: number; skillBreadth: number; costEfficiency: number; chargeability: number; experience: number; } type ParsedAzureUrl = { endpoint: string; apiVersion: string; deployment: string | null; // null for Responses API URLs (deployment not in path) urlType: "completions" | "responses"; }; /** Parse endpoint, deployment, and api-version out of an Azure URL. * Supports both Chat Completions and Responses API formats. */ function parseAzureUrl(raw: string): ParsedAzureUrl | null { try { const url = new URL(raw); const endpoint = `${url.protocol}//${url.host}`; const apiVersion = url.searchParams.get("api-version") ?? "2025-01-01-preview"; // Chat Completions: /openai/deployments/{name}/chat/completions const completionsMatch = url.pathname.match(/\/openai\/deployments\/([^/]+)\//); if (completionsMatch) { return { endpoint, apiVersion, deployment: completionsMatch[1]!, urlType: "completions" }; } // Responses API: /openai/responses if (url.pathname.includes("/openai/responses")) { return { endpoint, apiVersion, deployment: null, urlType: "responses" }; } return null; } catch { return null; } } export function SystemSettingsClient() { const [provider, setProvider] = useState("openai"); const [endpoint, setEndpoint] = useState(""); const [model, setModel] = useState(""); const [apiVersion, setApiVersion] = useState("2025-01-01-preview"); const [apiKey, setApiKey] = useState(""); const [maxTokens, setMaxTokens] = useState(2000); const [temperature, setTemperature] = useState(1); const [summaryPrompt, setSummaryPrompt] = useState(""); const [saved, setSaved] = useState(false); const [testResult, setTestResult] = useState<{ ok: boolean; error?: string; raw?: string | null; } | null>(null); const [urlPasteValue, setUrlPasteValue] = useState(""); const [urlParseError, setUrlParseError] = useState(false); const [urlParsedType, setUrlParsedType] = useState<"completions" | "responses" | null>(null); // Value Score settings const [scoreWeights, setScoreWeights] = useState({ skillDepth: 0.3, skillBreadth: 0.15, costEfficiency: 0.25, chargeability: 0.15, experience: 0.15, }); const [scoreVisibleRoles, setScoreVisibleRoles] = useState(["ADMIN", "MANAGER"]); const [scoreSaved, setScoreSaved] = useState(false); const [recomputeResult, setRecomputeResult] = useState<{ updated: number } | null>(null); // SMTP settings const [smtpHost, setSmtpHost] = useState(""); const [smtpPort, setSmtpPort] = useState(587); const [smtpUser, setSmtpUser] = useState(""); const [smtpPassword, setSmtpPassword] = useState(""); const [smtpFrom, setSmtpFrom] = useState(""); const [smtpTls, setSmtpTls] = useState(true); const [smtpSaved, setSmtpSaved] = useState(false); const [smtpTestResult, setSmtpTestResult] = useState<{ ok: boolean; error?: string } | null>( null, ); // Global anonymization const [anonymizationEnabled, setAnonymizationEnabled] = useState(false); const [anonymizationDomain, setAnonymizationDomain] = useState("superhartmut.de"); const [anonymizationSeed, setAnonymizationSeed] = useState(""); const [anonymizationSaved, setAnonymizationSaved] = useState(false); // Vacation defaults const [vacationDefaultDays, setVacationDefaultDays] = useState(28); const [vacationSaved, setVacationSaved] = useState(false); const { data: settings, isLoading } = trpc.settings.getSystemSettings.useQuery(undefined, { staleTime: 0, }); useEffect(() => { if (settings) { setProvider((settings.aiProvider ?? "openai") as Provider); setEndpoint(settings.azureOpenAiEndpoint ?? ""); setModel(settings.azureOpenAiDeployment ?? ""); setApiVersion(settings.azureApiVersion ?? "2025-01-01-preview"); setMaxTokens(settings.aiMaxCompletionTokens ?? 2000); setTemperature(settings.aiTemperature ?? 1); setSummaryPrompt(settings.aiSummaryPrompt ?? ""); if (settings.scoreWeights) { setScoreWeights(settings.scoreWeights as ScoreWeights); } if (settings.scoreVisibleRoles) { setScoreVisibleRoles(settings.scoreVisibleRoles as SystemRole[]); } // SMTP setSmtpHost(settings.smtpHost ?? ""); setSmtpPort(settings.smtpPort ?? 587); setSmtpUser(settings.smtpUser ?? ""); setSmtpFrom(settings.smtpFrom ?? ""); setSmtpTls(settings.smtpTls ?? true); // Global anonymization setAnonymizationEnabled(settings.anonymizationEnabled ?? false); setAnonymizationDomain(settings.anonymizationDomain ?? "superhartmut.de"); setAnonymizationSeed(""); // Vacation setVacationDefaultDays(settings.vacationDefaultDays ?? 28); } }, [settings]); function handleUrlPaste(raw: string) { setUrlPasteValue(raw); if (!raw) { setUrlParseError(false); setUrlParsedType(null); return; } const parsed = parseAzureUrl(raw); if (parsed) { setEndpoint(parsed.endpoint); setApiVersion(parsed.apiVersion); if (parsed.deployment) setModel(parsed.deployment); setUrlParseError(false); setUrlParsedType(parsed.urlType); setUrlPasteValue(""); } else { setUrlParseError(true); setUrlParsedType(null); } } const updateMutation = trpc.settings.updateSystemSettings.useMutation({ onSuccess: () => { setSaved(true); setTestResult(null); setTimeout(() => setSaved(false), 3000); }, }); const testMutation = trpc.settings.testAiConnection.useMutation({ onSuccess: (data) => setTestResult(data), onError: (err) => setTestResult({ ok: false, error: err.message }), }); const saveScoreMutation = trpc.settings.updateSystemSettings.useMutation({ onSuccess: () => { setScoreSaved(true); setTimeout(() => setScoreSaved(false), 3000); }, }); const recomputeMutation = trpc.resource.recomputeValueScores.useMutation({ onSuccess: (data) => setRecomputeResult(data), }); const saveSmtpMutation = trpc.settings.updateSystemSettings.useMutation({ onSuccess: () => { setSmtpSaved(true); setSmtpTestResult(null); setTimeout(() => setSmtpSaved(false), 3000); }, }); const testSmtpMutation = trpc.settings.testSmtpConnection.useMutation({ onSuccess: (data) => setSmtpTestResult(data), onError: (err) => setSmtpTestResult({ ok: false, error: err.message }), }); const saveAnonymizationMutation = trpc.settings.updateSystemSettings.useMutation({ onSuccess: () => { setAnonymizationSaved(true); setTimeout(() => setAnonymizationSaved(false), 3000); }, }); const saveVacationMutation = trpc.settings.updateSystemSettings.useMutation({ onSuccess: () => { setVacationSaved(true); setTimeout(() => setVacationSaved(false), 3000); }, }); function handleSaveSmtp() { saveSmtpMutation.mutate({ smtpHost: smtpHost || undefined, smtpPort, smtpUser: smtpUser || undefined, ...(smtpPassword ? { smtpPassword } : {}), smtpFrom: smtpFrom || undefined, smtpTls, }); } function handleSaveVacation() { saveVacationMutation.mutate({ vacationDefaultDays }); } function handleSaveAnonymization() { saveAnonymizationMutation.mutate({ anonymizationEnabled, anonymizationDomain: anonymizationDomain.trim() || "superhartmut.de", ...(anonymizationSeed.trim() ? { anonymizationSeed: anonymizationSeed.trim() } : {}), anonymizationMode: "global", }); } function handleSaveScoreSettings() { saveScoreMutation.mutate({ scoreWeights, scoreVisibleRoles }); } function updateWeight(key: keyof ScoreWeights, value: number) { setScoreWeights((prev) => ({ ...prev, [key]: value })); } function toggleRole(role: SystemRole) { setScoreVisibleRoles((prev) => prev.includes(role) ? prev.filter((r) => r !== role) : [...prev, role], ); } const weightSum = Object.values(scoreWeights).reduce((s, v) => s + v, 0); const weightSumOk = Math.abs(weightSum - 1.0) < 0.01; function handleSave() { updateMutation.mutate({ aiProvider: provider, azureOpenAiEndpoint: provider === "azure" ? endpoint : "", azureOpenAiDeployment: model, azureApiVersion: provider === "azure" ? apiVersion : undefined, aiMaxCompletionTokens: maxTokens, aiTemperature: temperature, aiSummaryPrompt: summaryPrompt || undefined, ...(apiKey ? { azureOpenAiApiKey: apiKey } : {}), }); } if (isLoading) { return (
); } return (

System Settings

Configure AI integration for skill profile generation.

AI Provider

{/* Provider toggle */}

{provider === "openai" ? "Use a standard OpenAI API key from platform.openai.com." : "Use a deployment on your own Azure OpenAI resource."}

{/* Azure-only fields */} {provider === "azure" && ( <> {/* Paste full URL shortcut */}

Paste a full completion URL to auto-fill all fields below:

handleUrlPaste(e.target.value)} /> {urlParseError && (

Could not parse URL — expected either a Chat Completions URL ( /openai/deployments/…/chat/completions) or a Responses API URL (/openai/responses).

)} {urlParsedType === "responses" && (

Responses API URL detected — endpoint and api-version filled in. Enter the{" "} deployment/model name manually below (it is not part of this URL).

)} {urlParsedType === "completions" && (

All fields filled from URL.

)}
setEndpoint(e.target.value)} />

Everything up to (not including) /openai/…

)} {/* Model / deployment name */}
setModel(e.target.value)} />

{provider === "azure" ? "The deployment name you chose when deploying the model in Azure." : "The model identifier, e.g. gpt-4o-mini, gpt-4o, gpt-3.5-turbo."}

{/* Azure-only: api version */} {provider === "azure" && (
setApiVersion(e.target.value)} />

The api-version query parameter from your endpoint URL. Default: 2025-01-01-preview

)} {/* API key */}
setApiKey(e.target.value)} autoComplete="new-password" />

{provider === "openai" ? "Your secret key from platform.openai.com → API keys. Starts with sk-." : "One of the two keys from Azure Portal → your resource → Keys and Endpoint."} {settings?.hasApiKey && " Leave blank to keep the existing key."}

{/* Test result */} {testResult && (
{testResult.ok ? ( Connection successful — AI summaries are ready to use. ) : (

Connection failed: {testResult.error}

{testResult.raw && (
Show raw error
                        {testResult.raw}
                      
)}
)}
)}
{saved && ( Saved! )}
{/* Generation settings */}

Generation Settings

{/* Max completion tokens */}
{maxTokens}
setMaxTokens(Number(e.target.value))} className="w-full accent-brand-600" />
500 {maxTokens < 1000 ? "⚠ May be empty for reasoning models (GPT-5, o1, o3)" : maxTokens <= 2000 ? "Recommended for reasoning models ✓" : maxTokens <= 4000 ? "High — allows longer bios" : "Very high"} 16000

Reasoning models (GPT-5, o1, o3) consume tokens internally before writing output. Set to at least 2000 to avoid empty responses.

{/* Temperature */}
{temperature.toFixed(1)}
setTemperature(Number(e.target.value))} className="w-full accent-brand-600" />
0 — deterministic {temperature <= 0.3 ? "Factual & consistent" : temperature <= 0.9 ? "Balanced" : temperature <= 1.0 ? "Default (1) — recommended ✓" : temperature <= 1.2 ? "Creative" : "Very creative / unpredictable"} 2 — creative

Some models (e.g. GPT-5) only accept the default value of 1. If generation fails with a temperature error, the system retries automatically without it.

{/* Summary prompt */}
{summaryPrompt && ( )}