mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-25 00:16:41 +02:00
feat: Anthropic API-Key über Admin-UI konfigurierbar
- GET /settings/ai-config: gibt { configured: boolean } zurück
- POST /settings/ai-config: speichert Key Base64-kodiert in Redis (PLATFORM_ADMIN)
- HelpService: dynamische Key-Auflösung aus Redis mit 60s In-Memory-Cache
- AdminAiSettingsPage: neue Admin-Seite /admin/ai-settings
- HelpPanel: zeigt Hinweis + Link wenn KI nicht konfiguriert
- Env-Variable ANTHROPIC_API_KEY hat weiterhin Vorrang (backward compat.)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c96ccb5fcc
commit
0c8a23ddc4
8 changed files with 495 additions and 87 deletions
84
Summarize.md
84
Summarize.md
|
|
@ -1,8 +1,88 @@
|
|||
# INSIGHT MVP - Aenderungsprotokoll
|
||||
|
||||
## Stand: 2026-03-13
|
||||
## Stand: 2026-03-15
|
||||
|
||||
### Aktueller Sprint: CRM Phase 3 — Kanban-Board + Microsoft 365 OAuth-Integration (Feature-Branch: feature/crm-service)
|
||||
### Aktueller Sprint: CRM Phase 3 + Stammdaten + Reporting + Hilfesystem + KI-Einstellungen (Feature-Branch: feature/crm-service)
|
||||
|
||||
---
|
||||
|
||||
### Aenderungen 2026-03-15 (20): KI-Einstellungen über Admin-UI konfigurierbar
|
||||
|
||||
#### Feature: Anthropic API-Key über Admin-UI setzen
|
||||
|
||||
**Problem**: `ANTHROPIC_API_KEY` war nur per `.env`-Datei setzbar — Admins ohne Server-Zugang konnten den KI-Chat nicht aktivieren.
|
||||
|
||||
**Backend (Core Service)**
|
||||
- `src/core/settings/settings.controller.ts` — 2 neue Endpoints:
|
||||
- `GET /settings/ai-config` — gibt `{ configured: boolean }` zurück (Key wird niemals zurückgesendet)
|
||||
- `POST /settings/ai-config` — speichert Key Base64-kodiert in Redis unter `platform_ai_config`; leerer String löscht den Key (@Roles PLATFORM_ADMIN)
|
||||
- `src/core/help/help.service.ts` — RedisService injiziert; Key wird per `getApiKey()` dynamisch geladen (Env-Variable hat Vorrang, 60s In-Memory-Cache); Cache-Invalidierung bei 401-Fehler
|
||||
- `src/core/help/help.module.ts` — RedisModule importiert
|
||||
|
||||
**Frontend**
|
||||
- `src/admin/AdminAiSettingsPage.tsx` (neu) — Admin-Seite mit Status-Anzeige, Password-Input, Speichern/Entfernen
|
||||
- `src/admin/AdminLayout.tsx` — Tab "KI-Einstellungen" ergänzt
|
||||
- `src/shell/App.tsx` — Route `/admin/ai-settings` + Import ergänzt
|
||||
- `src/components/HelpPanel/HelpPanel.tsx` — `useQuery(['settings', 'ai-config'])`: wenn nicht konfiguriert → Hinweis mit Link zu Admin-Seite statt Chat-Input
|
||||
|
||||
**Sicherheit**: GET gibt niemals den Key zurück. Base64-Kodierung (konsistent mit Projektstil). Backward-Kompatibilität: `ANTHROPIC_API_KEY` in `.env` hat weiterhin Vorrang.
|
||||
|
||||
---
|
||||
|
||||
### Aenderungen 2026-03-15 (19): Stammdaten, CRM Reporting, Hilfesystem
|
||||
|
||||
#### Feature 1: Stammdaten (Master Data)
|
||||
|
||||
**Backend (Core Service)**
|
||||
- `prisma/core.schema.prisma` — 5 neue Modelle: `Department`, `Location`, `CostCenter`, `JobTitle`, `SkillCategory` (Tabellen: departments, locations, cost_centers, job_titles, skill_categories)
|
||||
- `src/core/master-data/master-data.service.ts` — CRUD-Service für alle 5 Stammdaten-Entitäten
|
||||
- `src/core/master-data/master-data.controller.ts` — REST-Controller mit Public-Endpunkten (Dropdowns) + Admin-Endpunkten (@Roles('PLATFORM_ADMIN'))
|
||||
- `src/core/master-data/master-data.module.ts` — NestJS-Modul
|
||||
- `src/app.module.ts` — MasterDataModule registriert
|
||||
|
||||
**Frontend**
|
||||
- `src/admin/AdminMasterDataPage.tsx` — Admin-Seite mit 5 Tabs, inline Edit/Delete/Create
|
||||
- `src/admin/AdminLayout.tsx` — Tab "Stammdaten" ergänzt
|
||||
- `src/shell/App.tsx` — Route `/admin/master-data` ergänzt
|
||||
|
||||
**Deployment-Hinweis**: `prisma migrate deploy` auf Server erforderlich + Rebuild core-service + frontend
|
||||
|
||||
---
|
||||
|
||||
#### Feature 2: CRM Reporting
|
||||
|
||||
**Backend (CRM Service)**
|
||||
- `src/deals/deals.service.ts` — Methode `getStats()`: Win/Loss-Rate, Umsatz, Avg-Deal-Value, LostByReason, MonthlyTrend (12 Monate)
|
||||
- `src/deals/dto/stats-query.dto.ts` — `StatsQueryDto` mit Period-Enum (MONTH/QUARTER/YEAR)
|
||||
- `src/deals/deals.controller.ts` — GET `/deals/stats` Endpunkt
|
||||
- `src/activities/activities.service.ts` — Methode `getStats()`: Aktivitäten nach Typ, Erledigungsrate, Upcoming Tasks
|
||||
- `src/activities/activities.controller.ts` — GET `/activities/stats` Endpunkt
|
||||
|
||||
**Frontend**
|
||||
- `package.json` — `recharts ^2.12.0` als Dependency
|
||||
- `src/crm/types.ts` — Interfaces `DealStats`, `ActivityStats`, `MonthlyTrendEntry`, `LostByReasonEntry`, `ActivityStatsByType`
|
||||
- `src/crm/api.ts` — `dealStatsApi.get()`, `activitiesApi.getStats()` ergänzt
|
||||
- `src/crm/hooks.ts` — `useDealStats()`, `useActivityStats()` Hooks
|
||||
- `src/crm/reports/ReportsPage.tsx` — Reporting-Seite mit Period-Selector, Deal/Aktivitäts-Tabs, LineChart/BarChart/PieChart (recharts)
|
||||
- `src/shell/App.tsx` — Route `/crm/reports`
|
||||
- `src/shell/AppLayout.tsx` — Sidebar-Link "Reports" nach Kanban
|
||||
|
||||
---
|
||||
|
||||
#### Feature 3: Hilfesystem (KI-Chat + Tooltips + Panel)
|
||||
|
||||
**Backend (Core Service)**
|
||||
- `package.json` — `@anthropic-ai/sdk ^0.37.0`
|
||||
- `src/config/env.validation.ts` — `ANTHROPIC_API_KEY` (optional)
|
||||
- `src/core/help/help.service.ts` — Claude Haiku Integration, Chat-Methode mit System-Prompt
|
||||
- `src/core/help/help.controller.ts` — POST `/help/chat`
|
||||
- `src/core/help/help.module.ts` — NestJS-Modul
|
||||
- `src/app.module.ts` — HelpModule registriert
|
||||
|
||||
**Frontend**
|
||||
- `src/components/HelpTooltip/HelpTooltip.tsx` + `index.ts` — Hover-Tooltip mit ❓-Icon
|
||||
- `src/components/HelpPanel/HelpPanel.tsx` + `index.ts` — Side-Panel mit Kontexthilfe + KI-Chat
|
||||
- `src/shell/AppLayout.tsx` — ❓-Button in Topbar + HelpPanel-Integration
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { HelpController } from './help.controller';
|
||||
import { HelpService } from './help.service';
|
||||
import { RedisModule } from '../../redis/redis.module';
|
||||
|
||||
@Module({
|
||||
imports: [RedisModule],
|
||||
controllers: [HelpController],
|
||||
providers: [HelpService],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,42 +1,76 @@
|
|||
import { Injectable, Logger, ServiceUnavailableException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
import { RedisService } from '../../redis/redis.service';
|
||||
|
||||
export interface ChatMessage {
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
}
|
||||
|
||||
const AI_CONFIG_KEY = 'platform_ai_config';
|
||||
|
||||
@Injectable()
|
||||
export class HelpService {
|
||||
private readonly logger = new Logger(HelpService.name);
|
||||
private client: Anthropic | null = null;
|
||||
|
||||
constructor(private readonly config: ConfigService) {
|
||||
const apiKey = this.config.get<string>('ANTHROPIC_API_KEY');
|
||||
if (apiKey) {
|
||||
this.client = new Anthropic({ apiKey });
|
||||
this.logger.log('KI-Hilfe-Chat aktiviert');
|
||||
} else {
|
||||
this.logger.warn('ANTHROPIC_API_KEY nicht konfiguriert — KI-Chat deaktiviert');
|
||||
// In-Memory-Cache für den API-Key (60s TTL) — vermeidet Redis-Overhead pro Request
|
||||
private cachedKey: string | null = null;
|
||||
private cacheExpiry = 0;
|
||||
|
||||
constructor(
|
||||
private readonly config: ConfigService,
|
||||
private readonly redis: RedisService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Liest den Anthropic API-Key.
|
||||
* Priorität: Env-Variable (backward compat.) → Redis (Admin-UI-Konfiguration).
|
||||
* 60s In-Memory-Cache.
|
||||
*/
|
||||
private async getApiKey(): Promise<string | null> {
|
||||
// 1. Env-Variable hat Vorrang
|
||||
const envKey = this.config.get<string>('ANTHROPIC_API_KEY');
|
||||
if (envKey) return envKey;
|
||||
|
||||
// 2. Redis-Cache prüfen
|
||||
if (this.cachedKey !== null && Date.now() < this.cacheExpiry) {
|
||||
return this.cachedKey;
|
||||
}
|
||||
|
||||
// 3. Aus Redis laden (Base64-dekodieren)
|
||||
const raw = await this.redis.get(AI_CONFIG_KEY);
|
||||
this.cachedKey = raw ? Buffer.from(raw, 'base64').toString('utf-8') : null;
|
||||
this.cacheExpiry = Date.now() + 60_000;
|
||||
|
||||
if (this.cachedKey) {
|
||||
this.logger.debug('Anthropic API-Key aus Redis geladen');
|
||||
}
|
||||
|
||||
return this.cachedKey;
|
||||
}
|
||||
|
||||
async chat(messages: ChatMessage[], context?: string): Promise<{ reply: string }> {
|
||||
if (!this.client) {
|
||||
throw new ServiceUnavailableException('KI-Chat nicht konfiguriert. Bitte ANTHROPIC_API_KEY setzen.');
|
||||
const apiKey = await this.getApiKey();
|
||||
|
||||
if (!apiKey) {
|
||||
throw new ServiceUnavailableException(
|
||||
'KI-Chat nicht konfiguriert. Bitte einen Anthropic API-Key unter Admin > KI-Einstellungen hinterlegen.',
|
||||
);
|
||||
}
|
||||
|
||||
const client = new Anthropic({ apiKey });
|
||||
|
||||
const systemPrompt = [
|
||||
'Du bist ein freundlicher Hilfsassistent für INSIGHT, eine Business-Plattform für Mittelstandsunternehmen.',
|
||||
'INSIGHT enthält folgende Bereiche: CRM (Unternehmen, Kontakte, Deals, Aktivitäten, Kanban, Reports), Expertenprofil (Skills, Projekte, Zertifizierungen), Dashboard (E-Mail, Kalender, Aufgaben via Microsoft 365), Admin-Bereich (Benutzerverwaltung, Branding, Firmendaten, Stammdaten).',
|
||||
'INSIGHT enthält folgende Bereiche: CRM (Unternehmen, Kontakte, Deals, Aktivitäten, Kanban, Reports), Expertenprofil (Skills, Projekte, Zertifizierungen), Dashboard (E-Mail, Kalender, Aufgaben via Microsoft 365), Admin-Bereich (Benutzerverwaltung, Branding, Firmendaten, Stammdaten, KI-Einstellungen).',
|
||||
'Beantworte Fragen zur Bedienung der Plattform kompakt und hilfsbereit auf Deutsch.',
|
||||
'Halte Antworten kurz (max. 3-4 Sätze). Wenn du etwas nicht weißt, sag das ehrlich.',
|
||||
context ? `Aktueller Kontext: ${context}` : '',
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
try {
|
||||
const response = await this.client.messages.create({
|
||||
const response = await client.messages.create({
|
||||
model: 'claude-haiku-4-5',
|
||||
max_tokens: 1024,
|
||||
system: systemPrompt,
|
||||
|
|
@ -50,6 +84,11 @@ export class HelpService {
|
|||
|
||||
return { reply };
|
||||
} catch (err) {
|
||||
// Bei ungültigem API-Key Cache invalidieren
|
||||
if ((err as { status?: number }).status === 401) {
|
||||
this.cachedKey = null;
|
||||
this.cacheExpiry = 0;
|
||||
}
|
||||
this.logger.error('Claude API Fehler:', err);
|
||||
throw new ServiceUnavailableException('KI-Chat vorübergehend nicht verfügbar.');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ const EXTERNAL_LINKS_KEY = 'platform_external_links';
|
|||
const BRANDING_LOGO_KEY = 'platform_branding_logo';
|
||||
const SSL_CONFIG_KEY = 'platform_ssl_config';
|
||||
const COMPANY_SETTINGS_KEY = 'platform_company_settings';
|
||||
const AI_CONFIG_KEY = 'platform_ai_config';
|
||||
|
||||
interface CompanySettings {
|
||||
name: string | null;
|
||||
|
|
@ -482,6 +483,45 @@ export class SettingsController {
|
|||
return { success: true };
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// KI-Konfiguration (Anthropic API-Key)
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* GET /api/v1/settings/ai-config
|
||||
* Prüft ob ein Anthropic API-Key konfiguriert ist (gibt den Key NICHT zurück).
|
||||
*/
|
||||
@Get('ai-config')
|
||||
@ApiOperation({ summary: 'KI-Konfiguration: Status prüfen' })
|
||||
async getAiConfig(): Promise<{ configured: boolean }> {
|
||||
const raw = await this.redis.get(AI_CONFIG_KEY);
|
||||
return { configured: !!raw };
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/settings/ai-config
|
||||
* Anthropic API-Key speichern oder entfernen (nur PLATFORM_ADMIN).
|
||||
* Leerer apiKey = Key entfernen (KI-Chat deaktivieren).
|
||||
*/
|
||||
@Post('ai-config')
|
||||
@Roles('PLATFORM_ADMIN')
|
||||
@UseGuards(RolesGuard)
|
||||
@ApiOperation({ summary: 'Anthropic API-Key speichern (Admin)' })
|
||||
async saveAiConfig(
|
||||
@Body() body: { apiKey: string },
|
||||
): Promise<{ success: boolean }> {
|
||||
const key = body.apiKey?.trim();
|
||||
if (key) {
|
||||
// Base64-kodiert speichern — kein Plaintext im Redis-Dump
|
||||
await this.redis.set(AI_CONFIG_KEY, Buffer.from(key).toString('base64'));
|
||||
this.logger.log('Anthropic API-Key gespeichert');
|
||||
} else {
|
||||
await this.redis.del(AI_CONFIG_KEY);
|
||||
this.logger.log('Anthropic API-Key entfernt');
|
||||
}
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// SSL / Domain Configuration
|
||||
// ============================================================
|
||||
|
|
|
|||
198
packages/frontend/src/admin/AdminAiSettingsPage.tsx
Normal file
198
packages/frontend/src/admin/AdminAiSettingsPage.tsx
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
import { useState } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import api from '../api/client';
|
||||
|
||||
const cardStyle: React.CSSProperties = {
|
||||
background: 'var(--color-bg-card)',
|
||||
borderRadius: 'var(--radius-md)',
|
||||
boxShadow: 'var(--shadow-sm)',
|
||||
border: '1px solid var(--color-border)',
|
||||
padding: '1.5rem',
|
||||
marginBottom: '1.5rem',
|
||||
};
|
||||
|
||||
const labelStyle: React.CSSProperties = {
|
||||
display: 'block',
|
||||
fontSize: '0.75rem',
|
||||
fontWeight: 600,
|
||||
color: 'var(--color-text-secondary)',
|
||||
marginBottom: '0.25rem',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.04em',
|
||||
};
|
||||
|
||||
const inputStyle: React.CSSProperties = {
|
||||
width: '100%',
|
||||
padding: '0.5rem 0.75rem',
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
fontSize: '0.875rem',
|
||||
background: 'var(--color-bg)',
|
||||
color: 'var(--color-text)',
|
||||
boxSizing: 'border-box',
|
||||
fontFamily: 'monospace',
|
||||
};
|
||||
|
||||
export function AdminAiSettingsPage() {
|
||||
const qc = useQueryClient();
|
||||
const [apiKey, setApiKey] = useState('');
|
||||
const [saved, setSaved] = useState(false);
|
||||
|
||||
const { data, isLoading } = useQuery<{ configured: boolean }>({
|
||||
queryKey: ['settings', 'ai-config'],
|
||||
queryFn: () => api.get<{ configured: boolean }>('/settings/ai-config').then(r => r.data),
|
||||
});
|
||||
|
||||
const saveMutation = useMutation({
|
||||
mutationFn: (key: string) => api.post('/settings/ai-config', { apiKey: key }),
|
||||
onSuccess: () => {
|
||||
void qc.invalidateQueries({ queryKey: ['settings', 'ai-config'] });
|
||||
setApiKey('');
|
||||
setSaved(true);
|
||||
setTimeout(() => setSaved(false), 3000);
|
||||
},
|
||||
});
|
||||
|
||||
const removeMutation = useMutation({
|
||||
mutationFn: () => api.post('/settings/ai-config', { apiKey: '' }),
|
||||
onSuccess: () => {
|
||||
void qc.invalidateQueries({ queryKey: ['settings', 'ai-config'] });
|
||||
},
|
||||
});
|
||||
|
||||
const configured = data?.configured ?? false;
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 580 }}>
|
||||
<div style={cardStyle}>
|
||||
<h2 style={{ margin: '0 0 0.25rem', fontSize: '1.125rem', fontWeight: 700 }}>
|
||||
KI-Einstellungen
|
||||
</h2>
|
||||
<p style={{ margin: '0 0 1.5rem', color: 'var(--color-text-secondary)', fontSize: '0.875rem' }}>
|
||||
Konfigurieren Sie den Anthropic API-Key für den KI-Hilfe-Chat (❓ im Topbar).
|
||||
</p>
|
||||
|
||||
{/* Status */}
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: '0.5rem',
|
||||
marginBottom: '1.5rem',
|
||||
padding: '0.75rem 1rem',
|
||||
background: configured ? 'rgba(22,163,74,0.08)' : 'rgba(220,38,38,0.08)',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
border: `1px solid ${configured ? 'rgba(22,163,74,0.2)' : 'rgba(220,38,38,0.2)'}`,
|
||||
}}>
|
||||
<span style={{ fontSize: '0.875rem' }}>
|
||||
{configured ? '🟢' : '🔴'}
|
||||
</span>
|
||||
<span style={{
|
||||
fontSize: '0.875rem', fontWeight: 600,
|
||||
color: configured ? 'var(--color-success, #16a34a)' : 'var(--color-danger, #dc2626)',
|
||||
}}>
|
||||
{isLoading ? 'Prüfe...' : configured ? 'API-Key konfiguriert' : 'Kein API-Key hinterlegt'}
|
||||
</span>
|
||||
{configured && (
|
||||
<span style={{ fontSize: '0.8125rem', color: 'var(--color-text-secondary)', marginLeft: 'auto' }}>
|
||||
KI-Chat aktiv
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Neuen Key setzen */}
|
||||
<div style={{ marginBottom: '1rem' }}>
|
||||
<label style={labelStyle}>
|
||||
{configured ? 'Key ersetzen' : 'API-Key hinterlegen'}
|
||||
</label>
|
||||
<input
|
||||
style={inputStyle}
|
||||
type="password"
|
||||
value={apiKey}
|
||||
onChange={e => setApiKey(e.target.value)}
|
||||
placeholder="sk-ant-api03-..."
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
/>
|
||||
<p style={{ margin: '0.375rem 0 0', fontSize: '0.75rem', color: 'var(--color-text-secondary)' }}>
|
||||
Den API-Key erhalten Sie unter{' '}
|
||||
<a href="https://console.anthropic.com/settings/keys" target="_blank" rel="noopener noreferrer"
|
||||
style={{ color: 'var(--color-primary)' }}>
|
||||
console.anthropic.com
|
||||
</a>
|
||||
. Der Key wird verschlüsselt gespeichert und niemals zurückgegeben.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', flexWrap: 'wrap' }}>
|
||||
<button
|
||||
style={{
|
||||
padding: '0.5rem 1.25rem',
|
||||
background: 'var(--color-primary)',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 600,
|
||||
cursor: apiKey.trim() && !saveMutation.isPending ? 'pointer' : 'not-allowed',
|
||||
opacity: apiKey.trim() && !saveMutation.isPending ? 1 : 0.5,
|
||||
}}
|
||||
onClick={() => { if (apiKey.trim()) saveMutation.mutate(apiKey); }}
|
||||
disabled={!apiKey.trim() || saveMutation.isPending}
|
||||
>
|
||||
{saveMutation.isPending ? 'Speichern…' : 'Speichern'}
|
||||
</button>
|
||||
|
||||
{configured && (
|
||||
<button
|
||||
style={{
|
||||
padding: '0.5rem 1rem',
|
||||
background: 'none',
|
||||
color: 'var(--color-danger, #dc2626)',
|
||||
border: '1px solid var(--color-danger, #dc2626)',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 500,
|
||||
cursor: removeMutation.isPending ? 'not-allowed' : 'pointer',
|
||||
opacity: removeMutation.isPending ? 0.5 : 1,
|
||||
}}
|
||||
onClick={() => {
|
||||
if (confirm('API-Key wirklich entfernen? Der KI-Chat wird deaktiviert.')) {
|
||||
removeMutation.mutate();
|
||||
}
|
||||
}}
|
||||
disabled={removeMutation.isPending}
|
||||
>
|
||||
{removeMutation.isPending ? 'Entfernen…' : 'Key entfernen'}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{saved && (
|
||||
<span style={{ color: 'var(--color-success, #16a34a)', fontSize: '0.875rem', fontWeight: 500 }}>
|
||||
Gespeichert ✓
|
||||
</span>
|
||||
)}
|
||||
{(saveMutation.isError || removeMutation.isError) && (
|
||||
<span style={{ color: 'var(--color-danger, #dc2626)', fontSize: '0.875rem' }}>
|
||||
Fehler beim Speichern
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info-Box */}
|
||||
<div style={{
|
||||
...cardStyle,
|
||||
background: 'var(--color-bg)',
|
||||
marginBottom: 0,
|
||||
}}>
|
||||
<h3 style={{ margin: '0 0 0.75rem', fontSize: '0.875rem', fontWeight: 600, color: 'var(--color-text-secondary)' }}>
|
||||
ℹ️ Hinweise
|
||||
</h3>
|
||||
<ul style={{ margin: 0, paddingLeft: '1.25rem', fontSize: '0.8125rem', color: 'var(--color-text-secondary)', lineHeight: 1.6 }}>
|
||||
<li>Der KI-Hilfe-Chat nutzt <strong>Claude Haiku</strong> (kostengünstiges Modell, ~0,001 $ pro Anfrage).</li>
|
||||
<li>Empfehlung: Usage-Limits im Anthropic-Dashboard setzen.</li>
|
||||
<li>Eine in der Server-<code>.env</code> gesetzte Variable <code>ANTHROPIC_API_KEY</code> hat Vorrang vor dem hier hinterlegten Key.</li>
|
||||
<li>Änderungen werden innerhalb von 60 Sekunden wirksam.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ const tabs = [
|
|||
{ to: '/admin/profile-access', label: 'Profilzugriff' },
|
||||
{ to: '/admin/crm-settings', label: 'CRM Sichtbarkeit' },
|
||||
{ to: '/admin/master-data', label: 'Stammdaten' },
|
||||
{ to: '/admin/ai-settings', label: 'KI-Einstellungen' },
|
||||
];
|
||||
|
||||
export function AdminLayout() {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { useState, useRef, useEffect } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import api from '../../api/client';
|
||||
|
||||
interface ChatMessage {
|
||||
|
|
@ -23,6 +25,10 @@ const HELP_TEXTS: Record<string, { title: string; text: string }> = {
|
|||
title: 'Stammdaten',
|
||||
text: 'Verwalten Sie Referenzlisten wie Abteilungen, Standorte, Kostenstellen, Stellenbezeichnungen und Skill-Kategorien. Diese Listen werden in anderen Bereichen als Auswahloptionen verwendet.',
|
||||
},
|
||||
'admin-ai-settings': {
|
||||
title: 'KI-Einstellungen',
|
||||
text: 'Hinterlegen Sie hier Ihren Anthropic API-Key, um den KI-Assistenten zu aktivieren. Der Key wird sicher gespeichert und niemals zurückgegeben. Ohne Key stehen nur die statischen Hilfetexte zur Verfügung.',
|
||||
},
|
||||
'crm-deals': {
|
||||
title: 'Deals',
|
||||
text: 'Verwalten Sie Ihre Verkaufschancen. Jeder Deal hat einen Wert, eine Stage in Ihrer Pipeline und einen Status (Offen, Gewonnen, Verloren). Nutzen Sie das Kanban-Board für eine visuelle Übersicht.',
|
||||
|
|
@ -49,6 +55,16 @@ export function HelpPanel({ isOpen, onClose, pageKey }: Props) {
|
|||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { data: aiConfig } = useQuery({
|
||||
queryKey: ['settings', 'ai-config'],
|
||||
queryFn: () => api.get<{ configured: boolean }>('/settings/ai-config').then(r => r.data),
|
||||
staleTime: 60_000,
|
||||
enabled: isOpen,
|
||||
});
|
||||
|
||||
const aiConfigured = aiConfig?.configured ?? true; // optimistic default until loaded
|
||||
|
||||
const helpContent = HELP_TEXTS[pageKey] ?? {
|
||||
title: 'Hilfe',
|
||||
|
|
@ -141,6 +157,34 @@ export function HelpPanel({ isOpen, onClose, pageKey }: Props) {
|
|||
</p>
|
||||
</div>
|
||||
|
||||
{!aiConfigured ? (
|
||||
/* AI not configured — show hint */
|
||||
<div style={{
|
||||
flex: 1,
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
|
||||
padding: '1.5rem 1.25rem', gap: '0.75rem', textAlign: 'center',
|
||||
}}>
|
||||
<span style={{ fontSize: '2rem' }}>🔑</span>
|
||||
<p style={{ margin: 0, fontSize: '0.8125rem', color: 'var(--color-text-secondary)', lineHeight: 1.5 }}>
|
||||
Der KI-Assistent ist noch nicht eingerichtet.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => { onClose(); navigate('/admin/ai-settings'); }}
|
||||
style={{
|
||||
padding: '0.5rem 1rem',
|
||||
background: 'var(--color-primary)',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '0.8125rem',
|
||||
}}
|
||||
>
|
||||
→ Admin: KI-Einstellungen
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Messages */}
|
||||
<div style={{ flex: 1, overflowY: 'auto', padding: '0 1.25rem', display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
||||
{messages.length === 0 && (
|
||||
|
|
@ -217,6 +261,8 @@ export function HelpPanel({ isOpen, onClose, pageKey }: Props) {
|
|||
➤
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { AdminProfileAccessPage } from '../admin/AdminProfileAccessPage';
|
|||
import { AdminProfileDetailPage } from '../admin/AdminProfileDetailPage';
|
||||
import { AdminCrmSettingsPage } from '../admin/AdminCrmSettingsPage';
|
||||
import { AdminMasterDataPage } from '../admin/AdminMasterDataPage';
|
||||
import { AdminAiSettingsPage } from '../admin/AdminAiSettingsPage';
|
||||
import { ProfilePage } from '../profile/ProfilePage';
|
||||
import { ContactsPage } from '../crm/contacts/ContactsPage';
|
||||
import { ContactDetailPage } from '../crm/contacts/ContactDetailPage';
|
||||
|
|
@ -100,6 +101,7 @@ export function App() {
|
|||
<Route path="profile-access" element={<AdminProfileAccessPage />} />
|
||||
<Route path="crm-settings" element={<AdminCrmSettingsPage />} />
|
||||
<Route path="master-data" element={<AdminMasterDataPage />} />
|
||||
<Route path="ai-settings" element={<AdminAiSettingsPage />} />
|
||||
</Route>
|
||||
{/* Admin-Profildetail außerhalb des Admin-Layouts (volle Seite) */}
|
||||
<Route path="admin/profiles/:userId" element={<AdminProfileDetailPage />} />
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue