Skip to content

Commit 998ec8c

Browse files
author
manfredsteger
committed
Add AI-powered poll creation and settings management
Integrates AI functionality for creating polls, including backend routes, service logic, rate limiting, and frontend components for settings and creation. Also adds necessary types, schemas, and dependencies. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 1117a91e-7ac6-4005-bde2-487c64d5789f Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 7a6165d5-8a6c-4d1b-8789-64542c179a16 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/afc5b6d1-cfc6-4564-802f-661c3d73f96b/1117a91e-7ac6-4005-bde2-487c64d5789f/5rtIviP
1 parent 3c1a896 commit 998ec8c

File tree

14 files changed

+1137
-5
lines changed

14 files changed

+1137
-5
lines changed

.replit

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,5 @@ integrations = ["javascript_sendgrid:1.0.0"]
4747
[userenv.shared]
4848
ADMIN_USERNAME = "manfredsteger"
4949
ADMIN_EMAIL = "manfred.steger@ifp.bayern.de"
50+
AI_API_URL = "https://saia.gwdg.de/v1"
51+
AI_MODEL = "llama-3.3-70b-instruct"

client/src/components/admin/AdminDashboard.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
CalendarSettingsPanel,
2929
PentestToolsPanel,
3030
WCAGAccessibilityPanel,
31+
AiSettingsPanel,
3132
} from "./settings";
3233

3334
interface AdminDashboardProps {
@@ -325,6 +326,9 @@ export function AdminDashboard({ stats, users, polls, settings, userRole }: Admi
325326
{activeTab === "settings" && selectedSettingsPanel === 'wcag' && (
326327
<WCAGAccessibilityPanel onBack={() => setSelectedSettingsPanel(null)} />
327328
)}
329+
{activeTab === "settings" && selectedSettingsPanel === 'ai' && (
330+
<AiSettingsPanel onBack={() => setSelectedSettingsPanel(null)} />
331+
)}
328332

329333
{activeTab === "tests" && (
330334
<TestsPanel onBack={() => setActiveTab("overview")} />

client/src/components/admin/common/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ export interface AdminDashboardProps {
101101
userRole: 'admin' | 'manager';
102102
}
103103

104-
export type SettingsPanelId = 'oidc' | 'database' | 'email' | 'email-templates' | 'security' | 'matrix' | 'roles' | 'notifications' | 'session-timeout' | 'calendar' | 'pentest' | 'tests' | 'wcag' | null;
104+
export type SettingsPanelId = 'oidc' | 'database' | 'email' | 'email-templates' | 'security' | 'matrix' | 'roles' | 'notifications' | 'session-timeout' | 'calendar' | 'pentest' | 'tests' | 'wcag' | 'ai' | null;
105105

106106
export type AdminTab = 'overview' | 'monitoring' | 'polls' | 'users' | 'customize' | 'settings' | 'tests' | 'deletion-requests';
107107

client/src/components/admin/panels/SettingsPanel.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ import {
1414
Timer,
1515
Calendar,
1616
ShieldAlert,
17-
Target
17+
Target,
18+
Bot
1819
} from "lucide-react";
1920
import { SettingCard } from "../common/components";
2021
import type { SettingsPanelId as SettingsPanelType } from "../common/types";
@@ -178,6 +179,26 @@ export function SettingsPanel({
178179
/>
179180
</div>
180181
</div>
182+
183+
<div className="pt-6 border-t border-border">
184+
<div className="flex items-center gap-2 mb-4">
185+
<h3 className="text-lg font-semibold text-foreground">KI-Funktionen</h3>
186+
<span className="text-[10px] px-1.5 py-0.5 rounded-full bg-amber-500/20 text-amber-600 dark:text-amber-400 border border-amber-500/30 font-medium">
187+
Beta
188+
</span>
189+
</div>
190+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
191+
<SettingCard
192+
title="KI-Assistent"
193+
description="GWDG SAIA API konfigurieren, Modell wählen und Kontingente pro Rolle festlegen"
194+
icon={<Bot className="w-5 h-5" />}
195+
status="Konfigurieren"
196+
statusType="neutral"
197+
onClick={() => onSelectPanel('ai')}
198+
testId="setting-ai"
199+
/>
200+
</div>
201+
</div>
181202
</div>
182203
);
183204
}
Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
import { useState } from "react";
2+
import { useTranslation } from "react-i18next";
3+
import { useQuery, useMutation } from "@tanstack/react-query";
4+
import { queryClient, apiRequest } from "@/lib/queryClient";
5+
import { Button } from "@/components/ui/button";
6+
import { Badge } from "@/components/ui/badge";
7+
import { Switch } from "@/components/ui/switch";
8+
import { Label } from "@/components/ui/label";
9+
import { Input } from "@/components/ui/input";
10+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
11+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
12+
import { Alert, AlertDescription } from "@/components/ui/alert";
13+
import { ArrowLeft, Bot, Zap, Info, CheckCircle, XCircle, Infinity } from "lucide-react";
14+
import { useToast } from "@/hooks/use-toast";
15+
import type { AiSettings } from "@shared/schema";
16+
17+
const GWDG_MODELS = [
18+
{ id: "llama-3.3-70b-instruct", name: "LLaMA 3.3 70B", note: "Empfohlen" },
19+
{ id: "gemma-3-27b-it", name: "Gemma 3 27B", note: "Schnell" },
20+
{ id: "deepseek-r1-distill-llama-70b", name: "DeepSeek R1 70B", note: "Reasoning" },
21+
{ id: "qwen3-235b-a22b", name: "Qwen3 235B", note: "Sehr stark" },
22+
{ id: "mistral-large-3-675b-instruct-2512", name: "Mistral Large 675B", note: "Größtes Modell" },
23+
{ id: "meta-llama-3.1-8b-instruct", name: "LLaMA 3.1 8B", note: "Sehr schnell" },
24+
];
25+
26+
interface Props {
27+
onBack: () => void;
28+
}
29+
30+
interface AdminAiData {
31+
settings: AiSettings;
32+
apiConfigured: boolean;
33+
fallbackConfigured: boolean;
34+
envModel: string | null;
35+
envApiUrl: string | null;
36+
}
37+
38+
function RoleLimitControl({
39+
label,
40+
enabled,
41+
requestsPerHour,
42+
onEnabledChange,
43+
onLimitChange,
44+
}: {
45+
label: string;
46+
enabled: boolean;
47+
requestsPerHour: number | null;
48+
onEnabledChange: (v: boolean) => void;
49+
onLimitChange: (v: number | null) => void;
50+
}) {
51+
const [unlimited, setUnlimited] = useState(requestsPerHour === null);
52+
const [localValue, setLocalValue] = useState(requestsPerHour ?? 5);
53+
54+
const handleUnlimitedToggle = (checked: boolean) => {
55+
setUnlimited(checked);
56+
onLimitChange(checked ? null : localValue);
57+
};
58+
59+
const handleValueChange = (val: string) => {
60+
const num = parseInt(val, 10);
61+
if (!isNaN(num) && num >= 0) {
62+
setLocalValue(num);
63+
onLimitChange(num);
64+
}
65+
};
66+
67+
return (
68+
<div className="p-3 rounded-lg border border-border bg-muted/20">
69+
<div className="flex items-center justify-between mb-2">
70+
<span className="text-sm font-medium">{label}</span>
71+
<Switch checked={enabled} onCheckedChange={onEnabledChange} />
72+
</div>
73+
{enabled && (
74+
<div className="flex items-center gap-3 mt-2">
75+
<div className="flex items-center gap-2">
76+
<Switch
77+
checked={unlimited}
78+
onCheckedChange={handleUnlimitedToggle}
79+
id={`unlimited-${label}`}
80+
/>
81+
<Label htmlFor={`unlimited-${label}`} className="text-xs text-muted-foreground flex items-center gap-1">
82+
<Infinity className="w-3 h-3" /> Unbegrenzt
83+
</Label>
84+
</div>
85+
{!unlimited && (
86+
<div className="flex items-center gap-1">
87+
<Input
88+
type="number"
89+
min={0}
90+
max={1000}
91+
value={localValue}
92+
onChange={(e) => handleValueChange(e.target.value)}
93+
className="w-20 h-7 text-xs"
94+
/>
95+
<span className="text-xs text-muted-foreground">/Stunde</span>
96+
</div>
97+
)}
98+
</div>
99+
)}
100+
</div>
101+
);
102+
}
103+
104+
export function AiSettingsPanel({ onBack }: Props) {
105+
const { t } = useTranslation();
106+
const { toast } = useToast();
107+
108+
const { data, isLoading } = useQuery<AdminAiData>({
109+
queryKey: ["/api/v1/ai/admin/settings"],
110+
});
111+
112+
const [localSettings, setLocalSettings] = useState<AiSettings | null>(null);
113+
const settings: AiSettings | null = localSettings ?? data?.settings ?? null;
114+
115+
const saveMutation = useMutation({
116+
mutationFn: (s: AiSettings) =>
117+
apiRequest("PUT", "/api/v1/ai/admin/settings", s),
118+
onSuccess: () => {
119+
queryClient.invalidateQueries({ queryKey: ["/api/v1/ai/admin/settings"] });
120+
queryClient.invalidateQueries({ queryKey: ["/api/v1/ai/status"] });
121+
toast({ title: "KI-Einstellungen gespeichert" });
122+
},
123+
onError: () => {
124+
toast({ title: "Fehler beim Speichern", variant: "destructive" });
125+
},
126+
});
127+
128+
const update = (patch: Partial<AiSettings>) => {
129+
if (!settings) return;
130+
setLocalSettings({ ...settings, ...patch });
131+
};
132+
133+
const handleSave = () => {
134+
if (!settings) return;
135+
saveMutation.mutate(settings);
136+
};
137+
138+
if (isLoading || !settings) {
139+
return <div className="p-8 text-center text-muted-foreground">Lade...</div>;
140+
}
141+
142+
const apiOk = data?.apiConfigured;
143+
144+
return (
145+
<div className="space-y-6">
146+
<div className="flex items-center gap-3">
147+
<Button variant="ghost" size="sm" onClick={onBack}>
148+
<ArrowLeft className="w-4 h-4 mr-1" /> Zurück
149+
</Button>
150+
<div className="flex items-center gap-2">
151+
<Bot className="w-5 h-5 text-primary" />
152+
<h2 className="text-xl font-semibold">KI-Assistent</h2>
153+
<Badge variant="secondary" className="bg-amber-500/20 text-amber-600 dark:text-amber-400 border-amber-500/30">
154+
Beta
155+
</Badge>
156+
</div>
157+
</div>
158+
159+
{!apiOk && (
160+
<Alert className="border-amber-500/50 bg-amber-50 dark:bg-amber-950/20">
161+
<Info className="w-4 h-4 text-amber-600" />
162+
<AlertDescription className="text-amber-700 dark:text-amber-400">
163+
<strong>AI_API_KEY</strong> ist nicht gesetzt. Hinterlege den GWDG SAIA Bearer Token als Secret,
164+
damit die KI-Funktion genutzt werden kann.
165+
</AlertDescription>
166+
</Alert>
167+
)}
168+
169+
<div className="grid gap-6">
170+
<Card>
171+
<CardHeader>
172+
<CardTitle className="text-base flex items-center gap-2">
173+
<Zap className="w-4 h-4" /> Allgemein
174+
</CardTitle>
175+
</CardHeader>
176+
<CardContent className="space-y-4">
177+
<div className="flex items-center justify-between">
178+
<div>
179+
<Label>KI-Funktion aktivieren</Label>
180+
<p className="text-xs text-muted-foreground mt-0.5">
181+
Aktiviert den KI-Assistenten für alle konfigurierten Rollen
182+
</p>
183+
</div>
184+
<Switch
185+
checked={settings.enabled}
186+
onCheckedChange={(v) => update({ enabled: v })}
187+
/>
188+
</div>
189+
190+
<div className="space-y-1.5">
191+
<Label>Modell</Label>
192+
<Select
193+
value={settings.model}
194+
onValueChange={(v) => update({ model: v })}
195+
>
196+
<SelectTrigger>
197+
<SelectValue />
198+
</SelectTrigger>
199+
<SelectContent>
200+
{GWDG_MODELS.map((m) => (
201+
<SelectItem key={m.id} value={m.id}>
202+
<span>{m.name}</span>
203+
<span className="ml-2 text-xs text-muted-foreground">({m.note})</span>
204+
</SelectItem>
205+
))}
206+
</SelectContent>
207+
</Select>
208+
{data?.envModel && (
209+
<p className="text-xs text-muted-foreground">
210+
Env-Override aktiv: <code className="font-mono">{data.envModel}</code>
211+
</p>
212+
)}
213+
</div>
214+
215+
<div className="flex items-center gap-3 text-sm">
216+
<div className="flex items-center gap-1.5">
217+
{apiOk ? (
218+
<CheckCircle className="w-4 h-4 text-green-500" />
219+
) : (
220+
<XCircle className="w-4 h-4 text-red-500" />
221+
)}
222+
<span className="text-muted-foreground">
223+
API-Key: {apiOk ? "Konfiguriert" : "Nicht gesetzt"}
224+
</span>
225+
</div>
226+
{data?.fallbackConfigured && (
227+
<div className="flex items-center gap-1.5">
228+
<CheckCircle className="w-4 h-4 text-green-500" />
229+
<span className="text-muted-foreground">Fallback-Key: Konfiguriert</span>
230+
</div>
231+
)}
232+
</div>
233+
</CardContent>
234+
</Card>
235+
236+
<Card>
237+
<CardHeader>
238+
<CardTitle className="text-base">Rate-Limiting pro Rolle</CardTitle>
239+
<CardDescription>
240+
Kontingente pro Stunde. Unbegrenzt = keine Begrenzung, 0 = deaktiviert.
241+
</CardDescription>
242+
</CardHeader>
243+
<CardContent className="space-y-3">
244+
<RoleLimitControl
245+
label="Gäste (nicht angemeldet)"
246+
enabled={settings.guestLimits.enabled}
247+
requestsPerHour={settings.guestLimits.requestsPerHour}
248+
onEnabledChange={(v) =>
249+
update({ guestLimits: { ...settings.guestLimits, enabled: v } })
250+
}
251+
onLimitChange={(v) =>
252+
update({ guestLimits: { ...settings.guestLimits, requestsPerHour: v } })
253+
}
254+
/>
255+
<RoleLimitControl
256+
label="Angemeldete Nutzer"
257+
enabled={settings.userLimits.enabled}
258+
requestsPerHour={settings.userLimits.requestsPerHour}
259+
onEnabledChange={(v) =>
260+
update({ userLimits: { ...settings.userLimits, enabled: v } })
261+
}
262+
onLimitChange={(v) =>
263+
update({ userLimits: { ...settings.userLimits, requestsPerHour: v } })
264+
}
265+
/>
266+
<RoleLimitControl
267+
label="Administratoren"
268+
enabled={settings.adminLimits.enabled}
269+
requestsPerHour={settings.adminLimits.requestsPerHour}
270+
onEnabledChange={(v) =>
271+
update({ adminLimits: { ...settings.adminLimits, enabled: v } })
272+
}
273+
onLimitChange={(v) =>
274+
update({ adminLimits: { ...settings.adminLimits, requestsPerHour: v } })
275+
}
276+
/>
277+
</CardContent>
278+
</Card>
279+
</div>
280+
281+
<div className="flex gap-2">
282+
<Button onClick={handleSave} disabled={saveMutation.isPending}>
283+
{saveMutation.isPending ? "Speichert..." : "Einstellungen speichern"}
284+
</Button>
285+
<Button variant="outline" onClick={onBack}>
286+
Abbrechen
287+
</Button>
288+
</div>
289+
</div>
290+
);
291+
}

client/src/components/admin/settings/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ export { CalendarSettingsPanel } from './CalendarSettingsPanel';
1010
export { MatrixSettingsPanel } from './MatrixSettingsPanel';
1111
export { PentestToolsPanel } from './PentestToolsPanel';
1212
export { WCAGAccessibilityPanel } from './WCAGAccessibilityPanel';
13+
export { AiSettingsPanel } from './AiSettingsPanel';

0 commit comments

Comments
 (0)