474 lines
16 KiB
TypeScript
474 lines
16 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { trpc } from "~/lib/trpc/client.js";
|
|
import {
|
|
AiProviderPanel,
|
|
GenerationSettingsPanel,
|
|
} from "./system-settings/AiSettingsPanels.js";
|
|
import { LegacyRuntimeSecretsNotice } from "./system-settings/LegacyRuntimeSecretsNotice.js";
|
|
import {
|
|
type ImageProvider,
|
|
type Provider,
|
|
type ScoreWeights,
|
|
type SaveResult,
|
|
type SystemRole,
|
|
type UrlParsedType,
|
|
} from "./system-settings/shared.js";
|
|
import { AnonymizationSettingsPanel } from "./system-settings/AnonymizationSettingsPanel.js";
|
|
import { ImageGenerationPanel } from "./system-settings/ImageGenerationPanel.js";
|
|
import { SmtpSettingsPanel } from "./system-settings/SmtpSettingsPanel.js";
|
|
import { TimelineSettingsPanel } from "./system-settings/TimelineSettingsPanel.js";
|
|
import { VacationSettingsPanel } from "./system-settings/VacationSettingsPanel.js";
|
|
import { ValueScorePanel } from "./system-settings/ValueScorePanel.js";
|
|
import { parseAzureUrl, type GeminiTestResult } from "./system-settings/shared.js";
|
|
|
|
export function SystemSettingsClient() {
|
|
const [provider, setProvider] = useState<Provider>("openai");
|
|
const [endpoint, setEndpoint] = useState("");
|
|
const [model, setModel] = useState("");
|
|
const [apiVersion, setApiVersion] = useState("2025-01-01-preview");
|
|
const [maxTokens, setMaxTokens] = useState(2000);
|
|
const [temperature, setTemperature] = useState(1);
|
|
const [summaryPrompt, setSummaryPrompt] = useState("");
|
|
const [saved, setSaved] = useState(false);
|
|
const [testResult, setTestResult] = useState<SaveResult | null>(null);
|
|
const [urlPasteValue, setUrlPasteValue] = useState("");
|
|
const [urlParseError, setUrlParseError] = useState(false);
|
|
const [urlParsedType, setUrlParsedType] = useState<UrlParsedType>(null);
|
|
const [scoreWeights, setScoreWeights] = useState<ScoreWeights>({
|
|
skillDepth: 0.3,
|
|
skillBreadth: 0.15,
|
|
costEfficiency: 0.25,
|
|
chargeability: 0.15,
|
|
experience: 0.15,
|
|
});
|
|
const [scoreVisibleRoles, setScoreVisibleRoles] = useState<SystemRole[]>(["ADMIN", "MANAGER"]);
|
|
const [scoreSaved, setScoreSaved] = useState(false);
|
|
const [recomputeResult, setRecomputeResult] = useState<{ updated: number } | null>(null);
|
|
const [dalleDeployment, setDalleDeployment] = useState("");
|
|
const [dalleEndpoint, setDalleEndpoint] = useState("");
|
|
const [imageProvider, setImageProvider] = useState<ImageProvider>("dalle");
|
|
const [geminiModel, setGeminiModel] = useState("");
|
|
const [imageSaved, setImageSaved] = useState(false);
|
|
const [smtpHost, setSmtpHost] = useState("");
|
|
const [smtpPort, setSmtpPort] = useState(587);
|
|
const [smtpUser, setSmtpUser] = useState("");
|
|
const [smtpFrom, setSmtpFrom] = useState("");
|
|
const [smtpTls, setSmtpTls] = useState(true);
|
|
const [smtpSaved, setSmtpSaved] = useState(false);
|
|
const [smtpTestResult, setSmtpTestResult] = useState<SaveResult | null>(null);
|
|
const [anonymizationEnabled, setAnonymizationEnabled] = useState(false);
|
|
const [anonymizationDomain, setAnonymizationDomain] = useState("superhartmut.de");
|
|
const [anonymizationSaved, setAnonymizationSaved] = useState(false);
|
|
const [vacationDefaultDays, setVacationDefaultDays] = useState(28);
|
|
const [vacationSaved, setVacationSaved] = useState(false);
|
|
const [undoMaxSteps, setUndoMaxSteps] = useState(50);
|
|
const [timelineSaved, setTimelineSaved] = useState(false);
|
|
const [legacyCleanupResult, setLegacyCleanupResult] = useState<string | null>(null);
|
|
const [geminiTestResult, setGeminiTestResult] = useState<GeminiTestResult | null>(null);
|
|
|
|
const utils = trpc.useUtils();
|
|
const { data: settings, isLoading } = trpc.settings.getSystemSettings.useQuery(undefined, {
|
|
staleTime: 0,
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (!settings) {
|
|
return;
|
|
}
|
|
|
|
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[]);
|
|
}
|
|
setDalleDeployment(settings.azureDalleDeployment ?? "");
|
|
setDalleEndpoint(settings.azureDalleEndpoint ?? "");
|
|
setImageProvider((settings.imageProvider ?? "dalle") as ImageProvider);
|
|
setGeminiModel(settings.geminiModel ?? "");
|
|
setSmtpHost(settings.smtpHost ?? "");
|
|
setSmtpPort(settings.smtpPort ?? 587);
|
|
setSmtpUser(settings.smtpUser ?? "");
|
|
setSmtpFrom(settings.smtpFrom ?? "");
|
|
setSmtpTls(settings.smtpTls ?? true);
|
|
setAnonymizationEnabled(settings.anonymizationEnabled ?? false);
|
|
setAnonymizationDomain(settings.anonymizationDomain ?? "superhartmut.de");
|
|
setVacationDefaultDays(settings.vacationDefaultDays ?? 28);
|
|
setUndoMaxSteps(settings.timelineUndoMaxSteps ?? 50);
|
|
}, [settings]);
|
|
|
|
function invalidateSystemSettings() {
|
|
void utils.settings.getSystemSettings.invalidate();
|
|
}
|
|
|
|
function handleUrlPaste(raw: string) {
|
|
setUrlPasteValue(raw);
|
|
if (!raw) {
|
|
setUrlParseError(false);
|
|
setUrlParsedType(null);
|
|
return;
|
|
}
|
|
|
|
const parsed = parseAzureUrl(raw);
|
|
if (!parsed) {
|
|
setUrlParseError(true);
|
|
setUrlParsedType(null);
|
|
return;
|
|
}
|
|
|
|
setEndpoint(parsed.endpoint);
|
|
setApiVersion(parsed.apiVersion);
|
|
if (parsed.deployment) {
|
|
setModel(parsed.deployment);
|
|
}
|
|
setUrlParseError(false);
|
|
setUrlParsedType(parsed.urlType);
|
|
setUrlPasteValue("");
|
|
}
|
|
|
|
const updateMutation = trpc.settings.updateSystemSettings.useMutation({
|
|
onSuccess: () => {
|
|
setSaved(true);
|
|
setTestResult(null);
|
|
setLegacyCleanupResult(null);
|
|
invalidateSystemSettings();
|
|
setTimeout(() => setSaved(false), 3000);
|
|
},
|
|
});
|
|
|
|
const testMutation = trpc.settings.testAiConnection.useMutation({
|
|
onSuccess: (data) => setTestResult(data),
|
|
onError: (error) => setTestResult({ ok: false, error: error.message }),
|
|
});
|
|
|
|
const saveScoreMutation = trpc.settings.updateSystemSettings.useMutation({
|
|
onSuccess: () => {
|
|
setScoreSaved(true);
|
|
invalidateSystemSettings();
|
|
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);
|
|
setLegacyCleanupResult(null);
|
|
invalidateSystemSettings();
|
|
setTimeout(() => setSmtpSaved(false), 3000);
|
|
},
|
|
});
|
|
|
|
const testSmtpMutation = trpc.settings.testSmtpConnection.useMutation({
|
|
onSuccess: (data) => setSmtpTestResult(data),
|
|
onError: (error) => setSmtpTestResult({ ok: false, error: error.message }),
|
|
});
|
|
|
|
const saveAnonymizationMutation = trpc.settings.updateSystemSettings.useMutation({
|
|
onSuccess: () => {
|
|
setAnonymizationSaved(true);
|
|
setLegacyCleanupResult(null);
|
|
invalidateSystemSettings();
|
|
setTimeout(() => setAnonymizationSaved(false), 3000);
|
|
},
|
|
});
|
|
|
|
const saveVacationMutation = trpc.settings.updateSystemSettings.useMutation({
|
|
onSuccess: () => {
|
|
setVacationSaved(true);
|
|
invalidateSystemSettings();
|
|
setTimeout(() => setVacationSaved(false), 3000);
|
|
},
|
|
});
|
|
|
|
const saveTimelineMutation = trpc.settings.updateSystemSettings.useMutation({
|
|
onSuccess: () => {
|
|
setTimelineSaved(true);
|
|
invalidateSystemSettings();
|
|
setTimeout(() => setTimelineSaved(false), 3000);
|
|
},
|
|
});
|
|
|
|
const saveImageMutation = trpc.settings.updateSystemSettings.useMutation({
|
|
onSuccess: () => {
|
|
setImageSaved(true);
|
|
setLegacyCleanupResult(null);
|
|
invalidateSystemSettings();
|
|
setTimeout(() => setImageSaved(false), 3000);
|
|
},
|
|
});
|
|
|
|
const clearRuntimeSecretsMutation = trpc.settings.clearStoredRuntimeSecrets.useMutation({
|
|
onSuccess: (data) => {
|
|
setLegacyCleanupResult(
|
|
data.clearedFields.length > 0
|
|
? `Cleared ${data.clearedFields.length} legacy database secret field${data.clearedFields.length === 1 ? "" : "s"}.`
|
|
: "No legacy database secrets were left to clear.",
|
|
);
|
|
invalidateSystemSettings();
|
|
},
|
|
onError: (error) => setLegacyCleanupResult(error.message),
|
|
});
|
|
|
|
const testGeminiMutation = trpc.settings.testGeminiConnection.useMutation({
|
|
onSuccess: (data) => setGeminiTestResult(data as GeminiTestResult),
|
|
onError: (error) => setGeminiTestResult({ ok: false, error: error.message }),
|
|
});
|
|
|
|
function handleSave() {
|
|
updateMutation.mutate({
|
|
aiProvider: provider,
|
|
azureOpenAiEndpoint: provider === "azure" ? endpoint : "",
|
|
azureOpenAiDeployment: model,
|
|
azureApiVersion: provider === "azure" ? apiVersion : undefined,
|
|
aiMaxCompletionTokens: maxTokens,
|
|
aiTemperature: temperature,
|
|
aiSummaryPrompt: summaryPrompt || undefined,
|
|
});
|
|
}
|
|
|
|
function handleSaveScoreSettings() {
|
|
saveScoreMutation.mutate({ scoreWeights, scoreVisibleRoles });
|
|
}
|
|
|
|
function handleSaveImage() {
|
|
saveImageMutation.mutate({
|
|
imageProvider,
|
|
azureDalleDeployment: dalleDeployment || undefined,
|
|
azureDalleEndpoint: provider === "azure" && dalleEndpoint ? dalleEndpoint : undefined,
|
|
geminiModel: geminiModel || undefined,
|
|
});
|
|
}
|
|
|
|
function handleSaveSmtp() {
|
|
saveSmtpMutation.mutate({
|
|
smtpHost: smtpHost || undefined,
|
|
smtpPort,
|
|
smtpUser: smtpUser || undefined,
|
|
smtpFrom: smtpFrom || undefined,
|
|
smtpTls,
|
|
});
|
|
}
|
|
|
|
function handleSaveVacation() {
|
|
saveVacationMutation.mutate({ vacationDefaultDays });
|
|
}
|
|
|
|
function handleSaveTimeline() {
|
|
saveTimelineMutation.mutate({ timelineUndoMaxSteps: undoMaxSteps });
|
|
}
|
|
|
|
function handleSaveAnonymization() {
|
|
saveAnonymizationMutation.mutate({
|
|
anonymizationEnabled,
|
|
anonymizationDomain: anonymizationDomain.trim() || "superhartmut.de",
|
|
anonymizationMode: "global",
|
|
});
|
|
}
|
|
|
|
function updateWeight(key: keyof ScoreWeights, value: number) {
|
|
setScoreWeights((previous) => ({ ...previous, [key]: value }));
|
|
}
|
|
|
|
function toggleRole(role: SystemRole) {
|
|
setScoreVisibleRoles((previous) =>
|
|
previous.includes(role) ? previous.filter((entry) => entry !== role) : [...previous, role],
|
|
);
|
|
}
|
|
|
|
function handleClearLegacyRuntimeSecrets() {
|
|
if (
|
|
typeof window !== "undefined"
|
|
&& !window.confirm(
|
|
"Clear all legacy runtime secrets from database storage? Environment-based deployment secrets must already be configured.",
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
clearRuntimeSecretsMutation.mutate();
|
|
}
|
|
|
|
const weightSum = Object.values(scoreWeights).reduce((sum, value) => sum + value, 0);
|
|
const weightSumOk = Math.abs(weightSum - 1.0) < 0.01;
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="app-page">
|
|
<div className="h-8 w-48 rounded shimmer-skeleton" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!settings) {
|
|
return (
|
|
<div className="app-page">
|
|
<div className="rounded-2xl border border-red-200 bg-red-50 px-5 py-4 text-sm text-red-700 dark:border-red-800 dark:bg-red-950/30 dark:text-red-300">
|
|
System settings could not be loaded.
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="app-page space-y-6">
|
|
<div className="app-page-header gap-4">
|
|
<div>
|
|
<h1 className="app-page-title">System Settings</h1>
|
|
<p className="app-page-subtitle mt-1">
|
|
Configure AI integration for skill profile generation.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<LegacyRuntimeSecretsNotice
|
|
fields={settings.legacyStoredSecretFields}
|
|
result={legacyCleanupResult}
|
|
isPending={clearRuntimeSecretsMutation.isPending}
|
|
onClear={handleClearLegacyRuntimeSecrets}
|
|
/>
|
|
|
|
<div className="grid grid-cols-1 items-start gap-6 lg:grid-cols-2">
|
|
<AiProviderPanel
|
|
provider={provider}
|
|
endpoint={endpoint}
|
|
model={model}
|
|
apiVersion={apiVersion}
|
|
urlPasteValue={urlPasteValue}
|
|
urlParseError={urlParseError}
|
|
urlParsedType={urlParsedType}
|
|
runtimeSecret={settings.runtimeSecrets.azureOpenAiApiKey}
|
|
testResult={testResult}
|
|
isSaving={updateMutation.isPending}
|
|
isTesting={testMutation.isPending}
|
|
saved={saved}
|
|
onProviderChange={(nextProvider) => {
|
|
setProvider(nextProvider);
|
|
setTestResult(null);
|
|
}}
|
|
onEndpointChange={setEndpoint}
|
|
onModelChange={setModel}
|
|
onApiVersionChange={setApiVersion}
|
|
onUrlPaste={handleUrlPaste}
|
|
onSave={handleSave}
|
|
onTest={() => {
|
|
setTestResult(null);
|
|
testMutation.mutate();
|
|
}}
|
|
/>
|
|
|
|
<GenerationSettingsPanel
|
|
maxTokens={maxTokens}
|
|
temperature={temperature}
|
|
summaryPrompt={summaryPrompt}
|
|
defaultSummaryPrompt={settings.defaultSummaryPrompt ?? ""}
|
|
isSaving={updateMutation.isPending}
|
|
saved={saved}
|
|
onMaxTokensChange={setMaxTokens}
|
|
onTemperatureChange={setTemperature}
|
|
onSummaryPromptChange={setSummaryPrompt}
|
|
onSave={handleSave}
|
|
onResetSummaryPrompt={() => setSummaryPrompt("")}
|
|
/>
|
|
</div>
|
|
|
|
<ValueScorePanel
|
|
scoreWeights={scoreWeights}
|
|
scoreVisibleRoles={scoreVisibleRoles}
|
|
weightSum={weightSum}
|
|
weightSumOk={weightSumOk}
|
|
scoreSaved={scoreSaved}
|
|
recomputeResult={recomputeResult}
|
|
isSaving={saveScoreMutation.isPending}
|
|
isRecomputing={recomputeMutation.isPending}
|
|
onUpdateWeight={updateWeight}
|
|
onToggleRole={toggleRole}
|
|
onRecompute={() => {
|
|
setRecomputeResult(null);
|
|
recomputeMutation.mutate();
|
|
}}
|
|
onSave={handleSaveScoreSettings}
|
|
/>
|
|
|
|
<ImageGenerationPanel
|
|
provider={provider}
|
|
imageProvider={imageProvider}
|
|
dalleDeployment={dalleDeployment}
|
|
dalleEndpoint={dalleEndpoint}
|
|
geminiModel={geminiModel}
|
|
imageSaved={imageSaved}
|
|
geminiTestResult={geminiTestResult}
|
|
runtimeSecrets={settings.runtimeSecrets}
|
|
isSaving={saveImageMutation.isPending}
|
|
isTestingGemini={testGeminiMutation.isPending}
|
|
onImageProviderChange={setImageProvider}
|
|
onDalleDeploymentChange={setDalleDeployment}
|
|
onDalleEndpointChange={setDalleEndpoint}
|
|
onGeminiModelChange={setGeminiModel}
|
|
onSave={handleSaveImage}
|
|
onTestGemini={() => testGeminiMutation.mutate()}
|
|
/>
|
|
|
|
<SmtpSettingsPanel
|
|
smtpHost={smtpHost}
|
|
smtpPort={smtpPort}
|
|
smtpUser={smtpUser}
|
|
smtpFrom={smtpFrom}
|
|
smtpTls={smtpTls}
|
|
smtpSaved={smtpSaved}
|
|
smtpTestResult={smtpTestResult}
|
|
smtpSecret={settings.runtimeSecrets.smtpPassword}
|
|
isSaving={saveSmtpMutation.isPending}
|
|
isTesting={testSmtpMutation.isPending}
|
|
onSmtpHostChange={setSmtpHost}
|
|
onSmtpPortChange={setSmtpPort}
|
|
onSmtpUserChange={setSmtpUser}
|
|
onSmtpFromChange={setSmtpFrom}
|
|
onSmtpTlsChange={setSmtpTls}
|
|
onSave={handleSaveSmtp}
|
|
onTest={() => testSmtpMutation.mutate()}
|
|
/>
|
|
|
|
<VacationSettingsPanel
|
|
vacationDefaultDays={vacationDefaultDays}
|
|
vacationSaved={vacationSaved}
|
|
isSaving={saveVacationMutation.isPending}
|
|
onVacationDefaultDaysChange={setVacationDefaultDays}
|
|
onSave={handleSaveVacation}
|
|
/>
|
|
|
|
<TimelineSettingsPanel
|
|
undoMaxSteps={undoMaxSteps}
|
|
timelineSaved={timelineSaved}
|
|
isSaving={saveTimelineMutation.isPending}
|
|
onUndoMaxStepsChange={setUndoMaxSteps}
|
|
onSave={handleSaveTimeline}
|
|
/>
|
|
|
|
<AnonymizationSettingsPanel
|
|
anonymizationEnabled={anonymizationEnabled}
|
|
anonymizationDomain={anonymizationDomain}
|
|
anonymizationSaved={anonymizationSaved}
|
|
anonymizationSecret={settings.runtimeSecrets.anonymizationSeed}
|
|
isSaving={saveAnonymizationMutation.isPending}
|
|
onAnonymizationEnabledChange={setAnonymizationEnabled}
|
|
onAnonymizationDomainChange={setAnonymizationDomain}
|
|
onSave={handleSaveAnonymization}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|