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:
@@ -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" && (
|
||||
<>
|
||||
|
||||
@@ -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 på
|
||||
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 på 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) => {
|
||||
|
||||
Reference in New Issue
Block a user