Files
alva/src/felsokning/store.ts
T
Claude 8b79e3dfd5 AI-orkester: flera Claude-modeller routade per uppgift
Plattformens AI-endpoint är nu en modellorkester — servern äger hela
routingtabellen (modell, effort, systemprompt, schema per uppgift), så
den justeras utan klientändringar:

- handledning    → Claude Sonnet 5 (medium): realtidssvar på varje
                   bekräftad dokumentation — latenskänsligt på golvet.
- granskning     → Claude Opus 5 (hög effort): motsägelser, luckor och
                   förhastade slutsatser i hela arbetsloggen; ny knapp
                   'AI-granska underlaget' i briefen.
- sammanfattning → Claude Sonnet 5 (låg effort): AI-komplettering av
                   överlämningen med risker & osäkerheter; ny knapp i
                   överlämningsdialogen.
- metodikval     → Claude Haiku 4.5: klassificerar felbeskrivningen
                   när nyckelordsvalet inte träffar en specifik metodik;
                   valet sparas på ärendet (nytt fält metodikId).

Alla uppgifter delar AI-reglerna och det klassificerade svarsschemat;
modellen som svarade loggas i varje ai_svar-händelse och visas i UI:t.
Felbeskrivningsfältet fick även röstinmatning.

Verifierat: 26 vitest-tester gröna (inkl. att endpointen routar till
tre distinkta modeller och att granskningsprompten innehåller hela
loggen), 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
2026-08-03 01:23:04 +00:00

107 lines
3.5 KiB
TypeScript

// Tillstånd för Guidad Felsökning (MVP).
// Persistens: localStorage. I produktionsversionen ersätts detta av ett
// API-first-backend med samma händelsemodell — append-only-loggen gör
// synkronisering (inkl. offline-läge) konfliktfri.
import { create } from "zustand";
import { persist } from "zustand/middleware";
import type { Arende, Handelse, LoggPost, Objekt } from "./domain";
import { nyLoggPost } from "./domain";
import { felbeskrivning } from "./projektioner";
import { GENERISK_METODIK, metodikForId, valjMetodik, type Metodik } from "./metodik";
interface FelsokningState {
anvandare: string;
arenden: Record<string, Arende>;
nastaNummer: number;
sattAnvandare: (namn: string) => void;
skapaArende: (objekt: Objekt, felbeskrivningText: string, metodikId?: string) => string;
laggTill: (arendeId: string, handelse: Handelse) => void;
// Används endast av synken: ersätter listan med den ihopflätade versionen.
// Semantiken är fortfarande append-only — flätningen lägger bara till.
sammanfoga: (arendeId: string, handelser: LoggPost[]) => void;
// Lägger in ett färdigbyggt ärende (demoärende, importerat ärende).
laggInArende: (arende: Arende) => void;
}
function nyDelningskod(): string {
return Array.from(crypto.getRandomValues(new Uint8Array(12)), (b) =>
"abcdefghijklmnopqrstuvwxyz0123456789"[b % 36],
).join("");
}
export const useFelsokning = create<FelsokningState>()(
persist(
(set, get) => ({
anvandare: "",
arenden: {},
nastaNummer: 1,
sattAnvandare: (namn) => set({ anvandare: namn.trim() }),
skapaArende: (objekt, felbeskrivningText, metodikId) => {
const { anvandare, nastaNummer } = get();
const id = `arende-${Date.now().toString(36)}`;
const arende: Arende = {
id,
nummer: nastaNummer,
skapad: new Date().toISOString(),
delningskod: nyDelningskod(),
metodikId,
handelser: [
nyLoggPost(anvandare, { typ: "objekt_identifierat", objekt }),
nyLoggPost(anvandare, { typ: "felbeskrivning", text: felbeskrivningText }),
],
};
set((s) => ({
arenden: { ...s.arenden, [id]: arende },
nastaNummer: s.nastaNummer + 1,
}));
return id;
},
laggTill: (arendeId, handelse) => {
const { anvandare } = get();
set((s) => {
const arende = s.arenden[arendeId];
if (!arende) return s;
return {
arenden: {
...s.arenden,
[arendeId]: {
...arende,
handelser: [...arende.handelser, nyLoggPost(anvandare, handelse)],
},
},
};
});
},
sammanfoga: (arendeId, handelser) => {
set((s) => {
const arende = s.arenden[arendeId];
if (!arende || handelser.length === arende.handelser.length) return s;
return {
arenden: { ...s.arenden, [arendeId]: { ...arende, handelser } },
};
});
},
laggInArende: (arende) => {
set((s) => ({
arenden: { ...s.arenden, [arende.id]: arende },
nastaNummer: Math.max(s.nastaNummer, arende.nummer + 1),
}));
},
}),
{ name: "guidad-felsokning" },
),
);
export function metodikForArende(arende: Arende | undefined): Metodik {
if (!arende) return GENERISK_METODIK;
if (arende.metodikId) return metodikForId(arende.metodikId);
const text = felbeskrivning(arende);
return text ? valjMetodik(text) : GENERISK_METODIK;
}