AI:n drivs av plattformen — flytta Claude-anropet till backend

- Ny edge-funktion supabase/functions/felsokning-ai: äger Claude
  API-nyckeln (serverhemligheten ANTHROPIC_API_KEY), systemprompten,
  modellvalet (claude-opus-5) och svarsschemat. Kräver inloggad
  användare (verify_jwt = true), validerar promptlängd och hanterar
  refusal/fallback på serversidan.
- Klienten anropar plattformens endpoint via functions.invoke —
  @anthropic-ai/sdk borttagen ur klientberoendena, ingen nyckel
  hanteras eller lagras någonsin i webbläsaren.
- Nyckelpanelen på startsidan ersatt med statuspanel: AI-handledning
  ingår i tjänsten; i lokalt läge (ej inloggad) guidar den
  deterministiska metodiken ensam, utan felbrus.
- Master Prompt: onboarding utan API-nyckelsteg; nytt stycke om att
  AI:n drivs av plattformen och att AI-reglerna inte kan kringgås
  från klientsidan.
- Test låser att endpointen äger systemprompt, modell och schema.

Verifierat: 25 vitest-tester gröna, produktionsbygge ok, båda
Playwright-sviterna gröna.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
This commit is contained in:
Claude
2026-08-03 01:08:19 +00:00
parent 82b469b7e6
commit ed483774fb
11 changed files with 170 additions and 199 deletions
+9 -8
View File
@@ -185,18 +185,19 @@ function GuideFlik({
}) {
const steg = useMemo(() => nastaSteg(arende, metodik), [arende, metodik]);
const [visaOverlamning, setVisaOverlamning] = useState(false);
const aiNyckel = useFelsokning((s) => s.aiNyckel);
const [aiStatus, setAiStatus] = useState<"vilar" | "arbetar" | "fel">("vilar");
// AI:n svarar skriftligt på varje bekräftad dokumentation: klassificerat
// (observation/verifierat/hypotes/rekommendation) och loggat som händelse.
// Utan nyckel guidar den deterministiska metodiken ensam.
// AI:n drivs av plattformen och svarar skriftligt på varje bekräftad
// dokumentation: klassificerat (observation/verifierat/hypotes/
// rekommendation) och loggat som händelse. I lokalt läge (ej inloggad)
// guidar den deterministiska metodiken ensam.
const fragaAiOmDokumentation = async (inmatning: string) => {
if (!aiNyckel) return;
setAiStatus("arbetar");
try {
const svar = await fragaAi(aiNyckel, brief(arende, metodik, nu), metodik.namn, inmatning);
skicka({ typ: "ai_svar", rader: svar.rader, nastaSteg: svar.nastaSteg, modell: AI_MODELL });
const svar = await fragaAi(brief(arende, metodik, nu), metodik.namn, inmatning);
if (svar) {
skicka({ typ: "ai_svar", rader: svar.rader, nastaSteg: svar.nastaSteg, modell: AI_MODELL });
}
setAiStatus("vilar");
} catch {
setAiStatus("fel");
@@ -245,7 +246,7 @@ function GuideFlik({
>
{aiStatus === "arbetar" && <p className="mb-2 animate-pulse font-bold text-amber-400">AI analyserar </p>}
{aiStatus === "fel" && (
<p className="mb-2 font-bold text-red-400">AI-svaret kunde inte hämtas kontrollera nyckel och nät. Metodiken fortsätter som vanligt.</p>
<p className="mb-2 font-bold text-red-400">AI-svaret kunde inte hämtas försök igen. Metodiken fortsätter som vanligt.</p>
)}
{senasteAiSvar && senasteAiSvar.handelse.typ === "ai_svar" && (
<>
+6 -35
View File
@@ -12,11 +12,9 @@ type Filter = "alla" | "pagaende" | "klara";
// Dashboard enligt direktivet: endast det viktigaste — mina ärenden,
// pågående, klara och starta nytt ärende.
export default function Arendelista() {
const { anvandare, arenden, sattAnvandare, laggInArende, nastaNummer, aiNyckel, sattAiNyckel } = useFelsokning();
const { anvandare, arenden, sattAnvandare, laggInArende, nastaNummer } = useFelsokning();
const [namn, setNamn] = useState("");
const [filter, setFilter] = useState<Filter>("alla");
const [visaAi, setVisaAi] = useState(false);
const [nyckel, setNyckel] = useState("");
if (!anvandare) {
return (
@@ -94,39 +92,12 @@ export default function Arendelista() {
)}
<Panel rubrik="AI-handledning">
<p className="mb-2 text-zinc-300">
{aiNyckel
? `Aktiv — Claude (${AI_MODELL}) svarar klassificerat på dokumenterade observationer och mätvärden.`
: "Inte aktiverad — den deterministiska metodiken guidar. Lägg till organisationens Claude API-nyckel för AI-handledning."}
<p className="text-zinc-300">
Ingår i tjänsten och drivs av plattformen Claude ({AI_MODELL}) svarar klassificerat
dokumenterade observationer och mätvärden. Anropen går via plattformens backend; inga AI-nycklar
hanteras av verkstaden. Aktiveras automatiskt för inloggade användare; i lokalt läge guidar den
deterministiska metodiken ensam.
</p>
{!visaAi ? (
<StorKnapp variant="sekundar" onClick={() => setVisaAi(true)}>
{aiNyckel ? "Ändra API-nyckel" : "Lägg till Claude API-nyckel"}
</StorKnapp>
) : (
<>
<TextFalt label="Claude API-nyckel" varde={nyckel} satt={setNyckel} platshallare="sk-ant-…" losenord />
<p className="mb-3 text-sm text-zinc-500">
MVP: nyckeln sparas endast den här enheten och anropen görs direkt från webbläsaren. I
produktionsversionen hanteras nycklar av plattformens backend.
</p>
<div className="grid grid-cols-2 gap-2">
<StorKnapp variant="sekundar" onClick={() => { setVisaAi(false); setNyckel(""); }}>
Avbryt
</StorKnapp>
<StorKnapp
disabled={!nyckel.trim() && !aiNyckel}
onClick={() => {
sattAiNyckel(nyckel);
setVisaAi(false);
setNyckel("");
}}
>
{nyckel.trim() ? "Spara nyckel" : "Ta bort nyckel"}
</StorKnapp>
</div>
</>
)}
</Panel>
{lista.map((arende) => {