Files
recipe-app/frontend/app/admin/ai/AiAdminClient.tsx
T

210 lines
9.8 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useEffect, useState } from 'react';
export interface AiModelInfo {
id: string;
name: string;
description: string;
model: string;
path: string;
trigger: string;
access: string;
}
const STORAGE_KEY = 'mistral_api_key_meta';
interface KeyMeta {
createdAt: string;
validityMonths: string;
}
interface Props {
keyHint: string;
hasKey: boolean;
aiFunctions: AiModelInfo[];
}
export default function AiAdminClient({ keyHint, hasKey, aiFunctions }: Props) {
const [meta, setMeta] = useState<KeyMeta>({ createdAt: '', validityMonths: '' });
useEffect(() => {
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) setMeta(JSON.parse(stored));
} catch {
// ignore
}
}, []);
const saveMeta = (patch: Partial<KeyMeta>) => {
const updated = { ...meta, ...patch };
setMeta(updated);
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
} catch {
// ignore
}
};
const { daysLeft, expiryDate } = computeExpiry(meta.createdAt, meta.validityMonths);
const modelChip = (model: string) => {
const color = model.includes('tiny') ? '#6366f1' : '#0ea5e9';
return (
<span style={{ fontSize: '0.78rem', background: color, color: '#fff', borderRadius: '4px', padding: '2px 7px', fontFamily: 'monospace', whiteSpace: 'nowrap' }}>
{model}
</span>
);
};
const accessChip = (access: string) => {
const isAdmin = access === 'Admin';
const isPremium = access.includes('Premium');
const bg = isAdmin ? '#7c3aed' : isPremium ? '#f59e0b' : '#10b981';
return (
<span style={{ fontSize: '0.75rem', background: bg, color: '#fff', borderRadius: '4px', padding: '2px 7px', whiteSpace: 'nowrap' }}>
{access}
</span>
);
};
return (
<div>
{/* API-nyckel */}
<section style={{ background: '#fff', border: '1px solid #e5e7eb', borderRadius: '10px', padding: '1.5rem', marginBottom: '2rem' }}>
<h2 style={{ fontSize: '1.05rem', marginBottom: '1rem', display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
🔑 Mistral API-nyckel
</h2>
{!hasKey && (
<div style={{ background: '#fef2f2', border: '1px solid #fecaca', borderRadius: '8px', padding: '0.75rem 1rem', marginBottom: '1rem', fontSize: '0.9rem', color: '#991b1b' }}>
<strong>MISTRAL_API_KEY är inte konfigurerad</strong> alla AI-funktioner är inaktiva tills nyckeln sätts i miljövariablerna.
</div>
)}
<div style={{ display: 'grid', gridTemplateColumns: 'auto 1fr', gap: '0.5rem 1.5rem', alignItems: 'center', marginBottom: '1.25rem' }}>
<span style={{ color: '#555', fontSize: '0.9rem' }}>Status</span>
<span>
{hasKey
? <span style={{ color: '#10b981', fontWeight: 600 }}> Konfigurerad</span>
: <span style={{ color: '#ef4444', fontWeight: 600 }}> Saknas (MISTRAL_API_KEY ej satt)</span>}
</span>
<span style={{ color: '#555', fontSize: '0.9rem' }}>Nyckel (sista 4)</span>
<code style={{ fontFamily: 'monospace', fontSize: '1rem', letterSpacing: '0.15em', background: '#f3f4f6', padding: '2px 8px', borderRadius: '4px' }}>
****{keyHint}
</code>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '1rem', alignItems: 'end' }}>
<label style={{ display: 'flex', flexDirection: 'column', gap: '0.3rem', fontSize: '0.85rem', color: '#374151' }}>
Skapad datum
<input
type="date"
value={meta.createdAt}
onChange={(e) => saveMeta({ createdAt: e.target.value })}
style={{ padding: '0.4rem 0.6rem', border: '1px solid #d1d5db', borderRadius: '6px', fontSize: '0.9rem' }}
/>
</label>
<label style={{ display: 'flex', flexDirection: 'column', gap: '0.3rem', fontSize: '0.85rem', color: '#374151' }}>
Giltighet (månader)
<input
type="number"
min="1"
max="120"
value={meta.validityMonths}
onChange={(e) => saveMeta({ validityMonths: e.target.value })}
placeholder="t.ex. 12"
style={{ padding: '0.4rem 0.6rem', border: '1px solid #d1d5db', borderRadius: '6px', fontSize: '0.9rem' }}
/>
</label>
<div style={{ paddingBottom: '2px' }}>
{expiryDate && daysLeft !== null ? (
<div style={{ background: daysLeft <= 14 ? '#fef2f2' : daysLeft <= 30 ? '#fffbeb' : '#f0fdf4', border: `1px solid ${daysLeft <= 14 ? '#fecaca' : daysLeft <= 30 ? '#fde68a' : '#bbf7d0'}`, borderRadius: '8px', padding: '0.6rem 0.9rem' }}>
<div style={{ fontSize: '0.75rem', color: '#6b7280', marginBottom: '2px' }}>Förfaller {expiryDate}</div>
<div style={{ fontWeight: 700, fontSize: '1.1rem', color: daysLeft <= 14 ? '#dc2626' : daysLeft <= 30 ? '#d97706' : '#16a34a' }}>
{daysLeft <= 0 ? '⚠️ Nyckel har förfallit!' : `${daysLeft} dagar kvar`}
</div>
</div>
) : (
<div style={{ color: '#9ca3af', fontSize: '0.85rem', padding: '0.6rem 0' }}>
Fyll i datum och giltighet för att se återstående tid
</div>
)}
</div>
</div>
</section>
{/* AI-funktioner */}
<section style={{ background: '#fff', border: '1px solid #e5e7eb', borderRadius: '10px', padding: '1.5rem' }}>
<h2 style={{ fontSize: '1.05rem', marginBottom: '1rem' }}> Implementerade AI-funktioner</h2>
<div style={{ overflowX: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.88rem' }}>
<thead>
<tr style={{ borderBottom: '2px solid #e5e7eb', textAlign: 'left' }}>
<th style={{ padding: '0.5rem 0.75rem', color: '#374151', fontWeight: 600 }}>Status</th>
<th style={{ padding: '0.5rem 0.75rem', color: '#374151', fontWeight: 600 }}>Funktion</th>
<th style={{ padding: '0.5rem 0.75rem', color: '#374151', fontWeight: 600 }}>Modell</th>
<th style={{ padding: '0.5rem 0.75rem', color: '#374151', fontWeight: 600 }}>Åtkomst</th>
<th style={{ padding: '0.5rem 0.75rem', color: '#374151', fontWeight: 600 }}>Utlösare</th>
<th style={{ padding: '0.5rem 0.75rem', color: '#374151', fontWeight: 600 }}>Sida</th>
</tr>
</thead>
<tbody>
{aiFunctions.map((fn, i) => (
<tr key={i} style={{ borderBottom: '1px solid #f3f4f6', verticalAlign: 'top', opacity: hasKey ? 1 : 0.55 }}>
<td style={{ padding: '0.65rem 0.75rem' }}>
{hasKey ? (
<span title="Aktiv — API-nyckel konfigurerad" style={{ fontSize: '1.1rem' }}></span>
) : (
<span title="Inaktiv — MISTRAL_API_KEY saknas" style={{ fontSize: '1.1rem' }}>🔴</span>
)}
</td>
<div style={{ fontWeight: 500, marginBottom: '0.2rem' }}>{fn.name}</div>
<div style={{ color: '#6b7280', fontSize: '0.8rem', lineHeight: 1.4 }}>{fn.description}</div>
</td>
<td style={{ padding: '0.65rem 0.75rem' }}>{modelChip(fn.model)}</td>
<td style={{ padding: '0.65rem 0.75rem' }}>{accessChip(fn.access)}</td>
<td style={{ padding: '0.65rem 0.75rem', color: '#4b5563', fontSize: '0.82rem' }}>{fn.trigger}</td>
<td style={{ padding: '0.65rem 0.75rem' }}>
<a href={fn.path} style={{ color: '#0070f3', textDecoration: 'none', fontFamily: 'monospace', fontSize: '0.82rem' }}>{fn.path}</a>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div style={{ marginTop: '1rem', display: 'flex', gap: '1rem', flexWrap: 'wrap' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.4rem', fontSize: '0.8rem', color: '#6b7280' }}>
<span style={{ background: '#6366f1', color: '#fff', borderRadius: '4px', padding: '1px 6px', fontFamily: 'monospace', fontSize: '0.75rem' }}>tiny</span>
Snabb och kostnadseffektiv text- och bildtolkning
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.4rem', fontSize: '0.8rem', color: '#6b7280' }}>
<span style={{ background: '#0ea5e9', color: '#fff', borderRadius: '4px', padding: '1px 6px', fontFamily: 'monospace', fontSize: '0.75rem' }}>small</span>
Bättre resoneringsförmåga kategorisering och matchning
</div>
</div>
</section>
</div>
);
}
function computeExpiry(createdAt: string, validityMonths: string): { daysLeft: number | null; expiryDate: string | null } {
if (!createdAt || !validityMonths) return { daysLeft: null, expiryDate: null };
const months = parseInt(validityMonths, 10);
if (isNaN(months) || months <= 0) return { daysLeft: null, expiryDate: null };
const created = new Date(createdAt);
if (isNaN(created.getTime())) return { daysLeft: null, expiryDate: null };
const expiry = new Date(created);
expiry.setMonth(expiry.getMonth() + months);
const today = new Date();
today.setHours(0, 0, 0, 0);
expiry.setHours(0, 0, 0, 0);
const daysLeft = Math.round((expiry.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
const expiryDate = expiry.toLocaleDateString('sv-SE');
return { daysLeft, expiryDate };
}